diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 9ed8f8b..332ef78 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -30,6 +30,7 @@ jobs: /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 xml -o /ci/reports/coverage.xml" + sed -i "s/\/opt\/netbox\/netbox<\/source>/<\/source>/" ci/reports/coverage.xml timeout-minutes: 6 - name: Sonar scan diff --git a/CHANGELOG.md b/CHANGELOG.md index f834d79..f5a8285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Bulk import and bulk edit (#4) * Changelog with backdated changes (#51) * List filtering (#54) +* Show installations on native objects (#49) ### Changed diff --git a/ci/config/configuration.py b/ci/config/configuration.py index e9d5aa7..a412657 100644 --- a/ci/config/configuration.py +++ b/ci/config/configuration.py @@ -19,11 +19,6 @@ DEBUG = environ.get('DEBUG', 'False').lower() == 'true' DEVELOPER = environ.get('DEVELOPER', 'False').lower() == 'true' PLUGINS = ["netbox_slm"] -PLUGINS_CONFIG = { - "netbox_slm": { - "top_level_menu": environ.get('SLM_TOP_LEVEL_MENU', 'True').lower() == 'true', - }, -} REDIS = { 'tasks': { diff --git a/netbox_slm/__init__.py b/netbox_slm/__init__.py index 215d6c2..9066b0e 100644 --- a/netbox_slm/__init__.py +++ b/netbox_slm/__init__.py @@ -6,13 +6,18 @@ __version__ = "1.7.0" class SLMConfig(PluginConfig): name = "netbox_slm" verbose_name = "Software Lifecycle Management" - description = "Software Lifecycle Management Netbox Plugin." version = __version__ + description = "Software Lifecycle Management Netbox Plugin." author = "ICTU" author_email = "open-source-projects@ictu.nl" base_url = "slm" 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 diff --git a/netbox_slm/filtersets.py b/netbox_slm/filtersets.py index 80319b4..bbeeb49 100644 --- a/netbox_slm/filtersets.py +++ b/netbox_slm/filtersets.py @@ -10,7 +10,7 @@ from netbox_slm.models import ( SoftwareLicense, SoftwareReleaseTypes, ) -from virtualization.models import VirtualMachine, Cluster +from virtualization.models import Cluster, VirtualMachine class SoftwareProductFilterSet(NetBoxModelFilterSet): diff --git a/netbox_slm/forms/software_product_installation.py b/netbox_slm/forms/software_product_installation.py index 8f3d372..b648f7f 100644 --- a/netbox_slm/forms/software_product_installation.py +++ b/netbox_slm/forms/software_product_installation.py @@ -12,7 +12,7 @@ from utilities.forms.fields import ( ) from utilities.forms.rendering import FieldSet from utilities.forms.widgets import APISelect -from virtualization.models import VirtualMachine, Cluster +from virtualization.models import Cluster, VirtualMachine class SoftwareProductInstallationForm(NetBoxModelForm): diff --git a/netbox_slm/navigation.py b/netbox_slm/navigation.py index 4d8b25f..6ce7825 100644 --- a/netbox_slm/navigation.py +++ b/netbox_slm/navigation.py @@ -1,6 +1,4 @@ -from django.conf import settings - -from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu +from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config from . import SLMConfig 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( label="Software Lifecycle", groups=((SLMConfig.verbose_name, slm_items),), icon_class="mdi mdi-content-save", ) else: + # auto imported by default PluginConfig.menu_items = navigation.menu_items menu_items = slm_items diff --git a/netbox_slm/template_content.py b/netbox_slm/template_content.py new file mode 100644 index 0000000..fade1f7 --- /dev/null +++ b/netbox_slm/template_content.py @@ -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] diff --git a/netbox_slm/templates/netbox_slm/installations_card_include.html b/netbox_slm/templates/netbox_slm/installations_card_include.html new file mode 100644 index 0000000..2694da4 --- /dev/null +++ b/netbox_slm/templates/netbox_slm/installations_card_include.html @@ -0,0 +1,21 @@ +{% load helpers %} + +
+

+ Installations +{% if perms.netbox_slm.add_installation %} + +{% endif %} +

+{% 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 %} +
diff --git a/netbox_slm/tests/base.py b/netbox_slm/tests/base.py new file mode 100644 index 0000000..578e7cc --- /dev/null +++ b/netbox_slm/tests/base.py @@ -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() diff --git a/netbox_slm/tests/test_functional.py b/netbox_slm/tests/test_functional.py new file mode 100644 index 0000000..95fb875 --- /dev/null +++ b/netbox_slm/tests/test_functional.py @@ -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' Add an installation") + + device_response = self.client.get(f"/dcim/devices/{self.device.pk}/") + self.assertContains(device_response, f' 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' 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' Add an installation") diff --git a/netbox_slm/tests/test_models.py b/netbox_slm/tests/test_models.py index 1165ebf..1e33685 100644 --- a/netbox_slm/tests/test_models.py +++ b/netbox_slm/tests/test_models.py @@ -1,40 +1,8 @@ -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 +from .base import SlmBaseTestCase -class ModelTestCase(TestCase): - def setUp(self): - 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, - ) +class ModelTestCase(SlmBaseTestCase): + """Test basic model functionality and custom overrides""" def test_model_name(self): 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)) def test_absolute_url(self): - self.assertEqual("/plugins/slm/software-products/1/", self.software_product.get_absolute_url()) - self.assertEqual("/plugins/slm/versions/1/", self.software_product_version.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/software-products/{self.software_product.pk}/", self.software_product.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): self.assertEqual(