예제 #1
0
 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]})
예제 #2
0
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,
        )
예제 #3
0
 def get_orientation_desc(self):
     return Orientation.name_from_id(self.orientation)
예제 #4
0
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 '&nbsp;/&nbsp;'.join(result) if self.rack else '&mdash;'

    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()