mirror of
https://github.com/lucaspalomodevelop/netbox-acls.git
synced 2026-03-12 23:27:23 +00:00
commit
175f8c369f
@ -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
|
||||
|
||||
|
||||
@ -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(" ")),
|
||||
)
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
2
.devcontainer/env/netbox.env
vendored
2
.devcontainer/env/netbox.env
vendored
@ -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
|
||||
|
||||
@ -10,5 +10,7 @@ pycodestyle
|
||||
pydocstyle
|
||||
pylint
|
||||
pylint-django
|
||||
ruff
|
||||
sourcery-analytics
|
||||
wily
|
||||
yapf
|
||||
|
||||
2
.flake8
2
.flake8
@ -1,3 +1,3 @@
|
||||
[flake8]
|
||||
max-line-length = 160
|
||||
max-line-length = 140
|
||||
extend-ignore = E203
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -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
|
||||
|
||||
3
.github/linters/.flake8
vendored
3
.github/linters/.flake8
vendored
@ -1,3 +0,0 @@
|
||||
[flake8]
|
||||
max-line-length = 160
|
||||
extend-ignore = E203
|
||||
8
.github/linters/.isort.cfg
vendored
8
.github/linters/.isort.cfg
vendored
@ -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
|
||||
3
.github/linters/.jscpd.json
vendored
3
.github/linters/.jscpd.json
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"threshold": 10
|
||||
}
|
||||
47
.github/workflows/ci.yml
vendored
Normal file
47
.github/workflows/ci.yml
vendored
Normal file
@ -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
|
||||
30
.github/workflows/super-linter.yml
vendored
30
.github/workflows/super-linter.yml
vendored
@ -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 }}
|
||||
@ -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
|
||||
4
.jscpd.json
Normal file
4
.jscpd.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"threshold": 10,
|
||||
"ignore": ["**/migrations/**", "**/tests/**"]
|
||||
}
|
||||
@ -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
|
||||
|
||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -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
|
||||
@ -1,4 +1,3 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
recursive-include netbox_acls/templates *
|
||||
recursive-include netbox_acls/static *
|
||||
|
||||
10
Makefile
10
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))
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
7
TODO
Normal file
7
TODO
Normal file
@ -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
|
||||
99
configuration/configuration.py
Normal file
99
configuration/configuration.py
Normal file
@ -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
|
||||
11
configuration/logging.py
Normal file
11
configuration/logging.py
Normal file
@ -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,
|
||||
}
|
||||
13
configuration/plugins.py
Normal file
13
configuration/plugins.py
Normal file
@ -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": {},
|
||||
}
|
||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@ -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
|
||||
23
env/netbox.env
vendored
Normal file
23
env/netbox.env
vendored
Normal file
@ -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
|
||||
3
env/postgres.env
vendored
Normal file
3
env/postgres.env
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
POSTGRES_DB=netbox
|
||||
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
|
||||
POSTGRES_USER=netbox
|
||||
1
env/redis.env
vendored
Normal file
1
env/redis.env
vendored
Normal file
@ -0,0 +1 @@
|
||||
REDIS_PASSWORD=H733Kdjndks81
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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"),
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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],
|
||||
|
||||
2
netbox_acls/graphql/__init__.py
Normal file
2
netbox_acls/graphql/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .schema import *
|
||||
from .types import *
|
||||
22
netbox_acls/graphql/schema.py
Normal file
22
netbox_acls/graphql/schema.py
Normal file
@ -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
|
||||
@ -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
|
||||
@ -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"
|
||||
# ),
|
||||
# ),
|
||||
]
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
<h5 class="card-header">Access List</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<caption>Access List</caption>
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<td>{{ object.get_type_display }}</td>
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
<h5 class="card-header">ACL Extended Rule</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<caption>ACL Extended Rule</caption>
|
||||
<tr>
|
||||
<th scope="row">Access List</th>
|
||||
<td>
|
||||
@ -32,6 +33,7 @@
|
||||
<h5 class="card-header">Details</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<caption>Details</caption>
|
||||
<tr>
|
||||
<th scope="row">Remark</th>
|
||||
<td>{{ object.get_remark_display|placeholder }}</td>
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
<h5 class="card-header">ACL Interface Assignment</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<caption>Host</caption>
|
||||
<tr>
|
||||
<th scope="row">Host</th>
|
||||
<td>
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
<h5 class="card-header">ACL Standard Rule</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<caption>ACL Standard Rule</caption>
|
||||
<tr>
|
||||
<th scope="row">Access List</th>
|
||||
<td>
|
||||
@ -32,6 +33,7 @@
|
||||
<h5 class="card-header">Details</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<caption>Details</caption>
|
||||
<tr>
|
||||
<th scope="row">Remark</th>
|
||||
<td>{{ object.get_remark_display|placeholder }}</td>
|
||||
|
||||
0
netbox_acls/tests/__init__.py
Normal file
0
netbox_acls/tests/__init__.py
Normal file
97
netbox_acls/tests/test_api.py
Normal file
97
netbox_acls/tests/test_api.py
Normal file
@ -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,
|
||||
},
|
||||
]
|
||||
@ -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(),
|
||||
|
||||
@ -1 +1 @@
|
||||
__version__ = "1.2.2"
|
||||
__version__ = "1.3.0"
|
||||
|
||||
@ -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"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
@ -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
|
||||
38
setup.py
38
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",
|
||||
],
|
||||
)
|
||||
|
||||
38
test.sh
Executable file
38
test.sh
Executable file
@ -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}'"
|
||||
Loading…
x
Reference in New Issue
Block a user