| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335 |
- from functools import cached_property
- from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
- 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.choices import ColorChoices
- from netbox.models import OrganizationalModel, NetBoxModel
- 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.RESTRICT,
- 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 not self._state.adding and self.mode != InterfaceModeChoices.MODE_TAGGED:
- self.tagged_vlans.clear()
- return super().save(*args, **kwargs)
- @property
- def tunnel_termination(self):
- return self.tunnel_terminations.first()
- @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='+'
- )
- tunnel_terminations = GenericRelation(
- to='vpn.TunnelTermination',
- content_type_field='termination_type',
- object_id_field='termination_id',
- related_query_name='interface'
- )
- l2vpn_terminations = GenericRelation(
- to='vpn.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 not self._state.adding:
- 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(ModularComponentModel, TrackingModelMixin, MPTTModel):
- """
- An empty space within a Device which can house a child device
- """
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='children',
- blank=True,
- null=True,
- editable=False,
- db_index=True
- )
- position = models.CharField(
- verbose_name=_('position'),
- max_length=30,
- blank=True,
- help_text=_('Identifier to reference when renaming installed components')
- )
- objects = TreeManager()
- clone_fields = ('device',)
- class Meta(ModularComponentModel.Meta):
- constraints = (
- models.UniqueConstraint(
- fields=('device', 'module', 'name'),
- name='%(app_label)s_%(class)s_unique_device_module_name'
- ),
- )
- verbose_name = _('module bay')
- verbose_name_plural = _('module bays')
- class MPTTMeta:
- order_insertion_by = ('module',)
- def get_absolute_url(self):
- return reverse('dcim:modulebay', kwargs={'pk': self.pk})
- def clean(self):
- super().clean()
- # Check for recursion
- if module := self.module:
- module_bays = [self.pk]
- modules = []
- while module:
- if module.pk in modules or module.module_bay.pk in module_bays:
- raise ValidationError(_("A module bay cannot belong to a module installed within it."))
- modules.append(module.pk)
- module_bays.append(module.module_bay.pk)
- module = module.module_bay.module if module.module_bay else None
- def save(self, *args, **kwargs):
- if self.module:
- self.parent = self.module.module_bay
- super().save(*args, **kwargs)
- 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 hasattr(self, 'device') and 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.installed_device and getattr(self, 'device', None) == 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='contenttypes.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')
- indexes = (
- models.Index(fields=('component_type', 'component_id')),
- )
- 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 not self._state.adding:
- # 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")
- })
|