class Rack(CreatedUpdatedModel, CustomFieldModel): """ Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Each Rack is assigned to a Site and (optionally) a RackGroup. """ name = models.CharField(max_length=50) facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID') site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT) group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL) tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT) role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT) type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type') width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width', help_text='Rail-to-rail width') u_height = models.PositiveSmallIntegerField( default=42, verbose_name='Height (U)', validators=[MinValueValidator(1), MaxValueValidator(100)]) comments = models.TextField(blank=True) custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') objects = RackManager() class Meta: ordering = ['site', 'name'] unique_together = [ ['site', 'name'], ['site', 'facility_id'], ] def __unicode__(self): return self.display_name def get_absolute_url(self): return reverse('dcim:rack', args=[self.pk]) def clean(self): # Validate that Rack is tall enough to house the installed Devices if self.pk: top_device = Device.objects.filter(rack=self).exclude( position__isnull=True).order_by('-position').first() if top_device: min_height = top_device.position + top_device.device_type.u_height - 1 if self.u_height < min_height: raise ValidationError( "Rack must be at least {}U tall with currently installed devices." .format(min_height)) def to_csv(self): return ','.join([ self.site.name, self.group.name if self.group else '', self.name, self.facility_id or '', self.tenant.name if self.tenant else '', self.role.name if self.role else '', self.get_type_display() if self.type else '', str(self.width), str(self.u_height), ]) @property def units(self): return reversed(range(1, self.u_height + 1)) @property def display_name(self): if self.facility_id: return u"{} ({})".format(self.name, self.facility_id) return self.name def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack :param remove_redundant: If True, rack units occupied by a device already listed will be omitted """ elevation = OrderedDict() for u in reversed(range(1, self.u_height + 1)): elevation[u] = { 'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None } # Add devices to rack units list if self.pk: for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\ .annotate(devicebay_count=Count('device_bays'))\ .exclude(pk=exclude)\ .filter(rack=self, position__gt=0)\ .filter(Q(face=face) | Q(device_type__is_full_depth=True)): if remove_redundant: elevation[device.position]['device'] = device for u in range( device.position + 1, device.position + device.device_type.u_height): elevation.pop(u, None) else: for u in range( device.position, device.position + device.device_type.u_height): elevation[u]['device'] = device return [u for u in elevation.values()] def get_front_elevation(self): return self.get_rack_units(face=RACK_FACE_FRONT, remove_redundant=True) def get_rear_elevation(self): return self.get_rack_units(face=RACK_FACE_REAR, remove_redundant=True) def get_available_units(self, u_height=1, rack_face=None, exclude=list()): """ Return a list of units within the rack available to accommodate a device of a given U height (default 1). Optionally exclude one or more devices when calculating empty units (needed when moving a device from one position to another within a rack). :param u_height: Minimum number of contiguous free units required :param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth :param exclude: List of devices IDs to exclude (useful when moving a device within a rack) """ # Gather all devices which consume U space within the rack devices = self.devices.select_related().filter( position__gte=1).exclude(pk__in=exclude) # Initialize the rack unit skeleton units = range(1, self.u_height + 1) # Remove units consumed by installed devices for d in devices: if rack_face is None or d.face == rack_face or d.device_type.is_full_depth: for u in range(d.position, d.position + d.device_type.u_height): try: units.remove(u) except ValueError: # Found overlapping devices in the rack! pass # Remove units without enough space above them to accommodate a device of the specified height available_units = [] for u in units: if set(range(u, u + u_height)).issubset(units): available_units.append(u) return list(reversed(available_units)) def get_0u_devices(self): return self.devices.filter(position=0) def get_utilization(self): """ Determine the utilization rate of the rack and return it as a percentage. """ if self.u_consumed is None: self.u_consumed = 0 u_available = self.u_height - self.u_consumed return int(float(self.u_height - u_available) / self.u_height * 100)
class Device(CreatedUpdatedModel, 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 Rack, although associating it 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 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('DeviceType', related_name='instances', on_delete=models.PROTECT) device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT) tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT) platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL) name = NullableCharField(max_length=50, blank=True, null=True, unique=True) serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number') asset_tag = NullableCharField( max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag', help_text='A unique tag used to identify this device') rack = models.ForeignKey('Rack', related_name='devices', on_delete=models.PROTECT) position = models.PositiveSmallIntegerField( blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)', help_text='Number of the lowest U position occupied by the device') face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face') status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status') primary_ip4 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='Primary IPv4') primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='Primary IPv6') comments = models.TextField(blank=True) custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') objects = DeviceManager() class Meta: ordering = ['name'] unique_together = ['rack', 'position', 'face'] def __unicode__(self): return self.display_name def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) def clean(self): # Validate device type assignment if not hasattr(self, 'device_type'): raise ValidationError("Must specify device type.") # Child devices cannot be assigned to a rack face/unit if self.device_type.is_child_device and (self.face is not None or self.position): raise ValidationError( "Child device types cannot be assigned a rack face or position." ) # Validate position/face combination if self.position and self.face is None: raise ValidationError("Must specify rack face with rack position.") # 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( "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 def save(self, *args, **kwargs): is_new = not bool(self.pk) super(Device, self).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([ ConsolePort(device=self, name=template.name) for template in self.device_type.console_port_templates.all() ]) ConsoleServerPort.objects.bulk_create([ ConsoleServerPort(device=self, name=template.name) for template in self.device_type.cs_port_templates.all() ]) PowerPort.objects.bulk_create([ PowerPort(device=self, name=template.name) for template in self.device_type.power_port_templates.all() ]) PowerOutlet.objects.bulk_create([ PowerOutlet(device=self, name=template.name) for template in self.device_type.power_outlet_templates.all() ]) Interface.objects.bulk_create([ Interface(device=self, name=template.name, form_factor=template.form_factor, mgmt_only=template.mgmt_only) for template in self.device_type.interface_templates.all() ]) DeviceBay.objects.bulk_create([ DeviceBay(device=self, name=template.name) for template in self.device_type.device_bay_templates.all() ]) # Update Rack assignment for any child Devices Device.objects.filter(parent_bay__device=self).update(rack=self.rack) def to_csv(self): return ','.join([ self.name or '', self.device_role.name, self.tenant.name if self.tenant else '', self.device_type.manufacturer.name, self.device_type.model, self.platform.name if self.platform else '', self.serial, self.asset_tag if self.asset_tag else '', self.rack.site.name, self.rack.name, str(self.position) if self.position else '', self.get_face_display() or '', ]) @property def display_name(self): if self.name: return self.name elif self.position: return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position) else: return u"{} ({})".format(self.device_type, self.rack.name) @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_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_rpc_client(self): """ Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined. """ if not self.platform: return None return RPC_CLIENTS.get(self.platform.rpc_client)
class Rack(CreatedUpdatedModel): """ Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Each Rack is assigned to a Site and (optionally) a RackGroup. """ name = models.CharField(max_length=50) facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID') site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT) group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL) u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)') comments = models.TextField(blank=True) class Meta: ordering = ['site', 'name'] unique_together = [ ['site', 'name'], ['site', 'facility_id'], ] def __unicode__(self): return self.display_name def get_absolute_url(self): return reverse('dcim:rack', args=[self.pk]) def to_csv(self): return ','.join([ self.site.name, self.group.name if self.group else '', self.name, self.facility_id or '', str(self.u_height), ]) @property def units(self): return reversed(range(1, self.u_height + 1)) @property def display_name(self): if self.facility_id: return "{} ({})".format(self.name, self.facility_id) return self.name def get_rack_units(self, face=RACK_FACE_FRONT, remove_redundant=False): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) :param remove_redundant: If True, rack units occupied by a device already listed will be omitted """ elevation = OrderedDict() for u in reversed(range(1, self.u_height + 1)): elevation[u] = { 'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None } # Add devices to rack units list if self.pk: for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\ .filter(rack=self, position__gt=0).filter(Q(face=face) | Q(device_type__is_full_depth=True)): if remove_redundant: elevation[device.position]['device'] = device for u in range( device.position + 1, device.position + device.device_type.u_height): elevation.pop(u, None) else: for u in range( device.position, device.position + device.device_type.u_height): elevation[u]['device'] = device return [u for u in elevation.values()] def get_front_elevation(self): return self.get_rack_units(face=RACK_FACE_FRONT, remove_redundant=True) def get_rear_elevation(self): return self.get_rack_units(face=RACK_FACE_REAR, remove_redundant=True) def get_available_units(self, u_height=1, rack_face=None, exclude=list()): """ Return a list of units within the rack available to accommodate a device of a given U height (default 1). Optionally exclude one or more devices when calculating empty units (needed when moving a device from one position to another within a rack). :param u_height: Minimum number of contiguous free units required :param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth :param exclude: List of devices IDs to exclude (useful when moving a device within a rack) """ # Gather all devices which consume U space within the rack devices = self.devices.select_related().filter( position__gte=1).exclude(pk__in=exclude) # Initialize the rack unit skeleton units = range(1, self.u_height + 1) # Remove units consumed by installed devices for d in devices: if rack_face is None or d.face == rack_face or d.device_type.is_full_depth: for u in range(d.position, d.position + d.device_type.u_height): try: units.remove(u) except ValueError: # Found overlapping devices in the rack! pass # Remove units without enough space above them to accommodate a device of the specified height available_units = [] for u in units: if set(range(u, u + u_height)).issubset(units): available_units.append(u) return list(reversed(available_units)) def get_0u_devices(self): return self.devices.filter(position=0)