diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py
index f2c047910..8758bb55e 100644
--- a/netbox/dcim/constants.py
+++ b/netbox/dcim/constants.py
@@ -92,13 +92,15 @@ IFACE_FF_JUNIPER_VCP = 5200
# Other
IFACE_FF_OTHER = 32767
+VIFACE_FF_CHOICES = [
+ [IFACE_FF_VIRTUAL, 'Virtual'],
+ [IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
+]
+
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
- [
- [IFACE_FF_VIRTUAL, 'Virtual'],
- [IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
- ]
+ VIFACE_FF_CHOICES,
],
[
'Ethernet (fixed)',
diff --git a/netbox/dcim/migrations/0042_device_cluster.py b/netbox/dcim/migrations/0042_device_cluster.py
deleted file mode 100644
index f9a0a8637..000000000
--- a/netbox/dcim/migrations/0042_device_cluster.py
+++ /dev/null
@@ -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'),
- ),
- ]
diff --git a/netbox/dcim/migrations/0042_virtualization.py b/netbox/dcim/migrations/0042_virtualization.py
new file mode 100644
index 000000000..70c299ccb
--- /dev/null
+++ b/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'),
+ ),
+ ]
diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py
index 5bb2996e0..adf0d80a3 100644
--- a/netbox/dcim/models.py
+++ b/netbox/dcim/models.py
@@ -1152,13 +1152,26 @@ class PowerOutlet(models.Model):
@python_2_unicode_compatible
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(
- 'self',
- models.SET_NULL,
+ to='self',
+ on_delete=models.SET_NULL,
related_name='member_interfaces',
null=True,
blank=True,
@@ -1175,11 +1188,6 @@ class Interface(models.Model):
help_text="This interface is used only for out-of-band management"
)
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()
@@ -1192,6 +1200,18 @@ class Interface(models.Model):
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
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
raise ValidationError({
diff --git a/netbox/ipam/migrations/0019_ipaddress_interface_to_gfk.py b/netbox/ipam/migrations/0019_ipaddress_interface_to_gfk.py
deleted file mode 100644
index ab2f1b7f8..000000000
--- a/netbox/ipam/migrations/0019_ipaddress_interface_to_gfk.py
+++ /dev/null
@@ -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',
- )
- ]
diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py
index f053c3892..ddc1e9e48 100644
--- a/netbox/ipam/models.py
+++ b/netbox/ipam/models.py
@@ -409,15 +409,8 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
role = models.PositiveSmallIntegerField(
'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,
null=True, verbose_name='NAT (Inside)',
help_text="The IP for which this address is the \"outside\" IP")
diff --git a/netbox/templates/virtualization/inc/vminterface.html b/netbox/templates/virtualization/inc/interface.html
similarity index 83%
rename from netbox/templates/virtualization/inc/vminterface.html
rename to netbox/templates/virtualization/inc/interface.html
index ac6d98672..5a08848da 100644
--- a/netbox/templates/virtualization/inc/vminterface.html
+++ b/netbox/templates/virtualization/inc/interface.html
@@ -1,5 +1,5 @@
- {% if selectable and perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
+ {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
@@ -13,13 +13,13 @@
{{ iface.mtu|default:"" }}
{{ iface.mac_address|default:"" }}
- {% if perms.virtualization.change_vminterface %}
-
+ {% if perms.dcim.change_interface %}
+
{% endif %}
- {% if perms.virtualization.delete_vminterface %}
-
+ {% if perms.dcim.delete_interface %}
+
{% endif %}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html
index 40f4445ea..cea8a0a83 100644
--- a/netbox/templates/virtualization/virtualmachine.html
+++ b/netbox/templates/virtualization/virtualmachine.html
@@ -133,7 +133,7 @@
- {% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
+ {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
{% for iface in interfaces %}
- {% include 'virtualization/inc/vminterface.html' with selectable=True %}
+ {% include 'virtualization/inc/interface.html' with selectable=True %}
{% empty %}