mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
387 lines
11 KiB
Python
387 lines
11 KiB
Python
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
from django.db import models
|
|
|
|
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.querysets import RestrictedQuerySet
|
|
from utilities.ordering import naturalize_interface
|
|
from .device_components import (
|
|
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
|
|
)
|
|
|
|
|
|
__all__ = (
|
|
'ConsolePortTemplate',
|
|
'ConsoleServerPortTemplate',
|
|
'DeviceBayTemplate',
|
|
'FrontPortTemplate',
|
|
'InterfaceTemplate',
|
|
'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
|
|
)
|
|
|
|
objects = RestrictedQuerySet.as_manager()
|
|
|
|
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):
|
|
# 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)
|
|
|
|
|
|
@extras_features('webhooks')
|
|
class ConsolePortTemplate(ComponentTemplateModel):
|
|
"""
|
|
A template for a ConsolePort to be created for a new Device.
|
|
"""
|
|
type = models.CharField(
|
|
max_length=50,
|
|
choices=ConsolePortTypeChoices,
|
|
blank=True
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ('device_type', '_name')
|
|
unique_together = ('device_type', 'name')
|
|
|
|
def instantiate(self, device):
|
|
return ConsolePort(
|
|
device=device,
|
|
name=self.name,
|
|
label=self.label,
|
|
type=self.type
|
|
)
|
|
|
|
|
|
@extras_features('webhooks')
|
|
class ConsoleServerPortTemplate(ComponentTemplateModel):
|
|
"""
|
|
A template for a ConsoleServerPort to be created for a new Device.
|
|
"""
|
|
type = models.CharField(
|
|
max_length=50,
|
|
choices=ConsolePortTypeChoices,
|
|
blank=True
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ('device_type', '_name')
|
|
unique_together = ('device_type', 'name')
|
|
|
|
def instantiate(self, device):
|
|
return ConsoleServerPort(
|
|
device=device,
|
|
name=self.name,
|
|
label=self.label,
|
|
type=self.type
|
|
)
|
|
|
|
|
|
@extras_features('webhooks')
|
|
class PowerPortTemplate(ComponentTemplateModel):
|
|
"""
|
|
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)"
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ('device_type', '_name')
|
|
unique_together = ('device_type', 'name')
|
|
|
|
def instantiate(self, device):
|
|
return PowerPort(
|
|
device=device,
|
|
name=self.name,
|
|
label=self.label,
|
|
type=self.type,
|
|
maximum_draw=self.maximum_draw,
|
|
allocated_draw=self.allocated_draw
|
|
)
|
|
|
|
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(ComponentTemplateModel):
|
|
"""
|
|
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)"
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ('device_type', '_name')
|
|
unique_together = ('device_type', 'name')
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
|
|
# 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,
|
|
label=self.label,
|
|
type=self.type,
|
|
power_port=power_port,
|
|
feed_leg=self.feed_leg
|
|
)
|
|
|
|
|
|
@extras_features('webhooks')
|
|
class InterfaceTemplate(ComponentTemplateModel):
|
|
"""
|
|
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'
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ('device_type', '_name')
|
|
unique_together = ('device_type', 'name')
|
|
|
|
def instantiate(self, device):
|
|
return Interface(
|
|
device=device,
|
|
name=self.name,
|
|
label=self.label,
|
|
type=self.type,
|
|
mgmt_only=self.mgmt_only
|
|
)
|
|
|
|
|
|
@extras_features('webhooks')
|
|
class FrontPortTemplate(ComponentTemplateModel):
|
|
"""
|
|
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)
|
|
]
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ('device_type', '_name')
|
|
unique_together = (
|
|
('device_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, 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,
|
|
label=self.label,
|
|
type=self.type,
|
|
color=self.color,
|
|
rear_port=rear_port,
|
|
rear_port_position=self.rear_port_position
|
|
)
|
|
|
|
|
|
@extras_features('webhooks')
|
|
class RearPortTemplate(ComponentTemplateModel):
|
|
"""
|
|
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)
|
|
]
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ('device_type', '_name')
|
|
unique_together = ('device_type', 'name')
|
|
|
|
def instantiate(self, device):
|
|
return RearPort(
|
|
device=device,
|
|
name=self.name,
|
|
label=self.label,
|
|
type=self.type,
|
|
color=self.color,
|
|
positions=self.positions
|
|
)
|
|
|
|
|
|
@extras_features('webhooks')
|
|
class DeviceBayTemplate(ComponentTemplateModel):
|
|
"""
|
|
A template for a DeviceBay to be created for a new parent Device.
|
|
"""
|
|
class Meta:
|
|
ordering = ('device_type', '_name')
|
|
unique_together = ('device_type', 'name')
|
|
|
|
def instantiate(self, device):
|
|
return DeviceBay(
|
|
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."
|
|
)
|