diff --git a/.devcontainer/Dockerfile-plugin_dev b/.devcontainer/Dockerfile-plugin_dev
index ea10664..633f053 100644
--- a/.devcontainer/Dockerfile-plugin_dev
+++ b/.devcontainer/Dockerfile-plugin_dev
@@ -1,8 +1,8 @@
-ARG NETBOX_VARIANT=v3.4
+ARG NETBOX_VARIANT=v3.5
FROM netboxcommunity/netbox:${NETBOX_VARIANT}
-ARG NETBOX_INITIALIZERS_VARIANT=3.4.*
+ARG NETBOX_INITIALIZERS_VARIANT=3.5.*
ARG DEBIAN_FRONTEND=noninteractive
diff --git a/.devcontainer/configuration/configuration.py b/.devcontainer/configuration/configuration.py
index 004d725..0a41530 100644
--- a/.devcontainer/configuration/configuration.py
+++ b/.devcontainer/configuration/configuration.py
@@ -7,7 +7,7 @@ from os.path import abspath, dirname, join
# Read secret from file
def _read_secret(secret_name, default=None):
try:
- f = open("/run/secrets/" + secret_name, encoding="utf-8")
+ f = open(f"/run/secrets/{secret_name}", encoding="utf-8")
except OSError:
return default
else:
@@ -74,8 +74,7 @@ REDIS = {
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",
+ "SSL": environ.get("REDIS_CACHE_SSL", environ.get("REDIS_SSL", "False")).lower() == "true",
"INSECURE_SKIP_TLS_VERIFY": environ.get(
"REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY",
environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False"),
@@ -252,9 +251,7 @@ REMOTE_AUTH_BACKEND = environ.get(
"netbox.authentication.RemoteUserBackend",
)
REMOTE_AUTH_HEADER = environ.get("REMOTE_AUTH_HEADER", "HTTP_REMOTE_USER")
-REMOTE_AUTH_AUTO_CREATE_USER = (
- environ.get("REMOTE_AUTH_AUTO_CREATE_USER", "True").lower() == "true"
-)
+REMOTE_AUTH_AUTO_CREATE_USER = environ.get("REMOTE_AUTH_AUTO_CREATE_USER", "True").lower() == "true"
REMOTE_AUTH_DEFAULT_GROUPS = list(
filter(None, environ.get("REMOTE_AUTH_DEFAULT_GROUPS", "").split(" ")),
)
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index a3fc26f..6453f36 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -78,20 +78,31 @@
"extensions": [
"DavidAnson.vscode-markdownlint",
"GitHub.codespaces",
+ "GitHub.copilot-labs",
"GitHub.vscode-pull-request-github",
+ "Gruntfuggly.todo-tree",
"Tyriar.sort-lines",
"aaron-bond.better-comments",
"batisteo.vscode-django",
+ "charliermarsh.ruff",
"codezombiech.gitignore",
"esbenp.prettier-vscode",
+ "exiasr.hadolint",
"formulahendry.auto-rename-tag",
"mintlify.document",
+ "ms-python.isort",
+ "ms-python.pylint",
"ms-python.python",
"ms-python.vscode-pylance",
+ "ms-vscode.makefile-tools",
"mutantdino.resourcemonitor",
+ "oderwat.indent-rainbow",
"paulomenezes.duplicated-code",
+ "redhat.vscode-yaml",
"searKing.preview-vscode",
- "sourcery.sourcery"
+ "sourcery.sourcery",
+ "wholroyd.jinja",
+ "yzhang.markdown-all-in-one"
]
}
},
diff --git a/.devcontainer/env/netbox.env b/.devcontainer/env/netbox.env
index ba8a54e..13b0f61 100644
--- a/.devcontainer/env/netbox.env
+++ b/.devcontainer/env/netbox.env
@@ -15,7 +15,7 @@ REDIS_DATABASE=0
REDIS_HOST=redis
REDIS_INSECURE_SKIP_TLS_VERIFY=false
REDIS_PASSWORD=H733Kdjndks81
-SECRET_KEY=r8OwDznj!!dciP9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
+SECRET_KEY=r8OwDznj!!dciP9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNjaa
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
SUPERUSER_EMAIL=admin@example.com
SUPERUSER_NAME=admin
diff --git a/.devcontainer/requirements-dev.txt b/.devcontainer/requirements-dev.txt
index 13a832d..80fc9bf 100644
--- a/.devcontainer/requirements-dev.txt
+++ b/.devcontainer/requirements-dev.txt
@@ -10,5 +10,7 @@ pycodestyle
pydocstyle
pylint
pylint-django
+ruff
+sourcery-analytics
wily
yapf
diff --git a/.flake8 b/.flake8
index 55e13d2..e599bed 100644
--- a/.flake8
+++ b/.flake8
@@ -1,3 +1,3 @@
[flake8]
-max-line-length = 160
+max-line-length = 140
extend-ignore = E203
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index ac362e9..7008bae 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -23,14 +23,14 @@ body:
attributes:
label: NetBox access-list plugin version
description: What version of the NetBox access-list plugin are you currently running?
- placeholder: v1.2.0
+ placeholder: v1.3.0
validations:
required: true
- type: input
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.4.3
+ placeholder: v3.5.4
validations:
required: true
- type: textarea
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index ca31eaf..9f473b7 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.4.3
+ placeholder: v3.5.4
validations:
required: true
- type: dropdown
diff --git a/.github/linters/.flake8 b/.github/linters/.flake8
deleted file mode 100644
index 55e13d2..0000000
--- a/.github/linters/.flake8
+++ /dev/null
@@ -1,3 +0,0 @@
-[flake8]
-max-line-length = 160
-extend-ignore = E203
diff --git a/.github/linters/.isort.cfg b/.github/linters/.isort.cfg
deleted file mode 100644
index a9a8d64..0000000
--- a/.github/linters/.isort.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-[settings]
-profile = black
-
-; vertical hanging indent mode also used in black configuration
-multi_line_output = 3
-
-; necessary because black expect the trailing comma
-include_trailing_comma = true
diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json
deleted file mode 100644
index a474b30..0000000
--- a/.github/linters/.jscpd.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "threshold": 10
-}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4adfb50
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,47 @@
+---
+name: CI
+
+on:
+ push:
+ pull_request:
+
+# This ensures that previous jobs for the workflow are canceled when the ref is
+# updated.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ run-lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ with:
+ # Full git history is needed to get a proper list of changed files within `super-linter`
+ fetch-depth: 0
+
+ - name: Lint Code Base
+ uses: github/super-linter/slim@v4
+ env:
+ DEFAULT_BRANCH: dev
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SUPPRESS_POSSUM: true
+ LINTER_RULES_PATH: /
+ VALIDATE_ALL_CODEBASE: false
+ VALIDATE_DOCKERFILE: false
+ VALIDATE_JSCPD: true
+ FILTER_REGEX_EXCLUDE: (.*/)?(configuration/.*)
+
+ test:
+ runs-on: ubuntu-latest
+ name: Runs plugin tests
+ needs: run-lint
+ steps:
+ - id: git-checkout
+ name: Checkout
+ uses: actions/checkout@v3
+
+ - id: docker-test
+ name: Test the image
+ run: ./test.sh snapshot
diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml
deleted file mode 100644
index 49aa1ee..0000000
--- a/.github/workflows/super-linter.yml
+++ /dev/null
@@ -1,30 +0,0 @@
----
-# This workflow executes several linters on changed files based on languages used in your code base whenever
-# you push a code or open a pull request.
-#
-# You can adjust the behavior by modifying this file.
-# For more information, see:
-# https://github.com/github/super-linter
-name: Lint Code Base
-
-on:
- push:
- branches: ["dev"]
- pull_request:
- branches: ["dev"]
-jobs:
- run-lint:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v3
- with:
- # Full git history is needed to get a proper list of changed files within `super-linter`
- fetch-depth: 0
-
- - name: Lint Code Base
- uses: github/super-linter/slim@v4
- env:
- VALIDATE_ALL_CODEBASE: false
- DEFAULT_BRANCH: "dev"
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index a9a8d64..0000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-[settings]
-profile = black
-
-; vertical hanging indent mode also used in black configuration
-multi_line_output = 3
-
-; necessary because black expect the trailing comma
-include_trailing_comma = true
diff --git a/.jscpd.json b/.jscpd.json
new file mode 100644
index 0000000..481edca
--- /dev/null
+++ b/.jscpd.json
@@ -0,0 +1,4 @@
+{
+ "threshold": 10,
+ "ignore": ["**/migrations/**", "**/tests/**"]
+}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9f31f9d..4580d6c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,21 +9,25 @@ repos:
- id: debug-statements
- id: end-of-file-fixer
- id: name-tests-test
+ args:
+ - "--django"
- id: requirements-txt-fixer
- id: trailing-whitespace
- repo: https://github.com/PyCQA/isort
- rev: 5.11.4
+ rev: 5.12.0
hooks:
- id: isort
args:
- "--profile=black"
+ exclude: ^.devcontainer/
- repo: https://github.com/psf/black
- rev: 22.12.0
+ rev: 23.3.0
hooks:
- id: black
language_version: python3
+ exclude: ^.devcontainer/
- repo: https://github.com/asottile/add-trailing-comma
- rev: v2.4.0
+ rev: v2.5.1
hooks:
- id: add-trailing-comma
args:
@@ -32,25 +36,36 @@ repos:
rev: 6.0.0
hooks:
- id: flake8
+ exclude: ^.devcontainer/
- repo: https://github.com/asottile/pyupgrade
- rev: v3.3.1
+ rev: v3.7.0
hooks:
- id: pyupgrade
args:
- "--py39-plus"
- repo: https://github.com/adrienverge/yamllint
- rev: v1.29.0
+ rev: v1.32.0
hooks:
- id: yamllint
+ - repo: https://github.com/econchick/interrogate
+ rev: 1.5.0
+ hooks:
+ - id: interrogate
+ args: [--fail-under=90, --verbose]
+ exclude: (^.devcontainer/|^netbox_acls/migrations/)
#- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
# rev: v1.1.2
# hooks:
# - id: htmlhint
# args: [--config, .htmlhintrc]
- repo: https://github.com/igorshubovych/markdownlint-cli
- rev: v0.33.0
+ rev: v0.35.0
hooks:
- id: markdownlint
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.0.272
+ hooks:
+ - id: ruff
#- repo: local
# hooks:
# - id: wily
@@ -59,3 +74,13 @@ repos:
# verbose: true
# language: python
# additional_dependencies: [wily]
+ #- repo: https://github.com/sourcery-ai/sourcery
+ # rev: v1.0.4b23
+ # hooks:
+ # - id: sourcery
+ # # The best way to use Sourcery in a pre-commit hook:
+ # # * review only changed lines:
+ # # * omit the summary
+ # args:
+ # - --diff=git diff HEAD
+ # - --no-summary
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3b4a257
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+ARG NETBOX_VARIANT=v3.5
+
+FROM netboxcommunity/netbox:${NETBOX_VARIANT}
+
+RUN mkdir -pv /plugins/netbox-acls
+COPY . /plugins/netbox-acls
+
+RUN /opt/netbox/venv/bin/python3 /plugins/netbox-acls/setup.py develop && \
+ cp -rf /plugins/netbox-acls/netbox_acls/ /opt/netbox/venv/lib/python3.10/site-packages/netbox_acls
diff --git a/MANIFEST.in b/MANIFEST.in
index beb736b..35c0df2 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,3 @@
include README.md
include LICENSE
recursive-include netbox_acls/templates *
-recursive-include netbox_acls/static *
diff --git a/Makefile b/Makefile
index aa6c2f9..fb3ba16 100644
--- a/Makefile
+++ b/Makefile
@@ -70,9 +70,13 @@ start:
.PHONY: all ## Run all PLUGIN DEV targets
all: setup makemigrations migrate collectstatic initializers start
-#.PHONY: test
-#test:
-# ${VENV_PY_PATH} /opt/netbox/netbox/manage.py runserver test ${PLUGIN_NAME}
+.PHONY: rebuild ## Run PLUGIN DEV targets to rebuild
+rebuild: setup makemigrations migrate collectstatic start
+
+.PHONY: test
+test: setup
+ ${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py makemigrations ${PLUGIN_NAME} --check
+ ${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py test ${PLUGIN_NAME}
#relpatch:
# $(eval GSTATUS := $(shell git status --porcelain))
diff --git a/README.md b/README.md
index 6af94cf..286a368 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ Each Plugin Version listed below has been tested with its corresponding NetBox V
| 3.2 | 1.0.1 |
| 3.3 | 1.1.0 |
| 3.4 | 1.2.2 |
+| 3.5 | 1.3.0 |
## Installing
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..5218b1d
--- /dev/null
+++ b/TODO
@@ -0,0 +1,7 @@
+- TODO: ACL Form Bubble/ICON Extended/Standard
+- TODO: Add an Access List to an Interface Custom Fields after comments - DONE
+- TODO: ACL rules, look at last number and increment to next 10
+- TODO: Clone for ACL Interface should include device
+- TODO: Inconsistent errors for add/edit (where model is using a generic page)
+- TODO: Check Constants across codebase for consistency.
+- TODO: Test API, Forms, & Models - https://github.com/k01ek/netbox-bgp/tree/main/netbox_bgp/tests , https://github.com/DanSheps/netbox-secretstore/tree/develop/netbox_secretstore/tests & https://github.com/FlxPeters/netbox-plugin-prometheus-sd/tree/main/netbox_prometheus_sd/tests
diff --git a/configuration/configuration.py b/configuration/configuration.py
new file mode 100644
index 0000000..2cf4668
--- /dev/null
+++ b/configuration/configuration.py
@@ -0,0 +1,99 @@
+####
+## We recommend to not edit this file.
+## Create separate files to overwrite the settings.
+## See `extra.py` as an example.
+####
+
+from os import environ
+from os.path import abspath, dirname
+
+# For reference see https://netbox.readthedocs.io/en/stable/configuration/
+# Based on https://github.com/netbox-community/netbox/blob/master/netbox/netbox/configuration.example.py
+
+
+# Read secret from file
+def _read_secret(secret_name, default=None):
+ try:
+ f = open(f"/run/secrets/{secret_name}", encoding="utf-8")
+ except OSError:
+ return default
+ else:
+ with f:
+ return f.readline().strip()
+
+
+_BASE_DIR = dirname(dirname(abspath(__file__)))
+
+#########################
+# #
+# Required settings #
+# #
+#########################
+
+# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
+# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
+#
+# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
+ALLOWED_HOSTS = environ.get("ALLOWED_HOSTS", "*").split(" ")
+
+# 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": environ.get("DB_NAME", "netbox"), # Database name
+ "USER": environ.get("DB_USER", ""), # PostgreSQL username
+ "PASSWORD": _read_secret("db_password", environ.get("DB_PASSWORD", "")),
+ # PostgreSQL password
+ "HOST": environ.get("DB_HOST", "localhost"), # Database server
+ "PORT": environ.get("DB_PORT", ""), # Database port (leave blank for default)
+ "OPTIONS": {"sslmode": environ.get("DB_SSLMODE", "prefer")},
+ # Database connection SSLMODE
+ "CONN_MAX_AGE": int(environ.get("DB_CONN_MAX_AGE", "300")),
+ # Max database connection age
+ "DISABLE_SERVER_SIDE_CURSORS": environ.get(
+ "DB_DISABLE_SERVER_SIDE_CURSORS",
+ "False",
+ ).lower()
+ == "true",
+ # Disable the use of server-side cursors transaction pooling
+}
+
+# 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": environ.get("REDIS_HOST", "localhost"),
+ "PORT": int(environ.get("REDIS_PORT", 6379)),
+ "PASSWORD": _read_secret("redis_password", environ.get("REDIS_PASSWORD", "")),
+ "DATABASE": int(environ.get("REDIS_DATABASE", 0)),
+ "SSL": environ.get("REDIS_SSL", "False").lower() == "true",
+ "INSECURE_SKIP_TLS_VERIFY": environ.get(
+ "REDIS_INSECURE_SKIP_TLS_VERIFY",
+ "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": _read_secret(
+ "redis_cache_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",
+ "INSECURE_SKIP_TLS_VERIFY": environ.get(
+ "REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY",
+ environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False"),
+ ).lower()
+ == "true",
+ },
+}
+
+# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
+# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
+# symbols. NetBox will not run without this defined. For more information, see
+# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
+SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", ""))
+
+DEVELOPER = True
diff --git a/configuration/logging.py b/configuration/logging.py
new file mode 100644
index 0000000..86914ae
--- /dev/null
+++ b/configuration/logging.py
@@ -0,0 +1,11 @@
+# Remove first comment(#) on each line to implement this working logging example.
+# Add LOGLEVEL environment variable to netbox if you use this example & want a different log level.
+from os import environ
+
+# Set LOGLEVEL in netbox.env or docker-compose.overide.yml to override a logging level of INFO.
+LOGLEVEL = environ.get("LOGLEVEL", "INFO")
+
+LOGGING = {
+ "version": 1,
+ "disable_existing_loggers": True,
+}
diff --git a/configuration/plugins.py b/configuration/plugins.py
new file mode 100644
index 0000000..eb7c714
--- /dev/null
+++ b/configuration/plugins.py
@@ -0,0 +1,13 @@
+# Add your plugins and plugin settings here.
+# Of course uncomment this file out.
+
+# To learn how to build images with your required plugins
+# See https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins
+
+PLUGINS = [
+ "netbox_acls",
+]
+
+PLUGINS_CONFIG = { # type: ignore
+ "netbox_acls": {},
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..cc89ee5
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,30 @@
+---
+version: '3.4'
+
+services:
+ netbox:
+ build:
+ dockerfile: Dockerfile
+ context: .
+ args:
+ NETBOX_VARIANT: ${NETBOX_VARIANT}
+ depends_on:
+ - postgres
+ - redis
+ env_file: env/netbox.env
+ volumes:
+ - ./configuration:/etc/netbox/config:z,ro
+
+ # postgres
+ postgres:
+ image: postgres:14-alpine
+ env_file: env/postgres.env
+
+ # redis
+ redis:
+ image: redis:6-alpine
+ command:
+ - sh
+ - -c # this is to evaluate the $REDIS_PASSWORD from the env
+ - redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
+ env_file: env/redis.env
diff --git a/env/netbox.env b/env/netbox.env
new file mode 100644
index 0000000..8950d9f
--- /dev/null
+++ b/env/netbox.env
@@ -0,0 +1,23 @@
+ALLOWED_HOSTS=*
+CORS_ORIGIN_ALLOW_ALL=true
+DB_HOST=postgres
+DB_NAME=netbox
+DB_PASSWORD=J5brHrAXFLQSif0K
+DB_USER=netbox
+DEBUG=true
+ENFORCE_GLOBAL_UNIQUE=true
+LOGIN_REQUIRED=false
+GRAPHQL_ENABLED=true
+MAX_PAGE_SIZE=1000
+MEDIA_ROOT=/opt/netbox/netbox/media
+REDIS_DATABASE=0
+REDIS_HOST=redis
+REDIS_INSECURE_SKIP_TLS_VERIFY=false
+REDIS_PASSWORD=H733Kdjndks81
+SECRET_KEY=r8OwDznj!!dciP9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNjaa
+SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
+SUPERUSER_EMAIL=admin@example.com
+SUPERUSER_NAME=admin
+SUPERUSER_PASSWORD=admin
+STARTUP_SCRIPTS=false
+WEBHOOKS_ENABLED=true
diff --git a/env/postgres.env b/env/postgres.env
new file mode 100644
index 0000000..bb7b53c
--- /dev/null
+++ b/env/postgres.env
@@ -0,0 +1,3 @@
+POSTGRES_DB=netbox
+POSTGRES_PASSWORD=J5brHrAXFLQSif0K
+POSTGRES_USER=netbox
diff --git a/env/redis.env b/env/redis.env
new file mode 100644
index 0000000..44a1987
--- /dev/null
+++ b/env/redis.env
@@ -0,0 +1 @@
+REDIS_PASSWORD=H733Kdjndks81
diff --git a/netbox_acls/__init__.py b/netbox_acls/__init__.py
index fa5e548..4898b59 100644
--- a/netbox_acls/__init__.py
+++ b/netbox_acls/__init__.py
@@ -17,8 +17,8 @@ class NetBoxACLsConfig(PluginConfig):
version = __version__
description = "Manage simple ACLs in NetBox"
base_url = "access-lists"
- min_version = "3.4.0"
- max_version = "3.4.99"
+ min_version = "3.5.0"
+ max_version = "3.5.99"
config = NetBoxACLsConfig
diff --git a/netbox_acls/api/serializers.py b/netbox_acls/api/serializers.py
index b84a0bd..241134e 100644
--- a/netbox_acls/api/serializers.py
+++ b/netbox_acls/api/serializers.py
@@ -4,8 +4,7 @@ while Django itself handles the database abstraction.
"""
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ObjectDoesNotExist
-from drf_yasg.utils import swagger_serializer_method
+from drf_spectacular.utils import extend_schema_field
from ipam.api.serializers import NestedPrefixSerializer
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer
@@ -32,13 +31,9 @@ __all__ = [
# Sets a standard error message for ACL rules with an action of remark, but no remark set.
error_message_no_remark = "Action is set to remark, you MUST add a remark."
# Sets a standard error message for ACL rules with an action of remark, but no source_prefix is set.
-error_message_action_remark_source_prefix_set = (
- "Action is set to remark, Source Prefix CANNOT be set."
-)
+error_message_action_remark_source_prefix_set = "Action is set to remark, Source Prefix CANNOT be set."
# Sets a standard error message for ACL rules with an action not set to remark, but no remark is set.
-error_message_remark_without_action_remark = (
- "CANNOT set remark unless action is set to remark."
-)
+error_message_remark_without_action_remark = "CANNOT set remark unless action is set to remark."
# Sets a standard error message for ACL rules no associated to an ACL of the same type.
error_message_acl_type = "Provided parent Access List is not of right type."
@@ -81,7 +76,7 @@ class AccessListSerializer(NetBoxModelSerializer):
"rule_count",
)
- @swagger_serializer_method(serializer_or_field=serializers.DictField)
+ @extend_schema_field(serializers.DictField())
def get_assigned_object(self, obj):
serializer = get_serializer_for_model(
obj.assigned_object,
@@ -98,27 +93,8 @@ class AccessListSerializer(NetBoxModelSerializer):
"""
error_message = {}
- # Check that the GFK object is valid.
- if "assigned_object_type" in data and "assigned_object_id" in data:
- # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed.
- try:
- assigned_object = data[ # noqa: F841
- "assigned_object_type"
- ].get_object_for_this_type(
- id=data["assigned_object_id"],
- )
- except ObjectDoesNotExist:
- # Sets a standard error message for invalid GFK
- error_message_invalid_gfk = f"Invalid assigned_object {data['assigned_object_type']} ID {data['assigned_object_id']}"
- error_message["assigned_object_type"] = [error_message_invalid_gfk]
- error_message["assigned_object_id"] = [error_message_invalid_gfk]
-
# Check if Access List has no existing rules before change the Access List's type.
- if (
- self.instance
- and self.instance.type != data.get("type")
- and self.instance.rule_count > 0
- ):
+ if self.instance and self.instance.type != data.get("type") and self.instance.rule_count > 0:
error_message["type"] = [
"This ACL has ACL rules associated, CANNOT change ACL type.",
]
@@ -164,7 +140,7 @@ class ACLInterfaceAssignmentSerializer(NetBoxModelSerializer):
"last_updated",
)
- @swagger_serializer_method(serializer_or_field=serializers.DictField)
+ @extend_schema_field(serializers.DictField())
def get_assigned_object(self, obj):
serializer = get_serializer_for_model(
obj.assigned_object,
@@ -182,40 +158,15 @@ class ACLInterfaceAssignmentSerializer(NetBoxModelSerializer):
error_message = {}
acl_host = data["access_list"].assigned_object
- # Check that the GFK object is valid.
- if "assigned_object_type" in data and "assigned_object_id" in data:
- # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed.
- try:
- assigned_object = data[ # noqa: F841
- "assigned_object_type"
- ].get_object_for_this_type(
- id=data["assigned_object_id"],
- )
- except ObjectDoesNotExist:
- # Sets a standard error message for invalid GFK
- error_message_invalid_gfk = f"Invalid assigned_object {data['assigned_object_type']} ID {data['assigned_object_id']}"
- error_message["assigned_object_type"] = [error_message_invalid_gfk]
- error_message["assigned_object_id"] = [error_message_invalid_gfk]
-
if data["assigned_object_type"].model == "interface":
- interface_host = (
- data["assigned_object_type"]
- .get_object_for_this_type(id=data["assigned_object_id"])
- .device
- )
+ interface_host = data["assigned_object_type"].get_object_for_this_type(id=data["assigned_object_id"]).device
elif data["assigned_object_type"].model == "vminterface":
- interface_host = (
- data["assigned_object_type"]
- .get_object_for_this_type(id=data["assigned_object_id"])
- .virtual_machine
- )
+ interface_host = data["assigned_object_type"].get_object_for_this_type(id=data["assigned_object_id"]).virtual_machine
else:
interface_host = None
# Check that the associated interface's parent host has the selected ACL defined.
if acl_host != interface_host:
- error_acl_not_assigned_to_host = (
- "Access List not present on the selected interface's host."
- )
+ error_acl_not_assigned_to_host = "Access List not present on the selected interface's host."
error_message["access_list"] = [error_acl_not_assigned_to_host]
error_message["assigned_object_id"] = [error_acl_not_assigned_to_host]
diff --git a/netbox_acls/constants.py b/netbox_acls/constants.py
index 8d73006..468845c 100644
--- a/netbox_acls/constants.py
+++ b/netbox_acls/constants.py
@@ -10,6 +10,5 @@ ACL_HOST_ASSIGNMENT_MODELS = Q(
)
ACL_INTERFACE_ASSIGNMENT_MODELS = Q(
- Q(app_label="dcim", model="interface")
- | Q(app_label="virtualization", model="vminterface"),
+ Q(app_label="dcim", model="interface") | Q(app_label="virtualization", model="vminterface"),
)
diff --git a/netbox_acls/forms/bulk_edit.py b/netbox_acls/forms/bulk_edit.py
index 81b4dd8..7ac0ab6 100644
--- a/netbox_acls/forms/bulk_edit.py
+++ b/netbox_acls/forms/bulk_edit.py
@@ -7,11 +7,11 @@ Draft for a possible BulkEditForm, but may not be worth wile.
# from django.core.exceptions import ValidationError
# from django.utils.safestring import mark_safe
# from netbox.forms import NetBoxModelBulkEditForm
-# from utilities.forms import (
+# from utilities.forms.utils import add_blank_choice
+# from utilities.forms.fields import (
# ChoiceField,
# DynamicModelChoiceField,
# StaticSelect,
-# add_blank_choice,
# )
# from virtualization.models import VirtualMachine
diff --git a/netbox_acls/forms/filtersets.py b/netbox_acls/forms/filtersets.py
index f227e3f..37f20df 100644
--- a/netbox_acls/forms/filtersets.py
+++ b/netbox_acls/forms/filtersets.py
@@ -6,13 +6,13 @@ from dcim.models import Device, Interface, Region, Site, SiteGroup, VirtualChass
from django import forms
from ipam.models import Prefix
from netbox.forms import NetBoxModelFilterSetForm
-from utilities.forms import (
+from utilities.forms.fields import (
ChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
TagFilterField,
- add_blank_choice,
)
+from utilities.forms.utils import add_blank_choice
from virtualization.models import VirtualMachine, VMInterface
from ..choices import (
diff --git a/netbox_acls/forms/models.py b/netbox_acls/forms/models.py
index 4dc1738..0c95ef5 100644
--- a/netbox_acls/forms/models.py
+++ b/netbox_acls/forms/models.py
@@ -8,7 +8,7 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import mark_safe
from ipam.models import Prefix
from netbox.forms import NetBoxModelForm
-from utilities.forms import CommentField, DynamicModelChoiceField
+from utilities.forms.fields import CommentField, DynamicModelChoiceField
from virtualization.models import (
Cluster,
ClusterGroup,
@@ -39,20 +39,14 @@ help_text_acl_rule_logic = mark_safe(
# Sets a standard help_text value to be used by the various classes for acl action
help_text_acl_action = "Action the rule will take (remark, deny, or allow)."
# Sets a standard help_text value to be used by the various classes for acl index
-help_text_acl_rule_index = (
- "Determines the order of the rule in the ACL processing. AKA Sequence Number."
-)
+help_text_acl_rule_index = "Determines the order of the rule in the ACL processing. AKA Sequence Number."
# Sets a standard error message for ACL rules with an action of remark, but no remark set.
error_message_no_remark = "Action is set to remark, you MUST add a remark."
# Sets a standard error message for ACL rules with an action of remark, but no source_prefix is set.
-error_message_action_remark_source_prefix_set = (
- "Action is set to remark, Source Prefix CANNOT be set."
-)
+error_message_action_remark_source_prefix_set = "Action is set to remark, Source Prefix CANNOT be set."
# Sets a standard error message for ACL rules with an action not set to remark, but no remark is set.
-error_message_remark_without_action_remark = (
- "CANNOT set remark unless action is set to remark."
-)
+error_message_remark_without_action_remark = "CANNOT set remark unless action is set to remark."
class AccessListForm(NetBoxModelForm):
@@ -65,19 +59,20 @@ class AccessListForm(NetBoxModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
+ initial_params={
+ "sites": "$site",
+ },
)
site_group = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label="Site Group",
+ initial_params={"sites": "$site"},
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False,
- query_params={
- "region_id": "$region",
- "group_id": "$site_group",
- },
+ query_params={"region_id": "$region", "group_id": "$site_group"},
)
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
@@ -101,32 +96,24 @@ class AccessListForm(NetBoxModelForm):
queryset=ClusterType.objects.all(),
required=False,
)
-
cluster_group = DynamicModelChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
- query_params={
- "type_id": "$cluster_type",
- },
+ query_params={"type_id": "$cluster_type"},
)
-
cluster = DynamicModelChoiceField(
queryset=Cluster.objects.all(),
required=False,
- query_params={
- "type_id": "$cluster_type",
- "group_id": "$cluster_group",
- },
+ query_params={"type_id": "$cluster_type", "group_id": "$cluster_group"},
)
virtual_machine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
- label="Virtual Machine",
query_params={
+ "cluster_id": "$cluster",
"cluster_type_id": "$cluster_type",
"cluster_group_id": "$cluster_group",
- "cluster_id": "$cluster",
},
)
@@ -156,19 +143,32 @@ class AccessListForm(NetBoxModelForm):
}
def __init__(self, *args, **kwargs):
-
# Initialize helper selectors
instance = kwargs.get("instance")
initial = kwargs.get("initial", {}).copy()
if instance:
- if type(instance.assigned_object) is Device:
+ if isinstance(instance.assigned_object, Device):
initial["device"] = instance.assigned_object
- elif type(instance.assigned_object) is VirtualChassis:
- initial["virtual_chassis"] = instance.assigned_object
- elif type(instance.assigned_object) is VirtualMachine:
- initial["virtual_machine"] = instance.assigned_object
- kwargs["initial"] = initial
+ if instance.assigned_object.site:
+ initial["site"] = instance.assigned_object.site
+ if instance.assigned_object.site.group:
+ initial["site_group"] = instance.assigned_object.site.group
+ if instance.assigned_object.site.region:
+ initial["region"] = instance.assigned_object.site.region
+ elif isinstance(instance.assigned_object, VirtualMachine):
+ initial["virtual_machine"] = instance.assigned_object
+ if instance.assigned_object.cluster:
+ initial["cluster"] = instance.assigned_object.cluster
+ if instance.assigned_object.cluster.group:
+ initial["cluster_group"] = instance.assigned_object.cluster.group
+
+ if instance.assigned_object.cluster.type:
+ initial["cluster_type"] = instance.assigned_object.cluster.type
+ elif isinstance(instance.assigned_object, VirtualChassis):
+ initial["virtual_chassis"] = instance.assigned_object
+
+ kwargs["initial"] = initial
super().__init__(*args, **kwargs)
def clean(self):
@@ -190,13 +190,9 @@ class AccessListForm(NetBoxModelForm):
virtual_machine = cleaned_data.get("virtual_machine")
# Check if more than one host type selected.
- if (
- (device and virtual_chassis)
- or (device and virtual_machine)
- or (virtual_chassis and virtual_machine)
- ):
+ if (device and virtual_chassis) or (device and virtual_machine) or (virtual_chassis and virtual_machine):
raise forms.ValidationError(
- "Access Lists must be assigned to one host (either a device, virtual chassis or virtual machine) at a time.",
+ "Access Lists must be assigned to one host at a time. Either a device, virtual chassis or virtual machine."
)
# Check if no hosts selected.
if not device and not virtual_chassis and not virtual_machine:
@@ -221,28 +217,20 @@ class AccessListForm(NetBoxModelForm):
).exists()
# Check if duplicate entry.
- if (
- "name" in self.changed_data or host_type in self.changed_data
- ) and existing_acls:
- error_same_acl_name = (
- "An ACL with this name is already associated to this host."
- )
+ if ("name" in self.changed_data or host_type in self.changed_data) and existing_acls:
+ error_same_acl_name = "An ACL with this name is already associated to this host."
error_message |= {
host_type: [error_same_acl_name],
"name": [error_same_acl_name],
}
- if self.instance.pk:
- # Check if Access List has no existing rules before change the Access List's type.
- if (
- acl_type == ACLTypeChoices.TYPE_EXTENDED
- and self.instance.aclstandardrules.exists()
- ) or (
- acl_type == ACLTypeChoices.TYPE_STANDARD
- and self.instance.aclextendedrules.exists()
- ):
- error_message["type"] = [
- "This ACL has ACL rules associated, CANNOT change ACL type.",
- ]
+ # Check if Access List has no existing rules before change the Access List's type.
+ if self.instance.pk and (
+ (acl_type == ACLTypeChoices.TYPE_EXTENDED and self.instance.aclstandardrules.exists())
+ or (acl_type == ACLTypeChoices.TYPE_STANDARD and self.instance.aclextendedrules.exists())
+ ):
+ error_message["type"] = [
+ "This ACL has ACL rules associated, CANNOT change ACL type.",
+ ]
if error_message:
raise forms.ValidationError(error_message)
@@ -252,9 +240,7 @@ class AccessListForm(NetBoxModelForm):
def save(self, *args, **kwargs):
# Set assigned object
self.instance.assigned_object = (
- self.cleaned_data.get("device")
- or self.cleaned_data.get("virtual_chassis")
- or self.cleaned_data.get("virtual_machine")
+ self.cleaned_data.get("device") or self.cleaned_data.get("virtual_chassis") or self.cleaned_data.get("virtual_machine")
)
return super().save(*args, **kwargs)
@@ -315,7 +301,6 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm):
comments = CommentField()
def __init__(self, *args, **kwargs):
-
# Initialize helper selectors
instance = kwargs.get("instance")
initial = kwargs.get("initial", {}).copy()
@@ -367,15 +352,15 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm):
# Check if both interface and vminterface are set.
if interface and vminterface:
- error_too_many_interfaces = "Access Lists must be assigned to one type of interface at a time (VM interface or physical interface)"
+ error_too_many_interfaces = (
+ "Access Lists must be assigned to one type of interface at a time (VM interface or physical interface)"
+ )
error_message |= {
"interface": [error_too_many_interfaces],
"vminterface": [error_too_many_interfaces],
}
elif not (interface or vminterface):
- error_no_interface = (
- "An Access List assignment but specify an Interface or VM Interface."
- )
+ error_no_interface = "An Access List assignment but specify an Interface or VM Interface."
error_message |= {
"interface": [error_no_interface],
"vminterface": [error_no_interface],
@@ -401,9 +386,7 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm):
# Check that an interface's parent device/virtual_machine is assigned to the Access List.
if access_list_host != host:
- error_acl_not_assigned_to_host = (
- "Access List not present on selected host."
- )
+ error_acl_not_assigned_to_host = "Access List not present on selected host."
error_message |= {
"access_list": [error_acl_not_assigned_to_host],
assigned_object_type: [error_acl_not_assigned_to_host],
@@ -428,9 +411,7 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm):
assigned_object_type=assigned_object_type_id,
direction=direction,
).exists():
- error_interface_already_assigned = (
- "Interfaces can only have 1 Access List assigned in each direction."
- )
+ error_interface_already_assigned = "Interfaces can only have 1 Access List assigned in each direction."
error_message |= {
"direction": [error_interface_already_assigned],
assigned_object_type: [error_interface_already_assigned],
diff --git a/netbox_acls/graphql/__init__.py b/netbox_acls/graphql/__init__.py
new file mode 100644
index 0000000..dd2a695
--- /dev/null
+++ b/netbox_acls/graphql/__init__.py
@@ -0,0 +1,2 @@
+from .schema import *
+from .types import *
diff --git a/netbox_acls/graphql/schema.py b/netbox_acls/graphql/schema.py
new file mode 100644
index 0000000..0dc2dd2
--- /dev/null
+++ b/netbox_acls/graphql/schema.py
@@ -0,0 +1,22 @@
+from graphene import ObjectType
+from netbox.graphql.fields import ObjectField, ObjectListField
+
+from .types import *
+
+
+class Query(ObjectType):
+ """
+ Defines the queries available to this plugin via the graphql api.
+ """
+
+ access_list = ObjectField(AccessListType)
+ access_list_list = ObjectListField(AccessListType)
+
+ acl_extended_rule = ObjectField(ACLExtendedRuleType)
+ acl_extended_rule_list = ObjectListField(ACLExtendedRuleType)
+
+ acl_standard_rule = ObjectField(ACLStandardRuleType)
+ acl_standard_rule_list = ObjectListField(ACLStandardRuleType)
+
+
+schema = Query
diff --git a/netbox_acls/graphql.py b/netbox_acls/graphql/types.py
similarity index 73%
rename from netbox_acls/graphql.py
rename to netbox_acls/graphql/types.py
index 8de0dd2..f43774c 100644
--- a/netbox_acls/graphql.py
+++ b/netbox_acls/graphql/types.py
@@ -2,11 +2,9 @@
Define the object types and queries availble via the graphql api.
"""
-from graphene import ObjectType
-from netbox.graphql.fields import ObjectField, ObjectListField
from netbox.graphql.types import NetBoxObjectType
-from . import filtersets, models
+from .. import filtersets, models
__all__ = (
"AccessListType",
@@ -15,10 +13,6 @@ __all__ = (
"ACLStandardRuleType",
)
-#
-# Object types
-#
-
class AccessListType(NetBoxObjectType):
"""
@@ -78,26 +72,3 @@ class ACLStandardRuleType(NetBoxObjectType):
model = models.ACLStandardRule
fields = "__all__"
filterset_class = filtersets.ACLStandardRuleFilterSet
-
-
-#
-# Queries
-#
-
-
-class Query(ObjectType):
- """
- Defines the queries availible to this plugin via the graphql api.
- """
-
- access_list = ObjectField(AccessListType)
- access_list_list = ObjectListField(AccessListType)
-
- acl_extended_rule = ObjectField(ACLExtendedRuleType)
- acl_extended_rule_list = ObjectListField(ACLExtendedRuleType)
-
- acl_standard_rule = ObjectField(ACLStandardRuleType)
- acl_standard_rule_list = ObjectListField(ACLStandardRuleType)
-
-
-schema = Query
diff --git a/netbox_acls/migrations/0001_initial.py b/netbox_acls/migrations/0001_initial.py
index 9a530ce..e41a675 100644
--- a/netbox_acls/migrations/0001_initial.py
+++ b/netbox_acls/migrations/0001_initial.py
@@ -282,7 +282,9 @@ class Migration(migrations.Migration):
},
),
# migrations.AddConstraint(
- # model_name='accesslist',
- # constraint=models.UniqueConstraint(fields=('assigned_object_type', 'assigned_object_id'), name='accesslist_assigned_object'),
+ # model_name="accesslist",
+ # constraint=models.UniqueConstraint(
+ # fields=("assigned_object_type", "assigned_object_id"), name="accesslist_assigned_object"
+ # ),
# ),
]
diff --git a/netbox_acls/migrations/0002_alter_accesslist_options_and_more.py b/netbox_acls/migrations/0002_alter_accesslist_options_and_more.py
index ea5ab86..902248f 100644
--- a/netbox_acls/migrations/0002_alter_accesslist_options_and_more.py
+++ b/netbox_acls/migrations/0002_alter_accesslist_options_and_more.py
@@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
-
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("netbox_acls", "0001_initial"),
diff --git a/netbox_acls/migrations/0003_netbox_acls.py b/netbox_acls/migrations/0003_netbox_acls.py
index ccfcfbe..0af5aef 100644
--- a/netbox_acls/migrations/0003_netbox_acls.py
+++ b/netbox_acls/migrations/0003_netbox_acls.py
@@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
-
dependencies = [
("netbox_acls", "0002_alter_accesslist_options_and_more"),
]
@@ -15,28 +14,36 @@ class Migration(migrations.Migration):
model_name="accesslist",
name="custom_field_data",
field=models.JSONField(
- blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder
+ blank=True,
+ default=dict,
+ encoder=utilities.json.CustomFieldJSONEncoder,
),
),
migrations.AlterField(
model_name="aclextendedrule",
name="custom_field_data",
field=models.JSONField(
- blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder
+ blank=True,
+ default=dict,
+ encoder=utilities.json.CustomFieldJSONEncoder,
),
),
migrations.AlterField(
model_name="aclinterfaceassignment",
name="custom_field_data",
field=models.JSONField(
- blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder
+ blank=True,
+ default=dict,
+ encoder=utilities.json.CustomFieldJSONEncoder,
),
),
migrations.AlterField(
model_name="aclstandardrule",
name="custom_field_data",
field=models.JSONField(
- blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder
+ blank=True,
+ default=dict,
+ encoder=utilities.json.CustomFieldJSONEncoder,
),
),
]
diff --git a/netbox_acls/templates/netbox_acls/accesslist.html b/netbox_acls/templates/netbox_acls/accesslist.html
index 86a8083..bd4f003 100644
--- a/netbox_acls/templates/netbox_acls/accesslist.html
+++ b/netbox_acls/templates/netbox_acls/accesslist.html
@@ -20,6 +20,7 @@
+ Access List
| Type |
{{ object.get_type_display }} |
diff --git a/netbox_acls/templates/netbox_acls/aclextendedrule.html b/netbox_acls/templates/netbox_acls/aclextendedrule.html
index 47c4ad3..d4abbdf 100644
--- a/netbox_acls/templates/netbox_acls/aclextendedrule.html
+++ b/netbox_acls/templates/netbox_acls/aclextendedrule.html
@@ -7,6 +7,7 @@
+ ACL Extended Rule
| Access List |
@@ -32,6 +33,7 @@
+ Details
| Remark |
{{ object.get_remark_display|placeholder }} |
diff --git a/netbox_acls/templates/netbox_acls/aclinterfaceassignment.html b/netbox_acls/templates/netbox_acls/aclinterfaceassignment.html
index 4cabeb0..606f99e 100644
--- a/netbox_acls/templates/netbox_acls/aclinterfaceassignment.html
+++ b/netbox_acls/templates/netbox_acls/aclinterfaceassignment.html
@@ -8,6 +8,7 @@
+ Host
| Host |
diff --git a/netbox_acls/templates/netbox_acls/aclstandardrule.html b/netbox_acls/templates/netbox_acls/aclstandardrule.html
index 4a83b3c..c8e2562 100644
--- a/netbox_acls/templates/netbox_acls/aclstandardrule.html
+++ b/netbox_acls/templates/netbox_acls/aclstandardrule.html
@@ -7,6 +7,7 @@
+ ACL Standard Rule
| Access List |
@@ -32,6 +33,7 @@
+ Details
| Remark |
{{ object.get_remark_display|placeholder }} |
diff --git a/netbox_acls/tests/__init__.py b/netbox_acls/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/netbox_acls/tests/test_api.py b/netbox_acls/tests/test_api.py
new file mode 100644
index 0000000..207f1dd
--- /dev/null
+++ b/netbox_acls/tests/test_api.py
@@ -0,0 +1,97 @@
+from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
+from django.contrib.contenttypes.models import ContentType
+from django.urls import reverse
+from rest_framework import status
+from utilities.testing import APITestCase, APIViewTestCases
+
+from netbox_acls.choices import *
+from netbox_acls.models import *
+
+
+class AppTest(APITestCase):
+ def test_root(self):
+ url = reverse("plugins-api:netbox_acls-api:api-root")
+ response = self.client.get(f"{url}?format=api", **self.header)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+
+class ACLTestCase(
+ APIViewTestCases.APIViewTestCase,
+):
+ """Test the AccessList Test"""
+
+ model = AccessList
+ view_namespace = "plugins-api:netbox_acls"
+ brief_fields = ["display", "id", "name", "url"]
+
+ @classmethod
+ def setUpTestData(cls):
+ site = Site.objects.create(name="Site 1", slug="site-1")
+ manufacturer = Manufacturer.objects.create(
+ name="Manufacturer 1",
+ slug="manufacturer-1",
+ )
+ devicetype = DeviceType.objects.create(
+ manufacturer=manufacturer,
+ model="Device Type 1",
+ )
+ devicerole = DeviceRole.objects.create(
+ name="Device Role 1",
+ slug="device-role-1",
+ )
+ device = Device.objects.create(
+ name="Device 1",
+ site=site,
+ device_type=devicetype,
+ device_role=devicerole,
+ )
+
+ access_lists = (
+ AccessList(
+ name="testacl1",
+ assigned_object_type=ContentType.objects.get_for_model(Device),
+ assigned_object_id=device.id,
+ type=ACLTypeChoices.TYPE_STANDARD,
+ default_action=ACLActionChoices.ACTION_DENY,
+ ),
+ AccessList(
+ name="testacl2",
+ assigned_object_type=ContentType.objects.get_for_model(Device),
+ assigned_object_id=device.id,
+ type=ACLTypeChoices.TYPE_STANDARD,
+ default_action=ACLActionChoices.ACTION_DENY,
+ ),
+ AccessList(
+ name="testacl3",
+ assigned_object_type=ContentType.objects.get_for_model(Device),
+ assigned_object_id=device.id,
+ type=ACLTypeChoices.TYPE_STANDARD,
+ default_action=ACLActionChoices.ACTION_DENY,
+ ),
+ )
+ AccessList.objects.bulk_create(access_lists)
+
+ cls.create_data = [
+ {
+ "name": "testacl4",
+ "assigned_object_type": "dcim.device",
+ "assigned_object_id": device.id,
+ "type": ACLTypeChoices.TYPE_STANDARD,
+ "default_action": ACLActionChoices.ACTION_DENY,
+ },
+ {
+ "name": "testacl5",
+ "assigned_object_type": "dcim.device",
+ "assigned_object_id": device.id,
+ "type": ACLTypeChoices.TYPE_EXTENDED,
+ "default_action": ACLActionChoices.ACTION_DENY,
+ },
+ {
+ "name": "testacl6",
+ "assigned_object_type": "dcim.device",
+ "assigned_object_id": device.id,
+ "type": ACLTypeChoices.TYPE_STANDARD,
+ "default_action": ACLActionChoices.ACTION_DENY,
+ },
+ ]
diff --git a/netbox_acls/urls.py b/netbox_acls/urls.py
index c5f07a1..d7624a1 100644
--- a/netbox_acls/urls.py
+++ b/netbox_acls/urls.py
@@ -5,7 +5,7 @@ Map Views to URLs.
from django.urls import include, path
from utilities.urls import get_model_urls
-from . import models, views
+from . import views
urlpatterns = (
# Access Lists
@@ -47,7 +47,11 @@ urlpatterns = (
views.ACLInterfaceAssignmentEditView.as_view(),
name="aclinterfaceassignment_add",
),
- # path('interface-assignments/edit/', views.ACLInterfaceAssignmentBulkEditView.as_view(), name='aclinterfaceassignment_bulk_edit'),
+ # path(
+ # "interface-assignments/edit/",
+ # views.ACLInterfaceAssignmentBulkEditView.as_view(),
+ # name="aclinterfaceassignment_bulk_edit"
+ # ),
path(
"interface-assignments/delete/",
views.ACLInterfaceAssignmentBulkDeleteView.as_view(),
diff --git a/netbox_acls/version.py b/netbox_acls/version.py
index bc86c94..67bc602 100644
--- a/netbox_acls/version.py
+++ b/netbox_acls/version.py
@@ -1 +1 @@
-__version__ = "1.2.2"
+__version__ = "1.3.0"
diff --git a/netbox_acls/views.py b/netbox_acls/views.py
index d986945..9f1fa81 100644
--- a/netbox_acls/views.py
+++ b/netbox_acls/views.py
@@ -50,7 +50,8 @@ class AccessListView(generic.ObjectView):
def get_extra_context(self, request, instance):
"""
- Depending on the Access List type, the list view will return the required ACL Rule using the previous defined tables in tables.py.
+ Depending on the Access List type, the list view will return
+ the required ACL Rule using the previous defined tables in tables.py.
"""
if instance.type == choices.ACLTypeChoices.TYPE_EXTENDED:
@@ -227,8 +228,7 @@ class ACLInterfaceAssignmentEditView(generic.ObjectEditView):
"""
return {
- "access_list": request.GET.get("access_list")
- or request.POST.get("access_list"),
+ "access_list": request.GET.get("access_list") or request.POST.get("access_list"),
"direction": request.GET.get("direction") or request.POST.get("direction"),
}
@@ -360,8 +360,7 @@ class ACLStandardRuleEditView(generic.ObjectEditView):
"""
return {
- "access_list": request.GET.get("access_list")
- or request.POST.get("access_list"),
+ "access_list": request.GET.get("access_list") or request.POST.get("access_list"),
}
@@ -443,8 +442,7 @@ class ACLExtendedRuleEditView(generic.ObjectEditView):
"""
return {
- "access_list": request.GET.get("access_list")
- or request.POST.get("access_list"),
+ "access_list": request.GET.get("access_list") or request.POST.get("access_list"),
}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ab061a4
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,23 @@
+
+[tool.black]
+line-length = 140
+
+[tool.isort]
+profile = "black"
+include_trailing_comma = true
+multi_line_output = 3
+
+[tool.pylint]
+max-line-length = 140
+
+[tool.pyright]
+include = ["netbox_secrets"]
+exclude = [
+ "**/node_modules",
+ "**/__pycache__",
+]
+reportMissingImports = true
+reportMissingTypeStubs = false
+
+[tool.ruff]
+line-length = 140
diff --git a/setup.py b/setup.py
index 1b55f67..c8faa97 100644
--- a/setup.py
+++ b/setup.py
@@ -1,20 +1,30 @@
+"""
+Configuration for setuptools.
+"""
import codecs
import os.path
from setuptools import find_packages, setup
-with open("README.md", encoding="utf-8") as fh:
- long_description = fh.read()
+script_dir = os.path.abspath(os.path.dirname(__file__))
+
+with open(os.path.join(script_dir, "README.md"), encoding="utf-8") as fh:
+ long_description = fh.read().replace("(docs/img/", "(https://raw.githubusercontent.com/ryanmerolle/netbox-acls/release/docs/img/")
-def read(rel_path):
- here = os.path.abspath(os.path.dirname(__file__))
- with codecs.open(os.path.join(here, rel_path), "r") as fp:
+def read(relative_path):
+ """
+ Read a file and return its contents.
+ """
+ with codecs.open(os.path.join(script_dir, relative_path), "r") as fp:
return fp.read()
-def get_version(rel_path):
- for line in read(rel_path).splitlines():
+def get_version(relative_path):
+ """
+ Extract the version number from a file without importing it.
+ """
+ for line in read(relative_path).splitlines():
if not line.startswith("__version__"):
raise RuntimeError("Unable to find version string.")
delim = '"' if '"' in line else "'"
@@ -33,7 +43,19 @@ setup(
include_package_data=True,
zip_safe=False,
classifiers=[
- "Framework :: Django",
+ "Development Status :: 5 - Production/Stable",
+ "Natural Language :: English",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python",
"Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Intended Audience :: System Administrators",
+ "Intended Audience :: Telecommunications Industry",
+ "Framework :: Django",
+ "Topic :: System :: Networking",
+ "Topic :: Internet",
],
)
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..5fc2e06
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+# Runs the NetBox plugin unit tests
+# Usage:
+# ./test.sh latest
+# ./test.sh v2.9.7
+# ./test.sh develop-2.10
+
+# exit when a command exits with an exit code != 0
+set -e
+
+# NETBOX_VARIANT is used by `Dockerfile` to determine the tag
+NETBOX_VARIANT="${1-latest}"
+
+# The docker compose command to use
+doco="docker compose --file docker-compose.yml"
+
+test_netbox_unit_tests() {
+ echo "⏱ Running NetBox Unit Tests"
+ $doco run --rm netbox python manage.py makemigrations netbox_acls --check
+ $doco run --rm netbox python manage.py test netbox_acls
+}
+
+test_cleanup() {
+ echo "💣 Cleaning Up"
+ $doco down -v
+ $doco rm -fsv
+ docker image rm docker.io/library/netbox-acls-netbox || echo ''
+}
+
+export NETBOX_VARIANT=${NETBOX_VARIANT}
+
+echo "🐳🐳🐳 Start testing '${NETBOX_VARIANT}'"
+
+# Make sure the cleanup script is executed
+trap test_cleanup EXIT ERR
+test_netbox_unit_tests
+
+echo "🐳🐳🐳 Done testing '${NETBOX_VARIANT}'"
| | |