From ea5121ffe1adbdca08489fd0adf08592780012bb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 18 Oct 2018 15:43:55 -0400 Subject: [PATCH] Preliminary work on Cables --- netbox/dcim/constants.py | 17 +++ netbox/dcim/migrations/0066_cables.py | 178 ++++++++++++++++++++++++++ netbox/dcim/models.py | 100 ++++++++++++++- netbox/dcim/signals.py | 28 +++- 4 files changed, 315 insertions(+), 8 deletions(-) create mode 100644 netbox/dcim/migrations/0066_cables.py diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index d51ec97f3..d63735347 100644 --- a/netbox/dcim/constants.py +++ b/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'), +) diff --git a/netbox/dcim/migrations/0066_cables.py b/netbox/dcim/migrations/0066_cables.py new file mode 100644 index 000000000..2650e1636 --- /dev/null +++ b/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), + + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index cdbf78525..0a4d8afc5 100644 --- a/netbox/dcim/models.py +++ b/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) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 2aefdc229..68dce2938 100644 --- a/netbox/dcim/signals.py +++ b/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()