class GenericComponent(Component): label = models.CharField( verbose_name=_('label'), max_length=255, blank=True, null=True, default=None, ) sn = NullableCharField( verbose_name=_('vendor SN'), max_length=255, unique=True, null=True, blank=True, default=None, ) class Meta: verbose_name = _('generic component') verbose_name_plural = _('generic components')
class DiskShare(Component): share_id = models.PositiveIntegerField( verbose_name=_('share identifier'), null=True, blank=True, ) label = models.CharField( verbose_name=_('name'), max_length=255, blank=True, null=True, default=None, ) size = models.PositiveIntegerField( verbose_name=_('size (MiB)'), null=True, blank=True, ) snapshot_size = models.PositiveIntegerField( verbose_name=_('size for snapshots (MiB)'), null=True, blank=True, ) wwn = NullableCharField( verbose_name=_('Volume serial'), max_length=33, unique=True, ) full = models.BooleanField(default=True) def get_total_size(self): return (self.size or 0) + (self.snapshot_size or 0) class Meta: verbose_name = _('disk share') verbose_name_plural = _('disk shares') def __str__(self): return '%s (%s)' % (self.label, self.wwn)
class Service(PermByFieldMixin, AdminAbsoluteUrlMixin, NamedMixin, TimeStampMixin, models.Model): # Fixme: let's do service catalog replacement from that _allow_in_dashboard = True active = models.BooleanField(default=True) uid = NullableCharField(max_length=40, unique=True, blank=True, null=True) profit_center = models.ForeignKey(ProfitCenter, null=True, blank=True) business_segment = models.ForeignKey(BusinessSegment, null=True, blank=True) cost_center = models.CharField(max_length=100, blank=True) environments = models.ManyToManyField('Environment', through='ServiceEnvironment') business_owners = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name='services_business_owner', blank=True, ) technical_owners = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name='services_technical_owner', blank=True, ) support_team = models.ForeignKey( Team, null=True, blank=True, related_name='services', ) def __str__(self): return '{}'.format(self.name) @classmethod def get_autocomplete_queryset(cls): return cls._default_manager.filter(active=True)
class Transition(models.Model): name = models.CharField(max_length=50) model = models.ForeignKey(TransitionModel) run_asynchronously = models.BooleanField( default=False, help_text=_( 'Run this transition in the background (this could be enforced if ' 'you choose at least one asynchronous action)')) async_service_name = models.CharField( max_length=100, blank=True, null=True, default=DEFAULT_ASYNC_TRANSITION_SERVICE_NAME, help_text=_( 'Name of asynchronous (internal) service to run this transition. ' 'Fill this field only if you want to run this transition in the ' 'background.')) source = JSONField() target = models.CharField(max_length=50) actions = models.ManyToManyField('Action') template_name = models.CharField(max_length=255, blank=True, default='') success_url = NullableCharField(max_length=255, blank=True, null=True, default=None) class Meta: unique_together = ('name', 'model') app_label = 'transitions' def __str__(self): return self.name @property def permission_info(self): return { 'name': 'Can run {} transition'.format(self.name.lower()), 'content_type': self.model.content_type, 'codename': 'can_run_{}_transition'.format(slugify(self.name)) } @property def model_cls(self): return self.model.content_type.model_class() @property def is_async(self): return (self.run_asynchronously or any([func.is_async for func in self.get_pure_actions()])) @classmethod def transitions_for_model(cls, model, user=None): content_type = ContentType.objects.get_for_model(model) transitions = cls.objects.filter(model__content_type=content_type) return [ transition for transition in transitions if _check_user_perm_for_transition(user, transition) ] def _get_metric_name(self): return '{}.{}'.format(self.name, self.model.content_type.model) def get_pure_actions(self): return [ getattr(self.model_cls, action.name) for action in self.actions.all() ] def has_form(self): for action in self.get_pure_actions(): if getattr(action, 'form_fields', None): return True return False
class Asset(AdminAbsoluteUrlMixin, BaseObject): model = models.ForeignKey(AssetModel, related_name='assets', on_delete=models.PROTECT) # TODO: unify hostname for DCA, VirtualServer, Cluster and CloudHost # (use another model?) hostname = NullableCharField( blank=True, default=None, max_length=255, null=True, verbose_name=_('hostname'), # TODO: unique ) sn = NullableCharField( blank=True, max_length=200, null=True, verbose_name=_('SN'), unique=True, ) barcode = NullableCharField(blank=True, default=None, max_length=200, null=True, unique=True, verbose_name=_('barcode')) niw = NullableCharField( blank=True, default=None, max_length=200, null=True, verbose_name=_('inventory number'), ) required_support = models.BooleanField(default=False) order_no = models.CharField( verbose_name=_('order number'), blank=True, max_length=50, null=True, ) invoice_no = models.CharField( verbose_name=_('invoice number'), blank=True, db_index=True, max_length=128, null=True, ) invoice_date = models.DateField(blank=True, null=True) price = models.DecimalField( blank=True, decimal_places=2, default=0, max_digits=10, null=True, ) # to discuss: foreign key? provider = models.CharField( blank=True, max_length=100, null=True, ) depreciation_rate = models.DecimalField( blank=True, decimal_places=2, default=settings.DEFAULT_DEPRECIATION_RATE, help_text=_( 'This value is in percentage.' ' For example value: "100" means it depreciates during a year.' ' Value: "25" means it depreciates during 4 years, and so on... .' ), max_digits=5, ) force_depreciation = models.BooleanField( help_text=('Check if you no longer want to bill for this asset'), default=False, ) depreciation_end_date = models.DateField(blank=True, null=True) buyout_date = models.DateField(blank=True, null=True, db_index=True) task_url = models.URLField( blank=True, help_text=('External workflow system URL'), max_length=2048, null=True, ) budget_info = models.ForeignKey( BudgetInfo, blank=True, default=None, null=True, on_delete=models.PROTECT, ) property_of = models.ForeignKey( AssetHolder, on_delete=models.PROTECT, null=True, blank=True, ) start_usage = models.DateField( blank=True, null=True, help_text=( 'Fill it if date of first usage is different then date of creation' )) def __str__(self): return self.hostname or '' def calculate_buyout_date(self): """ Get buyout date. Calculate buyout date invoice_date + depreciation_rate months Returns: Deprecation date """ if self.depreciation_end_date: return self.depreciation_end_date elif self.invoice_date: return self.invoice_date + relativedelta( months=self.get_depreciation_months() + 1) else: return None def get_depreciation_months(self): return int((1 / (self.depreciation_rate / 100) * 12) if self.depreciation_rate else 0) def is_depreciated(self, date=None): date = date or datetime.date.today() if self.force_depreciation or not self.invoice_date: return True if self.depreciation_end_date: deprecation_date = self.deprecation_end_date else: deprecation_date = self.invoice_date + relativedelta( months=self.get_depreciation_months(), ) return deprecation_date < date def get_depreciated_months(self): # DEPRECATED # BACKWARD_COMPATIBILITY return self.get_depreciation_months() def is_deprecated(self, date=None): # DEPRECATED # BACKWARD_COMPATIBILITY return self.is_depreciated() def _liquidated_at(self, date): liquidated_history = self.get_history().filter( new_value='liquidated', field_name='status', ).order_by('-date')[:1] return liquidated_history and liquidated_history[0].date.date() <= date def clean(self): if not self.sn and not self.barcode: error_message = [_('SN or BARCODE field is required')] raise ValidationError({ 'sn': error_message, 'barcode': error_message }) def save(self, *args, **kwargs): # if you save barcode as empty string (instead of None) you could have # only one asset with empty barcode (because of `unique` constraint) # if you save barcode as None you could have many assets with empty # barcode (becasue `unique` constrainst is skipped) for unique_field in ['barcode', 'sn']: value = getattr(self, unique_field, None) if value == '': value = None setattr(self, unique_field, value) self.buyout_date = self.calculate_buyout_date() return super(Asset, self).save(*args, **kwargs)
class VirtualServer(PreviousStateMixin, DNSaaSPublisherMixin, AdminAbsoluteUrlMixin, NetworkableBaseObject, BaseObject): # parent field for VirtualServer is hypervisor! # TODO: limit parent to DataCenterAsset and CloudHost status = TransitionField( default=VirtualServerStatus.new.id, choices=VirtualServerStatus(), ) type = models.ForeignKey(VirtualServerType, related_name='virtual_servers') hostname = NullableCharField( blank=True, default=None, max_length=255, null=True, verbose_name=_('hostname'), unique=True, ) sn = NullableCharField( max_length=200, verbose_name=_('SN'), blank=True, default=None, null=True, unique=True, ) # TODO: remove this field cluster = models.ForeignKey(Cluster, blank=True, null=True) previous_dc_host_update_fields = ['hostname'] _allow_in_dashboard = True @cached_property def polymorphic_parent(self): return self.parent.last_descendant if self.parent_id else None def get_location(self): if (self.parent_id and self.parent.content_type_id == ContentType.objects.get_for_model(DataCenterAsset).id): parent = self.parent.asset.datacenterasset location = parent.get_location() if parent.hostname: location.append(parent.hostname) else: location = [] return location @property def model(self): return self.type @cached_property def rack_id(self): return self.rack.id if self.rack else None @cached_property def rack(self): if self.parent_id: polymorphic_parent = self.polymorphic_parent.last_descendant if (isinstance(polymorphic_parent, (DataCenterAsset, CloudHost))): return polymorphic_parent.rack return None class Meta: verbose_name = _('Virtual server (VM)') verbose_name_plural = _('Virtual servers (VM)') def __str__(self): return 'VirtualServer: {} ({})'.format(self.hostname, self.sn)
class BackOfficeAsset(Regionalizable, Asset): _allow_in_dashboard = True warehouse = models.ForeignKey(Warehouse, on_delete=models.PROTECT) owner = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, related_name='assets_as_owner', ) user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, related_name='assets_as_user', ) location = models.CharField(max_length=128, null=True, blank=True) purchase_order = models.CharField(max_length=50, null=True, blank=True) loan_end_date = models.DateField( null=True, blank=True, default=None, verbose_name=_('Loan end date'), ) status = TransitionField( default=BackOfficeAssetStatus.new.id, choices=BackOfficeAssetStatus(), ) imei = NullableCharField(max_length=18, null=True, blank=True, unique=True, verbose_name=_('IMEI')) imei2 = NullableCharField(max_length=18, null=True, blank=True, unique=True, verbose_name=_('IMEI 2')) office_infrastructure = models.ForeignKey(OfficeInfrastructure, null=True, blank=True) class Meta: verbose_name = _('Back Office Asset') verbose_name_plural = _('Back Office Assets') @property def country_code(self): iso2 = Country.name_from_id(int(self.region.country)).upper() return iso2_to_iso3(iso2) def __str__(self): return '{} (BC: {} / SN: {})'.format(self.hostname or '-', self.barcode or '-', self.sn or '-') def __repr__(self): return '<BackOfficeAsset: {}>'.format(self.id) def validate_imei(self, imei): return IMEI_SINCE_2003.match(imei) or IMEI_UNTIL_2003.match(imei) def clean(self): super().clean() if self.imei and not self.validate_imei(self.imei): raise ValidationError({ 'imei': _('%(imei)s is not IMEI format') % { 'imei': self.imei } }) if self.imei2 and not self.validate_imei(self.imei2): raise ValidationError({ 'imei2': _('%(imei)s is not IMEI format') % { 'imei': self.imei2 } # noqa }) def is_liquidated(self, date=None): date = date or datetime.date.today() # check if asset has status 'liquidated' and if yes, check if it has # this status on given date if (self.status == BackOfficeAssetStatus.liquidated and self._liquidated_at(date)): return True return False def generate_hostname(self, commit=True, template_vars=None): def render_template(template): template = Template(template) context = Context(template_vars or {}) return template.render(context) logger.warning( 'Generating new hostname for {} using {} old hostname {}'.format( self, template_vars, self.hostname)) prefix = render_template(ASSET_HOSTNAME_TEMPLATE.get('prefix', ''), ) postfix = render_template(ASSET_HOSTNAME_TEMPLATE.get('postfix', ''), ) counter_length = ASSET_HOSTNAME_TEMPLATE.get('counter_length', 5) last_hostname = AssetLastHostname.increment_hostname(prefix, postfix) self.hostname = last_hostname.formatted_hostname(fill=counter_length) if commit: self.save() # TODO: add message when hostname was changed def _try_assign_hostname(self, commit=False, country=None, force=False): if self.model.category and self.model.category.code: template_vars = { 'code': self.model.category.code, 'country_code': country or self.country_code, } if (force or not self.hostname or self.country_code not in self.hostname): self.generate_hostname(commit, template_vars) @classmethod def get_autocomplete_queryset(cls): return cls._default_manager.exclude( status=BackOfficeAssetStatus.liquidated.id) @classmethod @transition_action(form_fields={ 'user': { 'field': forms.CharField(label=_('User')), 'autocomplete_field': 'user', 'default_value': partial(autocomplete_user, field_name='user') } }, run_after=['unassign_user']) def assign_user(cls, instances, **kwargs): user = get_user_model().objects.get(pk=int(kwargs['user'])) for instance in instances: instance.user = user @classmethod @transition_action( form_fields={ 'owner': { 'field': forms.CharField(label=_('Owner')), 'autocomplete_field': 'owner', 'default_value': partial(autocomplete_user, field_name='owner') } }, help_text=_( 'During this transition owner will be assigned as well as new ' 'hostname might be generated for asset (only for particular model ' 'categories and only if owner\'s country has changed)'), run_after=['unassign_owner']) def assign_owner(cls, instances, **kwargs): owner = get_user_model().objects.get(pk=int(kwargs['owner'])) for instance in instances: instance.owner = owner @classmethod @transition_action(form_fields={ 'licences': { 'field': forms.ModelMultipleChoiceField( queryset=Licence.objects.all(), label=_('Licence'), required=False, ), 'autocomplete_field': 'licence', 'autocomplete_model': 'licences.BaseObjectLicence', 'widget_options': { 'multi': True }, } }, run_after=['unassign_licences']) def assign_licence(cls, instances, **kwargs): for instance in instances: for obj in kwargs['licences']: BaseObjectLicence.objects.get_or_create( base_object=instance, licence_id=obj.id, ) @classmethod @transition_action(run_after=['loan_report', 'return_report']) def unassign_owner(cls, instances, **kwargs): for instance in instances: kwargs['history_kwargs'][instance.pk]['affected_owner'] = str( instance.owner) instance.owner = None @classmethod @transition_action(run_after=['loan_report', 'return_report']) def unassign_user(cls, instances, **kwargs): for instance in instances: kwargs['history_kwargs'][instance.pk]['affected_user'] = str( instance.user) instance.user = None @classmethod @transition_action( form_fields={ 'loan_end_date': { 'field': forms.DateField( label=_('Loan end date'), widget=forms.TextInput(attrs={'class': 'datepicker'})) } }, ) def assign_loan_end_date(cls, instances, **kwargs): for instance in instances: instance.loan_end_date = kwargs['loan_end_date'] @classmethod @transition_action() def unassign_loan_end_date(cls, instances, **kwargs): for instance in instances: instance.loan_end_date = None @classmethod @transition_action( form_fields={ 'warehouse': { 'field': forms.CharField(label=_('Warehouse')), 'autocomplete_field': 'warehouse' } }) def assign_warehouse(cls, instances, **kwargs): warehouse = Warehouse.objects.get(pk=int(kwargs['warehouse'])) for instance in instances: instance.warehouse = warehouse @classmethod @transition_action( form_fields={ 'office_infrastructure': { 'field': forms.CharField(label=_('Office infrastructure')), 'autocomplete_field': 'office_infrastructure' } }, ) def assign_office_infrastructure(cls, instances, **kwargs): office_inf = OfficeInfrastructure.objects.get( pk=int(kwargs['office_infrastructure'])) for instance in instances: instance.office_infrastructure = office_inf @classmethod @transition_action(form_fields={ 'remarks': { 'field': forms.CharField(label=_('Remarks')), } }) def add_remarks(cls, instances, **kwargs): for instance in instances: instance.remarks = '{}\n{}'.format(instance.remarks, kwargs['remarks']) @classmethod @transition_action(form_fields={ 'task_url': { 'field': forms.URLField(label=_('task URL')), } }) def assign_task_url(cls, instances, **kwargs): for instance in instances: instance.task_url = kwargs['task_url'] @classmethod @transition_action() def unassign_licences(cls, instances, **kwargs): BaseObjectLicence.objects.filter(base_object__in=instances).delete() @classmethod @transition_action( form_fields={ 'country': { 'field': forms.ChoiceField( label=_('Country'), choices=Country(), **{ 'initial': Country.from_name( settings.CHANGE_HOSTNAME_ACTION_DEFAULT_COUNTRY. lower() # noqa: E501 ).id } if settings.CHANGE_HOSTNAME_ACTION_DEFAULT_COUNTRY else {}), } }, ) def change_hostname(cls, instances, **kwargs): country_id = kwargs['country'] country_name = Country.name_from_id(int(country_id)).upper() iso3_country_name = iso2_to_iso3(country_name) for instance in instances: instance._try_assign_hostname(country=iso3_country_name, force=True) @classmethod @transition_action( form_fields={ 'user': { 'field': forms.CharField(label=_('User')), 'autocomplete_field': 'user', }, 'owner': { 'field': forms.CharField(label=_('Owner')), 'autocomplete_field': 'owner', 'condition': lambda obj, actions: bool(obj.owner), } }) def change_user_and_owner(cls, instances, **kwargs): UserModel = get_user_model() # noqa user_id = kwargs.get('user', None) user = UserModel.objects.get(id=user_id) owner_id = kwargs.get('owner', None) for instance in instances: instance.user = user if not owner_id: instance.owner = user else: instance.owner = UserModel.objects.get(id=owner_id) instance.location = user.location @classmethod def _get_report_context(cls, instances): data_instances = [{ 'sn': obj.sn, 'model': str(obj.model), 'imei': obj.imei, 'imei2': obj.imei2, 'barcode': obj.barcode, } for obj in instances] return data_instances @classmethod @transition_action(precondition=_check_assets_owner) def must_be_owner_of_asset(cls, instances, **kwargs): """Only a precondition matters""" pass @classmethod @transition_action(precondition=_check_assets_user) def must_be_user_of_asset(cls, instances, **kwargs): """Only a precondition matters""" pass @classmethod @transition_action( form_fields={ 'accept': { 'field': forms.BooleanField( label=_('I have read and fully understand and ' 'accept the agreement.')) }, }) def accept_asset_release_agreement(cls, instances, requester, **kwargs): pass @classmethod @transition_action(run_after=['release_report']) def assign_requester_as_an_owner(cls, instances, requester, **kwargs): """Assign current user as an owner""" for instance in instances: instance.owner = requester instance.save() @classmethod @transition_action(form_fields={ 'report_language': { 'field': forms.ModelChoiceField( label=_('Release report language'), queryset=ReportLanguage.objects.all().order_by('-default'), empty_label=None), 'exclude_from_history': True } }, return_attachment=True, run_after=['assign_owner', 'assign_user']) def release_report(cls, instances, requester, transition_id, **kwargs): report_name = get_report_name_for_transition_id(transition_id) return generate_report(instances=instances, name=report_name, requester=requester, language=kwargs['report_language'], context=cls._get_report_context(instances)) @classmethod @transition_action( run_after=['release_report', 'return_report', 'loan_report']) def send_attachments_to_user(cls, requester, transition_id, **kwargs): context_func = get_hook('back_office.transition_action.email_context') send_transition_attachments_to_user(requester=requester, transition_id=transition_id, context_func=context_func, **kwargs) @classmethod @transition_action( form_fields={ 'report_language': { 'field': forms.ModelChoiceField( label=_('Return report language'), queryset=ReportLanguage.objects.all().order_by('-default'), empty_label=None), 'exclude_from_history': True }, }, return_attachment=True, precondition=_check_user_assigned, ) def return_report(cls, instances, requester, **kwargs): return generate_report(instances=instances, name='return', requester=requester, language=kwargs['report_language'], context=cls._get_report_context(instances)) @classmethod @transition_action( form_fields={ 'report_language': { 'field': forms.ModelChoiceField( label=_('Loan report language'), queryset=ReportLanguage.objects.all().order_by('-default'), empty_label=None), 'exclude_from_history': True } }, return_attachment=True, run_after=['assign_owner', 'assign_user', 'assign_loan_end_date']) def loan_report(cls, instances, requester, **kwargs): return generate_report(name='loan', requester=requester, instances=instances, language=kwargs['report_language'], context=cls._get_report_context(instances)) @classmethod @transition_action(verbose_name=_('Convert to DataCenter Asset'), disable_save_object=True, only_one_action=True, form_fields={ 'rack': { 'field': forms.CharField(label=_('Rack')), 'autocomplete_field': 'rack', 'autocomplete_model': 'data_center.DataCenterAsset' }, 'position': { 'field': forms.IntegerField(label=_('Position')), }, 'model': { 'field': forms.CharField(label=_('Model')), 'autocomplete_field': 'model', 'autocomplete_model': 'data_center.DataCenterAsset' }, 'service_env': { 'field': forms.CharField(label=_('Service env')), 'autocomplete_field': 'service_env', 'autocomplete_model': 'data_center.DataCenterAsset' } }) def convert_to_data_center_asset(cls, instances, **kwargs): from ralph.data_center.models.physical import DataCenterAsset, Rack # noqa from ralph.back_office.helpers import bo_asset_to_dc_asset_status_converter # noqa with transaction.atomic(): for i, instance in enumerate(instances): data_center_asset = DataCenterAsset() data_center_asset.rack = Rack.objects.get(pk=kwargs['rack']) data_center_asset.position = kwargs['position'] data_center_asset.service_env = ServiceEnvironment.objects.get( pk=kwargs['service_env']) data_center_asset.model = AssetModel.objects.get( pk=kwargs['model']) target_status = int( Transition.objects.values_list('target', flat=True).get( pk=kwargs['transition_id']) # noqa ) data_center_asset.status = bo_asset_to_dc_asset_status_converter( # noqa instance.status, target_status) move_parents_models(instance, data_center_asset, exclude_copy_fields=[ 'rack', 'model', 'service_env', 'status' ]) data_center_asset.save() # Save new asset to list, required to redirect url. # RunTransitionView.get_success_url() instances[i] = data_center_asset @classmethod @transition_action() def assign_hostname_if_empty_or_country_not_match(cls, instances, **kwargs): for instance in instances: instance._try_assign_hostname(commit=False, force=False)
class Cluster( PreviousStateMixin, DNSaaSPublisherMixin, AdminAbsoluteUrlMixin, WithManagementIPMixin, NetworkableBaseObject, BaseObject, models.Model ): name = models.CharField(_('name'), max_length=255, blank=True, null=True) hostname = NullableCharField( unique=True, null=True, blank=True, max_length=255, verbose_name=_('hostname') ) type = models.ForeignKey(ClusterType) base_objects = models.ManyToManyField( BaseObject, verbose_name=_('Assigned base objects'), through='BaseObjectCluster', related_name='+', ) status = TransitionField( default=ClusterStatus.in_use.id, choices=ClusterStatus(), ) def __str__(self): return '{} ({})'.format(self.name or self.hostname, self.type) def get_location(self): return self.masters[0].get_location() if self.masters else [] @property def model(self): return self.type @cached_property def masters(self): return self.get_masters(cast_base_object=True) def get_masters(self, cast_base_object=False): # prevents cyclic import from ralph.virtual.models import CloudHost, VirtualServer # noqa result = [] for obj in self.baseobjectcluster_set.all(): if obj.is_master: bo = obj.base_object # fetch final object if it's base object if cast_base_object and not isinstance( bo, # list equal to BaseObjectCluster.base_object.limit_models (Database, DataCenterAsset, CloudHost, VirtualServer) ): bo = bo.last_descendant result.append(bo) return result @cached_property def rack_id(self): return self.rack.id if self.rack else None @cached_property def rack(self): for master in self.masters: if isinstance(master, DataCenterAsset) and master.rack_id: return master.rack return None def _validate_name_hostname(self): if not self.name and not self.hostname: error_message = [_('At least one of name or hostname is required')] raise ValidationError( {'name': error_message, 'hostname': error_message} ) def clean(self): errors = {} for validator in [ super().clean, self._validate_name_hostname, ]: try: validator() except ValidationError as e: e.update_error_dict(errors) if errors: raise ValidationError(errors)
class NetworkEnvironment(AdminAbsoluteUrlMixin, TimeStampMixin, NamedMixin, models.Model): data_center = models.ForeignKey('data_center.DataCenter', verbose_name=_('data center')) queue = models.ForeignKey( 'DiscoveryQueue', verbose_name=_('discovery queue'), null=True, blank=True, on_delete=models.SET_NULL, ) hostname_template_counter_length = models.PositiveIntegerField( verbose_name=_('hostname template counter length'), default=4, ) hostname_template_prefix = models.CharField( verbose_name=_('hostname template prefix'), max_length=30, ) hostname_template_postfix = models.CharField( verbose_name=_('hostname template postfix'), max_length=30, help_text=_( 'This value will be used as a postfix when generating new hostname ' 'in this network environment. For example, when prefix is "s1", ' 'postfix is ".mydc.net" and counter length is 4, following ' ' hostnames will be generated: s10000.mydc.net, s10001.mydc.net, ..' ', s19999.mydc.net.')) domain = NullableCharField( verbose_name=_('domain'), max_length=255, blank=True, null=True, help_text=_('Used in DHCP configuration.'), ) remarks = models.TextField( verbose_name=_('remarks'), help_text=_('Additional information.'), blank=True, null=True, ) use_hostname_counter = models.BooleanField( default=True, help_text='If set to false hostname based on already added hostnames.') def __str__(self): return self.name class Meta: ordering = ('name', ) @property def HOSTNAME_MODELS(self): from ralph.data_center.models.virtual import Cluster from ralph.data_center.models.physical import DataCenterAsset from ralph.virtual.models import VirtualServer return (DataCenterAsset, VirtualServer, Cluster, IPAddress) @property def next_free_hostname(self): """ Retrieve next free hostname """ if self.use_hostname_counter: return AssetLastHostname.get_next_free_hostname( self.hostname_template_prefix, self.hostname_template_postfix, self.hostname_template_counter_length, self.check_hostname_is_available) else: result = self.next_hostname_without_model_counter() return result def check_hostname_is_available(self, hostname): if not hostname: return False for model_class in self.HOSTNAME_MODELS: if model_class.objects.filter(hostname=hostname).exists(): return False return True def issue_next_free_hostname(self): """ Retrieve and reserve next free hostname """ if self.use_hostname_counter: hostname = None while not self.check_hostname_is_available(hostname): hostname = AssetLastHostname.increment_hostname( self.hostname_template_prefix, self.hostname_template_postfix, ).formatted_hostname(self.hostname_template_counter_length) return hostname return self.next_hostname_without_model_counter() def current_counter_without_model(self): """ Return current counter based on already added hostnames Returns: counter int """ start = len(self.hostname_template_prefix) stop = -len(self.hostname_template_postfix) hostnames = [] for model_class in self.HOSTNAME_MODELS: item = model_class.objects.filter( hostname__iregex='{}[0-9]+{}'.format( self.hostname_template_prefix, self. hostname_template_postfix)).order_by('-hostname').first() if item and item.hostname: hostnames.append(item.hostname[start:stop]) counter = 0 if hostnames: # queryset guarantees that hostnames are valid number # therefore we can skip ValueError counter = int(sorted(hostnames, reverse=True)[0]) return counter def next_counter_without_model(self): """ Return next counter based on already added hostnames Returns: counter int """ return self.current_counter_without_model() + 1 def next_hostname_without_model_counter(self): """ Return hostname based on already added hostnames Returns: hostname string """ hostname = AssetLastHostname(prefix=self.hostname_template_prefix, counter=self.next_counter_without_model(), postfix=self.hostname_template_postfix) return hostname.formatted_hostname( self.hostname_template_counter_length)
class Job(TimeStampMixin): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) username = NullableCharField(max_length=200, null=True, blank=True) service_name = models.CharField(max_length=200, null=False, blank=False) _dumped_params = JSONField() status = models.PositiveIntegerField( verbose_name=_('job status'), choices=JobStatus(), default=JobStatus.QUEUED.id, ) _params = None objects = JobQuerySet.as_manager() class Meta: app_label = 'external_services' def __str__(self): return '{} ({})'.format(self.service_name, self.id) @property def user(self): try: return self.params['_request__user'] except KeyError: if self.username: try: return get_user_model()._default_manager.get( username=self.username ) except get_user_model().DoesNotExist: pass return None @property def is_running(self): """ Return True if job is not ended. """ return self.status in JOB_NOT_ENDED_STATUSES @property def is_frozen(self): """ Return True if job is frozen. """ return self.status == JobStatus.FROZEN @property def is_killed(self): """ Return True if job is killed. """ return self.status == JobStatus.KILLED @property def params(self): """ Should be called on job-side to extract params as they were passed to job. """ if self._params is None: self._params = self._restore_params(self._dumped_params) logger.debug('{} restored into {}'.format( self._dumped_params, self._params )) return self._params def _get_metric_name(self): return self.service_name def _update_dumped_params(self): # re-save job to store updated params in DB self._dumped_params = self.prepare_params(**self.params) logger.debug('Updating _dumped_params to {}'.format( self._dumped_params )) self.save() @collect_metrics('start') def start(self): """ Mark job as started. """ logger.info('Starting job {}'.format(self)) self.status = JobStatus.STARTED self.save() @collect_metrics('reschedule') def reschedule(self): """ Reschedule the same job again. """ # TODO: use rq scheduler self._update_dumped_params() logger.info('Rescheduling {}'.format(self)) service = InternalService(self.service_name) job = service.run_async(job_id=self.id) return job @collect_metrics('freeze') def freeze(self): self._update_dumped_params() logger.info('Freezing job {}'.format(self)) self.status = JobStatus.FROZEN self.save() @collect_metrics('unfreeze') def unfreeze(self): logger.info('Unfreezing {}'.format(self)) service = InternalService(self.service_name) job = service.run_async(job_id=self.id) return job @collect_metrics('kill') def kill(self): logger.info('Kill job {}'.format(self)) self.status = JobStatus.KILLED self.save() @collect_metrics('fail') def fail(self, reason=''): """ Mark job as failed. """ self._update_dumped_params() logger.info('Job {} has failed. Reason: {}'.format(self, reason)) self.status = JobStatus.FAILED self.save() @collect_metrics('success') def success(self): """ Mark job as successfuly ended. """ self._update_dumped_params() logger.info('Job {} has succeeded'.format(self)) self.status = JobStatus.FINISHED self.save() @classmethod def prepare_params(cls, **kwargs): user = kwargs.pop('requester', None) result = cls.dump_obj_to_jsonable(kwargs) result['_request__user'] = ( result.get('_request__user') or (cls.dump_obj_to_jsonable(user) if user else None) ) logger.debug('{} prepared into {}'.format(kwargs, result)) return result @classmethod def run(cls, service_name, requester, defaults=None, **kwargs): """ Run Job asynchronously in internal service (with DB and models access). """ service = InternalService(service_name) obj = cls._default_manager.create( service_name=service_name, username=requester.username if requester else None, _dumped_params=cls.prepare_params(requester=requester, **kwargs), **(defaults or {}) ) # commit transaction to allow worker to fetch it using job id transaction.commit() service.run_async(job_id=obj.id) return obj.id, obj @classmethod def dump_obj_to_jsonable(cls, obj): """ Dump obj to JSON-acceptable format """ result = obj if isinstance(obj, (list, tuple, set)): result = [cls.dump_obj_to_jsonable(p) for p in obj] elif isinstance(obj, QuerySet): result = { '__django_queryset': True, 'value': [i.pk for i in obj], 'content_type_id': ContentType.objects.get_for_model( obj.model ).pk } elif isinstance(obj, date): result = { '__date': True, 'value': str(obj) } elif isinstance(obj, dict): result = {} for k, v in obj.items(): result[k] = cls.dump_obj_to_jsonable(v) elif isinstance(obj, models.Model): # save Django object as 3-items dict with content type and object id result = { '__django_model': True, 'content_type_id': ContentType.objects.get_for_model(obj).pk, 'object_pk': obj.pk, } return result @classmethod def _restore_params(cls, obj): return cls._restore_django_models(obj) @classmethod def _restore_django_models(cls, obj): """ Restore Django objects from dump created with `dump_obj_to_jsonable` """ result = obj if isinstance(obj, (list, tuple)): result = [cls._restore_django_models(p) for p in obj] elif isinstance(obj, dict): if obj.get('__date') is True: result = parse(obj.get('value')).date() elif obj.get('__django_queryset') is True: ct = ContentType.objects.get_for_id(obj['content_type_id']) result = ct.model_class().objects.filter( pk__in=obj.get('value') ) elif obj.get('__django_model') is True: ct = ContentType.objects.get_for_id(obj['content_type_id']) result = ct.get_object_for_this_type(pk=obj['object_pk']) else: result = {} for k, v in obj.items(): result[k] = cls._restore_django_models(v) return result
class IPAddress(AdminAbsoluteUrlMixin, LastSeenMixin, TimeStampMixin, PreviousStateMixin, NetworkMixin, models.Model): _parent_attr = 'network' ethernet = models.OneToOneField( Ethernet, null=True, default=None, blank=True, on_delete=models.CASCADE, ) network = models.ForeignKey( Network, null=True, default=None, editable=False, related_name='ips', on_delete=models.SET_NULL, ) address = models.GenericIPAddressField( verbose_name=_('IP address'), help_text=_('Presented as string.'), unique=True, blank=False, null=False, ) hostname = NullableCharField( verbose_name=_('hostname'), max_length=255, null=True, blank=True, default=None, # TODO: unique ) number = models.DecimalField( verbose_name=_('IP address'), help_text=_('Presented as int.'), editable=False, unique=True, max_digits=39, decimal_places=0, default=None, ) is_management = models.BooleanField( verbose_name=_('Is management address'), default=False, ) is_public = models.BooleanField( verbose_name=_('Is public'), default=False, editable=False, ) is_gateway = models.BooleanField( verbose_name=_('Is gateway'), default=False, ) status = models.PositiveSmallIntegerField( default=IPAddressStatus.used.id, choices=IPAddressStatus(), ) dhcp_expose = models.BooleanField( default=False, verbose_name=_('Expose in DHCP'), ) objects = IPAddressQuerySet.as_manager() class Meta: verbose_name = _('IP address') verbose_name_plural = _('IP addresses') def __str__(self): return self.address def _hostname_is_unique_in_dc(self, hostname, dc): from ralph.dhcp.models import DHCPEntry entries_with_hostname = DHCPEntry.objects.filter( hostname=hostname, network__network_environment__data_center=dc) if self.pk: entries_with_hostname = entries_with_hostname.exclude(pk=self.pk) return not entries_with_hostname.exists() def validate_hostname_uniqueness_in_dc(self, hostname): network = self.get_network() if network and network.network_environment: dc = network.network_environment.data_center if not self._hostname_is_unique_in_dc(hostname, dc): raise ValidationError( 'Hostname "{hostname}" is already exposed in DHCP in {dc}.' .format(hostname=self.hostname, dc=dc)) def _validate_hostname_uniqueness_in_dc(self): if not self.dhcp_expose: return self.validate_hostname_uniqueness_in_dc(self.hostname) def _validate_expose_in_dhcp_and_mac(self): if ((not self.ethernet_id or (self.ethernet and not self.ethernet.mac)) and # noqa self.dhcp_expose): raise ValidationError( {'dhcp_expose': ('Cannot expose in DHCP without MAC address')}) def _validate_expose_in_dhcp_and_hostname(self): if not self.hostname and self.dhcp_expose: raise ValidationError( {'hostname': ('Cannot expose in DHCP without hostname')}) def _validate_change_when_exposing_in_dhcp(self): """ Check if one of hostname, address or ethernet is changed when entry is exposed in DHCP. """ if self.pk and settings.DHCP_ENTRY_FORBID_CHANGE: old_obj = self.__class__._default_manager.get(pk=self.pk) if old_obj.dhcp_expose: for attr_name, field_name in [ ('hostname', 'hostname'), ('address', 'address'), ('ethernet_id', 'ethernet'), ]: if getattr(old_obj, attr_name) != getattr(self, attr_name): raise ValidationError( 'Cannot change {} when exposing in DHCP'.format( field_name)) def clean(self): errors = {} for validator in [ super().clean, self._validate_expose_in_dhcp_and_mac, self._validate_expose_in_dhcp_and_hostname, self._validate_change_when_exposing_in_dhcp, self._validate_hostname_uniqueness_in_dc, ]: try: validator() except ValidationError as e: e.update_error_dict(errors) if errors: raise ValidationError(errors) def save(self, *args, **kwargs): if settings.CHECK_IP_HOSTNAME_ON_SAVE: if not self.address and self.hostname: self.address = network_tools.hostname(self.hostname, reverse=True) if not self.hostname and self.address: self.hostname = network_tools.hostname(self.address) if self.number and not self.address: self.address = ipaddress.ip_address(int(self.number)) else: self.number = int(ipaddress.ip_address(self.address or 0)) self._assign_parent() self.is_public = not self.ip.is_private # TODO: if not reserved, check for ethernet super(IPAddress, self).save(*args, **kwargs) @property def ip(self): return ipaddress.ip_address(self.address) @property def base_object(self): if not self.ethernet: return None return self.ethernet.base_object @base_object.setter def base_object(self, value): if self.ethernet: self.ethernet.base_object = value self.ethernet.save() else: eth = Ethernet.objects.create(base_object=value) self.ethernet = eth self.save() def search_networks(self): """ Search networks (ancestors) order first by min_ip descending, then by max_ip ascending, to get smallest ancestor network containing current network. """ int_value = int(self.ip) nets = Network.objects.filter(min_ip__lte=int_value, max_ip__gte=int_value).order_by( '-min_ip', 'max_ip') return nets