diff --git a/.devcontainer/configuration/configuration.py b/.devcontainer/configuration/configuration.py index 1db6051..72b3dc9 100644 --- a/.devcontainer/configuration/configuration.py +++ b/.devcontainer/configuration/configuration.py @@ -12,15 +12,16 @@ from os.path import abspath, dirname, join # 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): +def _read_secret(secret_name, default=None): try: - f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8') - except EnvironmentError: + f = open("/run/secrets/" + secret_name, encoding="utf-8") + except OSError: return default else: with f: return f.readline().strip() + _BASE_DIR = dirname(dirname(abspath(__file__))) ######################### @@ -33,44 +34,60 @@ _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. # # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] -ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split(' ') +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 + "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', + "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', + "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", }, } @@ -78,7 +95,7 @@ REDIS = { # 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', '')) +SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", "")) ######################### @@ -95,60 +112,82 @@ ADMINS = [ # URL schemes that are allowed within links in NetBox ALLOWED_URL_SCHEMES = ( - 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp', + "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 # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. -BANNER_TOP = environ.get('BANNER_TOP', '') -BANNER_BOTTOM = environ.get('BANNER_BOTTOM', '') +BANNER_TOP = environ.get("BANNER_TOP", "") +BANNER_BOTTOM = environ.get("BANNER_BOTTOM", "") # Text to include on the login page above the login form. HTML is allowed. -BANNER_LOGIN = environ.get('BANNER_LOGIN', '') +BANNER_LOGIN = environ.get("BANNER_LOGIN", "") # 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', '') +BASE_PATH = environ.get("BASE_PATH", "") # 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)) +CHANGELOG_RETENTION = int(environ.get("CHANGELOG_RETENTION", 90)) # 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 # 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_WHITELIST = list(filter(None, environ.get('CORS_ORIGIN_WHITELIST', 'https://localhost').split(' '))) -CORS_ORIGIN_REGEX_WHITELIST = [re.compile(r) for r in list(filter(None, environ.get('CORS_ORIGIN_REGEX_WHITELIST', '').split(' ')))] +CORS_ORIGIN_ALLOW_ALL = environ.get("CORS_ORIGIN_ALLOW_ALL", "False").lower() == "true" +CORS_ORIGIN_WHITELIST = list( + filter(None, environ.get("CORS_ORIGIN_WHITELIST", "https://localhost").split(" ")), +) +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 # sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging # on a production system. -DEBUG = environ.get('DEBUG', 'False').lower() == 'true' +DEBUG = environ.get("DEBUG", "False").lower() == "true" # Email settings EMAIL = { - 'SERVER': environ.get('EMAIL_SERVER', 'localhost'), - 'PORT': int(environ.get('EMAIL_PORT', 25)), - 'USERNAME': environ.get('EMAIL_USERNAME', ''), - 'PASSWORD': _read_secret('email_password', environ.get('EMAIL_PASSWORD', '')), - 'USE_SSL': environ.get('EMAIL_USE_SSL', 'False').lower() == 'true', - 'USE_TLS': environ.get('EMAIL_USE_TLS', 'False').lower() == 'true', - 'SSL_CERTFILE': environ.get('EMAIL_SSL_CERTFILE', ''), - 'SSL_KEYFILE': environ.get('EMAIL_SSL_KEYFILE', ''), - 'TIMEOUT': int(environ.get('EMAIL_TIMEOUT', 10)), # seconds - 'FROM_EMAIL': environ.get('EMAIL_FROM', ''), + "SERVER": environ.get("EMAIL_SERVER", "localhost"), + "PORT": int(environ.get("EMAIL_PORT", 25)), + "USERNAME": environ.get("EMAIL_USERNAME", ""), + "PASSWORD": _read_secret("email_password", environ.get("EMAIL_PASSWORD", "")), + "USE_SSL": environ.get("EMAIL_USE_SSL", "False").lower() == "true", + "USE_TLS": environ.get("EMAIL_USE_TLS", "False").lower() == "true", + "SSL_CERTFILE": environ.get("EMAIL_SSL_CERTFILE", ""), + "SSL_KEYFILE": environ.get("EMAIL_SSL_KEYFILE", ""), + "TIMEOUT": int(environ.get("EMAIL_TIMEOUT", 10)), # seconds + "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 # (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' +ENFORCE_GLOBAL_UNIQUE = environ.get("ENFORCE_GLOBAL_UNIQUE", "False").lower() == "true" # 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 `.`. Add '*' to this list to exempt all models. -EXEMPT_VIEW_PERMISSIONS = list(filter(None, environ.get('EXEMPT_VIEW_PERMISSIONS', '').split(' '))) +EXEMPT_VIEW_PERMISSIONS = list( + filter(None, environ.get("EXEMPT_VIEW_PERMISSIONS", "").split(" ")), +) # Enable GraphQL API. -GRAPHQL_ENABLED = environ.get('GRAPHQL_ENABLED', 'True').lower() == 'true' +GRAPHQL_ENABLED = environ.get("GRAPHQL_ENABLED", "True").lower() == "true" # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: # https://docs.djangoproject.com/en/stable/topics/logging/ @@ -156,93 +195,103 @@ LOGGING = {} # 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. -LOGIN_REQUIRED = environ.get('LOGIN_REQUIRED', 'False').lower() == 'true' +LOGIN_REQUIRED = environ.get("LOGIN_REQUIRED", "False").lower() == "true" # 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]) -LOGIN_TIMEOUT = int(environ.get('LOGIN_TIMEOUT', 1209600)) +LOGIN_TIMEOUT = int(environ.get("LOGIN_TIMEOUT", 1209600)) # 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' +MAINTENANCE_MODE = environ.get("MAINTENANCE_MODE", "False").lower() == "true" # 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 # all objects by specifying "?limit=0". -MAX_PAGE_SIZE = int(environ.get('MAX_PAGE_SIZE', 1000)) +MAX_PAGE_SIZE = int(environ.get("MAX_PAGE_SIZE", 1000)) # 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. -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' -METRICS_ENABLED = environ.get('METRICS_ENABLED', 'False').lower() == 'true' +METRICS_ENABLED = environ.get("METRICS_ENABLED", "False").lower() == "true" # 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_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_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) -PAGINATE_COUNT = int(environ.get('PAGINATE_COUNT', 50)) +PAGINATE_COUNT = int(environ.get("PAGINATE_COUNT", 50)) # Enable installed plugins. Add the name of each plugin to the list. PLUGINS = [] # 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. -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 # prefer IPv4 instead. -PREFER_IPV4 = environ.get('PREFER_IPV4', 'False').lower() == 'true' +PREFER_IPV4 = environ.get("PREFER_IPV4", "False").lower() == "true" # 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(environ.get('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 22)) -RACK_ELEVATION_DEFAULT_UNIT_WIDTH = int(environ.get('RACK_ELEVATION_DEFAULT_UNIT_WIDTH', 220)) +RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = int( + environ.get("RACK_ELEVATION_DEFAULT_UNIT_HEIGHT", 22), +) +RACK_ELEVATION_DEFAULT_UNIT_WIDTH = int( + environ.get("RACK_ELEVATION_DEFAULT_UNIT_WIDTH", 220), +) # Remote authentication support -REMOTE_AUTH_ENABLED = environ.get('REMOTE_AUTH_ENABLED', 'False').lower() == 'true' -REMOTE_AUTH_BACKEND = environ.get('REMOTE_AUTH_BACKEND', '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_DEFAULT_GROUPS = list(filter(None, environ.get('REMOTE_AUTH_DEFAULT_GROUPS', '').split(' '))) +REMOTE_AUTH_ENABLED = environ.get("REMOTE_AUTH_ENABLED", "False").lower() == "true" +REMOTE_AUTH_BACKEND = environ.get( + "REMOTE_AUTH_BACKEND", + "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_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 # 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) # 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') +REPORTS_ROOT = environ.get("REPORTS_ROOT", "/etc/netbox/reports") # Maximum execution time for background tasks, in seconds. -RQ_DEFAULT_TIMEOUT = int(environ.get('RQ_DEFAULT_TIMEOUT', 300)) +RQ_DEFAULT_TIMEOUT = int(environ.get("RQ_DEFAULT_TIMEOUT", 300)) # The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of # this setting is derived from the installed location. -SCRIPTS_ROOT = environ.get('SCRIPTS_ROOT', '/etc/netbox/scripts') +SCRIPTS_ROOT = environ.get("SCRIPTS_ROOT", "/etc/netbox/scripts") # 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 # 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("SESSIONS_ROOT", None) # 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: # https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date -DATE_FORMAT = environ.get('DATE_FORMAT', 'N j, Y') -SHORT_DATE_FORMAT = environ.get('SHORT_DATE_FORMAT', 'Y-m-d') -TIME_FORMAT = environ.get('TIME_FORMAT', 'g:i a') -SHORT_TIME_FORMAT = environ.get('SHORT_TIME_FORMAT', 'H:i:s') -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') +DATE_FORMAT = environ.get("DATE_FORMAT", "N j, Y") +SHORT_DATE_FORMAT = environ.get("SHORT_DATE_FORMAT", "Y-m-d") +TIME_FORMAT = environ.get("TIME_FORMAT", "g:i a") +SHORT_TIME_FORMAT = environ.get("SHORT_TIME_FORMAT", "H:i:s") +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") diff --git a/.devcontainer/configuration/logging.py b/.devcontainer/configuration/logging.py index 30632e0..23d1271 100644 --- a/.devcontainer/configuration/logging.py +++ b/.devcontainer/configuration/logging.py @@ -3,53 +3,52 @@ 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') +LOGLEVEL = environ.get("LOGLEVEL", "INFO") LOGGING = { - - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', - 'style': '{', - }, - 'simple': { - 'format': '{levelname} {message}', - 'style': '{', - }, - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - }, - }, - 'handlers': { - 'console': { - 'level': LOGLEVEL, - 'filters': ['require_debug_false'], - 'class': 'logging.StreamHandler', - 'formatter': 'simple' - }, - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler', - 'filters': ['require_debug_false'] - } - }, - 'loggers': { - 'django': { - 'handlers': ['console'], - 'propagate': True, - }, - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': False, - }, - 'django_auth_ldap': { - 'handlers': ['console',], - 'level': LOGLEVEL, - } - } + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "filters": { + "require_debug_false": { + "()": "django.utils.log.RequireDebugFalse", + }, + }, + "handlers": { + "console": { + "level": LOGLEVEL, + "filters": ["require_debug_false"], + "class": "logging.StreamHandler", + "formatter": "simple", + }, + "mail_admins": { + "level": "ERROR", + "class": "django.utils.log.AdminEmailHandler", + "filters": ["require_debug_false"], + }, + }, + "loggers": { + "django": { + "handlers": ["console"], + "propagate": True, + }, + "django.request": { + "handlers": ["mail_admins"], + "level": "ERROR", + "propagate": False, + }, + "django_auth_ldap": { + "handlers": ["console"], + "level": LOGLEVEL, + }, + }, } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 94e0f1e..0a585d5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,19 +24,15 @@ // "source.organizeImports": true // } //}, - //"python.sortImports.args": [ - // "--profile=black" - //], + "python.sortImports.args": [ + "--profile=black" + ], "python.sortImports.path": "/opt/netbox/venv/bin/isort", "python.analysis.typeCheckingMode": "strict", "python.analysis.extraPaths": [ - // "/opt/netbox/", "/opt/netbox/netbox" ], "python.autoComplete.extraPaths": [ - // "/opt/netbox/netbox/", - // "/opt/netbox/netbox/**", - // "/opt/netbox/netbox/**/**", "/opt/netbox/netbox" ], "python.defaultInterpreterPath": "/opt/netbox/venv/bin/python3", diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0e8e1e0..11f88e1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -77,5 +77,6 @@ into the release notes. Please put an x into the brackets (like `[x]`) if you've completed that task. --> -* [ ] I have explained my PR according to the information in the comments or in a linked issue. +* [ ] I have explained my PR according to the information in the comments + or in a linked issue. * [ ] My PR targets the `dev` branch. diff --git a/.htmlhintrc b/.htmlhintrc new file mode 100644 index 0000000..f4621ba --- /dev/null +++ b/.htmlhintrc @@ -0,0 +1,3 @@ +{ + "doctype-first": false +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f71548..2229037 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - - repo: 'https://github.com/pre-commit/pre-commit-hooks' + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: - id: trailing-whitespace @@ -10,38 +10,45 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - id: check-docstring-first - - repo: 'https://github.com/asottile/add-trailing-comma' + - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.3 hooks: - id: add-trailing-comma args: - - '--py36-plus' - - repo: 'https://github.com/asottile/pyupgrade' + - "--py36-plus" + - repo: https://github.com/asottile/pyupgrade rev: v2.37.3 hooks: - id: pyupgrade args: - - '--py37-plus' - - repo: 'https://github.com/PyCQA/isort' + - "--py37-plus" + - repo: https://github.com/PyCQA/isort rev: 5.10.1 hooks: - id: isort args: - - '--filter-files' - - repo: 'https://github.com/psf/black' + - "--filter-files" + - "--profile=black" + - repo: https://github.com/psf/black rev: 22.6.0 hooks: - id: black language_version: python3 - #- repo: 'https://github.com/adrienverge/yamllint' + #- repo: https://github.com/adrienverge/yamllint # rev: v1.26.3 # hooks: # - id: yamllint - - repo: 'https://github.com/psf/black' + - repo: https://github.com/psf/black rev: 22.6.0 hooks: - id: black - - repo: 'https://github.com/igorshubovych/markdownlint-cli' + - repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs + rev: v1.1.2 + hooks: + - id: htmlhint + # optional custom config: + args: [--config, .htmlhintrc] + - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.32.1 hooks: - id: markdownlint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b693c0..177b898 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,8 +3,10 @@ ## Reporting Bugs * First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) -of NetBox or this plugin [latest stable version](https://github.com/ryanmerolle/netbox-access-lists/releases). If you're running an older version, it's possible that the bug has -already been fixed or you are running a version of the plugin not tested with the NetBox version you are running [Compatibility Matrix](./README.md#compatibility). +of NetBox or this plugin [latest stable version](https://github.com/ryanmerolle/netbox-access-lists/releases). +If you're running an older version, it's possible that the bug has already been fixed +or you are running a version of the plugin not tested with the NetBox version +you are running [Compatibility Matrix](./README.md#compatibility). * Next, check the GitHub [issues list](https://github.com/ryanmerolle/netbox-access-lists/issues) to see if the bug you've found has already been reported. If you think you may diff --git a/LICENSE.txt b/LICENSE.txt index 3fd0bc3..f433b1a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -175,4 +175,3 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - diff --git a/README.md b/README.md index cc40cc7..c5e8e55 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ This plugin was first developed using 3.2.5, and tested with all of 3.2.0. ## Installing -For adding to a NetBox Docker setup see [the general instructions for using netbox-docker with plugins](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins). +For adding to a NetBox Docker setup see +[the general instructions for using netbox-docker with plugins](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins). While this is still in development and not yet on pypi you can install with pip: @@ -38,7 +39,8 @@ or by adding to your `local_requirements.txt` or `plugin_requirements.txt` (netb git+https://github.com/ryanmerolle/netbox-access-lists.git@dev ``` -Enable the plugin in `/opt/netbox/netbox/netbox/configuration.py` or if you use netbox-docker, your `/configuration/plugins.py` file : +Enable the plugin in `/opt/netbox/netbox/netbox/configuration.py`, + or if you use netbox-docker, your `/configuration/plugins.py` file : ```python PLUGINS = [ diff --git a/netbox_access_lists/api/serializers.py b/netbox_access_lists/api/serializers.py index ef7c926..4c5a536 100644 --- a/netbox_access_lists/api/serializers.py +++ b/netbox_access_lists/api/serializers.py @@ -103,7 +103,7 @@ class AccessListSerializer(NetBoxModelSerializer): if "assigned_object_type" in data and "assigned_object_id" in data: try: assigned_object = data["assigned_object_type"].get_object_for_this_type( - id=data["assigned_object_id"] + id=data["assigned_object_id"], ) except ObjectDoesNotExist: # Sets a standard error message for invalid GFK @@ -118,7 +118,7 @@ class AccessListSerializer(NetBoxModelSerializer): and self.instance.rule_count > 0 ): error_message["type"] = [ - "This ACL has ACL rules associated, CANNOT change ACL type." + "This ACL has ACL rules associated, CANNOT change ACL type.", ] if error_message: @@ -180,7 +180,7 @@ class ACLInterfaceAssignmentSerializer(NetBoxModelSerializer): if "assigned_object_type" in data and "assigned_object_id" in data: try: assigned_object = data["assigned_object_type"].get_object_for_this_type( - id=data["assigned_object_id"] + id=data["assigned_object_id"], ) except ObjectDoesNotExist: # Sets a standard error message for invalid GFK @@ -265,7 +265,7 @@ class ACLStandardRuleSerializer(NetBoxModelSerializer): # Check if action set to remark, but source_prefix set. if data.get("source_prefix"): error_message["source_prefix"] = [ - error_message_action_remark_source_prefix_set + error_message_action_remark_source_prefix_set, ] if error_message: @@ -339,27 +339,27 @@ class ACLExtendedRuleSerializer(NetBoxModelSerializer): # Check if action set to remark, but source_prefix set. if data.get("source_prefix"): error_message["source_prefix"] = [ - error_message_action_remark_source_prefix_set + error_message_action_remark_source_prefix_set, ] # Check if action set to remark, but source_ports set. if data.get("source_ports"): error_message["source_ports"] = [ - "Action is set to remark, Source Ports CANNOT be set." + "Action is set to remark, Source Ports CANNOT be set.", ] # Check if action set to remark, but destination_prefix set. if data.get("destination_prefix"): error_message["destination_prefix"] = [ - "Action is set to remark, Destination Prefix CANNOT be set." + "Action is set to remark, Destination Prefix CANNOT be set.", ] # Check if action set to remark, but destination_ports set. if data.get("destination_ports"): error_message["destination_ports"] = [ - "Action is set to remark, Destination Ports CANNOT be set." + "Action is set to remark, Destination Ports CANNOT be set.", ] # Check if action set to remark, but protocol set. if data.get("protocol"): error_message["protocol"] = [ - "Action is set to remark, Protocol CANNOT be set." + "Action is set to remark, Protocol CANNOT be set.", ] if error_message: diff --git a/netbox_access_lists/api/views.py b/netbox_access_lists/api/views.py index 589285f..9cf7729 100644 --- a/netbox_access_lists/api/views.py +++ b/netbox_access_lists/api/views.py @@ -40,7 +40,8 @@ class ACLInterfaceAssignmentViewSet(NetBoxModelViewSet): """ queryset = models.ACLInterfaceAssignment.objects.prefetch_related( - "access_list", "tags" + "access_list", + "tags", ) serializer_class = ACLInterfaceAssignmentSerializer filterset_class = filtersets.ACLInterfaceAssignmentFilterSet diff --git a/netbox_access_lists/forms/models.py b/netbox_access_lists/forms/models.py index bbf0575..a97f201 100644 --- a/netbox_access_lists/forms/models.py +++ b/netbox_access_lists/forms/models.py @@ -32,7 +32,7 @@ __all__ = ( # Sets a standard mark_safe help_text value to be used by the various classes help_text_acl_rule_logic = mark_safe( - "*Note: CANNOT be set if action is set to remark." + "*Note: CANNOT be set if action is set to remark.", ) # 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)." @@ -116,7 +116,7 @@ class AccessListForm(NetBoxModelForm): "default_action": "The default behavior of the ACL.", "name": "The name uniqueness per device is case insensitive.", "type": mark_safe( - "*Note: CANNOT be changed if ACL Rules are assoicated to this Access List." + "*Note: CANNOT be changed if ACL Rules are assoicated to this Access List.", ), } @@ -161,12 +161,12 @@ class AccessListForm(NetBoxModelForm): 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 (either a device, virtual chassis or virtual machine) at a time.", ) # 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." + "Access Lists must be assigned to a device, virtual chassis or virtual machine.", ) if device: @@ -175,12 +175,14 @@ class AccessListForm(NetBoxModelForm): elif virtual_chassis: host_type = "virtual_chassis" existing_acls = AccessList.objects.filter( - name=name, virtual_chassis=virtual_chassis + name=name, + virtual_chassis=virtual_chassis, ).exists() elif virtual_machine: host_type = "virtual_machine" existing_acls = AccessList.objects.filter( - name=name, virtual_machine=virtual_machine + name=name, + virtual_machine=virtual_machine, ).exists() host = cleaned_data.get(host_type) @@ -200,7 +202,7 @@ class AccessListForm(NetBoxModelForm): acl_type == "standard" and self.instance.aclextendedrules.exists() ): error_message["type"] = [ - "This ACL has ACL rules associated, CANNOT change ACL type." + "This ACL has ACL rules associated, CANNOT change ACL type.", ] if error_message: @@ -265,7 +267,7 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm): # }, label="Access List", help_text=mark_safe( - "*Note: Access List must be present on the device already." + "*Note: Access List must be present on the device already.", ), ) comments = CommentField() @@ -304,7 +306,7 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm): ) help_texts = { "direction": mark_safe( - "*Note: CANNOT assign 2 ACLs to the same interface & direction." + "*Note: CANNOT assign 2 ACLs to the same interface & direction.", ), } @@ -338,7 +340,7 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm): if interface or vminterface: assigned_object_id = VMInterface.objects.get(pk=assigned_object.pk).pk assigned_object_type_id = ContentType.objects.get_for_model( - assigned_object + assigned_object, ).pk access_list_host = AccessList.objects.get(pk=access_list.pk).assigned_object @@ -403,7 +405,7 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm): def save(self, *args, **kwargs): # Set assigned object self.instance.assigned_object = self.cleaned_data.get( - "interface" + "interface", ) or self.cleaned_data.get("vminterface") return super().save(*args, **kwargs) @@ -422,7 +424,7 @@ class ACLStandardRuleForm(NetBoxModelForm): "type": "standard", }, help_text=mark_safe( - "*Note: This field will only display Standard ACLs." + "*Note: This field will only display Standard ACLs.", ), label="Access List", ) @@ -457,7 +459,7 @@ class ACLStandardRuleForm(NetBoxModelForm): "index": help_text_acl_rule_index, "action": help_text_acl_action, "remark": mark_safe( - "*Note: CANNOT be set if source prefix OR action is set." + "*Note: CANNOT be set if source prefix OR action is set.", ), } @@ -480,7 +482,7 @@ class ACLStandardRuleForm(NetBoxModelForm): # Check if action set to remark, but source_prefix set. if cleaned_data.get("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. elif cleaned_data.get("remark"): @@ -504,7 +506,7 @@ class ACLExtendedRuleForm(NetBoxModelForm): "type": "extended", }, help_text=mark_safe( - "*Note: This field will only display Extended ACLs." + "*Note: This field will only display Extended ACLs.", ), label="Access List", ) @@ -562,7 +564,7 @@ class ACLExtendedRuleForm(NetBoxModelForm): "index": help_text_acl_rule_index, "protocol": help_text_acl_rule_logic, "remark": mark_safe( - "*Note: CANNOT be set if action is not set to remark." + "*Note: CANNOT be set if action is not set to remark.", ), "source_ports": help_text_acl_rule_logic, } @@ -591,27 +593,27 @@ class ACLExtendedRuleForm(NetBoxModelForm): # Check if action set to remark, but source_prefix set. if cleaned_data.get("source_prefix"): error_message["source_prefix"] = [ - error_message_action_remark_source_prefix_set + error_message_action_remark_source_prefix_set, ] # Check if action set to remark, but source_ports set. if cleaned_data.get("source_ports"): error_message["source_ports"] = [ - "Action is set to remark, Source Ports CANNOT be set." + "Action is set to remark, Source Ports CANNOT be set.", ] # Check if action set to remark, but destination_prefix set. if cleaned_data.get("destination_prefix"): error_message["destination_prefix"] = [ - "Action is set to remark, Destination Prefix CANNOT be set." + "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." + "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." + "Action is set to remark, Protocol CANNOT be set.", ] # Check if action not set to remark, but remark set. elif cleaned_data.get("remark"): diff --git a/netbox_access_lists/migrations/0001_initial.py b/netbox_access_lists/migrations/0001_initial.py index 74d93de..32b0717 100644 --- a/netbox_access_lists/migrations/0001_initial.py +++ b/netbox_access_lists/migrations/0001_initial.py @@ -30,7 +30,9 @@ class Migration(migrations.Migration): ( "id", models.BigAutoField( - auto_created=True, primary_key=True, serialize=False + auto_created=True, + primary_key=True, + serialize=False, ), ), ("created", models.DateTimeField(auto_now_add=True, null=True)), @@ -58,14 +60,15 @@ class Migration(migrations.Migration): ( "tags", taggit.managers.TaggableManager( - through="extras.TaggedItem", to="extras.Tag" + through="extras.TaggedItem", + to="extras.Tag", ), ), ], options={ "ordering": ("name", "device"), "unique_together": { - ("assigned_object_type", "assigned_object_id", "name") + ("assigned_object_type", "assigned_object_id", "name"), }, "verbose_name": "Access List", }, @@ -76,7 +79,9 @@ class Migration(migrations.Migration): ( "id", models.BigAutoField( - auto_created=True, primary_key=True, serialize=False + auto_created=True, + primary_key=True, + serialize=False, ), ), ("created", models.DateTimeField(auto_now_add=True, null=True)), @@ -110,7 +115,8 @@ class Migration(migrations.Migration): ( "tags", taggit.managers.TaggableManager( - through="extras.TaggedItem", to="extras.Tag" + through="extras.TaggedItem", + to="extras.Tag", ), ), ], @@ -127,7 +133,7 @@ class Migration(migrations.Migration): "assigned_object_id", "access_list", "direction", - ) + ), }, "verbose_name": "ACL Interface Assignment", }, @@ -138,7 +144,9 @@ class Migration(migrations.Migration): ( "id", models.BigAutoField( - auto_created=True, primary_key=True, serialize=False + auto_created=True, + primary_key=True, + serialize=False, ), ), ("created", models.DateTimeField(auto_now_add=True, null=True)), @@ -154,7 +162,8 @@ class Migration(migrations.Migration): ( "tags", taggit.managers.TaggableManager( - through="extras.TaggedItem", to="extras.Tag" + through="extras.TaggedItem", + to="extras.Tag", ), ), ( @@ -192,7 +201,9 @@ class Migration(migrations.Migration): ( "id", models.BigAutoField( - auto_created=True, primary_key=True, serialize=False + auto_created=True, + primary_key=True, + serialize=False, ), ), ("created", models.DateTimeField(auto_now_add=True, null=True)), @@ -208,7 +219,8 @@ class Migration(migrations.Migration): ( "tags", taggit.managers.TaggableManager( - through="extras.TaggedItem", to="extras.Tag" + through="extras.TaggedItem", + to="extras.Tag", ), ), ( diff --git a/netbox_access_lists/models/access_lists.py b/netbox_access_lists/models/access_lists.py index a20f428..6c6055f 100644 --- a/netbox_access_lists/models/access_lists.py +++ b/netbox_access_lists/models/access_lists.py @@ -135,7 +135,8 @@ class ACLInterfaceAssignment(NetBoxModel): it conveniently returns the absolute URL for any particular object. """ return reverse( - "plugins:netbox_access_lists:aclinterfaceassignment", args=[self.pk] + "plugins:netbox_access_lists:aclinterfaceassignment", + args=[self.pk], ) def get_direction_color(self): diff --git a/netbox_access_lists/template_content.py b/netbox_access_lists/template_content.py index b5b10b6..fa17039 100644 --- a/netbox_access_lists/template_content.py +++ b/netbox_access_lists/template_content.py @@ -22,7 +22,8 @@ class ACLInterfaceAssignments(PluginTemplateExtension): ctype = ContentType.objects.get_for_model(obj) if ctype.model in ["interface", "vminterface"]: acl_interface_assignments = ACLInterfaceAssignment.objects.filter( - assigned_object_id=obj.pk, assigned_object_type=ctype + assigned_object_id=obj.pk, + assigned_object_type=ctype, ) return self.render( @@ -44,7 +45,8 @@ class AccessLists(PluginTemplateExtension): ctype = ContentType.objects.get_for_model(obj) if ctype.model in ["device", "virtualchassis", "virtualmachine"]: access_lists = AccessList.objects.filter( - assigned_object_id=obj.pk, assigned_object_type=ctype + assigned_object_id=obj.pk, + assigned_object_type=ctype, ) return self.render( diff --git a/netbox_access_lists/urls.py b/netbox_access_lists/urls.py index 0f4f4c3..f5ed0db 100644 --- a/netbox_access_lists/urls.py +++ b/netbox_access_lists/urls.py @@ -11,7 +11,9 @@ urlpatterns = ( # Access Lists path("access-lists/", views.AccessListListView.as_view(), name="accesslist_list"), path( - "access-lists/add/", views.AccessListEditView.as_view(), name="accesslist_add" + "access-lists/add/", + views.AccessListEditView.as_view(), + name="accesslist_add", ), # path('access-lists/edit/', views.AccessListBulkEditView.as_view(), name='accesslist_bulk_edit'), path(