From 01d40ddecc1ba2c6c47aa09105dc80495a431865 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 24 Jul 2026 15:22:08 +0200 Subject: [PATCH] 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. --- .github/workflows/docker-image.yml | 43 ------ CHANGELOG.md | 9 ++ README.md | 140 ++++++++++++------ ci/.vale.ini | 12 -- ci/Dockerfile | 18 --- ci/Dockerfile-CI | 5 - ci/config/configuration.py | 48 ------ ci/docker-compose.ci.yml | 32 ---- ci/docker-compose.yml | 89 ----------- ci/env/netbox.env | 42 ------ ci/env/postgres.env | 3 - ci/env/redis-cache.env | 1 - ci/env/redis.env | 1 - ci/reports/.gitkeep | 0 ci/styles/config/vocabularies/Base/accept.txt | 66 --------- ci/styles/config/vocabularies/Base/reject.txt | 3 - netbox_slm/api/serializers.py | 12 ++ netbox_slm/filtersets.py | 5 + netbox_slm/forms/software_license.py | 44 ++++++ .../0011_softwarelicense_tenancy.py | 34 +++++ netbox_slm/models.py | 19 +++ netbox_slm/tables.py | 6 + .../templates/netbox_slm/softwarelicense.html | 8 + netbox_slm/tests/base.py | 7 + netbox_slm/tests/test_models.py | 15 ++ start-netbox.sh | 17 --- 26 files changed, 257 insertions(+), 422 deletions(-) delete mode 100644 .github/workflows/docker-image.yml delete mode 100644 ci/.vale.ini delete mode 100644 ci/Dockerfile delete mode 100644 ci/Dockerfile-CI delete mode 100644 ci/config/configuration.py delete mode 100644 ci/docker-compose.ci.yml delete mode 100644 ci/docker-compose.yml delete mode 100644 ci/env/netbox.env delete mode 100644 ci/env/postgres.env delete mode 100644 ci/env/redis-cache.env delete mode 100644 ci/env/redis.env delete mode 100644 ci/reports/.gitkeep delete mode 100644 ci/styles/config/vocabularies/Base/accept.txt delete mode 100644 ci/styles/config/vocabularies/Base/reject.txt create mode 100644 netbox_slm/migrations/0011_softwarelicense_tenancy.py delete mode 100755 start-netbox.sh diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index d6c7746..0000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -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/\/opt\/netbox\/netbox<\/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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 178d456..45e9909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index d521f21..67ae952 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,116 @@ # NetBox SLM -

NetBox SLM is a plugin for lifecycle management of software components, including versions and installations.

+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. -
-PyPi -Stars Badge -Forks Badge -Pull Requests Badge -Issues Badge -GitHub contributors -License Badge -
+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. diff --git a/ci/.vale.ini b/ci/.vale.ini deleted file mode 100644 index 1ae7f62..0000000 --- a/ci/.vale.ini +++ /dev/null @@ -1,12 +0,0 @@ -StylesPath = styles - -MinAlertLevel = suggestion -Vocab = Base - -Packages = proselint - -[*.md] -BasedOnStyles = Vale, proselint - -[ci/styles/**] -BasedOnStyles = diff --git a/ci/Dockerfile b/ci/Dockerfile deleted file mode 100644 index ddb0f95..0000000 --- a/ci/Dockerfile +++ /dev/null @@ -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 diff --git a/ci/Dockerfile-CI b/ci/Dockerfile-CI deleted file mode 100644 index 91ef309..0000000 --- a/ci/Dockerfile-CI +++ /dev/null @@ -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 diff --git a/ci/config/configuration.py b/ci/config/configuration.py deleted file mode 100644 index 69881f6..0000000 --- a/ci/config/configuration.py +++ /dev/null @@ -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', -} diff --git a/ci/docker-compose.ci.yml b/ci/docker-compose.ci.yml deleted file mode 100644 index 0f051b8..0000000 --- a/ci/docker-compose.ci.yml +++ /dev/null @@ -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" diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml deleted file mode 100644 index 276fe0d..0000000 --- a/ci/docker-compose.yml +++ /dev/null @@ -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 diff --git a/ci/env/netbox.env b/ci/env/netbox.env deleted file mode 100644 index b2f647e..0000000 --- a/ci/env/netbox.env +++ /dev/null @@ -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 diff --git a/ci/env/postgres.env b/ci/env/postgres.env deleted file mode 100644 index bb7b53c..0000000 --- a/ci/env/postgres.env +++ /dev/null @@ -1,3 +0,0 @@ -POSTGRES_DB=netbox -POSTGRES_PASSWORD=J5brHrAXFLQSif0K -POSTGRES_USER=netbox diff --git a/ci/env/redis-cache.env b/ci/env/redis-cache.env deleted file mode 100644 index 6285c33..0000000 --- a/ci/env/redis-cache.env +++ /dev/null @@ -1 +0,0 @@ -REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36 diff --git a/ci/env/redis.env b/ci/env/redis.env deleted file mode 100644 index 44a1987..0000000 --- a/ci/env/redis.env +++ /dev/null @@ -1 +0,0 @@ -REDIS_PASSWORD=H733Kdjndks81 diff --git a/ci/reports/.gitkeep b/ci/reports/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/ci/styles/config/vocabularies/Base/accept.txt b/ci/styles/config/vocabularies/Base/accept.txt deleted file mode 100644 index 7fdad0c..0000000 --- a/ci/styles/config/vocabularies/Base/accept.txt +++ /dev/null @@ -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 diff --git a/ci/styles/config/vocabularies/Base/reject.txt b/ci/styles/config/vocabularies/Base/reject.txt deleted file mode 100644 index 70dde23..0000000 --- a/ci/styles/config/vocabularies/Base/reject.txt +++ /dev/null @@ -1,3 +0,0 @@ -docker composition -Docker composition -docker-composition diff --git a/netbox_slm/api/serializers.py b/netbox_slm/api/serializers.py index 4998d1f..23c2d9c 100644 --- a/netbox_slm/api/serializers.py +++ b/netbox_slm/api/serializers.py @@ -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() diff --git a/netbox_slm/filtersets.py b/netbox_slm/filtersets.py index 594ce7b..311e111 100644 --- a/netbox_slm/filtersets.py +++ b/netbox_slm/filtersets.py @@ -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) diff --git a/netbox_slm/forms/software_license.py b/netbox_slm/forms/software_license.py index e3d52b2..4cf7e34 100644 --- a/netbox_slm/forms/software_license.py +++ b/netbox_slm/forms/software_license.py @@ -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", + ) diff --git a/netbox_slm/migrations/0011_softwarelicense_tenancy.py b/netbox_slm/migrations/0011_softwarelicense_tenancy.py new file mode 100644 index 0000000..3635e42 --- /dev/null +++ b/netbox_slm/migrations/0011_softwarelicense_tenancy.py @@ -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", + ), + ), + ] diff --git a/netbox_slm/models.py b/netbox_slm/models.py index 451abe6..466fb86 100644 --- a/netbox_slm/models.py +++ b/netbox_slm/models.py @@ -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: diff --git a/netbox_slm/tables.py b/netbox_slm/tables.py index 95dbcb6..cb04259 100644 --- a/netbox_slm/tables.py +++ b/netbox_slm/tables.py @@ -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", ) diff --git a/netbox_slm/templates/netbox_slm/softwarelicense.html b/netbox_slm/templates/netbox_slm/softwarelicense.html index 73f2c78..c532e85 100644 --- a/netbox_slm/templates/netbox_slm/softwarelicense.html +++ b/netbox_slm/templates/netbox_slm/softwarelicense.html @@ -40,6 +40,14 @@ Installation {{ object.installation|linkify }} + + Tenant Group + {{ object.tenant_group|linkify }} + + + Tenant + {{ object.tenant|linkify }} + Stored location {% if object.stored_location_url %} diff --git a/netbox_slm/tests/base.py b/netbox_slm/tests/base.py index 578e7cc..3a6f4c4 100644 --- a/netbox_slm/tests/base.py +++ b/netbox_slm/tests/base.py @@ -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() diff --git a/netbox_slm/tests/test_models.py b/netbox_slm/tests/test_models.py index 1e33685..4614930 100644 --- a/netbox_slm/tests/test_models.py +++ b/netbox_slm/tests/test_models.py @@ -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() diff --git a/start-netbox.sh b/start-netbox.sh deleted file mode 100755 index 749e0f9..0000000 --- a/start-netbox.sh +++ /dev/null @@ -1,17 +0,0 @@ -git clone -b release https://github.com/netbox-community/netbox-docker.git -cd netbox-docker -tee docker-compose.override.yml <