Jeremy Stretch 7 лет назад
Родитель
Сommit
ea5121ffe1
4 измененных файлов с 315 добавлено и 8 удалено
  1. 17 0
      netbox/dcim/constants.py
  2. 178 0
      netbox/dcim/migrations/0066_cables.py
  3. 94 6
      netbox/dcim/models.py
  4. 26 2
      netbox/dcim/signals.py

+ 17 - 0
netbox/dcim/constants.py

@@ -282,3 +282,20 @@ CONNECTION_STATUS_CHOICES = [
     [CONNECTION_STATUS_PLANNED, 'Planned'],
     [CONNECTION_STATUS_CONNECTED, 'Connected'],
 ]
+
+# Cable endpoint types
+CABLE_ENDPOINT_TYPES = (
+    'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport',
+)
+CABLE_CONNECTION_TYPES = CABLE_ENDPOINT_TYPES + (
+    'frontpanelport', 'rearpanelport',
+)
+
+# Cable types
+# TODO: Add more types
+CABLE_TYPE_COPPER = 1000
+CABLE_TYPE_FIBER = 2000
+CABLE_TYPE_CHOICES = (
+    (CABLE_TYPE_COPPER, 'Copper'),
+    (CABLE_TYPE_FIBER, 'Fiber'),
+)

+ 178 - 0
netbox/dcim/migrations/0066_cables.py

@@ -0,0 +1,178 @@
+# Generated by Django 2.0.8 on 2018-10-18 19:41
+
+from django.db import migrations, models
+import django.db.models.deletion
+import utilities.fields
+
+
+def console_connections_to_cables(apps, schema_editor):
+    """
+    Copy all existing console connections as Cables
+    """
+    ConsolePort = apps.get_model('dcim', 'ConsolePort')
+    Cable = apps.get_model('dcim', 'Cable')
+
+    # Load content types
+    ContentType = apps.get_model('contenttypes', 'ContentType')
+    consoleport_type = ContentType.objects.get(app_label='dcim', model='consoleport')
+    consoleserverport_type = ContentType.objects.get(app_label='dcim', model='consoleserverport')
+
+    # Create a new Cable instance from each console connection
+    for consoleport in ConsolePort.objects.filter(cs_port__isnull=False):
+        c = Cable()
+        # We have to assign GFK fields manually because we're inside a migration.
+        c.endpoint_a_type = consoleport_type
+        c.endpoint_a_id = consoleport.id
+        c.endpoint_b_type = consoleserverport_type
+        c.endpoint_b_id = consoleport.cs_port_id
+        c.connection_status = consoleport.connection_status
+        c.save()
+
+
+def power_connections_to_cables(apps, schema_editor):
+    """
+    Copy all existing power connections as Cables
+    """
+    PowerPort = apps.get_model('dcim', 'PowerPort')
+    Cable = apps.get_model('dcim', 'Cable')
+
+    # Load content types
+    ContentType = apps.get_model('contenttypes', 'ContentType')
+    powerport_type = ContentType.objects.get(app_label='dcim', model='powerport')
+    poweroutlet_type = ContentType.objects.get(app_label='dcim', model='poweroutlet')
+
+    # Create a new Cable instance from each power connection
+    for powerport in PowerPort.objects.filter(power_outlet__isnull=False):
+        c = Cable()
+        # We have to assign GFK fields manually because we're inside a migration.
+        c.endpoint_a_type = powerport_type
+        c.endpoint_a_id = powerport.id
+        c.endpoint_b_type = poweroutlet_type
+        c.endpoint_b_id = powerport.power_outlet_id
+        c.connection_status = powerport.connection_status
+        c.save()
+
+
+def interface_connections_to_cables(apps, schema_editor):
+    """
+    Copy all InterfaceConnections as Cables
+    """
+    InterfaceConnection = apps.get_model('dcim', 'InterfaceConnection')
+    Cable = apps.get_model('dcim', 'Cable')
+
+    # Load content types
+    ContentType = apps.get_model('contenttypes', 'ContentType')
+    interface_type = ContentType.objects.get(app_label='dcim', model='interface')
+
+    # Create a new Cable instance from each InterfaceConnection
+    for conn in InterfaceConnection.objects.all():
+        c = Cable()
+        # We have to assign GFK fields manually because we're inside a migration.
+        c.endpoint_a_type = interface_type
+        c.endpoint_a_id = conn.interface_a_id
+        c.endpoint_b_type = interface_type
+        c.endpoint_b_id = conn.interface_b_id
+        c.connection_status = conn.connection_status
+        c.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('dcim', '0065_patch_panel_ports'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Cable',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
+                ('created', models.DateField(auto_now_add=True, null=True)),
+                ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+                ('endpoint_a_id', models.PositiveIntegerField()),
+                ('endpoint_b_id', models.PositiveIntegerField()),
+                ('type', models.PositiveSmallIntegerField(blank=True, null=True)),
+                ('status', models.BooleanField(default=True)),
+                ('label', models.CharField(blank=True, max_length=100)),
+                ('color', utilities.fields.ColorField(blank=True, max_length=6)),
+                ('endpoint_a_type', models.ForeignKey(limit_choices_to={'model__in': ('consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontpanelport', 'rearpanelport')}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
+                ('endpoint_b_type', models.ForeignKey(limit_choices_to={'model__in': ('consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontpanelport', 'rearpanelport')}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='consoleport',
+            name='connected_endpoint_id',
+            field=models.PositiveIntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='consoleport',
+            name='connected_endpoint_type',
+            field=models.ForeignKey(blank=True, limit_choices_to={'model__in': ('consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport')}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
+        ),
+        migrations.AddField(
+            model_name='consoleserverport',
+            name='connected_endpoint_id',
+            field=models.PositiveIntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='consoleserverport',
+            name='connected_endpoint_type',
+            field=models.ForeignKey(blank=True, limit_choices_to={'model__in': ('consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport')}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
+        ),
+        migrations.AddField(
+            model_name='consoleserverport',
+            name='connection_status',
+            field=models.NullBooleanField(default=True),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='connected_endpoint_id',
+            field=models.PositiveIntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='connected_endpoint_type',
+            field=models.ForeignKey(blank=True, limit_choices_to={'model__in': ('consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport')}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='connection_status',
+            field=models.NullBooleanField(default=True),
+        ),
+        migrations.AddField(
+            model_name='poweroutlet',
+            name='connected_endpoint_id',
+            field=models.PositiveIntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='poweroutlet',
+            name='connected_endpoint_type',
+            field=models.ForeignKey(blank=True, limit_choices_to={'model__in': ('consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport')}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
+        ),
+        migrations.AddField(
+            model_name='poweroutlet',
+            name='connection_status',
+            field=models.NullBooleanField(default=True),
+        ),
+        migrations.AddField(
+            model_name='powerport',
+            name='connected_endpoint_id',
+            field=models.PositiveIntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='powerport',
+            name='connected_endpoint_type',
+            field=models.ForeignKey(blank=True, limit_choices_to={'model__in': ('consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport')}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='cable',
+            unique_together={('endpoint_b_type', 'endpoint_b_id'), ('endpoint_a_type', 'endpoint_a_id')},
+        ),
+
+        # Copy console/power/interface connections as Cables
+        migrations.RunPython(console_connections_to_cables),
+        migrations.RunPython(power_connections_to_cables),
+        migrations.RunPython(interface_connections_to_cables),
+
+    ]

+ 94 - 6
netbox/dcim/models.py

@@ -3,7 +3,8 @@ from itertools import count, groupby
 
 from django.conf import settings
 from django.contrib.auth.models import User
-from django.contrib.contenttypes.fields import GenericRelation
+from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
+from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.fields import ArrayField, JSONField
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
@@ -65,6 +66,32 @@ class ComponentModel(models.Model):
         ).save()
 
 
+class ConnectableModel(models.Model):
+    connected_endpoint_type = models.ForeignKey(
+        to=ContentType,
+        limit_choices_to={'model__in': CABLE_ENDPOINT_TYPES},
+        on_delete=models.PROTECT,
+        related_name='+',
+        blank=True,
+        null=True
+    )
+    connected_endpoint_id = models.PositiveIntegerField(
+        blank=True,
+        null=True
+    )
+    connected_endpoint = GenericForeignKey(
+        ct_field='connected_endpoint_type',
+        fk_field='connected_endpoint_id'
+    )
+    connection_status = models.NullBooleanField(
+        choices=CONNECTION_STATUS_CHOICES,
+        default=CONNECTION_STATUS_CONNECTED
+    )
+
+    class Meta:
+        abstract = True
+
+
 #
 # Regions
 #
@@ -1616,7 +1643,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
 # Console ports
 #
 
-class ConsolePort(ComponentModel):
+class ConsolePort(ConnectableModel, ComponentModel):
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     """
@@ -1679,7 +1706,7 @@ class ConsoleServerPortManager(models.Manager):
         }).order_by('device', 'name_padded')
 
 
-class ConsoleServerPort(ComponentModel):
+class ConsoleServerPort(ConnectableModel, ComponentModel):
     """
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
     """
@@ -1720,7 +1747,7 @@ class ConsoleServerPort(ComponentModel):
 # Power ports
 #
 
-class PowerPort(ComponentModel):
+class PowerPort(ConnectableModel, ComponentModel):
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     """
@@ -1782,7 +1809,7 @@ class PowerOutletManager(models.Manager):
         }).order_by('device', 'name_padded')
 
 
-class PowerOutlet(ComponentModel):
+class PowerOutlet(ConnectableModel, ComponentModel):
     """
     A physical power outlet (output) within a Device which provides power to a PowerPort.
     """
@@ -1823,7 +1850,7 @@ class PowerOutlet(ComponentModel):
 # Interfaces
 #
 
-class Interface(ComponentModel):
+class Interface(ConnectableModel, ComponentModel):
     """
     A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
     Interface via the creation of an InterfaceConnection.
@@ -2423,3 +2450,64 @@ class VirtualChassis(ChangeLoggedModel):
             self.master,
             self.domain,
         )
+
+
+#
+# Cables
+#
+
+class Cable(ChangeLoggedModel):
+    """
+    A physical connection between two endpoints.
+    """
+    endpoint_a_type = models.ForeignKey(
+        to=ContentType,
+        limit_choices_to={'model__in': CABLE_CONNECTION_TYPES},
+        on_delete=models.PROTECT,
+        related_name='+'
+    )
+    endpoint_a_id = models.PositiveIntegerField()
+    endpoint_a = GenericForeignKey(
+        ct_field='endpoint_a_type',
+        fk_field='endpoint_a_id'
+    )
+    endpoint_b_type = models.ForeignKey(
+        to=ContentType,
+        limit_choices_to={'model__in': CABLE_CONNECTION_TYPES},
+        on_delete=models.PROTECT,
+        related_name='+'
+    )
+    endpoint_b_id = models.PositiveIntegerField()
+    endpoint_b = GenericForeignKey(
+        ct_field='endpoint_b_type',
+        fk_field='endpoint_b_id'
+    )
+    type = models.PositiveSmallIntegerField(
+        choices=CABLE_TYPE_CHOICES,
+        blank=True,
+        null=True
+    )
+    status = models.BooleanField(
+        choices=CONNECTION_STATUS_CHOICES,
+        default=CONNECTION_STATUS_CONNECTED
+    )
+    label = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    color = ColorField(
+        blank=True
+    )
+
+    class Meta:
+        unique_together = (
+            ('endpoint_a_type', 'endpoint_a_id'),
+            ('endpoint_b_type', 'endpoint_b_id'),
+        )
+
+    # TODO: This should follow all cables in a path
+    def get_path_endpoints(self):
+        """
+        Return the endpoints connected by this cable path.
+        """
+        return (self.endpoint_a, self.endpoint_b)

+ 26 - 2
netbox/dcim/signals.py

@@ -1,7 +1,7 @@
-from django.db.models.signals import post_save, pre_delete
+from django.db.models.signals import post_save, post_delete, pre_delete
 from django.dispatch import receiver
 
-from .models import Device, VirtualChassis
+from .models import Cable, Device, VirtualChassis
 
 
 @receiver(post_save, sender=VirtualChassis)
@@ -19,3 +19,27 @@ def clear_virtualchassis_members(instance, **kwargs):
     When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.
     """
     Device.objects.filter(virtual_chassis=instance.pk).update(vc_position=None, vc_priority=None)
+
+
+@receiver(post_save, sender=Cable)
+def update_connected_endpoints(instance, **kwargs):
+    """
+    When a Cable is saved, update its connected endpoints.
+    """
+    endpoint_a, endpoint_b = instance.get_path_endpoints()
+    endpoint_a.connected_endpoint = endpoint_b
+    endpoint_a.save()
+    endpoint_b.connected_endpoint = endpoint_a
+    endpoint_b.save()
+
+
+@receiver(post_delete, sender=Cable)
+def nullify_connected_endpoints(instance, **kwargs):
+    """
+    When a Cable is deleted, nullify its connected endpoints.
+    """
+    endpoint_a, endpoint_b = instance.get_path_endpoints()
+    endpoint_a.connected_endpoint = None
+    endpoint_a.save()
+    endpoint_b.connected_endpoint = None
+    endpoint_b.save()