Bläddra i källkod

Preliminary work on Cables

Jeremy Stretch 7 år sedan
förälder
incheckning
ea5121ffe1
4 ändrade filer med 315 tillägg och 8 borttagningar
  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_PLANNED, 'Planned'],
     [CONNECTION_STATUS_CONNECTED, 'Connected'],
     [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.conf import settings
 from django.contrib.auth.models import User
 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.contrib.postgres.fields import ArrayField, JSONField
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
@@ -65,6 +66,32 @@ class ComponentModel(models.Model):
         ).save()
         ).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
 # Regions
 #
 #
@@ -1616,7 +1643,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
 # Console ports
 # Console ports
 #
 #
 
 
-class ConsolePort(ComponentModel):
+class ConsolePort(ConnectableModel, ComponentModel):
     """
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     """
     """
@@ -1679,7 +1706,7 @@ class ConsoleServerPortManager(models.Manager):
         }).order_by('device', 'name_padded')
         }).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.
     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
 # Power ports
 #
 #
 
 
-class PowerPort(ComponentModel):
+class PowerPort(ConnectableModel, ComponentModel):
     """
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     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')
         }).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.
     A physical power outlet (output) within a Device which provides power to a PowerPort.
     """
     """
@@ -1823,7 +1850,7 @@ class PowerOutlet(ComponentModel):
 # Interfaces
 # 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
     A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
     Interface via the creation of an InterfaceConnection.
     Interface via the creation of an InterfaceConnection.
@@ -2423,3 +2450,64 @@ class VirtualChassis(ChangeLoggedModel):
             self.master,
             self.master,
             self.domain,
             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 django.dispatch import receiver
 
 
-from .models import Device, VirtualChassis
+from .models import Cable, Device, VirtualChassis
 
 
 
 
 @receiver(post_save, sender=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.
     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)
     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()