1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00
netbox-community-netbox/netbox/dcim/models/device_component_templates.py

675 lines
19 KiB
Python
Raw Normal View History

2022-01-19 15:16:10 -05:00
from django.contrib.contenttypes.fields import GenericForeignKey
2021-12-29 15:02:25 -05:00
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
2021-12-29 15:02:25 -05:00
from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import *
from dcim.constants import *
from netbox.models import ChangeLoggedModel
2022-01-19 15:16:10 -05:00
from netbox.models.features import WebhooksMixin
from utilities.fields import ColorField, NaturalOrderingField
2021-12-29 15:02:25 -05:00
from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface
from .device_components import (
2021-12-29 15:02:25 -05:00
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
RearPort,
)
__all__ = (
'ConsolePortTemplate',
'ConsoleServerPortTemplate',
'DeviceBayTemplate',
'FrontPortTemplate',
'InterfaceTemplate',
2021-12-29 15:02:25 -05:00
'InventoryItemTemplate',
'ModuleBayTemplate',
'PowerOutletTemplate',
'PowerPortTemplate',
'RearPortTemplate',
)
2022-01-19 15:16:10 -05:00
class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='%(class)ss'
)
name = models.CharField(
max_length=64,
help_text="""
{module} is accepted as a substitution for the module bay position when attached to a module type.
"""
)
_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
2020-06-10 22:04:45 -04:00
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()
2022-01-26 20:25:23 -05:00
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
objectchange.related_object = self.device_type
return objectchange
2021-12-17 12:18:37 -05:00
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
2022-01-26 20:25:23 -05:00
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
if self.device_type is not None:
objectchange.related_object = self.device_type
elif self.module_type is not None:
objectchange.related_object = self.module_type
return objectchange
2021-12-17 12:18:37 -05:00
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."
)
2021-12-20 09:51:55 -05:00
def resolve_name(self, module):
if module:
2022-05-11 10:37:04 -04:00
return self.name.replace(MODULE_TOKEN, module.module_bay.position)
2021-12-20 09:51:55 -05:00
return self.name
def resolve_label(self, module):
if module:
2022-05-11 10:37:04 -04:00
return self.label.replace(MODULE_TOKEN, module.module_bay.position)
return self.label
2021-12-17 12:18:37 -05:00
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
)
2021-12-29 15:02:25 -05:00
component_model = ConsolePort
class Meta:
2021-12-17 12:18:37 -05:00
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
2021-12-17 16:12:03 -05:00
def instantiate(self, **kwargs):
2021-12-29 15:02:25 -05:00
return self.component_model(
2021-12-20 09:51:55 -05:00
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
2021-12-17 16:12:03 -05:00
type=self.type,
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'label': self.label,
'description': self.description,
}
2021-12-17 12:18:37 -05:00
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
)
2021-12-29 15:02:25 -05:00
component_model = ConsoleServerPort
class Meta:
2021-12-17 12:18:37 -05:00
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
2021-12-17 16:12:03 -05:00
def instantiate(self, **kwargs):
2021-12-29 15:02:25 -05:00
return self.component_model(
2021-12-20 09:51:55 -05:00
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
2021-12-17 16:12:03 -05:00
type=self.type,
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'label': self.label,
'description': self.description,
}
2021-12-17 12:18:37 -05:00
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)"
)
2021-12-29 15:02:25 -05:00
component_model = PowerPort
class Meta:
2021-12-17 12:18:37 -05:00
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
2021-12-17 16:12:03 -05:00
def instantiate(self, **kwargs):
2021-12-29 15:02:25 -05:00
return self.component_model(
2021-12-20 09:51:55 -05:00
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
type=self.type,
maximum_draw=self.maximum_draw,
2021-12-17 16:12:03 -05:00
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)."
})
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'maximum_draw': self.maximum_draw,
'allocated_draw': self.allocated_draw,
'label': self.label,
'description': self.description,
}
2021-12-17 12:18:37 -05:00
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)"
)
2021-12-29 15:02:25 -05:00
component_model = PowerOutlet
class Meta:
2021-12-17 12:18:37 -05:00
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def clean(self):
super().clean()
# Validate power port assignment
2021-12-17 12:18:37 -05:00
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"
)
2021-12-17 16:12:03 -05:00
def instantiate(self, **kwargs):
if self.power_port:
power_port_name = self.power_port.resolve_name(kwargs.get('module'))
power_port = PowerPort.objects.get(name=power_port_name, **kwargs)
else:
power_port = None
2021-12-29 15:02:25 -05:00
return self.component_model(
2021-12-20 09:51:55 -05:00
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
type=self.type,
power_port=power_port,
2021-12-17 16:12:03 -05:00
feed_leg=self.feed_leg,
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'power_port': self.power_port.name if self.power_port else None,
'feed_leg': self.feed_leg,
'label': self.label,
'description': self.description,
}
2021-12-17 12:18:37 -05:00
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'
)
poe_mode = models.CharField(
max_length=50,
choices=InterfacePoEModeChoices,
blank=True,
verbose_name='PoE mode'
)
poe_type = models.CharField(
max_length=50,
choices=InterfacePoETypeChoices,
blank=True,
verbose_name='PoE type'
)
2021-12-29 15:02:25 -05:00
component_model = Interface
class Meta:
2021-12-17 12:18:37 -05:00
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
2021-12-17 16:12:03 -05:00
def instantiate(self, **kwargs):
2021-12-29 15:02:25 -05:00
return self.component_model(
2021-12-20 09:51:55 -05:00
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
type=self.type,
2021-12-17 16:12:03 -05:00
mgmt_only=self.mgmt_only,
poe_mode=self.poe_mode,
poe_type=self.poe_type,
2021-12-17 16:12:03 -05:00
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'mgmt_only': self.mgmt_only,
'label': self.label,
'description': self.description,
2022-08-01 14:42:09 -04:00
'poe_mode': self.poe_mode,
'poe_type': self.poe_type,
}
2021-12-17 12:18:37 -05:00
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)
]
)
2021-12-29 15:02:25 -05:00
component_model = FrontPort
class Meta:
2021-12-17 12:18:37 -05:00
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
2021-12-17 12:18:37 -05:00
('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
2021-12-17 16:12:03 -05:00
def instantiate(self, **kwargs):
if self.rear_port:
rear_port_name = self.rear_port.resolve_name(kwargs.get('module'))
rear_port = RearPort.objects.get(name=rear_port_name, **kwargs)
else:
rear_port = None
2021-12-29 15:02:25 -05:00
return self.component_model(
2021-12-20 09:51:55 -05:00
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
type=self.type,
color=self.color,
rear_port=rear_port,
2021-12-17 16:12:03 -05:00
rear_port_position=self.rear_port_position,
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'color': self.color,
'rear_port': self.rear_port.name,
'rear_port_position': self.rear_port_position,
'label': self.label,
'description': self.description,
}
2021-12-17 12:18:37 -05:00
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)
]
)
2021-12-29 15:02:25 -05:00
component_model = RearPort
class Meta:
2021-12-17 12:18:37 -05:00
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
2021-12-17 16:12:03 -05:00
def instantiate(self, **kwargs):
2021-12-29 15:02:25 -05:00
return self.component_model(
2021-12-20 09:51:55 -05:00
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
type=self.type,
color=self.color,
2021-12-17 16:12:03 -05:00
positions=self.positions,
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'color': self.color,
'positions': self.positions,
'label': self.label,
'description': self.description,
}
class ModuleBayTemplate(ComponentTemplateModel):
"""
A template for a ModuleBay to be created for a new parent Device.
"""
2021-12-20 09:51:55 -05:00
position = models.CharField(
max_length=30,
blank=True,
help_text='Identifier to reference when renaming installed components'
)
2021-12-29 15:02:25 -05:00
component_model = ModuleBay
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def instantiate(self, device):
2021-12-29 15:02:25 -05:00
return self.component_model(
device=device,
name=self.name,
2021-12-20 09:51:55 -05:00
label=self.label,
position=self.position
)
def to_yaml(self):
return {
'name': self.name,
'label': self.label,
'position': self.position,
'description': self.description,
}
class DeviceBayTemplate(ComponentTemplateModel):
"""
A template for a DeviceBay to be created for a new parent Device.
"""
2021-12-29 15:02:25 -05:00
component_model = DeviceBay
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def instantiate(self, device):
2021-12-29 15:02:25 -05:00
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."
)
2021-12-29 15:02:25 -05:00
def to_yaml(self):
return {
'name': self.name,
'label': self.label,
'description': self.description,
}
2021-12-29 15:02:25 -05:00
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 = InventoryItem.objects.get(name=self.parent.name, **kwargs) if self.parent else None
2021-12-29 15:02:25 -05:00
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
)