from django.db import models from django.urls import reverse from django.utils.html import format_html, urlencode from netbox.models import NetBoxModel from utilities.querysets import RestrictedQuerySet from utilities.validators import EnhancedURLValidator class LaxURLField(models.URLField): """ NetBox Custom Field approach, based on utilities.forms.fields.LaxURLField Overriding default_validators is needed, as they are always added """ default_validators = [EnhancedURLValidator()] class SoftwareProduct(NetBoxModel): name = models.CharField(max_length=128) comments = models.TextField(blank=True) description = models.CharField(max_length=255, null=True, blank=True) manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, null=True, blank=True) objects = RestrictedQuerySet.as_manager() def __str__(self): return self.name def get_absolute_url(self): return reverse("plugins:netbox_slm:softwareproduct", kwargs={"pk": self.pk}) def get_installation_count(self): count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count() query_string = urlencode(dict(software_product=self.pk)) search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list") # Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582 return format_html(f"{count}") if count else "0" class SoftwareReleaseTypes(models.TextChoices): ALPHA = "A", "Alpha" BETA = "B", "Beta" RELEASE_CANDIDATE = "RC", "Release candidate" STABLE = "S", "Stable release" class SoftwareProductVersion(NetBoxModel): name = models.CharField(max_length=64) comments = models.TextField(blank=True) release_date = models.DateField(null=True, blank=True) documentation_url = LaxURLField(max_length=1024, null=True, blank=True) end_of_support = models.DateField(null=True, blank=True) filename = models.CharField(max_length=64, null=True, blank=True) file_checksum = models.CharField(max_length=128, null=True, blank=True) file_link = LaxURLField(max_length=1024, null=True, blank=True) release_type = models.CharField( max_length=3, choices=SoftwareReleaseTypes.choices, default=SoftwareReleaseTypes.STABLE, ) software_product = models.ForeignKey( to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT, ) objects = RestrictedQuerySet.as_manager() def __str__(self): return self.name def get_absolute_url(self): return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk}) def get_installation_count(self): count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count() query_string = urlencode(dict(version=self.pk)) search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list") # Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582 return format_html(f"{count}") if count else "0" class SoftwareProductInstallation(NetBoxModel): comments = models.TextField(blank=True) device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT, null=True, blank=True) virtualmachine = models.ForeignKey( to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True ) cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True) software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT) version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT) objects = RestrictedQuerySet.as_manager() def __str__(self): return f"{self.pk} ({self.platform})" class Meta: constraints = [ models.CheckConstraint( name="%(app_label)s_%(class)s_platform", check=( models.Q(device__isnull=False, virtualmachine__isnull=True, cluster__isnull=True) | models.Q(device__isnull=True, virtualmachine__isnull=False, cluster__isnull=True) | models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False) ), violation_error_message="Installation requires exactly one platform destination.", ) ] def get_absolute_url(self): return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk}) @property def platform(self): return self.device or self.virtualmachine or self.cluster def render_type(self): if self.device: return "device" if self.virtualmachine: return "virtualmachine" return "cluster" class SoftwareLicense(NetBoxModel): name = models.CharField(max_length=128) comments = models.TextField(blank=True) description = models.CharField(max_length=255, null=True, blank=True) type = models.CharField(max_length=128) stored_location = models.CharField(max_length=255, null=True, blank=True) stored_location_url = LaxURLField(max_length=1024, null=True, blank=True) start_date = models.DateField(null=True, blank=True) expiration_date = models.DateField(null=True, blank=True) support = models.BooleanField(default=None, null=True, blank=True) license_amount = models.PositiveIntegerField(default=None, null=True, blank=True) software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT) version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT, null=True, blank=True) installation = models.ForeignKey( to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True ) objects = RestrictedQuerySet.as_manager() def __str__(self): return self.name def get_absolute_url(self): return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk}) @property def stored_location_txt(self): if self.stored_location_url and not self.stored_location: return "Link" return self.stored_location