def _validate_orientation(self): """ Validate if orientation is valid for given position. """ if self.position is None: return if self.position == 0 and not Orientation.is_width(self.orientation): msg = 'Valid orientations for picked position are: {}'.format( ', '.join(choice.desc for choice in Orientation.WIDTH.choices)) raise ValidationError({'orientation': [msg]}) if self.position > 0 and not Orientation.is_depth(self.orientation): msg = 'Valid orientations for picked position are: {}'.format( ', '.join(choice.desc for choice in Orientation.DEPTH.choices)) raise ValidationError({'orientation': [msg]})
class RackAccessory(AdminAbsoluteUrlMixin, models.Model): accessory = models.ForeignKey(Accessory) rack = models.ForeignKey('Rack') orientation = models.PositiveIntegerField( choices=Orientation(), default=Orientation.front.id, ) position = models.IntegerField(null=True, blank=False) remarks = models.CharField( verbose_name='Additional remarks', max_length=1024, blank=True, ) class Meta: verbose_name_plural = _('rack accessories') def get_orientation_desc(self): return Orientation.name_from_id(self.orientation) def __str__(self): rack_name = self.rack.name if self.rack else '' accessory_name = self.accessory.name if self.accessory else '' return '{rack_name} - {accessory_name}'.format( rack_name=rack_name, accessory_name=accessory_name, )
def get_orientation_desc(self): return Orientation.name_from_id(self.orientation)
class DataCenterAsset( PreviousStateMixin, DNSaaSPublisherMixin, WithManagementIPMixin, NetworkableBaseObject, AutocompleteTooltipMixin, Asset ): _allow_in_dashboard = True previous_dc_host_update_fields = ['hostname'] rack = models.ForeignKey( Rack, null=True, blank=False, on_delete=models.PROTECT ) status = TransitionField( default=DataCenterAssetStatus.new.id, choices=DataCenterAssetStatus(), ) position = models.IntegerField(null=True, blank=True) orientation = models.PositiveIntegerField( choices=Orientation(), default=Orientation.front.id, ) slot_no = models.CharField( blank=True, help_text=_('Fill it if asset is blade server'), max_length=3, null=True, validators=[ RegexValidator( regex=VALID_SLOT_NUMBER_FORMAT, message=_( "Slot number should be a number from range 1-16 with " "an optional postfix 'A' or 'B' (e.g. '16A')" ), code='invalid_slot_no' ) ], verbose_name=_('slot number'), ) firmware_version = models.CharField( null=True, blank=True, max_length=256, verbose_name=_('firmware version'), ) bios_version = models.CharField( null=True, blank=True, max_length=256, verbose_name=_('BIOS version'), ) connections = models.ManyToManyField( 'self', through='Connection', symmetrical=False, ) source = models.PositiveIntegerField( blank=True, choices=AssetSource(), db_index=True, null=True, verbose_name=_("source"), ) delivery_date = models.DateField(null=True, blank=True) production_year = models.PositiveSmallIntegerField(null=True, blank=True) production_use_date = models.DateField(null=True, blank=True) autocomplete_tooltip_fields = [ 'rack', 'barcode', 'sn', ] _summary_fields = [ ('hostname', 'Hostname'), ('location', 'Location'), ('model__name', 'Model'), ] class Meta: verbose_name = _('data center asset') verbose_name_plural = _('data center assets') def __str__(self): return '{} (BC: {} / SN: {})'.format( self.hostname or '-', self.barcode or '-', self.sn or '-' ) def __repr__(self): return '<DataCenterAsset: {}>'.format(self.id) def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.pk: # When changing rack we search and save all descendants if self._previous_state['rack_id'] != self.rack_id: DataCenterAsset.objects.filter( parent=self ).update(rack=self.rack) # When changing position if is blade, # we search and save all descendants if self._previous_state['position'] != self.position: DataCenterAsset.objects.filter( parent=self ).update(position=self.position) def get_orientation_desc(self): return Orientation.name_from_id(self.orientation) def get_location(self): location = [] if self.rack: location.extend([ self.rack.server_room.data_center.name, self.rack.server_room.name, self.rack.name ]) if self.position: location.append(str(self.position)) if self.slot_no: location.append(str(self.slot_no)) return location @property def is_blade(self): if self.model_id and self.model.has_parent: return True return False @property def cores_count(self): """Returns cores count assigned to device in Ralph""" asset_cores_count = self.model.cores_count if self.model else 0 return asset_cores_count @cached_property def location(self): """ Additional column 'location' display filter by: data center, server_room, rack, position (if is blade) """ base_url = reverse('admin:data_center_datacenterasset_changelist') position = self.position if self.is_blade: position = generate_html_link( base_url, label=position, params={ 'rack': self.rack_id, 'position__start': self.position, 'position__end': self.position }, ) result = [ generate_html_link( base_url, label=self.rack.server_room.data_center.name, params={ 'rack__server_room__data_center': self.rack.server_room.data_center_id }, ), generate_html_link( base_url, label=self.rack.server_room.name, params={'rack__server_room': self.rack.server_room_id}, ), generate_html_link( base_url, label=self.rack.name, params={'rack': self.rack_id}, ) ] if self.rack and self.rack.server_room else [] if self.position: result.append(str(position)) if self.slot_no: result.append(str(self.slot_no)) return ' / '.join(result) if self.rack else '—' def _validate_orientation(self): """ Validate if orientation is valid for given position. """ if self.position is None: return if self.position == 0 and not Orientation.is_width(self.orientation): msg = 'Valid orientations for picked position are: {}'.format( ', '.join( choice.desc for choice in Orientation.WIDTH.choices ) ) raise ValidationError({'orientation': [msg]}) if self.position > 0 and not Orientation.is_depth(self.orientation): msg = 'Valid orientations for picked position are: {}'.format( ', '.join( choice.desc for choice in Orientation.DEPTH.choices ) ) raise ValidationError({'orientation': [msg]}) def _validate_position(self): """ Validate if position not empty when rack requires it. """ if ( self.rack and self.position is None and self.rack.require_position ): msg = 'Position is required for this rack' raise ValidationError({'position': [msg]}) def _validate_position_in_rack(self): """ Validate if position is in rack height range. """ if ( self.rack and self.position is not None and self.position > self.rack.max_u_height ): msg = 'Position is higher than "max u height" = {}'.format( self.rack.max_u_height, ) raise ValidationError({'position': [msg]}) if self.position is not None and self.position < 0: msg = 'Position should be 0 or greater' raise ValidationError({'position': msg}) def _validate_slot_no(self): if self.model_id: if self.model.has_parent and not self.slot_no: raise ValidationError({ 'slot_no': 'Slot number is required when asset is blade' }) if not self.model.has_parent and self.slot_no: raise ValidationError({ 'slot_no': ( 'Slot number cannot be filled when asset is not blade' ) }) if self.parent: dc_asset_with_slot_no = DataCenterAsset.objects.filter( parent=self.parent, slot_no=self.slot_no, orientation=self.orientation, ).exclude(pk=self.pk).first() if dc_asset_with_slot_no: message = mark_safe( ( 'Slot is already occupied by: ' '<a href="{}" target="_blank">{}</a>' ).format( reverse( 'admin:data_center_datacenterasset_change', args=[dc_asset_with_slot_no.id] ), dc_asset_with_slot_no ) ) raise ValidationError({ 'slot_no': message }) def clean(self): # TODO: this should be default logic of clean method; # we could register somehow validators (or take each func with # _validate prefix) and call it here errors = {} for validator in [ super().clean, self._validate_orientation, self._validate_position, self._validate_position_in_rack, self._validate_slot_no ]: try: validator() except ValidationError as e: e.update_error_dict(errors) if errors: raise ValidationError(errors) def get_related_assets(self): """Returns the children of a blade chassis""" orientations = [Orientation.front, Orientation.back] assets_by_orientation = [] for orientation in orientations: assets_by_orientation.append(list( DataCenterAsset.objects.select_related('model').filter( parent=self, orientation=orientation, model__has_parent=True, ).exclude(id=self.id) )) assets = [ Gap.generate_gaps(assets) for assets in assets_by_orientation ] return chain(*assets) @classmethod def get_autocomplete_queryset(cls): return cls._default_manager.exclude( status=DataCenterAssetStatus.liquidated.id ) @classmethod @transition_action( verbose_name=_('Change rack'), form_fields={ 'rack': { 'field': forms.CharField(widget=AutocompleteWidget( field=rack, admin_site=ralph_site )), } } ) def change_rack(cls, instances, **kwargs): rack = Rack.objects.get(pk=kwargs['rack']) for instance in instances: instance.rack = rack @classmethod @transition_action( verbose_name=_('Convert to BackOffice Asset'), disable_save_object=True, only_one_action=True, form_fields={ 'warehouse': { 'field': forms.CharField(label=_('Warehouse')), 'autocomplete_field': 'warehouse', 'autocomplete_model': 'back_office.BackOfficeAsset' }, 'region': { 'field': forms.CharField(label=_('Region')), 'autocomplete_field': 'region', 'autocomplete_model': 'back_office.BackOfficeAsset' } } ) def convert_to_backoffice_asset(cls, instances, **kwargs): with transaction.atomic(): for i, instance in enumerate(instances): back_office_asset = BackOfficeAsset() back_office_asset.region = Region.objects.get( pk=kwargs['region'] ) back_office_asset.warehouse = Warehouse.objects.get( pk=kwargs['warehouse'] ) target_status = int( Transition.objects.values_list('target', flat=True).get(pk=kwargs['transition_id']) # noqa ) back_office_asset.status = dc_asset_to_bo_asset_status_converter( # noqa instance.status, target_status ) move_parents_models( instance, back_office_asset, exclude_copy_fields=['status'] ) # Save new asset to list, required to redirect url. # RunTransitionView.get_success_url() instances[i] = back_office_asset @classmethod @transition_action( verbose_name=_('Cleanup scm status'), ) def cleanup_scm_statuscheck(cls, instances, **kwargs): with transaction.atomic(): for instance in instances: try: instance.scmstatuscheck.delete() except DataCenterAsset.scmstatuscheck.\ RelatedObjectDoesNotExist: pass @classmethod @transition_action( verbose_name=_('Assign additional IP and hostname pair'), form_fields={ 'network_pk': { 'field': forms.ChoiceField( label=_('Select network') ), 'choices': assign_additional_hostname_choices, 'exclude_from_history': True, }, }, ) def assign_additional_hostname(cls, instances, network_pk, **kwargs): """ Assign new hostname for instances based on selected network. """ network = Network.objects.get(pk=network_pk) env = network.network_environment with transaction.atomic(): for instance in instances: ethernet = Ethernet.objects.create(base_object=instance) ethernet.ipaddress = network.issue_next_free_ip() ethernet.ipaddress.hostname = env.issue_next_free_hostname() ethernet.ipaddress.save() ethernet.save()