Show SLM installations on native objects (#61)
Closes #49 Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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': {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
@@ -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")
|
||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user