Files
centurion_erp/app/itam/models/device.py

786 lines
18 KiB
Python

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.lib.feature_not_used import FeatureNotUsed
from core.mixins.history_save import SaveHistory
from core.models.centurion import CenturionModel
from itam.models.device_common import DeviceCommonFields
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(DeviceCommonFields, SaveHistory):
""" A way for the device owner to configure software to install/remove """
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.CASCADE,
verbose_name = 'Software'
)
action = models.IntegerField(
blank = True,
choices=Actions,
default=None,
help_text = 'Action to perform',
null=True,
verbose_name = 'Action',
)
version = models.ForeignKey(
SoftwareVersion,
blank= True,
default = None,
help_text = 'Version to install',
on_delete=models.CASCADE,
null = True,
verbose_name = 'Desired Version'
)
installedversion = models.ForeignKey(
SoftwareVersion,
blank= True,
default = None,
help_text = 'Version that is installed',
null = True,
on_delete=models.CASCADE,
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) -> dict:
return {
'device_id': self.device.id,
'pk': self.id
}
def get_url_kwargs_notes(self):
return FeatureNotUsed
@property
def parent_object(self):
""" Fetch the parent object """
return self.device
def save_history(self, before: dict, after: dict) -> bool:
from itam.models.device_software_history import DeviceSoftwareHistory
history = super().save_history(
before = before,
after = after,
history_model = DeviceSoftwareHistory,
)
return history
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