Browse Source

Ditched VMInterface in favor of reusing dcim.Interface

Jeremy Stretch 7 years ago
parent
commit
e9f75adddb

+ 6 - 4
netbox/dcim/constants.py

@@ -92,13 +92,15 @@ IFACE_FF_JUNIPER_VCP = 5200
 # Other
 # Other
 IFACE_FF_OTHER = 32767
 IFACE_FF_OTHER = 32767
 
 
+VIFACE_FF_CHOICES = [
+    [IFACE_FF_VIRTUAL, 'Virtual'],
+    [IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
+]
+
 IFACE_FF_CHOICES = [
 IFACE_FF_CHOICES = [
     [
     [
         'Virtual interfaces',
         'Virtual interfaces',
-        [
-            [IFACE_FF_VIRTUAL, 'Virtual'],
-            [IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
-        ]
+        VIFACE_FF_CHOICES,
     ],
     ],
     [
     [
         'Ethernet (fixed)',
         'Ethernet (fixed)',

+ 0 - 22
netbox/dcim/migrations/0042_device_cluster.py

@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.4 on 2017-08-18 19:46
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('virtualization', '0001_initial'),
-        ('dcim', '0041_napalm_integration'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='device',
-            name='cluster',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
-        ),
-    ]

+ 32 - 0
netbox/dcim/migrations/0042_virtualization.py

@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2017-08-29 17:49
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('virtualization', '0001_virtualization'),
+        ('dcim', '0041_napalm_integration'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='device',
+            name='cluster',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='virtual_machine',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'),
+        ),
+        migrations.AlterField(
+            model_name='interface',
+            name='device',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
+        ),
+    ]

+ 30 - 10
netbox/dcim/models.py

@@ -1152,13 +1152,26 @@ class PowerOutlet(models.Model):
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class Interface(models.Model):
 class Interface(models.Model):
     """
     """
-    A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
-    of an InterfaceConnection.
+    A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
+    Interface via the creation of an InterfaceConnection.
     """
     """
-    device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
+    device = models.ForeignKey(
+        to='Device',
+        on_delete=models.CASCADE,
+        related_name='interfaces',
+        null=True,
+        blank=True
+    )
+    virtual_machine = models.ForeignKey(
+        to='virtualization.VirtualMachine',
+        on_delete=models.CASCADE,
+        related_name='interfaces',
+        null=True,
+        blank=True
+    )
     lag = models.ForeignKey(
     lag = models.ForeignKey(
-        'self',
-        models.SET_NULL,
+        to='self',
+        on_delete=models.SET_NULL,
         related_name='member_interfaces',
         related_name='member_interfaces',
         null=True,
         null=True,
         blank=True,
         blank=True,
@@ -1175,11 +1188,6 @@ class Interface(models.Model):
         help_text="This interface is used only for out-of-band management"
         help_text="This interface is used only for out-of-band management"
     )
     )
     description = models.CharField(max_length=100, blank=True)
     description = models.CharField(max_length=100, blank=True)
-    ip_addresses = GenericRelation(
-        to='ipam.IPAddress',
-        content_type_field='interface_type',
-        object_id_field='interface_id'
-    )
 
 
     objects = InterfaceQuerySet.as_manager()
     objects = InterfaceQuerySet.as_manager()
 
 
@@ -1192,6 +1200,18 @@ class Interface(models.Model):
 
 
     def clean(self):
     def clean(self):
 
 
+        # An Interface must belong to a Device *or* to a VirtualMachine
+        if self.device and self.virtual_machine:
+            raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
+        if not self.device and not self.virtual_machine:
+            raise ValidationError("An interface must belong to either a device or a virtual machine.")
+
+        # VM interfaces must be virtual
+        if self.virtual_machine and self.form_factor not in VIRTUAL_IFACE_TYPES:
+            raise ValidationError({
+                'form_factor': "Virtual machines cannot have physical interfaces."
+            })
+
         # Virtual interfaces cannot be connected
         # Virtual interfaces cannot be connected
         if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
         if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
             raise ValidationError({
             raise ValidationError({

+ 0 - 44
netbox/ipam/migrations/0019_ipaddress_interface_to_gfk.py

@@ -1,44 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.4 on 2017-08-18 19:31
-from __future__ import unicode_literals
-
-from django.contrib.contenttypes.models import ContentType
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-def set_interface_type(apps, schema_editor):
-    """
-    Set the interface_type field to 'Interface' for all IP addresses assigned to an Interface.
-    """
-    Interface = apps.get_model('dcim', 'Interface')
-    interface_type = ContentType.objects.get_for_model(Interface)
-    IPAddress = apps.get_model('ipam', 'IPAddress')
-    IPAddress.objects.filter(interface__isnull=False).update(interface_type=interface_type)
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('contenttypes', '0002_remove_content_type_name'),
-        ('ipam', '0018_remove_service_uniqueness_constraint'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='ipaddress',
-            name='interface_type',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType'),
-        ),
-        migrations.RunPython(set_interface_type),
-        migrations.AlterField(
-            model_name='IPAddress',
-            name='interface',
-            field=models.PositiveIntegerField(blank=True, null=True),
-        ),
-        migrations.RenameField(
-            model_name='IPAddress',
-            old_name='interface',
-            new_name='interface_id',
-        )
-    ]

+ 2 - 9
netbox/ipam/models.py

@@ -409,15 +409,8 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     role = models.PositiveSmallIntegerField(
     role = models.PositiveSmallIntegerField(
         'Role', choices=IPADDRESS_ROLE_CHOICES, blank=True, null=True, help_text='The functional role of this IP'
         'Role', choices=IPADDRESS_ROLE_CHOICES, blank=True, null=True, help_text='The functional role of this IP'
     )
     )
-    interface_type = models.ForeignKey(
-        to=ContentType,
-        on_delete=models.PROTECT,
-        limit_choices_to=Q(app_label='dcim', model='interface') | Q(app_label='virtualization', model='vminterface'),
-        blank=True,
-        null=True
-    )
-    interface_id = models.PositiveIntegerField(blank=True, null=True)
-    interface = GenericForeignKey('interface_type', 'interface_id')
+    interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
+                                  null=True)
     nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
     nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
                                       null=True, verbose_name='NAT (Inside)',
                                       null=True, verbose_name='NAT (Inside)',
                                       help_text="The IP for which this address is the \"outside\" IP")
                                       help_text="The IP for which this address is the \"outside\" IP")

+ 5 - 5
netbox/templates/virtualization/inc/vminterface.html

@@ -1,5 +1,5 @@
 <tr class="interface{% if not iface.enabled %} danger{% endif %}">
 <tr class="interface{% if not iface.enabled %} danger{% endif %}">
-    {% if selectable and perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
+    {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
         <td class="pk">
         <td class="pk">
             <input name="pk" type="checkbox" value="{{ iface.pk }}" />
             <input name="pk" type="checkbox" value="{{ iface.pk }}" />
         </td>
         </td>
@@ -13,13 +13,13 @@
     <td>{{ iface.mtu|default:"" }}</td>
     <td>{{ iface.mtu|default:"" }}</td>
     <td>{{ iface.mac_address|default:"" }}</td>
     <td>{{ iface.mac_address|default:"" }}</td>
     <td class="text-right">
     <td class="text-right">
-        {% if perms.virtualization.change_vminterface %}
-            <a href="{% url 'virtualization:vminterface_edit' pk=iface.pk %}" class="btn btn-info btn-xs" title="Edit interface">
+        {% if perms.dcim.change_interface %}
+            <a href="{% url 'virtualization:interface_edit' pk=iface.pk %}" class="btn btn-info btn-xs" title="Edit interface">
                 <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
                 <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
             </a>
             </a>
         {% endif %}
         {% endif %}
-        {% if perms.virtualization.delete_vminterface %}
-            <a href="{% url 'virtualization:vminterface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
+        {% if perms.dcim.delete_interface %}
+            <a href="{% url 'virtualization:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
                 <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
                 <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
             </a>
             </a>
         {% endif %}
         {% endif %}

+ 13 - 13
netbox/templates/virtualization/virtualmachine.html

@@ -133,7 +133,7 @@
         </div>
         </div>
     </div>
     </div>
     <div class="col-md-7">
     <div class="col-md-7">
-        {% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
+        {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
             <form method="post">
             <form method="post">
             {% csrf_token %}
             {% csrf_token %}
             <input type="hidden" name="virtual_machine" value="{{ vm.pk }}" />
             <input type="hidden" name="virtual_machine" value="{{ vm.pk }}" />
@@ -145,13 +145,13 @@
                     <button class="btn btn-default btn-xs toggle-ips" selected="selected">
                     <button class="btn btn-default btn-xs toggle-ips" selected="selected">
                         <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
                         <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
                     </button>
                     </button>
-                    {% if perms.virtualization.change_vminterface and interfaces|length > 1 %}
+                    {% if perms.dcim.change_interface and interfaces|length > 1 %}
                         <button class="btn btn-default btn-xs toggle">
                         <button class="btn btn-default btn-xs toggle">
                             <span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
                             <span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
                         </button>
                         </button>
                     {% endif %}
                     {% endif %}
-                    {% if perms.virtualization.add_vminterface and interfaces|length > 10 %}
-                        <a href="{% url 'virtualization:vminterface_add' pk=vm.pk %}" class="btn btn-primary btn-xs">
+                    {% if perms.dcim.add_interface and interfaces|length > 10 %}
+                        <a href="{% url 'virtualization:interface_add' pk=vm.pk %}" class="btn btn-primary btn-xs">
                             <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
                             <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
                         </a>
                         </a>
                     {% endif %}
                     {% endif %}
@@ -159,28 +159,28 @@
             </div>
             </div>
             <table id="interfaces_table" class="table table-hover panel-body component-list">
             <table id="interfaces_table" class="table table-hover panel-body component-list">
                 {% for iface in interfaces %}
                 {% for iface in interfaces %}
-                    {% include 'virtualization/inc/vminterface.html' with selectable=True %}
+                    {% include 'virtualization/inc/interface.html' with selectable=True %}
                 {% empty %}
                 {% empty %}
                     <tr>
                     <tr>
                         <td colspan="4">No interfaces defined</td>
                         <td colspan="4">No interfaces defined</td>
                     </tr>
                     </tr>
                 {% endfor %}
                 {% endfor %}
             </table>
             </table>
-            {% if perms.virtualization.add_vminterface or perms.virtualization.delete_vminterface %}
+            {% if perms.dcim.add_interface or perms.dcim.delete_interface %}
                 <div class="panel-footer">
                 <div class="panel-footer">
-                    {% if interfaces and perms.virtualization.change_vminterface %}
-                        <button type="submit" name="_edit" formaction="{% url 'virtualization:vminterface_bulk_edit' pk=vm.pk %}" class="btn btn-warning btn-xs">
+                    {% if interfaces and perms.dcim.change_interface %}
+                        <button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=vm.pk %}" class="btn btn-warning btn-xs">
                             <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
                             <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
                         </button>
                         </button>
                     {% endif %}
                     {% endif %}
-                    {% if interfaces and perms.virtualization.delete_vminterface %}
-                        <button type="submit" name="_delete" formaction="{% url 'virtualization:vminterface_bulk_delete' pk=vm.pk %}" class="btn btn-danger btn-xs">
+                    {% if interfaces and perms.dcim.delete_interface %}
+                        <button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=vm.pk %}" class="btn btn-danger btn-xs">
                             <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
                             <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
                         </button>
                         </button>
                     {% endif %}
                     {% endif %}
-                    {% if perms.virtualization.add_vminterface %}
+                    {% if perms.dcim.add_interface %}
                         <div class="pull-right">
                         <div class="pull-right">
-                            <a href="{% url 'virtualization:vminterface_add' pk=vm.pk %}" class="btn btn-primary btn-xs">
+                            <a href="{% url 'virtualization:interface_add' pk=vm.pk %}" class="btn btn-primary btn-xs">
                                 <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
                                 <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
                             </a>
                             </a>
                         </div>
                         </div>
@@ -189,7 +189,7 @@
                  </div>
                  </div>
             {% endif %}
             {% endif %}
         </div>
         </div>
-        {% if perms.virtualization.delete_vminterface %}
+        {% if perms.dcim.delete_interface %}
             </form>
             </form>
         {% endif %}
         {% endif %}
 	</div>
 	</div>

+ 15 - 12
netbox/virtualization/api/serializers.py

@@ -3,10 +3,12 @@ from __future__ import unicode_literals
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from dcim.api.serializers import NestedPlatformSerializer
 from dcim.api.serializers import NestedPlatformSerializer
+from dcim.constants import VIFACE_FF_CHOICES
+from dcim.models import Interface
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.api.serializers import NestedTenantSerializer
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ValidatedModelSerializer
-from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
+from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
+from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 
 
 #
 #
@@ -102,7 +104,7 @@ class NestedVirtualMachineSerializer(serializers.ModelSerializer):
 class WritableVirtualMachineSerializer(CustomFieldModelSerializer):
 class WritableVirtualMachineSerializer(CustomFieldModelSerializer):
 
 
     class Meta:
     class Meta:
-        model = Cluster
+        model = VirtualMachine
         fields = [
         fields = [
             'id', 'name', 'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'comments', 'custom_fields',
             'id', 'name', 'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'comments', 'custom_fields',
         ]
         ]
@@ -112,28 +114,29 @@ class WritableVirtualMachineSerializer(CustomFieldModelSerializer):
 # VM interfaces
 # VM interfaces
 #
 #
 
 
-class VMInterfaceSerializer(serializers.ModelSerializer):
+class InterfaceSerializer(serializers.ModelSerializer):
     virtual_machine = NestedVirtualMachineSerializer()
     virtual_machine = NestedVirtualMachineSerializer()
+    form_factor = ChoiceFieldSerializer(choices=VIFACE_FF_CHOICES)
 
 
     class Meta:
     class Meta:
-        model = VMInterface
+        model = Interface
         fields = [
         fields = [
-            'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'description',
+            'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description',
         ]
         ]
 
 
 
 
-class NestedVMInterfaceSerializer(serializers.ModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
+class NestedInterfaceSerializer(serializers.ModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail')
 
 
     class Meta:
     class Meta:
-        model = VMInterface
+        model = Interface
         fields = ['id', 'url', 'name']
         fields = ['id', 'url', 'name']
 
 
 
 
-class WritableVMInterfaceSerializer(ValidatedModelSerializer):
+class WritableInterfaceSerializer(ValidatedModelSerializer):
 
 
     class Meta:
     class Meta:
-        model = VMInterface
+        model = Interface
         fields = [
         fields = [
-            'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'description',
+            'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description',
         ]
         ]

+ 1 - 1
netbox/virtualization/api/urls.py

@@ -23,7 +23,7 @@ router.register(r'clusters', views.ClusterViewSet)
 
 
 # VirtualMachines
 # VirtualMachines
 router.register(r'virtual-machines', views.VirtualMachineViewSet)
 router.register(r'virtual-machines', views.VirtualMachineViewSet)
-router.register(r'vm-interfaces', views.VMInterfaceViewSet)
+router.register(r'interfaces', views.InterfaceViewSet)
 
 
 app_name = 'virtualization-api'
 app_name = 'virtualization-api'
 urlpatterns = router.urls
 urlpatterns = router.urls

+ 6 - 5
netbox/virtualization/api/views.py

@@ -2,10 +2,11 @@ from __future__ import unicode_literals
 
 
 from rest_framework.viewsets import ModelViewSet
 from rest_framework.viewsets import ModelViewSet
 
 
+from dcim.models import Interface
 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 import filters
-from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
+from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from . import serializers
 from . import serializers
 
 
 
 
@@ -41,7 +42,7 @@ class VirtualMachineViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
     filter_class = filters.VirtualMachineFilter
     filter_class = filters.VirtualMachineFilter
 
 
 
 
-class VMInterfaceViewSet(WritableSerializerMixin, ModelViewSet):
-    queryset = VMInterface.objects.select_related('virtual_machine')
-    serializer_class = serializers.VMInterfaceSerializer
-    write_serializer_class = serializers.WritableVMInterfaceSerializer
+class InterfaceViewSet(WritableSerializerMixin, ModelViewSet):
+    queryset = Interface.objects.filter(virtual_machine__isnull=False).select_related('virtual_machine')
+    serializer_class = serializers.InterfaceSerializer
+    write_serializer_class = serializers.WritableInterfaceSerializer

+ 11 - 9
netbox/virtualization/forms.py

@@ -5,8 +5,9 @@ from mptt.forms import TreeNodeChoiceField
 from django import forms
 from django import forms
 from django.db.models import Count
 from django.db.models import Count
 
 
+from dcim.constants import VIFACE_FF_CHOICES
 from dcim.formfields import MACAddressFormField
 from dcim.formfields import MACAddressFormField
-from dcim.models import Device, Rack, Region, Site
+from dcim.models import Device, Interface, Rack, Region, Site
 from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
 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
@@ -15,7 +16,7 @@ from utilities.forms import (
     ChainedModelChoiceField, ChainedModelMultipleChoiceField, ComponentForm, ConfirmationForm, ExpandableNameField,
     ChainedModelChoiceField, ChainedModelMultipleChoiceField, ComponentForm, ConfirmationForm, ExpandableNameField,
     FilterChoiceField, SlugField,
     FilterChoiceField, SlugField,
 )
 )
-from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
+from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 
 
 #
 #
@@ -230,18 +231,19 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
 # VM interfaces
 # VM interfaces
 #
 #
 
 
-class VMInterfaceForm(BootstrapMixin, forms.ModelForm):
+class InterfaceForm(BootstrapMixin, forms.ModelForm):
 
 
     class Meta:
     class Meta:
-        model = VMInterface
-        fields = ['virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description']
+        model = Interface
+        fields = ['virtual_machine', 'name', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description']
         widgets = {
         widgets = {
             'virtual_machine': forms.HiddenInput(),
             'virtual_machine': forms.HiddenInput(),
         }
         }
 
 
 
 
-class VMInterfaceCreateForm(ComponentForm):
+class InterfaceCreateForm(ComponentForm):
     name_pattern = ExpandableNameField(label='Name')
     name_pattern = ExpandableNameField(label='Name')
+    form_factor = forms.ChoiceField(choices=VIFACE_FF_CHOICES)
     enabled = forms.BooleanField(required=False)
     enabled = forms.BooleanField(required=False)
     mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
     mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
     mac_address = MACAddressFormField(required=False, label='MAC Address')
     mac_address = MACAddressFormField(required=False, label='MAC Address')
@@ -253,11 +255,11 @@ class VMInterfaceCreateForm(ComponentForm):
         kwargs['initial'] = kwargs.get('initial', {}).copy()
         kwargs['initial'] = kwargs.get('initial', {}).copy()
         kwargs['initial'].update({'enabled': True})
         kwargs['initial'].update({'enabled': True})
 
 
-        super(VMInterfaceCreateForm, self).__init__(*args, **kwargs)
+        super(InterfaceCreateForm, self).__init__(*args, **kwargs)
 
 
 
 
-class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
-    pk = forms.ModelMultipleChoiceField(queryset=VMInterface.objects.all(), widget=forms.MultipleHiddenInput)
+class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
+    pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
     virtual_machine = forms.ModelChoiceField(queryset=VirtualMachine.objects.all(), widget=forms.HiddenInput)
     virtual_machine = forms.ModelChoiceField(queryset=VirtualMachine.objects.all(), widget=forms.HiddenInput)
     enabled = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
     enabled = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
     mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
     mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')

+ 3 - 24
netbox/virtualization/migrations/0001_initial.py

@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-# Generated by Django 1.11.4 on 2017-08-18 19:46
+# Generated by Django 1.11.4 on 2017-08-29 17:49
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-import dcim.fields
 from django.db import migrations, models
 from django.db import migrations, models
 import django.db.models.deletion
 import django.db.models.deletion
 import extras.models
 import extras.models
@@ -13,9 +12,9 @@ class Migration(migrations.Migration):
     initial = True
     initial = True
 
 
     dependencies = [
     dependencies = [
-        ('dcim', '0041_napalm_integration'),
-        ('ipam', '0019_ipaddress_interface_to_gfk'),
         ('tenancy', '0003_unicode_literals'),
         ('tenancy', '0003_unicode_literals'),
+        ('ipam', '0018_remove_service_uniqueness_constraint'),
+        ('dcim', '0041_napalm_integration'),
     ]
     ]
 
 
     operations = [
     operations = [
@@ -77,22 +76,6 @@ class Migration(migrations.Migration):
             },
             },
             bases=(models.Model, extras.models.CustomFieldModel),
             bases=(models.Model, extras.models.CustomFieldModel),
         ),
         ),
-        migrations.CreateModel(
-            name='VMInterface',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=30)),
-                ('enabled', models.BooleanField(default=True)),
-                ('mac_address', dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address')),
-                ('mtu', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU')),
-                ('description', models.CharField(blank=True, max_length=100)),
-                ('virtual_machine', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine')),
-            ],
-            options={
-                'verbose_name': 'VM interface',
-                'ordering': ['virtual_machine', 'name'],
-            },
-        ),
         migrations.AddField(
         migrations.AddField(
             model_name='cluster',
             model_name='cluster',
             name='group',
             name='group',
@@ -103,8 +86,4 @@ class Migration(migrations.Migration):
             name='type',
             name='type',
             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='virtualization.ClusterType'),
             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='virtualization.ClusterType'),
         ),
         ),
-        migrations.AlterUniqueTogether(
-            name='vminterface',
-            unique_together=set([('virtual_machine', 'name')]),
-        ),
     ]
     ]

+ 0 - 46
netbox/virtualization/models.py

@@ -188,49 +188,3 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
         return reverse('virtualization:virtualmachine', args=[self.pk])
         return reverse('virtualization:virtualmachine', args=[self.pk])
-
-
-@python_2_unicode_compatible
-class VMInterface(models.Model):
-    """
-    A virtual interface which belongs to a VirtualMachine. Like the dcim.Interface model, IPAddresses can be assigned to
-    VMInterfaces.
-    """
-    virtual_machine = models.ForeignKey(
-        to=VirtualMachine,
-        on_delete=models.CASCADE,
-        related_name='interfaces'
-    )
-    name = models.CharField(
-        max_length=30
-    )
-    enabled = models.BooleanField(
-        default=True
-    )
-    mac_address = MACAddressField(
-        blank=True,
-        null=True,
-        verbose_name='MAC Address'
-    )
-    mtu = models.PositiveSmallIntegerField(
-        blank=True,
-        null=True,
-        verbose_name='MTU'
-    )
-    description = models.CharField(
-        max_length=100,
-        blank=True
-    )
-    ip_addresses = GenericRelation(
-        to='ipam.IPAddress',
-        content_type_field='interface_type',
-        object_id_field='interface_id'
-    )
-
-    class Meta:
-        ordering = ['virtual_machine', 'name']
-        unique_together = ['virtual_machine', 'name']
-        verbose_name = 'VM interface'
-
-    def __str__(self):
-        return self.name

+ 4 - 3
netbox/virtualization/tables.py

@@ -3,8 +3,9 @@ from __future__ import unicode_literals
 import django_tables2 as tables
 import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
+from dcim.models import Interface
 from utilities.tables import BaseTable, ToggleColumn
 from utilities.tables import BaseTable, ToggleColumn
-from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
+from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 
 
 CLUSTERTYPE_ACTIONS = """
 CLUSTERTYPE_ACTIONS = """
@@ -89,8 +90,8 @@ class VirtualMachineTable(BaseTable):
 # VM components
 # VM components
 #
 #
 
 
-class VMInterfaceTable(BaseTable):
+class InterfaceTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
-        model = VMInterface
+        model = Interface
         fields = ('name', 'enabled', 'description')
         fields = ('name', 'enabled', 'description')

+ 6 - 6
netbox/virtualization/urls.py

@@ -41,11 +41,11 @@ urlpatterns = [
     url(r'^virtual-machines/(?P<pk>\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
     url(r'^virtual-machines/(?P<pk>\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
 
 
     # VM interfaces
     # VM interfaces
-    # url(r'^virtual-machines/interfaces/add/$', views.VMBulkAddVMInterfaceView.as_view(), name='vm_bulk_add_vminterface'),
-    url(r'^virtual-machines/(?P<pk>\d+)/interfaces/add/$', views.VMInterfaceCreateView.as_view(), name='vminterface_add'),
-    url(r'^virtual-machines/(?P<pk>\d+)/interfaces/edit/$', views.VMInterfaceBulkEditView.as_view(), name='vminterface_bulk_edit'),
-    url(r'^virtual-machines/(?P<pk>\d+)/interfaces/delete/$', views.VMInterfaceBulkDeleteView.as_view(), name='vminterface_bulk_delete'),
-    url(r'^vm-interfaces/(?P<pk>\d+)/edit/$', views.VMInterfaceEditView.as_view(), name='vminterface_edit'),
-    url(r'^vm-interfaces/(?P<pk>\d+)/delete/$', views.VMInterfaceDeleteView.as_view(), name='vminterface_delete'),
+    # url(r'^virtual-machines/interfaces/add/$', views.VMBulkAddInterfaceView.as_view(), name='vm_bulk_add_interface'),
+    url(r'^virtual-machines/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
+    url(r'^virtual-machines/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
+    url(r'^virtual-machines/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
+    url(r'^vm-interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
+    url(r'^vm-interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
 
 
 ]
 ]

+ 24 - 24
netbox/virtualization/views.py

@@ -7,13 +7,13 @@ from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 from django.views.generic import View
 from django.views.generic import View
 
 
-from dcim.models import Device
+from dcim.models import Device, Interface
 from dcim.tables import DeviceTable
 from dcim.tables import DeviceTable
 from utilities.views import (
 from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView,
     BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView,
     ObjectDeleteView, ObjectEditView, ObjectListView,
     ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
-from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
+from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from . import filters
 from . import filters
 from . import forms
 from . import forms
 from . import tables
 from . import tables
@@ -235,7 +235,7 @@ class VirtualMachineView(View):
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         vm = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
         vm = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
-        interfaces = VMInterface.objects.filter(virtual_machine=vm)
+        interfaces = Interface.objects.filter(virtual_machine=vm)
 
 
         return render(request, 'virtualization/virtualmachine.html', {
         return render(request, 'virtualization/virtualmachine.html', {
             'vm': vm,
             'vm': vm,
@@ -282,39 +282,39 @@ class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView):
 # VM interfaces
 # VM interfaces
 #
 #
 
 
-class VMInterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
-    permission_required = 'virtualization.add_vminterface'
+class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
+    permission_required = 'dcim.add_interface'
     parent_model = VirtualMachine
     parent_model = VirtualMachine
     parent_field = 'virtual_machine'
     parent_field = 'virtual_machine'
-    model = VMInterface
-    form = forms.VMInterfaceCreateForm
-    model_form = forms.VMInterfaceForm
+    model = Interface
+    form = forms.InterfaceCreateForm
+    model_form = forms.InterfaceForm
     template_name = 'virtualization/virtualmachine_component_add.html'
     template_name = 'virtualization/virtualmachine_component_add.html'
 
 
 
 
-class VMInterfaceEditView(PermissionRequiredMixin, ComponentEditView):
-    permission_required = 'virtualization.change_vminterface'
-    model = VMInterface
+class InterfaceEditView(PermissionRequiredMixin, ComponentEditView):
+    permission_required = 'dcim.change_interface'
+    model = Interface
     parent_field = 'virtual_machine'
     parent_field = 'virtual_machine'
-    form_class = forms.VMInterfaceForm
+    form_class = forms.InterfaceForm
 
 
 
 
-class VMInterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
-    permission_required = 'virtualization.delete_vminterface'
-    model = VMInterface
+class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
+    permission_required = 'dcim.delete_interface'
+    model = Interface
     parent_field = 'virtual_machine'
     parent_field = 'virtual_machine'
 
 
 
 
-class VMInterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
-    permission_required = 'virtualization.change_vminterface'
-    cls = VMInterface
+class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
+    permission_required = 'dcim.change_interface'
+    cls = Interface
     parent_cls = VirtualMachine
     parent_cls = VirtualMachine
-    table = tables.VMInterfaceTable
-    form = forms.VMInterfaceBulkEditForm
+    table = tables.InterfaceTable
+    form = forms.InterfaceBulkEditForm
 
 
 
 
-class VMInterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
-    permission_required = 'virtualization.delete_vminterface'
-    cls = VMInterface
+class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
+    permission_required = 'dcim.delete_interface'
+    cls = Interface
     parent_cls = VirtualMachine
     parent_cls = VirtualMachine
-    table = tables.VMInterfaceTable
+    table = tables.InterfaceTable