Show SLM installations on native objects (#61)

Closes #49

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
This commit is contained in:
wkoot
2025-02-24 16:57:18 +01:00
committed by GitHub
parent 4bd02ab6f9
commit ef45789b30
12 changed files with 196 additions and 52 deletions
+1
View File
@@ -30,6 +30,7 @@ jobs:
/opt/netbox/venv/bin/coverage run --source='netbox_slm' manage.py test netbox_slm &&\ /opt/netbox/venv/bin/coverage run --source='netbox_slm' manage.py test netbox_slm &&\
/opt/netbox/venv/bin/coverage report --fail-under=0 &&\ /opt/netbox/venv/bin/coverage report --fail-under=0 &&\
/opt/netbox/venv/bin/coverage xml -o /ci/reports/coverage.xml" /opt/netbox/venv/bin/coverage xml -o /ci/reports/coverage.xml"
sed -i "s/<source>\/opt\/netbox\/netbox<\/source>/<source><\/source>/" ci/reports/coverage.xml
timeout-minutes: 6 timeout-minutes: 6
- name: Sonar scan - name: Sonar scan
+1
View File
@@ -8,6 +8,7 @@
* Bulk import and bulk edit (#4) * Bulk import and bulk edit (#4)
* Changelog with backdated changes (#51) * Changelog with backdated changes (#51)
* List filtering (#54) * List filtering (#54)
* Show installations on native objects (#49)
### Changed ### Changed
-5
View File
@@ -19,11 +19,6 @@ DEBUG = environ.get('DEBUG', 'False').lower() == 'true'
DEVELOPER = environ.get('DEVELOPER', 'False').lower() == 'true' DEVELOPER = environ.get('DEVELOPER', 'False').lower() == 'true'
PLUGINS = ["netbox_slm"] PLUGINS = ["netbox_slm"]
PLUGINS_CONFIG = {
"netbox_slm": {
"top_level_menu": environ.get('SLM_TOP_LEVEL_MENU', 'True').lower() == 'true',
},
}
REDIS = { REDIS = {
'tasks': { 'tasks': {
+7 -2
View File
@@ -6,13 +6,18 @@ __version__ = "1.7.0"
class SLMConfig(PluginConfig): class SLMConfig(PluginConfig):
name = "netbox_slm" name = "netbox_slm"
verbose_name = "Software Lifecycle Management" verbose_name = "Software Lifecycle Management"
description = "Software Lifecycle Management Netbox Plugin."
version = __version__ version = __version__
description = "Software Lifecycle Management Netbox Plugin."
author = "ICTU" author = "ICTU"
author_email = "open-source-projects@ictu.nl" author_email = "open-source-projects@ictu.nl"
base_url = "slm" base_url = "slm"
required_settings = [] required_settings = []
default_settings = {"version_info": False} default_settings = {
"top_level_menu": True,
"link_cluster_installations": "right",
"link_device_installations": "right",
"link_virtualmachine_installations": "right",
}
config = SLMConfig config = SLMConfig
+1 -1
View File
@@ -10,7 +10,7 @@ from netbox_slm.models import (
SoftwareLicense, SoftwareLicense,
SoftwareReleaseTypes, SoftwareReleaseTypes,
) )
from virtualization.models import VirtualMachine, Cluster from virtualization.models import Cluster, VirtualMachine
class SoftwareProductFilterSet(NetBoxModelFilterSet): class SoftwareProductFilterSet(NetBoxModelFilterSet):
@@ -12,7 +12,7 @@ from utilities.forms.fields import (
) )
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelect from utilities.forms.widgets import APISelect
from virtualization.models import VirtualMachine, Cluster from virtualization.models import Cluster, VirtualMachine
class SoftwareProductInstallationForm(NetBoxModelForm): class SoftwareProductInstallationForm(NetBoxModelForm):
+3 -4
View File
@@ -1,6 +1,4 @@
from django.conf import settings from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config
from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu
from . import SLMConfig from . import SLMConfig
slm_items = ( slm_items = (
@@ -82,11 +80,12 @@ slm_items = (
), ),
) )
if settings.PLUGINS_CONFIG["netbox_slm"].get("top_level_menu") is True: if get_plugin_config("netbox_slm", "top_level_menu"):
menu = PluginMenu( menu = PluginMenu(
label="Software Lifecycle", label="Software Lifecycle",
groups=((SLMConfig.verbose_name, slm_items),), groups=((SLMConfig.verbose_name, slm_items),),
icon_class="mdi mdi-content-save", icon_class="mdi mdi-content-save",
) )
else: else:
# auto imported by default PluginConfig.menu_items = navigation.menu_items
menu_items = slm_items menu_items = slm_items
+36
View File
@@ -0,0 +1,36 @@
from netbox.plugins import PluginTemplateExtension, get_plugin_config
installations = dict(
device=get_plugin_config("netbox_slm", "link_device_installations"),
cluster=get_plugin_config("netbox_slm", "link_cluster_installations"),
virtualmachine=get_plugin_config("netbox_slm", "link_virtualmachine_installations"),
)
class InstallationsCard(PluginTemplateExtension):
models = ["dcim.device", "virtualization.cluster", "virtualization.virtualmachine"]
def get_object_name(self):
object_model_meta = self.context["object"]._meta # one of the self.models defined above
return object_model_meta.model_name
def render_card(self):
return self.render(
"netbox_slm/installations_card_include.html",
extra_context={
"search_obj": self.get_object_name(),
},
)
def left_page(self):
return self.render_card() if installations[self.get_object_name()] == "left" else ""
def right_page(self):
return self.render_card() if installations[self.get_object_name()] == "right" else ""
def full_width_page(self):
return self.render_card() if installations[self.get_object_name()] == "full" else ""
# auto imported by default PluginConfig.template_extensions = template_content.template_extensions
template_extensions = [InstallationsCard]
@@ -0,0 +1,21 @@
{% load helpers %}
<div class="card">
<h2 class="card-header">
Installations
{% if perms.netbox_slm.add_installation %}
<div class="card-actions">
<a href="{% url 'plugins:netbox_slm:softwareproductinstallation_add' %}?{{ search_obj }}={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add an installation
</a>
</div>
{% endif %}
</h2>
{% if search_obj == "cluster" %}
{% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" cluster=object.pk %}
{% elif search_obj == "device" %}
{% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" device=object.pk %}
{% elif search_obj == "virtualmachine" %}
{% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" virtualmachine=object.pk %}
{% endif %}
</div>
+59
View File
@@ -0,0 +1,59 @@
from django.test import TestCase
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
from virtualization.models import Cluster, ClusterType, VirtualMachine
class SlmBaseTestCase(TestCase):
p_name: str = "test product"
v_name: str = "test version"
l_name: str = "test license"
test_url: str = "https://github.com/ICTU/netbox_slm"
vm: VirtualMachine
software_product: SoftwareProduct
software_product_version: SoftwareProductVersion
software_product_installation: SoftwareProductInstallation
@classmethod
def setUpClass(cls):
super().setUpClass()
manufacturer = Manufacturer.objects.create(name="test manufacturer")
device_type = DeviceType.objects.create(model="test device type", manufacturer=manufacturer)
device_role = DeviceRole.objects.create(name="test device role")
site = Site.objects.create(name="test site")
cls.device = Device.objects.create(name="test device", device_type=device_type, role=device_role, site=site)
cls.vm = VirtualMachine.objects.create(name="test VM")
cluster_type = ClusterType.objects.create(name="test cluster type")
cls.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
cls.software_product = SoftwareProduct.objects.create(name=cls.p_name)
cls.software_product_version = SoftwareProductVersion.objects.create(
name=cls.v_name, software_product=cls.software_product
)
cls.software_product_installation = SoftwareProductInstallation.objects.create(
virtualmachine=cls.vm, software_product=cls.software_product, version=cls.software_product_version
)
cls.software_license = SoftwareLicense.objects.create(
name=cls.l_name,
software_product=cls.software_product,
version=cls.software_product_version,
installation=cls.software_product_installation,
stored_location_url=cls.test_url,
)
@classmethod
def tearDownClass(cls):
SoftwareLicense.objects.all().delete()
SoftwareProductInstallation.objects.all().delete()
SoftwareProductVersion.objects.all().delete()
SoftwareProduct.objects.all().delete()
Cluster.objects.all().delete()
ClusterType.objects.all().delete()
VirtualMachine.objects.all().delete()
Device.objects.all().delete()
Site.objects.all().delete()
DeviceRole.objects.all().delete()
DeviceType.objects.all().delete()
Manufacturer.objects.all().delete()
super().tearDownClass()
+51
View File
@@ -0,0 +1,51 @@
from unittest.mock import patch
from django.contrib.auth import get_user_model
from .base import SlmBaseTestCase
class FunctionalTestCase(SlmBaseTestCase):
"""Functional test cases that require a client and user with permissions"""
def setUp(self):
test_user, _ = get_user_model().objects.get_or_create(username="test", is_superuser=True)
self.client.force_login(test_user)
@classmethod
def tearDownClass(cls):
get_user_model().objects.all().delete()
super().tearDownClass()
def test_template_content(self):
cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/")
self.assertContains(cluster_response, f'<a href="/plugins/slm/installations/add/?cluster={self.cluster.pk}"')
self.assertContains(cluster_response, "</span> Add an installation")
device_response = self.client.get(f"/dcim/devices/{self.device.pk}/")
self.assertContains(device_response, f'<a href="/plugins/slm/installations/add/?device={self.device.pk}"')
self.assertContains(device_response, "</span> Add an installation")
vm_response = self.client.get(f"/virtualization/virtual-machines/{self.vm.pk}/")
self.assertTemplateUsed(vm_response, "netbox_slm/installations_card_include.html")
self.assertContains(vm_response, f'<a href="/plugins/slm/installations/add/?virtualmachine={self.vm.pk}"')
def test_setting_skip_content(self):
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_virtualmachine_installations=None)))
with patch.dict("netbox_slm.template_content.installations", {"virtualmachine": None}):
vm_response = self.client.get(f"/virtualization/virtual-machines/{self.vm.pk}/")
self.assertTemplateNotUsed(vm_response, "netbox_slm/installations_card_include.html")
def test_setting_left_content(self):
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_device_installations="left")))
with patch.dict("netbox_slm.template_content.installations", {"device": "left"}):
device_response = self.client.get(f"/dcim/devices/{self.device.pk}/")
self.assertContains(device_response, f'<a href="/plugins/slm/installations/add/?device={self.device.pk}"')
self.assertContains(device_response, "</span> Add an installation")
def test_setting_full_content(self):
# self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_cluster_installations="full")))
with patch.dict("netbox_slm.template_content.installations", {"cluster": "full"}):
cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/")
self.assertContains(cluster_response, f'<a href="/plugins/slm/installations/add/?cluster={self.cluster.pk}"')
self.assertContains(cluster_response, "</span> Add an installation")
+15 -39
View File
@@ -1,40 +1,8 @@
from django.test import TestCase from .base import SlmBaseTestCase
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
from virtualization.models import Cluster, ClusterType, VirtualMachine
class ModelTestCase(TestCase): class ModelTestCase(SlmBaseTestCase):
def setUp(self): """Test basic model functionality and custom overrides"""
self.p_name = "test product"
self.v_name = "test version"
self.l_name = "test license"
manufacturer = Manufacturer.objects.create(name="test manufacturer")
device_type = DeviceType.objects.create(model="test device type", manufacturer=manufacturer)
device_role = DeviceRole.objects.create(name="test device role")
site = Site.objects.create(name="test site")
self.device = Device.objects.create(name="test device", device_type=device_type, role=device_role, site=site)
self.vm = VirtualMachine.objects.create(name="test VM")
cluster_type = ClusterType.objects.create(name="test cluster type")
self.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
self.test_url = "https://github.com/ICTU/netbox_slm"
self.software_product = SoftwareProduct.objects.create(name=self.p_name)
self.software_product_version = SoftwareProductVersion.objects.create(
name=self.v_name, software_product=self.software_product
)
self.software_product_installation = SoftwareProductInstallation.objects.create(
virtualmachine=self.vm, software_product=self.software_product, version=self.software_product_version
)
self.software_license = SoftwareLicense.objects.create(
name=self.l_name,
software_product=self.software_product,
version=self.software_product_version,
installation=self.software_product_installation,
stored_location_url=self.test_url,
)
def test_model_name(self): def test_model_name(self):
self.assertEqual(self.p_name, str(self.software_product)) self.assertEqual(self.p_name, str(self.software_product))
@@ -43,10 +11,18 @@ class ModelTestCase(TestCase):
self.assertEqual(self.l_name, str(self.software_license)) self.assertEqual(self.l_name, str(self.software_license))
def test_absolute_url(self): def test_absolute_url(self):
self.assertEqual("/plugins/slm/software-products/1/", self.software_product.get_absolute_url()) self.assertEqual(
self.assertEqual("/plugins/slm/versions/1/", self.software_product_version.get_absolute_url()) f"/plugins/slm/software-products/{self.software_product.pk}/", self.software_product.get_absolute_url()
self.assertEqual("/plugins/slm/installations/1/", self.software_product_installation.get_absolute_url()) )
self.assertEqual("/plugins/slm/licenses/1/", self.software_license.get_absolute_url()) self.assertEqual(
f"/plugins/slm/versions/{self.software_product_version.pk}/",
self.software_product_version.get_absolute_url(),
)
self.assertEqual(
f"/plugins/slm/installations/{self.software_product_installation.pk}/",
self.software_product_installation.get_absolute_url(),
)
self.assertEqual(f"/plugins/slm/licenses/{self.software_license.pk}/", self.software_license.get_absolute_url())
def test_get_installation_count(self): def test_get_installation_count(self):
self.assertEqual( self.assertEqual(