| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285 |
- from functools import cached_property
- from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
- from django.contrib.contenttypes.models import ContentType
- from django.core.exceptions import ValidationError
- from django.core.validators import MaxValueValidator, MinValueValidator
- from django.db import models
- from django.db.models import Sum
- from django.urls import reverse
- from django.utils.translation import gettext_lazy as _
- from mptt.models import MPTTModel, TreeForeignKey
- from dcim.choices import *
- from dcim.constants import *
- from dcim.fields import MACAddressField, WWNField
- from netbox.models import OrganizationalModel, NetBoxModel
- from utilities.choices import ColorChoices
- from utilities.fields import ColorField, NaturalOrderingField
- from utilities.mptt import TreeManager
- from utilities.ordering import naturalize_interface
- from utilities.query_functions import CollateAsChar
- from utilities.tracking import TrackingModelMixin
- from wireless.choices import *
- from wireless.utils import get_channel_attr
- __all__ = (
- 'BaseInterface',
- 'CabledObjectModel',
- 'ConsolePort',
- 'ConsoleServerPort',
- 'DeviceBay',
- 'FrontPort',
- 'Interface',
- 'InventoryItem',
- 'InventoryItemRole',
- 'ModuleBay',
- 'PathEndpoint',
- 'PowerOutlet',
- 'PowerPort',
- 'RearPort',
- )
- class ComponentModel(NetBoxModel):
- """
- An abstract model inherited by any model which has a parent Device.
- """
- device = models.ForeignKey(
- to='dcim.Device',
- on_delete=models.CASCADE,
- related_name='%(class)ss'
- )
- name = models.CharField(
- verbose_name=_('name'),
- max_length=64
- )
- _name = NaturalOrderingField(
- target_field='name',
- max_length=100,
- blank=True
- )
- label = models.CharField(
- verbose_name=_('label'),
- max_length=64,
- blank=True,
- help_text=_('Physical label')
- )
- description = models.CharField(
- verbose_name=_('description'),
- max_length=200,
- blank=True
- )
- class Meta:
- abstract = True
- ordering = ('device', '_name')
- constraints = (
- models.UniqueConstraint(
- fields=('device', 'name'),
- name='%(app_label)s_%(class)s_unique_device_name'
- ),
- )
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Cache the original Device ID for reference under clean()
- self._original_device = self.__dict__.get('device_id')
- def __str__(self):
- if self.label:
- return f"{self.name} ({self.label})"
- return self.name
- def to_objectchange(self, action):
- objectchange = super().to_objectchange(action)
- objectchange.related_object = self.device
- return objectchange
- def clean(self):
- super().clean()
- # Check list of Modules that allow device field to be changed
- if (type(self) not in [InventoryItem]) and (self.pk is not None) and (self._original_device != self.device_id):
- raise ValidationError({
- "device": _("Components cannot be moved to a different device.")
- })
- @property
- def parent_object(self):
- return self.device
- class ModularComponentModel(ComponentModel):
- module = models.ForeignKey(
- to='dcim.Module',
- on_delete=models.CASCADE,
- related_name='%(class)ss',
- blank=True,
- null=True
- )
- inventory_items = GenericRelation(
- to='dcim.InventoryItem',
- content_type_field='component_type',
- object_id_field='component_id'
- )
- class Meta(ComponentModel.Meta):
- abstract = True
- class CabledObjectModel(models.Model):
- """
- An abstract model inherited by all models to which a Cable can terminate. Provides the `cable` and `cable_end`
- fields for caching cable associations, as well as `mark_connected` to designate "fake" connections.
- """
- cable = models.ForeignKey(
- to='dcim.Cable',
- on_delete=models.SET_NULL,
- related_name='+',
- blank=True,
- null=True
- )
- cable_end = models.CharField(
- verbose_name=_('cable end'),
- max_length=1,
- blank=True,
- choices=CableEndChoices
- )
- mark_connected = models.BooleanField(
- verbose_name=_('mark connected'),
- default=False,
- help_text=_('Treat as if a cable is connected')
- )
- cable_terminations = GenericRelation(
- to='dcim.CableTermination',
- content_type_field='termination_type',
- object_id_field='termination_id',
- related_query_name='%(class)s',
- )
- class Meta:
- abstract = True
- def clean(self):
- super().clean()
- if self.cable and not self.cable_end:
- raise ValidationError({
- "cable_end": _("Must specify cable end (A or B) when attaching a cable.")
- })
- if self.cable_end and not self.cable:
- raise ValidationError({
- "cable_end": _("Cable end must not be set without a cable.")
- })
- if self.mark_connected and self.cable:
- raise ValidationError({
- "mark_connected": _("Cannot mark as connected with a cable attached.")
- })
- @property
- def link(self):
- """
- Generic wrapper for a Cable, WirelessLink, or some other relation to a connected termination.
- """
- return self.cable
- @cached_property
- def link_peers(self):
- if self.cable:
- peers = self.cable.terminations.exclude(cable_end=self.cable_end).prefetch_related('termination')
- return [peer.termination for peer in peers]
- return []
- @property
- def _occupied(self):
- return bool(self.mark_connected or self.cable_id)
- @property
- def parent_object(self):
- raise NotImplementedError(
- _("{class_name} models must declare a parent_object property").format(class_name=self.__class__.__name__)
- )
- @property
- def opposite_cable_end(self):
- if not self.cable_end:
- return None
- return CableEndChoices.SIDE_A if self.cable_end == CableEndChoices.SIDE_B else CableEndChoices.SIDE_B
- class PathEndpoint(models.Model):
- """
- An abstract model inherited by any CabledObjectModel subclass which represents the end of a CablePath; specifically,
- these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, and PowerFeed.
- `_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in
- dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the
- CablePath model. `_path` should not be accessed directly; rather, use the `path` property.
- `connected_endpoints()` is a convenience method for returning the destination of the associated CablePath, if any.
- """
- _path = models.ForeignKey(
- to='dcim.CablePath',
- on_delete=models.SET_NULL,
- null=True,
- blank=True
- )
- class Meta:
- abstract = True
- def trace(self):
- origin = self
- path = []
- # Construct the complete path (including e.g. bridged interfaces)
- while origin is not None:
- if origin._path is None:
- break
- path.extend(origin._path.path_objects)
- # If the path ends at a non-connected pass-through port, pad out the link and far-end terminations
- if len(path) % 3 == 1:
- path.extend(([], []))
- # If the path ends at a site or provider network, inject a null "link" to render an attachment
- elif len(path) % 3 == 2:
- path.insert(-1, [])
- # Check for a bridged relationship to continue the trace
- destinations = origin._path.destinations
- if len(destinations) == 1:
- origin = getattr(destinations[0], 'bridge', None)
- else:
- origin = None
- # Return the path as a list of three-tuples (A termination(s), cable(s), B termination(s))
- return list(zip(*[iter(path)] * 3))
- @property
- def path(self):
- return self._path
- @cached_property
- def connected_endpoints(self):
- """
- Caching accessor for the attached CablePath's destination (if any)
- """
- return self._path.destinations if self._path else []
- #
- # Console components
- #
- class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
- """
- A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=ConsolePortTypeChoices,
- blank=True,
- help_text=_('Physical port type')
- )
- speed = models.PositiveIntegerField(
- verbose_name=_('speed'),
- choices=ConsolePortSpeedChoices,
- blank=True,
- null=True,
- help_text=_('Port speed in bits per second')
- )
- clone_fields = ('device', 'module', 'type', 'speed')
- class Meta(ModularComponentModel.Meta):
- verbose_name = _('console port')
- verbose_name_plural = _('console ports')
- def get_absolute_url(self):
- return reverse('dcim:consoleport', kwargs={'pk': self.pk})
- class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
- """
- A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=ConsolePortTypeChoices,
- blank=True,
- help_text=_('Physical port type')
- )
- speed = models.PositiveIntegerField(
- verbose_name=_('speed'),
- choices=ConsolePortSpeedChoices,
- blank=True,
- null=True,
- help_text=_('Port speed in bits per second')
- )
- clone_fields = ('device', 'module', 'type', 'speed')
- class Meta(ModularComponentModel.Meta):
- verbose_name = _('console server port')
- verbose_name_plural = _('console server ports')
- def get_absolute_url(self):
- return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
- #
- # Power components
- #
- class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
- """
- A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=PowerPortTypeChoices,
- blank=True,
- help_text=_('Physical port type')
- )
- maximum_draw = models.PositiveIntegerField(
- verbose_name=_('maximum draw'),
- blank=True,
- null=True,
- validators=[MinValueValidator(1)],
- help_text=_("Maximum power draw (watts)")
- )
- allocated_draw = models.PositiveIntegerField(
- verbose_name=_('allocated draw'),
- blank=True,
- null=True,
- validators=[MinValueValidator(1)],
- help_text=_('Allocated power draw (watts)')
- )
- clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
- class Meta(ModularComponentModel.Meta):
- verbose_name = _('power port')
- verbose_name_plural = _('power ports')
- def get_absolute_url(self):
- return reverse('dcim:powerport', kwargs={'pk': self.pk})
- def clean(self):
- super().clean()
- if self.maximum_draw is not None and self.allocated_draw is not None:
- if self.allocated_draw > self.maximum_draw:
- raise ValidationError({
- 'allocated_draw': _(
- "Allocated draw cannot exceed the maximum draw ({maximum_draw}W)."
- ).format(maximum_draw=self.maximum_draw)
- })
- def get_downstream_powerports(self, leg=None):
- """
- Return a queryset of all PowerPorts connected via cable to a child PowerOutlet. For example, in the topology
- below, PP1.get_downstream_powerports() would return PP2-4.
- ---- PO1 <---> PP2
- /
- PP1 ------- PO2 <---> PP3
- \
- ---- PO3 <---> PP4
- """
- poweroutlets = self.poweroutlets.filter(cable__isnull=False)
- if leg:
- poweroutlets = poweroutlets.filter(feed_leg=leg)
- if not poweroutlets:
- return PowerPort.objects.none()
- q = Q()
- for poweroutlet in poweroutlets:
- q |= Q(
- cable=poweroutlet.cable,
- cable_end=poweroutlet.opposite_cable_end
- )
- return PowerPort.objects.filter(q)
- def get_power_draw(self):
- """
- Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort.
- """
- from dcim.models import PowerFeed
- # Calculate aggregate draw of all child power outlets if no numbers have been defined manually
- if self.allocated_draw is None and self.maximum_draw is None:
- utilization = self.get_downstream_powerports().aggregate(
- maximum_draw_total=Sum('maximum_draw'),
- allocated_draw_total=Sum('allocated_draw'),
- )
- ret = {
- 'allocated': utilization['allocated_draw_total'] or 0,
- 'maximum': utilization['maximum_draw_total'] or 0,
- 'outlet_count': self.poweroutlets.count(),
- 'legs': [],
- }
- # Calculate per-leg aggregates for three-phase power feeds
- if len(self.link_peers) == 1 and isinstance(self.link_peers[0], PowerFeed) and \
- self.link_peers[0].phase == PowerFeedPhaseChoices.PHASE_3PHASE:
- for leg, leg_name in PowerOutletFeedLegChoices:
- utilization = self.get_downstream_powerports(leg=leg).aggregate(
- maximum_draw_total=Sum('maximum_draw'),
- allocated_draw_total=Sum('allocated_draw'),
- )
- ret['legs'].append({
- 'name': leg_name,
- 'allocated': utilization['allocated_draw_total'] or 0,
- 'maximum': utilization['maximum_draw_total'] or 0,
- 'outlet_count': self.poweroutlets.filter(feed_leg=leg).count(),
- })
- return ret
- # Default to administratively defined values
- return {
- 'allocated': self.allocated_draw or 0,
- 'maximum': self.maximum_draw or 0,
- 'outlet_count': self.poweroutlets.count(),
- 'legs': [],
- }
- class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
- """
- A physical power outlet (output) within a Device which provides power to a PowerPort.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=PowerOutletTypeChoices,
- blank=True,
- help_text=_('Physical port type')
- )
- power_port = models.ForeignKey(
- to='dcim.PowerPort',
- on_delete=models.SET_NULL,
- blank=True,
- null=True,
- related_name='poweroutlets'
- )
- feed_leg = models.CharField(
- verbose_name=_('feed leg'),
- max_length=50,
- choices=PowerOutletFeedLegChoices,
- blank=True,
- help_text=_('Phase (for three-phase feeds)')
- )
- clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
- class Meta(ModularComponentModel.Meta):
- verbose_name = _('power outlet')
- verbose_name_plural = _('power outlets')
- def get_absolute_url(self):
- return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
- def clean(self):
- super().clean()
- # Validate power port assignment
- if self.power_port and self.power_port.device != self.device:
- raise ValidationError(
- _("Parent power port ({power_port}) must belong to the same device").format(power_port=self.power_port)
- )
- #
- # Interfaces
- #
- class BaseInterface(models.Model):
- """
- Abstract base class for fields shared by dcim.Interface and virtualization.VMInterface.
- """
- enabled = models.BooleanField(
- verbose_name=_('enabled'),
- default=True
- )
- mac_address = MACAddressField(
- null=True,
- blank=True,
- verbose_name=_('MAC address')
- )
- mtu = models.PositiveIntegerField(
- blank=True,
- null=True,
- validators=[
- MinValueValidator(INTERFACE_MTU_MIN),
- MaxValueValidator(INTERFACE_MTU_MAX)
- ],
- verbose_name=_('MTU')
- )
- mode = models.CharField(
- verbose_name=_('mode'),
- max_length=50,
- choices=InterfaceModeChoices,
- blank=True,
- help_text=_('IEEE 802.1Q tagging strategy')
- )
- parent = models.ForeignKey(
- to='self',
- on_delete=models.SET_NULL,
- related_name='child_interfaces',
- null=True,
- blank=True,
- verbose_name=_('parent interface')
- )
- bridge = models.ForeignKey(
- to='self',
- on_delete=models.SET_NULL,
- related_name='bridge_interfaces',
- null=True,
- blank=True,
- verbose_name=_('bridge interface')
- )
- class Meta:
- abstract = True
- def save(self, *args, **kwargs):
- # Remove untagged VLAN assignment for non-802.1Q interfaces
- if not self.mode:
- 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 != InterfaceModeChoices.MODE_TAGGED:
- self.tagged_vlans.clear()
- return super().save(*args, **kwargs)
- @property
- def count_ipaddresses(self):
- return self.ip_addresses.count()
- @property
- def count_fhrp_groups(self):
- return self.fhrp_group_assignments.count()
- class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
- """
- A network interface within a Device. A physical Interface can connect to exactly one other Interface.
- """
- # Override ComponentModel._name to specify naturalize_interface function
- _name = NaturalOrderingField(
- target_field='name',
- naturalize_function=naturalize_interface,
- max_length=100,
- blank=True
- )
- vdcs = models.ManyToManyField(
- to='dcim.VirtualDeviceContext',
- related_name='interfaces'
- )
- lag = models.ForeignKey(
- to='self',
- on_delete=models.SET_NULL,
- related_name='member_interfaces',
- null=True,
- blank=True,
- verbose_name=_('parent LAG')
- )
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=InterfaceTypeChoices
- )
- mgmt_only = models.BooleanField(
- default=False,
- verbose_name=_('management only'),
- help_text=_('This interface is used only for out-of-band management')
- )
- speed = models.PositiveIntegerField(
- blank=True,
- null=True,
- verbose_name=_('speed (Kbps)')
- )
- duplex = models.CharField(
- verbose_name=_('duplex'),
- max_length=50,
- blank=True,
- null=True,
- choices=InterfaceDuplexChoices
- )
- wwn = WWNField(
- null=True,
- blank=True,
- verbose_name=_('WWN'),
- help_text=_('64-bit World Wide Name')
- )
- rf_role = models.CharField(
- max_length=30,
- choices=WirelessRoleChoices,
- blank=True,
- verbose_name=_('wireless role')
- )
- rf_channel = models.CharField(
- max_length=50,
- choices=WirelessChannelChoices,
- blank=True,
- verbose_name=_('wireless channel')
- )
- rf_channel_frequency = models.DecimalField(
- max_digits=7,
- decimal_places=2,
- blank=True,
- null=True,
- verbose_name=_('channel frequency (MHz)'),
- help_text=_("Populated by selected channel (if set)")
- )
- rf_channel_width = models.DecimalField(
- max_digits=7,
- decimal_places=3,
- blank=True,
- null=True,
- verbose_name=('channel width (MHz)'),
- help_text=_("Populated by selected channel (if set)")
- )
- tx_power = models.PositiveSmallIntegerField(
- blank=True,
- null=True,
- validators=(MaxValueValidator(127),),
- verbose_name=_('transmit power (dBm)')
- )
- poe_mode = models.CharField(
- max_length=50,
- choices=InterfacePoEModeChoices,
- blank=True,
- verbose_name=_('PoE mode')
- )
- poe_type = models.CharField(
- max_length=50,
- choices=InterfacePoETypeChoices,
- blank=True,
- verbose_name=_('PoE type')
- )
- wireless_link = models.ForeignKey(
- to='wireless.WirelessLink',
- on_delete=models.SET_NULL,
- related_name='+',
- blank=True,
- null=True
- )
- wireless_lans = models.ManyToManyField(
- to='wireless.WirelessLAN',
- related_name='interfaces',
- blank=True,
- verbose_name=_('wireless LANs')
- )
- 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')
- )
- vrf = models.ForeignKey(
- to='ipam.VRF',
- on_delete=models.SET_NULL,
- related_name='interfaces',
- null=True,
- blank=True,
- verbose_name=_('VRF')
- )
- ip_addresses = GenericRelation(
- to='ipam.IPAddress',
- content_type_field='assigned_object_type',
- object_id_field='assigned_object_id',
- related_query_name='interface'
- )
- fhrp_group_assignments = GenericRelation(
- to='ipam.FHRPGroupAssignment',
- content_type_field='interface_type',
- object_id_field='interface_id',
- related_query_name='+'
- )
- l2vpn_terminations = GenericRelation(
- to='ipam.L2VPNTermination',
- content_type_field='assigned_object_type',
- object_id_field='assigned_object_id',
- related_query_name='interface',
- )
- clone_fields = (
- 'device', 'module', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'mtu', 'mode', 'speed', 'duplex', 'rf_role',
- 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'poe_mode', 'poe_type', 'vrf',
- )
- class Meta(ModularComponentModel.Meta):
- ordering = ('device', CollateAsChar('_name'))
- verbose_name = _('interface')
- verbose_name_plural = _('interfaces')
- def get_absolute_url(self):
- return reverse('dcim:interface', kwargs={'pk': self.pk})
- def clean(self):
- super().clean()
- # Virtual Interfaces cannot have a Cable attached
- if self.is_virtual and self.cable:
- raise ValidationError({
- 'type': _("{display_type} interfaces cannot have a cable attached.").format(
- display_type=self.get_type_display()
- )
- })
- # Virtual Interfaces cannot be marked as connected
- if self.is_virtual and self.mark_connected:
- raise ValidationError({
- 'mark_connected': _("{display_type} interfaces cannot be marked as connected.".format(
- display_type=self.get_type_display())
- )
- })
- # Parent validation
- # An interface cannot be its own parent
- if self.pk and self.parent_id == self.pk:
- raise ValidationError({'parent': _("An interface cannot be its own parent.")})
- # A physical interface cannot have a parent interface
- if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
- raise ValidationError({'parent': _("Only virtual interfaces may be assigned to a parent interface.")})
- # An interface's parent must belong to the same device or virtual chassis
- if self.parent and self.parent.device != self.device:
- if self.device.virtual_chassis is None:
- raise ValidationError({
- 'parent': _(
- "The selected parent interface ({interface}) belongs to a different device ({device})"
- ).format(interface=self.parent, device=self.parent.device)
- })
- elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
- raise ValidationError({
- 'parent': _(
- "The selected parent interface ({interface}) belongs to {device}, which is not part of "
- "virtual chassis {virtual_chassis}."
- ).format(
- interface=self.parent,
- device=self.parent_device,
- virtual_chassis=self.device.virtual_chassis
- )
- })
- # Bridge validation
- # An interface cannot be bridged to itself
- if self.pk and self.bridge_id == self.pk:
- raise ValidationError({'bridge': _("An interface cannot be bridged to itself.")})
- # A bridged interface belong to the same device or virtual chassis
- if self.bridge and self.bridge.device != self.device:
- if self.device.virtual_chassis is None:
- raise ValidationError({
- 'bridge': _(
- "The selected bridge interface ({bridge}) belongs to a different device ({device})."
- ).format(bridge=self.bridge, device=self.bridge.device)
- })
- elif self.bridge.device.virtual_chassis != self.device.virtual_chassis:
- raise ValidationError({
- 'bridge': _(
- "The selected bridge interface ({interface}) belongs to {device}, which is not part of virtual "
- "chassis {virtual_chassis}."
- ).format(
- interface=self.bridge, device=self.bridge.device, virtual_chassis=self.device.virtual_chassis
- )
- })
- # LAG validation
- # A virtual interface cannot have a parent LAG
- if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None:
- raise ValidationError({'lag': _("Virtual interfaces cannot have a parent LAG interface.")})
- # A LAG interface cannot be its own parent
- if self.pk and self.lag_id == self.pk:
- raise ValidationError({'lag': _("A LAG interface cannot be its own parent.")})
- # An interface's LAG must belong to the same device or virtual chassis
- if self.lag and self.lag.device != self.device:
- if self.device.virtual_chassis is None:
- raise ValidationError({
- 'lag': _(
- "The selected LAG interface ({lag}) belongs to a different device ({device})."
- ).format(lag=self.lag, device=self.lag.device)
- })
- elif self.lag.device.virtual_chassis != self.device.virtual_chassis:
- raise ValidationError({
- 'lag': _(
- "The selected LAG interface ({lag}) belongs to {device}, which is not part of virtual chassis "
- "{virtual_chassis}.".format(
- lag=self.lag, device=self.lag.device, virtual_chassis=self.device.virtual_chassis)
- )
- })
- # PoE validation
- # Only physical interfaces may have a PoE mode/type assigned
- if self.poe_mode and self.is_virtual:
- raise ValidationError({
- 'poe_mode': _("Virtual interfaces cannot have a PoE mode.")
- })
- if self.poe_type and self.is_virtual:
- raise ValidationError({
- 'poe_type': _("Virtual interfaces cannot have a PoE type.")
- })
- # An interface with a PoE type set must also specify a mode
- if self.poe_type and not self.poe_mode:
- raise ValidationError({
- 'poe_type': _("Must specify PoE mode when designating a PoE type.")
- })
- # Wireless validation
- # RF role & channel may only be set for wireless interfaces
- if self.rf_role and not self.is_wireless:
- raise ValidationError({'rf_role': _("Wireless role may be set only on wireless interfaces.")})
- if self.rf_channel and not self.is_wireless:
- raise ValidationError({'rf_channel': _("Channel may be set only on wireless interfaces.")})
- # Validate channel frequency against interface type and selected channel (if any)
- if self.rf_channel_frequency:
- if not self.is_wireless:
- raise ValidationError({
- 'rf_channel_frequency': _("Channel frequency may be set only on wireless interfaces."),
- })
- if self.rf_channel and self.rf_channel_frequency != get_channel_attr(self.rf_channel, 'frequency'):
- raise ValidationError({
- 'rf_channel_frequency': _("Cannot specify custom frequency with channel selected."),
- })
- # Validate channel width against interface type and selected channel (if any)
- if self.rf_channel_width:
- if not self.is_wireless:
- raise ValidationError({'rf_channel_width': _("Channel width may be set only on wireless interfaces.")})
- if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'):
- raise ValidationError({'rf_channel_width': _("Cannot specify custom width with channel selected.")})
- # VLAN validation
- # Validate untagged VLAN
- if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
- raise ValidationError({
- 'untagged_vlan': _(
- "The untagged VLAN ({untagged_vlan}) must belong to the same site as the interface's parent "
- "device, or it must be global."
- ).format(untagged_vlan=self.untagged_vlan)
- })
- def save(self, *args, **kwargs):
- # Set absolute channel attributes from selected options
- if self.rf_channel and not self.rf_channel_frequency:
- self.rf_channel_frequency = get_channel_attr(self.rf_channel, 'frequency')
- if self.rf_channel and not self.rf_channel_width:
- self.rf_channel_width = get_channel_attr(self.rf_channel, 'width')
- super().save(*args, **kwargs)
- @property
- def _occupied(self):
- return super()._occupied or bool(self.wireless_link_id)
- @property
- def is_wired(self):
- return not self.is_virtual and not self.is_wireless
- @property
- def is_virtual(self):
- return self.type in VIRTUAL_IFACE_TYPES
- @property
- def is_wireless(self):
- return self.type in WIRELESS_IFACE_TYPES
- @property
- def is_lag(self):
- return self.type == InterfaceTypeChoices.TYPE_LAG
- @property
- def is_bridge(self):
- return self.type == InterfaceTypeChoices.TYPE_BRIDGE
- @property
- def link(self):
- return self.cable or self.wireless_link
- @cached_property
- def link_peers(self):
- if self.cable:
- return super().link_peers
- if self.wireless_link:
- # Return the opposite side of the attached wireless link
- if self.wireless_link.interface_a == self:
- return [self.wireless_link.interface_b]
- else:
- return [self.wireless_link.interface_a]
- return []
- @property
- def l2vpn_termination(self):
- return self.l2vpn_terminations.first()
- #
- # Pass-through ports
- #
- class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
- """
- A pass-through port on the front of a Device.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=PortTypeChoices
- )
- color = ColorField(
- verbose_name=_('color'),
- blank=True
- )
- rear_port = models.ForeignKey(
- to='dcim.RearPort',
- on_delete=models.CASCADE,
- related_name='frontports'
- )
- rear_port_position = models.PositiveSmallIntegerField(
- verbose_name=_('rear port position'),
- default=1,
- validators=[
- MinValueValidator(REARPORT_POSITIONS_MIN),
- MaxValueValidator(REARPORT_POSITIONS_MAX)
- ],
- help_text=_('Mapped position on corresponding rear port')
- )
- clone_fields = ('device', 'type', 'color')
- class Meta(ModularComponentModel.Meta):
- constraints = (
- models.UniqueConstraint(
- fields=('device', 'name'),
- name='%(app_label)s_%(class)s_unique_device_name'
- ),
- models.UniqueConstraint(
- fields=('rear_port', 'rear_port_position'),
- name='%(app_label)s_%(class)s_unique_rear_port_position'
- ),
- )
- verbose_name = _('front port')
- verbose_name_plural = _('front ports')
- def get_absolute_url(self):
- return reverse('dcim:frontport', kwargs={'pk': self.pk})
- def clean(self):
- super().clean()
- if hasattr(self, 'rear_port'):
- # Validate rear port assignment
- if self.rear_port.device != self.device:
- raise ValidationError({
- "rear_port": _(
- "Rear port ({rear_port}) must belong to the same device"
- ).format(rear_port=self.rear_port)
- })
- # Validate rear port position assignment
- if self.rear_port_position > self.rear_port.positions:
- raise ValidationError({
- "rear_port_position": _(
- "Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
- "positions."
- ).format(
- rear_port_position=self.rear_port_position,
- name=self.rear_port.name,
- positions=self.rear_port.positions
- )
- })
- class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
- """
- A pass-through port on the rear of a Device.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=PortTypeChoices
- )
- color = ColorField(
- verbose_name=_('color'),
- blank=True
- )
- positions = models.PositiveSmallIntegerField(
- verbose_name=_('positions'),
- default=1,
- validators=[
- MinValueValidator(REARPORT_POSITIONS_MIN),
- MaxValueValidator(REARPORT_POSITIONS_MAX)
- ],
- help_text=_('Number of front ports which may be mapped')
- )
- clone_fields = ('device', 'type', 'color', 'positions')
- class Meta(ModularComponentModel.Meta):
- verbose_name = _('rear port')
- verbose_name_plural = _('rear ports')
- def get_absolute_url(self):
- return reverse('dcim:rearport', kwargs={'pk': self.pk})
- def clean(self):
- super().clean()
- # Check that positions count is greater than or equal to the number of associated FrontPorts
- if self.pk:
- frontport_count = self.frontports.count()
- if self.positions < frontport_count:
- raise ValidationError({
- "positions": _(
- "The number of positions cannot be less than the number of mapped front ports "
- "({frontport_count})"
- ).format(frontport_count=frontport_count)
- })
- #
- # Bays
- #
- class ModuleBay(ComponentModel, TrackingModelMixin):
- """
- An empty space within a Device which can house a child device
- """
- position = models.CharField(
- verbose_name=_('position'),
- max_length=30,
- blank=True,
- help_text=_('Identifier to reference when renaming installed components')
- )
- clone_fields = ('device',)
- class Meta(ComponentModel.Meta):
- verbose_name = _('module bay')
- verbose_name_plural = _('module bays')
- def get_absolute_url(self):
- return reverse('dcim:modulebay', kwargs={'pk': self.pk})
- class DeviceBay(ComponentModel, TrackingModelMixin):
- """
- An empty space within a Device which can house a child device
- """
- installed_device = models.OneToOneField(
- to='dcim.Device',
- on_delete=models.SET_NULL,
- related_name=_('parent_bay'),
- blank=True,
- null=True
- )
- clone_fields = ('device',)
- class Meta(ComponentModel.Meta):
- verbose_name = _('device bay')
- verbose_name_plural = _('device bays')
- def get_absolute_url(self):
- return reverse('dcim:devicebay', kwargs={'pk': self.pk})
- def clean(self):
- super().clean()
- # Validate that the parent Device can have DeviceBays
- if not self.device.device_type.is_parent_device:
- raise ValidationError(_("This type of device ({device_type}) does not support device bays.").format(
- device_type=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."))
- # Check that the installed device is not already installed elsewhere
- if self.installed_device:
- current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
- if current_bay and current_bay != self:
- raise ValidationError({
- 'installed_device': _(
- "Cannot install the specified device; device is already installed in {bay}."
- ).format(bay=current_bay)
- })
- #
- # Inventory items
- #
- class InventoryItemRole(OrganizationalModel):
- """
- Inventory items may optionally be assigned a functional role.
- """
- color = ColorField(
- verbose_name=_('color'),
- default=ColorChoices.COLOR_GREY
- )
- class Meta:
- ordering = ('name',)
- verbose_name = _('inventory item role')
- verbose_name_plural = _('inventory item roles')
- def get_absolute_url(self):
- return reverse('dcim:inventoryitemrole', args=[self.pk])
- class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
- """
- 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.
- """
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='child_items',
- blank=True,
- null=True,
- db_index=True
- )
- component_type = models.ForeignKey(
- to=ContentType,
- limit_choices_to=MODULAR_COMPONENT_MODELS,
- on_delete=models.PROTECT,
- related_name='+',
- blank=True,
- null=True
- )
- component_id = models.PositiveBigIntegerField(
- blank=True,
- null=True
- )
- component = GenericForeignKey(
- ct_field='component_type',
- fk_field='component_id'
- )
- role = models.ForeignKey(
- to='dcim.InventoryItemRole',
- on_delete=models.PROTECT,
- related_name='inventory_items',
- blank=True,
- null=True
- )
- 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,
- help_text=_('Manufacturer-assigned part identifier')
- )
- 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(
- verbose_name=_('discovered'),
- default=False,
- help_text=_('This item was automatically discovered')
- )
- objects = TreeManager()
- clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',)
- class Meta:
- ordering = ('device__id', 'parent__id', '_name')
- constraints = (
- models.UniqueConstraint(
- fields=('device', 'parent', 'name'),
- name='%(app_label)s_%(class)s_unique_device_parent_name'
- ),
- )
- verbose_name = _('inventory item')
- verbose_name_plural = _('inventory items')
- def get_absolute_url(self):
- return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
- def clean(self):
- super().clean()
- # An InventoryItem cannot be its own parent
- if self.pk and self.parent_id == self.pk:
- raise ValidationError({
- "parent": _("Cannot assign self as parent.")
- })
- # Validation for moving InventoryItems
- if self.pk:
- # Cannot move an InventoryItem to another device if it has a parent
- if self.parent and self.parent.device != self.device:
- raise ValidationError({
- "parent": _("Parent inventory item does not belong to the same device.")
- })
- # Prevent moving InventoryItems with children
- first_child = self.get_children().first()
- if first_child and first_child.device != self.device:
- raise ValidationError(_("Cannot move an inventory item with dependent children"))
- # When moving an InventoryItem to another device, remove any associated component
- if self.component and self.component.device != self.device:
- self.component = None
- else:
- if self.component and self.component.device != self.device:
- raise ValidationError({
- "device": _("Cannot assign inventory item to component on another device")
- })
|