diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 9730b4c..d6c7746 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,7 +14,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml index 0867598..8445347 100644 --- a/.github/workflows/markdown-lint.yml +++ b/.github/workflows/markdown-lint.yml @@ -14,7 +14,7 @@ jobs: vale: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 0e8096b..523e262 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -19,8 +19,9 @@ jobs: - python-version: 3.11 - python-version: 3.12 - python-version: 3.13 + - python-version: 3.14 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/zizmor-scan.yml b/.github/workflows/zizmor-scan.yml index 22c01cf..b339011 100644 --- a/.github/workflows/zizmor-scan.yml +++ b/.github/workflows/zizmor-scan.yml @@ -17,7 +17,7 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/CHANGELOG.md b/CHANGELOG.md index c50ba8a..178d456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +## [1.9.0](https://github.com/ICTU/netbox_slm/releases/tag/1.9.0) - 2026-06-25 + +### Changed + +* Support NetBox v4.6.3 (#76) +* Support Python 3.14 (#76) + ## [1.8.3](https://github.com/ICTU/netbox_slm/releases/tag/1.8.3) - 2025-12-18 ### Fixed diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 5aee724..a07dbaa 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -4,6 +4,8 @@ ## 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 diff --git a/ci/Dockerfile b/ci/Dockerfile index ff5cbcd..ddb0f95 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,4 +1,4 @@ -ARG PYTHON_VERSION=3.12 +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 @@ -12,7 +12,7 @@ RUN pip install -U .[build] &&\ python -m build &&\ pip install --no-index /build -FROM netboxcommunity/netbox:v4.2.3 -ARG PYTHON_VERSION=3.12 +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 index 6ff4195..91ef309 100644 --- a/ci/Dockerfile-CI +++ b/ci/Dockerfile-CI @@ -1,5 +1,5 @@ -FROM netboxcommunity/netbox:v4.2.3 -ARG PYTHON_VERSION=3.12 +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 index a412657..69881f6 100644 --- a/ci/config/configuration.py +++ b/ci/config/configuration.py @@ -1,18 +1,20 @@ """ 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.2.3/netbox/netbox/configuration_testing.py +Based on https://github.com/netbox-community/netbox/blob/v4.6.3/netbox/netbox/configuration_testing.py """ from os import environ ALLOWED_HOSTS = ['*'] -DATABASE = { - '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')), +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' @@ -38,3 +40,9 @@ REDIS = { } 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 index ef73fbd..0f051b8 100644 --- a/ci/docker-compose.ci.yml +++ b/ci/docker-compose.ci.yml @@ -12,7 +12,7 @@ services: healthcheck: disable: true volumes: - - ./config:/etc/netbox/config:ro + - ./config:/etc/netbox/config:z,ro - ./reports:/ci/reports:rw - ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro netbox-worker: @@ -22,14 +22,8 @@ services: healthcheck: disable: true volumes: - - ./config:/etc/netbox/config:ro + - ./config:/etc/netbox/config:z,ro - ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro - netbox-housekeeping: - image: netbox:slm - depends_on: - - netbox - healthcheck: - disable: true postgres: ports: - "5432:5432" diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index 8ae9410..276fe0d 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -1,21 +1,21 @@ --- -# based on https://github.com/netbox-community/netbox-docker/blob/3.2.0/docker-compose.yml +# based on https://github.com/netbox-community/netbox-docker/blob/5.0.1/docker-compose.yml services: netbox: &netbox - image: netboxcommunity/netbox:${VERSION-v4.2.3} + image: docker.io/netboxcommunity/netbox:${VERSION-v4.6.3} depends_on: - postgres - redis - redis-cache env_file: env/netbox.env - user: "unit:root" + 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:ro + - ./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 @@ -33,22 +33,10 @@ services: start_period: 20s timeout: 3s interval: 15s - netbox-housekeeping: - <<: *netbox - depends_on: - netbox: - condition: service_healthy - command: - - /opt/netbox/housekeeping.sh - healthcheck: - test: ps -aux | grep -v grep | grep -q housekeeping || exit 1 - start_period: 20s - timeout: 3s - interval: 15s # postgres postgres: - image: postgres:17-alpine + image: docker.io/postgres:18-alpine healthcheck: test: pg_isready -q -t 2 -d $$POSTGRES_DB -U $$POSTGRES_USER start_period: 20s @@ -57,15 +45,15 @@ services: retries: 5 env_file: env/postgres.env volumes: - - netbox-postgres-data:/var/lib/postgresql/data + - netbox-postgres:/var/lib/postgresql # redis redis: - image: valkey/valkey:8.0-alpine + image: docker.io/valkey/valkey:9.0-alpine command: - sh - - -c - - valkey-server --appendonly yes --requirepass $$REDIS_PASSWORD + - -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 @@ -76,11 +64,11 @@ services: volumes: - netbox-redis-data:/data redis-cache: - image: valkey/valkey:8.0-alpine + image: docker.io/valkey/valkey:9.0-alpine command: - sh - - -c - - valkey-server --requirepass $$REDIS_PASSWORD + - -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: @@ -89,7 +77,7 @@ services: volumes: netbox-media-files: driver: local - netbox-postgres-data: + netbox-postgres: driver: local netbox-redis-cache-data: driver: local diff --git a/netbox_slm/__init__.py b/netbox_slm/__init__.py index 87391d0..b3e34c8 100644 --- a/netbox_slm/__init__.py +++ b/netbox_slm/__init__.py @@ -16,7 +16,7 @@ limitations under the License. from netbox.plugins import PluginConfig -__version__ = "1.8.3" +__version__ = "1.9.0" class SLMConfig(PluginConfig): diff --git a/netbox_slm/migrations/0007_softwareproductinstallation_cluster.py b/netbox_slm/migrations/0007_softwareproductinstallation_cluster.py index ea5d78b..1d1102d 100644 --- a/netbox_slm/migrations/0007_softwareproductinstallation_cluster.py +++ b/netbox_slm/migrations/0007_softwareproductinstallation_cluster.py @@ -24,6 +24,6 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='softwareproductinstallation', - constraint=models.CheckConstraint(check=models.Q(models.Q(('cluster__isnull', True), ('device__isnull', False), ('virtualmachine__isnull', True)), models.Q(('cluster__isnull', True), ('device__isnull', True), ('virtualmachine__isnull', False)), models.Q(('cluster__isnull', False), ('device__isnull', True), ('virtualmachine__isnull', True)), _connector='OR'), name='netbox_slm_softwareproductinstallation_platform', violation_error_message='Installation requires exactly one platform destination.'), + constraint=models.CheckConstraint(condition=models.Q(models.Q(('cluster__isnull', True), ('device__isnull', False), ('virtualmachine__isnull', True)), models.Q(('cluster__isnull', True), ('device__isnull', True), ('virtualmachine__isnull', False)), models.Q(('cluster__isnull', False), ('device__isnull', True), ('virtualmachine__isnull', True)), _connector='OR'), name='netbox_slm_softwareproductinstallation_platform', violation_error_message='Installation requires exactly one platform destination.'), ), ] diff --git a/netbox_slm/models.py b/netbox_slm/models.py index 1780980..451abe6 100644 --- a/netbox_slm/models.py +++ b/netbox_slm/models.py @@ -1,7 +1,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse -from django.utils.html import format_html, urlencode +from django.utils.html import format_html from license_expression import Licensing, get_spdx_licensing from netbox.models import NetBoxModel @@ -37,10 +37,10 @@ class SoftwareProduct(NetBoxModel): def get_installation_count(self): count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count() - query_string = urlencode(dict(software_product_id=self.pk)) - search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list") - # Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582 - return format_html(f"{count}") if count else "0" + search_target = reverse( + "plugins:netbox_slm:softwareproductinstallation_list", query={"software_product_id": self.pk} + ) + return format_html("{}", search_target, count) if count else "0" class SoftwareReleaseTypes(models.TextChoices): @@ -83,10 +83,8 @@ class SoftwareProductVersion(NetBoxModel): def get_installation_count(self): count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count() - query_string = urlencode(dict(version_id=self.pk)) - search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list") - # Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582 - return format_html(f"{count}") if count else "0" + search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list", query={"version_id": self.pk}) + return format_html("{}", search_target, count) if count else "0" class SoftwareProductInstallation(NetBoxModel): @@ -109,7 +107,7 @@ class SoftwareProductInstallation(NetBoxModel): constraints = [ models.CheckConstraint( name="%(app_label)s_%(class)s_platform", - check=( + condition=( models.Q(device__isnull=False, virtualmachine__isnull=True, cluster__isnull=True) | models.Q(device__isnull=True, virtualmachine__isnull=False, cluster__isnull=True) | models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False) diff --git a/pyproject.toml b/pyproject.toml index 24e07d6..ca92a92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,23 +26,24 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] dependencies = [ - "license-expression == 30.4.1", + "license-expression == 30.4.4", ] [project.optional-dependencies] build = [ - "build == 1.2.2.post1", - "setuptools == 75.8.0", - "twine == 6.1.0", + "build == 1.5.0", + "setuptools == 82.0.1", + "twine == 6.2.0", ] ci = [ - "coverage == 7.6.11", - "ruff == 0.9.6", + "coverage == 7.14.3", + "ruff == 0.15.19", ] [project.urls] diff --git a/sonar-project.properties b/sonar-project.properties index a64ba97..0217eb7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,13 +2,13 @@ sonar.organization=ictu sonar.projectKey=ICTU_netbox_slm sonar.projectName=netbox-slm -sonar.projectVersion=1.8.3 +sonar.projectVersion=1.9.0 # Path is relative to the sonar-project.properties file sonar.sources=netbox_slm sonar.exclusions=netbox_slm/tests/** sonar.tests=netbox_slm/tests -sonar.python.version=3.11, 3.12, 3.13 +sonar.python.version=3.11, 3.12, 3.13, 3.14 # Python test reports sonar.python.coverage.reportPaths=/ci/reports/coverage.xml