diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ce32f24 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.github +.travis.yml +build* +*.env diff --git a/Dockerfile b/Dockerfile index 02cdc08..289d7bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,11 @@ RUN apk add --no-cache \ postgresql-dev \ wget -RUN pip install gunicorn +RUN pip install \ +# gunicorn is used for launching netbox + gunicorn \ +# ruamel is used in startup_scripts + ruamel.yaml WORKDIR /opt @@ -31,11 +35,14 @@ RUN pip install -r requirements.txt COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py COPY docker/gunicorn_config.py /opt/netbox/ COPY docker/nginx.conf /etc/netbox-nginx/nginx.conf +COPY docker/docker-entrypoint.sh docker-entrypoint.sh +COPY startup_scripts/ /opt/netbox/startup_scripts/ +COPY initializers/ /opt/netbox/initializers/ +COPY configuration/configuration.py /etc/netbox/configuration.py WORKDIR /opt/netbox/netbox -COPY docker/docker-entrypoint.sh /docker-entrypoint.sh -ENTRYPOINT [ "/docker-entrypoint.sh" ] +ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ] VOLUME ["/etc/netbox-nginx/"] diff --git a/README.md b/README.md index 478dbe4..0390dc2 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,11 @@ For example defining `ALLOWED_HOSTS=localhost ::1 127.0.0.1` would allows access [compose-env]: https://docs.docker.com/compose/environment-variables/ -### Custom Initialisation Code (e.g. Automatically Setting Up Custom Fields) +### Custom Initialization Code (e.g. Automatically Setting Up Custom Fields) -When using `docker-compose`, all the python scripts present in `docker/startup_scripts` will automatically be executed after the application boots in the context of `./manage.py`. +When using `docker-compose`, all the python scripts present in `/opt/netbox/startup_scripts` will automatically be executed after the application boots in the context of `./manage.py`. -That mechanism can be used for many things, and in particular to load Netbox custom fields: +That mechanism can be used for many things, e.g. to create Netbox custom fields: ```python # docker/startup_scripts/load_custom_fields.py @@ -94,12 +94,54 @@ if created: my_custom_field.obj_type.add(device_type) ``` +#### Initializers + +Initializers are built-in startup scripts for defining Netbox custom fields, groups and users. +All you need to do is to mount you own `initializers` folder ([see `docker-compose.yml`][netbox-docker-compose]). +Look at the [`initializers` folder][netbox-docker-initializers] to learn how the files must look like. + +Here's an example for defining a custom field: + +```yaml +# initializers/custom_fields.yml +text_field: + type: text + label: Custom Text + description: Enter text in a text field. + required: false + filterable: true + weight: 0 + on_objects: + - dcim.models.Device + - dcim.models.Rack + - ipam.models.IPAddress + - ipam.models.Prefix + - tenancy.models.Tenant + - virtualization.models.VirtualMachine +``` + +[netbox-docker-initializers]: https://github.com/ninech/netbox-docker/tree/master/initializers +[netbox-docker-compose]: https://github.com/ninech/netbox-docker/blob/master/docker-compose.yml + +#### Custom Docker Image + +You can also build your own Netbox Docker image containing your own startup scripts, custom fields, users and groups +like this: + +``` +ARG VERSION=latest +FROM ninech/netbox:$VERSION + +COPY startup_scripts/ /opt/netbox/startup_scripts/ +COPY initializers/ /opt/netbox/initializers/ +``` + ### Production The default settings are optimized for (local) development environments. You should therefore adjust the configuration for production setups, at least the following variables: -* `ALLOWED_HOSTS`: Add all URLs that lead to your netbox instance. +* `ALLOWED_HOSTS`: Add all URLs that lead to your Netbox instance. * `DB_*`: Use a persistent database. * `EMAIL_*`: Use your own mailserver. * `MAX_PAGE_SIZE`: Use the recommended default of 1000. @@ -109,7 +151,7 @@ You should therefore adjust the configuration for production setups, at least th You may run this image in a cluster such as Docker Swarm, Kubernetes or OpenShift, but this is advanced level. -In this case, we encourage you to statically configure Netbox by starting from [Netbox's example config file][default-config], and mounting it into your container using the mechanism provided by your container platform (i.e. [Docker Swarm configs][swarm-config], [Kubernetes ConfigMap][k8s-config], [OpenShift ConfigMaps][openshift-config]). +In this case, we encourage you to statically configure Netbox by starting from [Netbox's example config file][default-config], and mounting it into your container in the directory `/etc/netbox/` using the mechanism provided by your container platform (i.e. [Docker Swarm configs][swarm-config], [Kubernetes ConfigMap][k8s-config], [OpenShift ConfigMaps][openshift-config]). But if you rather continue to configure your application through environment variables, you may continue to use [the built-in configuration file][docker-config]. We discourage storing secrets in environment variables, as environment variable are passed on to all sub-processes and may leak easily into other systems, e.g. error collecting tools that often collect all environment variables whenever an error occurs. diff --git a/configuration/configuration.py b/configuration/configuration.py new file mode 100644 index 0000000..5eba733 --- /dev/null +++ b/configuration/configuration.py @@ -0,0 +1,156 @@ +import os +import socket + +# For reference see http://netbox.readthedocs.io/en/latest/configuration/mandatory-settings/ +# Based on https://github.com/digitalocean/netbox/blob/develop/netbox/netbox/configuration.example.py + +# Read secret from file +def read_secret(secret_name): + try: + f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8') + except EnvironmentError: + return '' + else: + with f: + return f.readline().strip() + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.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 = os.environ.get('ALLOWED_HOSTS', socket.gethostname()).split(' ') + +# PostgreSQL database configuration. +DATABASE = { + 'NAME': os.environ.get('DB_NAME', 'netbox'), # Database name + 'USER': os.environ.get('DB_USER', ''), # PostgreSQL username + 'PASSWORD': os.environ.get('DB_PASSWORD', read_secret('db_password')), + # PostgreSQL password + 'HOST': os.environ.get('DB_HOST', 'localhost'), # Database server + 'PORT': os.environ.get('DB_PORT', ''), # Database port (leave blank for default) +} + +# 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/dev/ref/settings/#std:setting-SECRET_KEY +SECRET_KEY = os.environ.get('SECRET_KEY', read_secret('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'], +] + +# 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 = os.environ.get('BANNER_TOP', '') +BANNER_BOTTOM = os.environ.get('BANNER_BOTTOM', '') + +# Text to include on the login page above the login form. HTML is allowed. +BANNER_LOGIN = os.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 = os.environ.get('BASE_PATH', '') + +# 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 = os.environ.get('CORS_ORIGIN_ALLOW_ALL', False) +CORS_ORIGIN_WHITELIST = os.environ.get('CORS_ORIGIN_WHITELIST', '').split(' ') +CORS_ORIGIN_REGEX_WHITELIST = [ + # r'^(https?://)?(\w+\.)?example\.com$', +] + +# 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 = os.environ.get('DEBUG', False) + +# Email settings +EMAIL = { + 'SERVER': os.environ.get('EMAIL_SERVER', 'localhost'), + 'PORT': int(os.environ.get('EMAIL_PORT', 25)), + 'USERNAME': os.environ.get('EMAIL_USERNAME', ''), + 'PASSWORD': os.environ.get('EMAIL_PASSWORD', read_secret('email_password')), + 'TIMEOUT': int(os.environ.get('EMAIL_TIMEOUT', 10)), # seconds + 'FROM_EMAIL': os.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 = os.environ.get('ENFORCE_GLOBAL_UNIQUE', False) + +# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: +# https://docs.djangoproject.com/en/1.11/topics/logging/ +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 = os.environ.get('LOGIN_REQUIRED', False) + +# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set: +# BASE_PATH = 'netbox/' +BASE_PATH = os.environ.get('BASE_PATH', '') + +# Setting this to True will display a "maintenance mode" banner at the top of every page. +MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False) + +# 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(os.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 = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media')) + +# Credentials that NetBox will use to access live devices. +NAPALM_USERNAME = os.environ.get('NAPALM_USERNAME', '') +NAPALM_PASSWORD = os.environ.get('NAPALM_PASSWORD', read_secret('napalm_password')) + +# NAPALM timeout (in seconds). (Default: 30) +NAPALM_TIMEOUT = int(os.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(os.environ.get('PAGINATE_COUNT', 50)) + +# 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 = os.environ.get('PREFER_IPV4', False) + +# 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 = os.environ.get('REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')) + +# Time zone (default: UTC) +TIME_ZONE = os.environ.get('TIME_ZONE', 'UTC') + +# Date/time formatting. See the following link for supported formats: +# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date +DATE_FORMAT = os.environ.get('DATE_FORMAT', 'N j, Y') +SHORT_DATE_FORMAT = os.environ.get('SHORT_DATE_FORMAT', 'Y-m-d') +TIME_FORMAT = os.environ.get('TIME_FORMAT', 'g:i a') +SHORT_TIME_FORMAT = os.environ.get('SHORT_TIME_FORMAT', 'H:i:s') +DATETIME_FORMAT = os.environ.get('DATETIME_FORMAT', 'N j, Y g:i a') +SHORT_DATETIME_FORMAT = os.environ.get('SHORT_DATETIME_FORMAT', 'Y-m-d H:i') diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 9177dd2..158defc 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -9,11 +9,13 @@ services: depends_on: - postgres env_file: netbox.env + volumes: + - ./configuration:/etc/netbox:ro command: - ./manage.py - test postgres: - image: postgres:9.6-alpine + image: postgres:10.2-alpine env_file: postgres.env volumes: netbox-static-files: diff --git a/docker-compose.yml b/docker-compose.yml index 34e4572..71735a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,23 +10,25 @@ services: - postgres env_file: netbox.env volumes: - - ./docker/startup_scripts:/opt/netbox/netbox/startup_scripts + - ./startup_scripts:/opt/netbox/startup_scripts:ro + - ./initializers:/opt/netbox/initializers:ro + - ./configuration:/etc/netbox:ro - netbox-nginx-config:/etc/netbox-nginx/ - netbox-static-files:/opt/netbox/netbox/static - netbox-media-files:/opt/netbox/netbox/media - netbox-report-files:/opt/netbox/netbox/reports nginx: - image: nginx:1.11-alpine + image: nginx:1.13-alpine command: nginx -g 'daemon off;' -c /etc/netbox-nginx/nginx.conf depends_on: - netbox ports: - 8080 volumes: - - netbox-static-files:/opt/netbox/netbox/static - - netbox-nginx-config:/etc/netbox-nginx/ + - netbox-static-files:/opt/netbox/netbox/static:ro + - netbox-nginx-config:/etc/netbox-nginx/:ro postgres: - image: postgres:9.6-alpine + image: postgres:10.2-alpine env_file: postgres.env volumes: - netbox-postgres-data:/var/lib/postgresql/data diff --git a/docker/configuration.docker.py b/docker/configuration.docker.py index 5eba733..fbca950 100644 --- a/docker/configuration.docker.py +++ b/docker/configuration.docker.py @@ -1,156 +1,10 @@ -import os -import socket +import importlib.util +import sys -# For reference see http://netbox.readthedocs.io/en/latest/configuration/mandatory-settings/ -# Based on https://github.com/digitalocean/netbox/blob/develop/netbox/netbox/configuration.example.py - -# Read secret from file -def read_secret(secret_name): - try: - f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8') - except EnvironmentError: - return '' - else: - with f: - return f.readline().strip() - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.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 = os.environ.get('ALLOWED_HOSTS', socket.gethostname()).split(' ') - -# PostgreSQL database configuration. -DATABASE = { - 'NAME': os.environ.get('DB_NAME', 'netbox'), # Database name - 'USER': os.environ.get('DB_USER', ''), # PostgreSQL username - 'PASSWORD': os.environ.get('DB_PASSWORD', read_secret('db_password')), - # PostgreSQL password - 'HOST': os.environ.get('DB_HOST', 'localhost'), # Database server - 'PORT': os.environ.get('DB_PORT', ''), # Database port (leave blank for default) -} - -# 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/dev/ref/settings/#std:setting-SECRET_KEY -SECRET_KEY = os.environ.get('SECRET_KEY', read_secret('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'], -] - -# 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 = os.environ.get('BANNER_TOP', '') -BANNER_BOTTOM = os.environ.get('BANNER_BOTTOM', '') - -# Text to include on the login page above the login form. HTML is allowed. -BANNER_LOGIN = os.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 = os.environ.get('BASE_PATH', '') - -# 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 = os.environ.get('CORS_ORIGIN_ALLOW_ALL', False) -CORS_ORIGIN_WHITELIST = os.environ.get('CORS_ORIGIN_WHITELIST', '').split(' ') -CORS_ORIGIN_REGEX_WHITELIST = [ - # r'^(https?://)?(\w+\.)?example\.com$', -] - -# 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 = os.environ.get('DEBUG', False) - -# Email settings -EMAIL = { - 'SERVER': os.environ.get('EMAIL_SERVER', 'localhost'), - 'PORT': int(os.environ.get('EMAIL_PORT', 25)), - 'USERNAME': os.environ.get('EMAIL_USERNAME', ''), - 'PASSWORD': os.environ.get('EMAIL_PASSWORD', read_secret('email_password')), - 'TIMEOUT': int(os.environ.get('EMAIL_TIMEOUT', 10)), # seconds - 'FROM_EMAIL': os.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 = os.environ.get('ENFORCE_GLOBAL_UNIQUE', False) - -# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: -# https://docs.djangoproject.com/en/1.11/topics/logging/ -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 = os.environ.get('LOGIN_REQUIRED', False) - -# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set: -# BASE_PATH = 'netbox/' -BASE_PATH = os.environ.get('BASE_PATH', '') - -# Setting this to True will display a "maintenance mode" banner at the top of every page. -MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False) - -# 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(os.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 = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media')) - -# Credentials that NetBox will use to access live devices. -NAPALM_USERNAME = os.environ.get('NAPALM_USERNAME', '') -NAPALM_PASSWORD = os.environ.get('NAPALM_PASSWORD', read_secret('napalm_password')) - -# NAPALM timeout (in seconds). (Default: 30) -NAPALM_TIMEOUT = int(os.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(os.environ.get('PAGINATE_COUNT', 50)) - -# 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 = os.environ.get('PREFER_IPV4', False) - -# 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 = os.environ.get('REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')) - -# Time zone (default: UTC) -TIME_ZONE = os.environ.get('TIME_ZONE', 'UTC') - -# Date/time formatting. See the following link for supported formats: -# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date -DATE_FORMAT = os.environ.get('DATE_FORMAT', 'N j, Y') -SHORT_DATE_FORMAT = os.environ.get('SHORT_DATE_FORMAT', 'Y-m-d') -TIME_FORMAT = os.environ.get('TIME_FORMAT', 'g:i a') -SHORT_TIME_FORMAT = os.environ.get('SHORT_TIME_FORMAT', 'H:i:s') -DATETIME_FORMAT = os.environ.get('DATETIME_FORMAT', 'N j, Y g:i a') -SHORT_DATETIME_FORMAT = os.environ.get('SHORT_DATETIME_FORMAT', 'Y-m-d H:i') +try: + spec = importlib.util.spec_from_file_location('configuration', '/etc/netbox/configuration.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + sys.modules['netbox.configuration'] = module +except: + raise ImportError('') diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index ff22cad..bcf418f 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -39,7 +39,8 @@ if not User.objects.filter(username='${SUPERUSER_NAME}'): Token.objects.create(user=u, key='${SUPERUSER_API_TOKEN}') END -for script in $(ls startup_scripts/*.py 2> /dev/null); do +for script in /opt/netbox/startup_scripts/*.py; do + echo "⚙️ Executing '$script'" ./manage.py shell --plain < "${script}" done @@ -48,5 +49,6 @@ done echo "✅ Initialisation is done." -# launch whatever is passed by docker via RUN +# launch whatever is passed by docker +# (i.e. the RUN instruction in the Dockerfile) exec ${@} diff --git a/docker/gunicorn_config.py b/docker/gunicorn_config.py index 65f4b89..059e394 100644 --- a/docker/gunicorn_config.py +++ b/docker/gunicorn_config.py @@ -2,7 +2,6 @@ command = '/usr/bin/gunicorn' pythonpath = '/opt/netbox/netbox' bind = '0.0.0.0:8001' workers = 3 -user = 'root' errorlog = '-' accesslog = '-' capture_output = False diff --git a/docker/startup_scripts/.gitkeep b/docker/startup_scripts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/initializers/custom_fields.yml b/initializers/custom_fields.yml new file mode 100644 index 0000000..ccde9a0 --- /dev/null +++ b/initializers/custom_fields.yml @@ -0,0 +1,66 @@ +# text_field: +# type: text +# label: Custom Text +# description: Enter text in a text field. +# required: false +# filterable: true +# weight: 0 +# on_objects: +# - dcim.models.Device +# - dcim.models.Rack +# - ipam.models.IPAddress +# - ipam.models.Prefix +# - tenancy.models.Tenant +# - virtualization.models.VirtualMachine +# integer_field: +# type: integer +# label: Custom Number +# description: Enter numbers into an integer field. +# required: true +# filterable: true +# weight: 10 +# on_objects: +# - tenancy.models.Tenant +# selection_field: +# type: selection +# label: Choose between items +# required: false +# filterable: true +# weight: 30 +# on_objects: +# - dcim.models.Device +# choices: +# - value: First Item +# weight: 10 +# - value: Second Item +# weight: 20 +# - value: Third Item +# weight: 30 +# - value: Fifth Item +# weight: 50 +# - value: Fourth Item +# weight: 40 +# boolean_field: +# type: boolean +# label: Yes Or No? +# required: true +# filterable: true +# default: "false" # important: but "false" in quotes! +# weight: 90 +# on_objects: +# - dcim.models.Device +# url_field: +# type: url +# label: Hyperlink +# description: Link to something nice. +# required: true +# filterable: false +# on_objects: +# - tenancy.models.Tenant +# date_field: +# type: date +# label: Important Date +# required: false +# filterable: false +# on_objects: +# - dcim.models.Device diff --git a/initializers/groups.yml b/initializers/groups.yml new file mode 100644 index 0000000..1f4a5a7 --- /dev/null +++ b/initializers/groups.yml @@ -0,0 +1,9 @@ +# applications: +# users: +# - technical_user +# readers: +# users: +# - reader +# writers: +# users: +# - writer diff --git a/initializers/users.yml b/initializers/users.yml new file mode 100644 index 0000000..ed57fef --- /dev/null +++ b/initializers/users.yml @@ -0,0 +1,6 @@ +# technical_user: +# api_token: 0123456789technicaluser789abcdef01234567 # must be looooong! +# reader: +# password: reader +# writer: +# password: writer diff --git a/startup_scripts/00_users.py b/startup_scripts/00_users.py new file mode 100644 index 0000000..1db0ebe --- /dev/null +++ b/startup_scripts/00_users.py @@ -0,0 +1,20 @@ +from django.contrib.auth.models import Group, User +from users.models import Token + +from ruamel.yaml import YAML + +with open('/opt/netbox/initializers/users.yml', 'r') as stream: + yaml=YAML(typ='safe') + users = yaml.load(stream) + + if users is not None: + for username, user_details in users.items(): + if not User.objects.filter(username=username): + user = User.objects.create_user( + username = username, + password = user_details.get('password', 0) or User.objects.make_random_password) + + print("👤 Created user ",username) + + if user_details.get('api_token', 0): + Token.objects.create(user=user, key=user_details['api_token']) diff --git a/startup_scripts/10_groups.py b/startup_scripts/10_groups.py new file mode 100644 index 0000000..7932874 --- /dev/null +++ b/startup_scripts/10_groups.py @@ -0,0 +1,19 @@ +from django.contrib.auth.models import Group, User +from ruamel.yaml import YAML + +with open('/opt/netbox/initializers/groups.yml', 'r') as stream: + yaml=YAML(typ='safe') + groups = yaml.load(stream) + + if groups is not None: + for groupname, group_details in groups.items(): + group, created = Group.objects.get_or_create(name=groupname) + + if created: + print("👥 Created group", groupname) + + for username in group_details['users']: + user = User.objects.get(username=username) + + if user: + user.groups.add(group) diff --git a/startup_scripts/20_custom_fields.py b/startup_scripts/20_custom_fields.py new file mode 100644 index 0000000..5c40e37 --- /dev/null +++ b/startup_scripts/20_custom_fields.py @@ -0,0 +1,68 @@ +from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_URL, CF_TYPE_SELECT +from extras.models import CustomField, CustomFieldChoice + +from ruamel.yaml import YAML + +text_to_fields = { + 'boolean': CF_TYPE_BOOLEAN, + 'date': CF_TYPE_DATE, + 'integer': CF_TYPE_INTEGER, + 'selection': CF_TYPE_SELECT, + 'text': CF_TYPE_TEXT, + 'url': CF_TYPE_URL, +} + +def get_class_for_class_path(class_path): + import importlib + from django.contrib.contenttypes.models import ContentType + + module_name, class_name = class_path.rsplit(".", 1) + module = importlib.import_module(module_name) + clazz = getattr(module, class_name) + return ContentType.objects.get_for_model(clazz) + +with open('/opt/netbox/initializers/custom_fields.yml', 'r') as stream: + yaml = YAML(typ='safe') + customfields = yaml.load(stream) + + if customfields is not None: + for cf_name, cf_details in customfields.items(): + custom_field, created = CustomField.objects.get_or_create(name = cf_name) + + if created: + if cf_details.get('default', 0): + custom_field.default = cf_details['default'] + + if cf_details.get('description', 0): + custom_field.description = cf_details['description'] + + if cf_details.get('filterable', 0): + custom_field.is_filterables = cf_details['filterable'] + + if cf_details.get('label', 0): + custom_field.label = cf_details['label'] + + for object_type in cf_details.get('on_objects', []): + custom_field.obj_type.add(get_class_for_class_path(object_type)) + + if cf_details.get('required', 0): + custom_field.required = cf_details['required'] + + if cf_details.get('type', 0): + custom_field.type = text_to_fields[cf_details['type']] + + if cf_details.get('weight', 0): + custom_field.weight = cf_details['weight'] + + custom_field.save() + + for choice_details in cf_details.get('choices', []): + choice = CustomFieldChoice.objects.create( + field=custom_field, + value=choice_details['value']) + + if choice_details.get('weight', 0): + choice.weight = choice_details['weight'] + choice.save() + + print("🔧 Created custom field", cf_name)