diff --git a/netbox/templates/virtualization/inc/vminterface.html b/netbox/templates/virtualization/inc/vminterface.html new file mode 100644 index 000000000..ac6d98672 --- /dev/null +++ b/netbox/templates/virtualization/inc/vminterface.html @@ -0,0 +1,65 @@ + + {% if selectable and perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %} + + + + {% endif %} + + {{ iface.name }} + {% if iface.description %} + + {% endif %} + + {{ iface.mtu|default:"" }} + {{ iface.mac_address|default:"" }} + + {% if perms.virtualization.change_vminterface %} + + + + {% endif %} + {% if perms.virtualization.delete_vminterface %} + + + + {% endif %} + + +{% for ip in iface.ip_addresses.all %} + + {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} + + {% endif %} + + {{ ip }} + {% if ip.description %} + + {% endif %} + {% if device.primary_ip4 == ip or device.primary_ip6 == ip %} + Primary + {% endif %} + + + {% if ip.vrf %} + {{ ip.vrf }} + {% else %} + Global + {% endif %} + + + {{ ip.get_status_display }} + + + {% if perms.ipam.change_ipaddress %} + + + + {% endif %} + {% if perms.ipam.delete_ipaddress %} + + + + {% endif %} + + +{% endfor %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 2bfa225c5..ecd857f21 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -41,7 +41,7 @@

{% block title %}{{ vm }}{% endblock %}

{% include 'inc/created_updated.html' with obj=vm %}
-
+
Virtual Machine @@ -132,7 +132,66 @@
-
+
+ {% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %} +
+ {% csrf_token %} + + {% endif %} +
+
+ Interfaces +
+ + {% if perms.virtualization.change_vminterface and interfaces|length > 1 %} + + {% endif %} + {% if perms.virtualization.add_vminterface and interfaces|length > 10 %} + + Add interfaces + + {% endif %} +
+
+ + {% for iface in interfaces %} + {% include 'virtualization/inc/vminterface.html' with selectable=True %} + {% empty %} + + + + {% endfor %} +
No interfaces defined
+ {% if perms.virtualization.add_vminterface or perms.virtualization.delete_vminterface %} + + {% endif %} +
+ {% if perms.virtualization.delete_vminterface %} +
+ {% endif %}
{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine_component_add.html b/netbox/templates/virtualization/virtualmachine_component_add.html new file mode 100644 index 000000000..7cac56705 --- /dev/null +++ b/netbox/templates/virtualization/virtualmachine_component_add.html @@ -0,0 +1,44 @@ +{% extends '_base.html' %} +{% load helpers %} +{% load form_helpers %} + +{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %} + +{% block content %} +
+ {% csrf_token %} +
+
+ {% if form.non_field_errors %} +
+
Errors
+
+ {{ form.non_field_errors }} +
+
+ {% endif %} +
+
+ {{ component_type|bettertitle }} +
+
+
+ +
+

{{ parent }}

+
+
+ {% render_form form %} +
+
+
+
+ + + Cancel +
+
+
+
+
+{% endblock %} diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 7dbf7dcb3..cb2288416 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -3,11 +3,15 @@ from __future__ import unicode_literals from django import forms from django.db.models import Count +from dcim.formfields import MACAddressFormField from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from tenancy.forms import TenancyForm from tenancy.models import Tenant -from utilities.forms import APISelect, BootstrapMixin, ChainedModelChoiceField, FilterChoiceField, SlugField -from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine +from utilities.forms import ( + APISelect, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedModelChoiceField, ComponentForm, + ExpandableNameField, FilterChoiceField, SlugField, +) +from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface # @@ -157,3 +161,44 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')), label='Cluster' ) + + +# +# VM interfaces +# + +class VMInterfaceForm(BootstrapMixin, forms.ModelForm): + + class Meta: + model = VMInterface + fields = ['virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description'] + widgets = { + 'virtual_machine': forms.HiddenInput(), + } + + +class VMInterfaceCreateForm(ComponentForm): + name_pattern = ExpandableNameField(label='Name') + enabled = forms.BooleanField(required=False) + mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU') + mac_address = MACAddressFormField(required=False, label='MAC Address') + description = forms.CharField(max_length=100, required=False) + + def __init__(self, *args, **kwargs): + + # Set interfaces enabled by default + kwargs['initial'] = kwargs.get('initial', {}).copy() + kwargs['initial'].update({'enabled': True}) + + super(VMInterfaceCreateForm, self).__init__(*args, **kwargs) + + +class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField(queryset=VMInterface.objects.all(), widget=forms.MultipleHiddenInput) + virtual_machine = forms.ModelChoiceField(queryset=VirtualMachine.objects.all(), widget=forms.HiddenInput) + enabled = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect) + mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU') + description = forms.CharField(max_length=100, required=False) + + class Meta: + nullable_fields = ['mtu', 'description'] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 7cdd46158..ce5f266d8 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -225,6 +225,7 @@ class VMInterface(models.Model): class Meta: ordering = ['virtual_machine', 'name'] unique_together = ['virtual_machine', 'name'] + verbose_name = 'VM interface' def __str__(self): return self.name diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 3794a59b5..97261f3ae 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -83,3 +83,14 @@ class VirtualMachineTable(BaseTable): class Meta(BaseTable.Meta): model = VirtualMachine fields = ('pk', 'name', 'cluster', 'tenant', 'vcpus', 'memory', 'disk') + + +# +# VM components +# + +class VMInterfaceTable(BaseTable): + + class Meta(BaseTable.Meta): + model = VMInterface + fields = ('name', 'enabled', 'description') diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index 81038e3d4..8542919a4 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -38,4 +38,12 @@ urlpatterns = [ url(r'^virtual-machines/(?P\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), url(r'^virtual-machines/(?P\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), + # VM interfaces + # url(r'^virtual-machines/interfaces/add/$', views.VMBulkAddVMInterfaceView.as_view(), name='vm_bulk_add_vminterface'), + url(r'^virtual-machines/(?P\d+)/interfaces/add/$', views.VMInterfaceCreateView.as_view(), name='vminterface_add'), + url(r'^virtual-machines/(?P\d+)/interfaces/edit/$', views.VMInterfaceBulkEditView.as_view(), name='vminterface_bulk_edit'), + url(r'^virtual-machines/(?P\d+)/interfaces/delete/$', views.VMInterfaceBulkDeleteView.as_view(), name='vminterface_bulk_delete'), + url(r'^vm-interfaces/(?P\d+)/edit/$', views.VMInterfaceEditView.as_view(), name='vminterface_edit'), + url(r'^vm-interfaces/(?P\d+)/delete/$', views.VMInterfaceDeleteView.as_view(), name='vminterface_delete'), + ] diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 8d6388445..a46a55cae 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -155,9 +155,11 @@ class VirtualMachineView(View): def get(self, request, pk): vm = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk) + interfaces = VMInterface.objects.filter(virtual_machine=vm) return render(request, 'virtualization/virtualmachine.html', { 'vm': vm, + 'interfaces': interfaces, }) @@ -200,36 +202,43 @@ class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView): # VM interfaces # -# class VMInterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): -# permission_required = 'virtualization.add_vminterface' -# parent_model = VirtualMachine -# parent_field = 'vm' -# model = VMInterface -# form = forms.VMInterfaceCreateForm -# model_form = forms.VMInterfaceForm -# -# -# class VMInterfaceEditView(PermissionRequiredMixin, ComponentEditView): -# permission_required = 'virtualization.change_vminterface' -# model = VMInterface -# form_class = forms.VMInterfaceForm -# -# -# class VMInterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView): -# permission_required = 'virtualization.delete_vminterface' -# model = VMInterface -# -# -# class VMInterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): -# permission_required = 'virtualization.change_vminterface' -# cls = VMInterface -# parent_cls = VirtualMachine -# table = tables.VMInterfaceTable -# form = forms.VMInterfaceBulkEditForm -# -# -# class VMInterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): -# permission_required = 'virtualization.delete_vminterface' -# cls = VMInterface -# parent_cls = VirtualMachine -# table = tables.VMInterfaceTable +class VMInterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): + permission_required = 'virtualization.add_vminterface' + parent_model = VirtualMachine + parent_field = 'virtual_machine' + model = VMInterface + form = forms.VMInterfaceCreateForm + model_form = forms.VMInterfaceForm + template_name = 'virtualization/virtualmachine_component_add.html' + + +class VMInterfaceEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'virtualization.change_vminterface' + model = VMInterface + form_class = forms.VMInterfaceForm + + def get_return_url(self, request, obj): + return obj.virtual_machine.get_absolute_url() + + +class VMInterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'virtualization.delete_vminterface' + model = VMInterface + + def get_return_url(self, request, obj): + return obj.virtual_machine.get_absolute_url() + + +class VMInterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'virtualization.change_vminterface' + cls = VMInterface + parent_cls = VirtualMachine + table = tables.VMInterfaceTable + form = forms.VMInterfaceBulkEditForm + + +class VMInterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'virtualization.delete_vminterface' + cls = VMInterface + parent_cls = VirtualMachine + table = tables.VMInterfaceTable