Added virtualization filters

This commit is contained in:
Jeremy Stretch 2017-08-16 17:00:17 -04:00
parent 4ef55502b4
commit 9acd792abe
10 changed files with 281 additions and 20 deletions

View File

@ -17,7 +17,7 @@
<input type="text" name="q" class="form-control" placeholder="Search virtual machines" /> <input type="text" name="q" class="form-control" placeholder="Search virtual machines" />
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span> <span class="fa fa-search"></span>
</button> </button>
</span> </span>
</div> </div>
@ -27,13 +27,13 @@
<div class="pull-right"> <div class="pull-right">
{% if perms.virtualization.change_virtualmachine %} {% if perms.virtualization.change_virtualmachine %}
<a href="{% url 'virtualization:virtualmachine_edit' pk=vm.pk %}" class="btn btn-warning"> <a href="{% url 'virtualization:virtualmachine_edit' pk=vm.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> <span class="fa fa-pencil"></span>
Edit this VM Edit this VM
</a> </a>
{% endif %} {% endif %}
{% if perms.virtualization.delete_virtualmachine %} {% if perms.virtualization.delete_virtualmachine %}
<a href="{% url 'virtualization:virtualmachine_delete' pk=vm.pk %}" class="btn btn-danger"> <a href="{% url 'virtualization:virtualmachine_delete' pk=vm.pk %}" class="btn btn-danger">
<span class="fa fa-trash" aria-hidden="true"></span> <span class="fa fa-trash"></span>
Delete this VM Delete this VM
</a> </a>
{% endif %} {% endif %}
@ -53,7 +53,68 @@
</tr> </tr>
<tr> <tr>
<td>Cluster</td> <td>Cluster</td>
<td><a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a></td> <td>
{% if vm.cluster.group %}
<a href="{{ vm.cluster.group.get_absolute_url }}">{{ vm.cluster.group }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a>
</td>
</tr>
<tr>
<td>Cluster Type</td>
<td>{{ vm.cluster.type }}</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if vm.tenant %}
{% if vm.tenant.group %}
<a href="{{ vm.tenant.group.get_absolute_url }}">{{ vm.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ vm.tenant.get_absolute_url }}">{{ vm.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
</table>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Resources</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Virtual CPUs</td>
<td>
{% if vm.vcpus %}
{{ vm.vcpus }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Memory</td>
<td>
{% if vm.memory %}
{{ vm.memory }} MB
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Disk Space</td>
<td>
{% if vm.disk %}
{{ vm.disk }} GB
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -6,13 +6,17 @@
<div class="panel-heading"><strong>Virtual Machine</strong></div> <div class="panel-heading"><strong>Virtual Machine</strong></div>
<div class="panel-body"> <div class="panel-body">
{% render_field form.name %} {% render_field form.name %}
{% render_field form.cluster_group %}
{% render_field form.cluster %}
{% render_field form.platform %} {% render_field form.platform %}
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Cluster</strong></div> <div class="panel-heading"><strong>Resources</strong></div>
<div class="panel-body"> <div class="panel-body">
{% render_field form.cluster %} {% render_field form.vcpus %}
{% render_field form.memory %}
{% render_field form.disk %}
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -12,7 +12,7 @@
Import virtual machines Import virtual machines
</a> </a>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='virtualmachines' %} {% include 'inc/export_button.html' with obj_type='virtual machines' %}
</div> </div>
<h1>{% block title %}Virtual Machines{% endblock %}</h1> <h1>{% block title %}Virtual Machines{% endblock %}</h1>
<div class="row"> <div class="row">
@ -24,3 +24,32 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
var cluster_group_list = $('#id_cluster_group');
var cluster_list = $('#id_cluster_id');
// Update cluster options based on selected group
cluster_group_list.change(function() {
var selected_groups = $(this).val();
if (selected_groups) {
cluster_list.empty();
$.ajax({
url: netbox_api_path + 'virtualization/clusters/?limit=500&group=' + selected_groups.join('&group='),
dataType: 'json',
success: function (response, status) {
$.each(response["results"], function (index, cluster) {
var option = $("<option></option>").attr("value", cluster.id).text(cluster.name);
cluster_list.append(option);
});
}
});
}
});
});
</script>
{% endblock %}

View File

@ -4,6 +4,7 @@ from rest_framework.viewsets import ModelViewSet
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from utilities.api import WritableSerializerMixin from utilities.api import WritableSerializerMixin
from virtualization import filters
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
from . import serializers from . import serializers
@ -26,6 +27,7 @@ class ClusterViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Cluster.objects.select_related('type', 'group') queryset = Cluster.objects.select_related('type', 'group')
serializer_class = serializers.ClusterSerializer serializer_class = serializers.ClusterSerializer
write_serializer_class = serializers.WritableClusterSerializer write_serializer_class = serializers.WritableClusterSerializer
filter_class = filters.ClusterFilter
# #
@ -36,6 +38,7 @@ class VirtualMachineViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = VirtualMachine.objects.all() queryset = VirtualMachine.objects.all()
serializer_class = serializers.VirtualMachineSerializer serializer_class = serializers.VirtualMachineSerializer
write_serializer_class = serializers.WritableVirtualMachineSerializer write_serializer_class = serializers.WritableVirtualMachineSerializer
filter_class = filters.VirtualMachineFilter
class VMInterfaceViewSet(WritableSerializerMixin, ModelViewSet): class VMInterfaceViewSet(WritableSerializerMixin, ModelViewSet):

View File

@ -0,0 +1,99 @@
from __future__ import unicode_literals
import django_filters
from django.db.models import Q
from dcim.models import Platform
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
class ClusterFilter(CustomFieldFilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
)
group_id = NullableModelMultipleChoiceFilter(
queryset=ClusterGroup.objects.all(),
label='Parent group (ID)',
)
group = NullableModelMultipleChoiceFilter(
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
label='Parent group (slug)',
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ClusterType.objects.all(),
label='Cluster type (ID)',
)
type = django_filters.ModelMultipleChoiceFilter(
name='type__slug',
queryset=ClusterType.objects.all(),
to_field_name='slug',
label='Cluster type (slug)',
)
class Meta:
model = Cluster
fields = ['name']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(comments__icontains=value)
)
class VirtualMachineFilter(CustomFieldFilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
)
cluster_group_id = NullableModelMultipleChoiceFilter(
name='cluster__group',
queryset=ClusterGroup.objects.all(),
label='Cluster group (ID)',
)
cluster_group = NullableModelMultipleChoiceFilter(
name='cluster__group__slug',
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
label='Cluster group (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
platform_id = NullableModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label='Platform (ID)',
)
platform = NullableModelMultipleChoiceFilter(
name='platform',
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
)
class Meta:
model = VirtualMachine
fields = ['name', 'cluster']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(comments__icontains=value)
)

View File

@ -1,11 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django import forms from django import forms
from django.db.models import Count
from extras.forms import CustomFieldBulkEditForm, CustomFieldForm from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import BootstrapMixin, SlugField from utilities.forms import APISelect, BootstrapMixin, ChainedModelChoiceField, FilterChoiceField, SlugField
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -67,15 +68,58 @@ class ClusterCSVForm(forms.ModelForm):
fields = ['name', 'type', 'group'] fields = ['name', 'type', 'group']
class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Cluster
q = forms.CharField(required=False, label='Search')
group = FilterChoiceField(
queryset=ClusterGroup.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug',
null_option=(0, 'None'),
required=False,
)
type = FilterChoiceField(
queryset=ClusterType.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug',
required=False,
)
# #
# Virtual Machines # Virtual Machines
# #
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
cluster_group = forms.ModelChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'cluster', 'nullable': 'true'}
)
)
cluster = ChainedModelChoiceField(
queryset=Cluster.objects.all(),
chains=(
('group', 'cluster_group'),
),
widget=APISelect(
api_url='/api/virtualization/clusters/?group_id={{cluster_group}}'
)
)
class Meta: class Meta:
model = VirtualMachine model = VirtualMachine
fields = ['name', 'cluster', 'tenant', 'platform', 'comments'] fields = ['name', 'cluster_group', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments']
def __init__(self, *args, **kwargs):
# Initialize helper selector
instance = kwargs.get('instance')
if instance.pk and instance.cluster is not None:
initial = kwargs.get('initial', {}).copy()
initial['cluster_group'] = instance.cluster.group
kwargs['initial'] = initial
super(VirtualMachineForm, self).__init__(*args, **kwargs)
class VirtualMachineCSVForm(forms.ModelForm): class VirtualMachineCSVForm(forms.ModelForm):
@ -89,7 +133,7 @@ class VirtualMachineCSVForm(forms.ModelForm):
) )
class Meta: class Meta:
fields = ['cluster', 'name', 'tenant'] fields = ['cluster', 'name', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments']
class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -99,3 +143,17 @@ class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class Meta: class Meta:
nullable_fields = ['tenant'] nullable_fields = ['tenant']
class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VirtualMachine
q = forms.CharField(required=False, label='Search')
cluster_group = FilterChoiceField(
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
null_option=(0, 'None'),
)
cluster_id = FilterChoiceField(
queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')),
label='Cluster'
)

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-16 19:22 # Generated by Django 1.11.4 on 2017-08-16 19:27
from __future__ import unicode_literals from __future__ import unicode_literals
import dcim.fields import dcim.fields
@ -13,9 +13,9 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('dcim', '0041_napalm_integration'),
('ipam', '0018_remove_service_uniqueness_constraint'), ('ipam', '0018_remove_service_uniqueness_constraint'),
('tenancy', '0003_unicode_literals'), ('tenancy', '0003_unicode_literals'),
('dcim', '0041_napalm_integration'),
] ]
operations = [ operations = [
@ -63,9 +63,9 @@ class Migration(migrations.Migration):
('created', models.DateField(auto_now_add=True)), ('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)), ('last_updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=64, unique=True)), ('name', models.CharField(max_length=64, unique=True)),
('vcpus', models.PositiveSmallIntegerField(blank=True, verbose_name='vCPUs')), ('vcpus', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='vCPUs')),
('memory', models.PositiveIntegerField(blank=True, verbose_name='Memory (MB)')), ('memory', models.PositiveIntegerField(blank=True, null=True, verbose_name='Memory (MB)')),
('disk', models.PositiveIntegerField(blank=True, verbose_name='Disk (GB)')), ('disk', models.PositiveIntegerField(blank=True, null=True, verbose_name='Disk (GB)')),
('comments', models.TextField(blank=True)), ('comments', models.TextField(blank=True)),
('cluster', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='virtualization.Cluster')), ('cluster', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='virtualization.Cluster')),
('platform', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='virtual_machines', to='dcim.Platform')), ('platform', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='virtual_machines', to='dcim.Platform')),

View File

@ -161,14 +161,17 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
) )
vcpus = models.PositiveSmallIntegerField( vcpus = models.PositiveSmallIntegerField(
blank=True, blank=True,
null=True,
verbose_name='vCPUs' verbose_name='vCPUs'
) )
memory = models.PositiveIntegerField( memory = models.PositiveIntegerField(
blank=True, blank=True,
null=True,
verbose_name='Memory (MB)' verbose_name='Memory (MB)'
) )
disk = models.PositiveIntegerField( disk = models.PositiveIntegerField(
blank=True, blank=True,
null=True,
verbose_name='Disk (GB)' verbose_name='Disk (GB)'
) )
comments = models.TextField( comments = models.TextField(

View File

@ -77,8 +77,9 @@ class ClusterTable(BaseTable):
class VirtualMachineTable(BaseTable): class VirtualMachineTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')])
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VirtualMachine model = VirtualMachine
fields = ('pk', 'name', 'tenant') fields = ('pk', 'name', 'cluster', 'tenant', 'vcpus', 'memory', 'disk')

View File

@ -11,6 +11,7 @@ from utilities.views import (
ObjectDeleteView, ObjectEditView, ObjectListView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
from . import filters
from . import forms from . import forms
from . import tables from . import tables
@ -84,6 +85,8 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class ClusterListView(ObjectListView): class ClusterListView(ObjectListView):
queryset = Cluster.objects.annotate(vm_count=Count('virtual_machines')) queryset = Cluster.objects.annotate(vm_count=Count('virtual_machines'))
table = tables.ClusterTable table = tables.ClusterTable
filter = filters.ClusterFilter
filter_form = forms.ClusterFilterForm
template_name = 'virtualization/cluster_list.html' template_name = 'virtualization/cluster_list.html'
@ -138,8 +141,8 @@ class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class VirtualMachineListView(ObjectListView): class VirtualMachineListView(ObjectListView):
queryset = VirtualMachine.objects.select_related('tenant') queryset = VirtualMachine.objects.select_related('tenant')
# filter = filters.VirtualMachineFilter filter = filters.VirtualMachineFilter
# filter_form = forms.VirtualMachineFilterForm filter_form = forms.VirtualMachineFilterForm
table = tables.VirtualMachineTable table = tables.VirtualMachineTable
template_name = 'virtualization/virtualmachine_list.html' template_name = 'virtualization/virtualmachine_list.html'
@ -184,7 +187,7 @@ class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'virtualization.change_virtualmachine' permission_required = 'virtualization.change_virtualmachine'
cls = VirtualMachine cls = VirtualMachine
queryset = VirtualMachine.objects.select_related('tenant') queryset = VirtualMachine.objects.select_related('tenant')
# filter = filters.VirtualMachineFilter filter = filters.VirtualMachineFilter
table = tables.VirtualMachineTable table = tables.VirtualMachineTable
form = forms.VirtualMachineBulkEditForm form = forms.VirtualMachineBulkEditForm
default_return_url = 'virtualization:virtualmachine_list' default_return_url = 'virtualization:virtualmachine_list'