dcim: rearrange models, fixing #3092

This commit is contained in:
hellerve 2019-05-27 14:40:22 +02:00
parent edabc8eee9
commit aa9a88c87e
4 changed files with 1143 additions and 1103 deletions

View File

@ -0,0 +1,3 @@
from .models import *
from .component_models import *
from .component_template_models import *

View File

@ -0,0 +1,876 @@
from collections import OrderedDict
from itertools import count, groupby
from django.conf import settings
from django.contrib.auth.models import User
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
from django.db import models
from django.db.models import Count, Q
from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager
from timezone_field import TimeZoneField
from dcim.constants import *
from dcim.exceptions import LoopDetected
from dcim.fields import ASNField, MACAddressField
from dcim.managers import InterfaceManager
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange
from utilities.fields import ColorField
from utilities.managers import NaturalOrderingManager
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object, to_meters
class ComponentModel(models.Model):
class Meta:
abstract = True
def log_change(self, user, request_id, action):
"""
Log an ObjectChange including the parent Device/VM.
"""
try:
parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None)
except ObjectDoesNotExist:
# The parent device/VM has already been deleted
parent = None
ObjectChange(
user=user,
request_id=request_id,
changed_object=self,
related_object=parent,
action=action,
object_data=serialize_object(self)
).save()
@property
def parent(self):
return getattr(self, 'device', None)
class CableTermination(models.Model):
cable = models.ForeignKey(
to='dcim.Cable',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
# Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted.
_cabled_as_a = GenericRelation(
to='dcim.Cable',
content_type_field='termination_a_type',
object_id_field='termination_a_id'
)
_cabled_as_b = GenericRelation(
to='dcim.Cable',
content_type_field='termination_b_type',
object_id_field='termination_b_id'
)
class Meta:
abstract = True
def trace(self, position=1, follow_circuits=False, cable_history=None):
"""
Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
[
(termination A, cable, termination B),
(termination C, cable, termination D),
(termination E, cable, termination F)
]
"""
def get_peer_port(termination, position=1, follow_circuits=False):
from circuits.models import CircuitTermination
# Map a front port to its corresponding rear port
if isinstance(termination, FrontPort):
return termination.rear_port, termination.rear_port_position
# Map a rear port/position to its corresponding front port
elif isinstance(termination, RearPort):
if position not in range(1, termination.positions + 1):
raise Exception("Invalid position for {} ({} positions): {})".format(
termination, termination.positions, position
))
try:
peer_port = FrontPort.objects.get(
rear_port=termination,
rear_port_position=position,
)
return peer_port, 1
except ObjectDoesNotExist:
return None, None
# Follow a circuit to its other termination
elif isinstance(termination, CircuitTermination) and follow_circuits:
peer_termination = termination.get_peer_termination()
if peer_termination is None:
return None, None
return peer_termination, position
# Termination is not a pass-through port
else:
return None, None
if not self.cable:
return [(self, None, None)]
# Record cable history to detect loops
if cable_history is None:
cable_history = []
elif self.cable in cable_history:
raise LoopDetected()
cable_history.append(self.cable)
far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a
path = [(self, self.cable, far_end)]
peer_port, position = get_peer_port(far_end, position, follow_circuits)
if peer_port is None:
return path
try:
next_segment = peer_port.trace(position, follow_circuits, cable_history)
except LoopDetected:
return path
if next_segment is None:
return path + [(peer_port, None, None)]
return path + next_segment
def get_cable_peer(self):
if self.cable is None:
return None
if self._cabled_as_a.exists():
return self.cable.termination_b
if self._cabled_as_b.exists():
return self.cable.termination_a
#
# Console ports
#
class ConsolePort(CableTermination, ComponentModel):
"""
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='consoleports'
)
name = models.CharField(
max_length=50
)
connected_endpoint = models.OneToOneField(
to='dcim.ConsoleServerPort',
on_delete=models.SET_NULL,
related_name='connected_endpoint',
blank=True,
null=True
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True
)
objects = NaturalOrderingManager()
tags = TaggableManager()
csv_headers = ['device', 'name']
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __str__(self):
return self.name
def get_absolute_url(self):
return self.device.get_absolute_url()
def to_csv(self):
return (
self.device.identifier,
self.name,
)
#
# Console server ports
#
class ConsoleServerPort(CableTermination, ComponentModel):
"""
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='consoleserverports'
)
name = models.CharField(
max_length=50
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True
)
objects = NaturalOrderingManager()
tags = TaggableManager()
csv_headers = ['device', 'name']
class Meta:
unique_together = ['device', 'name']
def __str__(self):
return self.name
def get_absolute_url(self):
return self.device.get_absolute_url()
def to_csv(self):
return (
self.device.identifier,
self.name,
)
#
# Power ports
#
class PowerPort(CableTermination, ComponentModel):
"""
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='powerports'
)
name = models.CharField(
max_length=50
)
connected_endpoint = models.OneToOneField(
to='dcim.PowerOutlet',
on_delete=models.SET_NULL,
related_name='connected_endpoint',
blank=True,
null=True
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True
)
objects = NaturalOrderingManager()
tags = TaggableManager()
csv_headers = ['device', 'name']
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __str__(self):
return self.name
def get_absolute_url(self):
return self.device.get_absolute_url()
def to_csv(self):
return (
self.device.identifier,
self.name,
)
#
# Power outlets
#
class PowerOutlet(CableTermination, ComponentModel):
"""
A physical power outlet (output) within a Device which provides power to a PowerPort.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='poweroutlets'
)
name = models.CharField(
max_length=50
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True
)
objects = NaturalOrderingManager()
tags = TaggableManager()
csv_headers = ['device', 'name']
class Meta:
unique_together = ['device', 'name']
def __str__(self):
return self.name
def get_absolute_url(self):
return self.device.get_absolute_url()
def to_csv(self):
return (
self.device.identifier,
self.name,
)
#
# Interfaces
#
class Interface(CableTermination, ComponentModel):
"""
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
Interface.
"""
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
)
name = models.CharField(
max_length=64
)
_connected_interface = models.OneToOneField(
to='self',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
_connected_circuittermination = models.OneToOneField(
to='circuits.CircuitTermination',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True
)
lag = models.ForeignKey(
to='self',
on_delete=models.SET_NULL,
related_name='member_interfaces',
null=True,
blank=True,
verbose_name='Parent LAG'
)
form_factor = models.PositiveSmallIntegerField(
choices=IFACE_FF_CHOICES,
default=IFACE_FF_10GE_SFP_PLUS
)
enabled = models.BooleanField(
default=True
)
mac_address = MACAddressField(
null=True,
blank=True,
verbose_name='MAC Address'
)
mtu = models.PositiveIntegerField(
blank=True,
null=True,
validators=[MinValueValidator(1), MaxValueValidator(65536)],
verbose_name='MTU'
)
mgmt_only = models.BooleanField(
default=False,
verbose_name='OOB Management',
help_text='This interface is used only for out-of-band management'
)
description = models.CharField(
max_length=100,
blank=True
)
mode = models.PositiveSmallIntegerField(
choices=IFACE_MODE_CHOICES,
blank=True,
null=True
)
untagged_vlan = models.ForeignKey(
to='ipam.VLAN',
on_delete=models.SET_NULL,
related_name='interfaces_as_untagged',
null=True,
blank=True,
verbose_name='Untagged VLAN'
)
tagged_vlans = models.ManyToManyField(
to='ipam.VLAN',
related_name='interfaces_as_tagged',
blank=True,
verbose_name='Tagged VLANs'
)
objects = InterfaceManager()
tags = TaggableManager()
csv_headers = [
'device', 'virtual_machine', 'name', 'lag', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
'description', 'mode',
]
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('dcim:interface', kwargs={'pk': self.pk})
def to_csv(self):
return (
self.device.identifier if self.device else None,
self.virtual_machine.name if self.virtual_machine else None,
self.name,
self.lag.name if self.lag else None,
self.get_form_factor_display(),
self.enabled,
self.mac_address,
self.mtu,
self.mgmt_only,
self.description,
self.get_mode_display(),
)
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 is not IFACE_FF_VIRTUAL:
raise ValidationError({
'form_factor': "Virtual machines can only have virtual interfaces."
})
# Virtual interfaces cannot be connected
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and (
self.cable or getattr(self, 'circuit_termination', False)
):
raise ValidationError({
'form_factor': "Virtual and wireless interfaces cannot be connected to another interface or circuit. "
"Disconnect the interface or choose a suitable form factor."
})
# An interface's LAG must belong to the same device (or VC master)
if self.lag and self.lag.device not in [self.device, self.device.get_vc_master()]:
raise ValidationError({
'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format(
self.lag.name, self.lag.device.name
)
})
# A virtual interface cannot have a parent LAG
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
raise ValidationError({
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
})
# Only a LAG can have LAG members
if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists():
raise ValidationError({
'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format(
", ".join([iface.name for iface in self.member_interfaces.all()])
)
})
# Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
raise ValidationError({
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
"device/VM, or it must be global".format(self.untagged_vlan)
})
def save(self, *args, **kwargs):
# Remove untagged VLAN assignment for non-802.1Q interfaces
if self.mode is None:
self.untagged_vlan = None
# Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
if self.pk and self.mode is not IFACE_MODE_TAGGED:
self.tagged_vlans.clear()
return super().save(*args, **kwargs)
def log_change(self, user, request_id, action):
"""
Include the connected Interface (if any).
"""
# It's possible that an Interface can be deleted _after_ its parent Device/VM, in which case trying to resolve
# the component parent will raise DoesNotExist. For more discussion, see
# https://github.com/digitalocean/netbox/issues/2323
try:
parent_obj = self.device or self.virtual_machine
except ObjectDoesNotExist:
parent_obj = None
ObjectChange(
user=user,
request_id=request_id,
changed_object=self,
related_object=parent_obj,
action=action,
object_data=serialize_object(self)
).save()
@property
def connected_endpoint(self):
if self._connected_interface:
return self._connected_interface
return self._connected_circuittermination
@connected_endpoint.setter
def connected_endpoint(self, value):
from circuits.models import CircuitTermination
if value is None:
self._connected_interface = None
self._connected_circuittermination = None
elif isinstance(value, Interface):
self._connected_interface = value
self._connected_circuittermination = None
elif isinstance(value, CircuitTermination):
self._connected_interface = None
self._connected_circuittermination = value
else:
raise ValueError(
"Connected endpoint must be an Interface or CircuitTermination, not {}.".format(type(value))
)
@property
def parent(self):
return self.device or self.virtual_machine
@property
def is_connectable(self):
return self.form_factor not in NONCONNECTABLE_IFACE_TYPES
@property
def is_virtual(self):
return self.form_factor in VIRTUAL_IFACE_TYPES
@property
def is_wireless(self):
return self.form_factor in WIRELESS_IFACE_TYPES
@property
def is_lag(self):
return self.form_factor == IFACE_FF_LAG
@property
def count_ipaddresses(self):
return self.ip_addresses.count()
#
# Pass-through ports
#
class FrontPort(CableTermination, ComponentModel):
"""
A pass-through port on the front of a Device.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='frontports'
)
name = models.CharField(
max_length=64
)
type = models.PositiveSmallIntegerField(
choices=PORT_TYPE_CHOICES
)
rear_port = models.ForeignKey(
to='dcim.RearPort',
on_delete=models.CASCADE,
related_name='frontports'
)
rear_port_position = models.PositiveSmallIntegerField(
default=1,
validators=[MinValueValidator(1), MaxValueValidator(64)]
)
description = models.CharField(
max_length=100,
blank=True
)
objects = NaturalOrderingManager()
tags = TaggableManager()
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
class Meta:
ordering = ['device', 'name']
unique_together = [
['device', 'name'],
['rear_port', 'rear_port_position'],
]
def __str__(self):
return self.name
def to_csv(self):
return (
self.device.identifier,
self.name,
self.get_type_display(),
self.rear_port.name,
self.rear_port_position,
self.description,
)
def clean(self):
# Validate rear port assignment
if self.rear_port.device != self.device:
raise ValidationError(
"Rear port ({}) must belong to the same device".format(self.rear_port)
)
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError(
"Invalid rear port position ({}); rear port {} has only {} positions".format(
self.rear_port_position, self.rear_port.name, self.rear_port.positions
)
)
class RearPort(CableTermination, ComponentModel):
"""
A pass-through port on the rear of a Device.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='rearports'
)
name = models.CharField(
max_length=64
)
type = models.PositiveSmallIntegerField(
choices=PORT_TYPE_CHOICES
)
positions = models.PositiveSmallIntegerField(
default=1,
validators=[MinValueValidator(1), MaxValueValidator(64)]
)
description = models.CharField(
max_length=100,
blank=True
)
objects = NaturalOrderingManager()
tags = TaggableManager()
csv_headers = ['device', 'name', 'type', 'positions', 'description']
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __str__(self):
return self.name
def to_csv(self):
return (
self.device.identifier,
self.name,
self.get_type_display(),
self.positions,
self.description,
)
#
# Device bays
#
class DeviceBay(ComponentModel):
"""
An empty space within a Device which can house a child device
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='device_bays'
)
name = models.CharField(
max_length=50,
verbose_name='Name'
)
installed_device = models.OneToOneField(
to='dcim.Device',
on_delete=models.SET_NULL,
related_name='parent_bay',
blank=True,
null=True
)
objects = NaturalOrderingManager()
tags = TaggableManager()
csv_headers = ['device', 'name', 'installed_device']
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __str__(self):
return '{} - {}'.format(self.device.name, self.name)
def get_absolute_url(self):
return self.device.get_absolute_url()
def to_csv(self):
return (
self.device.identifier,
self.name,
self.installed_device.identifier if self.installed_device else None,
)
def clean(self):
# Validate that the parent Device can have DeviceBays
if not self.device.device_type.is_parent_device:
raise ValidationError("This type of device ({}) does not support device bays.".format(
self.device.device_type
))
# Cannot install a device into itself, obviously
if self.device == self.installed_device:
raise ValidationError("Cannot install a device into itself.")
#
# Inventory items
#
class InventoryItem(ComponentModel):
"""
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
InventoryItems are used only for inventory purposes.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='inventory_items'
)
parent = models.ForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='child_items',
blank=True,
null=True
)
name = models.CharField(
max_length=50,
verbose_name='Name'
)
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
related_name='inventory_items',
blank=True,
null=True
)
part_id = models.CharField(
max_length=50,
verbose_name='Part ID',
blank=True
)
serial = models.CharField(
max_length=50,
verbose_name='Serial number',
blank=True
)
asset_tag = models.CharField(
max_length=50,
unique=True,
blank=True,
null=True,
verbose_name='Asset tag',
help_text='A unique tag used to identify this item'
)
discovered = models.BooleanField(
default=False,
verbose_name='Discovered'
)
description = models.CharField(
max_length=100,
blank=True
)
tags = TaggableManager()
csv_headers = [
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
]
class Meta:
ordering = ['device__id', 'parent__id', 'name']
unique_together = ['device', 'parent', 'name']
def __str__(self):
return self.name
def get_absolute_url(self):
return self.device.get_absolute_url()
def to_csv(self):
return (
self.device.name or '{{{}}}'.format(self.device.pk),
self.name,
self.manufacturer.name if self.manufacturer else None,
self.part_id,
self.serial,
self.asset_tag,
self.discovered,
self.description,
)

View File

@ -0,0 +1,258 @@
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from dcim.constants import *
from dcim.managers import InterfaceManager
from extras.models import ObjectChange
from utilities.managers import NaturalOrderingManager
from utilities.utils import serialize_object
class ComponentTemplateModel(models.Model):
class Meta:
abstract = True
def log_change(self, user, request_id, action):
"""
Log an ObjectChange including the parent DeviceType.
"""
ObjectChange(
user=user,
request_id=request_id,
changed_object=self,
related_object=self.device_type,
action=action,
object_data=serialize_object(self)
).save()
class ConsolePortTemplate(ComponentTemplateModel):
"""
A template for a ConsolePort to be created for a new Device.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='consoleport_templates'
)
name = models.CharField(
max_length=50
)
objects = NaturalOrderingManager()
class Meta:
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __str__(self):
return self.name
class ConsoleServerPortTemplate(ComponentTemplateModel):
"""
A template for a ConsoleServerPort to be created for a new Device.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='consoleserverport_templates'
)
name = models.CharField(
max_length=50
)
objects = NaturalOrderingManager()
class Meta:
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __str__(self):
return self.name
class PowerPortTemplate(ComponentTemplateModel):
"""
A template for a PowerPort to be created for a new Device.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='powerport_templates'
)
name = models.CharField(
max_length=50
)
objects = NaturalOrderingManager()
class Meta:
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __str__(self):
return self.name
class PowerOutletTemplate(ComponentTemplateModel):
"""
A template for a PowerOutlet to be created for a new Device.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='poweroutlet_templates'
)
name = models.CharField(
max_length=50
)
objects = NaturalOrderingManager()
class Meta:
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __str__(self):
return self.name
class InterfaceTemplate(ComponentTemplateModel):
"""
A template for a physical data interface on a new Device.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='interface_templates'
)
name = models.CharField(
max_length=64
)
form_factor = models.PositiveSmallIntegerField(
choices=IFACE_FF_CHOICES,
default=IFACE_FF_10GE_SFP_PLUS
)
mgmt_only = models.BooleanField(
default=False,
verbose_name='Management only'
)
objects = InterfaceManager()
class Meta:
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __str__(self):
return self.name
class FrontPortTemplate(ComponentTemplateModel):
"""
Template for a pass-through port on the front of a new Device.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='frontport_templates'
)
name = models.CharField(
max_length=64
)
type = models.PositiveSmallIntegerField(
choices=PORT_TYPE_CHOICES
)
rear_port = models.ForeignKey(
to='dcim.RearPortTemplate',
on_delete=models.CASCADE,
related_name='frontport_templates'
)
rear_port_position = models.PositiveSmallIntegerField(
default=1,
validators=[MinValueValidator(1), MaxValueValidator(64)]
)
objects = NaturalOrderingManager()
class Meta:
ordering = ['device_type', 'name']
unique_together = [
['device_type', 'name'],
['rear_port', 'rear_port_position'],
]
def __str__(self):
return self.name
def clean(self):
# Validate rear port assignment
if self.rear_port.device_type != self.device_type:
raise ValidationError(
"Rear port ({}) must belong to the same device type".format(self.rear_port)
)
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError(
"Invalid rear port position ({}); rear port {} has only {} positions".format(
self.rear_port_position, self.rear_port.name, self.rear_port.positions
)
)
class RearPortTemplate(ComponentTemplateModel):
"""
Template for a pass-through port on the rear of a new Device.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='rearport_templates'
)
name = models.CharField(
max_length=64
)
type = models.PositiveSmallIntegerField(
choices=PORT_TYPE_CHOICES
)
positions = models.PositiveSmallIntegerField(
default=1,
validators=[MinValueValidator(1), MaxValueValidator(64)]
)
objects = NaturalOrderingManager()
class Meta:
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __str__(self):
return self.name
class DeviceBayTemplate(ComponentTemplateModel):
"""
A template for a DeviceBay to be created for a new parent Device.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='device_bay_templates'
)
name = models.CharField(
max_length=50
)
objects = NaturalOrderingManager()
class Meta:
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __str__(self):
return self.name

File diff suppressed because it is too large Load Diff