1from django.core.exceptions import ObjectDoesNotExist, ValidationError 2from django.core.validators import MaxValueValidator, MinValueValidator 3from django.db import models 4 5from dcim.choices import * 6from dcim.constants import * 7from extras.utils import extras_features 8from netbox.models import ChangeLoggedModel 9from utilities.fields import ColorField, NaturalOrderingField 10from utilities.querysets import RestrictedQuerySet 11from utilities.ordering import naturalize_interface 12from .device_components import ( 13 ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort, 14) 15 16 17__all__ = ( 18 'ConsolePortTemplate', 19 'ConsoleServerPortTemplate', 20 'DeviceBayTemplate', 21 'FrontPortTemplate', 22 'InterfaceTemplate', 23 'PowerOutletTemplate', 24 'PowerPortTemplate', 25 'RearPortTemplate', 26) 27 28 29class ComponentTemplateModel(ChangeLoggedModel): 30 device_type = models.ForeignKey( 31 to='dcim.DeviceType', 32 on_delete=models.CASCADE, 33 related_name='%(class)ss' 34 ) 35 name = models.CharField( 36 max_length=64 37 ) 38 _name = NaturalOrderingField( 39 target_field='name', 40 max_length=100, 41 blank=True 42 ) 43 label = models.CharField( 44 max_length=64, 45 blank=True, 46 help_text="Physical label" 47 ) 48 description = models.CharField( 49 max_length=200, 50 blank=True 51 ) 52 53 objects = RestrictedQuerySet.as_manager() 54 55 class Meta: 56 abstract = True 57 58 def __str__(self): 59 if self.label: 60 return f"{self.name} ({self.label})" 61 return self.name 62 63 def instantiate(self, device): 64 """ 65 Instantiate a new component on the specified Device. 66 """ 67 raise NotImplementedError() 68 69 def to_objectchange(self, action): 70 # Annotate the parent DeviceType 71 try: 72 device_type = self.device_type 73 except ObjectDoesNotExist: 74 # The parent DeviceType has already been deleted 75 device_type = None 76 return super().to_objectchange(action, related_object=device_type) 77 78 79@extras_features('webhooks') 80class ConsolePortTemplate(ComponentTemplateModel): 81 """ 82 A template for a ConsolePort to be created for a new Device. 83 """ 84 type = models.CharField( 85 max_length=50, 86 choices=ConsolePortTypeChoices, 87 blank=True 88 ) 89 90 class Meta: 91 ordering = ('device_type', '_name') 92 unique_together = ('device_type', 'name') 93 94 def instantiate(self, device): 95 return ConsolePort( 96 device=device, 97 name=self.name, 98 label=self.label, 99 type=self.type 100 ) 101 102 103@extras_features('webhooks') 104class ConsoleServerPortTemplate(ComponentTemplateModel): 105 """ 106 A template for a ConsoleServerPort to be created for a new Device. 107 """ 108 type = models.CharField( 109 max_length=50, 110 choices=ConsolePortTypeChoices, 111 blank=True 112 ) 113 114 class Meta: 115 ordering = ('device_type', '_name') 116 unique_together = ('device_type', 'name') 117 118 def instantiate(self, device): 119 return ConsoleServerPort( 120 device=device, 121 name=self.name, 122 label=self.label, 123 type=self.type 124 ) 125 126 127@extras_features('webhooks') 128class PowerPortTemplate(ComponentTemplateModel): 129 """ 130 A template for a PowerPort to be created for a new Device. 131 """ 132 type = models.CharField( 133 max_length=50, 134 choices=PowerPortTypeChoices, 135 blank=True 136 ) 137 maximum_draw = models.PositiveSmallIntegerField( 138 blank=True, 139 null=True, 140 validators=[MinValueValidator(1)], 141 help_text="Maximum power draw (watts)" 142 ) 143 allocated_draw = models.PositiveSmallIntegerField( 144 blank=True, 145 null=True, 146 validators=[MinValueValidator(1)], 147 help_text="Allocated power draw (watts)" 148 ) 149 150 class Meta: 151 ordering = ('device_type', '_name') 152 unique_together = ('device_type', 'name') 153 154 def instantiate(self, device): 155 return PowerPort( 156 device=device, 157 name=self.name, 158 label=self.label, 159 type=self.type, 160 maximum_draw=self.maximum_draw, 161 allocated_draw=self.allocated_draw 162 ) 163 164 def clean(self): 165 super().clean() 166 167 if self.maximum_draw is not None and self.allocated_draw is not None: 168 if self.allocated_draw > self.maximum_draw: 169 raise ValidationError({ 170 'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)." 171 }) 172 173 174@extras_features('webhooks') 175class PowerOutletTemplate(ComponentTemplateModel): 176 """ 177 A template for a PowerOutlet to be created for a new Device. 178 """ 179 type = models.CharField( 180 max_length=50, 181 choices=PowerOutletTypeChoices, 182 blank=True 183 ) 184 power_port = models.ForeignKey( 185 to='dcim.PowerPortTemplate', 186 on_delete=models.SET_NULL, 187 blank=True, 188 null=True, 189 related_name='poweroutlet_templates' 190 ) 191 feed_leg = models.CharField( 192 max_length=50, 193 choices=PowerOutletFeedLegChoices, 194 blank=True, 195 help_text="Phase (for three-phase feeds)" 196 ) 197 198 class Meta: 199 ordering = ('device_type', '_name') 200 unique_together = ('device_type', 'name') 201 202 def clean(self): 203 super().clean() 204 205 # Validate power port assignment 206 if self.power_port and self.power_port.device_type != self.device_type: 207 raise ValidationError( 208 "Parent power port ({}) must belong to the same device type".format(self.power_port) 209 ) 210 211 def instantiate(self, device): 212 if self.power_port: 213 power_port = PowerPort.objects.get(device=device, name=self.power_port.name) 214 else: 215 power_port = None 216 return PowerOutlet( 217 device=device, 218 name=self.name, 219 label=self.label, 220 type=self.type, 221 power_port=power_port, 222 feed_leg=self.feed_leg 223 ) 224 225 226@extras_features('webhooks') 227class InterfaceTemplate(ComponentTemplateModel): 228 """ 229 A template for a physical data interface on a new Device. 230 """ 231 # Override ComponentTemplateModel._name to specify naturalize_interface function 232 _name = NaturalOrderingField( 233 target_field='name', 234 naturalize_function=naturalize_interface, 235 max_length=100, 236 blank=True 237 ) 238 type = models.CharField( 239 max_length=50, 240 choices=InterfaceTypeChoices 241 ) 242 mgmt_only = models.BooleanField( 243 default=False, 244 verbose_name='Management only' 245 ) 246 247 class Meta: 248 ordering = ('device_type', '_name') 249 unique_together = ('device_type', 'name') 250 251 def instantiate(self, device): 252 return Interface( 253 device=device, 254 name=self.name, 255 label=self.label, 256 type=self.type, 257 mgmt_only=self.mgmt_only 258 ) 259 260 261@extras_features('webhooks') 262class FrontPortTemplate(ComponentTemplateModel): 263 """ 264 Template for a pass-through port on the front of a new Device. 265 """ 266 type = models.CharField( 267 max_length=50, 268 choices=PortTypeChoices 269 ) 270 color = ColorField( 271 blank=True 272 ) 273 rear_port = models.ForeignKey( 274 to='dcim.RearPortTemplate', 275 on_delete=models.CASCADE, 276 related_name='frontport_templates' 277 ) 278 rear_port_position = models.PositiveSmallIntegerField( 279 default=1, 280 validators=[ 281 MinValueValidator(REARPORT_POSITIONS_MIN), 282 MaxValueValidator(REARPORT_POSITIONS_MAX) 283 ] 284 ) 285 286 class Meta: 287 ordering = ('device_type', '_name') 288 unique_together = ( 289 ('device_type', 'name'), 290 ('rear_port', 'rear_port_position'), 291 ) 292 293 def clean(self): 294 super().clean() 295 296 try: 297 298 # Validate rear port assignment 299 if self.rear_port.device_type != self.device_type: 300 raise ValidationError( 301 "Rear port ({}) must belong to the same device type".format(self.rear_port) 302 ) 303 304 # Validate rear port position assignment 305 if self.rear_port_position > self.rear_port.positions: 306 raise ValidationError( 307 "Invalid rear port position ({}); rear port {} has only {} positions".format( 308 self.rear_port_position, self.rear_port.name, self.rear_port.positions 309 ) 310 ) 311 312 except RearPortTemplate.DoesNotExist: 313 pass 314 315 def instantiate(self, device): 316 if self.rear_port: 317 rear_port = RearPort.objects.get(device=device, name=self.rear_port.name) 318 else: 319 rear_port = None 320 return FrontPort( 321 device=device, 322 name=self.name, 323 label=self.label, 324 type=self.type, 325 color=self.color, 326 rear_port=rear_port, 327 rear_port_position=self.rear_port_position 328 ) 329 330 331@extras_features('webhooks') 332class RearPortTemplate(ComponentTemplateModel): 333 """ 334 Template for a pass-through port on the rear of a new Device. 335 """ 336 type = models.CharField( 337 max_length=50, 338 choices=PortTypeChoices 339 ) 340 color = ColorField( 341 blank=True 342 ) 343 positions = models.PositiveSmallIntegerField( 344 default=1, 345 validators=[ 346 MinValueValidator(REARPORT_POSITIONS_MIN), 347 MaxValueValidator(REARPORT_POSITIONS_MAX) 348 ] 349 ) 350 351 class Meta: 352 ordering = ('device_type', '_name') 353 unique_together = ('device_type', 'name') 354 355 def instantiate(self, device): 356 return RearPort( 357 device=device, 358 name=self.name, 359 label=self.label, 360 type=self.type, 361 color=self.color, 362 positions=self.positions 363 ) 364 365 366@extras_features('webhooks') 367class DeviceBayTemplate(ComponentTemplateModel): 368 """ 369 A template for a DeviceBay to be created for a new parent Device. 370 """ 371 class Meta: 372 ordering = ('device_type', '_name') 373 unique_together = ('device_type', 'name') 374 375 def instantiate(self, device): 376 return DeviceBay( 377 device=device, 378 name=self.name, 379 label=self.label 380 ) 381 382 def clean(self): 383 if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT: 384 raise ValidationError( 385 f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays." 386 ) 387