class VirtualMachine(PrimaryModel, ConfigContextModel): """ A virtual machine which runs inside a Cluster. """ cluster = models.ForeignKey( to='virtualization.Cluster', on_delete=models.PROTECT, related_name='virtual_machines' ) tenant = models.ForeignKey( to='tenancy.Tenant', on_delete=models.PROTECT, related_name='virtual_machines', blank=True, null=True ) platform = models.ForeignKey( to='dcim.Platform', on_delete=models.SET_NULL, related_name='virtual_machines', blank=True, null=True ) name = models.CharField( max_length=64 ) _name = NaturalOrderingField( target_field='name', max_length=100, blank=True ) status = models.CharField( max_length=50, choices=VirtualMachineStatusChoices, default=VirtualMachineStatusChoices.STATUS_ACTIVE, verbose_name='Status' ) role = models.ForeignKey( to='dcim.DeviceRole', on_delete=models.PROTECT, related_name='virtual_machines', limit_choices_to={'vm_role': True}, blank=True, null=True ) primary_ip4 = models.OneToOneField( to='ipam.IPAddress', on_delete=models.SET_NULL, related_name='+', blank=True, null=True, verbose_name='Primary IPv4' ) primary_ip6 = models.OneToOneField( to='ipam.IPAddress', on_delete=models.SET_NULL, related_name='+', blank=True, null=True, verbose_name='Primary IPv6' ) vcpus = models.DecimalField( max_digits=6, decimal_places=2, blank=True, null=True, verbose_name='vCPUs', validators=( MinValueValidator(0.01), ) ) memory = models.PositiveIntegerField( blank=True, null=True, verbose_name='Memory (MB)' ) disk = models.PositiveIntegerField( blank=True, null=True, verbose_name='Disk (GB)' ) comments = models.TextField( blank=True ) secrets = GenericRelation( to='secrets.Secret', content_type_field='assigned_object_type', object_id_field='assigned_object_id', related_query_name='virtual_machine' ) objects = ConfigContextModelQuerySet.as_manager() csv_headers = [ 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', ] clone_fields = [ 'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk', ] class Meta: ordering = ('_name', 'pk') # Name may be non-unique unique_together = [ ['cluster', 'tenant', 'name'] ] def __str__(self): return self.name def get_absolute_url(self): return reverse('virtualization:virtualmachine', args=[self.pk]) def validate_unique(self, exclude=None): # Check for a duplicate name on a VM assigned to the same Cluster and no Tenant. This is necessary # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation # of the uniqueness constraint without manual intervention. if self.tenant is None and VirtualMachine.objects.exclude(pk=self.pk).filter( name=self.name, cluster=self.cluster, tenant__isnull=True ): raise ValidationError({ 'name': 'A virtual machine with this name already exists in the assigned cluster.' }) super().validate_unique(exclude) def clean(self): super().clean() # Validate primary IP addresses interfaces = self.interfaces.all() for field in ['primary_ip4', 'primary_ip6']: ip = getattr(self, field) if ip is not None: if ip.assigned_object in interfaces: pass elif ip.nat_inside is not None and ip.nat_inside.assigned_object in interfaces: pass else: raise ValidationError({ field: f"The specified IP address ({ip}) is not assigned to this VM.", }) def to_csv(self): return ( self.name, self.get_status_display(), self.role.name if self.role else None, self.cluster.name, self.tenant.name if self.tenant else None, self.platform.name if self.platform else None, self.vcpus, self.memory, self.disk, self.comments, ) def get_status_class(self): return VirtualMachineStatusChoices.CSS_CLASSES.get(self.status) @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 @property def site(self): return self.cluster.site
class Device(PrimaryModel, ConfigContextModel): """ 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 site, and optionally to a rack within that site. Associating a device 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/device bay 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( to='dcim.DeviceType', on_delete=models.PROTECT, related_name='instances' ) device_role = models.ForeignKey( to='dcim.DeviceRole', on_delete=models.PROTECT, related_name='devices' ) tenant = models.ForeignKey( to='tenancy.Tenant', on_delete=models.PROTECT, related_name='devices', blank=True, null=True ) platform = models.ForeignKey( to='dcim.Platform', on_delete=models.SET_NULL, related_name='devices', blank=True, null=True ) name = models.CharField( max_length=64, blank=True, null=True ) _name = NaturalOrderingField( target_field='name', max_length=100, blank=True, null=True ) serial = models.CharField( max_length=50, blank=True, verbose_name='Serial number' ) asset_tag = models.CharField( max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag', help_text='A unique tag used to identify this device' ) site = models.ForeignKey( to='dcim.Site', on_delete=models.PROTECT, related_name='devices' ) location = models.ForeignKey( to='dcim.Location', on_delete=models.PROTECT, related_name='devices', blank=True, null=True ) rack = models.ForeignKey( to='dcim.Rack', on_delete=models.PROTECT, related_name='devices', blank=True, null=True ) position = models.PositiveSmallIntegerField( blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)', help_text='The lowest-numbered unit occupied by the device' ) face = models.CharField( max_length=50, blank=True, choices=DeviceFaceChoices, verbose_name='Rack face' ) status = models.CharField( max_length=50, choices=DeviceStatusChoices, default=DeviceStatusChoices.STATUS_ACTIVE ) primary_ip4 = models.OneToOneField( to='ipam.IPAddress', on_delete=models.SET_NULL, related_name='primary_ip4_for', blank=True, null=True, verbose_name='Primary IPv4' ) primary_ip6 = models.OneToOneField( to='ipam.IPAddress', on_delete=models.SET_NULL, related_name='primary_ip6_for', blank=True, null=True, verbose_name='Primary IPv6' ) cluster = models.ForeignKey( to='virtualization.Cluster', on_delete=models.SET_NULL, related_name='devices', blank=True, null=True ) virtual_chassis = models.ForeignKey( to='VirtualChassis', on_delete=models.SET_NULL, related_name='members', blank=True, null=True ) vc_position = models.PositiveSmallIntegerField( blank=True, null=True, validators=[MaxValueValidator(255)] ) vc_priority = models.PositiveSmallIntegerField( blank=True, null=True, validators=[MaxValueValidator(255)] ) comments = models.TextField( blank=True ) images = GenericRelation( to='extras.ImageAttachment' ) secrets = GenericRelation( to='secrets.Secret', content_type_field='assigned_object_type', object_id_field='assigned_object_id', related_query_name='device' ) fabric = models.ForeignKey( to='netbox_ix_plugin.Fabric', on_delete=models.PROTECT, related_name='devices' ) objects = ConfigContextModelQuerySet.as_manager() csv_headers = [ 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'site', 'location', 'rack_name', 'position', 'face', 'comments', ] clone_fields = [ 'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'status', 'cluster', ] class Meta: ordering = ('_name', 'pk') # Name may be null unique_together = ( ('site', 'tenant', 'name'), # See validate_unique below ('rack', 'position', 'face'), ('virtual_chassis', 'vc_position'), ) def __str__(self): return self.display_name or super().__str__() def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) def validate_unique(self, exclude=None): # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation # of the uniqueness constraint without manual intervention. if self.name and hasattr(self, 'site') and self.tenant is None: if Device.objects.exclude(pk=self.pk).filter( name=self.name, site=self.site, tenant__isnull=True ): raise ValidationError({ 'name': 'A device with this name already exists.' }) super().validate_unique(exclude) def clean(self): super().clean() # Validate site/location/rack combination if self.rack and self.site != self.rack.site: raise ValidationError({ 'rack': f"Rack {self.rack} does not belong to site {self.site}.", }) if self.location and self.site != self.location.site: raise ValidationError({ 'location': f"Location {self.location} does not belong to site {self.site}.", }) if self.rack and self.location and self.rack.location != self.location: raise ValidationError({ 'rack': f"Rack {self.rack} does not belong to location {self.location}.", }) elif self.rack: self.location = self.rack.location if self.rack is None: if self.face: raise ValidationError({ 'face': "Cannot select a rack face without assigning a rack.", }) if self.position: raise ValidationError({ 'position': "Cannot select a rack position without assigning a rack.", }) # Validate position/face combination if self.position and not self.face: raise ValidationError({ 'face': "Must specify rack face when defining rack position.", }) # Prevent 0U devices from being assigned to a specific position if self.position and self.device_type.u_height == 0: raise ValidationError({ 'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position." }) if self.rack: try: # Child devices cannot be assigned to a rack face/unit if self.device_type.is_child_device and self.face: raise ValidationError({ 'face': "Child device types cannot be assigned to a rack face. This is an attribute of the " "parent device." }) if self.device_type.is_child_device and self.position: raise ValidationError({ 'position': "Child device types cannot be assigned to a rack position. This is an attribute of " "the parent device." }) # 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 [] 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({ 'position': f"U{self.position} is already occupied or does not have sufficient space to " f"accommodate this device type: {self.device_type} ({self.device_type.u_height}U)" }) except DeviceType.DoesNotExist: pass # Validate primary IP addresses vc_interfaces = self.vc_interfaces(if_master=False) if self.primary_ip4: if self.primary_ip4.family != 4: raise ValidationError({ 'primary_ip4': f"{self.primary_ip4} is not an IPv4 address." }) if self.primary_ip4.assigned_object in vc_interfaces: pass elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.assigned_object in vc_interfaces: pass else: raise ValidationError({ 'primary_ip4': f"The specified IP address ({self.primary_ip4}) is not assigned to this device." }) if self.primary_ip6: if self.primary_ip6.family != 6: raise ValidationError({ 'primary_ip6': f"{self.primary_ip6} is not an IPv6 address." }) if self.primary_ip6.assigned_object in vc_interfaces: pass elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.assigned_object in vc_interfaces: pass else: raise ValidationError({ 'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device." }) # Validate manufacturer/platform if hasattr(self, 'device_type') and self.platform: if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer: raise ValidationError({ 'platform': "The assigned platform is limited to {} device types, but this device's type belongs " "to {}.".format(self.platform.manufacturer, self.device_type.manufacturer) }) # A Device can only be assigned to a Cluster in the same Site (or no Site) if self.cluster and self.cluster.site is not None and self.cluster.site != self.site: raise ValidationError({ 'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site) }) # Validate virtual chassis assignment if self.virtual_chassis and self.vc_position is None: raise ValidationError({ 'vc_position': "A device assigned to a virtual chassis must have its position defined." }) def save(self, *args, **kwargs): is_new = not bool(self.pk) super().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( [x.instantiate(self) for x in self.device_type.consoleporttemplates.all()] ) ConsoleServerPort.objects.bulk_create( [x.instantiate(self) for x in self.device_type.consoleserverporttemplates.all()] ) PowerPort.objects.bulk_create( [x.instantiate(self) for x in self.device_type.powerporttemplates.all()] ) PowerOutlet.objects.bulk_create( [x.instantiate(self) for x in self.device_type.poweroutlettemplates.all()] ) Interface.objects.bulk_create( [x.instantiate(self) for x in self.device_type.interfacetemplates.all()] ) RearPort.objects.bulk_create( [x.instantiate(self) for x in self.device_type.rearporttemplates.all()] ) FrontPort.objects.bulk_create( [x.instantiate(self) for x in self.device_type.frontporttemplates.all()] ) DeviceBay.objects.bulk_create( [x.instantiate(self) for x in self.device_type.devicebaytemplates.all()] ) # Update Site and Rack assignment for any child Devices devices = Device.objects.filter(parent_bay__device=self) for device in devices: device.site = self.site device.rack = self.rack device.save() def to_csv(self): return ( self.name or '', self.device_role.name, self.tenant.name if self.tenant else None, self.device_type.manufacturer.name, self.device_type.model, self.platform.name if self.platform else None, self.serial, self.asset_tag, self.get_status_display(), self.site.name, self.rack.location.name if self.rack and self.rack.location else None, self.rack.name if self.rack else None, self.position, self.get_face_display(), self.comments, ) @property def display_name(self): if self.name: return self.name elif self.virtual_chassis: return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})' elif self.device_type: return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})' else: return '' # Device has not yet been created @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 @property def interfaces_count(self): return self.vc_interfaces().count() def get_vc_master(self): """ If this Device is a VirtualChassis member, return the VC master. Otherwise, return None. """ return self.virtual_chassis.master if self.virtual_chassis else None def vc_interfaces(self, if_master=True): """ Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another Device belonging to the same VirtualChassis. :param if_master: If True, return VC member interfaces only if this Device is the VC master. """ filter = Q(device=self) if self.virtual_chassis and (self.virtual_chassis.master == self or not if_master): filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False) return Interface.objects.filter(filter) def get_cables(self, pk_list=False): """ Return a QuerySet or PK list matching all Cables connected to a component of this Device. """ from .cables import Cable cable_pks = [] for component_model in [ ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort ]: cable_pks += component_model.objects.filter( device=self, cable__isnull=False ).values_list('cable', flat=True) if pk_list: return cable_pks return Cable.objects.filter(pk__in=cable_pks) 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_status_class(self): return DeviceStatusChoices.CSS_CLASSES.get(self.status)