import json import re from datetime import timedelta from django.core.exceptions import ( ValidationError ) from django.db import models from django.utils.timezone import now from access.fields import AutoLastModifiedField from centurion.helpers.merge_software import merge_software from core.classes.icon import Icon from core.models.centurion import CenturionModel from itam.models.device_models import DeviceModel from itam.models.software import Software, SoftwareVersion from itam.models.operating_system import OperatingSystemVersion from settings.models.app_settings import AppSettings class DeviceType( CenturionModel, ): model_tag = 'device_type' class Meta: ordering = [ 'name' ] verbose_name = 'Device Type' verbose_name_plural = 'Device Types' name = models.CharField( blank = False, help_text = 'The items name', max_length = 50, unique = True, verbose_name = 'Name' ) modified = AutoLastModifiedField() page_layout: dict = [ { "name": "Details", "slug": "details", "sections": [ { "layout": "double", "left": [ 'organization', 'name', ], "right": [ 'model_notes', 'created', 'modified', ] } ] }, { "name": "Knowledge Base", "slug": "kb_articles", "sections": [ { "layout": "table", "field": "knowledge_base", } ] }, { "name": "Notes", "slug": "notes", "sections": [] }, ] table_fields: list = [ 'name', 'organization', 'created', 'modified' ] def clean(self): app_settings = AppSettings.objects.get(owner_organization=None) if app_settings.device_type_is_global: self.organization = app_settings.global_organization def __str__(self): return self.name class Device( CenturionModel, ): model_tag = 'device' reserved_config_keys: list = [ 'software' ] class Meta: ordering = [ 'name', 'organization' ] verbose_name = 'Device' verbose_name_plural = 'Devices' def validate_hostname_format(self): pattern = r'^[a-z]{1}[a-z|0-9|\-]+[a-z|0-9]{1}$' if not re.match(pattern, str(self).lower()): raise ValidationError( message = '[RFC1035 2.3.1] A hostname must start with a letter,' \ 'end with a letter or digit, and have as interior characters only letters,' \ ' digits, and hyphen.', code = 'invalid_hostname' ) name = models.CharField( blank = False, help_text = 'Hostname of this device', max_length = 50, unique = True, validators = [ validate_hostname_format ], verbose_name = 'Name' ) serial_number = models.CharField( blank = True, help_text = 'Serial number of the device.', max_length = 50, null = True, unique = True, verbose_name = 'Serial Number', ) def validate_uuid_format(self): pattern = r'[0-9|a-f|A-F]{8}\-[0-9|a-f|A-F]{4}\-[0-9|a-f|A-F]{4}\-[0-9|a-f|A-F]{4}\-[0-9|a-f|A-F]{12}' if not re.match(pattern, str(self)): raise ValidationError( message = f'UUID must be formated to match regex {str(pattern)}', code = 'invalid_uuid' ) uuid = models.UUIDField( blank = True, help_text = 'System GUID/UUID.', max_length = 50, null = True, unique = True, validators = [ validate_uuid_format ], verbose_name = 'UUID' ) device_model = models.ForeignKey( DeviceModel, blank = True, help_text = 'Model of the device.', null = True, on_delete = models.PROTECT, verbose_name = 'Model' ) device_type = models.ForeignKey( DeviceType, blank = True, help_text = 'Type of device.', null = True, on_delete = models.PROTECT, verbose_name = 'Type' ) def validate_config_keys_not_reserved(self): if self: value: dict = self for invalid_key in Device.reserved_config_keys: if invalid_key in value.keys(): raise ValidationError( message = f'json key "{invalid_key}" is a reserved configuration key' ) config = models.JSONField( blank = True, help_text = 'Configuration for this device', null = True, validators=[ validate_config_keys_not_reserved ], verbose_name = 'Host Configuration', ) inventorydate = models.DateTimeField( blank = True, help_text = 'Date and time of the last inventory', null = True, verbose_name = 'Last Inventory Date', ) is_virtual = models.BooleanField( blank = True, default = False, help_text = 'Is this device a virtual machine', null = False, verbose_name = 'Is Virtual', ) modified = AutoLastModifiedField() table_fields: list = [ 'status_icon', "name", "device_model", "device_type", "organization", "created", "modified", "model", "nbsp" ] page_layout: dict = [ { "name": "Details", "slug": "details", "sections": [ { "layout": "double", "left": [ 'organization', 'device_type', 'device_model', 'name', 'serial_number', 'uuid', 'inventorydate', 'created', 'modified', ], "right": [ 'model_notes', 'is_virtual', ] }, { "layout": "table", "name": "Operating System", "field": "operating_system", }, { "layout": "table", "name": "Dependent Services", "field": "service", }, { "layout": "single", "fields": [ 'config', ] } ] }, { "name": "Software", "slug": "software", "sections": [ { "layout": "table", "field": "software", } ] }, { "name": "Tickets", "slug": "tickets", "sections": [ { "layout": "table", "field": "tickets", } ], }, { "name": "Knowledge Base", "slug": "kb_articles", "sections": [ { "layout": "table", "field": "knowledge_base", } ] }, { "name": "Notes", "slug": "notes", "sections": [] }, { "name": "Config Management", "slug": "config_management", "sections": [ { "layout": "single", "fields": [ "rendered_config", ] } ] } ] def clean_fields(self, exclude = None): if self.uuid is not None: self.uuid = str(self.uuid).lower() super().clean_fields(exclude = exclude) def save( self, force_insert=False, force_update=False, using=None, update_fields=None ): """ Save Device Model After saving the device update the related items so that they are a part of the same organization as the device. """ super().save( force_insert=False, force_update=False, using=None, update_fields=None ) models_to_update =[ DeviceSoftware, DeviceOperatingSystem ] for update_model in models_to_update: obj = update_model.objects.filter( device = self.id, ) if obj.exists(): obj.update( organization = self.organization, ) from config_management.models.groups import ConfigGroupHosts ConfigGroupHosts.objects.filter( host = self.id, ).delete() def __str__(self): return self.name @property def status_icon(self) -> list([Icon]): icons: list(Icon) = [] icons += [ Icon( name = f'device_status_{self.status.lower()}', style = f'icon-device-status-{self.status.lower()}' ) ] return icons @property def status(self) -> str: """ Fetch Device status Returns: str: Current status of the item """ if self.inventorydate: check_date = self.inventorydate else: check_date = now() + timedelta(days=99) one = (now() - check_date).days status: str = 'UNK' if (now() - check_date).days >= 0 and (now() - check_date).days <= 1: status = 'OK' elif (now() - check_date).days >= 2 and (now() - check_date).days < 3: status = 'WARN' elif (now() - check_date).days >= 3: status = 'BAD' return status @property def get_configuration(self): softwares = DeviceSoftware.objects.filter(device=self.id) config = { "software": [] } host_software = [] group_software = [] for software in softwares: if software.action: if int(software.action) == 1: state = 'present' elif int(software.action) == 0: state = 'absent' software_action = { "name": software.software.slug, "state": state } if software.version: software_action['version'] = software.version.name host_software += [ software_action ] config: dict = config from config_management.models.groups import ConfigGroupHosts if self.id: config_groups = ConfigGroupHosts.objects.filter(host=self.id).order_by('group') for group in config_groups: rendered_config = group.group.render_config() if rendered_config: config.update(json.loads(rendered_config)) rendered_config: dict = json.loads(rendered_config) if 'software' in rendered_config.keys(): group_software = group_software + rendered_config['software'] config['software'] = merge_software(group_software, host_software) if self.config: config.update(self.config) from itim.models.services import Service services = Service.objects.filter( device = self.pk ) for service in services: if service.config_variables: service_config:dict = { service.config_key_variable: service.config_variables } config.update(service_config) return config class DeviceSoftware( CenturionModel, ): """ A way for the device owner to configure software to install/remove """ _audit_enabled = False _notes_enabled = False class Meta: ordering = [ '-action', 'software' ] verbose_name = 'Device Software' verbose_name_plural = 'Device Softwares' class Actions(models.IntegerChoices): INSTALL = 1, 'Install' REMOVE = 0, 'Remove' device = models.ForeignKey( Device, blank = False, help_text = 'Device this software is on', on_delete = models.CASCADE, null = False, verbose_name = 'Device' ) software = models.ForeignKey( Software, blank = False, help_text = 'Software Name', null = False, on_delete = models.PROTECT, verbose_name = 'Software' ) action = models.IntegerField( blank = True, choices = Actions, help_text = 'Action to perform', null = True, verbose_name = 'Action', ) version = models.ForeignKey( SoftwareVersion, blank = True, help_text = 'Version to install', on_delete = models.PROTECT, null = True, verbose_name = 'Desired Version' ) installedversion = models.ForeignKey( SoftwareVersion, blank = True, help_text = 'Version that is installed', null = True, on_delete = models.PROTECT, related_name = 'installedversion', verbose_name = 'Installed Version' ) installed = models.DateTimeField( blank = True, help_text = 'Date detected as installed', null = True, verbose_name = 'Date Installed' ) modified = AutoLastModifiedField() page_layout: list = [] table_fields: list = [ "nbsp", "software", "category", "action_badge", "version", "installedversion", "installed", "nbsp" ] @property def action_badge(self): from core.classes.badge import Badge text:str = 'Add' if self.action: text = self.get_action_display() return Badge( icon_name = f'action_{text.lower()}', icon_style = f'badge-icon-action-{text.lower()}', text = text, text_style = f'badge-text-action-{text.lower()}', url = '_self', ) def get_url_kwargs(self, many = False) -> dict: kwargs = super().get_url_kwargs( many = many ) kwargs.update({ 'device_id': self.device.id, }) return kwargs @property def parent_object(self): """ Fetch the parent object """ return self.device class DeviceOperatingSystem( CenturionModel, ): _audit_enabled = False _notes_enabled = False class Meta: ordering = [ 'device', ] verbose_name = 'Device Operating System' verbose_name_plural = 'Device Operating Systems' model_notes = None device = models.OneToOneField( Device, blank = False, help_text = 'Device for the Operating System', on_delete = models.CASCADE, null = False, verbose_name = 'Device', unique = True ) operating_system_version = models.ForeignKey( OperatingSystemVersion, blank = False, help_text = 'Operating system version', null = False, on_delete = models.PROTECT, verbose_name = 'Operating System/Version', ) version = models.CharField( blank = False, help_text = 'Version detected as installed', max_length = 15, null = False, verbose_name = 'Installed Version', ) installdate = models.DateTimeField( blank = True, help_text = 'Date and time detected as installed', null = True, verbose_name = 'Install Date', ) modified = AutoLastModifiedField() page_layout: list = [ { "name": "Details", "slug": "details", "sections": [ { "layout": "single", "fields": [ 'operating_system_version', 'version', 'installdate' ], } ] } ] table_fields: list = [ 'device', 'operating_system_version', 'version', 'installdate', ] def get_url_kwargs(self, many = False) -> dict: kwargs = super().get_url_kwargs( many = many ) kwargs.update({ 'device_id': self.device.id, }) return kwargs @property def parent_object(self): """ Fetch the parent object """ return self.device