Preliminary work on Cables

This commit is contained in:
Jeremy Stretch 2018-10-18 15:43:55 -04:00
parent 3eddeeadc5
commit ea5121ffe1
4 changed files with 315 additions and 8 deletions

View File

@ -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'),
)

View File

@ -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),
]

View File

@ -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)

View File

@ -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()