2 Commits
Author SHA1 Message Date
MrBlake e1c817229b feat: aufräumaktion 2026-07-24 15:22:59 +02:00
MrBlake 01d40ddecc feat: add tenancy support for software licenses
Add tenant group and tenant assignments across the model, forms, filters, API, tables, imports, and tests. Replace the Docker-based setup with Git/Pip installation documentation for NetBox 4.6.5.
2026-07-24 15:22:08 +02:00
33 changed files with 257 additions and 848 deletions
-43
View File
@@ -1,43 +0,0 @@
---
name: Docker Image CI
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Build the Docker image
run: docker compose build --no-cache
- name: Verify the Docker image
# smoke test by checking if the migrations for app netbox_slm can be displayed
run: docker compose run netbox sh -c "/opt/netbox/venv/bin/python manage.py showmigrations netbox_slm"
- name: Run Django tests with coverage for netbox_slm
run: |
chmod go+w ci/reports
docker compose run netbox sh -c "\
/opt/netbox/venv/bin/coverage run --source='netbox_slm' manage.py test netbox_slm &&\
/opt/netbox/venv/bin/coverage report --fail-under=0 &&\
/opt/netbox/venv/bin/coverage xml -o /ci/reports/coverage.xml"
sed -i "s/<source>\/opt\/netbox\/netbox<\/source>/<source><\/source>/" ci/reports/coverage.xml
timeout-minutes: 6
- name: Sonar scan
if: env.SONAR_TOKEN != null
uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-25
View File
@@ -1,25 +0,0 @@
---
name: Markdown lint
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
vale:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: errata-ai/vale-action@85f9f7f2c5f449ac0ae5b66662961bae3f77ca6a # v2.1.2
with:
fail_on_error: true
filter_mode: nofilter
vale_flags: "--glob=*.md --config=ci/.vale.ini"
-40
View File
@@ -1,40 +0,0 @@
---
name: Python
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- python-version: 3.11
- python-version: 3.12
- python-version: 3.13
- python-version: 3.14
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
- name: Install ci tools
run: pip install .[ci]
- name: Run ruff lint
run: ruff check netbox_slm
- name: Run ruff format
run: ruff format --check netbox_slm
-25
View File
@@ -1,25 +0,0 @@
---
name: GitHub Actions Security Analysis with zizmor
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
+9
View File
@@ -2,6 +2,15 @@
## [Unreleased]
### Added
* Optional tenant group and tenant assignments for software licenses
### Changed
* Support NetBox v4.6.5
* Replace the Docker-based setup with Git/Pip installation documentation
## [1.9.0](https://github.com/ICTU/netbox_slm/releases/tag/1.9.0) - 2026-06-25
### Changed
-133
View File
@@ -1,133 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement by
[opening a new issue in this repository's issue tracker][open issue].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
[open issue]: https://github.com/ICTU/netbox_slm/issues/new
-34
View File
@@ -1,34 +0,0 @@
# NetBox SLM contributing guidelines
## What do I need to know to help?
If you are looking to help to with a code contribution our project uses, you can find inspiration in the [issue list](https://github.com/ICTU/netbox_slm/issues/).
Additionally, review the [README](./README.md) document and [NetBox Plugin Development](https://docs.netbox.dev/en/stable/plugins/development/) docs.
## How do I make a contribution?
Never made an open source contribution before? Wondering how contributions work in the in our project? Here's a quick rundown!
1. Find an issue that you are interested in addressing or a feature that you would like to add.
2. Fork the repository associated with the issue to your local GitHub organization. This means that you will have a copy of the repository under **your-GitHub-username/repository-name**.
3. Clone the repository to your local machine using `git clone https://github.com/github-username/repository-name.git`.
4. Create a new branch for your fix using `git checkout -b branch-name-here`.
5. Make the appropriate changes for the issue you are trying to address or the feature that you want to add.
6. Use git `add insert-paths-of-changed-files-here` to add the file contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index.
7. Use `git commit -m "Insert a short message of the changes made here"` to store the contents of the index with a descriptive message.
8. Push the changes to the remote repository using `git push origin branch-name-here`.
9. Submit a pull request to the upstream repository.
10. Title the pull request with a short description of the changes made and the issue or bug number associated with your change. For example, you can title an issue like so "Added more log outputting to resolve #4352".
11. In the description of the pull request, explain the changes that you made, any issues you think exist with the pull request you made, and any questions you have for the maintainer. It's OK if your pull request is not perfect (no pull request is), the reviewer will be able to help you fix any problems and improve it!
12. Wait for the pull request to be reviewed by a maintainer.
13. Make changes to the pull request if the reviewing maintainer recommends them.
14. Celebrate your success after your pull request is merged!
## Where can I go for help?
If you need help, you can ask questions by [opening a new issue in this repository's issue tracker](https://github.com/ICTU/netbox_slm/issues/new).
## What does the Code of Conduct mean for me?
Our [Code of Conduct](./CODE_OF_CONDUCT.md) means that you are responsible for treating everyone on the project with respect and courtesy regardless of their identity.
If you are the victim of any inappropriate behavior or comments as described in our Code of Conduct, we are here for you and will do the best to ensure that the abuser is reprimanded appropriately, per our code.
-153
View File
@@ -1,153 +0,0 @@
# Maintenance related tasks
## Version upgrade workflow
1. Update version spec in `netbox_slm/__init__.py` and `sonar-project.properties`
1. Update NetBox base image versions in `ci/Dockerfile`, `ci/Dockerfile-CI` and `ci/docker-compose.yml`
1. Ensure that the `PYTHON_VERSION` in the `Dockerfile`s is the same as the NetBox community docker base image
1. Check for any runtime errors and warnings in the `netbox-*` container logs
1. Create new version tag on GitHub, following semantic versioning as: `MAJOR.MINOR.PATCH`
1. Update the `CHANGELOG.md` with new version information and move `[Unreleased]` items to new version section
1. Build the package: `python -m build`
1. Upload the distributions to PyPI: `twine upload --skip-existing dist/*`
## 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 PostgreSQL and Redis container instances, basically ignoring the
NetBox docker runtime server.
### Steps
from your projects directory clone the NetBox repository
```shell
$ git clone https://github.com/netbox-community/netbox
$ cd netbox
```
install the virtual environment
```shell
$ pipenv shell
$ pipenv install
```
create and edit `netbox/configuration.py` (based on the template file) add these lines at the end of the file;
```python
DEBUG = True
SECRET_KEY = 'dummy'
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
```shell
$ 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 PostgreSQL ports to the localhost (make sure theres no processes
using these ports or change the Dockerfile(s) accordingly)
Note, you can also start and stop NetBox by hand:
```shell
$ cd netbox-docker
$ docker compose up -d
```
or stop the stack with
```shell
$ docker compose down
```
#### to start fresh:
```shell
$ 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 PostgreSQL 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):
```python
# 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,
}
}
```
Now you can run commands from the NetBox repository like this;
```shell
$ cd netbox/netbox
$ 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 browser to see the auto reloading version of the NetBox UI.
Port 8000 is taken by the Docker ran variant.
+98 -42
View File
@@ -1,60 +1,116 @@
# NetBox SLM
<p align="center"><i>NetBox SLM is a plugin for lifecycle management of software components, including versions and installations.</i></p>
NetBox SLM erweitert NetBox um die Verwaltung von Softwareprodukten, Versionen, Installationen und Lizenzen.
Lizenzen können optional einer NetBox-Mandantengruppe und einem Mandanten zugeordnet werden.
<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>
Diese Variante ist für **NetBox 4.6.5** vorgesehen.
## Funktionen
## Installation Guide
- Softwareprodukte und Hersteller verwalten
- Versionen, Release-Typen und Supportzeiträume erfassen
- Installationen Geräten, virtuellen Maschinen oder Clustern zuordnen
- Softwarelizenzen einschließlich Laufzeit, Umfang und Ablageort verwalten
- Lizenzen Mandantengruppen und Mandanten zuordnen
- Bedienung über die NetBox-Oberfläche und REST-API
- Filter, Bulk-Import und Bulk-Bearbeitung
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` repository and set up the ``docker-compose.override.yml``.
## Installation
Note that this plugin is only tested against a single NetBox version at this time, see [Dockerfile-CI](https://github.com/ICTU/netbox_slm/blob/master/ci/Dockerfile-CI).
NetBox installiert lokale Erweiterungen aus `/opt/netbox/local_requirements.txt`. Die Git-URL muss dort dauerhaft
eingetragen werden, damit das Plugin bei einem späteren NetBox-Upgrade erneut installiert wird.
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. Git-Repository als lokale Anforderung eintragen
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
``configuration/plugins.py``.
2. Create a ``plugin_requirements.txt`` with ``netbox-slm`` as
contents.
3. Create a ``Dockerfile-SLM`` with contents:
```dockerfile
FROM netboxcommunity/netbox:vX.Y.Z
COPY ../pyproject.toml /tmp/
RUN uv pip install -r /tmp/pyproject.toml
```bash
echo "git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git" >> /opt/netbox/local_requirements.txt
```
4. Create a ``docker-compose.override.yml`` with contents:
Vorher kann geprüft werden, ob der Eintrag bereits vorhanden ist:
```yaml
version: "3.7"
services:
netbox:
ports:
- "8000:8080"
build:
context: .
dockerfile: Dockerfile-SLM
image: netbox:slm
netbox-worker:
image: netbox:slm
```bash
grep -qxF "git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git" /opt/netbox/local_requirements.txt || \
echo "git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git" >> /opt/netbox/local_requirements.txt
```
Now, build the image: ``docker compose build --no-cache``
### 2. Plugin mit Pip installieren
And finally, run NetBox with the SLM plugin: ``docker compose up``
Die Installation muss im Python-Virtual-Environment von NetBox erfolgen:
```bash
/opt/netbox/venv/bin/pip install "git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git"
```
## Get in touch
Alternativ können alle lokalen Anforderungen installiert werden:
Point of contact for this repository is [Mart Visser](https://github.com/MartVisser), who can be reached by [opening a new issue in this repository's issue tracker](https://github.com/ICTU/netbox_slm/issues/new).
```bash
/opt/netbox/venv/bin/pip install -r /opt/netbox/local_requirements.txt
```
### 3. Plugin in NetBox aktivieren
In `/opt/netbox/netbox/netbox/configuration.py` ergänzen:
```python
PLUGINS = [
"netbox_slm",
]
```
Falls bereits andere Plugins eingetragen sind, wird `"netbox_slm"` lediglich zur vorhandenen Liste hinzugefügt.
Eine zusätzliche `PLUGINS_CONFIG`-Konfiguration ist für den Standardbetrieb nicht erforderlich.
### 4. Datenbank migrieren und statische Dateien aktualisieren
```bash
cd /opt/netbox/netbox
/opt/netbox/venv/bin/python manage.py migrate
/opt/netbox/venv/bin/python manage.py collectstatic --no-input
```
### 5. NetBox-Dienste neu starten
Bei einer üblichen systemd-Installation:
```bash
sudo systemctl restart netbox netbox-rq
```
Je nach Installation können die Namen der Dienste abweichen.
## Aktualisierung
```bash
/opt/netbox/venv/bin/pip install --upgrade --force-reinstall \
"git+https://git.mrblake.cc/MrBlake/NetBox-SLM.git"
cd /opt/netbox/netbox
/opt/netbox/venv/bin/python manage.py migrate
/opt/netbox/venv/bin/python manage.py collectstatic --no-input
sudo systemctl restart netbox netbox-rq
```
Der Eintrag in `/opt/netbox/local_requirements.txt` bleibt dabei bestehen.
## Deinstallation
1. `"netbox_slm"` aus der `PLUGINS`-Liste entfernen.
2. Die Git-Zeile aus `/opt/netbox/local_requirements.txt` entfernen.
3. Das Python-Paket deinstallieren:
```bash
/opt/netbox/venv/bin/pip uninstall netbox-slm
```
Die Plugin-Tabellen werden dabei nicht automatisch aus der Datenbank gelöscht.
## Ursprung und Lizenz
Dieses Repository basiert auf dem ursprünglichen Projekt
[ICTU/netbox_slm](https://github.com/ICTU/netbox_slm). **ICTU** ist der ursprüngliche Autor und Herausgeber.
[Mart Visser](https://github.com/MartVisser) wird im Originalprojekt als Ansprechpartner genannt.
Die Anpassungen in diesem Fork werden unter
[git.mrblake.cc/MrBlake/NetBox-SLM](https://git.mrblake.cc/MrBlake/NetBox-SLM) gepflegt.
Das Projekt steht unter der [Apache License 2.0](LICENSE). Die Hinweise auf die ursprünglichen Urheber bleiben
erhalten.
-16
View File
@@ -1,16 +0,0 @@
# NetBox SLM Security Policy
## Current status
NetBox SLM is a plugin for [NetBox](https://github.com/netbox-community/netbox/) with mostly default configuration.
In order to stay up to date, monitor NetBox security findings and update accordingly.
## Supported Versions
Only the latest version of NetBox SLM is currently being supported with security updates.
The intention is to keep the plugin compatible with the most recent NetBox version(s), there is no incentive to patch older tags.
## Reporting a Vulnerability
You can privately [report a vulnerability issue in this repository's issue tracker](https://github.com/ICTU/netbox_slm/security/advisories/new).
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.
-12
View File
@@ -1,12 +0,0 @@
StylesPath = styles
MinAlertLevel = suggestion
Vocab = Base
Packages = proselint
[*.md]
BasedOnStyles = Vale, proselint
[ci/styles/**]
BasedOnStyles =
-18
View File
@@ -1,18 +0,0 @@
ARG PYTHON_VERSION=3.14
FROM python:${PYTHON_VERSION}-alpine AS compile-image
# Python version build arg is not actually supported by NetBox container image, only used for maintenance convenience
WORKDIR /build
RUN python3 -m venv /opt/netbox/venv &&\
source /opt/netbox/venv/bin/activate
COPY ../ /build
RUN pip install -U .[build] &&\
python -m build &&\
pip install --no-index /build
FROM netboxcommunity/netbox:v4.6.3
ARG PYTHON_VERSION=3.14
COPY --from=compile-image /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm
-5
View File
@@ -1,5 +0,0 @@
FROM netboxcommunity/netbox:v4.6.3
ARG PYTHON_VERSION=3.14
COPY ../pyproject.toml /tmp/
RUN uv pip install -r /tmp/pyproject.toml --extra ci
-48
View File
@@ -1,48 +0,0 @@
"""
Minimal NetBox config for NetBox SLM plugin testing, docs: https://netbox.readthedocs.io/en/stable/configuration/
Based on https://github.com/netbox-community/netbox/blob/v4.6.3/netbox/netbox/configuration_testing.py
"""
from os import environ
ALLOWED_HOSTS = ['*']
DATABASES = {
'default': {
'NAME': environ.get('DB_NAME', 'netbox'),
'USER': environ.get('DB_USER', ''),
'PASSWORD': environ.get('DB_PASSWORD', ''),
'HOST': environ.get('DB_HOST', 'localhost'),
'PORT': environ.get('DB_PORT', ''),
'CONN_MAX_AGE': int(environ.get('DB_CONN_MAX_AGE', '300')),
}
}
DEBUG = environ.get('DEBUG', 'False').lower() == 'true'
DEVELOPER = environ.get('DEVELOPER', 'False').lower() == 'true'
PLUGINS = ["netbox_slm"]
REDIS = {
'tasks': {
'HOST': environ.get('REDIS_HOST', 'localhost'),
'PORT': int(environ.get('REDIS_PORT', 6379)),
'PASSWORD': environ.get('REDIS_PASSWORD', ''),
'DATABASE': int(environ.get('REDIS_DATABASE', 0)),
'SSL': environ.get('REDIS_SSL', 'False').lower() == 'true',
},
'caching': {
'HOST': environ.get('REDIS_CACHE_HOST', environ.get('REDIS_HOST', 'localhost')),
'PORT': int(environ.get('REDIS_CACHE_PORT', environ.get('REDIS_PORT', 6379))),
'PASSWORD': environ.get('REDIS_CACHE_PASSWORD', environ.get('REDIS_PASSWORD', '')),
'DATABASE': int(environ.get('REDIS_CACHE_DATABASE', 1)),
'SSL': environ.get('REDIS_CACHE_SSL', environ.get('REDIS_SSL', 'False')).lower() == 'true',
},
}
SECRET_KEY = 'dummydummydummydummydummydummydummydummydummydummy'
DEFAULT_PERMISSIONS = {}
API_TOKEN_PEPPERS = {
1: 'TEST-VALUE-DO-NOT-USE-TEST-VALUE-DO-NOT-USE-TEST-VALUE-DO-NOT-USE',
}
-32
View File
@@ -1,32 +0,0 @@
---
services:
netbox:
ports:
- "8001:8080"
build:
context: ..
dockerfile: ci/Dockerfile-CI
image: netbox:slm
environment:
- COVERAGE_FILE=/tmp/.coverage
healthcheck:
disable: true
volumes:
- ./config:/etc/netbox/config:z,ro
- ./reports:/ci/reports:rw
- ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro
netbox-worker:
image: netbox:slm
depends_on:
- netbox
healthcheck:
disable: true
volumes:
- ./config:/etc/netbox/config:z,ro
- ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro
postgres:
ports:
- "5432:5432"
redis:
ports:
- "6379:6379"
-89
View File
@@ -1,89 +0,0 @@
---
# based on https://github.com/netbox-community/netbox-docker/blob/5.0.1/docker-compose.yml
services:
netbox: &netbox
image: docker.io/netboxcommunity/netbox:${VERSION-v4.6.3}
depends_on:
- postgres
- redis
- redis-cache
env_file: env/netbox.env
user: "netbox:root"
healthcheck:
test: curl -f http://localhost:8080/login/ || exit 1
start_period: 90s
timeout: 3s
interval: 15s
volumes:
- ./config:/etc/netbox/config:z,ro
- netbox-media-files:/opt/netbox/netbox/media:rw
- netbox-reports-files:/opt/netbox/netbox/reports:rw
- netbox-scripts-files:/opt/netbox/netbox/scripts:rw
netbox-worker:
<<: *netbox
depends_on:
netbox:
condition: service_healthy
command:
- /opt/netbox/venv/bin/python
- /opt/netbox/netbox/manage.py
- rqworker
healthcheck:
test: ps -aux | grep -v grep | grep -q rqworker || exit 1
start_period: 20s
timeout: 3s
interval: 15s
# postgres
postgres:
image: docker.io/postgres:18-alpine
healthcheck:
test: pg_isready -q -t 2 -d $$POSTGRES_DB -U $$POSTGRES_USER
start_period: 20s
timeout: 30s
interval: 10s
retries: 5
env_file: env/postgres.env
volumes:
- netbox-postgres:/var/lib/postgresql
# redis
redis:
image: docker.io/valkey/valkey:9.0-alpine
command:
- sh
- -c # this is to evaluate the $REDIS_PASSWORD from the env
- valkey-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
healthcheck: &redis-healthcheck
test: '[ $$(valkey-cli --pass "$${REDIS_PASSWORD}" ping) = ''PONG'' ]'
start_period: 5s
timeout: 3s
interval: 1s
retries: 5
env_file: env/redis.env
volumes:
- netbox-redis-data:/data
redis-cache:
image: docker.io/valkey/valkey:9.0-alpine
command:
- sh
- -c # this is to evaluate the $REDIS_PASSWORD from the env
- valkey-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
healthcheck: *redis-healthcheck
env_file: env/redis-cache.env
volumes:
- netbox-redis-cache-data:/data
volumes:
netbox-media-files:
driver: local
netbox-postgres:
driver: local
netbox-redis-cache-data:
driver: local
netbox-redis-data:
driver: local
netbox-reports-files:
driver: local
netbox-scripts-files:
driver: local
-42
View File
@@ -1,42 +0,0 @@
CORS_ORIGIN_ALLOW_ALL=True
DB_HOST=postgres
DB_NAME=netbox
DB_PASSWORD=J5brHrAXFLQSif0K
DB_USER=netbox
EMAIL_FROM=netbox@bar.com
EMAIL_PASSWORD=
EMAIL_PORT=25
EMAIL_SERVER=localhost
EMAIL_SSL_CERTFILE=
EMAIL_SSL_KEYFILE=
EMAIL_TIMEOUT=5
EMAIL_USERNAME=netbox
# EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`!
EMAIL_USE_SSL=false
EMAIL_USE_TLS=false
GRAPHQL_ENABLED=true
HOUSEKEEPING_INTERVAL=86400
MAX_PAGE_SIZE=1000
MEDIA_ROOT=/opt/netbox/netbox/media
METRICS_ENABLED=false
NAPALM_PASSWORD=
NAPALM_TIMEOUT=10
NAPALM_USERNAME=
REDIS_CACHE_DATABASE=1
REDIS_CACHE_HOST=redis-cache
REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false
REDIS_CACHE_PASSWORD=t4Ph722qJ5QHeQ1qfu36
REDIS_CACHE_SSL=false
REDIS_DATABASE=0
REDIS_HOST=redis
REDIS_INSECURE_SKIP_TLS_VERIFY=false
REDIS_PASSWORD=H733Kdjndks81
REDIS_SSL=false
RELEASE_CHECK_URL=https://api.github.com/repos/netbox-community/netbox/releases
SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
SKIP_SUPERUSER=false
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
SUPERUSER_EMAIL=admin@example.com
SUPERUSER_NAME=admin
SUPERUSER_PASSWORD=admin
WEBHOOKS_ENABLED=true
-3
View File
@@ -1,3 +0,0 @@
POSTGRES_DB=netbox
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
POSTGRES_USER=netbox
-1
View File
@@ -1 +0,0 @@
REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36
-1
View File
@@ -1 +0,0 @@
REDIS_PASSWORD=H733Kdjndks81
View File
@@ -1,66 +0,0 @@
Anchore
APIs
Caddy
Checkmarx
Cobertura
Dependabot
Docker-composition
Dockerfile
DTDs
ESLint
Gravatar
Jira
JMeter
JUnit
NCover
Nginx
OJAudit
OpenShift
PDFs
Pydantic
Robocop
Snyk
Trello
Trivy
UUIDs
Wekan
[Hh]ostname
[Uu]nmerged
asyncio
autoformatting
breakpoint
clearable
cloc
discoverability
donut
errored
favicon
fixme
hostnames?
hotspots?
lookback
misconfigured
mypy
namespace
npm
parameterizable
phpldapadmin
[Pp]erformancetest
severities
sparkline
subfolders
submenus
suppressions
todo
tooltips?
tracebacks?
unencrypted
unicode
unmerged
upvotes
url
xml
NetBox
NetBox SLM
Mart
Visser
@@ -1,3 +0,0 @@
docker composition
Docker composition
docker-composition
+12
View File
@@ -1,3 +1,4 @@
from rest_framework import serializers
from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField
from netbox.api.serializers import NetBoxModelSerializer
@@ -27,6 +28,8 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer):
"software_product",
"version",
"installation",
"tenant_group",
"tenant",
"tags",
"comments",
"custom_field_data",
@@ -38,6 +41,15 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer):
def get_display(self, obj):
return f"{obj}"
def validate(self, attrs):
tenant_group = attrs.get("tenant_group", getattr(self.instance, "tenant_group", None))
tenant = attrs.get("tenant", getattr(self.instance, "tenant", None))
if tenant_group and tenant and tenant.group_id != tenant_group.pk:
raise serializers.ValidationError(
{"tenant": "The selected tenant does not belong to the selected tenant group."}
)
return attrs
class SoftwareProductSerializer(NetBoxModelSerializer):
display = SerializerMethodField()
+5
View File
@@ -11,6 +11,7 @@ from netbox_slm.models import (
SoftwareReleaseTypes,
)
from virtualization.models import Cluster, VirtualMachine
from tenancy.models import Tenant, TenantGroup
class SoftwareProductFilterSet(NetBoxModelFilterSet):
@@ -122,6 +123,8 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
)
version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
installation_id = ModelMultipleChoiceFilter(queryset=SoftwareProductInstallation.objects.all())
tenant_group_id = ModelMultipleChoiceFilter(queryset=TenantGroup.objects.all(), label="Tenant Group")
tenant_id = ModelMultipleChoiceFilter(queryset=Tenant.objects.all(), label="Tenant")
class Meta:
model = SoftwareLicense
@@ -139,6 +142,8 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
| Q(installation__device__name__icontains=value)
| Q(installation__virtualmachine__name__icontains=value)
| Q(installation__cluster__name__icontains=value)
| Q(tenant_group__name__icontains=value)
| Q(tenant__name__icontains=value)
| Q(comments__icontains=value)
)
return queryset.filter(qs_filter)
+44
View File
@@ -9,6 +9,7 @@ from netbox_slm.models import (
SoftwareLicense,
spdx_license_names,
)
from tenancy.models import Tenant, TenantGroup
from utilities.forms.constants import BOOLEAN_WITH_BLANK_CHOICES
from utilities.forms.fields import (
CommentField,
@@ -48,6 +49,17 @@ class SoftwareLicenseForm(NetBoxModelForm):
),
query_params=dict(software_product="$software_product"),
)
tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label="Tenant Group",
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
query_params={"group_id": "$tenant_group"},
label="Tenant",
)
class Meta:
model = SoftwareLicense
@@ -65,6 +77,8 @@ class SoftwareLicenseForm(NetBoxModelForm):
"license_amount",
"version",
"installation",
"tenant_group",
"tenant",
"tags",
"comments",
)
@@ -84,6 +98,8 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
"software_product_id",
"version_id",
"installation_id",
"tenant_group_id",
"tenant_id",
),
)
selector_fields = ("q", "filter_id", "name")
@@ -112,6 +128,17 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
required=False,
label="Installation",
)
tenant_group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label="Tenant Group",
)
tenant_id = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
required=False,
query_params={"group_id": "$tenant_group_id"},
label="Tenant",
)
class SoftwareLicenseBulkImportForm(NetBoxModelImportForm):
@@ -132,6 +159,8 @@ class SoftwareLicenseBulkImportForm(NetBoxModelImportForm):
"license_amount",
"version",
"installation",
"tenant_group",
"tenant",
"tags",
)
@@ -151,6 +180,8 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
"software_product",
"version",
"installation",
"tenant_group",
"tenant",
),
)
nullable_fields = (
@@ -164,6 +195,8 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
"license_amount",
"version",
"installation",
"tenant_group",
"tenant",
)
tag = TagFilterField(model)
@@ -197,3 +230,14 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
),
query_params=dict(software_product="$software_product"),
)
tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label="Tenant Group",
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
query_params={"group_id": "$tenant_group"},
label="Tenant",
)
@@ -0,0 +1,34 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("tenancy", "0001_initial"),
("netbox_slm", "0010_softwarelicense_spdx_expression"),
]
operations = [
migrations.AddField(
model_name="softwarelicense",
name="tenant_group",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="software_licenses",
to="tenancy.tenantgroup",
),
),
migrations.AddField(
model_name="softwarelicense",
name="tenant",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="software_licenses",
to="tenancy.tenant",
),
),
]
+19
View File
@@ -163,6 +163,20 @@ class SoftwareLicense(NetBoxModel):
installation = models.ForeignKey(
to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True
)
tenant_group = models.ForeignKey(
to="tenancy.TenantGroup",
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="software_licenses",
)
tenant = models.ForeignKey(
to="tenancy.Tenant",
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="software_licenses",
)
objects = RestrictedQuerySet.as_manager()
@@ -172,6 +186,11 @@ class SoftwareLicense(NetBoxModel):
def get_absolute_url(self):
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
def clean(self):
super().clean()
if self.tenant_group and self.tenant and self.tenant.group_id != self.tenant_group_id:
raise ValidationError({"tenant": "The selected tenant does not belong to the selected tenant group."})
@property
def stored_location_txt(self):
if self.stored_location_url and not self.stored_location:
+6
View File
@@ -144,6 +144,8 @@ class SoftwareLicenseTable(NetBoxTable):
software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True)
version = tables.Column(accessor="version", linkify=True)
installation = tables.Column(accessor="installation", linkify=True)
tenant_group = tables.Column(accessor="tenant_group", verbose_name="Tenant Group", linkify=True)
tenant = tables.Column(accessor="tenant", linkify=True)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
@@ -162,6 +164,8 @@ class SoftwareLicenseTable(NetBoxTable):
"software_product",
"version",
"installation",
"tenant_group",
"tenant",
"support",
"license_amount",
"tags",
@@ -173,6 +177,8 @@ class SoftwareLicenseTable(NetBoxTable):
"manufacturer",
"software_product",
"installation",
"tenant_group",
"tenant",
"expiration_date",
"tags",
)
@@ -40,6 +40,14 @@
<th scope="row">Installation</th>
<td>{{ object.installation|linkify }}</td>
</tr>
<tr>
<th scope="row">Tenant Group</th>
<td>{{ object.tenant_group|linkify }}</td>
</tr>
<tr>
<th scope="row">Tenant</th>
<td>{{ object.tenant|linkify }}</td>
</tr>
<tr>
<th scope="row">Stored location</th>
{% if object.stored_location_url %}
+7
View File
@@ -2,6 +2,7 @@ from django.test import TestCase
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
from tenancy.models import Tenant, TenantGroup
from virtualization.models import Cluster, ClusterType, VirtualMachine
@@ -26,6 +27,8 @@ class SlmBaseTestCase(TestCase):
cls.vm = VirtualMachine.objects.create(name="test VM")
cluster_type = ClusterType.objects.create(name="test cluster type")
cls.cluster = Cluster.objects.create(name="test cluster", type=cluster_type)
cls.tenant_group = TenantGroup.objects.create(name="test tenant group", slug="test-tenant-group")
cls.tenant = Tenant.objects.create(name="test tenant", slug="test-tenant", group=cls.tenant_group)
cls.software_product = SoftwareProduct.objects.create(name=cls.p_name)
cls.software_product_version = SoftwareProductVersion.objects.create(
@@ -40,6 +43,8 @@ class SlmBaseTestCase(TestCase):
version=cls.software_product_version,
installation=cls.software_product_installation,
stored_location_url=cls.test_url,
tenant_group=cls.tenant_group,
tenant=cls.tenant,
)
@classmethod
@@ -48,6 +53,8 @@ class SlmBaseTestCase(TestCase):
SoftwareProductInstallation.objects.all().delete()
SoftwareProductVersion.objects.all().delete()
SoftwareProduct.objects.all().delete()
Tenant.objects.all().delete()
TenantGroup.objects.all().delete()
Cluster.objects.all().delete()
ClusterType.objects.all().delete()
VirtualMachine.objects.all().delete()
+15
View File
@@ -1,3 +1,6 @@
from django.core.exceptions import ValidationError
from tenancy.models import Tenant, TenantGroup
from .base import SlmBaseTestCase
@@ -56,3 +59,15 @@ class ModelTestCase(SlmBaseTestCase):
self.software_license.stored_location = "GitHub"
self.software_license.save()
self.assertEqual("GitHub", self.software_license.stored_location_txt)
def test_software_license_tenancy(self):
self.assertEqual(self.tenant_group, self.software_license.tenant_group)
self.assertEqual(self.tenant, self.software_license.tenant)
def test_software_license_rejects_tenant_from_other_group(self):
other_group = TenantGroup.objects.create(name="other group", slug="other-group")
other_tenant = Tenant.objects.create(name="other tenant", slug="other-tenant", group=other_group)
self.software_license.tenant = other_tenant
with self.assertRaisesMessage(ValidationError, "does not belong to the selected tenant group"):
self.software_license.full_clean()
-17
View File
@@ -1,17 +0,0 @@
git clone -b release https://github.com/netbox-community/netbox-docker.git
cd netbox-docker
tee docker-compose.override.yml <<EOF
version: '3.7'
services:
netbox:
ports:
- 8000:8080
postgres:
ports:
- 5432:5432
redis:
ports:
- 6379:6379
EOF
docker-compose pull
docker-compose up