|
|
@@ -17,173 +17,59 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|
|
from taggit.managers import TaggableManager
|
|
|
from timezone_field import TimeZoneField
|
|
|
|
|
|
-from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
|
|
|
+from dcim.choices import *
|
|
|
+from dcim.constants import *
|
|
|
+from dcim.fields import ASNField
|
|
|
+from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
|
|
|
from utilities.fields import ColorField
|
|
|
from utilities.managers import NaturalOrderingManager
|
|
|
from utilities.models import ChangeLoggedModel
|
|
|
-from utilities.utils import foreground_color, serialize_object, to_meters
|
|
|
-from virtualization.choices import VMInterfaceTypeChoices
|
|
|
-
|
|
|
-from .choices import *
|
|
|
-from .constants import *
|
|
|
-from .exceptions import LoopDetected
|
|
|
-from .fields import ASNField, MACAddressField
|
|
|
-from .managers import InterfaceManager
|
|
|
-
|
|
|
-
|
|
|
-class ComponentTemplateModel(models.Model):
|
|
|
-
|
|
|
- class Meta:
|
|
|
- abstract = True
|
|
|
-
|
|
|
- def instantiate(self, device):
|
|
|
- """
|
|
|
- Instantiate a new component on the specified Device.
|
|
|
- """
|
|
|
- raise NotImplementedError()
|
|
|
-
|
|
|
- def to_objectchange(self, action):
|
|
|
- return ObjectChange(
|
|
|
- changed_object=self,
|
|
|
- object_repr=str(self),
|
|
|
- action=action,
|
|
|
- related_object=self.device_type,
|
|
|
- object_data=serialize_object(self)
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-class ComponentModel(models.Model):
|
|
|
- description = models.CharField(
|
|
|
- max_length=100,
|
|
|
- blank=True
|
|
|
- )
|
|
|
-
|
|
|
- class Meta:
|
|
|
- abstract = True
|
|
|
-
|
|
|
- def to_objectchange(self, action):
|
|
|
- # Annotate 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
|
|
|
-
|
|
|
- return ObjectChange(
|
|
|
- changed_object=self,
|
|
|
- object_repr=str(self),
|
|
|
- action=action,
|
|
|
- related_object=parent,
|
|
|
- object_data=serialize_object(self)
|
|
|
- )
|
|
|
-
|
|
|
- @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'
|
|
|
- )
|
|
|
-
|
|
|
- is_path_endpoint = True
|
|
|
-
|
|
|
- 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
|
|
|
+from utilities.utils import foreground_color, to_meters
|
|
|
+from .device_components import (
|
|
|
+ CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet,
|
|
|
+ PowerPort, RearPort,
|
|
|
+)
|
|
|
+from .device_component_templates import (
|
|
|
+ ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
|
|
|
+ PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+__all__ = (
|
|
|
+ 'Cable',
|
|
|
+ 'CableTermination',
|
|
|
+ 'ConsolePort',
|
|
|
+ 'ConsolePortTemplate',
|
|
|
+ 'ConsoleServerPort',
|
|
|
+ 'ConsoleServerPortTemplate',
|
|
|
+ 'Device',
|
|
|
+ 'DeviceBay',
|
|
|
+ 'DeviceBayTemplate',
|
|
|
+ 'DeviceRole',
|
|
|
+ 'DeviceType',
|
|
|
+ 'FrontPort',
|
|
|
+ 'FrontPortTemplate',
|
|
|
+ 'Interface',
|
|
|
+ 'InterfaceTemplate',
|
|
|
+ 'InventoryItem',
|
|
|
+ 'Manufacturer',
|
|
|
+ 'Platform',
|
|
|
+ 'PowerFeed',
|
|
|
+ 'PowerOutlet',
|
|
|
+ 'PowerOutletTemplate',
|
|
|
+ 'PowerPanel',
|
|
|
+ 'PowerPort',
|
|
|
+ 'PowerPortTemplate',
|
|
|
+ 'Rack',
|
|
|
+ 'RackGroup',
|
|
|
+ 'RackReservation',
|
|
|
+ 'RackRole',
|
|
|
+ 'RearPort',
|
|
|
+ 'RearPortTemplate',
|
|
|
+ 'Region',
|
|
|
+ 'Site',
|
|
|
+ 'VirtualChassis',
|
|
|
+)
|
|
|
|
|
|
|
|
|
#
|
|
|
@@ -1171,1737 +1057,532 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
|
|
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
|
|
|
|
|
|
|
|
|
-class ConsolePortTemplate(ComponentTemplateModel):
|
|
|
+#
|
|
|
+# Devices
|
|
|
+#
|
|
|
+
|
|
|
+class DeviceRole(ChangeLoggedModel):
|
|
|
"""
|
|
|
- A template for a ConsolePort to be created for a new Device.
|
|
|
+ Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
|
|
+ color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
|
|
|
+ virtual machines as well.
|
|
|
"""
|
|
|
- device_type = models.ForeignKey(
|
|
|
- to='dcim.DeviceType',
|
|
|
- on_delete=models.CASCADE,
|
|
|
- related_name='consoleport_templates'
|
|
|
- )
|
|
|
name = models.CharField(
|
|
|
- max_length=50
|
|
|
- )
|
|
|
- type = models.CharField(
|
|
|
max_length=50,
|
|
|
- choices=ConsolePortTypeChoices,
|
|
|
- blank=True
|
|
|
+ unique=True
|
|
|
)
|
|
|
-
|
|
|
- objects = NaturalOrderingManager()
|
|
|
-
|
|
|
- class Meta:
|
|
|
- ordering = ['device_type', 'name']
|
|
|
- unique_together = ['device_type', 'name']
|
|
|
-
|
|
|
- def __str__(self):
|
|
|
- return self.name
|
|
|
-
|
|
|
- def instantiate(self, device):
|
|
|
- return ConsolePort(
|
|
|
- device=device,
|
|
|
- name=self.name,
|
|
|
- type=self.type
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-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'
|
|
|
+ slug = models.SlugField(
|
|
|
+ unique=True
|
|
|
)
|
|
|
- name = models.CharField(
|
|
|
- max_length=50
|
|
|
+ color = ColorField()
|
|
|
+ vm_role = models.BooleanField(
|
|
|
+ default=True,
|
|
|
+ verbose_name='VM Role',
|
|
|
+ help_text='Virtual machines may be assigned to this role'
|
|
|
)
|
|
|
- type = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=ConsolePortTypeChoices,
|
|
|
- blank=True
|
|
|
+ description = models.CharField(
|
|
|
+ max_length=100,
|
|
|
+ blank=True,
|
|
|
)
|
|
|
|
|
|
- objects = NaturalOrderingManager()
|
|
|
+ csv_headers = ['name', 'slug', 'color', 'vm_role', 'description']
|
|
|
|
|
|
class Meta:
|
|
|
- ordering = ['device_type', 'name']
|
|
|
- unique_together = ['device_type', 'name']
|
|
|
+ ordering = ['name']
|
|
|
|
|
|
def __str__(self):
|
|
|
return self.name
|
|
|
|
|
|
- def instantiate(self, device):
|
|
|
- return ConsoleServerPort(
|
|
|
- device=device,
|
|
|
- name=self.name,
|
|
|
- type=self.type
|
|
|
+ def to_csv(self):
|
|
|
+ return (
|
|
|
+ self.name,
|
|
|
+ self.slug,
|
|
|
+ self.color,
|
|
|
+ self.vm_role,
|
|
|
+ self.description,
|
|
|
)
|
|
|
|
|
|
|
|
|
-class PowerPortTemplate(ComponentTemplateModel):
|
|
|
+class Platform(ChangeLoggedModel):
|
|
|
"""
|
|
|
- A template for a PowerPort to be created for a new Device.
|
|
|
+ Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
|
|
+ NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
|
|
|
+ specifying a NAPALM driver.
|
|
|
"""
|
|
|
- device_type = models.ForeignKey(
|
|
|
- to='dcim.DeviceType',
|
|
|
- on_delete=models.CASCADE,
|
|
|
- related_name='powerport_templates'
|
|
|
- )
|
|
|
name = models.CharField(
|
|
|
- max_length=50
|
|
|
+ max_length=100,
|
|
|
+ unique=True
|
|
|
)
|
|
|
- type = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=PowerPortTypeChoices,
|
|
|
- blank=True
|
|
|
+ slug = models.SlugField(
|
|
|
+ unique=True,
|
|
|
+ max_length=100
|
|
|
)
|
|
|
- maximum_draw = models.PositiveSmallIntegerField(
|
|
|
+ manufacturer = models.ForeignKey(
|
|
|
+ to='dcim.Manufacturer',
|
|
|
+ on_delete=models.PROTECT,
|
|
|
+ related_name='platforms',
|
|
|
blank=True,
|
|
|
null=True,
|
|
|
- validators=[MinValueValidator(1)],
|
|
|
- help_text="Maximum power draw (watts)"
|
|
|
+ help_text='Optionally limit this platform to devices of a certain manufacturer'
|
|
|
+ )
|
|
|
+ napalm_driver = models.CharField(
|
|
|
+ max_length=50,
|
|
|
+ blank=True,
|
|
|
+ verbose_name='NAPALM driver',
|
|
|
+ help_text='The name of the NAPALM driver to use when interacting with devices'
|
|
|
)
|
|
|
- allocated_draw = models.PositiveSmallIntegerField(
|
|
|
+ napalm_args = JSONField(
|
|
|
blank=True,
|
|
|
null=True,
|
|
|
- validators=[MinValueValidator(1)],
|
|
|
- help_text="Allocated power draw (watts)"
|
|
|
+ verbose_name='NAPALM arguments',
|
|
|
+ help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)'
|
|
|
)
|
|
|
|
|
|
- objects = NaturalOrderingManager()
|
|
|
+ csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
|
|
|
|
|
|
class Meta:
|
|
|
- ordering = ['device_type', 'name']
|
|
|
- unique_together = ['device_type', 'name']
|
|
|
+ ordering = ['name']
|
|
|
|
|
|
def __str__(self):
|
|
|
return self.name
|
|
|
|
|
|
- def instantiate(self, device):
|
|
|
- return PowerPort(
|
|
|
- device=device,
|
|
|
- name=self.name,
|
|
|
- maximum_draw=self.maximum_draw,
|
|
|
- allocated_draw=self.allocated_draw
|
|
|
+ def get_absolute_url(self):
|
|
|
+ return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
|
|
|
+
|
|
|
+ def to_csv(self):
|
|
|
+ return (
|
|
|
+ self.name,
|
|
|
+ self.slug,
|
|
|
+ self.manufacturer.name if self.manufacturer else None,
|
|
|
+ self.napalm_driver,
|
|
|
+ self.napalm_args,
|
|
|
)
|
|
|
|
|
|
|
|
|
-class PowerOutletTemplate(ComponentTemplateModel):
|
|
|
+class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|
|
"""
|
|
|
- A template for a PowerOutlet to be created for a new Device.
|
|
|
+ A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
|
|
+ DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
|
|
|
+
|
|
|
+ Each Device must be assigned to a site, and optionally to a rack within that site. Associating a device with a
|
|
|
+ particular rack face or unit is optional (for example, vertically mounted PDUs do not consume rack units).
|
|
|
+
|
|
|
+ When a new Device is created, console/power/interface/device bay components are created along with it as dictated
|
|
|
+ by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
|
|
|
+ creation of a Device.
|
|
|
"""
|
|
|
device_type = models.ForeignKey(
|
|
|
to='dcim.DeviceType',
|
|
|
- on_delete=models.CASCADE,
|
|
|
- related_name='poweroutlet_templates'
|
|
|
+ on_delete=models.PROTECT,
|
|
|
+ related_name='instances'
|
|
|
)
|
|
|
- name = models.CharField(
|
|
|
- max_length=50
|
|
|
+ device_role = models.ForeignKey(
|
|
|
+ to='dcim.DeviceRole',
|
|
|
+ on_delete=models.PROTECT,
|
|
|
+ related_name='devices'
|
|
|
)
|
|
|
- type = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=PowerOutletTypeChoices,
|
|
|
- blank=True
|
|
|
+ tenant = models.ForeignKey(
|
|
|
+ to='tenancy.Tenant',
|
|
|
+ on_delete=models.PROTECT,
|
|
|
+ related_name='devices',
|
|
|
+ blank=True,
|
|
|
+ null=True
|
|
|
)
|
|
|
- power_port = models.ForeignKey(
|
|
|
- to='dcim.PowerPortTemplate',
|
|
|
+ platform = models.ForeignKey(
|
|
|
+ to='dcim.Platform',
|
|
|
on_delete=models.SET_NULL,
|
|
|
+ related_name='devices',
|
|
|
blank=True,
|
|
|
- null=True,
|
|
|
- related_name='poweroutlet_templates'
|
|
|
+ null=True
|
|
|
+ )
|
|
|
+ name = models.CharField(
|
|
|
+ max_length=64,
|
|
|
+ blank=True,
|
|
|
+ null=True
|
|
|
)
|
|
|
- feed_leg = models.CharField(
|
|
|
+ serial = models.CharField(
|
|
|
max_length=50,
|
|
|
- choices=PowerOutletFeedLegChoices,
|
|
|
blank=True,
|
|
|
- help_text="Phase (for three-phase feeds)"
|
|
|
+ verbose_name='Serial number'
|
|
|
)
|
|
|
-
|
|
|
- objects = NaturalOrderingManager()
|
|
|
-
|
|
|
- class Meta:
|
|
|
- ordering = ['device_type', 'name']
|
|
|
- unique_together = ['device_type', 'name']
|
|
|
-
|
|
|
- def __str__(self):
|
|
|
- return self.name
|
|
|
-
|
|
|
- def clean(self):
|
|
|
-
|
|
|
- # Validate power port assignment
|
|
|
- if self.power_port and self.power_port.device_type != self.device_type:
|
|
|
- raise ValidationError(
|
|
|
- "Parent power port ({}) must belong to the same device type".format(self.power_port)
|
|
|
- )
|
|
|
-
|
|
|
- def instantiate(self, device):
|
|
|
- if self.power_port:
|
|
|
- power_port = PowerPort.objects.get(device=device, name=self.power_port.name)
|
|
|
- else:
|
|
|
- power_port = None
|
|
|
- return PowerOutlet(
|
|
|
- device=device,
|
|
|
- name=self.name,
|
|
|
- power_port=power_port,
|
|
|
- feed_leg=self.feed_leg
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-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'
|
|
|
+ asset_tag = models.CharField(
|
|
|
+ max_length=50,
|
|
|
+ blank=True,
|
|
|
+ null=True,
|
|
|
+ unique=True,
|
|
|
+ verbose_name='Asset tag',
|
|
|
+ help_text='A unique tag used to identify this device'
|
|
|
)
|
|
|
- name = models.CharField(
|
|
|
- max_length=64
|
|
|
+ site = models.ForeignKey(
|
|
|
+ to='dcim.Site',
|
|
|
+ on_delete=models.PROTECT,
|
|
|
+ related_name='devices'
|
|
|
)
|
|
|
- type = models.CharField(
|
|
|
+ rack = models.ForeignKey(
|
|
|
+ to='dcim.Rack',
|
|
|
+ on_delete=models.PROTECT,
|
|
|
+ related_name='devices',
|
|
|
+ blank=True,
|
|
|
+ null=True
|
|
|
+ )
|
|
|
+ position = models.PositiveSmallIntegerField(
|
|
|
+ blank=True,
|
|
|
+ null=True,
|
|
|
+ validators=[MinValueValidator(1)],
|
|
|
+ verbose_name='Position (U)',
|
|
|
+ help_text='The lowest-numbered unit occupied by the device'
|
|
|
+ )
|
|
|
+ face = models.CharField(
|
|
|
max_length=50,
|
|
|
- choices=InterfaceTypeChoices
|
|
|
+ blank=True,
|
|
|
+ choices=DeviceFaceChoices,
|
|
|
+ verbose_name='Rack face'
|
|
|
)
|
|
|
- mgmt_only = models.BooleanField(
|
|
|
- default=False,
|
|
|
- verbose_name='Management only'
|
|
|
+ status = models.CharField(
|
|
|
+ max_length=50,
|
|
|
+ choices=DeviceStatusChoices,
|
|
|
+ default=DeviceStatusChoices.STATUS_ACTIVE
|
|
|
)
|
|
|
-
|
|
|
- objects = InterfaceManager()
|
|
|
-
|
|
|
- class Meta:
|
|
|
- ordering = ['device_type', 'name']
|
|
|
- unique_together = ['device_type', 'name']
|
|
|
-
|
|
|
- def __str__(self):
|
|
|
- return self.name
|
|
|
-
|
|
|
- def instantiate(self, device):
|
|
|
- return Interface(
|
|
|
- device=device,
|
|
|
- name=self.name,
|
|
|
- type=self.type,
|
|
|
- mgmt_only=self.mgmt_only
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-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'
|
|
|
+ primary_ip4 = models.OneToOneField(
|
|
|
+ to='ipam.IPAddress',
|
|
|
+ on_delete=models.SET_NULL,
|
|
|
+ related_name='primary_ip4_for',
|
|
|
+ blank=True,
|
|
|
+ null=True,
|
|
|
+ verbose_name='Primary IPv4'
|
|
|
)
|
|
|
- name = models.CharField(
|
|
|
- max_length=64
|
|
|
+ primary_ip6 = models.OneToOneField(
|
|
|
+ to='ipam.IPAddress',
|
|
|
+ on_delete=models.SET_NULL,
|
|
|
+ related_name='primary_ip6_for',
|
|
|
+ blank=True,
|
|
|
+ null=True,
|
|
|
+ verbose_name='Primary IPv6'
|
|
|
)
|
|
|
- type = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=PortTypeChoices
|
|
|
+ cluster = models.ForeignKey(
|
|
|
+ to='virtualization.Cluster',
|
|
|
+ on_delete=models.SET_NULL,
|
|
|
+ related_name='devices',
|
|
|
+ blank=True,
|
|
|
+ null=True
|
|
|
)
|
|
|
- rear_port = models.ForeignKey(
|
|
|
- to='dcim.RearPortTemplate',
|
|
|
- on_delete=models.CASCADE,
|
|
|
- related_name='frontport_templates'
|
|
|
+ virtual_chassis = models.ForeignKey(
|
|
|
+ to='VirtualChassis',
|
|
|
+ on_delete=models.SET_NULL,
|
|
|
+ related_name='members',
|
|
|
+ blank=True,
|
|
|
+ null=True
|
|
|
)
|
|
|
- rear_port_position = models.PositiveSmallIntegerField(
|
|
|
- default=1,
|
|
|
- validators=[MinValueValidator(1), MaxValueValidator(64)]
|
|
|
+ vc_position = models.PositiveSmallIntegerField(
|
|
|
+ blank=True,
|
|
|
+ null=True,
|
|
|
+ validators=[MaxValueValidator(255)]
|
|
|
+ )
|
|
|
+ vc_priority = models.PositiveSmallIntegerField(
|
|
|
+ blank=True,
|
|
|
+ null=True,
|
|
|
+ validators=[MaxValueValidator(255)]
|
|
|
+ )
|
|
|
+ comments = models.TextField(
|
|
|
+ blank=True
|
|
|
+ )
|
|
|
+ custom_field_values = GenericRelation(
|
|
|
+ to='extras.CustomFieldValue',
|
|
|
+ content_type_field='obj_type',
|
|
|
+ object_id_field='obj_id'
|
|
|
+ )
|
|
|
+ images = GenericRelation(
|
|
|
+ to='extras.ImageAttachment'
|
|
|
)
|
|
|
|
|
|
objects = NaturalOrderingManager()
|
|
|
+ tags = TaggableManager(through=TaggedItem)
|
|
|
+
|
|
|
+ csv_headers = [
|
|
|
+ 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
|
|
+ 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
|
|
|
+ ]
|
|
|
+ clone_fields = [
|
|
|
+ 'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster',
|
|
|
+ ]
|
|
|
+
|
|
|
+ STATUS_CLASS_MAP = {
|
|
|
+ DeviceStatusChoices.STATUS_OFFLINE: 'warning',
|
|
|
+ DeviceStatusChoices.STATUS_ACTIVE: 'success',
|
|
|
+ DeviceStatusChoices.STATUS_PLANNED: 'info',
|
|
|
+ DeviceStatusChoices.STATUS_STAGED: 'primary',
|
|
|
+ DeviceStatusChoices.STATUS_FAILED: 'danger',
|
|
|
+ DeviceStatusChoices.STATUS_INVENTORY: 'default',
|
|
|
+ DeviceStatusChoices.STATUS_DECOMMISSIONING: 'warning',
|
|
|
+ }
|
|
|
|
|
|
class Meta:
|
|
|
- ordering = ['device_type', 'name']
|
|
|
+ ordering = ['name']
|
|
|
unique_together = [
|
|
|
- ['device_type', 'name'],
|
|
|
- ['rear_port', 'rear_port_position'],
|
|
|
+ ['site', 'tenant', 'name'], # See validate_unique below
|
|
|
+ ['rack', 'position', 'face'],
|
|
|
+ ['virtual_chassis', 'vc_position'],
|
|
|
]
|
|
|
+ permissions = (
|
|
|
+ ('napalm_read', 'Read-only access to devices via NAPALM'),
|
|
|
+ ('napalm_write', 'Read/write access to devices via NAPALM'),
|
|
|
+ )
|
|
|
|
|
|
def __str__(self):
|
|
|
- return self.name
|
|
|
+ return self.display_name or super().__str__()
|
|
|
|
|
|
- def clean(self):
|
|
|
+ def get_absolute_url(self):
|
|
|
+ return reverse('dcim:device', args=[self.pk])
|
|
|
|
|
|
- # 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)
|
|
|
- )
|
|
|
+ def validate_unique(self, exclude=None):
|
|
|
|
|
|
- # 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
|
|
|
- )
|
|
|
- )
|
|
|
+ # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
|
|
|
+ # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
|
|
+ # of the uniqueness constraint without manual intervention.
|
|
|
+ if self.tenant is None and Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True):
|
|
|
+ raise ValidationError({
|
|
|
+ 'name': 'A device with this name already exists.'
|
|
|
+ })
|
|
|
|
|
|
- def instantiate(self, device):
|
|
|
- if self.rear_port:
|
|
|
- rear_port = RearPort.objects.get(device=device, name=self.rear_port.name)
|
|
|
- else:
|
|
|
- rear_port = None
|
|
|
- return FrontPort(
|
|
|
- device=device,
|
|
|
- name=self.name,
|
|
|
- type=self.type,
|
|
|
- rear_port=rear_port,
|
|
|
- rear_port_position=self.rear_port_position
|
|
|
- )
|
|
|
+ super().validate_unique(exclude)
|
|
|
|
|
|
+ def clean(self):
|
|
|
|
|
|
-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.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=PortTypeChoices
|
|
|
- )
|
|
|
- positions = models.PositiveSmallIntegerField(
|
|
|
- default=1,
|
|
|
- validators=[MinValueValidator(1), MaxValueValidator(64)]
|
|
|
- )
|
|
|
+ super().clean()
|
|
|
|
|
|
- objects = NaturalOrderingManager()
|
|
|
+ # Validate site/rack combination
|
|
|
+ if self.rack and self.site != self.rack.site:
|
|
|
+ raise ValidationError({
|
|
|
+ 'rack': "Rack {} does not belong to site {}.".format(self.rack, self.site),
|
|
|
+ })
|
|
|
|
|
|
- class Meta:
|
|
|
- ordering = ['device_type', 'name']
|
|
|
- unique_together = ['device_type', 'name']
|
|
|
+ if self.rack is None:
|
|
|
+ if self.face:
|
|
|
+ raise ValidationError({
|
|
|
+ 'face': "Cannot select a rack face without assigning a rack.",
|
|
|
+ })
|
|
|
+ if self.position:
|
|
|
+ raise ValidationError({
|
|
|
+ 'face': "Cannot select a rack position without assigning a rack.",
|
|
|
+ })
|
|
|
|
|
|
- def __str__(self):
|
|
|
- return self.name
|
|
|
+ # Validate position/face combination
|
|
|
+ if self.position and not self.face:
|
|
|
+ raise ValidationError({
|
|
|
+ 'face': "Must specify rack face when defining rack position.",
|
|
|
+ })
|
|
|
|
|
|
- def instantiate(self, device):
|
|
|
- return RearPort(
|
|
|
- device=device,
|
|
|
- name=self.name,
|
|
|
- type=self.type,
|
|
|
- positions=self.positions
|
|
|
- )
|
|
|
+ # Prevent 0U devices from being assigned to a specific position
|
|
|
+ if self.position and self.device_type.u_height == 0:
|
|
|
+ raise ValidationError({
|
|
|
+ 'position': "A U0 device type ({}) cannot be assigned to a rack position.".format(self.device_type)
|
|
|
+ })
|
|
|
|
|
|
+ if self.rack:
|
|
|
|
|
|
-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
|
|
|
- )
|
|
|
+ try:
|
|
|
+ # Child devices cannot be assigned to a rack face/unit
|
|
|
+ if self.device_type.is_child_device and self.face is not None:
|
|
|
+ raise ValidationError({
|
|
|
+ 'face': "Child device types cannot be assigned to a rack face. This is an attribute of the "
|
|
|
+ "parent device."
|
|
|
+ })
|
|
|
+ if self.device_type.is_child_device and self.position:
|
|
|
+ raise ValidationError({
|
|
|
+ 'position': "Child device types cannot be assigned to a rack position. This is an attribute of "
|
|
|
+ "the parent device."
|
|
|
+ })
|
|
|
|
|
|
- objects = NaturalOrderingManager()
|
|
|
+ # Validate rack space
|
|
|
+ rack_face = self.face if not self.device_type.is_full_depth else None
|
|
|
+ exclude_list = [self.pk] if self.pk else []
|
|
|
+ try:
|
|
|
+ available_units = self.rack.get_available_units(
|
|
|
+ u_height=self.device_type.u_height, rack_face=rack_face, exclude=exclude_list
|
|
|
+ )
|
|
|
+ if self.position and self.position not in available_units:
|
|
|
+ raise ValidationError({
|
|
|
+ 'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) "
|
|
|
+ "{} ({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
|
|
+ })
|
|
|
+ except Rack.DoesNotExist:
|
|
|
+ pass
|
|
|
|
|
|
- class Meta:
|
|
|
- ordering = ['device_type', 'name']
|
|
|
- unique_together = ['device_type', 'name']
|
|
|
+ except DeviceType.DoesNotExist:
|
|
|
+ pass
|
|
|
|
|
|
- def __str__(self):
|
|
|
- return self.name
|
|
|
+ # Validate primary IP addresses
|
|
|
+ vc_interfaces = self.vc_interfaces.all()
|
|
|
+ if self.primary_ip4:
|
|
|
+ if self.primary_ip4.interface in vc_interfaces:
|
|
|
+ pass
|
|
|
+ elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in vc_interfaces:
|
|
|
+ pass
|
|
|
+ else:
|
|
|
+ raise ValidationError({
|
|
|
+ 'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format(
|
|
|
+ self.primary_ip4),
|
|
|
+ })
|
|
|
+ if self.primary_ip6:
|
|
|
+ if self.primary_ip6.interface in vc_interfaces:
|
|
|
+ pass
|
|
|
+ elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.interface in vc_interfaces:
|
|
|
+ pass
|
|
|
+ else:
|
|
|
+ raise ValidationError({
|
|
|
+ 'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(
|
|
|
+ self.primary_ip6),
|
|
|
+ })
|
|
|
|
|
|
- def instantiate(self, device):
|
|
|
- return DeviceBay(
|
|
|
- device=device,
|
|
|
- name=self.name
|
|
|
- )
|
|
|
+ # Validate manufacturer/platform
|
|
|
+ if hasattr(self, 'device_type') and self.platform:
|
|
|
+ if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
|
|
|
+ raise ValidationError({
|
|
|
+ 'platform': "The assigned platform is limited to {} device types, but this device's type belongs "
|
|
|
+ "to {}.".format(self.platform.manufacturer, self.device_type.manufacturer)
|
|
|
+ })
|
|
|
|
|
|
+ # A Device can only be assigned to a Cluster in the same Site (or no Site)
|
|
|
+ if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
|
|
|
+ raise ValidationError({
|
|
|
+ 'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
|
|
|
+ })
|
|
|
|
|
|
-#
|
|
|
-# Devices
|
|
|
-#
|
|
|
+ # Validate virtual chassis assignment
|
|
|
+ if self.virtual_chassis and self.vc_position is None:
|
|
|
+ raise ValidationError({
|
|
|
+ 'vc_position': "A device assigned to a virtual chassis must have its position defined."
|
|
|
+ })
|
|
|
|
|
|
-class DeviceRole(ChangeLoggedModel):
|
|
|
- """
|
|
|
- Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
|
|
- color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
|
|
|
- virtual machines as well.
|
|
|
- """
|
|
|
- name = models.CharField(
|
|
|
- max_length=50,
|
|
|
- unique=True
|
|
|
- )
|
|
|
- slug = models.SlugField(
|
|
|
- unique=True
|
|
|
- )
|
|
|
- color = ColorField()
|
|
|
- vm_role = models.BooleanField(
|
|
|
- default=True,
|
|
|
- verbose_name='VM Role',
|
|
|
- help_text='Virtual machines may be assigned to this role'
|
|
|
- )
|
|
|
- description = models.CharField(
|
|
|
- max_length=100,
|
|
|
- blank=True,
|
|
|
- )
|
|
|
-
|
|
|
- csv_headers = ['name', 'slug', 'color', 'vm_role', 'description']
|
|
|
-
|
|
|
- class Meta:
|
|
|
- ordering = ['name']
|
|
|
-
|
|
|
- def __str__(self):
|
|
|
- return self.name
|
|
|
-
|
|
|
- def to_csv(self):
|
|
|
- return (
|
|
|
- self.name,
|
|
|
- self.slug,
|
|
|
- self.color,
|
|
|
- self.vm_role,
|
|
|
- self.description,
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-class Platform(ChangeLoggedModel):
|
|
|
- """
|
|
|
- Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
|
|
- NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
|
|
|
- specifying a NAPALM driver.
|
|
|
- """
|
|
|
- name = models.CharField(
|
|
|
- max_length=100,
|
|
|
- unique=True
|
|
|
- )
|
|
|
- slug = models.SlugField(
|
|
|
- unique=True,
|
|
|
- max_length=100
|
|
|
- )
|
|
|
- manufacturer = models.ForeignKey(
|
|
|
- to='dcim.Manufacturer',
|
|
|
- on_delete=models.PROTECT,
|
|
|
- related_name='platforms',
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- help_text='Optionally limit this platform to devices of a certain manufacturer'
|
|
|
- )
|
|
|
- napalm_driver = models.CharField(
|
|
|
- max_length=50,
|
|
|
- blank=True,
|
|
|
- verbose_name='NAPALM driver',
|
|
|
- help_text='The name of the NAPALM driver to use when interacting with devices'
|
|
|
- )
|
|
|
- napalm_args = JSONField(
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- verbose_name='NAPALM arguments',
|
|
|
- help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)'
|
|
|
- )
|
|
|
-
|
|
|
- csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
|
|
|
-
|
|
|
- class Meta:
|
|
|
- ordering = ['name']
|
|
|
-
|
|
|
- def __str__(self):
|
|
|
- return self.name
|
|
|
-
|
|
|
- def get_absolute_url(self):
|
|
|
- return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
|
|
|
-
|
|
|
- def to_csv(self):
|
|
|
- return (
|
|
|
- self.name,
|
|
|
- self.slug,
|
|
|
- self.manufacturer.name if self.manufacturer else None,
|
|
|
- self.napalm_driver,
|
|
|
- self.napalm_args,
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|
|
- """
|
|
|
- A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
|
|
- DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
|
|
|
-
|
|
|
- Each Device must be assigned to a site, and optionally to a rack within that site. Associating a device with a
|
|
|
- particular rack face or unit is optional (for example, vertically mounted PDUs do not consume rack units).
|
|
|
-
|
|
|
- When a new Device is created, console/power/interface/device bay components are created along with it as dictated
|
|
|
- by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
|
|
|
- creation of a Device.
|
|
|
- """
|
|
|
- device_type = models.ForeignKey(
|
|
|
- to='dcim.DeviceType',
|
|
|
- on_delete=models.PROTECT,
|
|
|
- related_name='instances'
|
|
|
- )
|
|
|
- device_role = models.ForeignKey(
|
|
|
- to='dcim.DeviceRole',
|
|
|
- on_delete=models.PROTECT,
|
|
|
- related_name='devices'
|
|
|
- )
|
|
|
- tenant = models.ForeignKey(
|
|
|
- to='tenancy.Tenant',
|
|
|
- on_delete=models.PROTECT,
|
|
|
- related_name='devices',
|
|
|
- blank=True,
|
|
|
- null=True
|
|
|
- )
|
|
|
- platform = models.ForeignKey(
|
|
|
- to='dcim.Platform',
|
|
|
- on_delete=models.SET_NULL,
|
|
|
- related_name='devices',
|
|
|
- blank=True,
|
|
|
- null=True
|
|
|
- )
|
|
|
- name = models.CharField(
|
|
|
- max_length=64,
|
|
|
- blank=True,
|
|
|
- null=True
|
|
|
- )
|
|
|
- serial = models.CharField(
|
|
|
- max_length=50,
|
|
|
- blank=True,
|
|
|
- verbose_name='Serial number'
|
|
|
- )
|
|
|
- asset_tag = models.CharField(
|
|
|
- max_length=50,
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- unique=True,
|
|
|
- verbose_name='Asset tag',
|
|
|
- help_text='A unique tag used to identify this device'
|
|
|
- )
|
|
|
- site = models.ForeignKey(
|
|
|
- to='dcim.Site',
|
|
|
- on_delete=models.PROTECT,
|
|
|
- related_name='devices'
|
|
|
- )
|
|
|
- rack = models.ForeignKey(
|
|
|
- to='dcim.Rack',
|
|
|
- on_delete=models.PROTECT,
|
|
|
- related_name='devices',
|
|
|
- blank=True,
|
|
|
- null=True
|
|
|
- )
|
|
|
- position = models.PositiveSmallIntegerField(
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- validators=[MinValueValidator(1)],
|
|
|
- verbose_name='Position (U)',
|
|
|
- help_text='The lowest-numbered unit occupied by the device'
|
|
|
- )
|
|
|
- face = models.CharField(
|
|
|
- max_length=50,
|
|
|
- blank=True,
|
|
|
- choices=DeviceFaceChoices,
|
|
|
- verbose_name='Rack face'
|
|
|
- )
|
|
|
- status = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=DeviceStatusChoices,
|
|
|
- default=DeviceStatusChoices.STATUS_ACTIVE
|
|
|
- )
|
|
|
- primary_ip4 = models.OneToOneField(
|
|
|
- to='ipam.IPAddress',
|
|
|
- on_delete=models.SET_NULL,
|
|
|
- related_name='primary_ip4_for',
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- verbose_name='Primary IPv4'
|
|
|
- )
|
|
|
- primary_ip6 = models.OneToOneField(
|
|
|
- to='ipam.IPAddress',
|
|
|
- on_delete=models.SET_NULL,
|
|
|
- related_name='primary_ip6_for',
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- verbose_name='Primary IPv6'
|
|
|
- )
|
|
|
- cluster = models.ForeignKey(
|
|
|
- to='virtualization.Cluster',
|
|
|
- on_delete=models.SET_NULL,
|
|
|
- related_name='devices',
|
|
|
- blank=True,
|
|
|
- null=True
|
|
|
- )
|
|
|
- virtual_chassis = models.ForeignKey(
|
|
|
- to='VirtualChassis',
|
|
|
- on_delete=models.SET_NULL,
|
|
|
- related_name='members',
|
|
|
- blank=True,
|
|
|
- null=True
|
|
|
- )
|
|
|
- vc_position = models.PositiveSmallIntegerField(
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- validators=[MaxValueValidator(255)]
|
|
|
- )
|
|
|
- vc_priority = models.PositiveSmallIntegerField(
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- validators=[MaxValueValidator(255)]
|
|
|
- )
|
|
|
- comments = models.TextField(
|
|
|
- blank=True
|
|
|
- )
|
|
|
- custom_field_values = GenericRelation(
|
|
|
- to='extras.CustomFieldValue',
|
|
|
- content_type_field='obj_type',
|
|
|
- object_id_field='obj_id'
|
|
|
- )
|
|
|
- images = GenericRelation(
|
|
|
- to='extras.ImageAttachment'
|
|
|
- )
|
|
|
-
|
|
|
- objects = NaturalOrderingManager()
|
|
|
- tags = TaggableManager(through=TaggedItem)
|
|
|
-
|
|
|
- csv_headers = [
|
|
|
- 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
|
|
- 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
|
|
|
- ]
|
|
|
- clone_fields = [
|
|
|
- 'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster',
|
|
|
- ]
|
|
|
-
|
|
|
- STATUS_CLASS_MAP = {
|
|
|
- DeviceStatusChoices.STATUS_OFFLINE: 'warning',
|
|
|
- DeviceStatusChoices.STATUS_ACTIVE: 'success',
|
|
|
- DeviceStatusChoices.STATUS_PLANNED: 'info',
|
|
|
- DeviceStatusChoices.STATUS_STAGED: 'primary',
|
|
|
- DeviceStatusChoices.STATUS_FAILED: 'danger',
|
|
|
- DeviceStatusChoices.STATUS_INVENTORY: 'default',
|
|
|
- DeviceStatusChoices.STATUS_DECOMMISSIONING: 'warning',
|
|
|
- }
|
|
|
-
|
|
|
- class Meta:
|
|
|
- ordering = ['name']
|
|
|
- unique_together = [
|
|
|
- ['site', 'tenant', 'name'], # See validate_unique below
|
|
|
- ['rack', 'position', 'face'],
|
|
|
- ['virtual_chassis', 'vc_position'],
|
|
|
- ]
|
|
|
- permissions = (
|
|
|
- ('napalm_read', 'Read-only access to devices via NAPALM'),
|
|
|
- ('napalm_write', 'Read/write access to devices via NAPALM'),
|
|
|
- )
|
|
|
-
|
|
|
- def __str__(self):
|
|
|
- return self.display_name or super().__str__()
|
|
|
-
|
|
|
- def get_absolute_url(self):
|
|
|
- return reverse('dcim:device', args=[self.pk])
|
|
|
-
|
|
|
- def validate_unique(self, exclude=None):
|
|
|
-
|
|
|
- # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
|
|
|
- # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
|
|
- # of the uniqueness constraint without manual intervention.
|
|
|
- if self.tenant is None and Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True):
|
|
|
- raise ValidationError({
|
|
|
- 'name': 'A device with this name already exists.'
|
|
|
- })
|
|
|
-
|
|
|
- super().validate_unique(exclude)
|
|
|
-
|
|
|
- def clean(self):
|
|
|
-
|
|
|
- super().clean()
|
|
|
-
|
|
|
- # Validate site/rack combination
|
|
|
- if self.rack and self.site != self.rack.site:
|
|
|
- raise ValidationError({
|
|
|
- 'rack': "Rack {} does not belong to site {}.".format(self.rack, self.site),
|
|
|
- })
|
|
|
-
|
|
|
- if self.rack is None:
|
|
|
- if self.face:
|
|
|
- raise ValidationError({
|
|
|
- 'face': "Cannot select a rack face without assigning a rack.",
|
|
|
- })
|
|
|
- if self.position:
|
|
|
- raise ValidationError({
|
|
|
- 'face': "Cannot select a rack position without assigning a rack.",
|
|
|
- })
|
|
|
-
|
|
|
- # Validate position/face combination
|
|
|
- if self.position and not self.face:
|
|
|
- raise ValidationError({
|
|
|
- 'face': "Must specify rack face when defining rack position.",
|
|
|
- })
|
|
|
-
|
|
|
- # Prevent 0U devices from being assigned to a specific position
|
|
|
- if self.position and self.device_type.u_height == 0:
|
|
|
- raise ValidationError({
|
|
|
- 'position': "A U0 device type ({}) cannot be assigned to a rack position.".format(self.device_type)
|
|
|
- })
|
|
|
-
|
|
|
- if self.rack:
|
|
|
-
|
|
|
- try:
|
|
|
- # Child devices cannot be assigned to a rack face/unit
|
|
|
- if self.device_type.is_child_device and self.face is not None:
|
|
|
- raise ValidationError({
|
|
|
- 'face': "Child device types cannot be assigned to a rack face. This is an attribute of the "
|
|
|
- "parent device."
|
|
|
- })
|
|
|
- if self.device_type.is_child_device and self.position:
|
|
|
- raise ValidationError({
|
|
|
- 'position': "Child device types cannot be assigned to a rack position. This is an attribute of "
|
|
|
- "the parent device."
|
|
|
- })
|
|
|
-
|
|
|
- # Validate rack space
|
|
|
- rack_face = self.face if not self.device_type.is_full_depth else None
|
|
|
- exclude_list = [self.pk] if self.pk else []
|
|
|
- try:
|
|
|
- available_units = self.rack.get_available_units(
|
|
|
- u_height=self.device_type.u_height, rack_face=rack_face, exclude=exclude_list
|
|
|
- )
|
|
|
- if self.position and self.position not in available_units:
|
|
|
- raise ValidationError({
|
|
|
- 'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) "
|
|
|
- "{} ({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
|
|
- })
|
|
|
- except Rack.DoesNotExist:
|
|
|
- pass
|
|
|
-
|
|
|
- except DeviceType.DoesNotExist:
|
|
|
- pass
|
|
|
-
|
|
|
- # Validate primary IP addresses
|
|
|
- vc_interfaces = self.vc_interfaces.all()
|
|
|
- if self.primary_ip4:
|
|
|
- if self.primary_ip4.interface in vc_interfaces:
|
|
|
- pass
|
|
|
- elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in vc_interfaces:
|
|
|
- pass
|
|
|
- else:
|
|
|
- raise ValidationError({
|
|
|
- 'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format(
|
|
|
- self.primary_ip4),
|
|
|
- })
|
|
|
- if self.primary_ip6:
|
|
|
- if self.primary_ip6.interface in vc_interfaces:
|
|
|
- pass
|
|
|
- elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.interface in vc_interfaces:
|
|
|
- pass
|
|
|
- else:
|
|
|
- raise ValidationError({
|
|
|
- 'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(
|
|
|
- self.primary_ip6),
|
|
|
- })
|
|
|
-
|
|
|
- # Validate manufacturer/platform
|
|
|
- if hasattr(self, 'device_type') and self.platform:
|
|
|
- if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
|
|
|
- raise ValidationError({
|
|
|
- 'platform': "The assigned platform is limited to {} device types, but this device's type belongs "
|
|
|
- "to {}.".format(self.platform.manufacturer, self.device_type.manufacturer)
|
|
|
- })
|
|
|
-
|
|
|
- # A Device can only be assigned to a Cluster in the same Site (or no Site)
|
|
|
- if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
|
|
|
- raise ValidationError({
|
|
|
- 'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
|
|
|
- })
|
|
|
-
|
|
|
- # Validate virtual chassis assignment
|
|
|
- if self.virtual_chassis and self.vc_position is None:
|
|
|
- raise ValidationError({
|
|
|
- 'vc_position': "A device assigned to a virtual chassis must have its position defined."
|
|
|
- })
|
|
|
-
|
|
|
- def save(self, *args, **kwargs):
|
|
|
-
|
|
|
- is_new = not bool(self.pk)
|
|
|
-
|
|
|
- super().save(*args, **kwargs)
|
|
|
-
|
|
|
- # If this is a new Device, instantiate all of the related components per the DeviceType definition
|
|
|
- if is_new:
|
|
|
- ConsolePort.objects.bulk_create(
|
|
|
- [x.instantiate(self) for x in self.device_type.consoleport_templates.all()]
|
|
|
- )
|
|
|
- ConsoleServerPort.objects.bulk_create(
|
|
|
- [x.instantiate(self) for x in self.device_type.consoleserverport_templates.all()]
|
|
|
- )
|
|
|
- PowerPort.objects.bulk_create(
|
|
|
- [x.instantiate(self) for x in self.device_type.powerport_templates.all()]
|
|
|
- )
|
|
|
- PowerOutlet.objects.bulk_create(
|
|
|
- [x.instantiate(self) for x in self.device_type.poweroutlet_templates.all()]
|
|
|
- )
|
|
|
- Interface.objects.bulk_create(
|
|
|
- [x.instantiate(self) for x in self.device_type.interface_templates.all()]
|
|
|
- )
|
|
|
- RearPort.objects.bulk_create(
|
|
|
- [x.instantiate(self) for x in self.device_type.rearport_templates.all()]
|
|
|
- )
|
|
|
- FrontPort.objects.bulk_create(
|
|
|
- [x.instantiate(self) for x in self.device_type.frontport_templates.all()]
|
|
|
- )
|
|
|
- DeviceBay.objects.bulk_create(
|
|
|
- [x.instantiate(self) for x in self.device_type.device_bay_templates.all()]
|
|
|
- )
|
|
|
-
|
|
|
- # Update Site and Rack assignment for any child Devices
|
|
|
- devices = Device.objects.filter(parent_bay__device=self)
|
|
|
- for device in devices:
|
|
|
- device.site = self.site
|
|
|
- device.rack = self.rack
|
|
|
- device.save()
|
|
|
-
|
|
|
- def to_csv(self):
|
|
|
- return (
|
|
|
- self.name or '',
|
|
|
- self.device_role.name,
|
|
|
- self.tenant.name if self.tenant else None,
|
|
|
- self.device_type.manufacturer.name,
|
|
|
- self.device_type.model,
|
|
|
- self.platform.name if self.platform else None,
|
|
|
- self.serial,
|
|
|
- self.asset_tag,
|
|
|
- self.get_status_display(),
|
|
|
- self.site.name,
|
|
|
- self.rack.group.name if self.rack and self.rack.group else None,
|
|
|
- self.rack.name if self.rack else None,
|
|
|
- self.position,
|
|
|
- self.get_face_display(),
|
|
|
- self.comments,
|
|
|
- )
|
|
|
-
|
|
|
- @property
|
|
|
- def display_name(self):
|
|
|
- if self.name:
|
|
|
- return self.name
|
|
|
- elif self.virtual_chassis and self.virtual_chassis.master.name:
|
|
|
- return "{}:{}".format(self.virtual_chassis.master, self.vc_position)
|
|
|
- elif hasattr(self, 'device_type'):
|
|
|
- return "{}".format(self.device_type)
|
|
|
- return ""
|
|
|
-
|
|
|
- @property
|
|
|
- def identifier(self):
|
|
|
- """
|
|
|
- Return the device name if set; otherwise return the Device's primary key as {pk}
|
|
|
- """
|
|
|
- if self.name is not None:
|
|
|
- return self.name
|
|
|
- return '{{{}}}'.format(self.pk)
|
|
|
-
|
|
|
- @property
|
|
|
- def primary_ip(self):
|
|
|
- if settings.PREFER_IPV4 and self.primary_ip4:
|
|
|
- return self.primary_ip4
|
|
|
- elif self.primary_ip6:
|
|
|
- return self.primary_ip6
|
|
|
- elif self.primary_ip4:
|
|
|
- return self.primary_ip4
|
|
|
- else:
|
|
|
- return None
|
|
|
-
|
|
|
- def get_vc_master(self):
|
|
|
- """
|
|
|
- If this Device is a VirtualChassis member, return the VC master. Otherwise, return None.
|
|
|
- """
|
|
|
- return self.virtual_chassis.master if self.virtual_chassis else None
|
|
|
-
|
|
|
- @property
|
|
|
- def vc_interfaces(self):
|
|
|
- """
|
|
|
- Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another
|
|
|
- Device belonging to the same VirtualChassis.
|
|
|
- """
|
|
|
- filter = Q(device=self)
|
|
|
- if self.virtual_chassis and self.virtual_chassis.master == self:
|
|
|
- filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
|
|
|
- return Interface.objects.filter(filter)
|
|
|
-
|
|
|
- def get_cables(self, pk_list=False):
|
|
|
- """
|
|
|
- Return a QuerySet or PK list matching all Cables connected to a component of this Device.
|
|
|
- """
|
|
|
- cable_pks = []
|
|
|
- for component_model in [
|
|
|
- ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort
|
|
|
- ]:
|
|
|
- cable_pks += component_model.objects.filter(
|
|
|
- device=self, cable__isnull=False
|
|
|
- ).values_list('cable', flat=True)
|
|
|
- if pk_list:
|
|
|
- return cable_pks
|
|
|
- return Cable.objects.filter(pk__in=cable_pks)
|
|
|
-
|
|
|
- def get_children(self):
|
|
|
- """
|
|
|
- Return the set of child Devices installed in DeviceBays within this Device.
|
|
|
- """
|
|
|
- return Device.objects.filter(parent_bay__device=self.pk)
|
|
|
-
|
|
|
- def get_status_class(self):
|
|
|
- return self.STATUS_CLASS_MAP.get(self.status)
|
|
|
-
|
|
|
-
|
|
|
-#
|
|
|
-# 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
|
|
|
- )
|
|
|
- type = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=ConsolePortTypeChoices,
|
|
|
- blank=True
|
|
|
- )
|
|
|
- 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(through=TaggedItem)
|
|
|
-
|
|
|
- csv_headers = ['device', 'name', 'type', 'description']
|
|
|
-
|
|
|
- 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,
|
|
|
- self.type,
|
|
|
- self.description,
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-#
|
|
|
-# 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
|
|
|
- )
|
|
|
- type = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=ConsolePortTypeChoices,
|
|
|
- blank=True
|
|
|
- )
|
|
|
- connection_status = models.NullBooleanField(
|
|
|
- choices=CONNECTION_STATUS_CHOICES,
|
|
|
- blank=True
|
|
|
- )
|
|
|
-
|
|
|
- objects = NaturalOrderingManager()
|
|
|
- tags = TaggableManager(through=TaggedItem)
|
|
|
-
|
|
|
- csv_headers = ['device', 'name', 'type', 'description']
|
|
|
-
|
|
|
- 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,
|
|
|
- self.type,
|
|
|
- self.description,
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-#
|
|
|
-# 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
|
|
|
- )
|
|
|
- type = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=PowerPortTypeChoices,
|
|
|
- blank=True
|
|
|
- )
|
|
|
- maximum_draw = models.PositiveSmallIntegerField(
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- validators=[MinValueValidator(1)],
|
|
|
- help_text="Maximum power draw (watts)"
|
|
|
- )
|
|
|
- allocated_draw = models.PositiveSmallIntegerField(
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- validators=[MinValueValidator(1)],
|
|
|
- help_text="Allocated power draw (watts)"
|
|
|
- )
|
|
|
- _connected_poweroutlet = models.OneToOneField(
|
|
|
- to='dcim.PowerOutlet',
|
|
|
- on_delete=models.SET_NULL,
|
|
|
- related_name='connected_endpoint',
|
|
|
- blank=True,
|
|
|
- null=True
|
|
|
- )
|
|
|
- _connected_powerfeed = models.OneToOneField(
|
|
|
- to='dcim.PowerFeed',
|
|
|
- on_delete=models.SET_NULL,
|
|
|
- related_name='+',
|
|
|
- blank=True,
|
|
|
- null=True
|
|
|
- )
|
|
|
- connection_status = models.NullBooleanField(
|
|
|
- choices=CONNECTION_STATUS_CHOICES,
|
|
|
- blank=True
|
|
|
- )
|
|
|
-
|
|
|
- objects = NaturalOrderingManager()
|
|
|
- tags = TaggableManager(through=TaggedItem)
|
|
|
-
|
|
|
- csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
|
|
-
|
|
|
- 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,
|
|
|
- self.get_type_display(),
|
|
|
- self.maximum_draw,
|
|
|
- self.allocated_draw,
|
|
|
- self.description,
|
|
|
- )
|
|
|
-
|
|
|
- @property
|
|
|
- def connected_endpoint(self):
|
|
|
- if self._connected_poweroutlet:
|
|
|
- return self._connected_poweroutlet
|
|
|
- return self._connected_powerfeed
|
|
|
-
|
|
|
- @connected_endpoint.setter
|
|
|
- def connected_endpoint(self, value):
|
|
|
- if value is None:
|
|
|
- self._connected_poweroutlet = None
|
|
|
- self._connected_powerfeed = None
|
|
|
- elif isinstance(value, PowerOutlet):
|
|
|
- self._connected_poweroutlet = value
|
|
|
- self._connected_powerfeed = None
|
|
|
- elif isinstance(value, PowerFeed):
|
|
|
- self._connected_poweroutlet = None
|
|
|
- self._connected_powerfeed = value
|
|
|
- else:
|
|
|
- raise ValueError(
|
|
|
- "Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value))
|
|
|
- )
|
|
|
-
|
|
|
- def get_power_draw(self):
|
|
|
- """
|
|
|
- Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort.
|
|
|
- """
|
|
|
- # 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:
|
|
|
- outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True)
|
|
|
- utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).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': len(outlet_ids),
|
|
|
- 'legs': [],
|
|
|
- }
|
|
|
-
|
|
|
- # Calculate per-leg aggregates for three-phase feeds
|
|
|
- if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
|
|
|
- for leg, leg_name in PowerOutletFeedLegChoices:
|
|
|
- outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
|
|
|
- utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).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': len(outlet_ids),
|
|
|
- })
|
|
|
-
|
|
|
- return ret
|
|
|
-
|
|
|
- # Default to administratively defined values
|
|
|
- return {
|
|
|
- 'allocated': self.allocated_draw or 0,
|
|
|
- 'maximum': self.maximum_draw or 0,
|
|
|
- 'outlet_count': PowerOutlet.objects.filter(power_port=self).count(),
|
|
|
- 'legs': [],
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-#
|
|
|
-# 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
|
|
|
- )
|
|
|
- type = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=PowerOutletTypeChoices,
|
|
|
- blank=True
|
|
|
- )
|
|
|
- power_port = models.ForeignKey(
|
|
|
- to='dcim.PowerPort',
|
|
|
- on_delete=models.SET_NULL,
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- related_name='poweroutlets'
|
|
|
- )
|
|
|
- feed_leg = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=PowerOutletFeedLegChoices,
|
|
|
- blank=True,
|
|
|
- help_text="Phase (for three-phase feeds)"
|
|
|
- )
|
|
|
- connection_status = models.NullBooleanField(
|
|
|
- choices=CONNECTION_STATUS_CHOICES,
|
|
|
- blank=True
|
|
|
- )
|
|
|
-
|
|
|
- objects = NaturalOrderingManager()
|
|
|
- tags = TaggableManager(through=TaggedItem)
|
|
|
-
|
|
|
- csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description']
|
|
|
-
|
|
|
- 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,
|
|
|
- self.get_type_display(),
|
|
|
- self.power_port.name if self.power_port else None,
|
|
|
- self.get_feed_leg_display(),
|
|
|
- self.description,
|
|
|
- )
|
|
|
-
|
|
|
- def clean(self):
|
|
|
-
|
|
|
- # Validate power port assignment
|
|
|
- if self.power_port and self.power_port.device != self.device:
|
|
|
- raise ValidationError(
|
|
|
- "Parent power port ({}) must belong to the same device".format(self.power_port)
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
-#
|
|
|
-# 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'
|
|
|
- )
|
|
|
- type = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=InterfaceTypeChoices
|
|
|
- )
|
|
|
- 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'
|
|
|
- )
|
|
|
- mode = models.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=InterfaceModeChoices,
|
|
|
- blank=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(through=TaggedItem)
|
|
|
-
|
|
|
- csv_headers = [
|
|
|
- 'device', 'virtual_machine', 'name', 'lag', 'type', '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_type_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.type not in VMInterfaceTypeChoices.values():
|
|
|
- raise ValidationError({
|
|
|
- 'type': "Invalid interface type for a virtual machine: {}".format(self.type)
|
|
|
- })
|
|
|
-
|
|
|
- # Virtual interfaces cannot be connected
|
|
|
- if self.type in NONCONNECTABLE_IFACE_TYPES and (
|
|
|
- self.cable or getattr(self, 'circuit_termination', False)
|
|
|
- ):
|
|
|
- raise ValidationError({
|
|
|
- 'type': "Virtual and wireless interfaces cannot be connected to another interface or circuit. "
|
|
|
- "Disconnect the interface or choose a suitable type."
|
|
|
- })
|
|
|
-
|
|
|
- # 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.type in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
|
|
|
- raise ValidationError({
|
|
|
- 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_type_display())
|
|
|
- })
|
|
|
-
|
|
|
- # Only a LAG can have LAG members
|
|
|
- if self.type != InterfaceTypeChoices.TYPE_LAG and self.member_interfaces.exists():
|
|
|
- raise ValidationError({
|
|
|
- 'type': "Cannot change interface type; 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 InterfaceModeChoices.MODE_TAGGED:
|
|
|
- self.tagged_vlans.clear()
|
|
|
-
|
|
|
- return super().save(*args, **kwargs)
|
|
|
-
|
|
|
- def to_objectchange(self, action):
|
|
|
- # Annotate the parent Device/VM
|
|
|
- try:
|
|
|
- parent_obj = self.device or self.virtual_machine
|
|
|
- except ObjectDoesNotExist:
|
|
|
- parent_obj = None
|
|
|
-
|
|
|
- return ObjectChange(
|
|
|
- changed_object=self,
|
|
|
- object_repr=str(self),
|
|
|
- action=action,
|
|
|
- related_object=parent_obj,
|
|
|
- object_data=serialize_object(self)
|
|
|
- )
|
|
|
-
|
|
|
- @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.type not in NONCONNECTABLE_IFACE_TYPES
|
|
|
-
|
|
|
- @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 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.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=PortTypeChoices
|
|
|
- )
|
|
|
- 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)]
|
|
|
- )
|
|
|
-
|
|
|
- is_path_endpoint = False
|
|
|
-
|
|
|
- objects = NaturalOrderingManager()
|
|
|
- tags = TaggableManager(through=TaggedItem)
|
|
|
-
|
|
|
- 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 save(self, *args, **kwargs):
|
|
|
|
|
|
- def to_csv(self):
|
|
|
- return (
|
|
|
- self.device.identifier,
|
|
|
- self.name,
|
|
|
- self.get_type_display(),
|
|
|
- self.rear_port.name,
|
|
|
- self.rear_port_position,
|
|
|
- self.description,
|
|
|
- )
|
|
|
+ is_new = not bool(self.pk)
|
|
|
|
|
|
- def clean(self):
|
|
|
+ super().save(*args, **kwargs)
|
|
|
|
|
|
- # 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)
|
|
|
+ # If this is a new Device, instantiate all of the related components per the DeviceType definition
|
|
|
+ if is_new:
|
|
|
+ ConsolePort.objects.bulk_create(
|
|
|
+ [x.instantiate(self) for x in self.device_type.consoleport_templates.all()]
|
|
|
)
|
|
|
-
|
|
|
- # 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
|
|
|
- )
|
|
|
+ ConsoleServerPort.objects.bulk_create(
|
|
|
+ [x.instantiate(self) for x in self.device_type.consoleserverport_templates.all()]
|
|
|
+ )
|
|
|
+ PowerPort.objects.bulk_create(
|
|
|
+ [x.instantiate(self) for x in self.device_type.powerport_templates.all()]
|
|
|
+ )
|
|
|
+ PowerOutlet.objects.bulk_create(
|
|
|
+ [x.instantiate(self) for x in self.device_type.poweroutlet_templates.all()]
|
|
|
+ )
|
|
|
+ Interface.objects.bulk_create(
|
|
|
+ [x.instantiate(self) for x in self.device_type.interface_templates.all()]
|
|
|
+ )
|
|
|
+ RearPort.objects.bulk_create(
|
|
|
+ [x.instantiate(self) for x in self.device_type.rearport_templates.all()]
|
|
|
+ )
|
|
|
+ FrontPort.objects.bulk_create(
|
|
|
+ [x.instantiate(self) for x in self.device_type.frontport_templates.all()]
|
|
|
+ )
|
|
|
+ DeviceBay.objects.bulk_create(
|
|
|
+ [x.instantiate(self) for x in self.device_type.device_bay_templates.all()]
|
|
|
)
|
|
|
|
|
|
-
|
|
|
-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.CharField(
|
|
|
- max_length=50,
|
|
|
- choices=PortTypeChoices
|
|
|
- )
|
|
|
- positions = models.PositiveSmallIntegerField(
|
|
|
- default=1,
|
|
|
- validators=[MinValueValidator(1), MaxValueValidator(64)]
|
|
|
- )
|
|
|
-
|
|
|
- is_path_endpoint = False
|
|
|
-
|
|
|
- objects = NaturalOrderingManager()
|
|
|
- tags = TaggableManager(through=TaggedItem)
|
|
|
-
|
|
|
- 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(through=TaggedItem)
|
|
|
-
|
|
|
- csv_headers = ['device', 'name', 'installed_device', 'description']
|
|
|
-
|
|
|
- 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()
|
|
|
+ # Update Site and Rack assignment for any child Devices
|
|
|
+ devices = Device.objects.filter(parent_bay__device=self)
|
|
|
+ for device in devices:
|
|
|
+ device.site = self.site
|
|
|
+ device.rack = self.rack
|
|
|
+ device.save()
|
|
|
|
|
|
def to_csv(self):
|
|
|
return (
|
|
|
- self.device.identifier,
|
|
|
- self.name,
|
|
|
- self.installed_device.identifier if self.installed_device else None,
|
|
|
- self.description,
|
|
|
+ self.name or '',
|
|
|
+ self.device_role.name,
|
|
|
+ self.tenant.name if self.tenant else None,
|
|
|
+ self.device_type.manufacturer.name,
|
|
|
+ self.device_type.model,
|
|
|
+ self.platform.name if self.platform else None,
|
|
|
+ self.serial,
|
|
|
+ self.asset_tag,
|
|
|
+ self.get_status_display(),
|
|
|
+ self.site.name,
|
|
|
+ self.rack.group.name if self.rack and self.rack.group else None,
|
|
|
+ self.rack.name if self.rack else None,
|
|
|
+ self.position,
|
|
|
+ self.get_face_display(),
|
|
|
+ self.comments,
|
|
|
)
|
|
|
|
|
|
- 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.")
|
|
|
-
|
|
|
- # 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 {}".format(
|
|
|
- current_bay
|
|
|
- )
|
|
|
- })
|
|
|
-
|
|
|
-
|
|
|
-#
|
|
|
-# Inventory items
|
|
|
-#
|
|
|
+ @property
|
|
|
+ def display_name(self):
|
|
|
+ if self.name:
|
|
|
+ return self.name
|
|
|
+ elif self.virtual_chassis and self.virtual_chassis.master.name:
|
|
|
+ return "{}:{}".format(self.virtual_chassis.master, self.vc_position)
|
|
|
+ elif hasattr(self, 'device_type'):
|
|
|
+ return "{}".format(self.device_type)
|
|
|
+ return ""
|
|
|
|
|
|
-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'
|
|
|
- )
|
|
|
+ @property
|
|
|
+ def identifier(self):
|
|
|
+ """
|
|
|
+ Return the device name if set; otherwise return the Device's primary key as {pk}
|
|
|
+ """
|
|
|
+ if self.name is not None:
|
|
|
+ return self.name
|
|
|
+ return '{{{}}}'.format(self.pk)
|
|
|
|
|
|
- tags = TaggableManager(through=TaggedItem)
|
|
|
+ @property
|
|
|
+ def primary_ip(self):
|
|
|
+ if settings.PREFER_IPV4 and self.primary_ip4:
|
|
|
+ return self.primary_ip4
|
|
|
+ elif self.primary_ip6:
|
|
|
+ return self.primary_ip6
|
|
|
+ elif self.primary_ip4:
|
|
|
+ return self.primary_ip4
|
|
|
+ else:
|
|
|
+ return None
|
|
|
|
|
|
- csv_headers = [
|
|
|
- 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
|
|
- ]
|
|
|
+ def get_vc_master(self):
|
|
|
+ """
|
|
|
+ If this Device is a VirtualChassis member, return the VC master. Otherwise, return None.
|
|
|
+ """
|
|
|
+ return self.virtual_chassis.master if self.virtual_chassis else None
|
|
|
|
|
|
- class Meta:
|
|
|
- ordering = ['device__id', 'parent__id', 'name']
|
|
|
- unique_together = ['device', 'parent', 'name']
|
|
|
+ @property
|
|
|
+ def vc_interfaces(self):
|
|
|
+ """
|
|
|
+ Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another
|
|
|
+ Device belonging to the same VirtualChassis.
|
|
|
+ """
|
|
|
+ filter = Q(device=self)
|
|
|
+ if self.virtual_chassis and self.virtual_chassis.master == self:
|
|
|
+ filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
|
|
|
+ return Interface.objects.filter(filter)
|
|
|
|
|
|
- def __str__(self):
|
|
|
- return self.name
|
|
|
+ def get_cables(self, pk_list=False):
|
|
|
+ """
|
|
|
+ Return a QuerySet or PK list matching all Cables connected to a component of this Device.
|
|
|
+ """
|
|
|
+ cable_pks = []
|
|
|
+ for component_model in [
|
|
|
+ ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort
|
|
|
+ ]:
|
|
|
+ cable_pks += component_model.objects.filter(
|
|
|
+ device=self, cable__isnull=False
|
|
|
+ ).values_list('cable', flat=True)
|
|
|
+ if pk_list:
|
|
|
+ return cable_pks
|
|
|
+ return Cable.objects.filter(pk__in=cable_pks)
|
|
|
|
|
|
- def get_absolute_url(self):
|
|
|
- return self.device.get_absolute_url()
|
|
|
+ def get_children(self):
|
|
|
+ """
|
|
|
+ Return the set of child Devices installed in DeviceBays within this Device.
|
|
|
+ """
|
|
|
+ return Device.objects.filter(parent_bay__device=self.pk)
|
|
|
|
|
|
- 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,
|
|
|
- )
|
|
|
+ def get_status_class(self):
|
|
|
+ return self.STATUS_CLASS_MAP.get(self.status)
|
|
|
|
|
|
|
|
|
#
|