15 Commits
33 changed files with 195 additions and 524 deletions
+2 -2
View File
@@ -4,10 +4,10 @@ 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
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.
limitations under the License.
View File
+59 -49
View File
@@ -1,42 +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
~~~~~~~~~~~~~~~~~~
WARNING: This plugin is only tested with Netbox 3.2 at this time.
## Known Issues
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``.
- WARNING: This plugin is only tested with Netbox 3.2 at this time.
- CSV/Bulk imports for SoftwareProduct, Version and Installation are currently broken (WIP)
Next, follow these instructions (based on the `Netbox docker variant
instructions <https://github.com/netbox-community/netbox-docker/wiki/Configuration#custom-configuration-files>`__)
## 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:
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
``configuration/extra.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.98`` as
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==1.1`` as
contents.
3. Create a ``Dockerfile-SLM`` with contents:
.. code:: dockerfile
```
FROM netboxcommunity/netbox:v3.2.X
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:
@@ -50,24 +55,24 @@ to install the Netbox SLM plugin:
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
~~~~~~~~~~~~~~~
## Releasing Guide
To draft a release;
update the setup.py file to reflect the new version, then from the *src*
directory run
::
```
# make sure to update the version in netbox_slm/__init__.py
$ 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.
@@ -75,77 +80,82 @@ 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)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## Developer Guide (local installation)
*Follow the steps below on your local system to run netbox and the
netbox_slm plugin in developer mode*
Setup
~~~~~
### 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.
Steps
~~~~~
::
### Steps
from your projects directory clone the netbox repository
```
$ git clone https://github.com/netbox-community/netbox
$ cd netbox
```
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
@@ -153,8 +163,7 @@ 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:
@@ -201,15 +210,16 @@ from the netbox docker example):
}
<<collapsed>>
```
Now you can run commands from the netbox repository like this;
::
```
$ 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 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.
+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'
verbose_name = 'Software Lifecycle Management'
description = 'Software Lifecycle Management'
version = '0.98'
version = '1.1'
author = 'Hedde van der Heide'
author_email = 'hedde.vanderheide@ictu.nl'
base_url = 'slm'
@@ -14,4 +14,4 @@ class SLMConfig(PluginConfig):
'version_info': False
}
config = SLMConfig
config = SLMConfig
@@ -1,14 +1,11 @@
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from netbox.api.viewsets import NetBoxModelViewSet
from rest_framework.routers import APIRootView
from netbox.api.viewsets import NetBoxModelViewSet
from netbox_slm.api.serializers import (
SoftwareProductSerializer, SoftwareProductVersionSerializer, SoftwareProductInstallationSerializer,
)
from netbox_slm.filters import (
SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter,
from netbox_slm.filtersets import (
SoftwareProductFilterSet, SoftwareProductVersionFilterSet, SoftwareProductInstallationFilterSet,
)
from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
@@ -27,16 +24,16 @@ class NetboxSLMRootView(APIRootView):
class SoftwareProductViewSet(NetBoxModelViewSet):
queryset = SoftwareProduct.objects.all()
serializer_class = SoftwareProductSerializer
filterset_class = SoftwareProductFilter
filterset_class = SoftwareProductFilterSet
class SoftwareProductVersionViewSet(NetBoxModelViewSet):
queryset = SoftwareProductVersion.objects.all()
serializer_class = SoftwareProductVersionSerializer
filterset_class = SoftwareProductVersionFilter
filterset_class = SoftwareProductVersionFilterSet
class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
queryset = SoftwareProductInstallation.objects.all()
serializer_class = SoftwareProductInstallationSerializer
filterset_class = SoftwareProductInstallationFilter
filterset_class = SoftwareProductInstallationFilterSet
+51
View File
@@ -0,0 +1,51 @@
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 = tuple()
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)
+22 -40
View File
@@ -1,16 +1,17 @@
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 dcim.models import Manufacturer, Device
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.forms import (
BootstrapMixin, DynamicModelChoiceField, APISelect, DynamicModelMultipleChoiceField
DynamicModelChoiceField, APISelect, TagFilterField, ChoiceField
)
from virtualization.models import VirtualMachine
@@ -21,9 +22,6 @@ class SoftwareProductForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
# initial_params={
# 'device_types': 'device_type'
# }
)
class Meta:
@@ -32,24 +30,18 @@ class SoftwareProductForm(NetBoxModelForm):
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
"""Form for filtering SoftwareProduct instances."""
model = SoftwareProduct
q = forms.CharField(required=False, label="Search")
name = forms.CharField(
required=False,
label="Name",
fieldsets = (
(None, ('q', 'tag')),
)
# tag = TagFilterField(SoftwareProduct)
tag = TagFilterField(model)
class SoftwareProductCSVForm(NetBoxModelCSVForm):
class Meta:
model = SoftwareProduct
fields = ("name",)
fields = ("name", "manufacturer",)
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
@@ -78,24 +70,14 @@ class SoftwareProductVersionForm(NetBoxModelForm):
model = SoftwareProductVersion
fields = ("name", "software_product", "tags")
# def clean(self):
# import pdb;pdb.set_trace()
# return super(SoftwareProductVersionForm, self).clean()
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
"""Form for filtering SoftwareProductVersion instances."""
model = SoftwareProductVersion
q = forms.CharField(required=False, label="Search")
name = forms.CharField(
required=False,
label="Name",
fieldsets = (
(None, ('q', 'tag')),
)
# tag = TagFilterField(SoftwareProduct)
tag = TagFilterField(model)
class SoftwareProductVersionCSVForm(NetBoxModelCSVForm):
@@ -121,16 +103,10 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
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(),
@@ -154,6 +130,14 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
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."))
@@ -161,13 +145,12 @@ class SoftwareProductInstallationForm(NetBoxModelForm):
class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
"""Form for filtering SoftwareProductInstallation instances."""
model = SoftwareProductInstallation
fieldsets = (
(None, ('q', 'tag',)),
)
q = forms.CharField(required=False, label="Search")
# tag = TagFilterField(SoftwareProduct)
tag = TagFilterField(model)
class SoftwareProductInstallationCSVForm(NetBoxModelCSVForm):
@@ -189,4 +172,3 @@ class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = (
(None, ('software_product', 'version',)),
)
# nullable_fields = ('',)
@@ -1,16 +1,12 @@
from django.db import models
from django.urls import reverse, reverse_lazy
from django.utils import safestring
# from extras.utils import extras_features # todo this changed in netbox 3.1.X and higher, currently not used but checkout why
from netbox.models import NetBoxModel
from utilities.querysets import RestrictedQuerySet
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class SoftwareProduct(NetBoxModel):
"""
A SoftwareProduct represents ...
"""
name = models.CharField(max_length=128)
description = models.CharField(max_length=255, null=True, blank=True)
@@ -36,7 +32,6 @@ class SoftwareProduct(NetBoxModel):
)) if count else "0"
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class SoftwareProductVersion(NetBoxModel):
software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct',
@@ -60,7 +55,6 @@ class SoftwareProductVersion(NetBoxModel):
)) if count else "0"
# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class SoftwareProductInstallation(NetBoxModel):
device = models.ForeignKey(
to='dcim.Device',
@@ -71,8 +65,8 @@ class SoftwareProductInstallation(NetBoxModel):
virtualmachine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.PROTECT,
null = True,
blank = True
null=True,
blank=True
)
software_product = models.ForeignKey(
to='netbox_slm.SoftwareProduct',
@@ -1,6 +1,7 @@
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
menu_items = (
PluginMenuItem(
link='plugins:netbox_slm:softwareproduct_list',
View File
@@ -1,5 +1,6 @@
from django.urls import path
from netbox.views.generic import ObjectChangeLogView
from netbox_slm import views
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
+34 -45
View File
@@ -1,25 +1,19 @@
from netbox.views import generic
from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter
from netbox_slm.forms import (
SoftwareProductForm, SoftwareProductFilterForm, SoftwareProductCSVForm, SoftwareProductBulkEditForm,
SoftwareProductVersionForm, SoftwareProductVersionFilterForm, SoftwareProductVersionCSVForm,
SoftwareProductVersionBulkEditForm, SoftwareProductInstallationForm, SoftwareProductInstallationFilterForm,
SoftwareProductInstallationCSVForm, SoftwareProductInstallationBulkEditForm
)
from netbox_slm import filtersets
from netbox_slm import forms
from netbox_slm import tables
from netbox_slm.models import (
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
)
from netbox_slm.tables import SoftwareProductTable, SoftwareProductVersionTable, SoftwareProductInstallationTable
class SoftwareProductListView(generic.ObjectListView):
"""View for listing all existing SoftwareProducts."""
queryset = SoftwareProduct.objects.all()
filterset = SoftwareProductFilter
filterset_form = SoftwareProductFilterForm
table = SoftwareProductTable
# template_name = "netbox_slm/object_list.html"
filterset = filtersets.SoftwareProductFilterSet
filterset_form = forms.SoftwareProductFilterForm
table = tables.SoftwareProductTable
class SoftwareProductView(generic.ObjectView):
@@ -36,7 +30,7 @@ class SoftwareProductEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProduct instance."""
queryset = SoftwareProduct.objects.all()
form = SoftwareProductForm
form = forms.SoftwareProductForm
class SoftwareProductDeleteView(generic.ObjectDeleteView):
@@ -47,30 +41,30 @@ class SoftwareProductDeleteView(generic.ObjectDeleteView):
class SoftwareProductBulkImportView(generic.BulkImportView):
queryset = SoftwareProduct.objects.all()
model_form = SoftwareProductCSVForm
table = SoftwareProductTable
model_form = forms.SoftwareProductCSVForm
table = tables.SoftwareProductTable
class SoftwareProductBulkEditView(generic.BulkEditView):
queryset = SoftwareProduct.objects.all()
filterset = SoftwareProductFilter
table = SoftwareProductTable
form = SoftwareProductBulkEditForm
filterset = filtersets.SoftwareProductFilterSet
filterset_form = forms.SoftwareProductFilterForm
table = tables.SoftwareProductTable
form = forms.SoftwareProductBulkEditForm
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProduct.objects.all()
table = SoftwareProductTable
table = tables.SoftwareProductTable
class SoftwareProductVersionListView(generic.ObjectListView):
"""View for listing all existing SoftwareProductVersions."""
queryset = SoftwareProductVersion.objects.all()
filterset = SoftwareProductVersionFilter
filterset_form = SoftwareProductVersionFilterForm
table = SoftwareProductVersionTable
# template_name = "netbox_slm/object_list.html"
filterset = filtersets.SoftwareProductVersionFilterSet
filterset_form = forms.SoftwareProductVersionFilterForm
table = tables.SoftwareProductVersionTable
class SoftwareProductVersionView(generic.ObjectView):
@@ -87,7 +81,7 @@ class SoftwareProductVersionEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProductVersion instance."""
queryset = SoftwareProductVersion.objects.all()
form = SoftwareProductVersionForm
form = forms.SoftwareProductVersionForm
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
@@ -98,30 +92,29 @@ class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
queryset = SoftwareProductVersion.objects.all()
model_form = SoftwareProductVersionCSVForm
table = SoftwareProductVersionTable
model_form = forms.SoftwareProductVersionCSVForm
table = tables.SoftwareProductVersionTable
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
queryset = SoftwareProductVersion.objects.all()
filterset = SoftwareProductVersionFilter
table = SoftwareProductVersionTable
form = SoftwareProductVersionBulkEditForm
filterset = filtersets.SoftwareProductVersionFilterSet
table = tables.SoftwareProductVersionTable
form = forms.SoftwareProductVersionBulkEditForm
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductVersion.objects.all()
table = SoftwareProductVersionTable
table = tables.SoftwareProductVersionTable
class SoftwareProductInstallationListView(generic.ObjectListView):
"""View for listing all existing SoftwareProductInstallations."""
queryset = SoftwareProductInstallation.objects.all()
filterset = SoftwareProductInstallationFilter
filterset_form = SoftwareProductInstallationFilterForm
table = SoftwareProductInstallationTable
# template_name = "netbox_slm/object_list.html"
filterset = filtersets.SoftwareProductInstallationFilterSet
filterset_form = forms.SoftwareProductInstallationFilterForm
table = tables.SoftwareProductInstallationTable
class SoftwareProductInstallationView(generic.ObjectView):
@@ -129,16 +122,12 @@ class SoftwareProductInstallationView(generic.ObjectView):
queryset = SoftwareProductInstallation.objects.all()
# def get_extra_context(self, request, instance):
# records = instance.record_set.all()
# return {"records": records}
class SoftwareProductInstallationEditView(generic.ObjectEditView):
"""View for editing and creating a SoftwareProductInstallation instance."""
queryset = SoftwareProductInstallation.objects.all()
form = SoftwareProductInstallationForm
form = forms.SoftwareProductInstallationForm
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
@@ -149,17 +138,17 @@ class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
queryset = SoftwareProductInstallation.objects.all()
model_form = SoftwareProductInstallationCSVForm
table = SoftwareProductInstallationTable
model_form = forms.SoftwareProductInstallationCSVForm
table = tables.SoftwareProductInstallationTable
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
queryset = SoftwareProductInstallation.objects.all()
filterset = SoftwareProductInstallationFilter
table = SoftwareProductInstallationTable
form = SoftwareProductInstallationBulkEditForm
filterset = filtersets.SoftwareProductInstallationFilterSet
table = tables.SoftwareProductInstallationTable
form = forms.SoftwareProductInstallationBulkEditForm
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
queryset = SoftwareProductInstallation.objects.all()
table = SoftwareProductInstallationTable
table = tables.SoftwareProductInstallationTable
+1 -1
View File
@@ -1,6 +1,6 @@
[metadata]
name = netbox-slm
version = 0.98
version = 1.1
description = Software Lifecycle Management Netbox Plugin.
# long_description = file: README.rst
# long_description_content_type='text/x-rst'
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.
-215
View File
@@ -1,215 +0,0 @@
Software Lifecycle Management
-----------------------------
*Netbox Plugin for lifecycle management of software components, including versions and installations*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--------------
Installation Guide
~~~~~~~~~~~~~~~~~~
WARNING: This plugin is only tested with Netbox 3.2 at this time.
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:
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
``configuration/extra.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==0.98`` as
contents.
3. Create a ``Dockerfile-SLM`` with contents:
.. code:: dockerfile
FROM netboxcommunity/netbox:v3.2.X
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
::
# make sure to update the version in netbox_slm/__init__.py
$ 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.
Steps
~~~~~
::
from your projects directory clone the netbox repository
$ git clone https://github.com/netbox-community/netbox
$ cd netbox
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,)
-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 NetBoxModelFilterSet
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
from utilities.forms import DynamicModelMultipleChoiceField
class BaseFilter(NetBoxModelFilterSet):
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)