netbox 3.6 support (#167)

netbox 3.6 support thanks to @abhi1693 & @kbelokon
This commit is contained in:
Ryan Merolle 2024-03-27 00:09:32 -04:00 committed by GitHub
parent bc17018346
commit f0b461616b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 316 additions and 333 deletions

View File

@ -1,15 +1,15 @@
ARG NETBOX_VARIANT=v3.5 ARG NETBOX_VARIANT=v3.6
FROM netboxcommunity/netbox:${NETBOX_VARIANT} FROM netboxcommunity/netbox:${NETBOX_VARIANT}
ARG NETBOX_INITIALIZERS_VARIANT=3.5.* ARG NETBOX_INITIALIZERS_VARIANT=3.6.*
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
# Install APT packages # Install APT packages
# hadolint ignore=DL3008 # hadolint ignore=DL3008
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends curl git make openssh-client python3.10-dev sudo wget zsh \ && apt-get -y install --no-install-recommends curl git make openssh-client python3.11-dev sudo wget zsh \
&& apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*
# Install development & ide dependencies # Install development & ide dependencies
@ -17,12 +17,11 @@ COPY requirements-dev.txt /tmp/pip-tmp/
RUN /opt/netbox/venv/bin/python3 -m pip install --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements-dev.txt \ RUN /opt/netbox/venv/bin/python3 -m pip install --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements-dev.txt \
&& rm -rf /tmp/* && rm -rf /tmp/*
ARG USERNAME=vscode ARG USERNAME=ubuntu
ARG USER_UID=1000 ARG USER_UID=1000
ARG USER_GID=$USER_UID ARG USER_GID=$USER_UID
RUN useradd -l -md /home/vscode -s /usr/bin/zsh -u $USER_UID $USERNAME \ RUN usermod -aG sudo $USERNAME \
&& usermod -aG sudo $USERNAME \
&& echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \
&& mkdir /opt/netbox/netbox/netbox-acls \ && mkdir /opt/netbox/netbox/netbox-acls \
&& chown $USERNAME:$USERNAME /opt/netbox /etc/netbox /opt/unit -R && chown $USERNAME:$USERNAME /opt/netbox /etc/netbox /opt/unit -R

View File

@ -1,19 +1,46 @@
# Based on https://github.com/netbox-community/netbox-docker/blob/release/configuration/configuration.py ####
## We recommend to not edit this file.
## Create separate files to overwrite the settings.
## See `extra.py` as an example.
####
import re import re
from os import environ from os import environ
from os.path import abspath, dirname, join from os.path import abspath, dirname, join
from typing import Any, Callable, Tuple
# For reference see https://docs.netbox.dev/en/stable/configuration/
# Based on https://github.com/netbox-community/netbox/blob/develop/netbox/netbox/configuration_example.py
###
# NetBox-Docker Helper functions
###
# Read secret from file # Read secret from file
def _read_secret(secret_name, default=None): def _read_secret(secret_name: str, default: str | None = None) -> str | None:
try: try:
f = open(f"/run/secrets/{secret_name}", encoding="utf-8") f = open(f'/run/secrets/{secret_name}', 'r', encoding='utf-8')
except OSError: except EnvironmentError:
return default return default
else: else:
with f: with f:
return f.readline().strip() return f.readline().strip()
# If the `map_fn` isn't defined, then the value that is read from the environment (or the default value if not found) is returned.
# If the `map_fn` is defined, then `map_fn` is invoked and the value (that was read from the environment or the default value if not found)
# is passed to it as a parameter. The value returned from `map_fn` is then the return value of this function.
# The `map_fn` is not invoked, if the value (that was read from the environment or the default value if not found) is None.
def _environ_get_and_map(variable_name: str, default: str | None = None, map_fn: Callable[[str], Any | None] = None) -> Any | None:
env_value = environ.get(variable_name, default)
if env_value is None:
return env_value
return env_value if not map_fn else map_fn(env_value)
_AS_BOOL = lambda value : value.lower() == 'true'
_AS_INT = lambda value : int(value)
_AS_LIST = lambda value : list(filter(None, value.split(' ')))
_BASE_DIR = dirname(dirname(abspath(__file__))) _BASE_DIR = dirname(dirname(abspath(__file__)))
@ -27,26 +54,25 @@ _BASE_DIR = dirname(dirname(abspath(__file__)))
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. # 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'] # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
ALLOWED_HOSTS = environ.get("ALLOWED_HOSTS", "*").split(" ") ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split(' ')
# ensure that '*' or 'localhost' is always in ALLOWED_HOSTS (needed for health checks)
if '*' not in ALLOWED_HOSTS and 'localhost' not in ALLOWED_HOSTS:
ALLOWED_HOSTS.append('localhost')
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases # https://docs.djangoproject.com/en/stable/ref/settings/#databases
DATABASE = { DATABASE = {
"NAME": environ.get("DB_NAME", "netbox"), # Database name 'NAME': environ.get('DB_NAME', 'netbox'), # Database name
"USER": environ.get("DB_USER", ""), # PostgreSQL username 'USER': environ.get('DB_USER', ''), # PostgreSQL username
"PASSWORD": _read_secret("db_password", environ.get("DB_PASSWORD", "")), 'PASSWORD': _read_secret('db_password', environ.get('DB_PASSWORD', '')),
# PostgreSQL password # PostgreSQL password
"HOST": environ.get("DB_HOST", "localhost"), # Database server 'HOST': environ.get('DB_HOST', 'localhost'), # Database server
"PORT": environ.get("DB_PORT", ""), # Database port (leave blank for default) 'PORT': environ.get('DB_PORT', ''), # Database port (leave blank for default)
"OPTIONS": {"sslmode": environ.get("DB_SSLMODE", "prefer")}, 'OPTIONS': {'sslmode': environ.get('DB_SSLMODE', 'prefer')},
# Database connection SSLMODE # Database connection SSLMODE
"CONN_MAX_AGE": int(environ.get("DB_CONN_MAX_AGE", "300")), 'CONN_MAX_AGE': _environ_get_and_map('DB_CONN_MAX_AGE', '300', _AS_INT),
# Max database connection age # Max database connection age
"DISABLE_SERVER_SIDE_CURSORS": environ.get( 'DISABLE_SERVER_SIDE_CURSORS': _environ_get_and_map('DB_DISABLE_SERVER_SIDE_CURSORS', 'False', _AS_BOOL),
"DB_DISABLE_SERVER_SIDE_CURSORS",
"False",
).lower()
== "true",
# Disable the use of server-side cursors transaction pooling # Disable the use of server-side cursors transaction pooling
} }
@ -54,32 +80,23 @@ DATABASE = {
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
# to use two separate database IDs. # to use two separate database IDs.
REDIS = { REDIS = {
"tasks": { 'tasks': {
"HOST": environ.get("REDIS_HOST", "localhost"), 'HOST': environ.get('REDIS_HOST', 'localhost'),
"PORT": int(environ.get("REDIS_PORT", 6379)), 'PORT': _environ_get_and_map('REDIS_PORT', 6379, _AS_INT),
"PASSWORD": _read_secret("redis_password", environ.get("REDIS_PASSWORD", "")), 'USERNAME': environ.get('REDIS_USERNAME', ''),
"DATABASE": int(environ.get("REDIS_DATABASE", 0)), 'PASSWORD': _read_secret('redis_password', environ.get('REDIS_PASSWORD', '')),
"SSL": environ.get("REDIS_SSL", "False").lower() == "true", 'DATABASE': _environ_get_and_map('REDIS_DATABASE', 0, _AS_INT),
"INSECURE_SKIP_TLS_VERIFY": environ.get( 'SSL': _environ_get_and_map('REDIS_SSL', 'False', _AS_BOOL),
"REDIS_INSECURE_SKIP_TLS_VERIFY", 'INSECURE_SKIP_TLS_VERIFY': _environ_get_and_map('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False', _AS_BOOL),
"False",
).lower()
== "true",
}, },
"caching": { 'caching': {
"HOST": environ.get("REDIS_CACHE_HOST", environ.get("REDIS_HOST", "localhost")), 'HOST': environ.get('REDIS_CACHE_HOST', environ.get('REDIS_HOST', 'localhost')),
"PORT": int(environ.get("REDIS_CACHE_PORT", environ.get("REDIS_PORT", 6379))), 'PORT': _environ_get_and_map('REDIS_CACHE_PORT', environ.get('REDIS_PORT', '6379'), _AS_INT),
"PASSWORD": _read_secret( 'USERNAME': environ.get('REDIS_CACHE_USERNAME', environ.get('REDIS_USERNAME', '')),
"redis_cache_password", 'PASSWORD': _read_secret('redis_cache_password', environ.get('REDIS_CACHE_PASSWORD', environ.get('REDIS_PASSWORD', ''))),
environ.get("REDIS_CACHE_PASSWORD", environ.get("REDIS_PASSWORD", "")), 'DATABASE': _environ_get_and_map('REDIS_CACHE_DATABASE', '1', _AS_INT),
), 'SSL': _environ_get_and_map('REDIS_CACHE_SSL', environ.get('REDIS_SSL', 'False'), _AS_BOOL),
"DATABASE": int(environ.get("REDIS_CACHE_DATABASE", 1)), 'INSECURE_SKIP_TLS_VERIFY': _environ_get_and_map('REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY', environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False'), _AS_BOOL),
"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",
}, },
} }
@ -87,7 +104,7 @@ REDIS = {
# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and # 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 # symbols. NetBox will not run without this defined. For more information, see
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", "")) SECRET_KEY = _read_secret('secret_key', environ.get('SECRET_KEY', ''))
######################### #########################
@ -96,195 +113,203 @@ SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", ""))
# # # #
######################### #########################
# Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of # # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
# application errors (assuming correct email settings are provided). # # application errors (assuming correct email settings are provided).
ADMINS = [ # ADMINS = [
# ['John Doe', 'jdoe@example.com'], # # ['John Doe', 'jdoe@example.com'],
] # ]
# URL schemes that are allowed within links in NetBox if 'ALLOWED_URL_SCHEMES' in environ:
ALLOWED_URL_SCHEMES = ( ALLOWED_URL_SCHEMES = _environ_get_and_map('ALLOWED_URL_SCHEMES', None, _AS_LIST)
"file",
"ftp",
"ftps",
"http",
"https",
"irc",
"mailto",
"sftp",
"ssh",
"tel",
"telnet",
"tftp",
"vnc",
"xmpp",
)
# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same # Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
BANNER_TOP = environ.get("BANNER_TOP", "") if 'BANNER_TOP' in environ:
BANNER_BOTTOM = environ.get("BANNER_BOTTOM", "") BANNER_TOP = environ.get('BANNER_TOP', None)
if 'BANNER_BOTTOM' in environ:
BANNER_BOTTOM = environ.get('BANNER_BOTTOM', None)
# Text to include on the login page above the login form. HTML is allowed. # Text to include on the login page above the login form. HTML is allowed.
BANNER_LOGIN = environ.get("BANNER_LOGIN", "") if 'BANNER_LOGIN' in environ:
BANNER_LOGIN = environ.get('BANNER_LOGIN', None)
# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
# BASE_PATH = 'netbox/'
BASE_PATH = environ.get("BASE_PATH", "")
# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90) # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
CHANGELOG_RETENTION = int(environ.get("CHANGELOG_RETENTION", 90)) if 'CHANGELOG_RETENTION' in environ:
CHANGELOG_RETENTION = _environ_get_and_map('CHANGELOG_RETENTION', None, _AS_INT)
# Maximum number of days to retain job results (scripts and reports). Set to 0 to retain job results in the database indefinitely. (Default: 90)
if 'JOB_RETENTION' in environ:
JOB_RETENTION = _environ_get_and_map('JOB_RETENTION', None, _AS_INT)
# JOBRESULT_RETENTION was renamed to JOB_RETENTION in the v3.5.0 release of NetBox. For backwards compatibility, map JOBRESULT_RETENTION to JOB_RETENTION
elif 'JOBRESULT_RETENTION' in environ:
JOB_RETENTION = _environ_get_and_map('JOBRESULT_RETENTION', None, _AS_INT)
# API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be # API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be
# allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or # allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or
# CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers # CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers
CORS_ORIGIN_ALLOW_ALL = environ.get("CORS_ORIGIN_ALLOW_ALL", "False").lower() == "true" CORS_ORIGIN_ALLOW_ALL = _environ_get_and_map('CORS_ORIGIN_ALLOW_ALL', 'False', _AS_BOOL)
CORS_ORIGIN_WHITELIST = list( CORS_ORIGIN_WHITELIST = _environ_get_and_map('CORS_ORIGIN_WHITELIST', 'https://localhost', _AS_LIST)
filter(None, environ.get("CORS_ORIGIN_WHITELIST", "https://localhost").split(" ")), CORS_ORIGIN_REGEX_WHITELIST = [re.compile(r) for r in _environ_get_and_map('CORS_ORIGIN_REGEX_WHITELIST', '', _AS_LIST)]
)
CORS_ORIGIN_REGEX_WHITELIST = [
re.compile(r)
for r in list(
filter(None, environ.get("CORS_ORIGIN_REGEX_WHITELIST", "").split(" ")),
)
]
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal # Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging # sensitive information about your installation. Only enable debugging while performing testing.
# on a production system. # Never enable debugging on a production system.
DEBUG = environ.get("DEBUG", "False").lower() == "true" DEBUG = _environ_get_and_map('DEBUG', 'False', _AS_BOOL)
# Set to True to enable DEVELOPER Mode. WARNING: ONLY netbox developers or plugin developers need this access. # This parameter serves as a safeguard to prevent some potentially dangerous behavior,
DEVELOPER = environ.get("DEVELOPER_MODE", "False").lower() == "true" # such as generating new database schema migrations.
# Set this to True only if you are actively developing the NetBox code base.
DEVELOPER = _environ_get_and_map('DEVELOPER', 'False', _AS_BOOL)
# Email settings # Email settings
EMAIL = { EMAIL = {
"SERVER": environ.get("EMAIL_SERVER", "localhost"), 'SERVER': environ.get('EMAIL_SERVER', 'localhost'),
"PORT": int(environ.get("EMAIL_PORT", 25)), 'PORT': _environ_get_and_map('EMAIL_PORT', 25, _AS_INT),
"USERNAME": environ.get("EMAIL_USERNAME", ""), 'USERNAME': environ.get('EMAIL_USERNAME', ''),
"PASSWORD": _read_secret("email_password", environ.get("EMAIL_PASSWORD", "")), 'PASSWORD': _read_secret('email_password', environ.get('EMAIL_PASSWORD', '')),
"USE_SSL": environ.get("EMAIL_USE_SSL", "False").lower() == "true", 'USE_SSL': _environ_get_and_map('EMAIL_USE_SSL', 'False', _AS_BOOL),
"USE_TLS": environ.get("EMAIL_USE_TLS", "False").lower() == "true", 'USE_TLS': _environ_get_and_map('EMAIL_USE_TLS', 'False', _AS_BOOL),
"SSL_CERTFILE": environ.get("EMAIL_SSL_CERTFILE", ""), 'SSL_CERTFILE': environ.get('EMAIL_SSL_CERTFILE', ''),
"SSL_KEYFILE": environ.get("EMAIL_SSL_KEYFILE", ""), 'SSL_KEYFILE': environ.get('EMAIL_SSL_KEYFILE', ''),
"TIMEOUT": int(environ.get("EMAIL_TIMEOUT", 10)), # seconds 'TIMEOUT': _environ_get_and_map('EMAIL_TIMEOUT', 10, _AS_INT), # seconds
"FROM_EMAIL": environ.get("EMAIL_FROM", ""), 'FROM_EMAIL': environ.get('EMAIL_FROM', ''),
} }
# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table # Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True. # (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
ENFORCE_GLOBAL_UNIQUE = environ.get("ENFORCE_GLOBAL_UNIQUE", "False").lower() == "true" if 'ENFORCE_GLOBAL_UNIQUE' in environ:
ENFORCE_GLOBAL_UNIQUE = _environ_get_and_map('ENFORCE_GLOBAL_UNIQUE', None, _AS_BOOL)
# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and # Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and
# by anonymous users. List models in the form `<app>.<model>`. Add '*' to this list to exempt all models. # by anonymous users. List models in the form `<app>.<model>`. Add '*' to this list to exempt all models.
EXEMPT_VIEW_PERMISSIONS = list( EXEMPT_VIEW_PERMISSIONS = _environ_get_and_map('EXEMPT_VIEW_PERMISSIONS', '', _AS_LIST)
filter(None, environ.get("EXEMPT_VIEW_PERMISSIONS", "").split(" ")),
) # HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks).
# HTTP_PROXIES = {
# 'http': 'http://10.10.1.10:3128',
# 'https': 'http://10.10.1.10:1080',
# }
# IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing
# NetBox from an internal IP.
INTERNAL_IPS = _environ_get_and_map('INTERNAL_IPS', '127.0.0.1 ::1', _AS_LIST)
# Enable GraphQL API. # Enable GraphQL API.
GRAPHQL_ENABLED = environ.get("GRAPHQL_ENABLED", "True").lower() == "true" if 'GRAPHQL_ENABLED' in environ:
GRAPHQL_ENABLED = _environ_get_and_map('GRAPHQL_ENABLED', None, _AS_BOOL)
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: # # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
# https://docs.djangoproject.com/en/stable/topics/logging/ # # https://docs.djangoproject.com/en/stable/topics/logging/
LOGGING = {} # LOGGING = {}
# Automatically reset the lifetime of a valid session upon each authenticated request. Enables users to remain
# authenticated to NetBox indefinitely.
LOGIN_PERSISTENCE = _environ_get_and_map('LOGIN_PERSISTENCE', 'False', _AS_BOOL)
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users # Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
# are permitted to access most data in NetBox (excluding secrets) but not make any changes. # are permitted to access most data in NetBox (excluding secrets) but not make any changes.
LOGIN_REQUIRED = environ.get("LOGIN_REQUIRED", "False").lower() == "true" LOGIN_REQUIRED = _environ_get_and_map('LOGIN_REQUIRED', 'False', _AS_BOOL)
# The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to # The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to
# re-authenticate. (Default: 1209600 [14 days]) # re-authenticate. (Default: 1209600 [14 days])
LOGIN_TIMEOUT = int(environ.get("LOGIN_TIMEOUT", 1209600)) LOGIN_TIMEOUT = _environ_get_and_map('LOGIN_TIMEOUT', 1209600, _AS_INT)
# Setting this to True will display a "maintenance mode" banner at the top of every page. # Setting this to True will display a "maintenance mode" banner at the top of every page.
MAINTENANCE_MODE = environ.get("MAINTENANCE_MODE", "False").lower() == "true" if 'MAINTENANCE_MODE' in environ:
MAINTENANCE_MODE = _environ_get_and_map('MAINTENANCE_MODE', None, _AS_BOOL)
# Maps provider
if 'MAPS_URL' in environ:
MAPS_URL = environ.get('MAPS_URL', None)
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. # An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request # "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
# all objects by specifying "?limit=0". # all objects by specifying "?limit=0".
MAX_PAGE_SIZE = int(environ.get("MAX_PAGE_SIZE", 1000)) if 'MAX_PAGE_SIZE' in environ:
MAX_PAGE_SIZE = _environ_get_and_map('MAX_PAGE_SIZE', None, _AS_INT)
# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that # The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
# the default value of this setting is derived from the installed location. # the default value of this setting is derived from the installed location.
MEDIA_ROOT = environ.get("MEDIA_ROOT", join(_BASE_DIR, "media")) MEDIA_ROOT = environ.get('MEDIA_ROOT', join(_BASE_DIR, 'media'))
# Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' # Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics'
METRICS_ENABLED = environ.get("METRICS_ENABLED", "False").lower() == "true" METRICS_ENABLED = _environ_get_and_map('METRICS_ENABLED', 'False', _AS_BOOL)
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
NAPALM_USERNAME = environ.get("NAPALM_USERNAME", "")
NAPALM_PASSWORD = _read_secret("napalm_password", environ.get("NAPALM_PASSWORD", ""))
# NAPALM timeout (in seconds). (Default: 30)
NAPALM_TIMEOUT = int(environ.get("NAPALM_TIMEOUT", 30))
# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
# be provided as a dictionary.
NAPALM_ARGS = {}
# Determine how many objects to display per page within a list. (Default: 50) # Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = int(environ.get("PAGINATE_COUNT", 50)) if 'PAGINATE_COUNT' in environ:
PAGINATE_COUNT = _environ_get_and_map('PAGINATE_COUNT', None, _AS_INT)
# Enable installed plugins. Add the name of each plugin to the list. # # Enable installed plugins. Add the name of each plugin to the list.
PLUGINS = [] # PLUGINS = []
# Plugins configuration settings. These settings are used by various plugins that the user may have installed. # # Plugins configuration settings. These settings are used by various plugins that the user may have installed.
# Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. # # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings.
PLUGINS_CONFIG = {} # PLUGINS_CONFIG = {
# }
# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to # When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
# prefer IPv4 instead. # prefer IPv4 instead.
PREFER_IPV4 = environ.get("PREFER_IPV4", "False").lower() == "true" if 'PREFER_IPV4' in environ:
PREFER_IPV4 = _environ_get_and_map('PREFER_IPV4', None, _AS_BOOL)
# The default value for the amperage field when creating new power feeds.
if 'POWERFEED_DEFAULT_AMPERAGE' in environ:
POWERFEED_DEFAULT_AMPERAGE = _environ_get_and_map('POWERFEED_DEFAULT_AMPERAGE', None, _AS_INT)
# The default value (percentage) for the max_utilization field when creating new power feeds.
if 'POWERFEED_DEFAULT_MAX_UTILIZATION' in environ:
POWERFEED_DEFAULT_MAX_UTILIZATION = _environ_get_and_map('POWERFEED_DEFAULT_MAX_UTILIZATION', None, _AS_INT)
# The default value for the voltage field when creating new power feeds.
if 'POWERFEED_DEFAULT_VOLTAGE' in environ:
POWERFEED_DEFAULT_VOLTAGE = _environ_get_and_map('POWERFEED_DEFAULT_VOLTAGE', None, _AS_INT)
# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1. # Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1.
RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = int( if 'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT' in environ:
environ.get("RACK_ELEVATION_DEFAULT_UNIT_HEIGHT", 22), RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = _environ_get_and_map('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', None, _AS_INT)
) if 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH' in environ:
RACK_ELEVATION_DEFAULT_UNIT_WIDTH = int( RACK_ELEVATION_DEFAULT_UNIT_WIDTH = _environ_get_and_map('RACK_ELEVATION_DEFAULT_UNIT_WIDTH', None, _AS_INT)
environ.get("RACK_ELEVATION_DEFAULT_UNIT_WIDTH", 220),
)
# Remote authentication support # Remote authentication support
REMOTE_AUTH_ENABLED = environ.get("REMOTE_AUTH_ENABLED", "False").lower() == "true" REMOTE_AUTH_ENABLED = _environ_get_and_map('REMOTE_AUTH_ENABLED', 'False', _AS_BOOL)
REMOTE_AUTH_BACKEND = environ.get( REMOTE_AUTH_BACKEND = _environ_get_and_map('REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend', _AS_LIST)
"REMOTE_AUTH_BACKEND", REMOTE_AUTH_HEADER = environ.get('REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER')
"netbox.authentication.RemoteUserBackend", REMOTE_AUTH_AUTO_CREATE_USER = _environ_get_and_map('REMOTE_AUTH_AUTO_CREATE_USER', 'False', _AS_BOOL)
) REMOTE_AUTH_DEFAULT_GROUPS = _environ_get_and_map('REMOTE_AUTH_DEFAULT_GROUPS', '', _AS_LIST)
REMOTE_AUTH_HEADER = environ.get("REMOTE_AUTH_HEADER", "HTTP_REMOTE_USER") # REMOTE_AUTH_DEFAULT_PERMISSIONS = {}
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(" ")),
)
# This repository is used to check whether there is a new release of NetBox available. Set to None to disable the # This repository is used to check whether there is a new release of NetBox available. Set to None to disable the
# version check or use the URL below to check for release in the official NetBox repository. # version check or use the URL below to check for release in the official NetBox repository.
# https://api.github.com/repos/netbox-community/netbox/releases RELEASE_CHECK_URL = environ.get('RELEASE_CHECK_URL', None)
RELEASE_CHECK_URL = environ.get("RELEASE_CHECK_URL", None) # RELEASE_CHECK_URL = 'https://api.github.com/repos/netbox-community/netbox/releases'
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
# this setting is derived from the installed location.
REPORTS_ROOT = environ.get("REPORTS_ROOT", "/etc/netbox/reports")
# Maximum execution time for background tasks, in seconds. # Maximum execution time for background tasks, in seconds.
RQ_DEFAULT_TIMEOUT = int(environ.get("RQ_DEFAULT_TIMEOUT", 300)) RQ_DEFAULT_TIMEOUT = _environ_get_and_map('RQ_DEFAULT_TIMEOUT', 300, _AS_INT)
# The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of # The name to use for the csrf token cookie.
# this setting is derived from the installed location. CSRF_COOKIE_NAME = environ.get('CSRF_COOKIE_NAME', 'csrftoken')
SCRIPTS_ROOT = environ.get("SCRIPTS_ROOT", "/etc/netbox/scripts")
# Cross-Site-Request-Forgery-Attack settings. If Netbox is sitting behind a reverse proxy, you might need to set the CSRF_TRUSTED_ORIGINS flag.
# Django 4.0 requires to specify the URL Scheme in this setting. An example environment variable could be specified like:
# CSRF_TRUSTED_ORIGINS=https://demo.netbox.dev http://demo.netbox.dev
CSRF_TRUSTED_ORIGINS = _environ_get_and_map('CSRF_TRUSTED_ORIGINS', '', _AS_LIST)
# The name to use for the session cookie.
SESSION_COOKIE_NAME = environ.get('SESSION_COOKIE_NAME', 'sessionid')
# By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use # By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use
# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only # local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only
# database access.) Note that the user as which NetBox runs must have read and write permissions to this path. # database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
SESSION_FILE_PATH = environ.get("SESSIONS_ROOT", None) SESSION_FILE_PATH = environ.get('SESSION_FILE_PATH', environ.get('SESSIONS_ROOT', None))
# Time zone (default: UTC) # Time zone (default: UTC)
TIME_ZONE = environ.get("TIME_ZONE", "UTC") TIME_ZONE = environ.get('TIME_ZONE', 'UTC')
# Date/time formatting. See the following link for supported formats: # Date/time formatting. See the following link for supported formats:
# https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date # https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
DATE_FORMAT = environ.get("DATE_FORMAT", "N j, Y") DATE_FORMAT = environ.get('DATE_FORMAT', 'N j, Y')
SHORT_DATE_FORMAT = environ.get("SHORT_DATE_FORMAT", "Y-m-d") SHORT_DATE_FORMAT = environ.get('SHORT_DATE_FORMAT', 'Y-m-d')
TIME_FORMAT = environ.get("TIME_FORMAT", "g:i a") TIME_FORMAT = environ.get('TIME_FORMAT', 'g:i a')
SHORT_TIME_FORMAT = environ.get("SHORT_TIME_FORMAT", "H:i:s") SHORT_TIME_FORMAT = environ.get('SHORT_TIME_FORMAT', 'H:i:s')
DATETIME_FORMAT = environ.get("DATETIME_FORMAT", "N j, Y g:i a") DATETIME_FORMAT = environ.get('DATETIME_FORMAT', 'N j, Y g:i a')
SHORT_DATETIME_FORMAT = environ.get("SHORT_DATETIME_FORMAT", "Y-m-d H:i") SHORT_DATETIME_FORMAT = environ.get('SHORT_DATETIME_FORMAT', 'Y-m-d H:i')

View File

@ -104,5 +104,5 @@
//"postAttachCommand": "source /opt/netbox/venv/bin/activate", //"postAttachCommand": "source /opt/netbox/venv/bin/activate",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode" "remoteUser": "ubuntu"
} }

View File

@ -16,10 +16,10 @@ services:
interval: 15s interval: 15s
test: "curl -f http://localhost:8080/api/ || exit 1" test: "curl -f http://localhost:8080/api/ || exit 1"
volumes: volumes:
- ./configuration:/etc/netbox/config:z,ro - ./configuration:/etc/netbox/config:ro
#- ./reports:/etc/netbox/reports:z,ro #- netbox-media-files:/opt/netbox/netbox/media:rw
#- ./scripts:/etc/netbox/scripts:z,ro #- netbox-reports-files:/opt/netbox/netbox/reports:rw
#- netbox-media-files:/opt/netbox/netbox/media:z #- netbox-scripts-files:/opt/netbox/netbox/scripts:rw
#netbox-worker: #netbox-worker:
# <<: *netbox # <<: *netbox
# depends_on: # depends_on:

View File

@ -15,7 +15,7 @@ REDIS_DATABASE=0
REDIS_HOST=redis REDIS_HOST=redis
REDIS_INSECURE_SKIP_TLS_VERIFY=false REDIS_INSECURE_SKIP_TLS_VERIFY=false
REDIS_PASSWORD=H733Kdjndks81 REDIS_PASSWORD=H733Kdjndks81
SECRET_KEY=r8OwDznj!!dciP9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNjaa SECRET_KEY='r(m)9nLGnz$(_q3N4z1k(EFsMCjjjzx08x9VhNVcfd%6RF#r!6DE@+V5Zk2X'
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567 SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
SUPERUSER_EMAIL=admin@example.com SUPERUSER_EMAIL=admin@example.com
SUPERUSER_NAME=admin SUPERUSER_NAME=admin

View File

@ -23,14 +23,14 @@ body:
attributes: attributes:
label: NetBox access-list plugin version label: NetBox access-list plugin version
description: What version of the NetBox access-list plugin are you currently running? description: What version of the NetBox access-list plugin are you currently running?
placeholder: v1.3.0 placeholder: v1.4.0
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.5.4 placeholder: v3.6.3
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@ -15,7 +15,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.5.4 placeholder: v3.6.3
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -44,4 +44,4 @@ jobs:
- id: docker-test - id: docker-test
name: Test the image name: Test the image
run: ./test.sh snapshot run: ./test.sh

View File

@ -1,7 +1,7 @@
--- ---
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: check-docstring-first - id: check-docstring-first
- id: check-merge-conflict - id: check-merge-conflict
@ -21,24 +21,24 @@ repos:
- "--profile=black" - "--profile=black"
exclude: ^.devcontainer/ exclude: ^.devcontainer/
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.3.0 rev: 23.9.1
hooks: hooks:
- id: black - id: black
language_version: python3 language_version: python3
exclude: ^.devcontainer/ exclude: ^.devcontainer/
- repo: https://github.com/asottile/add-trailing-comma - repo: https://github.com/asottile/add-trailing-comma
rev: v2.5.1 rev: v3.1.0
hooks: hooks:
- id: add-trailing-comma - id: add-trailing-comma
args: args:
- "--py36-plus" - "--py36-plus"
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 6.0.0 rev: 6.1.0
hooks: hooks:
- id: flake8 - id: flake8
exclude: ^.devcontainer/ exclude: ^.devcontainer/
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.7.0 rev: v3.15.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: args:
@ -59,11 +59,11 @@ repos:
# - id: htmlhint # - id: htmlhint
# args: [--config, .htmlhintrc] # args: [--config, .htmlhintrc]
- repo: https://github.com/igorshubovych/markdownlint-cli - repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.35.0 rev: v0.37.0
hooks: hooks:
- id: markdownlint - id: markdownlint
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.272 rev: v0.0.292
hooks: hooks:
- id: ruff - id: ruff
#- repo: local #- repo: local

View File

@ -1,4 +1,4 @@
ARG NETBOX_VARIANT=v3.5 ARG NETBOX_VARIANT=v3.6
FROM netboxcommunity/netbox:${NETBOX_VARIANT} FROM netboxcommunity/netbox:${NETBOX_VARIANT}
@ -6,4 +6,4 @@ RUN mkdir -pv /plugins/netbox-acls
COPY . /plugins/netbox-acls COPY . /plugins/netbox-acls
RUN /opt/netbox/venv/bin/python3 /plugins/netbox-acls/setup.py develop && \ 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 cp -rf /plugins/netbox-acls/netbox_acls/ /opt/netbox/venv/lib/python3.11/site-packages/netbox_acls

View File

@ -38,10 +38,11 @@ Each Plugin Version listed below has been tested with its corresponding NetBox V
| NetBox Version | Plugin Version | | NetBox Version | Plugin Version |
|:--------------:|:--------------:| |:--------------:|:--------------:|
| 3.2 | 1.0.1 | | 3.6 | 1.4.0 |
| 3.3 | 1.1.0 |
| 3.4 | 1.2.2 |
| 3.5 | 1.3.0 | | 3.5 | 1.3.0 |
| 3.4 | 1.2.2 |
| 3.3 | 1.1.0 |
| 3.2 | 1.0.1 |
## Installing ## Installing

View File

@ -6,8 +6,6 @@ services:
build: build:
dockerfile: Dockerfile dockerfile: Dockerfile
context: . context: .
args:
NETBOX_VARIANT: ${NETBOX_VARIANT}
depends_on: depends_on:
- postgres - postgres
- redis - redis
@ -17,12 +15,12 @@ services:
# postgres # postgres
postgres: postgres:
image: postgres:14-alpine image: postgres:15-alpine
env_file: env/postgres.env env_file: env/postgres.env
# redis # redis
redis: redis:
image: redis:6-alpine image: redis:7-alpine
command: command:
- sh - sh
- -c # this is to evaluate the $REDIS_PASSWORD from the env - -c # this is to evaluate the $REDIS_PASSWORD from the env

2
env/netbox.env vendored
View File

@ -14,7 +14,7 @@ REDIS_DATABASE=0
REDIS_HOST=redis REDIS_HOST=redis
REDIS_INSECURE_SKIP_TLS_VERIFY=false REDIS_INSECURE_SKIP_TLS_VERIFY=false
REDIS_PASSWORD=H733Kdjndks81 REDIS_PASSWORD=H733Kdjndks81
SECRET_KEY=r8OwDznj!!dciP9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNjaa SECRET_KEY='r(m)9nLGnz$(_q3N4z1k(EFsMCjjjzx08x9VhNVcfd%6RF#r!6DE@+V5Zk2X'
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567 SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
SUPERUSER_EMAIL=admin@example.com SUPERUSER_EMAIL=admin@example.com
SUPERUSER_NAME=admin SUPERUSER_NAME=admin

View File

@ -17,8 +17,8 @@ class NetBoxACLsConfig(PluginConfig):
version = __version__ version = __version__
description = "Manage simple ACLs in NetBox" description = "Manage simple ACLs in NetBox"
base_url = "access-lists" base_url = "access-lists"
min_version = "3.5.0" min_version = "3.6.0"
max_version = "3.5.99" max_version = "3.6.99"
config = NetBoxACLsConfig config = NetBoxACLsConfig

View File

@ -7,7 +7,6 @@ from django import forms
from ipam.models import Prefix from ipam.models import Prefix
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from utilities.forms.fields import ( from utilities.forms.fields import (
ChoiceField,
DynamicModelChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, DynamicModelMultipleChoiceField,
TagFilterField, TagFilterField,
@ -74,11 +73,11 @@ class AccessListFilterForm(NetBoxModelFilterSetForm):
queryset=VirtualChassis.objects.all(), queryset=VirtualChassis.objects.all(),
required=False, required=False,
) )
type = ChoiceField( type = forms.ChoiceField(
choices=add_blank_choice(ACLTypeChoices), choices=add_blank_choice(ACLTypeChoices),
required=False, required=False,
) )
default_action = ChoiceField( default_action = forms.ChoiceField(
choices=add_blank_choice(ACLActionChoices), choices=add_blank_choice(ACLActionChoices),
required=False, required=False,
label="Default Action", label="Default Action",
@ -158,7 +157,7 @@ class ACLInterfaceAssignmentFilterForm(NetBoxModelFilterSetForm):
}, },
label="Access List", label="Access List",
) )
direction = ChoiceField( direction = forms.ChoiceField(
choices=add_blank_choice(ACLAssignmentDirectionChoices), choices=add_blank_choice(ACLAssignmentDirectionChoices),
required=False, required=False,
) )
@ -187,7 +186,7 @@ class ACLStandardRuleFilterForm(NetBoxModelFilterSetForm):
required=False, required=False,
label="Source Prefix", label="Source Prefix",
) )
action = ChoiceField( action = forms.ChoiceField(
choices=add_blank_choice(ACLRuleActionChoices), choices=add_blank_choice(ACLRuleActionChoices),
required=False, required=False,
) )
@ -211,7 +210,7 @@ class ACLExtendedRuleFilterForm(NetBoxModelFilterSetForm):
queryset=AccessList.objects.all(), queryset=AccessList.objects.all(),
required=False, required=False,
) )
action = ChoiceField( action = forms.ChoiceField(
choices=add_blank_choice(ACLRuleActionChoices), choices=add_blank_choice(ACLRuleActionChoices),
required=False, required=False,
) )
@ -225,7 +224,7 @@ class ACLExtendedRuleFilterForm(NetBoxModelFilterSetForm):
required=False, required=False,
label="Destination Prefix", label="Destination Prefix",
) )
protocol = ChoiceField( protocol = forms.ChoiceField(
choices=add_blank_choice(ACLProtocolChoices), choices=add_blank_choice(ACLProtocolChoices),
required=False, required=False,
) )

View File

@ -3,8 +3,8 @@ Defines each django model's GUI form to add or edit objects for each django mode
""" """
from dcim.models import Device, Interface, Region, Site, SiteGroup, VirtualChassis from dcim.models import Device, Interface, Region, Site, SiteGroup, VirtualChassis
from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from ipam.models import Prefix from ipam.models import Prefix
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
@ -179,63 +179,49 @@ class AccessListForm(NetBoxModelForm):
- Check if duplicate entry. (Because of GFK.) - Check if duplicate entry. (Because of GFK.)
- Check if Access List has no existing rules before change the Access List's type. - Check if Access List has no existing rules before change the Access List's type.
""" """
cleaned_data = super().clean() super().clean()
error_message = {}
if self.errors.get("name"): if self.errors.get("name"):
return cleaned_data return
name = cleaned_data.get("name")
acl_type = cleaned_data.get("type") name = self.cleaned_data.get("name")
device = cleaned_data.get("device") acl_type = self.cleaned_data.get("type")
virtual_chassis = cleaned_data.get("virtual_chassis") device = self.cleaned_data.get("device")
virtual_machine = cleaned_data.get("virtual_machine") virtual_chassis = self.cleaned_data.get("virtual_chassis")
virtual_machine = self.cleaned_data.get("virtual_machine")
# Check if more than one host type selected. # 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( raise ValidationError(
"Access Lists must be assigned to one host at a time. Either a device, virtual chassis or virtual machine." {"__all__": "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:
raise forms.ValidationError(
"Access Lists must be assigned to a device, virtual chassis or virtual machine.",
) )
# Check if no hosts selected.
if not device and not virtual_chassis and not virtual_machine:
raise ValidationError({"__all__": "Access Lists must be assigned to a device, virtual chassis or virtual machine."})
existing_acls = None
if device: if device:
host_type = "device" host_type = "device"
existing_acls = AccessList.objects.filter(name=name, device=device).exists() existing_acls = AccessList.objects.filter(name=name, device=device).exists()
elif virtual_machine: elif virtual_machine:
host_type = "virtual_machine" host_type = "virtual_machine"
existing_acls = AccessList.objects.filter( existing_acls = AccessList.objects.filter(name=name, virtual_machine=virtual_machine).exists()
name=name, elif virtual_chassis:
virtual_machine=virtual_machine,
).exists()
else:
host_type = "virtual_chassis" host_type = "virtual_chassis"
existing_acls = AccessList.objects.filter( existing_acls = AccessList.objects.filter(name=name, virtual_chassis=virtual_chassis).exists()
name=name,
virtual_chassis=virtual_chassis,
).exists()
# Check if duplicate entry. # Check if duplicate entry.
if ("name" in self.changed_data or host_type in self.changed_data) and existing_acls: 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_same_acl_name = "An ACL with this name is already associated to this host."
error_message |= { raise ValidationError({host_type: [error_same_acl_name], "name": [error_same_acl_name]})
host_type: [error_same_acl_name],
"name": [error_same_acl_name],
}
# Check if Access List has no existing rules before change the Access List's type. # Check if Access List has no existing rules before change the Access List's type.
if self.instance.pk and ( if self.instance.pk and (
(acl_type == ACLTypeChoices.TYPE_EXTENDED and self.instance.aclstandardrules.exists()) (acl_type == ACLTypeChoices.TYPE_EXTENDED and self.instance.aclstandardrules.exists())
or (acl_type == ACLTypeChoices.TYPE_STANDARD and self.instance.aclextendedrules.exists()) or (acl_type == ACLTypeChoices.TYPE_STANDARD and self.instance.aclextendedrules.exists())
): ):
error_message["type"] = [ raise ValidationError({"type": ["This ACL has ACL rules associated, CANNOT change ACL type."]})
"This ACL has ACL rules associated, CANNOT change ACL type.",
]
if error_message:
raise forms.ValidationError(error_message)
return cleaned_data
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Set assigned object # Set assigned object
@ -343,12 +329,13 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm):
- Check for duplicate entry. (Because of GFK) - Check for duplicate entry. (Because of GFK)
- Check that the interface does not have an existing ACL applied in the direction already. - Check that the interface does not have an existing ACL applied in the direction already.
""" """
cleaned_data = super().clean() super().clean()
error_message = {} error_message = {}
access_list = cleaned_data.get("access_list") access_list = self.cleaned_data.get("access_list")
direction = cleaned_data.get("direction") direction = self.cleaned_data.get("direction")
interface = cleaned_data.get("interface") interface = self.cleaned_data.get("interface")
vminterface = cleaned_data.get("vminterface") vminterface = self.cleaned_data.get("vminterface")
# Check if both interface and vminterface are set. # Check if both interface and vminterface are set.
if interface and vminterface: if interface and vminterface:
@ -366,22 +353,20 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm):
"vminterface": [error_no_interface], "vminterface": [error_no_interface],
} }
else: else:
# Define assigned_object, assigned_object_type, host_type, and host based on interface or vminterface
if interface: if interface:
assigned_object = interface assigned_object = interface
assigned_object_type = "interface" assigned_object_type = "interface"
host_type = "device" host_type = "device"
host = Interface.objects.get(pk=assigned_object.pk).device host = Interface.objects.get(pk=assigned_object.pk).device
assigned_object_id = Interface.objects.get(pk=assigned_object.pk).pk
else: else:
assigned_object = vminterface assigned_object = vminterface
assigned_object_type = "vminterface" assigned_object_type = "vminterface"
host_type = "virtual_machine" host_type = "virtual_machine"
host = VMInterface.objects.get(pk=assigned_object.pk).virtual_machine host = VMInterface.objects.get(pk=assigned_object.pk).virtual_machine
assigned_object_id = VMInterface.objects.get(pk=assigned_object.pk).pk
assigned_object_type_id = ContentType.objects.get_for_model( assigned_object_id = assigned_object.pk
assigned_object, assigned_object_type_id = ContentType.objects.get_for_model(assigned_object).pk
).pk
access_list_host = AccessList.objects.get(pk=access_list.pk).assigned_object access_list_host = AccessList.objects.get(pk=access_list.pk).assigned_object
# Check that an interface's parent device/virtual_machine is assigned to the Access List. # Check that an interface's parent device/virtual_machine is assigned to the Access List.
@ -392,20 +377,22 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm):
assigned_object_type: [error_acl_not_assigned_to_host], assigned_object_type: [error_acl_not_assigned_to_host],
host_type: [error_acl_not_assigned_to_host], host_type: [error_acl_not_assigned_to_host],
} }
# Check for duplicate entry.
if ACLInterfaceAssignment.objects.filter( # Check for duplicate entry and existing ACL in the direction.
existing_acl = ACLInterfaceAssignment.objects.filter(
access_list=access_list, access_list=access_list,
assigned_object_id=assigned_object_id, assigned_object_id=assigned_object_id,
assigned_object_type=assigned_object_type_id, assigned_object_type=assigned_object_type_id,
direction=direction, direction=direction,
).exists(): )
if existing_acl.exists():
error_duplicate_entry = "An ACL with this name is already associated to this interface & direction." error_duplicate_entry = "An ACL with this name is already associated to this interface & direction."
error_message |= { error_message |= {
"access_list": [error_duplicate_entry], "access_list": [error_duplicate_entry],
"direction": [error_duplicate_entry], "direction": [error_duplicate_entry],
assigned_object_type: [error_duplicate_entry], assigned_object_type: [error_duplicate_entry],
} }
# Check that the interface does not have an existing ACL applied in the direction already.
if ACLInterfaceAssignment.objects.filter( if ACLInterfaceAssignment.objects.filter(
assigned_object_id=assigned_object_id, assigned_object_id=assigned_object_id,
assigned_object_type=assigned_object_type_id, assigned_object_type=assigned_object_type_id,
@ -418,8 +405,7 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm):
} }
if error_message: if error_message:
raise forms.ValidationError(error_message) raise ValidationError(error_message)
return cleaned_data
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Set assigned object # Set assigned object
@ -484,27 +470,27 @@ class ACLStandardRuleForm(NetBoxModelForm):
- Check if action set to remark, but source_prefix set. - Check if action set to remark, but source_prefix set.
- Check remark set, but action not set to remark. - Check remark set, but action not set to remark.
""" """
cleaned_data = super().clean() super().clean()
cleaned_data = self.cleaned_data
error_message = {} error_message = {}
# No need to check for unique_together since there is no usage of GFK action = cleaned_data.get("action")
remark = cleaned_data.get("remark")
source_prefix = cleaned_data.get("source_prefix")
if cleaned_data.get("action") == "remark": if action == "remark":
# Check if action set to remark, but no remark set. # Check if action set to remark, but no remark set.
if not cleaned_data.get("remark"): if not remark:
error_message["remark"] = [error_message_no_remark] error_message["remark"] = [error_message_no_remark]
# Check if action set to remark, but source_prefix set. # Check if action set to remark, but source_prefix set.
if cleaned_data.get("source_prefix"): if source_prefix:
error_message["source_prefix"] = [ error_message["source_prefix"] = [error_message_action_remark_source_prefix_set]
error_message_action_remark_source_prefix_set,
]
# Check remark set, but action not set to remark. # Check remark set, but action not set to remark.
elif cleaned_data.get("remark"): elif remark:
error_message["remark"] = [error_message_remark_without_action_remark] error_message["remark"] = [error_message_remark_without_action_remark]
if error_message: if error_message:
raise forms.ValidationError(error_message) raise ValidationError(error_message)
return cleaned_data
class ACLExtendedRuleForm(NetBoxModelForm): class ACLExtendedRuleForm(NetBoxModelForm):
@ -588,48 +574,36 @@ class ACLExtendedRuleForm(NetBoxModelForm):
- Check if action set to remark, but source_ports set. - Check if action set to remark, but source_ports set.
- Check if action set to remark, but destination_prefix set. - Check if action set to remark, but destination_prefix set.
- Check if action set to remark, but destination_ports set. - Check if action set to remark, but destination_ports set.
- Check if action set to remark, but destination_ports set.
- Check if action set to remark, but protocol set. - Check if action set to remark, but protocol set.
- Check remark set, but action not set to remark. - Check remark set, but action not set to remark.
""" """
cleaned_data = super().clean() super().clean()
cleaned_data = self.cleaned_data
error_message = {} error_message = {}
# No need to check for unique_together since there is no usage of GFK action = cleaned_data.get("action")
remark = cleaned_data.get("remark")
source_prefix = cleaned_data.get("source_prefix")
source_ports = cleaned_data.get("source_ports")
destination_prefix = cleaned_data.get("destination_prefix")
destination_ports = cleaned_data.get("destination_ports")
protocol = cleaned_data.get("protocol")
if cleaned_data.get("action") == "remark": if action == "remark":
# Check if action set to remark, but no remark set. if not remark:
if not cleaned_data.get("remark"):
error_message["remark"] = [error_message_no_remark] error_message["remark"] = [error_message_no_remark]
# Check if action set to remark, but source_prefix set. if source_prefix:
if cleaned_data.get("source_prefix"): error_message["source_prefix"] = [error_message_action_remark_source_prefix_set]
error_message["source_prefix"] = [ if source_ports:
error_message_action_remark_source_prefix_set, error_message["source_ports"] = ["Action is set to remark, Source Ports CANNOT be set."]
] if destination_prefix:
# Check if action set to remark, but source_ports set. error_message["destination_prefix"] = ["Action is set to remark, Destination Prefix CANNOT be set."]
if cleaned_data.get("source_ports"): if destination_ports:
error_message["source_ports"] = [ error_message["destination_ports"] = ["Action is set to remark, Destination Ports CANNOT be set."]
"Action is set to remark, Source Ports CANNOT be set.", if protocol:
] error_message["protocol"] = ["Action is set to remark, Protocol CANNOT be set."]
# Check if action set to remark, but destination_prefix set. elif remark:
if cleaned_data.get("destination_prefix"):
error_message["destination_prefix"] = [
"Action is set to remark, Destination Prefix CANNOT be set.",
]
# Check if action set to remark, but destination_ports set.
if cleaned_data.get("destination_ports"):
error_message["destination_ports"] = [
"Action is set to remark, Destination Ports CANNOT be set.",
]
# Check if action set to remark, but protocol set.
if cleaned_data.get("protocol"):
error_message["protocol"] = [
"Action is set to remark, Protocol CANNOT be set.",
]
# Check if action not set to remark, but remark set.
elif cleaned_data.get("remark"):
error_message["remark"] = [error_message_remark_without_action_remark] error_message["remark"] = [error_message_remark_without_action_remark]
if error_message: if error_message:
raise forms.ValidationError(error_message) raise ValidationError(error_message)
return cleaned_data

View File

@ -64,7 +64,7 @@ class AccessListTable(NetBoxTable):
"rule_count", "rule_count",
"default_action", "default_action",
"comments", "comments",
"actions", "action",
"tags", "tags",
) )
default_columns = ( default_columns = (
@ -147,7 +147,6 @@ class ACLStandardRuleTable(NetBoxTable):
"access_list", "access_list",
"index", "index",
"action", "action",
"actions",
"remark", "remark",
"tags", "tags",
"description", "description",
@ -157,7 +156,6 @@ class ACLStandardRuleTable(NetBoxTable):
"access_list", "access_list",
"index", "index",
"action", "action",
"actions",
"remark", "remark",
"source_prefix", "source_prefix",
"tags", "tags",
@ -189,7 +187,6 @@ class ACLExtendedRuleTable(NetBoxTable):
"access_list", "access_list",
"index", "index",
"action", "action",
"actions",
"remark", "remark",
"tags", "tags",
"description", "description",
@ -203,7 +200,6 @@ class ACLExtendedRuleTable(NetBoxTable):
"access_list", "access_list",
"index", "index",
"action", "action",
"actions",
"remark", "remark",
"tags", "tags",
"source_prefix", "source_prefix",

View File

@ -1 +1 @@
__version__ = "1.3.0" __version__ = "1.4.0"

13
test.sh
View File

@ -1,16 +1,9 @@
#!/bin/bash #!/bin/bash
# Runs the NetBox plugin unit tests # 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 # exit when a command exits with an exit code != 0
set -e set -e
# NETBOX_VARIANT is used by `Dockerfile` to determine the tag
NETBOX_VARIANT="${1-latest}"
# The docker compose command to use # The docker compose command to use
doco="docker compose --file docker-compose.yml" doco="docker compose --file docker-compose.yml"
@ -27,12 +20,10 @@ test_cleanup() {
docker image rm docker.io/library/netbox-acls-netbox || echo '' docker image rm docker.io/library/netbox-acls-netbox || echo ''
} }
export NETBOX_VARIANT=${NETBOX_VARIANT} echo "🐳🐳🐳 Start testing"
echo "🐳🐳🐳 Start testing '${NETBOX_VARIANT}'"
# Make sure the cleanup script is executed # Make sure the cleanup script is executed
trap test_cleanup EXIT ERR trap test_cleanup EXIT ERR
test_netbox_unit_tests test_netbox_unit_tests
echo "🐳🐳🐳 Done testing '${NETBOX_VARIANT}'" echo "🐳🐳🐳 Done testing"