From 9acd792abe042f6797300f05a2eef4e5964b21c3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 16 Aug 2017 17:00:17 -0400 Subject: [PATCH] Added virtualization filters --- .../virtualization/virtualmachine.html | 69 ++++++++++++- .../virtualization/virtualmachine_edit.html | 8 +- .../virtualization/virtualmachine_list.html | 31 +++++- netbox/virtualization/api/views.py | 3 + netbox/virtualization/filters.py | 99 +++++++++++++++++++ netbox/virtualization/forms.py | 66 ++++++++++++- .../virtualization/migrations/0001_initial.py | 10 +- netbox/virtualization/models.py | 3 + netbox/virtualization/tables.py | 3 +- netbox/virtualization/views.py | 9 +- 10 files changed, 281 insertions(+), 20 deletions(-) create mode 100644 netbox/virtualization/filters.py diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 66427321c..2bfa225c5 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -17,7 +17,7 @@ @@ -27,13 +27,13 @@
{% if perms.virtualization.change_virtualmachine %} - + Edit this VM {% endif %} {% if perms.virtualization.delete_virtualmachine %} - + Delete this VM {% endif %} @@ -53,7 +53,68 @@ Cluster - {{ vm.cluster }} + + {% if vm.cluster.group %} + {{ vm.cluster.group }} + + {% endif %} + {{ vm.cluster }} + + + + Cluster Type + {{ vm.cluster.type }} + + + Tenant + + {% if vm.tenant %} + {% if vm.tenant.group %} + {{ vm.tenant.group.name }} + + {% endif %} + {{ vm.tenant }} + {% else %} + None + {% endif %} + + + +
+
+
+ Resources +
+ + + + + + + + + + + +
Virtual CPUs + {% if vm.vcpus %} + {{ vm.vcpus }} + {% else %} + N/A + {% endif %} +
Memory + {% if vm.memory %} + {{ vm.memory }} MB + {% else %} + N/A + {% endif %} +
Disk Space + {% if vm.disk %} + {{ vm.disk }} GB + {% else %} + N/A + {% endif %} +
diff --git a/netbox/templates/virtualization/virtualmachine_edit.html b/netbox/templates/virtualization/virtualmachine_edit.html index 41b085579..14acd13db 100644 --- a/netbox/templates/virtualization/virtualmachine_edit.html +++ b/netbox/templates/virtualization/virtualmachine_edit.html @@ -6,13 +6,17 @@
Virtual Machine
{% render_field form.name %} + {% render_field form.cluster_group %} + {% render_field form.cluster %} {% render_field form.platform %}
-
Cluster
+
Resources
- {% render_field form.cluster %} + {% render_field form.vcpus %} + {% render_field form.memory %} + {% render_field form.disk %}
diff --git a/netbox/templates/virtualization/virtualmachine_list.html b/netbox/templates/virtualization/virtualmachine_list.html index ae9959e3d..5cd601be5 100644 --- a/netbox/templates/virtualization/virtualmachine_list.html +++ b/netbox/templates/virtualization/virtualmachine_list.html @@ -12,7 +12,7 @@ Import virtual machines {% endif %} - {% include 'inc/export_button.html' with obj_type='virtualmachines' %} + {% include 'inc/export_button.html' with obj_type='virtual machines' %}

{% block title %}Virtual Machines{% endblock %}

@@ -24,3 +24,32 @@
{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 79fd73ca8..4c1d065d6 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -4,6 +4,7 @@ from rest_framework.viewsets import ModelViewSet from extras.api.views import CustomFieldModelViewSet from utilities.api import WritableSerializerMixin +from virtualization import filters from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from . import serializers @@ -26,6 +27,7 @@ class ClusterViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Cluster.objects.select_related('type', 'group') serializer_class = serializers.ClusterSerializer write_serializer_class = serializers.WritableClusterSerializer + filter_class = filters.ClusterFilter # @@ -36,6 +38,7 @@ class VirtualMachineViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = VirtualMachine.objects.all() serializer_class = serializers.VirtualMachineSerializer write_serializer_class = serializers.WritableVirtualMachineSerializer + filter_class = filters.VirtualMachineFilter class VMInterfaceViewSet(WritableSerializerMixin, ModelViewSet): diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py new file mode 100644 index 000000000..4efcd06e2 --- /dev/null +++ b/netbox/virtualization/filters.py @@ -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) + ) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 8176ce676..7dbf7dcb3 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals 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.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 @@ -67,15 +68,58 @@ class ClusterCSVForm(forms.ModelForm): 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 # 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: 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): @@ -89,7 +133,7 @@ class VirtualMachineCSVForm(forms.ModelForm): ) class Meta: - fields = ['cluster', 'name', 'tenant'] + fields = ['cluster', 'name', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments'] class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): @@ -99,3 +143,17 @@ class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class Meta: 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' + ) diff --git a/netbox/virtualization/migrations/0001_initial.py b/netbox/virtualization/migrations/0001_initial.py index ddf7c897d..1f812d209 100644 --- a/netbox/virtualization/migrations/0001_initial.py +++ b/netbox/virtualization/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- 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 import dcim.fields @@ -13,9 +13,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('dcim', '0041_napalm_integration'), ('ipam', '0018_remove_service_uniqueness_constraint'), ('tenancy', '0003_unicode_literals'), + ('dcim', '0041_napalm_integration'), ] operations = [ @@ -63,9 +63,9 @@ class Migration(migrations.Migration): ('created', models.DateField(auto_now_add=True)), ('last_updated', models.DateTimeField(auto_now=True)), ('name', models.CharField(max_length=64, unique=True)), - ('vcpus', models.PositiveSmallIntegerField(blank=True, verbose_name='vCPUs')), - ('memory', models.PositiveIntegerField(blank=True, verbose_name='Memory (MB)')), - ('disk', models.PositiveIntegerField(blank=True, verbose_name='Disk (GB)')), + ('vcpus', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='vCPUs')), + ('memory', models.PositiveIntegerField(blank=True, null=True, verbose_name='Memory (MB)')), + ('disk', models.PositiveIntegerField(blank=True, null=True, verbose_name='Disk (GB)')), ('comments', models.TextField(blank=True)), ('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')), diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 0cd6ac791..042bdf904 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -161,14 +161,17 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel): ) vcpus = models.PositiveSmallIntegerField( blank=True, + null=True, verbose_name='vCPUs' ) memory = models.PositiveIntegerField( blank=True, + null=True, verbose_name='Memory (MB)' ) disk = models.PositiveIntegerField( blank=True, + null=True, verbose_name='Disk (GB)' ) comments = models.TextField( diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index a5bbad9a2..3794a59b5 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -77,8 +77,9 @@ class ClusterTable(BaseTable): class VirtualMachineTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() + cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) class Meta(BaseTable.Meta): model = VirtualMachine - fields = ('pk', 'name', 'tenant') + fields = ('pk', 'name', 'cluster', 'tenant', 'vcpus', 'memory', 'disk') diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 1122cc28e..4be2d2563 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -11,6 +11,7 @@ from utilities.views import ( ObjectDeleteView, ObjectEditView, ObjectListView, ) from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface +from . import filters from . import forms from . import tables @@ -84,6 +85,8 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ClusterListView(ObjectListView): queryset = Cluster.objects.annotate(vm_count=Count('virtual_machines')) table = tables.ClusterTable + filter = filters.ClusterFilter + filter_form = forms.ClusterFilterForm template_name = 'virtualization/cluster_list.html' @@ -138,8 +141,8 @@ class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VirtualMachineListView(ObjectListView): queryset = VirtualMachine.objects.select_related('tenant') - # filter = filters.VirtualMachineFilter - # filter_form = forms.VirtualMachineFilterForm + filter = filters.VirtualMachineFilter + filter_form = forms.VirtualMachineFilterForm table = tables.VirtualMachineTable template_name = 'virtualization/virtualmachine_list.html' @@ -184,7 +187,7 @@ class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'virtualization.change_virtualmachine' cls = VirtualMachine queryset = VirtualMachine.objects.select_related('tenant') - # filter = filters.VirtualMachineFilter + filter = filters.VirtualMachineFilter table = tables.VirtualMachineTable form = forms.VirtualMachineBulkEditForm default_return_url = 'virtualization:virtualmachine_list'