40 Commits
Author SHA1 Message Date
Hedde van der Heide a53486ca23 bump version to 1.2 2022-05-19 11:08:57 +02:00
Hedde van der Heide 4bc418a5d0 fixed filter bug versions api and set required on manufacturer for softwareproducts add 2022-05-19 11:06:34 +02:00
Hedde van der Heide e952111a53 version bump 1.1 2022-05-03 15:21:18 +02:00
Hedde van der Heide e73cb5fa1a Merge branch 'master' of https://github.com/ICTU/netbox_slm 2022-05-03 15:19:55 +02:00
Hedde van der Heide 56552bdd02 fixed filter search 2022-05-03 15:19:53 +02:00
Hedde van der Heide f914e78812 Update README.md 2022-05-02 12:39:47 +02:00
Hedde van der Heide c9a7b2315e Update README.md 2022-05-02 12:37:36 +02:00
Hedde van der Heide a8c9fc5fbe Delete LICENSE.md 2022-05-02 12:37:18 +02:00
Hedde van der Heide a1f6e3dc92 Create LICENSE 2022-05-02 12:37:11 +02:00
Hedde van der Heide 4f8053c0f6 Rename LICENSE to LICENSE.md 2022-05-02 12:32:20 +02:00
Hedde van der Heide 3b3a6d63d4 Update and rename README.rst to README.md 2022-05-02 12:32:08 +02:00
Hedde van der Heide ce283dea52 updated README 2022-04-22 14:04:29 +02:00
Hedde van der Heide f3579cce63 updated README 2022-04-22 13:57:49 +02:00
Hedde van der Heide 3bb711ea84 updated README 2022-04-22 13:57:45 +02:00
Hedde van der Heide 9d76bb421d normalized project structure 2022-04-22 13:52:26 +02:00
Hedde van der Heide 7a24221444 moved top level files 2022-04-22 13:33:17 +02:00
Hedde van der Heide ac198ef7cf Large overhaul of naming conventions and code clean ups 2022-04-22 13:30:10 +02:00
Hedde van der Heide 6d05b1b6ca bumped version 2022-04-15 09:09:41 +02:00
Hedde van der Heide 049fb045c0 added missing migrations (though stale) 2022-04-15 09:06:28 +02:00
Hedde van der Heide 9e81b94d31 bumped version 2022-04-15 09:02:19 +02:00
Hedde van der Heide 18fed2c029 fixed reverse relationship naming (to default) and added ordering methods for calculated attributes 2022-04-15 09:00:36 +02:00
Hedde van der Heide a02acbc561 fixed installation ordering issue with field methods 2022-04-14 11:35:25 +02:00
Hedde van der Heide 74d648b94d update README.rst, added warning 2022-04-10 11:24:50 +02:00
Hedde van der Heide e9422d70e3 update README.rst 2022-04-10 11:22:33 +02:00
Hedde van der Heide 15b61fd7f9 bumped version for next release 2022-04-10 11:18:22 +02:00
Hedde van der Heide c264536850 Merge branch 'master' of https://github.com/ICTU/netbox_slm 2022-04-10 11:10:06 +02:00
Hedde van der Heide 2dbcaa4877 3.2.X compatibility fixes 2022-04-10 11:10:00 +02:00
Hedde van der Heide 6884bb3b94 Merge remote-tracking branch 'origin/feature/netbox-3.1.X-compatibility' 2022-04-07 09:31:42 +02:00
Hedde van der Heide bf47320864 fixed import no longer working under netbox >=3.1.X: 2022-04-07 09:30:28 +02:00
Hedde van der Heide f7bb83c913 Update README.rst 2022-03-28 12:35:56 +02:00
Hedde van der Heide 0519796af6 Update README.rst 2022-03-28 12:35:32 +02:00
Hedde van der Heide 19fc473286 version bump 0.94 2022-03-28 11:14:03 +02:00
Hedde van der Heide f4188b957f fixed 404 bug, added default ordering for some tables 2022-03-28 11:07:33 +02:00
Hedde van der Heide 51736942e7 fixed display bug installations detail, merged device/vm columns and added type for installations table view 2022-03-24 11:15:53 +01:00
Hedde van der Heide bf63046edb fixed bugs and updated to version 0.93: 2022-02-11 12:52:51 +01:00
Hedde van der Heide 72274e59b5 fixed bugs and updated to version 0.93: 2022-02-11 12:51:37 +01:00
Hedde van der Heide ae06bea12d tmp fix with setup 2022-01-21 13:16:24 +01:00
Hedde van der Heide 6937a0ecca added MANIFEST.in to include templates 2022-01-21 13:04:45 +01:00
Hedde van der Heide e7eb5b46e4 added MANIFEST.in to include templates 2022-01-21 13:02:42 +01:00
Hedde van der Heide adea4437c9 fixed breaking changes (partially, templates require another fix) 2021-12-23 11:09:58 +01:00
47 changed files with 804 additions and 1686 deletions
View File
+55 -52
View File
@@ -1,40 +1,47 @@
Software Lifecycle Management <h1 align="center">NetBox SLM</h1>
-----------------------------
*Netbox Plugin for lifecycle management of software components, including versions and installations* <p align="center"><i>Netbox SLM is a plugin for lifecycle management of software components, including versions and installations.</i></p>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-------------- <div align="center">
<a href="https://pypi.org/project/netbox_slm/"><img src="https://img.shields.io/pypi/v/netbox_slm" alt="PyPi"/></a>
<a href="https://github.com/ICTU/netbox_slm/stargazers"><img src="https://img.shields.io/github/stars/ICTU/netbox_slm" alt="Stars Badge"/></a>
<a href="https://github.com/ICTU/netbox_slm/network/members"><img src="https://img.shields.io/github/forks/ICTU/netbox_slm" alt="Forks Badge"/></a>
<a href="https://github.com/ICTU/netbox_slm/pulls"><img src="https://img.shields.io/github/issues-pr/ICTU/netbox_slm" alt="Pull Requests Badge"/></a>
<a href="https://github.com/ICTU/netbox_slm/issues"><img src="https://img.shields.io/github/issues/ICTU/netbox_slm" alt="Issues Badge"/></a>
<a href="https://github.com/ICTU/netbox_slm/graphs/contributors"><img alt="GitHub contributors" src="https://img.shields.io/github/contributors/ICTU/netbox_slm?color=2b9348"></a>
<a href="https://github.com/ICTU/netbox_slm/blob/master/LICENSE"><img src="https://img.shields.io/github/license/ICTU/netbox_slm?color=2b9348" alt="License Badge"/></a>
</div>
Installation Guide
~~~~~~~~~~~~~~~~~~
When using the Docker version of Netbox, first follow the `netbox-docker ## Known Issues
quickstart <https://github.com/netbox-community/netbox-docker#quickstart>`__
instructions to clone the netbox-docker repo and set up the
``docker-compose.override.yml``.
Next, follow these instructions (based on the generic `Netbox plugin - WARNING: This plugin is only tested with Netbox 3.2 at this time.
instructions <https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins>`__) - CSV/Bulk imports for SoftwareProduct, Version and Installation are currently broken (WIP)
## Installation Guide
When using the Docker version of Netbox, first follow the netbox-docker [quickstart](https://github.com/netbox-community/netbox-docker#quickstart) instructions to clone the netbox-docker repo and set up the ``docker-compose.override.yml``.
Next, follow these instructions (based on the Netbox docker variant
[instructions](https://github.com/netbox-community/netbox-docker/wiki/Configuration#custom-configuration-files))
to install the Netbox SLM plugin: to install the Netbox SLM plugin:
1. Add ``netbox_slm`` to the ``PLUGINS`` list in 1. Add ``netbox_slm`` to the ``PLUGINS`` list in
``configuration/configuration.py``. ``configuration/extra.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.9`` as 2. Create a ``plugin_requirements.txt`` with ``netbox-slm==1.2`` as
contents. contents.
3. Create a ``Dockerfile-SLM`` with contents: 3. Create a ``Dockerfile-SLM`` with contents:
.. code:: dockerfile ```
FROM netboxcommunity/netbox:v3.2.X
FROM netboxcommunity/netbox:v3.0.9 # The Netbox SLM plugin currently only works with v3.0.9 of Netbox due to backwards incompatible changes in newer version of Netbox
COPY ./plugin_requirements.txt / COPY ./plugin_requirements.txt /
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
```
4. Create a ``docker-compose.override.yml`` with contents: 4. Create a ``docker-compose.override.yml`` with contents:
.. code:: yaml ```
version: '3.4' version: '3.4'
services: services:
netbox: netbox:
@@ -48,23 +55,24 @@ to install the Netbox SLM plugin:
image: netbox:slm image: netbox:slm
netbox-housekeeping: netbox-housekeeping:
image: netbox:slm image: netbox:slm
```
Now, build the image: ``docker compose build --no-cache`` Now, build the image: ``docker compose build --no-cache``
And finally, run Netbox with the SLM plugin: ``docker compose up`` And finally, run Netbox with the SLM plugin: ``docker compose up``
Releasing Guide ## Releasing Guide
~~~~~~~~~~~~~~~
To draft a release; To draft a release;
update the setup.py file to reflect the new version, then from the *src* update the setup.py file to reflect the new version, then from the *src*
directory run directory run
:: ```
# make sure to update the version in netbox_slm/__init__.py
$ python setup.py sdist $ python setup.py sdist
$ twine upload dist/* $ twine upload dist/*
```
On Github.com create a similar tag and version. These steps could be On Github.com create a similar tag and version. These steps could be
automated with a github workflow. automated with a github workflow.
@@ -72,65 +80,56 @@ automated with a github workflow.
n.b. Currently the plugin is configured to use a personal pypi account, n.b. Currently the plugin is configured to use a personal pypi account,
this should be changed. this should be changed.
Developer Guide (local installation) ## Developer Guide (local installation)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*Follow the steps below on your local system to run netbox and the *Follow the steps below on your local system to run netbox and the
netbox_slm plugin in developer mode* netbox_slm plugin in developer mode*
Setup ### Setup
~~~~~
The goal below is to run all Netbox components in Docker and run a local The goal below is to run all Netbox components in Docker and run a local
Netbox Django copy with auto-reload to develop the plugin pointing to Netbox Django copy with auto-reload to develop the plugin pointing to
the Dockerized postgres and redis instances, basically ignoring the the Dockerized postgres and redis instances, basically ignoring the
netbox docker runtime server. netbox docker runtime server.
Gotchas ### Steps
~~~~~~~~
*Netboxs exception handler seems to supress initial Exceptions,
obfuscating missing imports or other Django related issues by re-raising
an opague error, e.g. netbox_slm plugin namespace doesnotexist. Its
recommended to often check the runtime server or patch Netboxs global
exception handler.*
Steps
~~~~~
::
from your projects directory clone the netbox repository from your projects directory clone the netbox repository
```
$ git clone https://github.com/netbox-community/netbox $ git clone https://github.com/netbox-community/netbox
$ cd netbox $ cd netbox
$ git checkout f5356b8 (to pin 3.0.9) ```
install the virtual environment install the virtual environment
```
$ pipenv shell $ pipenv shell
$ pipenv install $ pipenv install
```
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file; create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
```
DEBUG = True DEBUG = True
SECRET_KEY = 'dummy' SECRET_KEY = 'dummy'
DEVELOPER = True DEVELOPER = True
PLUGINS = [ PLUGINS = [
'netbox_slm', 'netbox_slm',
] ]
```
The Netbox installation above will be used to run Django management The Netbox installation above will be used to run Django management
commands like runserver, makemigrations and migrate, which will be commands like runserver, makemigrations and migrate, which will be
explained in the next steps below; explained in the next steps below;
::
from your projects directory clone the netbox-slm repository from your projects directory clone the netbox-slm repository
```
$ git clone https://github.com/ICTU/netbox_slm $ git clone https://github.com/ICTU/netbox_slm
$ cd netbox_slm $ cd netbox_slm
$ ./start-netbox.sh $ ./start-netbox.sh
```
This will start Netbox locally (requires Docker) and forward the redis This will start Netbox locally (requires Docker) and forward the redis
and postgres ports to the localhost (make sure theres no processes and postgres ports to the localhost (make sure theres no processes
@@ -138,20 +137,24 @@ using these ports or change the dockerfiles accordingly)
Note, you can also start and stop netbox by hand: Note, you can also start and stop netbox by hand:
:: ```
$ cd netbox-docker $ cd netbox-docker
$ docker-compose up -d $ docker-compose up -d
```
or stop the stack with or stop the stack with
```
$ docker-compose down $ docker-compose down
```
# to start fresh: # to start fresh:
```
$ docker-compose down $ docker-compose down
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera $ docker volume rm netbox-docker_netbox-postgres-data # et cetera
$ docker-compose up -d --force-recreate $ docker-compose up -d --force-recreate
```
this will require you to re-run the migrate commando's for netbox-slm, see further down below this will require you to re-run the migrate commando's for netbox-slm, see further down below
@@ -160,8 +163,7 @@ redis connection strings (username, password) to the ones the netbox
docker backend is using, for example (using default user and passwords docker backend is using, for example (using default user and passwords
from the netbox docker example): from the netbox docker example):
:: ```
<<collapsed>> <<collapsed>>
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
@@ -208,15 +210,16 @@ from the netbox docker example):
} }
<<collapsed>> <<collapsed>>
```
Now you can run commands from the netbox repository like this; Now you can run commands from the netbox repository like this;
:: ```
$ cd netbox/netbox $ cd netbox/netbox
$ export PYTHONPATH=../../netbox-slm/src/ $ export PYTHONPATH=../../netbox_slm/netbox_slm/ # or with the pipenv activated run `python3 setup.py develop` from the netbox_slm directory
$ python3 manage.py migrate netbox_slm $ python3 manage.py migrate netbox_slm
$ python3 manage.py runserver 8001 $ python3 manage.py runserver 8001
```
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading Visit http://127.0.0.1:8001 in the browser to see the auto reloading
version of the netbox UI. Port 8000 is taken by the docker ran variant. version of the netbox UI. Port 8000 is taken by the docker ran variant.
+13
View File
@@ -0,0 +1,13 @@
# Security Policy
## Current status
Netbox SLM is a plugin for Netbox with mostly default configuration. We advise to monitor Netbox security findings and update accordingly.
## Supported Versions
Only the latest version of Netbox SLM is currently being supported with security updates. We aim to keep the plugin compatible with the latest Netbox version, but have no incentive to patch older tags at this point.
## Reporting a Vulnerability
Please report security vulnerabilities by opening a ticket. The aim is to get back to you within 24 hours with a confirmation of the issue and a brief action plan or a request for more information.
@@ -5,7 +5,7 @@ class SLMConfig(PluginConfig):
name = 'netbox_slm' name = 'netbox_slm'
verbose_name = 'Software Lifecycle Management' verbose_name = 'Software Lifecycle Management'
description = 'Software Lifecycle Management' description = 'Software Lifecycle Management'
version = '0.1' version = '1.2'
author = 'Hedde van der Heide' author = 'Hedde van der Heide'
author_email = 'hedde.vanderheide@ictu.nl' author_email = 'hedde.vanderheide@ictu.nl'
base_url = 'slm' base_url = 'slm'
View File
+55
View File
@@ -0,0 +1,55 @@
from rest_framework import serializers
from netbox.api.serializers import NetBoxModelSerializer
from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
)
class SoftwareProductSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproduct-detail"
)
class Meta:
model = SoftwareProduct
fields = [
'id', 'display', 'url', 'name', 'tags', 'custom_field_data', 'created', 'last_updated',
]
def get_display(self, obj):
return f"{obj.manufacturer.name} - {obj.name}"
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproductversion-detail"
)
class Meta:
model = SoftwareProductVersion
fields = [
'id', 'display', 'url', 'name', 'software_product', 'tags', 'custom_field_data', 'created', 'last_updated',
]
def get_display(self, obj):
return obj.name
class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail"
)
class Meta:
model = SoftwareProductInstallation
fields = [
'id', 'display', 'url', 'device', 'virtualmachine', 'software_product', 'version', 'tags',
'custom_field_data', 'created', 'last_updated',
]
def get_display(self, obj):
return obj
@@ -1,13 +1,15 @@
from netbox.api import OrderedDefaultRouter from netbox.api.routers import NetBoxRouter
from netbox_slm.api.views import ( from netbox_slm.api.views import (
NetboxSLMRootView, NetboxSLMRootView,
SoftwareProductViewSet, SoftwareProductViewSet,
SoftwareProductVersionViewSet, SoftwareProductVersionViewSet,
SoftwareProductInstallationViewSet,
) )
router = OrderedDefaultRouter() router = NetBoxRouter()
router.APIRootView = NetboxSLMRootView router.APIRootView = NetboxSLMRootView
router.register("softwareproducts", SoftwareProductViewSet) router.register("softwareproducts", SoftwareProductViewSet)
router.register("softwareproductversions", SoftwareProductVersionViewSet) router.register("softwareproductversions", SoftwareProductVersionViewSet)
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
urlpatterns = router.urls urlpatterns = router.urls
+39
View File
@@ -0,0 +1,39 @@
from netbox.api.viewsets import NetBoxModelViewSet
from rest_framework.routers import APIRootView
from netbox_slm.api.serializers import (
SoftwareProductSerializer, SoftwareProductVersionSerializer, SoftwareProductInstallationSerializer,
)
from netbox_slm.filtersets import (
SoftwareProductFilterSet, SoftwareProductVersionFilterSet, SoftwareProductInstallationFilterSet,
)
from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
)
class NetboxSLMRootView(APIRootView):
"""
NetboxSLM API root view
"""
def get_view_name(self):
return "NetboxSLM"
class SoftwareProductViewSet(NetBoxModelViewSet):
queryset = SoftwareProduct.objects.all()
serializer_class = SoftwareProductSerializer
filterset_class = SoftwareProductFilterSet
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
queryset = SoftwareProductVersion.objects.all()
serializer_class = SoftwareProductVersionSerializer
filterset_class = SoftwareProductVersionFilterSet
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
queryset = SoftwareProductInstallation.objects.all()
serializer_class = SoftwareProductInstallationSerializer
filterset_class = SoftwareProductInstallationFilterSet
+53
View File
@@ -0,0 +1,53 @@
from django.db.models import Q
from netbox.filtersets import NetBoxModelFilterSet
from netbox_slm.models import *
class SoftwareProductFilterSet(NetBoxModelFilterSet):
"""Filter capabilities for SoftwareProduct instances."""
class Meta:
model = SoftwareProduct
fields = tuple()
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) | \
Q(manufacturer__name__icontains=value)
return queryset.filter(qs_filter)
class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
"""Filter capabilities for SoftwareProductVersion instances."""
class Meta:
model = SoftwareProductVersion
fields = (
"software_product",
)
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) | \
Q(software_product__name__icontains=value) | \
Q(software_product__manufacturer__name__icontains=value)
return queryset.filter(qs_filter)
class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
"""Filter capabilities for SoftwareProductInstallation instances."""
class Meta:
model = SoftwareProductInstallation
fields = tuple()
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(software_product__name__icontains=value) | \
Q(software_product__manufacturer__name__icontains=value) | \
Q(version__name__icontains=value)
return queryset.filter(qs_filter)
+174
View File
@@ -0,0 +1,174 @@
from django import forms
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from dcim.models import Manufacturer, Device
from netbox.forms import (
NetBoxModelForm,
NetBoxModelCSVForm,
NetBoxModelBulkEditForm,
NetBoxModelFilterSetForm,
)
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.forms import (
DynamicModelChoiceField, APISelect, TagFilterField, ChoiceField
)
from virtualization.models import VirtualMachine
class SoftwareProductForm(NetBoxModelForm):
"""Form for creating a new SoftwareProduct object."""
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=True,
)
class Meta:
model = SoftwareProduct
fields = ("name", "manufacturer", "description", "tags")
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProduct
fieldsets = (
(None, ('q', 'tag')),
)
tag = TagFilterField(model)
class SoftwareProductCSVForm(NetBoxModelCSVForm):
class Meta:
model = SoftwareProduct
fields = ("name", "manufacturer",)
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
model = SoftwareProduct
nullable_fields = []
class SoftwareProductVersionForm(NetBoxModelForm):
"""Form for creating a new SoftwareProductVersion object."""
name = forms.CharField(label=_("Version"))
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
)
class Meta:
model = SoftwareProductVersion
fields = ("name", "software_product", "tags")
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductVersion
fieldsets = (
(None, ('q', 'tag')),
)
tag = TagFilterField(model)
class SoftwareProductVersionCSVForm(NetBoxModelCSVForm):
class Meta:
model = SoftwareProductVersion
fields = ("name",)
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
model = SoftwareProductVersion
nullable_fields = []
class SoftwareProductInstallationForm(NetBoxModelForm):
"""Form for creating a new SoftwareProductInstallation object."""
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
)
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
),
query_params={
'software_product': '$software_product',
}
)
class Meta:
model = SoftwareProductInstallation
fields = ("device", "virtualmachine", "software_product", "version", "tags")
def clean_version(self):
version = self.cleaned_data['version']
software_product = self.cleaned_data['software_product']
if version not in software_product.softwareproductversion_set.all():
raise forms.ValidationError(_(f"Version `{version}` doesn't exist on {software_product}, make sure you've "
f"selected a compatible version or first select the software product."))
return version
def clean(self):
if not any([self.cleaned_data['device'], self.cleaned_data['virtualmachine']]):
raise forms.ValidationError(_("Installation requires atleast one virtualmachine or device destination."))
return super(SoftwareProductInstallationForm, self).clean()
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
model = SoftwareProductInstallation
fieldsets = (
(None, ('q', 'tag',)),
)
tag = TagFilterField(model)
class SoftwareProductInstallationCSVForm(NetBoxModelCSVForm):
class Meta:
model = SoftwareProductInstallation
fields = tuple()
class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=False
)
model = SoftwareProductInstallation
fieldsets = (
(None, ('software_product', 'version',)),
)
@@ -0,0 +1,20 @@
# Generated by Django 3.2.9 on 2022-01-24 08:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0133_port_colors'),
('netbox_slm', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='softwareproduct',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='software_products', to='dcim.manufacturer'),
),
]
@@ -0,0 +1,43 @@
# Generated by Django 3.2.9 on 2022-04-07 12:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('netbox_slm', '0002_alter_softwareproduct_manufacturer'),
]
operations = [
migrations.AlterField(
model_name='softwareproduct',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='softwareproduct',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='softwareproductversion',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='softwareproductversion',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
),
]
@@ -0,0 +1,46 @@
# Generated by Django 3.2.9 on 2022-04-15 07:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0153_created_datetimefield'),
('virtualization', '0029_created_datetimefield'),
('netbox_slm', '0003_auto_20220407_1209'),
]
operations = [
migrations.AlterField(
model_name='softwareproduct',
name='manufacturer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.manufacturer'),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.device'),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='software_product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='version',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion'),
),
migrations.AlterField(
model_name='softwareproductinstallation',
name='virtualmachine',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.virtualmachine'),
),
migrations.AlterField(
model_name='softwareproductversion',
name='software_product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'),
),
]
@@ -1,23 +1,19 @@
from django.db import models from django.db import models
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import safestring from django.utils import safestring
from extras.utils import extras_features
from netbox.models import PrimaryModel from netbox.models import NetBoxModel
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class SoftwareProduct(NetBoxModel):
class SoftwareProduct(PrimaryModel):
"""
A SoftwareProduct represents ...
"""
name = models.CharField(max_length=128) name = models.CharField(max_length=128)
description = models.CharField(max_length=255, null=True, blank=True) description = models.CharField(max_length=255, null=True, blank=True)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
to='dcim.Manufacturer', to='dcim.Manufacturer',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='software_products' null=True, blank=True
) )
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@@ -36,12 +32,10 @@ class SoftwareProduct(PrimaryModel):
)) if count else "0" )) if count else "0"
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class SoftwareProductVersion(NetBoxModel):
class SoftwareProductVersion(PrimaryModel):
software_product = models.ForeignKey( software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct', to='netbox_slm.SoftwareProduct',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='softwareproduct_versions'
) )
name = models.CharField(max_length=64) name = models.CharField(max_length=64)
@@ -61,31 +55,26 @@ class SoftwareProductVersion(PrimaryModel):
)) if count else "0" )) if count else "0"
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class SoftwareProductInstallation(NetBoxModel):
class SoftwareProductInstallation(PrimaryModel):
device = models.ForeignKey( device = models.ForeignKey(
to='dcim.Device', to='dcim.Device',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='softwareproduct_installations',
null=True, null=True,
blank=True blank=True
) )
virtualmachine = models.ForeignKey( virtualmachine = models.ForeignKey(
to='virtualization.VirtualMachine', to='virtualization.VirtualMachine',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='softwareproduct_installations',
null=True, null=True,
blank=True blank=True
) )
software_product = models.ForeignKey( software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct', to='netbox_slm.SoftwareProduct',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='software_products'
) )
version = models.ForeignKey( version = models.ForeignKey(
to='netbox_slm.SoftwareProductVersion', to='netbox_slm.SoftwareProductVersion',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='softwareproduct_versions'
) )
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@@ -95,3 +84,9 @@ class SoftwareProductInstallation(PrimaryModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk}) return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
def get_platform(self):
return self.device or self.virtualmachine
def render_type(self):
return f"{'device' if self.device else 'virtualmachine'}"
@@ -1,6 +1,7 @@
from extras.plugins import PluginMenuButton, PluginMenuItem from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices from utilities.choices import ButtonColorChoices
menu_items = ( menu_items = (
PluginMenuItem( PluginMenuItem(
link='plugins:netbox_slm:softwareproduct_list', link='plugins:netbox_slm:softwareproduct_list',
@@ -1,10 +1,12 @@
import django_tables2 as tables import django_tables2 as tables
from django.db.models import Count
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.tables import BaseTable, ChoiceFieldColumn, ToggleColumn from netbox.tables import NetBoxTable, ChoiceFieldColumn, ToggleColumn, columns
class SoftwareProductTable(BaseTable): class SoftwareProductTable(NetBoxTable):
"""Table for displaying SoftwareProduct objects.""" """Table for displaying SoftwareProduct objects."""
pk = ToggleColumn() pk = ToggleColumn()
@@ -15,11 +17,11 @@ class SoftwareProductTable(BaseTable):
) )
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations') installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
# tags = TagColumn( tags = columns.TagColumn(
# url_name="plugins:netbox_dns:zone_list", url_name="plugins:netbox_slm:softwareproduct_list",
# ) )
class Meta(BaseTable.Meta): class Meta(NetBoxTable.Meta):
model = SoftwareProduct model = SoftwareProduct
fields = ( fields = (
"pk", "pk",
@@ -27,7 +29,7 @@ class SoftwareProductTable(BaseTable):
"manufacturer", "manufacturer",
"description", "description",
"installations", "installations",
# "tags", "tags",
) )
default_columns = ( default_columns = (
"pk", "pk",
@@ -35,11 +37,23 @@ class SoftwareProductTable(BaseTable):
"manufacturer", "manufacturer",
"description", "description",
"installations", "installations",
# "tags", "tags",
)
sequence = (
"manufacturer",
"name",
"description",
"installations",
) )
def order_installations(self, queryset, is_descending):
queryset = queryset.annotate(
count=Count('softwareproductinstallation__id')
).order_by(("-" if is_descending else "") + "count")
return queryset, True
class SoftwareProductVersionTable(BaseTable):
class SoftwareProductVersionTable(NetBoxTable):
"""Table for displaying SoftwareProductVersion objects.""" """Table for displaying SoftwareProductVersion objects."""
pk = ToggleColumn() pk = ToggleColumn()
@@ -54,11 +68,11 @@ class SoftwareProductVersionTable(BaseTable):
) )
installations = tables.Column(accessor='get_installation_count', verbose_name='Installations') installations = tables.Column(accessor='get_installation_count', verbose_name='Installations')
# tags = TagColumn( tags = columns.TagColumn(
# url_name="plugins:netbox_dns:zone_list", url_name="plugins:netbox_slm:softwareproductversion_list",
# ) )
class Meta(BaseTable.Meta): class Meta(NetBoxTable.Meta):
model = SoftwareProductVersion model = SoftwareProductVersion
fields = ( fields = (
"pk", "pk",
@@ -66,7 +80,7 @@ class SoftwareProductVersionTable(BaseTable):
"software_product", "software_product",
"manufacturer", "manufacturer",
"installations", "installations",
# "tags", "tags",
) )
default_columns = ( default_columns = (
"pk", "pk",
@@ -74,11 +88,23 @@ class SoftwareProductVersionTable(BaseTable):
"software_product", "software_product",
"manufacturer", "manufacturer",
"installations", "installations",
# "tags", "tags",
)
sequence = (
"manufacturer",
"software_product",
"name",
"installations",
) )
def order_installations(self, queryset, is_descending):
queryset = queryset.annotate(
count=Count('softwareproductinstallation__id')
).order_by(("-" if is_descending else "") + "count")
return queryset, True
class SoftwareProductInstallationTable(BaseTable):
class SoftwareProductInstallationTable(NetBoxTable):
"""Table for displaying SoftwareProductInstallation objects.""" """Table for displaying SoftwareProductInstallation objects."""
pk = ToggleColumn() pk = ToggleColumn()
@@ -91,6 +117,11 @@ class SoftwareProductInstallationTable(BaseTable):
accessor=Accessor('virtualmachine'), accessor=Accessor('virtualmachine'),
linkify=True linkify=True
) )
platform = tables.Column(
accessor='get_platform',
linkify=True
)
type = tables.Column(accessor='render_type')
software_product = tables.Column( software_product = tables.Column(
accessor=Accessor('software_product'), accessor=Accessor('software_product'),
linkify=True linkify=True
@@ -100,26 +131,37 @@ class SoftwareProductInstallationTable(BaseTable):
linkify=True linkify=True
) )
# tags = TagColumn( tags = columns.TagColumn(
# url_name="plugins:netbox_dns:zone_list", url_name="plugins:netbox_slm:softwareproductinstallation_list",
# ) )
class Meta(BaseTable.Meta): class Meta(NetBoxTable.Meta):
model = SoftwareProductInstallation model = SoftwareProductInstallation
fields = ( fields = (
"pk", "pk",
"name", "name",
"device", "platform",
"virtualmachine", "type",
"software_product", "software_product",
"version", "version",
# "tags", "tags",
) )
default_columns = ( default_columns = (
"pk", "pk",
"device", "platform",
"virtualmachine", "type",
"software_product", "software_product",
"version", "version",
# "tags", "tags",
) )
def order_platform(self, queryset, is_descending):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
return queryset, True
def order_type(self, queryset, is_descending):
queryset = queryset.order_by(("device" if is_descending else "virtualmachine"))
return queryset, True
def render_software_product(self, value, **kwargs):
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
@@ -0,0 +1,46 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software Product
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
{% for version in object.softwareproduct_versions.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
</a>
{% empty %}
n/a
{% endfor %}
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{# {% include 'inc/panels/comments.html' %}#}
{% plugin_left_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,49 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software Product Installation
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
{% if object.device %}
<tr>
<th scope="row">Device</th>
<td>{{ object.device }}</td>
</tr>
{% else %}
<tr>
<th scope="row">Virtualmachine</th>
<td>{{ object.virtualmachine }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">Software Product</th>
<td>{{ object.software_product }}</td>
</tr>
<tr>
<th scope="row">Version</th>
<td>{{ object.version }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{# {% include 'inc/panels/comments.html' %}#}
{% plugin_left_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,40 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row my-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Software Product Version
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Installations</th>
<td>
{{ object.get_installation_count }}
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{# {% include 'inc/panels/comments.html' %}#}
{% plugin_left_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}
+28 -14
View File
@@ -1,5 +1,6 @@
from django.urls import path from django.urls import path
from extras.views import ObjectChangeLogView from netbox.views.generic import ObjectChangeLogView
from netbox_slm import views from netbox_slm import views
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
@@ -9,9 +10,11 @@ urlpatterns = [
path("software-products/add/", views.SoftwareProductEditView.as_view(), name='softwareproduct_add'), path("software-products/add/", views.SoftwareProductEditView.as_view(), name='softwareproduct_add'),
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"), path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"), path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(), name="softwareproduct_bulk_delete"), path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(),
name="softwareproduct_bulk_delete"),
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"), path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
path("software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(), name="softwareproduct_delete"), path("software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(),
name="softwareproduct_delete"),
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"), path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
path( path(
"software-products/<int:pk>/changelog/", "software-products/<int:pk>/changelog/",
@@ -23,11 +26,14 @@ urlpatterns = [
# Software Product Versions # Software Product Versions
path("versions/", views.SoftwareProductVersionListView.as_view(), name='softwareproductversion_list'), path("versions/", views.SoftwareProductVersionListView.as_view(), name='softwareproductversion_list'),
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name='softwareproductversion_add'), path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name='softwareproductversion_add'),
path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), name="softwareproductversion_import"), path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(),
name="softwareproductversion_import"),
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"), path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(), name="softwareproductversion_bulk_delete"), path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(),
name="softwareproductversion_bulk_delete"),
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"), path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(), name="softwareproductversion_delete"), path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(),
name="softwareproductversion_delete"),
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"), path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
path( path(
"versions/<int:pk>/changelog/", "versions/<int:pk>/changelog/",
@@ -37,14 +43,22 @@ urlpatterns = [
), ),
# Software Product Versions # Software Product Versions
path("installations/", views.SoftwareProductInstallationListView.as_view(), name='softwareproductinstallation_list'), path("installations/", views.SoftwareProductInstallationListView.as_view(),
path("installations/add/", views.SoftwareProductInstallationEditView.as_view(), name='softwareproductinstallation_add'), name='softwareproductinstallation_list'),
path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(), name="softwareproductinstallation_import"), path("installations/add/", views.SoftwareProductInstallationEditView.as_view(),
path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(), name="softwareproductinstallation_bulk_edit"), name='softwareproductinstallation_add'),
path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(), name="softwareproductinstallation_bulk_delete"), path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(),
path("installations/<int:pk>/", views.SoftwareProductInstallationView.as_view(), name="softwareproductinstallation"), name="softwareproductinstallation_import"),
path("installations/<int:pk>/delete/", views.SoftwareProductInstallationDeleteView.as_view(), name="softwareproductinstallation_delete"), path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(),
path("installations/<int:pk>/edit/", views.SoftwareProductInstallationEditView.as_view(), name="softwareproductinstallation_edit"), name="softwareproductinstallation_bulk_edit"),
path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(),
name="softwareproductinstallation_bulk_delete"),
path("installations/<int:pk>/", views.SoftwareProductInstallationView.as_view(),
name="softwareproductinstallation"),
path("installations/<int:pk>/delete/", views.SoftwareProductInstallationDeleteView.as_view(),
name="softwareproductinstallation_delete"),
path("installations/<int:pk>/edit/", views.SoftwareProductInstallationEditView.as_view(),
name="softwareproductinstallation_edit"),
path( path(
"installations/<int:pk>/changelog/", "installations/<int:pk>/changelog/",
ObjectChangeLogView.as_view(), ObjectChangeLogView.as_view(),
+39 -50
View File
@@ -1,25 +1,19 @@
from netbox.views import generic from netbox.views import generic
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter from netbox_slm import filtersets
from netbox_slm.forms import ( from netbox_slm import forms
SoftwareProductForm, SoftwareProductFilterForm, SoftwareProductCSVForm, SoftwareProductBulkEditForm, from netbox_slm import tables
SoftwareProductVersionForm, SoftwareProductVersionFilterForm, SoftwareProductVersionCSVForm,
SoftwareProductVersionBulkEditForm, SoftwareProductInstallationForm, SoftwareProductInstallationFilterForm,
SoftwareProductInstallationCSVForm, SoftwareProductInstallationBulkEditForm
)
from netbox_slm.models import ( from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
) )
from netbox_slm.tables import SoftwareProductTable, SoftwareProductVersionTable, SoftwareProductInstallationTable
class SoftwareProductListView(generic.ObjectListView): class SoftwareProductListView(generic.ObjectListView):
"""View for listing all existing SoftwareProducts.""" """View for listing all existing SoftwareProducts."""
queryset = SoftwareProduct.objects.all() queryset = SoftwareProduct.objects.all()
filterset = SoftwareProductFilter filterset = filtersets.SoftwareProductFilterSet
filterset_form = SoftwareProductFilterForm filterset_form = forms.SoftwareProductFilterForm
table = SoftwareProductTable table = tables.SoftwareProductTable
template_name = "netbox_slm/object_list.html"
class SoftwareProductView(generic.ObjectView): class SoftwareProductView(generic.ObjectView):
@@ -28,7 +22,7 @@ class SoftwareProductView(generic.ObjectView):
queryset = SoftwareProduct.objects.all() queryset = SoftwareProduct.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
versions = instance.softwareproduct_versions.all() versions = instance.softwareproductversion_set.all()
return {"versions": versions} return {"versions": versions}
@@ -36,7 +30,7 @@ class SoftwareProductEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProduct instance.""" """View for editing and creating a SoftwareProduct instance."""
queryset = SoftwareProduct.objects.all() queryset = SoftwareProduct.objects.all()
model_form = SoftwareProductForm form = forms.SoftwareProductForm
class SoftwareProductDeleteView(generic.ObjectDeleteView): class SoftwareProductDeleteView(generic.ObjectDeleteView):
@@ -47,30 +41,30 @@ class SoftwareProductDeleteView(generic.ObjectDeleteView):
class SoftwareProductBulkImportView(generic.BulkImportView): class SoftwareProductBulkImportView(generic.BulkImportView):
queryset = SoftwareProduct.objects.all() queryset = SoftwareProduct.objects.all()
model_form = SoftwareProductCSVForm model_form = forms.SoftwareProductCSVForm
table = SoftwareProductTable table = tables.SoftwareProductTable
class SoftwareProductBulkEditView(generic.BulkEditView): class SoftwareProductBulkEditView(generic.BulkEditView):
queryset = SoftwareProduct.objects.all() queryset = SoftwareProduct.objects.all()
filterset = SoftwareProductFilter filterset = filtersets.SoftwareProductFilterSet
table = SoftwareProductTable filterset_form = forms.SoftwareProductFilterForm
form = SoftwareProductBulkEditForm table = tables.SoftwareProductTable
form = forms.SoftwareProductBulkEditForm
class SoftwareProductBulkDeleteView(generic.BulkDeleteView): class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProduct.objects.all() queryset = SoftwareProduct.objects.all()
table = SoftwareProductTable table = tables.SoftwareProductTable
class SoftwareProductVersionListView(generic.ObjectListView): class SoftwareProductVersionListView(generic.ObjectListView):
"""View for listing all existing SoftwareProductVersions.""" """View for listing all existing SoftwareProductVersions."""
queryset = SoftwareProductVersion.objects.all() queryset = SoftwareProductVersion.objects.all()
filterset = SoftwareProductVersionFilter filterset = filtersets.SoftwareProductVersionFilterSet
filterset_form = SoftwareProductVersionFilterForm filterset_form = forms.SoftwareProductVersionFilterForm
table = SoftwareProductVersionTable table = tables.SoftwareProductVersionTable
template_name = "netbox_slm/object_list.html"
class SoftwareProductVersionView(generic.ObjectView): class SoftwareProductVersionView(generic.ObjectView):
@@ -78,16 +72,16 @@ class SoftwareProductVersionView(generic.ObjectView):
queryset = SoftwareProductVersion.objects.all() queryset = SoftwareProductVersion.objects.all()
# def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
# records = instance.record_set.all() installation_count = instance.get_installation_count()
# return {"records": records} return {"installations": installation_count}
class SoftwareProductVersionEditView(generic.ObjectEditView): class SoftwareProductVersionEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProductVersion instance.""" """View for editing and creating a SoftwareProductVersion instance."""
queryset = SoftwareProductVersion.objects.all() queryset = SoftwareProductVersion.objects.all()
model_form = SoftwareProductVersionForm form = forms.SoftwareProductVersionForm
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView): class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
@@ -98,47 +92,42 @@ class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
class SoftwareProductVersionBulkImportView(generic.BulkImportView): class SoftwareProductVersionBulkImportView(generic.BulkImportView):
queryset = SoftwareProductVersion.objects.all() queryset = SoftwareProductVersion.objects.all()
model_form = SoftwareProductVersionCSVForm model_form = forms.SoftwareProductVersionCSVForm
table = SoftwareProductVersionTable table = tables.SoftwareProductVersionTable
class SoftwareProductVersionBulkEditView(generic.BulkEditView): class SoftwareProductVersionBulkEditView(generic.BulkEditView):
queryset = SoftwareProductVersion.objects.all() queryset = SoftwareProductVersion.objects.all()
filterset = SoftwareProductVersionFilter filterset = filtersets.SoftwareProductVersionFilterSet
table = SoftwareProductVersionTable table = tables.SoftwareProductVersionTable
form = SoftwareProductVersionBulkEditForm form = forms.SoftwareProductVersionBulkEditForm
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView): class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductVersion.objects.all() queryset = SoftwareProductVersion.objects.all()
table = SoftwareProductVersionTable table = tables.SoftwareProductVersionTable
class SoftwareProductInstallationListView(generic.ObjectListView): class SoftwareProductInstallationListView(generic.ObjectListView):
"""View for listing all existing SoftwareProductInstallations.""" """View for listing all existing SoftwareProductInstallations."""
queryset = SoftwareProductInstallation.objects.all() queryset = SoftwareProductInstallation.objects.all()
filterset = SoftwareProductInstallationFilter filterset = filtersets.SoftwareProductInstallationFilterSet
filterset_form = SoftwareProductInstallationFilterForm filterset_form = forms.SoftwareProductInstallationFilterForm
table = SoftwareProductInstallationTable table = tables.SoftwareProductInstallationTable
template_name = "netbox_slm/object_list.html"
class SoftwareProductInstallationView(generic.ObjectView): class SoftwareProductInstallationView(generic.ObjectView):
"""Display SoftwareProductInstallation details""" """Display SoftwareProductInstallation details"""
queryset = SoftwareProductVersion.objects.all() queryset = SoftwareProductInstallation.objects.all()
# def get_extra_context(self, request, instance):
# records = instance.record_set.all()
# return {"records": records}
class SoftwareProductInstallationEditView(generic.ObjectEditView): class SoftwareProductInstallationEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProductInstallation instance.""" """View for editing and creating a SoftwareProductInstallation instance."""
queryset = SoftwareProductInstallation.objects.all() queryset = SoftwareProductInstallation.objects.all()
model_form = SoftwareProductInstallationForm form = forms.SoftwareProductInstallationForm
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView): class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
@@ -149,17 +138,17 @@ class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
class SoftwareProductInstallationBulkImportView(generic.BulkImportView): class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
queryset = SoftwareProductInstallation.objects.all() queryset = SoftwareProductInstallation.objects.all()
model_form = SoftwareProductInstallationCSVForm model_form = forms.SoftwareProductInstallationCSVForm
table = SoftwareProductInstallationTable table = tables.SoftwareProductInstallationTable
class SoftwareProductInstallationBulkEditView(generic.BulkEditView): class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
queryset = SoftwareProductInstallation.objects.all() queryset = SoftwareProductInstallation.objects.all()
filterset = SoftwareProductInstallationFilter filterset = filtersets.SoftwareProductInstallationFilterSet
table = SoftwareProductInstallationTable table = tables.SoftwareProductInstallationTable
form = SoftwareProductInstallationBulkEditForm form = forms.SoftwareProductInstallationBulkEditForm
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView): class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductInstallation.objects.all() queryset = SoftwareProductInstallation.objects.all()
table = SoftwareProductInstallationTable table = tables.SoftwareProductInstallationTable
+5 -5
View File
@@ -1,8 +1,9 @@
[metadata] [metadata]
name = netbox-slm name = netbox-slm
version = 0.91 version = 1.2
description = Software Lifecycle Management Netbox Plugin. description = Software Lifecycle Management Netbox Plugin.
long_description = file: README.rst # long_description = file: README.rst
# long_description_content_type='text/x-rst'
author = Hedde van der Heide author = Hedde van der Heide
author_email = hedde.vanderheide@ictu.nl author_email = hedde.vanderheide@ictu.nl
license = Apache 2.0 license = Apache 2.0
@@ -15,6 +16,7 @@ classifiers =
Programming Language :: Python Programming Language :: Python
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9
Topic :: Internet :: WWW/HTTP Topic :: Internet :: WWW/HTTP
@@ -23,6 +25,4 @@ classifiers =
[options] [options]
include_package_data = true include_package_data = true
packages = find: packages = find:
python_requires = >=3.6 python_requires = >=3.7
install_requires =
Netbox >= 3.0.9
View File
-13
View File
@@ -1,13 +0,0 @@
Copyright 2022 ICTU
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-187
View File
@@ -1,187 +0,0 @@
## Software Lifecycle Management
### _Netbox Plugin for lifecycle management of software components, including versions and installations_
---
### Installation Guide
When using the Docker version of Netbox, first follow the [netbox-docker quickstart](https://github.com/netbox-community/netbox-docker#quickstart) instructions to clone the netbox-docker repo and set up the `docker-compose.override.yml`.
Next, follow these instructions (based on the generic [Netbox plugin instructions](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)) to install the Netbox SLM plugin:
1. Add `netbox_slm` to the `PLUGINS` list in `configuration/configuration.py`.
2. Create a `plugin_requirements.txt` with `netbox-slm==0.9` as contents.
3. Create a `Dockerfile-SLM` with contents:
```Dockerfile
FROM netboxcommunity/netbox:v3.0.9 # The Netbox SLM plugin currently only works with v3.0.9 of Netbox due to backwards incompatible changes in newer version of Netbox
COPY ./plugin_requirements.txt /
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
```
4. Create a `docker-compose.override.yml` with contents:
```yaml
version: '3.4'
services:
netbox:
ports:
- 8000:8080
build:
context: .
dockerfile: Dockerfile-SLM
image: netbox:slm
netbox-worker:
image: netbox:slm
netbox-housekeeping:
image: netbox:slm
```
Now, build the image: `docker compose build --no-cache`
And finally, run Netbox with the SLM plugin: `docker compose up`
### Releasing Guide
To draft a release;
update the setup.py file to reflect the new version, then from the *src* directory run
```
$ python setup.py sdist
$ twine upload dist/*
```
On Github.com create a similar tag and version. These steps could be automated with a github workflow.
n.b. Currently the plugin is configured to use a personal pypi account, this should be changed.
### Developer Guide (local installation)
_Follow the steps below on your local system to run netbox and the netbox_slm plugin in developer mode_
### Setup
The goal below is to run all Netbox components in Docker and run a local Netbox Django copy with auto-reload to develop the plugin pointing to the Dockerized postgres and redis instances, basically ignoring the netbox docker runtime server.
### Gotcha's
_Netbox's exception handler seems to supress initial Exceptions, obfuscating missing imports or other Django related issues by re-raising an opague error, e.g. netbox_slm plugin namespace doesnotexist. It's recommended to often check the runtime server or patch Netbox's global exception handler._
### Steps
```
from your projects directory clone the netbox repository
$ git clone https://github.com/netbox-community/netbox
$ cd netbox
$ git checkout f5356b8 (to pin 3.0.9)
install the virtual environment
$ pipenv shell
$ pipenv install
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
DEBUG = True
SECRET_KEY = 'dummy'
DEVELOPER = True
PLUGINS = [
'netbox_slm',
]
````
The Netbox installation above will be used to run Django management commands like runserver, makemigrations and migrate, which will be explained in the next steps below;
```
from your projects directory clone the netbox-slm repository
$ git clone https://github.com/ICTU/netbox_slm
$ cd netbox_slm
$ ./start-netbox.sh
````
This will start Netbox locally (requires Docker) and forward the redis and postgres ports to the localhost (make sure there's no processes using these ports or change the dockerfiles accordingly)
Note, you can also start and stop netbox by hand:
```
$ cd netbox-docker
$ docker-compose up -d
or stop the stack with
$ docker-compose down
# to start fresh:
$ docker-compose down
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
$ docker-compose up -d --force-recreate
this will require you to re-run the migrate commando's for netbox-slm, see further down below
```
Go back to the netbox configuration.py file and update the postgres and redis connection strings (username, password) to the ones the netbox docker backend is using, for example (using default user and passwords from the netbox docker example):
```
<<collapsed>>
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
DATABASE = {
'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
'HOST': 'localhost', # Database server
'PORT': '', # Database port (leave blank for default)
'CONN_MAX_AGE': 300, # Max database connection age
}
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
# to use two separate database IDs.
REDIS = {
'tasks': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 0,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 1,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
}
}
<<collapsed>>
```
Now you can run commands from the netbox repository like this;
```
$ cd netbox/netbox
$ export PYTHONPATH=../../netbox-slm/src/
$ python3 manage.py migrate netbox_slm
$ python3 manage.py runserver 8001
```
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading version of the netbox UI. Port 8000 is taken by the docker ran variant.
-222
View File
@@ -1,222 +0,0 @@
Software Lifecycle Management
-----------------------------
*Netbox Plugin for lifecycle management of software components, including versions and installations*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--------------
Installation Guide
~~~~~~~~~~~~~~~~~~
When using the Docker version of Netbox, first follow the `netbox-docker
quickstart <https://github.com/netbox-community/netbox-docker#quickstart>`__
instructions to clone the netbox-docker repo and set up the
``docker-compose.override.yml``.
Next, follow these instructions (based on the generic `Netbox plugin
instructions <https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins>`__)
to install the Netbox SLM plugin:
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
``configuration/configuration.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.9`` as
contents.
3. Create a ``Dockerfile-SLM`` with contents:
.. code:: dockerfile
FROM netboxcommunity/netbox:v3.0.9 # The Netbox SLM plugin currently only works with v3.0.9 of Netbox due to backwards incompatible changes in newer version of Netbox
COPY ./plugin_requirements.txt /
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
4. Create a ``docker-compose.override.yml`` with contents:
.. code:: yaml
version: '3.4'
services:
netbox:
ports:
- 8000:8080
build:
context: .
dockerfile: Dockerfile-SLM
image: netbox:slm
netbox-worker:
image: netbox:slm
netbox-housekeeping:
image: netbox:slm
Now, build the image: ``docker compose build --no-cache``
And finally, run Netbox with the SLM plugin: ``docker compose up``
Releasing Guide
~~~~~~~~~~~~~~~
To draft a release;
update the setup.py file to reflect the new version, then from the *src*
directory run
::
$ python setup.py sdist
$ twine upload dist/*
On Github.com create a similar tag and version. These steps could be
automated with a github workflow.
n.b. Currently the plugin is configured to use a personal pypi account,
this should be changed.
Developer Guide (local installation)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*Follow the steps below on your local system to run netbox and the
netbox_slm plugin in developer mode*
Setup
~~~~~
The goal below is to run all Netbox components in Docker and run a local
Netbox Django copy with auto-reload to develop the plugin pointing to
the Dockerized postgres and redis instances, basically ignoring the
netbox docker runtime server.
Gotchas
~~~~~~~~
*Netboxs exception handler seems to supress initial Exceptions,
obfuscating missing imports or other Django related issues by re-raising
an opague error, e.g. netbox_slm plugin namespace doesnotexist. Its
recommended to often check the runtime server or patch Netboxs global
exception handler.*
Steps
~~~~~
::
from your projects directory clone the netbox repository
$ git clone https://github.com/netbox-community/netbox
$ cd netbox
$ git checkout f5356b8 (to pin 3.0.9)
install the virtual environment
$ pipenv shell
$ pipenv install
create and edit netbox/configuration.py (based on the template file) add these lines at the end of the file;
DEBUG = True
SECRET_KEY = 'dummy'
DEVELOPER = True
PLUGINS = [
'netbox_slm',
]
The Netbox installation above will be used to run Django management
commands like runserver, makemigrations and migrate, which will be
explained in the next steps below;
::
from your projects directory clone the netbox-slm repository
$ git clone https://github.com/ICTU/netbox_slm
$ cd netbox_slm
$ ./start-netbox.sh
This will start Netbox locally (requires Docker) and forward the redis
and postgres ports to the localhost (make sure theres no processes
using these ports or change the dockerfiles accordingly)
Note, you can also start and stop netbox by hand:
::
$ cd netbox-docker
$ docker-compose up -d
or stop the stack with
$ docker-compose down
# to start fresh:
$ docker-compose down
$ docker volume rm netbox-docker_netbox-postgres-data # et cetera
$ docker-compose up -d --force-recreate
this will require you to re-run the migrate commando's for netbox-slm, see further down below
Go back to the netbox configuration.py file and update the postgres and
redis connection strings (username, password) to the ones the netbox
docker backend is using, for example (using default user and passwords
from the netbox docker example):
::
<<collapsed>>
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
DATABASE = {
'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username
'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password
'HOST': 'localhost', # Database server
'PORT': '', # Database port (leave blank for default)
'CONN_MAX_AGE': 300, # Max database connection age
}
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
# to use two separate database IDs.
REDIS = {
'tasks': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 0,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': 'H733Kdjndks81',
'DATABASE': 1,
'SSL': False,
# Set this to True to skip TLS certificate verification
# This can expose the connection to attacks, be careful
# 'INSECURE_SKIP_TLS_VERIFY': False,
}
}
<<collapsed>>
Now you can run commands from the netbox repository like this;
::
$ cd netbox/netbox
$ export PYTHONPATH=../../netbox-slm/src/
$ python3 manage.py migrate netbox_slm
$ python3 manage.py runserver 8001
Visit http://127.0.0.1:8001 in the browesr to see the auto reloading
version of the netbox UI. Port 8000 is taken by the docker ran variant.
-14
View File
@@ -1,14 +0,0 @@
from django.contrib import admin
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
class SoftwareProductVersionInline(admin.TabularInline):
model = SoftwareProductVersion
fields = ('name',)
extra = 0
@admin.register(SoftwareProduct)
class SoftwareProductAdmin(admin.ModelAdmin):
list_display = ('name',)
inlines = (SoftwareProductVersionInline,)
-40
View File
@@ -1,40 +0,0 @@
from rest_framework import serializers
from netbox.api.serializers import PrimaryModelSerializer, OrganizationalModelSerializer
# from netbox_slm.api.nested_serializers import (
# NestedRecordSerializer,
# NestedNameServerSerializer,
# )
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
class SoftwareProductSerializer(PrimaryModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproduct-detail"
)
class Meta:
model = SoftwareProduct
fields = [
'id', 'display', 'url', 'name', 'tags', 'custom_field_data', 'created', 'last_updated',
]
def get_display(self, obj):
return obj.name
class SoftwareProductVersionSerializer(PrimaryModelSerializer):
display = serializers.SerializerMethodField()
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_slm-api:softwareproductversion-detail"
)
class Meta:
model = SoftwareProductVersion
fields = [
'id', 'display', 'url', 'name', 'software_product', 'tags', 'custom_field_data', 'created', 'last_updated',
]
def get_display(self, obj):
return obj.name
-44
View File
@@ -1,44 +0,0 @@
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.routers import APIRootView
from extras.api.views import CustomFieldModelViewSet
from netbox_slm.api.serializers import SoftwareProductSerializer, SoftwareProductVersionSerializer
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
class NetboxSLMRootView(APIRootView):
"""
NetboxSLM API root view
"""
def get_view_name(self):
return "NetboxSLM"
class SoftwareProductViewSet(CustomFieldModelViewSet):
queryset = SoftwareProduct.objects.all()
serializer_class = SoftwareProductSerializer
filterset_class = SoftwareProductFilter
# @action(detail=True, methods=["get"])
# def records(self, request, pk=None):
# records = Record.objects.filter(zone=pk)
# serializer = RecordSerializer(records, many=True, context={"request": request})
# return Response(serializer.data)
class SoftwareProductVersionViewSet(CustomFieldModelViewSet):
queryset = SoftwareProductVersion.objects.all()
serializer_class = SoftwareProductVersionSerializer
filterset_class = SoftwareProductVersionFilter
# @action(detail=True, methods=["get"])
# def records(self, request, pk=None):
# records = Record.objects.filter(zone=pk)
# serializer = RecordSerializer(records, many=True, context={"request": request})
# return Response(serializer.data)
# for reference: https://github.com/auroraresearchlab/netbox-dns/blob/main/netbox_dns/api/views.py
-50
View File
@@ -1,50 +0,0 @@
from django.forms import BoundField
from django.urls import reverse
from utilities.forms import DynamicModelMultipleChoiceField
class CustomDynamicModelMultipleChoiceField(DynamicModelMultipleChoiceField):
def get_bound_field(self, form, field_name):
bound_field = BoundField(form, self, field_name)
# Set initial value based on prescribed child fields (if not already set)
if not self.initial and self.initial_params:
filter_kwargs = {}
for kwarg, child_field in self.initial_params.items():
value = form.initial.get(child_field.lstrip("$"))
if value:
filter_kwargs[kwarg] = value
if filter_kwargs:
self.initial = self.queryset.filter(**filter_kwargs).first()
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
# will be populated on-demand via the APISelect widget.
data = bound_field.value()
if data:
field_name = getattr(self, "to_field_name") or "pk"
filter = self.filter(field_name=field_name)
try:
self.queryset = filter.filter(self.queryset, data)
except (TypeError, ValueError):
# Catch any error caused by invalid initial data passed from the user
self.queryset = self.queryset.none()
else:
self.queryset = self.queryset.none()
# Set the data URL on the APISelect widget (if not already set)
widget = bound_field.field.widget
if not widget.attrs.get("data-url"):
app_label = self.queryset.model._meta.app_label
model_name = self.queryset.model._meta.model_name
#
# Custom url for work with plugin api
#
data_url = reverse(
"plugins-api:{}-api:{}-list".format(app_label, model_name)
)
widget.attrs["data-url"] = data_url
return bound_field
-75
View File
@@ -1,75 +0,0 @@
import django_filters
from django.db.models import Q
from django.utils.translation import gettext as _
from extras.filters import TagFilter
from netbox.filtersets import PrimaryModelFilterSet
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.forms import DynamicModelMultipleChoiceField
class BaseFilter(PrimaryModelFilterSet):
q = django_filters.CharFilter(
method="search",
label="Search",
)
class SoftwareProductFilter(BaseFilter):
"""Filter capabilities for SoftwareProduct instances."""
name = django_filters.CharFilter(
lookup_expr="icontains",
)
# tag = TagFilter()
class Meta:
model = SoftwareProduct
fields = ("name",) # "tag")
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
return queryset.filter(qs_filter)
class SoftwareProductVersionFilter(BaseFilter):
name = django_filters.CharFilter(
lookup_expr="icontains", label="Version"
)
# software_product_id = django_filters.ModelMultipleChoiceFilter(
# queryset=SoftwareProduct.objects.all(),
# label='SoftwareProduct (ID)',
# )
# software_product = django_filters.ModelMultipleChoiceFilter(
# field_name='software_product__name',
# queryset=SoftwareProduct.objects.all(),
# to_field_name='name',
# label='SoftwareProduct (name)',
# )
# tag = TagFilter()
class Meta:
model = SoftwareProductVersion
fields = ("name", "software_product") # "tag")
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(name__icontains=value) # | Q(status__icontains=value)
return queryset.filter(qs_filter)
class SoftwareProductInstallationFilter(BaseFilter):
class Meta:
model = SoftwareProductInstallation
fields = tuple() # "tag")
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(software_product__name__icontains=value) | Q(version__name__icontains=value)
return queryset.filter(qs_filter)
-202
View File
@@ -1,202 +0,0 @@
from django import forms
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from extras.forms import (
CustomFieldModelForm,
CustomFieldModelCSVForm,
AddRemoveTagsForm,
CustomFieldModelBulkEditForm,
CustomFieldModelFilterForm,
)
from dcim.models import Manufacturer, Device
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.forms import (
BootstrapMixin, DynamicModelChoiceField, APISelect, DynamicModelMultipleChoiceField
)
from virtualization.models import VirtualMachine
class SoftwareProductForm(BootstrapMixin, CustomFieldModelForm):
"""Form for creating a new SoftwareProduct object."""
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
# initial_params={
# 'device_types': 'device_type'
# }
)
# tags = DynamicModelMultipleChoiceField(
# queryset=Tag.objects.all(),
# required=False,
# )
class Meta:
model = SoftwareProduct
fields = ("name", "manufacturer", "description",) # "tags")
class SoftwareProductFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
"""Form for filtering SoftwareProduct instances."""
model = SoftwareProduct
q = forms.CharField(required=False, label="Search")
name = forms.CharField(
required=False,
label="Name",
)
# tag = TagFilterField(SoftwareProduct)
class SoftwareProductCSVForm(CustomFieldModelCSVForm):
class Meta:
model = SoftwareProduct
fields = ("name",)
class SoftwareProductBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
nullable_fields = []
class SoftwareProductVersionForm(BootstrapMixin, CustomFieldModelForm):
"""Form for creating a new SoftwareProductVersion object."""
name = forms.CharField(label=_("Version"))
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=False,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
)
# tags = DynamicModelMultipleChoiceField(
# queryset=Tag.objects.all(),
# required=False,
# )
class Meta:
model = SoftwareProductVersion
fields = ("name", "software_product",) # "tags")
# def clean(self):
# import pdb;pdb.set_trace()
# return super(SoftwareProductVersionForm, self).clean()
class SoftwareProductVersionFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
"""Form for filtering SoftwareProductVersion instances."""
model = SoftwareProductVersion
q = forms.CharField(required=False, label="Search")
name = forms.CharField(
required=False,
label="Name",
)
# tag = TagFilterField(SoftwareProduct)
class SoftwareProductVersionCSVForm(CustomFieldModelCSVForm):
class Meta:
model = SoftwareProductVersion
fields = ("name",)
class SoftwareProductVersionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
nullable_fields = []
class SoftwareProductInstallationForm(BootstrapMixin, CustomFieldModelForm):
"""Form for creating a new SoftwareProductInstallation object."""
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
# initial_params={
# 'device_types': 'device_type'
# }
)
virtualmachine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
# initial_params={
# 'device_types': 'device_type'
# }
)
software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
),
)
version = DynamicModelChoiceField(
queryset=SoftwareProductVersion.objects.all(),
required=True,
widget=APISelect(
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
),
query_params={
'software_product': '$software_product',
}
)
# todo need version and device ?
# tags = DynamicModelMultipleChoiceField(
# queryset=Tag.objects.all(),
# required=False,
# )
class Meta:
model = SoftwareProductInstallation
fields = ("device", "virtualmachine", "software_product", "version",) # "tags")
# def clean(self):
# import pdb;pdb.set_trace()
# return super(SoftwareProductInstallationForm, self).clean()
class SoftwareProductInstallationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
"""Form for filtering SoftwareProductInstallation instances."""
model = SoftwareProductInstallation
q = forms.CharField(required=False, label="Search")
# tag = TagFilterField(SoftwareProduct)
class SoftwareProductInstallationCSVForm(CustomFieldModelCSVForm):
class Meta:
model = SoftwareProductInstallation
fields = tuple()
class SoftwareProductInstallationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SoftwareProductInstallation.objects.all(),
widget=forms.MultipleHiddenInput(),
)
class Meta:
nullable_fields = []
View File
View File
View File
@@ -1,93 +0,0 @@
{% extends 'base/layout.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load view_helpers %}
{% block header %}
{# Breadcrumbs #}
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
<ol class="breadcrumb">
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url object|plugin_viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
{% endblock %}
</ol>
</nav>
{{ block.super }}
{% endblock %}
{% block title %}{{ object }}{% endblock %}
{% block subtitle %}
<div class="object-subtitle">
<span>Created {{ object.created|annotated_date }}</span>
<span class="separator">&middot;</span>
<span>Updated <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> ago</span>
</div>
{% endblock %}
{% block controls %}
{# Clone/Edit/Delete Buttons #}
<div class="controls">
<div class="control-group">
{% plugin_buttons object %}
{# Extra buttons #}
{% block extra_controls %}{% endblock %}
{% if object.clone_fields and request.user|can_add:object %}
{% plugin_clone_button object %}
{% endif %}
{% if request.user|can_change:object %}
{% plugin_edit_button object %}
{% endif %}
{% if request.user|can_delete:object %}
{% plugin_delete_button object %}
{% endif %}
</div>
<div class="control-group">
{% custom_links object %}
</div>
</div>
{% endblock controls %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
{% block tab_items %}
<li class="nav-item" role="presentation">
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
</li>
{% endblock tab_items %}
{% if perms.extras.view_journalentry %}
{% with journal_viewname=object|plugin_viewname:'journal' %}
{% url journal_viewname pk=object.pk as journal_url %}
{% if journal_url %}
<li role="presentation" class="nav-item">
<a href="{{ journal_url }}" class="nav-link{% if active_tab == 'journal'%} active{% endif %}">
Journal {% badge object.journal_entries.count %}
</a>
</li>
{% endif %}
{% endwith %}
{% endif %}
{% if perms.extras.view_objectchange %}
{% with changelog_viewname=object|plugin_viewname:'changelog' %}
{% url changelog_viewname pk=object.pk as changelog_url %}
{% if changelog_url %}
<li role="presentation" class="nav-item">
<a href="{{ changelog_url }}" class="nav-link{% if active_tab == 'changelog'%} active{% endif %}">Change Log</a>
</li>
{% endif %}
{% endwith %}
{% endif %}
</ul>
{% endblock tabs %}
{% block content-wrapper %}
<div class="tab-content">
{% block content %}{% endblock %}
</div>
{% endblock %}
@@ -1,141 +0,0 @@
{% extends 'base/layout.html' %}
{% load buttons %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load static %}
{% load view_helpers %}
{% block controls %}
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
{% if permissions.add and 'add' in action_buttons %}
{% add_button content_type.model_class|plugin_viewname:"add" %}
{% endif %}
{% if permissions.add and 'import' in action_buttons %}
{% import_button content_type.model_class|plugin_viewname:"import" %}
{% endif %}
{% if 'export' in action_buttons %}
{% export_button content_type %}
{% endif %}
</div>
</div>
{% endblock controls %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
{% block tab_items %}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
{% badge table.page.paginator.count %}
</button>
</li>
{% if filter_form %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
Filters
{% if filter_form %}{% badge filter_form.changed_data|length bg_class="primary" %}{% endif %}
</button>
</li>
{% endif %}
{% endblock tab_items %}
</ul>
{% endblock tabs %}
{% block content-wrapper %}
<div class="tab-content">
{# Object list #}
<div class="tab-pane show active" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
{# Applied filters #}
{% if filter_form %}
{% applied_filters filter_form request.GET %}
{% endif %}
{# "Select all" form #}
{% if table.paginator.num_pages > 1 %}
{% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
<div id="select-all-box" class="d-none card noprint">
<form method="post" class="form col-md-12">
{% csrf_token %}
<div class="card-body">
<div class="float-end">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled>
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled>
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
</button>
{% endif %}
</div>
<div class="form-check">
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
<label for="select-all" class="form-check-label">
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
</label>
</div>
</div>
</form>
</div>
{% endwith %}
{% endif %}
{# Object table controls #}
{% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %}
<form method="post" class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
{# Object table #}
<div class="card">
<div class="card-body">
<div class="table-responsive">
{% render_table table 'inc/table.html' %}
</div>
</div>
</div>
{# Form buttons #}
{% if permissions.change or permissions.delete %}
{% with bulk_edit_url=content_type.model_class|plugin_validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|plugin_validated_viewname:"bulk_delete" %}
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% block bulk_buttons %}{% endblock %}
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
</button>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}
</form>
{# Paginator #}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
{# Filter form #}
{% if filter_form %}
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
{% include 'inc/filter_list.html' %}
</div>
{% endif %}
</div>
{# Table config form #}
{% table_config_form table table_name="ObjectTable" %}
{% endblock content-wrapper %}
@@ -1,76 +0,0 @@
{% extends 'netbox_slm/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load view_helpers %}
{% load perms %}
{% block extra_controls %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Software Product
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
{% for version in object.softwareproduct_versions.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
</a>
{% endfor %}
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Versions</h5>
<div class="card-body">
<table class="table table-hover attr-table table-striped">
<thead>
<tr>
<th scope="col">NAME</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for version in versions %}
<tr>
<td>{{ version.name|truncatechars:32 }}</td>
<td class="noprint text-end text-nowrap">
{% if request.user|can_change:object %}
{% plugin_edit_button version %}
{% endif %}
{% if request.user|can_delete:object %}
{% plugin_delete_button version %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -1,82 +0,0 @@
{% extends 'netbox_slm/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load view_helpers %}
{% load perms %}
{% block extra_controls %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Software Product
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Installation</th>
<td>{{ object }}</td>
</tr>
<tr>
<th scope="row">bla</th>
<td>
<!-- {% for version in object.softwareproduct_versions.all %}-->
<!-- <a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">-->
<!-- <span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>-->
<!-- </a>-->
<!-- {% endfor %}-->
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<!--<div class="row">-->
<!-- <div class="col col-md-12">-->
<!-- <div class="card">-->
<!-- <h5 class="card-header">Records</h5>-->
<!-- <div class="card-body">-->
<!-- <table class="table table-hover attr-table table-striped">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th scope="col">TYPE</th>-->
<!-- <th scope="col">NAME</th>-->
<!-- <th scope="col">VALUE</th>-->
<!-- <th scope="col">TTL</th>-->
<!-- <th scope="col">Actions</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- {% for record in records %}-->
<!-- <tr>-->
<!-- <td>{{ record.type }}</td>-->
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
<!-- <td>{{ record.ttl }}</td>-->
<!-- <td class="noprint text-end text-nowrap">-->
<!-- {% if request.user|can_change:object %}-->
<!-- {% plugin_edit_button record %}-->
<!-- {% endif %}-->
<!-- {% if request.user|can_delete:object %}-->
<!-- {% plugin_delete_button record %}-->
<!-- {% endif %}-->
<!-- </td>-->
<!-- </tr>-->
<!-- {% endfor %}-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
{% endblock %}
@@ -1,82 +0,0 @@
{% extends 'netbox_slm/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load view_helpers %}
{% load perms %}
{% block extra_controls %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Software Product
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Installations</th>
<td>
<!-- {% for version in object.softwareproduct_versions.all %}-->
<!-- <a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">-->
<!-- <span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>-->
<!-- </a>-->
<!-- {% endfor %}-->
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<!--<div class="row">-->
<!-- <div class="col col-md-12">-->
<!-- <div class="card">-->
<!-- <h5 class="card-header">Records</h5>-->
<!-- <div class="card-body">-->
<!-- <table class="table table-hover attr-table table-striped">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th scope="col">TYPE</th>-->
<!-- <th scope="col">NAME</th>-->
<!-- <th scope="col">VALUE</th>-->
<!-- <th scope="col">TTL</th>-->
<!-- <th scope="col">Actions</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- {% for record in records %}-->
<!-- <tr>-->
<!-- <td>{{ record.type }}</td>-->
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
<!-- <td>{{ record.ttl }}</td>-->
<!-- <td class="noprint text-end text-nowrap">-->
<!-- {% if request.user|can_change:object %}-->
<!-- {% plugin_edit_button record %}-->
<!-- {% endif %}-->
<!-- {% if request.user|can_delete:object %}-->
<!-- {% plugin_delete_button record %}-->
<!-- {% endif %}-->
<!-- </td>-->
<!-- </tr>-->
<!-- {% endfor %}-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
{% endblock %}
@@ -1,91 +0,0 @@
{% extends 'netbox_slm/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load view_helpers %}
{% load perms %}
{% block extra_controls %}
{% comment %}
<a href="{% url 'plugins:netbox_slm:record_add' %}?zone={{ object.id }}" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-plus-thick"></i> Add Record
</a>
{% endcomment %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Zone
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
<ul>
<!-- {% for nameserver in object.nameservers.all %}-->
<!-- <li>-->
<!-- <a href="{% url 'plugins:netbox_slm:nameserver' pk=nameserver.pk %}">-->
<!-- {{ nameserver }}-->
<!-- </a>-->
<!-- </li>-->
<!-- {% endfor %}-->
</ul>
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<!--<div class="row">-->
<!-- <div class="col col-md-12">-->
<!-- <div class="card">-->
<!-- <h5 class="card-header">Records</h5>-->
<!-- <div class="card-body">-->
<!-- <table class="table table-hover attr-table table-striped">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th scope="col">TYPE</th>-->
<!-- <th scope="col">NAME</th>-->
<!-- <th scope="col">VALUE</th>-->
<!-- <th scope="col">TTL</th>-->
<!-- <th scope="col">Actions</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- {% for record in records %}-->
<!-- <tr>-->
<!-- <td>{{ record.type }}</td>-->
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
<!-- <td>{{ record.ttl }}</td>-->
<!-- <td class="noprint text-end text-nowrap">-->
<!-- {% if request.user|can_change:object %}-->
<!-- {% plugin_edit_button record %}-->
<!-- {% endif %}-->
<!-- {% if request.user|can_delete:object %}-->
<!-- {% plugin_delete_button record %}-->
<!-- {% endif %}-->
<!-- </td>-->
<!-- </tr>-->
<!-- {% endfor %}-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
{% endblock %}
@@ -1,94 +0,0 @@
from django import template
from django.urls import NoReverseMatch, reverse
from django import template
from utilities.templatetags.buttons import _get_viewname
from utilities.utils import prepare_cloned_fields
register = template.Library()
@register.filter()
def plugin_validated_viewname(model, action):
"""Return the view name for :
* the given model and action if valid
* or the given model and action if valid inside a plugin
* or None if invalid.
"""
core_viewname = f"{model._meta.app_label}:{model._meta.model_name}_{action}"
plugin_viewname = (
f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}"
)
try:
# Validate and return the view name. We don't return the actual URL yet because many of the templates
# are written to pass a name to {% url %}.
reverse(core_viewname)
return core_viewname
except NoReverseMatch:
try:
reverse(plugin_viewname)
return plugin_viewname
except NoReverseMatch:
return None
@register.filter()
def plugin_viewname(model, action):
"""
Return the view name for the given model and action. Does not perform any validation.
"""
return f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}"
#
# Instance buttons
#
@register.inclusion_tag("buttons/clone.html")
def plugin_clone_button(instance):
viewname = _get_viewname(instance, "add")
url = reverse(f"plugins:{viewname}")
# Populate cloned field values
param_string = prepare_cloned_fields(instance)
if param_string:
url = f"{url}?{param_string}"
return {
"url": url,
}
@register.inclusion_tag("buttons/edit.html")
def plugin_edit_button(instance, **kwargs):
viewname = _get_viewname(instance, "edit")
url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs})
return {
"url": url,
}
@register.inclusion_tag("buttons/delete.html")
def plugin_delete_button(instance, **kwargs):
viewname = _get_viewname(instance, "delete")
url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs})
return {
"url": url,
}
#
# List buttons
#
@register.inclusion_tag("buttons/add.html")
def plugin_add_button(url):
url = reverse(f"plugins:{url}")
return {
"add_url": url,
}