| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587 |
- from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
- from django.contrib.contenttypes.models import ContentType
- from django.core.exceptions import ObjectDoesNotExist, ValidationError
- from django.core.validators import MaxValueValidator, MinValueValidator
- from django.db import models
- from mptt.models import MPTTModel, TreeForeignKey
- from dcim.choices import *
- from dcim.constants import *
- from extras.utils import extras_features
- from netbox.models import ChangeLoggedModel
- from utilities.fields import ColorField, NaturalOrderingField
- from utilities.mptt import TreeManager
- from utilities.ordering import naturalize_interface
- from .device_components import (
- ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
- RearPort,
- )
- __all__ = (
- 'ConsolePortTemplate',
- 'ConsoleServerPortTemplate',
- 'DeviceBayTemplate',
- 'FrontPortTemplate',
- 'InterfaceTemplate',
- 'InventoryItemTemplate',
- 'ModuleBayTemplate',
- 'PowerOutletTemplate',
- 'PowerPortTemplate',
- 'RearPortTemplate',
- )
- class ComponentTemplateModel(ChangeLoggedModel):
- device_type = models.ForeignKey(
- to='dcim.DeviceType',
- on_delete=models.CASCADE,
- related_name='%(class)ss'
- )
- name = models.CharField(
- max_length=64
- )
- _name = NaturalOrderingField(
- target_field='name',
- max_length=100,
- blank=True
- )
- label = models.CharField(
- max_length=64,
- blank=True,
- help_text="Physical label"
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
- class Meta:
- abstract = True
- def __str__(self):
- if self.label:
- return f"{self.name} ({self.label})"
- return self.name
- def instantiate(self, device):
- """
- Instantiate a new component on the specified Device.
- """
- raise NotImplementedError()
- def to_objectchange(self, action, related_object=None):
- # Annotate the parent DeviceType
- try:
- device_type = self.device_type
- except ObjectDoesNotExist:
- # The parent DeviceType has already been deleted
- device_type = None
- return super().to_objectchange(action, related_object=device_type)
- class ModularComponentTemplateModel(ComponentTemplateModel):
- """
- A ComponentTemplateModel which supports optional assignment to a ModuleType.
- """
- device_type = models.ForeignKey(
- to='dcim.DeviceType',
- on_delete=models.CASCADE,
- related_name='%(class)ss',
- blank=True,
- null=True
- )
- module_type = models.ForeignKey(
- to='dcim.ModuleType',
- on_delete=models.CASCADE,
- related_name='%(class)ss',
- blank=True,
- null=True
- )
- class Meta:
- abstract = True
- def to_objectchange(self, action, related_object=None):
- # Annotate the parent DeviceType or ModuleType
- try:
- if getattr(self, 'device_type'):
- return super().to_objectchange(action, related_object=self.device_type)
- except ObjectDoesNotExist:
- pass
- try:
- if getattr(self, 'module_type'):
- return super().to_objectchange(action, related_object=self.module_type)
- except ObjectDoesNotExist:
- pass
- return super().to_objectchange(action)
- def clean(self):
- super().clean()
- # A component template must belong to a DeviceType *or* to a ModuleType
- if self.device_type and self.module_type:
- raise ValidationError(
- "A component template cannot be associated with both a device type and a module type."
- )
- if not self.device_type and not self.module_type:
- raise ValidationError(
- "A component template must be associated with either a device type or a module type."
- )
- def resolve_name(self, module):
- if module:
- return self.name.replace('{module}', module.module_bay.position)
- return self.name
- @extras_features('webhooks')
- class ConsolePortTemplate(ModularComponentTemplateModel):
- """
- A template for a ConsolePort to be created for a new Device.
- """
- type = models.CharField(
- max_length=50,
- choices=ConsolePortTypeChoices,
- blank=True
- )
- component_model = ConsolePort
- class Meta:
- ordering = ('device_type', 'module_type', '_name')
- unique_together = (
- ('device_type', 'name'),
- ('module_type', 'name'),
- )
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.label,
- type=self.type,
- **kwargs
- )
- @extras_features('webhooks')
- class ConsoleServerPortTemplate(ModularComponentTemplateModel):
- """
- A template for a ConsoleServerPort to be created for a new Device.
- """
- type = models.CharField(
- max_length=50,
- choices=ConsolePortTypeChoices,
- blank=True
- )
- component_model = ConsoleServerPort
- class Meta:
- ordering = ('device_type', 'module_type', '_name')
- unique_together = (
- ('device_type', 'name'),
- ('module_type', 'name'),
- )
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.label,
- type=self.type,
- **kwargs
- )
- @extras_features('webhooks')
- class PowerPortTemplate(ModularComponentTemplateModel):
- """
- A template for a PowerPort to be created for a new Device.
- """
- 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)"
- )
- component_model = PowerPort
- class Meta:
- ordering = ('device_type', 'module_type', '_name')
- unique_together = (
- ('device_type', 'name'),
- ('module_type', 'name'),
- )
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.label,
- type=self.type,
- maximum_draw=self.maximum_draw,
- allocated_draw=self.allocated_draw,
- **kwargs
- )
- 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': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
- })
- @extras_features('webhooks')
- class PowerOutletTemplate(ModularComponentTemplateModel):
- """
- A template for a PowerOutlet to be created for a new Device.
- """
- type = models.CharField(
- max_length=50,
- choices=PowerOutletTypeChoices,
- blank=True
- )
- power_port = models.ForeignKey(
- to='dcim.PowerPortTemplate',
- on_delete=models.SET_NULL,
- blank=True,
- null=True,
- related_name='poweroutlet_templates'
- )
- feed_leg = models.CharField(
- max_length=50,
- choices=PowerOutletFeedLegChoices,
- blank=True,
- help_text="Phase (for three-phase feeds)"
- )
- component_model = PowerOutlet
- class Meta:
- ordering = ('device_type', 'module_type', '_name')
- unique_together = (
- ('device_type', 'name'),
- ('module_type', 'name'),
- )
- def clean(self):
- super().clean()
- # Validate power port assignment
- if self.power_port:
- if self.device_type and self.power_port.device_type != self.device_type:
- raise ValidationError(
- f"Parent power port ({self.power_port}) must belong to the same device type"
- )
- if self.module_type and self.power_port.module_type != self.module_type:
- raise ValidationError(
- f"Parent power port ({self.power_port}) must belong to the same module type"
- )
- def instantiate(self, **kwargs):
- if self.power_port:
- power_port = PowerPort.objects.get(name=self.power_port.name, **kwargs)
- else:
- power_port = None
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.label,
- type=self.type,
- power_port=power_port,
- feed_leg=self.feed_leg,
- **kwargs
- )
- @extras_features('webhooks')
- class InterfaceTemplate(ModularComponentTemplateModel):
- """
- A template for a physical data interface on a new Device.
- """
- # Override ComponentTemplateModel._name to specify naturalize_interface function
- _name = NaturalOrderingField(
- target_field='name',
- naturalize_function=naturalize_interface,
- max_length=100,
- blank=True
- )
- type = models.CharField(
- max_length=50,
- choices=InterfaceTypeChoices
- )
- mgmt_only = models.BooleanField(
- default=False,
- verbose_name='Management only'
- )
- component_model = Interface
- class Meta:
- ordering = ('device_type', 'module_type', '_name')
- unique_together = (
- ('device_type', 'name'),
- ('module_type', 'name'),
- )
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.label,
- type=self.type,
- mgmt_only=self.mgmt_only,
- **kwargs
- )
- @extras_features('webhooks')
- class FrontPortTemplate(ModularComponentTemplateModel):
- """
- Template for a pass-through port on the front of a new Device.
- """
- type = models.CharField(
- max_length=50,
- choices=PortTypeChoices
- )
- color = ColorField(
- blank=True
- )
- rear_port = models.ForeignKey(
- to='dcim.RearPortTemplate',
- on_delete=models.CASCADE,
- related_name='frontport_templates'
- )
- rear_port_position = models.PositiveSmallIntegerField(
- default=1,
- validators=[
- MinValueValidator(REARPORT_POSITIONS_MIN),
- MaxValueValidator(REARPORT_POSITIONS_MAX)
- ]
- )
- component_model = FrontPort
- class Meta:
- ordering = ('device_type', 'module_type', '_name')
- unique_together = (
- ('device_type', 'name'),
- ('module_type', 'name'),
- ('rear_port', 'rear_port_position'),
- )
- def clean(self):
- super().clean()
- try:
- # Validate rear port assignment
- if self.rear_port.device_type != self.device_type:
- raise ValidationError(
- "Rear port ({}) must belong to the same device type".format(self.rear_port)
- )
- # Validate rear port position assignment
- if self.rear_port_position > self.rear_port.positions:
- raise ValidationError(
- "Invalid rear port position ({}); rear port {} has only {} positions".format(
- self.rear_port_position, self.rear_port.name, self.rear_port.positions
- )
- )
- except RearPortTemplate.DoesNotExist:
- pass
- def instantiate(self, **kwargs):
- if self.rear_port:
- rear_port = RearPort.objects.get(name=self.rear_port.name, **kwargs)
- else:
- rear_port = None
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.label,
- type=self.type,
- color=self.color,
- rear_port=rear_port,
- rear_port_position=self.rear_port_position,
- **kwargs
- )
- @extras_features('webhooks')
- class RearPortTemplate(ModularComponentTemplateModel):
- """
- Template for a pass-through port on the rear of a new Device.
- """
- type = models.CharField(
- max_length=50,
- choices=PortTypeChoices
- )
- color = ColorField(
- blank=True
- )
- positions = models.PositiveSmallIntegerField(
- default=1,
- validators=[
- MinValueValidator(REARPORT_POSITIONS_MIN),
- MaxValueValidator(REARPORT_POSITIONS_MAX)
- ]
- )
- component_model = RearPort
- class Meta:
- ordering = ('device_type', 'module_type', '_name')
- unique_together = (
- ('device_type', 'name'),
- ('module_type', 'name'),
- )
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.label,
- type=self.type,
- color=self.color,
- positions=self.positions,
- **kwargs
- )
- @extras_features('webhooks')
- class ModuleBayTemplate(ComponentTemplateModel):
- """
- A template for a ModuleBay to be created for a new parent Device.
- """
- position = models.CharField(
- max_length=30,
- blank=True,
- help_text='Identifier to reference when renaming installed components'
- )
- component_model = ModuleBay
- class Meta:
- ordering = ('device_type', '_name')
- unique_together = ('device_type', 'name')
- def instantiate(self, device):
- return self.component_model(
- device=device,
- name=self.name,
- label=self.label,
- position=self.position
- )
- @extras_features('webhooks')
- class DeviceBayTemplate(ComponentTemplateModel):
- """
- A template for a DeviceBay to be created for a new parent Device.
- """
- component_model = DeviceBay
- class Meta:
- ordering = ('device_type', '_name')
- unique_together = ('device_type', 'name')
- def instantiate(self, device):
- return self.component_model(
- device=device,
- name=self.name,
- label=self.label
- )
- def clean(self):
- if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
- raise ValidationError(
- f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
- )
- @extras_features('webhooks')
- class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
- """
- A template for an InventoryItem to be created for a new parent Device.
- """
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='child_items',
- blank=True,
- null=True,
- db_index=True
- )
- component_type = models.ForeignKey(
- to=ContentType,
- limit_choices_to=MODULAR_COMPONENT_TEMPLATE_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_item_templates',
- blank=True,
- null=True
- )
- manufacturer = models.ForeignKey(
- to='dcim.Manufacturer',
- on_delete=models.PROTECT,
- related_name='inventory_item_templates',
- blank=True,
- null=True
- )
- part_id = models.CharField(
- max_length=50,
- verbose_name='Part ID',
- blank=True,
- help_text='Manufacturer-assigned part identifier'
- )
- objects = TreeManager()
- component_model = InventoryItem
- class Meta:
- ordering = ('device_type__id', 'parent__id', '_name')
- unique_together = ('device_type', 'parent', 'name')
- def instantiate(self, **kwargs):
- parent = InventoryItemTemplate.objects.get(name=self.parent.name, **kwargs) if self.parent else None
- if self.component:
- model = self.component.component_model
- component = model.objects.get(name=self.component.name, **kwargs)
- else:
- component = None
- return self.component_model(
- parent=parent,
- name=self.name,
- label=self.label,
- component=component,
- role=self.role,
- manufacturer=self.manufacturer,
- part_id=self.part_id,
- **kwargs
- )
|