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 @@
+
+
+ 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'