mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-16 12:38:17 -06:00
dcim: rearrange models, fixing #3092
This commit is contained in:
parent
edabc8eee9
commit
aa9a88c87e
3
netbox/dcim/models/__init__.py
Normal file
3
netbox/dcim/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .models import *
|
||||
from .component_models import *
|
||||
from .component_template_models import *
|
876
netbox/dcim/models/component_models.py
Normal file
876
netbox/dcim/models/component_models.py
Normal 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,
|
||||
)
|
258
netbox/dcim/models/component_template_models.py
Normal file
258
netbox/dcim/models/component_template_models.py
Normal 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
Loading…
Reference in New Issue
Block a user