From 4c9fa076a883813d44e0e5968ec43fe112aff12e Mon Sep 17 00:00:00 2001 From: Damien A Date: Fri, 10 Nov 2023 18:06:55 +0100 Subject: [PATCH] =?UTF-8?q?Add=20Netbox=20Docker=20Compose=20files=20?= =?UTF-8?q?=F0=9F=90=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netbox/Dockerfile | 127 +++++++++ netbox/configuration/configuration.py | 318 +++++++++++++++++++++++ netbox/configuration/extra.py | 49 ++++ netbox/configuration/ldap/extra.py | 28 ++ netbox/configuration/ldap/ldap_config.py | 111 ++++++++ netbox/configuration/logging.py | 55 ++++ netbox/configuration/plugins.py | 13 + netbox/docker-compose.override.yml | 5 + netbox/docker-compose.yml | 90 +++++++ netbox/docker/configuration.docker.py | 91 +++++++ netbox/docker/docker-entrypoint.sh | 99 +++++++ netbox/docker/housekeeping.sh | 8 + netbox/docker/launch-netbox.sh | 57 ++++ netbox/docker/ldap_config.docker.py | 23 ++ netbox/docker/nginx-unit.json | 57 ++++ netbox/env/netbox.env | 34 +++ netbox/env/postgres.env | 3 + netbox/env/redis-cache.env | 1 + netbox/env/redis.env | 1 + 19 files changed, 1170 insertions(+) create mode 100644 netbox/Dockerfile create mode 100644 netbox/configuration/configuration.py create mode 100644 netbox/configuration/extra.py create mode 100644 netbox/configuration/ldap/extra.py create mode 100644 netbox/configuration/ldap/ldap_config.py create mode 100644 netbox/configuration/logging.py create mode 100644 netbox/configuration/plugins.py create mode 100644 netbox/docker-compose.override.yml create mode 100644 netbox/docker-compose.yml create mode 100644 netbox/docker/configuration.docker.py create mode 100755 netbox/docker/docker-entrypoint.sh create mode 100755 netbox/docker/housekeeping.sh create mode 100755 netbox/docker/launch-netbox.sh create mode 100644 netbox/docker/ldap_config.docker.py create mode 100644 netbox/docker/nginx-unit.json create mode 100644 netbox/env/netbox.env create mode 100644 netbox/env/postgres.env create mode 100644 netbox/env/redis-cache.env create mode 100644 netbox/env/redis.env diff --git a/netbox/Dockerfile b/netbox/Dockerfile new file mode 100644 index 0000000..558f764 --- /dev/null +++ b/netbox/Dockerfile @@ -0,0 +1,127 @@ +ARG FROM +FROM ${FROM} as builder + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update -qq \ + && apt-get upgrade \ + --yes -qq --no-install-recommends \ + && apt-get install \ + --yes -qq --no-install-recommends \ + build-essential \ + ca-certificates \ + libldap-dev \ + libpq-dev \ + libsasl2-dev \ + libssl-dev \ + libxml2-dev \ + libxmlsec1 \ + libxmlsec1-dev \ + libxmlsec1-openssl \ + libxslt-dev \ + pkg-config \ + python3-dev \ + python3-pip \ + python3-venv \ + && python3 -m venv /opt/netbox/venv \ + && /opt/netbox/venv/bin/python3 -m pip install --upgrade \ + pip \ + setuptools \ + wheel + +ARG NETBOX_PATH +COPY ${NETBOX_PATH}/requirements.txt requirements-container.txt / +RUN \ + # We compile 'psycopg' in the build process + sed -i -e '/psycopg/d' /requirements.txt && \ + # Gunicorn is not needed because we use Nginx Unit + sed -i -e '/gunicorn/d' /requirements.txt && \ + # We need 'social-auth-core[all]' in the Docker image. But if we put it in our own requirements-container.txt + # we have potential version conflicts and the build will fail. + # That's why we just replace it in the original requirements.txt. + sed -i -e 's/social-auth-core\[openidconnect\]/social-auth-core\[all\]/g' /requirements.txt && \ + /opt/netbox/venv/bin/pip install \ + -r /requirements.txt \ + -r /requirements-container.txt + +### +# Main stage +### + +ARG FROM +FROM ${FROM} as main + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update -qq \ + && apt-get upgrade \ + --yes -qq --no-install-recommends \ + && apt-get install \ + --yes -qq --no-install-recommends \ + bzip2 \ + ca-certificates \ + curl \ + libldap-common \ + libpq5 \ + libxmlsec1-openssl \ + openssh-client \ + openssl \ + python3 \ + python3-distutils \ + tini \ + && curl --silent --output /usr/share/keyrings/nginx-keyring.gpg \ + https://unit.nginx.org/keys/nginx-keyring.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/ubuntu/ lunar unit" \ + > /etc/apt/sources.list.d/unit.list \ + && apt-get update -qq \ + && apt-get install \ + --yes -qq --no-install-recommends \ + unit=1.30.0-1~lunar \ + unit-python3.11=1.30.0-1~lunar \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /opt/netbox/venv /opt/netbox/venv + +ARG NETBOX_PATH +COPY ${NETBOX_PATH} /opt/netbox +# Copy the modified 'requirements*.txt' files, to have the files actually used during installation +COPY --from=builder /requirements.txt /requirements-container.txt /opt/netbox/ + +COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py +COPY docker/ldap_config.docker.py /opt/netbox/netbox/netbox/ldap_config.py +COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh +COPY docker/housekeeping.sh /opt/netbox/housekeeping.sh +COPY docker/launch-netbox.sh /opt/netbox/launch-netbox.sh +COPY configuration/ /etc/netbox/config/ +COPY docker/nginx-unit.json /etc/unit/ + +WORKDIR /opt/netbox/netbox + +# Must set permissions for '/opt/netbox/netbox/media' directory +# to g+w so that pictures can be uploaded to netbox. +RUN mkdir -p static /opt/unit/state/ /opt/unit/tmp/ \ + && chown -R unit:root /opt/unit/ media reports scripts \ + && chmod -R g+w /opt/unit/ media reports scripts \ + && cd /opt/netbox/ && SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python -m mkdocs build \ + --config-file /opt/netbox/mkdocs.yml --site-dir /opt/netbox/netbox/project-static/docs/ \ + && SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py collectstatic --no-input + +ENV LANG=C.utf8 PATH=/opt/netbox/venv/bin:$PATH +ENTRYPOINT [ "/usr/bin/tini", "--" ] + +CMD [ "/opt/netbox/docker-entrypoint.sh", "/opt/netbox/launch-netbox.sh" ] + +LABEL netbox.original-tag="" \ + netbox.git-branch="" \ + netbox.git-ref="" \ + netbox.git-url="" \ +# See https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys + org.opencontainers.image.created="" \ + org.opencontainers.image.title="NetBox Docker" \ + org.opencontainers.image.description="A container based distribution of NetBox, the free and open IPAM and DCIM solution." \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.authors="The netbox-docker contributors." \ + org.opencontainers.image.vendor="The netbox-docker contributors." \ + org.opencontainers.image.url="https://github.com/netbox-community/netbox-docker" \ + org.opencontainers.image.documentation="https://github.com/netbox-community/netbox-docker/wiki" \ + org.opencontainers.image.source="https://github.com/netbox-community/netbox-docker.git" \ + org.opencontainers.image.revision="" \ + org.opencontainers.image.version="" diff --git a/netbox/configuration/configuration.py b/netbox/configuration/configuration.py new file mode 100644 index 0000000..d3bffb4 --- /dev/null +++ b/netbox/configuration/configuration.py @@ -0,0 +1,318 @@ +#### +## We recommend to not edit this file. +## Create separate files to overwrite the settings. +## See `extra.py` as an example. +#### + +import re +from os import environ +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 +def _read_secret(secret_name: str, default: str | None = None) -> str | None: + try: + f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8') + except EnvironmentError: + return default + else: + with f: + 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 == None: + return env_value + + if not map_fn: + return env_value + + return 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__))) + +######################### +# # +# Required settings # +# # +######################### + +# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write +# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. +# +# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] +ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split(' ') +# 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: +# 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': _environ_get_and_map('DB_CONN_MAX_AGE', '300', _AS_INT), + # Max database connection age + 'DISABLE_SERVER_SIDE_CURSORS': _environ_get_and_map('DB_DISABLE_SERVER_SIDE_CURSORS', 'False', _AS_BOOL), + # 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': _environ_get_and_map('REDIS_PORT', 6379, _AS_INT), + 'USERNAME': environ.get('REDIS_USERNAME', ''), + 'PASSWORD': _read_secret('redis_password', environ.get('REDIS_PASSWORD', '')), + 'DATABASE': _environ_get_and_map('REDIS_DATABASE', 0, _AS_INT), + 'SSL': _environ_get_and_map('REDIS_SSL', 'False', _AS_BOOL), + 'INSECURE_SKIP_TLS_VERIFY': _environ_get_and_map('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False', _AS_BOOL), + }, + 'caching': { + 'HOST': environ.get('REDIS_CACHE_HOST', environ.get('REDIS_HOST', 'localhost')), + 'PORT': _environ_get_and_map('REDIS_CACHE_PORT', environ.get('REDIS_PORT', '6379'), _AS_INT), + 'USERNAME': environ.get('REDIS_CACHE_USERNAME', environ.get('REDIS_USERNAME', '')), + 'PASSWORD': _read_secret('redis_cache_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), + 'INSECURE_SKIP_TLS_VERIFY': _environ_get_and_map('REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY', environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False'), _AS_BOOL), + }, +} + +# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. +# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and +# symbols. NetBox will not run without this defined. For more information, see +# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY +SECRET_KEY = _read_secret('secret_key', environ.get('SECRET_KEY', '')) + + +######################### +# # +# Optional settings # +# # +######################### + +# # 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). +# ADMINS = [ +# # ['John Doe', 'jdoe@example.com'], +# ] + +if 'ALLOWED_URL_SCHEMES' in environ: + ALLOWED_URL_SCHEMES = _environ_get_and_map('ALLOWED_URL_SCHEMES', None, _AS_LIST) + +# 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. +if 'BANNER_TOP' in environ: + 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. +if 'BANNER_LOGIN' in environ: + BANNER_LOGIN = environ.get('BANNER_LOGIN', None) + +# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 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 +# 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_and_map('CORS_ORIGIN_ALLOW_ALL', 'False', _AS_BOOL) +CORS_ORIGIN_WHITELIST = _environ_get_and_map('CORS_ORIGIN_WHITELIST', 'https://localhost', _AS_LIST) +CORS_ORIGIN_REGEX_WHITELIST = [re.compile(r) for r in _environ_get_and_map('CORS_ORIGIN_REGEX_WHITELIST', '', _AS_LIST)] + +# 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_and_map('DEBUG', 'False', _AS_BOOL) + +# This parameter serves as a safeguard to prevent some potentially dangerous behavior, +# 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 = { + 'SERVER': environ.get('EMAIL_SERVER', 'localhost'), + 'PORT': _environ_get_and_map('EMAIL_PORT', 25, _AS_INT), + 'USERNAME': environ.get('EMAIL_USERNAME', ''), + 'PASSWORD': _read_secret('email_password', environ.get('EMAIL_PASSWORD', '')), + 'USE_SSL': _environ_get_and_map('EMAIL_USE_SSL', 'False', _AS_BOOL), + 'USE_TLS': _environ_get_and_map('EMAIL_USE_TLS', 'False', _AS_BOOL), + 'SSL_CERTFILE': environ.get('EMAIL_SSL_CERTFILE', ''), + 'SSL_KEYFILE': environ.get('EMAIL_SSL_KEYFILE', ''), + 'TIMEOUT': _environ_get_and_map('EMAIL_TIMEOUT', 10, _AS_INT), # 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. +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 +# by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. +EXEMPT_VIEW_PERMISSIONS = _environ_get_and_map('EXEMPT_VIEW_PERMISSIONS', '', _AS_LIST) + +# 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. +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: +# # https://docs.djangoproject.com/en/stable/topics/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 +# are permitted to access most data in NetBox (excluding secrets) but not make any changes. +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 +# re-authenticate. (Default: 1209600 [14 days]) +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. +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. +# "?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". +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 default value of this setting is derived from the installed location. +MEDIA_ROOT = environ.get('MEDIA_ROOT', join(_BASE_DIR, 'media')) + +# Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' +METRICS_ENABLED = _environ_get_and_map('METRICS_ENABLED', 'False', _AS_BOOL) + +# Determine how many objects to display per page within a list. (Default: 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. +# 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 = { +# } + +# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to +# prefer IPv4 instead. +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. +if 'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT' in environ: + 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 = _environ_get_and_map('RACK_ELEVATION_DEFAULT_UNIT_WIDTH', None, _AS_INT) + +# Remote authentication support +REMOTE_AUTH_ENABLED = _environ_get_and_map('REMOTE_AUTH_ENABLED', 'False', _AS_BOOL) +REMOTE_AUTH_BACKEND = _environ_get_and_map('REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend', _AS_LIST) +REMOTE_AUTH_HEADER = environ.get('REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER') +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_DEFAULT_PERMISSIONS = {} + +# 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. +RELEASE_CHECK_URL = environ.get('RELEASE_CHECK_URL', None) +# RELEASE_CHECK_URL = 'https://api.github.com/repos/netbox-community/netbox/releases' + +# Maximum execution time for background tasks, in seconds. +RQ_DEFAULT_TIMEOUT = _environ_get_and_map('RQ_DEFAULT_TIMEOUT', 300, _AS_INT) + +# The name to use for the csrf token cookie. +CSRF_COOKIE_NAME = environ.get('CSRF_COOKIE_NAME', 'csrftoken') + +# 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 +# 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('SESSION_FILE_PATH', environ.get('SESSIONS_ROOT', None)) + +# Time zone (default: 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') diff --git a/netbox/configuration/extra.py b/netbox/configuration/extra.py new file mode 100644 index 0000000..8bd1337 --- /dev/null +++ b/netbox/configuration/extra.py @@ -0,0 +1,49 @@ +#### +## This file contains extra configuration options that can't be configured +## directly through environment variables. +#### + +## 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). +# ADMINS = [ +# # ['John Doe', 'jdoe@example.com'], +# ] + + +## 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', +# ) + +## Enable installed plugins. Add the name of each plugin to the list. +# from netbox.configuration.configuration import PLUGINS +# PLUGINS.append('my_plugin') + +## 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. +# from netbox.configuration.configuration import PLUGINS_CONFIG +# PLUGINS_CONFIG['my_plugin'] = { +# 'foo': 'bar', +# 'buzz': 'bazz' +# } + + +## Remote authentication support +# REMOTE_AUTH_DEFAULT_PERMISSIONS = {} + + +## By default uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the +## class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example: +# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage' +# STORAGE_CONFIG = { +# 'AWS_ACCESS_KEY_ID': 'Key ID', +# 'AWS_SECRET_ACCESS_KEY': 'Secret', +# 'AWS_STORAGE_BUCKET_NAME': 'netbox', +# 'AWS_S3_REGION_NAME': 'eu-west-1', +# } + + +## This file can contain arbitrary Python code, e.g.: +# from datetime import datetime +# now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") +# BANNER_TOP = f'This instance started on {now}.' diff --git a/netbox/configuration/ldap/extra.py b/netbox/configuration/ldap/extra.py new file mode 100644 index 0000000..4505197 --- /dev/null +++ b/netbox/configuration/ldap/extra.py @@ -0,0 +1,28 @@ +#### +## This file contains extra configuration options that can't be configured +## directly through environment variables. +## All vairables set here overwrite any existing found in ldap_config.py +#### + +# # This Python script inherits all the imports from ldap_config.py +# from django_auth_ldap.config import LDAPGroupQuery # Imported since not in ldap_config.py + +# # Sets a base requirement of membetship to netbox-user-ro, netbox-user-rw, or netbox-user-admin. +# AUTH_LDAP_REQUIRE_GROUP = ( +# LDAPGroupQuery("cn=netbox-user-ro,ou=groups,dc=example,dc=com") +# | LDAPGroupQuery("cn=netbox-user-rw,ou=groups,dc=example,dc=com") +# | LDAPGroupQuery("cn=netbox-user-admin,ou=groups,dc=example,dc=com") +# ) + +# # Sets LDAP Flag groups variables with example. +# AUTH_LDAP_USER_FLAGS_BY_GROUP = { +# "is_staff": ( +# LDAPGroupQuery("cn=netbox-user-ro,ou=groups,dc=example,dc=com") +# | LDAPGroupQuery("cn=netbox-user-rw,ou=groups,dc=example,dc=com") +# | LDAPGroupQuery("cn=netbox-user-admin,ou=groups,dc=example,dc=com") +# ), +# "is_superuser": "cn=netbox-user-admin,ou=groups,dc=example,dc=com", +# } + +# # Sets LDAP Mirror groups variables with example groups +# AUTH_LDAP_MIRROR_GROUPS = ["netbox-user-ro", "netbox-user-rw", "netbox-user-admin"] diff --git a/netbox/configuration/ldap/ldap_config.py b/netbox/configuration/ldap/ldap_config.py new file mode 100644 index 0000000..82fad72 --- /dev/null +++ b/netbox/configuration/ldap/ldap_config.py @@ -0,0 +1,111 @@ +from importlib import import_module +from os import environ + +import ldap +from django_auth_ldap.config import LDAPSearch + + +# Read secret from file +def _read_secret(secret_name, default=None): + try: + f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8') + except EnvironmentError: + return default + else: + with f: + return f.readline().strip() + +# Import and return the group type based on string name +def _import_group_type(group_type_name): + mod = import_module('django_auth_ldap.config') + try: + return getattr(mod, group_type_name)() + except: + return None + +# Server URI +AUTH_LDAP_SERVER_URI = environ.get('AUTH_LDAP_SERVER_URI', '') + +# The following may be needed if you are binding to Active Directory. +AUTH_LDAP_CONNECTION_OPTIONS = { + ldap.OPT_REFERRALS: 0 +} + +AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = environ.get('AUTH_LDAP_BIND_AS_AUTHENTICATING_USER', 'False').lower() == 'true' + +# Set the DN and password for the NetBox service account if needed. +if not AUTH_LDAP_BIND_AS_AUTHENTICATING_USER: + AUTH_LDAP_BIND_DN = environ.get('AUTH_LDAP_BIND_DN', '') + AUTH_LDAP_BIND_PASSWORD = _read_secret('auth_ldap_bind_password', environ.get('AUTH_LDAP_BIND_PASSWORD', '')) + +# Set a string template that describes any user’s distinguished name based on the username. +AUTH_LDAP_USER_DN_TEMPLATE = environ.get('AUTH_LDAP_USER_DN_TEMPLATE', None) + +# Enable STARTTLS for ldap authentication. +AUTH_LDAP_START_TLS = environ.get('AUTH_LDAP_START_TLS', 'False').lower() == 'true' + +# Include this setting if you want to ignore certificate errors. This might be needed to accept a self-signed cert. +# Note that this is a NetBox-specific setting which sets: +# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) +LDAP_IGNORE_CERT_ERRORS = environ.get('LDAP_IGNORE_CERT_ERRORS', 'False').lower() == 'true' + +# Include this setting if you want to validate the LDAP server certificates against a CA certificate directory on your server +# Note that this is a NetBox-specific setting which sets: +# ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, LDAP_CA_CERT_DIR) +LDAP_CA_CERT_DIR = environ.get('LDAP_CA_CERT_DIR', None) + +# Include this setting if you want to validate the LDAP server certificates against your own CA. +# Note that this is a NetBox-specific setting which sets: +# ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, LDAP_CA_CERT_FILE) +LDAP_CA_CERT_FILE = environ.get('LDAP_CA_CERT_FILE', None) + +AUTH_LDAP_USER_SEARCH_BASEDN = environ.get('AUTH_LDAP_USER_SEARCH_BASEDN', '') +AUTH_LDAP_USER_SEARCH_ATTR = environ.get('AUTH_LDAP_USER_SEARCH_ATTR', 'sAMAccountName') +AUTH_LDAP_USER_SEARCH_FILTER: str = environ.get( + 'AUTH_LDAP_USER_SEARCH_FILTER', f'({AUTH_LDAP_USER_SEARCH_ATTR}=%(user)s)' +) + +AUTH_LDAP_USER_SEARCH = LDAPSearch( + AUTH_LDAP_USER_SEARCH_BASEDN, ldap.SCOPE_SUBTREE, AUTH_LDAP_USER_SEARCH_FILTER +) + +# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group +# heirarchy. + +AUTH_LDAP_GROUP_SEARCH_BASEDN = environ.get('AUTH_LDAP_GROUP_SEARCH_BASEDN', '') +AUTH_LDAP_GROUP_SEARCH_CLASS = environ.get('AUTH_LDAP_GROUP_SEARCH_CLASS', 'group') + +AUTH_LDAP_GROUP_SEARCH_FILTER: str = environ.get( + 'AUTH_LDAP_GROUP_SEARCH_FILTER', f'(objectclass={AUTH_LDAP_GROUP_SEARCH_CLASS})' +) +AUTH_LDAP_GROUP_SEARCH = LDAPSearch( + AUTH_LDAP_GROUP_SEARCH_BASEDN, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER +) +AUTH_LDAP_GROUP_TYPE = _import_group_type(environ.get('AUTH_LDAP_GROUP_TYPE', 'GroupOfNamesType')) + +# Define a group required to login. +AUTH_LDAP_REQUIRE_GROUP = environ.get('AUTH_LDAP_REQUIRE_GROUP_DN') + +# Define special user types using groups. Exercise great caution when assigning superuser status. +AUTH_LDAP_USER_FLAGS_BY_GROUP = {} + +if AUTH_LDAP_REQUIRE_GROUP is not None: + AUTH_LDAP_USER_FLAGS_BY_GROUP = { + "is_active": environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', ''), + "is_staff": environ.get('AUTH_LDAP_IS_ADMIN_DN', ''), + "is_superuser": environ.get('AUTH_LDAP_IS_SUPERUSER_DN', '') + } + +# For more granular permissions, we can map LDAP groups to Django groups. +AUTH_LDAP_FIND_GROUP_PERMS = environ.get('AUTH_LDAP_FIND_GROUP_PERMS', 'True').lower() == 'true' +AUTH_LDAP_MIRROR_GROUPS = environ.get('AUTH_LDAP_MIRROR_GROUPS', '').lower() == 'true' + +# Cache groups for one hour to reduce LDAP traffic +AUTH_LDAP_CACHE_TIMEOUT = int(environ.get('AUTH_LDAP_CACHE_TIMEOUT', 3600)) + +# Populate the Django user from the LDAP directory. +AUTH_LDAP_USER_ATTR_MAP = { + "first_name": environ.get('AUTH_LDAP_ATTR_FIRSTNAME', 'givenName'), + "last_name": environ.get('AUTH_LDAP_ATTR_LASTNAME', 'sn'), + "email": environ.get('AUTH_LDAP_ATTR_MAIL', 'mail') +} diff --git a/netbox/configuration/logging.py b/netbox/configuration/logging.py new file mode 100644 index 0000000..d786768 --- /dev/null +++ b/netbox/configuration/logging.py @@ -0,0 +1,55 @@ +# # Remove first comment(#) on each line to implement this working logging example. +# # Add LOGLEVEL environment variable to netbox if you use this example & want a different log level. +# from os import environ + +# # Set LOGLEVEL in netbox.env or docker-compose.overide.yml to override a logging level of INFO. +# LOGLEVEL = environ.get('LOGLEVEL', 'INFO') + +# LOGGING = { + +# 'version': 1, +# 'disable_existing_loggers': 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/netbox/configuration/plugins.py b/netbox/configuration/plugins.py new file mode 100644 index 0000000..c0b1a1f --- /dev/null +++ b/netbox/configuration/plugins.py @@ -0,0 +1,13 @@ +# Add your plugins and plugin settings here. +# Of course uncomment this file out. + +# To learn how to build images with your required plugins +# See https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins + +# PLUGINS = ["netbox_bgp"] + +# PLUGINS_CONFIG = { +# "netbox_bgp": { +# ADD YOUR SETTINGS HERE +# } +# } diff --git a/netbox/docker-compose.override.yml b/netbox/docker-compose.override.yml new file mode 100644 index 0000000..f08d6c0 --- /dev/null +++ b/netbox/docker-compose.override.yml @@ -0,0 +1,5 @@ +version: '3.4' +services: + netbox: + ports: + - 8000:8080 diff --git a/netbox/docker-compose.yml b/netbox/docker-compose.yml new file mode 100644 index 0000000..2b8482c --- /dev/null +++ b/netbox/docker-compose.yml @@ -0,0 +1,90 @@ +version: '3.4' +services: + netbox: &netbox + image: docker.io/netboxcommunity/netbox:${VERSION-v3.6-2.7.0} + depends_on: + - postgres + - redis + - redis-cache + env_file: env/netbox.env + healthcheck: + start_period: 60s + timeout: 3s + interval: 15s + test: "curl -f http://localhost:8080/api/ || exit 1" + volumes: + - ./configuration:/etc/netbox/config:z,ro + - netbox-media-files:/opt/netbox/netbox/media:rw + - netbox-reports-files:/opt/netbox/netbox/reports:rw + - netbox-scripts-files:/opt/netbox/netbox/scripts:rw + platform: linux/amd64 + netbox-worker: + <<: *netbox + depends_on: + netbox: + condition: service_healthy + command: + - /opt/netbox/venv/bin/python + - /opt/netbox/netbox/manage.py + - rqworker + healthcheck: + start_period: 20s + timeout: 3s + interval: 15s + test: "ps -aux | grep -v grep | grep -q rqworker || exit 1" + netbox-housekeeping: + <<: *netbox + depends_on: + netbox: + condition: service_healthy + command: + - /opt/netbox/housekeeping.sh + healthcheck: + start_period: 20s + timeout: 3s + interval: 15s + test: "ps -aux | grep -v grep | grep -q housekeeping || exit 1" + + # postgres + postgres: + image: docker.io/postgres:15-alpine + env_file: env/postgres.env + volumes: + - netbox-postgres-data:/var/lib/postgresql/data + platform: linux/amd64 + + # redis + redis: + image: docker.io/redis:7-alpine + command: + - sh + - -c # this is to evaluate the $REDIS_PASSWORD from the env + - redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose + env_file: env/redis.env + volumes: + - netbox-redis-data:/data + platform: linux/amd64 + redis-cache: + image: docker.io/redis:7-alpine + command: + - sh + - -c # this is to evaluate the $REDIS_PASSWORD from the env + - redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose + env_file: env/redis-cache.env + volumes: + - netbox-redis-cache-data:/data + platform: linux/amd64 + +volumes: + netbox-media-files: + driver: local + netbox-postgres-data: + driver: local + netbox-redis-cache-data: + driver: local + netbox-redis-data: + driver: local + netbox-reports-files: + driver: local + netbox-scripts-files: + driver: local diff --git a/netbox/docker/configuration.docker.py b/netbox/docker/configuration.docker.py new file mode 100644 index 0000000..413f802 --- /dev/null +++ b/netbox/docker/configuration.docker.py @@ -0,0 +1,91 @@ +## Generic Parts +# These functions are providing the functionality to load +# arbitrary configuration files. +# +# They can be imported by other code (see `ldap_config.py` for an example). + +import importlib.util +import sys +from os import scandir +from os.path import abspath, isfile + + +def _filename(f): + return f.name + + +def _import(module_name, path, loaded_configurations): + spec = importlib.util.spec_from_file_location("", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + sys.modules[module_name] = module + + loaded_configurations.insert(0, module) + + print(f"🧬 loaded config '{path}'") + + +def read_configurations(config_module, config_dir, main_config): + loaded_configurations = [] + + main_config_path = abspath(f"{config_dir}/{main_config}.py") + if isfile(main_config_path): + _import(f"{config_module}.{main_config}", main_config_path, loaded_configurations) + else: + print(f"⚠️ Main configuration '{main_config_path}' not found.") + + with scandir(config_dir) as it: + for f in sorted(it, key=_filename): + if not f.is_file(): + continue + + if f.name.startswith("__"): + continue + + if not f.name.endswith(".py"): + continue + + if f.name == f"{main_config}.py": + continue + + if f.name == f"{config_dir}.py": + continue + + module_name = f"{config_module}.{f.name[:-len('.py')]}".replace(".", "_") + _import(module_name, f.path, loaded_configurations) + + if len(loaded_configurations) == 0: + print(f"‼️ No configuration files found in '{config_dir}'.") + raise ImportError(f"No configuration files found in '{config_dir}'.") + + return loaded_configurations + + +## Specific Parts +# This section's code actually loads the various configuration files +# into the module with the given name. +# It contains the logic to resolve arbitrary configuration options by +# levaraging dynamic programming using `__getattr__`. + + +_loaded_configurations = read_configurations( + config_dir="/etc/netbox/config/", + config_module="netbox.configuration", + main_config="configuration", +) + + +def __getattr__(name): + for config in _loaded_configurations: + try: + return getattr(config, name) + except: + pass + raise AttributeError + + +def __dir__(): + names = [] + for config in _loaded_configurations: + names.extend(config.__dir__()) + return names diff --git a/netbox/docker/docker-entrypoint.sh b/netbox/docker/docker-entrypoint.sh new file mode 100755 index 0000000..9b39689 --- /dev/null +++ b/netbox/docker/docker-entrypoint.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Runs on every start of the NetBox Docker container + +# Stop when an error occures +set -e + +# Allows NetBox to be run as non-root users +umask 002 + +# Load correct Python3 env +# shellcheck disable=SC1091 +source /opt/netbox/venv/bin/activate + +# Try to connect to the DB +DB_WAIT_TIMEOUT=${DB_WAIT_TIMEOUT-3} +MAX_DB_WAIT_TIME=${MAX_DB_WAIT_TIME-30} +CUR_DB_WAIT_TIME=0 +while [ "${CUR_DB_WAIT_TIME}" -lt "${MAX_DB_WAIT_TIME}" ]; do + # Read and truncate connection error tracebacks to last line by default + exec {psfd}< <(./manage.py showmigrations 2>&1) + read -rd '' DB_ERR <&$psfd || : + exec {psfd}<&- + wait $! && break + if [ -n "$DB_WAIT_DEBUG" ]; then + echo "$DB_ERR" + else + readarray -tn 0 DB_ERR_LINES <<<"$DB_ERR" + echo "${DB_ERR_LINES[@]: -1}" + echo "[ Use DB_WAIT_DEBUG=1 in netbox.env to print full traceback for errors here ]" + fi + echo "⏳ Waiting on DB... (${CUR_DB_WAIT_TIME}s / ${MAX_DB_WAIT_TIME}s)" + sleep "${DB_WAIT_TIMEOUT}" + CUR_DB_WAIT_TIME=$((CUR_DB_WAIT_TIME + DB_WAIT_TIMEOUT)) +done +if [ "${CUR_DB_WAIT_TIME}" -ge "${MAX_DB_WAIT_TIME}" ]; then + echo "❌ Waited ${MAX_DB_WAIT_TIME}s or more for the DB to become ready." + exit 1 +fi +# Check if update is needed +if ! ./manage.py migrate --check >/dev/null 2>&1; then + echo "⚙️ Applying database migrations" + ./manage.py migrate --no-input + echo "⚙️ Running trace_paths" + ./manage.py trace_paths --no-input + echo "⚙️ Removing stale content types" + ./manage.py remove_stale_contenttypes --no-input + echo "⚙️ Removing expired user sessions" + ./manage.py clearsessions + echo "⚙️ Building search index (lazy)" + ./manage.py reindex --lazy +fi + +# Create Superuser if required +if [ "$SKIP_SUPERUSER" == "true" ]; then + echo "↩️ Skip creating the superuser" +else + if [ -z ${SUPERUSER_NAME+x} ]; then + SUPERUSER_NAME='admin' + fi + if [ -z ${SUPERUSER_EMAIL+x} ]; then + SUPERUSER_EMAIL='admin@example.com' + fi + if [ -f "/run/secrets/superuser_password" ]; then + SUPERUSER_PASSWORD="$(