diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..4209120 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,26 @@ +kind: pipeline +type: ssh +name: default + +clone: + disable: true + +server: + host: + from_secret: host + user: + from_secret: user + password: + from_secret: password + +steps: + - name: release + commands: + - cd /mnt/md0/experimental/netbox + - git pull + +trigger: + branch: + - master + event: + - push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6344a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/etc/postgres diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md index 212c0fc..7e405ba 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ -# netbox +### netbox +- [docs](https://netbox.readthedocs.io/en/stable/) +- [git](https://github.com/netbox-community/netbox-docker) + +### services +- [netbox](https://hub.docker.com/r/netboxcommunity/netbox) +- [nginx](https://hub.docker.com/_/nginx) +- [postgres](https://hub.docker.com/_/postgres) +- [redis](https://hub.docker.com/_/redis) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3d5c1c8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,74 @@ +version: "3" + +volumes: + netbox-static-files: + driver: local + netbox-nginx-config: + driver: local + netbox-media-files: + driver: local + netbox-redis-data: + driver: local + +services: + netbox: &netbox + image: netboxcommunity/netbox:v2.9.9 + depends_on: + - postgres + - redis + - redis-cache + - netbox-worker + env_file: env/netbox.env + user: '101' + volumes: + - ./etc/netbox/etc/netbox/config:/etc/netbox/config:z,ro + - ./etc/netbox/etc/netbox/reports:/etc/netbox/reports:z,ro + - ./etc/netbox/etc/netbox/scripts:/etc/netbox/scripts:z,ro + - ./etc/netbox/opt/netbox/initializers:/opt/netbox/initializers:z,ro + - ./etc/netbox/opt/netbox/startup_scripts:/opt/netbox/startup_scripts:z,ro + - netbox-nginx-config:/etc/netbox-nginx:z + - netbox-static-files:/opt/netbox/netbox/static:z + - netbox-media-files:/opt/netbox/netbox/media:z + netbox-worker: + <<: *netbox + depends_on: + - redis + entrypoint: + - python3 + - /opt/netbox/netbox/manage.py + command: + - rqworker + + nginx: + command: nginx -c /etc/netbox-nginx/nginx.conf + image: nginx:1.19-alpine + depends_on: + - netbox + ports: + - "3009:8080" + volumes: + - netbox-nginx-config:/etc/netbox-nginx/:ro + - netbox-static-files:/opt/netbox/netbox/static:ro + + postgres: + image: postgres:13.1-alpine + env_file: env/postgres.env + volumes: + - ./etc/postgres/var/lib/postgresql/data:/var/lib/postgresql/data + + redis: + image: redis:6.0.9-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 + redis-cache: + image: redis:6.0.9-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 diff --git a/env/.gitignore b/env/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/env/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/etc/netbox/etc/netbox/config/configuration.py b/etc/netbox/etc/netbox/config/configuration.py new file mode 100644 index 0000000..639e1b6 --- /dev/null +++ b/etc/netbox/etc/netbox/config/configuration.py @@ -0,0 +1,248 @@ +#### +## We recommend to not edit this file. +## Create separate files to overwrite the settings. +## See `extra.py` as an example. +#### + +import re + +from os.path import dirname, abspath, join +from os import environ + +# For reference see https://netbox.readthedocs.io/en/stable/configuration/ +# Based on https://github.com/netbox-community/netbox/blob/master/netbox/netbox/configuration.example.py + +# Read secret from file +def _read_secret(secret_name, default = None): + try: + f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8') + except EnvironmentError: + return default + else: + with f: + return f.readline().strip() + +_BASE_DIR = dirname(dirname(abspath(__file__))) + +######################### +# # +# Required settings # +# # +######################### + +# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write +# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. +# +# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] +ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split(' ') + +# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: +# https://docs.djangoproject.com/en/stable/ref/settings/#databases +DATABASE = { + 'NAME': environ.get('DB_NAME', 'netbox'), # Database name + 'USER': environ.get('DB_USER', ''), # PostgreSQL username + 'PASSWORD': _read_secret('db_password', environ.get('DB_PASSWORD', '')), + # PostgreSQL password + 'HOST': environ.get('DB_HOST', 'localhost'), # Database server + 'PORT': environ.get('DB_PORT', ''), # Database port (leave blank for default) + 'OPTIONS': {'sslmode': environ.get('DB_SSLMODE', 'prefer')}, + # Database connection SSLMODE + 'CONN_MAX_AGE': int(environ.get('DB_CONN_MAX_AGE', '300')), + # Max database connection age +} + +# 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', + }, + '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', + }, +} + +# 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'], +] + +# 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', +) + +# 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', '') + +# Text to include on the login page above the login form. HTML is allowed. +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', '') + +# Cache timeout in seconds. Set to 0 to dissable caching. Defaults to 900 (15 minutes) +CACHE_TIMEOUT = int(environ.get('CACHE_TIMEOUT', 900)) + +# 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)) + +# 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(' ')))] + +# 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' + +# 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', ''), +} + +# 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' + +# 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(' '))) + +# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: +# https://docs.djangoproject.com/en/stable/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 = 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 = environ.get('LOGIN_TIMEOUT', None) + +# 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' + +# 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)) + +# 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('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 timeout (in seconds). (Default: 30) +NAPALM_TIMEOUT = int(environ.get('NAPALM_TIMEOUT', 30)) + +# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must +# be provided as a dictionary. +NAPALM_ARGS = {} + +# Determine how many objects to display per page within a list. (Default: 50) +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 = { +} + +# 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' + +# 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)) + +# 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(' '))) + +# This determines how often the GitHub API is called to check the latest release of NetBox. Must be at least 1 hour. +RELEASE_CHECK_TIMEOUT = int(environ.get('RELEASE_CHECK_TIMEOUT', 24 * 3600)) + +# 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) + +# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of +# this setting is derived from the installed location. +REPORTS_ROOT = environ.get('REPORTS_ROOT', '/etc/netbox/reports') + +# Maximum execution time for background tasks, in seconds. +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') + +# 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('REPORTS_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/etc/netbox/etc/netbox/config/extra.py b/etc/netbox/etc/netbox/config/extra.py new file mode 100644 index 0000000..46f1877 --- /dev/null +++ b/etc/netbox/etc/netbox/config/extra.py @@ -0,0 +1,55 @@ +#### +## 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', +# ) + + +## NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must +## be provided as a dictionary. +# NAPALM_ARGS = {} + + +## 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/etc/netbox/etc/netbox/config/ldap/ldap_config.py b/etc/netbox/etc/netbox/config/ldap/ldap_config.py new file mode 100644 index 0000000..4cd5b8b --- /dev/null +++ b/etc/netbox/etc/netbox/config/ldap/ldap_config.py @@ -0,0 +1,84 @@ +import ldap + +from django_auth_ldap.config import LDAPSearch +from importlib import import_module +from os import environ + +# 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 +} + +# Set the DN and password for the NetBox service account. +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' + +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 = LDAPSearch(AUTH_LDAP_USER_SEARCH_BASEDN, + ldap.SCOPE_SUBTREE, + "(" + AUTH_LDAP_USER_SEARCH_ATTR + "=%(user)s)") + +# 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 = LDAPSearch(AUTH_LDAP_GROUP_SEARCH_BASEDN, ldap.SCOPE_SUBTREE, + "(objectClass=" + AUTH_LDAP_GROUP_SEARCH_CLASS + ")") +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 = { + "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/etc/netbox/etc/netbox/reports/devices.py.example b/etc/netbox/etc/netbox/reports/devices.py.example new file mode 100644 index 0000000..670eeb6 --- /dev/null +++ b/etc/netbox/etc/netbox/reports/devices.py.example @@ -0,0 +1,46 @@ +from dcim.choices import DeviceStatusChoices +from dcim.models import ConsolePort, Device, PowerPort +from extras.reports import Report + + +class DeviceConnectionsReport(Report): + description = "Validate the minimum physical connections for each device" + + def test_console_connection(self): + + # Check that every console port for every active device has a connection defined. + active = DeviceStatusChoices.STATUS_ACTIVE + for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=active): + if console_port.connected_endpoint is None: + self.log_failure( + console_port.device, + "No console connection defined for {}".format(console_port.name) + ) + elif not console_port.connection_status: + self.log_warning( + console_port.device, + "Console connection for {} marked as planned".format(console_port.name) + ) + else: + self.log_success(console_port.device) + + def test_power_connections(self): + + # Check that every active device has at least two connected power supplies. + for device in Device.objects.filter(status=DeviceStatusChoices.STATUS_ACTIVE): + connected_ports = 0 + for power_port in PowerPort.objects.filter(device=device): + if power_port.connected_endpoint is not None: + connected_ports += 1 + if not power_port.connection_status: + self.log_warning( + device, + "Power connection for {} marked as planned".format(power_port.name) + ) + if connected_ports < 2: + self.log_failure( + device, + "{} connected power supplies found (2 needed)".format(connected_ports) + ) + else: + self.log_success(device) diff --git a/etc/netbox/etc/netbox/scripts/__init__.py b/etc/netbox/etc/netbox/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/etc/netbox/opt/netbox/initializers/aggregates.yml b/etc/netbox/opt/netbox/initializers/aggregates.yml new file mode 100644 index 0000000..d923a04 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/aggregates.yml @@ -0,0 +1,6 @@ +# - prefix: 10.0.0.0/16 +# rir: RFC1918 +# - prefix: fd00:ccdd::/32 +# rir: RFC4193 ULA +# - prefix: 2001:db8::/32 +# rir: RFC3849 diff --git a/etc/netbox/opt/netbox/initializers/cluster_types.yml b/etc/netbox/opt/netbox/initializers/cluster_types.yml new file mode 100644 index 0000000..136dd1d --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/cluster_types.yml @@ -0,0 +1,2 @@ +# - name: Hyper-V +# slug: hyper-v diff --git a/etc/netbox/opt/netbox/initializers/clusters.yml b/etc/netbox/opt/netbox/initializers/clusters.yml new file mode 100644 index 0000000..1b1aca0 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/clusters.yml @@ -0,0 +1,5 @@ +# - name: cluster1 +# type: Hyper-V +# - name: cluster2 +# type: Hyper-V +# site: SING 1 diff --git a/etc/netbox/opt/netbox/initializers/custom_fields.yml b/etc/netbox/opt/netbox/initializers/custom_fields.yml new file mode 100644 index 0000000..4085ab0 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/custom_fields.yml @@ -0,0 +1,97 @@ +## Possible Choices: +## type: +## - text +## - integer +## - boolean +## - date +## - url +## - select +## filter_logic: +## - disabled +## - loose +## - exact +## +## Examples: + +# text_field: +# type: text +# label: Custom Text +# description: Enter text in a text field. +# required: false +# weight: 0 +# on_objects: +# - dcim.models.Device +# - dcim.models.Rack +# - dcim.models.Site +# - dcim.models.DeviceType +# - 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 +# filter_logic: loose +# weight: 10 +# on_objects: +# - tenancy.models.Tenant +# select_field: +# type: select +# label: Choose between items +# required: false +# filter_logic: exact +# 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 +# select_field_auto_weight: +# type: select +# label: Choose between items +# required: false +# filter_logic: loose +# weight: 30 +# on_objects: +# - dcim.models.Device +# choices: +# - value: A +# - value: B +# - value: C +# - value: "D like deprecated" +# weight: 999 +# - value: E +# boolean_field: +# type: boolean +# label: Yes Or No? +# required: true +# filter_logic: loose +# 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 +# filter_logic: disabled +# on_objects: +# - tenancy.models.Tenant +# date_field: +# type: date +# label: Important Date +# required: false +# filter_logic: disabled +# on_objects: +# - dcim.models.Device diff --git a/etc/netbox/opt/netbox/initializers/dcim_interfaces.yml b/etc/netbox/opt/netbox/initializers/dcim_interfaces.yml new file mode 100644 index 0000000..4030530 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/dcim_interfaces.yml @@ -0,0 +1,18 @@ +## Possible Choices: +## type: +## - virtual +## - lag +## - 1000base-t +## - ... and many more. See for yourself: +## https://github.com/netbox-community/netbox/blob/295d4f0394b431351c0cb2c3ecc791df68c6c2fb/netbox/dcim/choices.py#L510 +## +## Examples: + +# - device: server01 +# enabled: true +# type: virtual +# name: to-server02 +# - device: server02 +# enabled: true +# type: virtual +# name: to-server01 diff --git a/etc/netbox/opt/netbox/initializers/device_roles.yml b/etc/netbox/opt/netbox/initializers/device_roles.yml new file mode 100644 index 0000000..ee4234f --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/device_roles.yml @@ -0,0 +1,15 @@ +# - name: switch +# slug: switch +# color: Grey +# - name: router +# slug: router +# color: Cyan +# - name: load-balancer +# slug: load-balancer +# color: Red +# - name: server +# slug: server +# color: Blue +# - name: patchpanel +# slug: patchpanel +# color: Black diff --git a/etc/netbox/opt/netbox/initializers/device_types.yml b/etc/netbox/opt/netbox/initializers/device_types.yml new file mode 100644 index 0000000..9b27da3 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/device_types.yml @@ -0,0 +1,23 @@ +# - model: Model 1 +# manufacturer: Manufacturer 1 +# slug: model-1 +# u_height: 2 +# custom_fields: +# text_field: Description +# - model: Model 2 +# manufacturer: Manufacturer 1 +# slug: model-2 +# custom_fields: +# text_field: Description +# - model: Model 3 +# manufacturer: Manufacturer 1 +# slug: model-3 +# is_full_depth: false +# u_height: 0 +# custom_fields: +# text_field: Description +# - model: Other +# manufacturer: No Name +# slug: other +# custom_fields: +# text_field: Description diff --git a/etc/netbox/opt/netbox/initializers/devices.yml b/etc/netbox/opt/netbox/initializers/devices.yml new file mode 100644 index 0000000..e968503 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/devices.yml @@ -0,0 +1,44 @@ +## Possible Choices: +## face: +## - front +## - rear +## status: +## - offline +## - active +## - planned +## - staged +## - failed +## - inventory +## - decommissioning +## +## Examples: + +# - name: server01 +# device_role: server +# device_type: Other +# site: AMS 1 +# rack: rack-01 +# face: front +# position: 1 +# custom_fields: +# text_field: Description +# - name: server02 +# device_role: server +# device_type: Other +# site: AMS 2 +# rack: rack-02 +# face: front +# position: 2 +# primary_ip4: 10.1.1.2/24 +# primary_ip6: 2001:db8:a000:1::2/64 +# custom_fields: +# text_field: Description +# - name: server03 +# device_role: server +# device_type: Other +# site: SING 1 +# rack: rack-03 +# face: front +# position: 3 +# custom_fields: +# text_field: Description diff --git a/etc/netbox/opt/netbox/initializers/groups.yml b/etc/netbox/opt/netbox/initializers/groups.yml new file mode 100644 index 0000000..b91ef39 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/groups.yml @@ -0,0 +1,35 @@ +## To list all permissions, run: +## +## docker-compose run --rm --entrypoint /bin/bash netbox +## $ ./manage.py migrate +## $ ./manage.py shell +## > from django.contrib.auth.models import Permission +## > print('\n'.join([p.codename for p in Permission.objects.all()])) +## +## Permission lists support wildcards. See the examples below. +## +## Examples: + +# applications: +# users: +# - technical_user +# readers: +# users: +# - reader +# writers: +# users: +# - writer +# permissions: +# - delete_device +# - delete_virtualmachine +# - add_* +# - change_* +# vm_managers: +# permissions: +# - '*_virtualmachine' +# device_managers: +# permissions: +# - '*device*' +# creators: +# permissions: +# - add_* diff --git a/etc/netbox/opt/netbox/initializers/ip_addresses.yml b/etc/netbox/opt/netbox/initializers/ip_addresses.yml new file mode 100644 index 0000000..6ac38e9 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/ip_addresses.yml @@ -0,0 +1,44 @@ +## Possible Choices: +## status: +## - active +## - reserved +## - deprecated +## - dhcp +## role: +## - loopback +## - secondary +## - anycast +## - vip +## - vrrp +## - hsrp +## - glbp +## - carp +## +## Examples: + +# - address: 10.1.1.1/24 +# device: server01 +# interface: to-server02 +# status: active +# vrf: vrf1 +# - address: 2001:db8:a000:1::1/64 +# device: server01 +# interface: to-server02 +# status: active +# vrf: vrf1 +# - address: 10.1.1.2/24 +# device: server02 +# interface: to-server01 +# status: active +# - address: 2001:db8:a000:1::2/64 +# device: server02 +# interface: to-server01 +# status: active +# - address: 10.1.1.10/24 +# description: reserved IP +# status: reserved +# tenant: tenant1 +# - address: 2001:db8:a000:1::10/64 +# description: reserved IP +# status: reserved +# tenant: tenant1 diff --git a/etc/netbox/opt/netbox/initializers/manufacturers.yml b/etc/netbox/opt/netbox/initializers/manufacturers.yml new file mode 100644 index 0000000..023e8e1 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/manufacturers.yml @@ -0,0 +1,6 @@ +# - name: Manufacturer 1 +# slug: manufacturer-1 +# - name: Manufacturer 2 +# slug: manufacturer-2 +# - name: No Name +# slug: no-name diff --git a/etc/netbox/opt/netbox/initializers/platforms.yml b/etc/netbox/opt/netbox/initializers/platforms.yml new file mode 100644 index 0000000..b0b7ba3 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/platforms.yml @@ -0,0 +1,15 @@ +# - name: Platform 1 +# slug: platform-1 +# manufacturer: Manufacturer 1 +# napalm_driver: driver1 +# napalm_args: "{'arg1': 'value1', 'arg2': 'value2'}" +# - name: Platform 2 +# slug: platform-2 +# manufacturer: Manufacturer 2 +# napalm_driver: driver2 +# napalm_args: "{'arg1': 'value1', 'arg2': 'value2'}" +# - name: Platform 3 +# slug: platform-3 +# manufacturer: No Name +# napalm_driver: driver3 +# napalm_args: "{'arg1': 'value1', 'arg2': 'value2'}" diff --git a/etc/netbox/opt/netbox/initializers/prefix_vlan_roles.yml b/etc/netbox/opt/netbox/initializers/prefix_vlan_roles.yml new file mode 100644 index 0000000..0ea7720 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/prefix_vlan_roles.yml @@ -0,0 +1,2 @@ +# - name: Main Management +# slug: main-management diff --git a/etc/netbox/opt/netbox/initializers/prefixes.yml b/etc/netbox/opt/netbox/initializers/prefixes.yml new file mode 100644 index 0000000..fbf3eee --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/prefixes.yml @@ -0,0 +1,29 @@ +## Possible Choices: +## status: +## - container +## - active +## - reserved +## - deprecated +## +## Examples: + +# - description: prefix1 +# prefix: 10.1.1.0/24 +# site: AMS 1 +# status: active +# tenant: tenant1 +# vlan: vlan1 +# - description: prefix2 +# prefix: 10.1.2.0/24 +# site: AMS 2 +# status: active +# tenant: tenant2 +# vlan: vlan2 +# is_pool: true +# vrf: vrf2 +# - description: ipv6 prefix1 +# prefix: 2001:db8:a000:1::/64 +# site: AMS 2 +# status: active +# tenant: tenant2 +# vlan: vlan2 diff --git a/etc/netbox/opt/netbox/initializers/rack_groups.yml b/etc/netbox/opt/netbox/initializers/rack_groups.yml new file mode 100644 index 0000000..244fc00 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/rack_groups.yml @@ -0,0 +1,3 @@ +# - name: cage 101 +# slug: cage-101 +# site: SING 1 diff --git a/etc/netbox/opt/netbox/initializers/rack_roles.yml b/etc/netbox/opt/netbox/initializers/rack_roles.yml new file mode 100644 index 0000000..e8d1e3e --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/rack_roles.yml @@ -0,0 +1,12 @@ +# - name: Role 1 +# slug: role-1 +# color: Pink +# - name: Role 2 +# slug: role-2 +# color: Cyan +# - name: Role 3 +# slug: role-3 +# color: Grey +# - name: Role 4 +# slug: role-4 +# color: Teal diff --git a/etc/netbox/opt/netbox/initializers/racks.yml b/etc/netbox/opt/netbox/initializers/racks.yml new file mode 100644 index 0000000..51502de --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/racks.yml @@ -0,0 +1,41 @@ +## Possible Choices: +## width: +## - 19 +## - 23 +## types: +## - 2-post-frame +## - 4-post-frame +## - 4-post-cabinet +## - wall-frame +## - wall-cabinet +## outer_unit: +## - mm +## - in +## +## Examples: + +# - site: AMS 1 +# name: rack-01 +# role: Role 1 +# type: 4-post-cabinet +# width: 19 +# u_height: 47 +# custom_fields: +# text_field: Description +# - site: AMS 2 +# name: rack-02 +# role: Role 2 +# type: 4-post-cabinet +# width: 19 +# u_height: 47 +# custom_fields: +# text_field: Description +# - site: SING 1 +# name: rack-03 +# group: cage 101 +# role: Role 3 +# type: 4-post-cabinet +# width: 19 +# u_height: 47 +# custom_fields: +# text_field: Description diff --git a/etc/netbox/opt/netbox/initializers/regions.yml b/etc/netbox/opt/netbox/initializers/regions.yml new file mode 100644 index 0000000..40624f3 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/regions.yml @@ -0,0 +1,2 @@ + - name: Saint-Petersburg + slug: spb diff --git a/etc/netbox/opt/netbox/initializers/rirs.yml b/etc/netbox/opt/netbox/initializers/rirs.yml new file mode 100644 index 0000000..0aacd59 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/rirs.yml @@ -0,0 +1,9 @@ +# - is_private: true +# name: RFC1918 +# slug: rfc1918 +# - is_private: true +# name: RFC4193 ULA +# slug: rfc4193-ula +# - is_private: true +# name: RFC3849 +# slug: rfc3849 diff --git a/etc/netbox/opt/netbox/initializers/sites.yml b/etc/netbox/opt/netbox/initializers/sites.yml new file mode 100644 index 0000000..f3e05ba --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/sites.yml @@ -0,0 +1,32 @@ +# - name: AMS 1 +# slug: ams1 +# region: Downtown +# status: active +# facility: Amsterdam 1 +# asn: 12345 +# custom_fields: +# text_field: Description +# - name: AMS 2 +# slug: ams2 +# region: Downtown +# status: active +# facility: Amsterdam 2 +# asn: 54321 +# custom_fields: +# text_field: Description +# - name: AMS 3 +# slug: ams3 +# region: Suburbs +# status: active +# facility: Amsterdam 3 +# asn: 67890 +# custom_fields: +# text_field: Description +# - name: SING 1 +# slug: sing1 +# region: Singapore +# status: active +# facility: Singapore 1 +# asn: 09876 +# custom_fields: +# text_field: Description diff --git a/etc/netbox/opt/netbox/initializers/tenant_groups.yml b/etc/netbox/opt/netbox/initializers/tenant_groups.yml new file mode 100644 index 0000000..87ecd4d --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/tenant_groups.yml @@ -0,0 +1,4 @@ +# - name: Tenant Group 1 +# slug: tenant-group-1 +# - name: Tenant Group 2 +# slug: tenant-group-2 diff --git a/etc/netbox/opt/netbox/initializers/tenants.yml b/etc/netbox/opt/netbox/initializers/tenants.yml new file mode 100644 index 0000000..1dbfbe5 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/tenants.yml @@ -0,0 +1,5 @@ +# - name: tenant1 +# slug: tenant1 +# - name: tenant2 +# slug: tenant2 +# group: Tenant Group 2 diff --git a/etc/netbox/opt/netbox/initializers/users.yml b/etc/netbox/opt/netbox/initializers/users.yml new file mode 100644 index 0000000..2aea62e --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/users.yml @@ -0,0 +1,23 @@ +## To list all permissions, run: +## +## docker-compose run --rm --entrypoint /bin/bash netbox +## $ ./manage.py migrate +## $ ./manage.py shell +## > from django.contrib.auth.models import Permission +## > print('\n'.join([p.codename for p in Permission.objects.all()])) +## +## Permission lists support wildcards. See the examples below. +## +## Examples: + +# technical_user: +# api_token: 0123456789technicaluser789abcdef01234567 # must be looooong! +# reader: +# password: reader +# writer: +# password: writer +# permissions: +# - delete_device +# - delete_virtualmachine +# - add_* +# - change_* diff --git a/etc/netbox/opt/netbox/initializers/virtual_machines.yml b/etc/netbox/opt/netbox/initializers/virtual_machines.yml new file mode 100644 index 0000000..918df93 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/virtual_machines.yml @@ -0,0 +1,28 @@ +## Possible Choices: +## status: +## - active +## - offline +## - staged +## +## Examples: + +# - cluster: cluster1 +# comments: VM1 +# disk: 200 +# memory: 4096 +# name: virtual machine 1 +# platform: Platform 2 +# status: active +# tenant: tenant1 +# vcpus: 8 +# - cluster: cluster1 +# comments: VM2 +# disk: 100 +# memory: 2048 +# name: virtual machine 2 +# platform: Platform 2 +# primary_ip4: 10.1.1.10/24 +# primary_ip6: 2001:db8:a000:1::10/64 +# status: active +# tenant: tenant1 +# vcpus: 8 diff --git a/etc/netbox/opt/netbox/initializers/virtualization_interfaces.yml b/etc/netbox/opt/netbox/initializers/virtualization_interfaces.yml new file mode 100644 index 0000000..aeedd58 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/virtualization_interfaces.yml @@ -0,0 +1,12 @@ +# - description: Network Interface 1 +# enabled: true +# mac_address: 00:77:77:77:77:77 +# mtu: 1500 +# name: Network Interface 1 +# virtual_machine: virtual machine 1 +# - description: Network Interface 2 +# enabled: true +# mac_address: 00:55:55:55:55:55 +# mtu: 1500 +# name: Network Interface 2 +# virtual_machine: virtual machine 1 diff --git a/etc/netbox/opt/netbox/initializers/vlan_groups.yml b/etc/netbox/opt/netbox/initializers/vlan_groups.yml new file mode 100644 index 0000000..3e0bb00 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/vlan_groups.yml @@ -0,0 +1,6 @@ +# - name: VLAN group 1 +# site: AMS 1 +# slug: vlan-group-1 +# - name: VLAN group 2 +# site: AMS 1 +# slug: vlan-group-2 diff --git a/etc/netbox/opt/netbox/initializers/vlans.yml b/etc/netbox/opt/netbox/initializers/vlans.yml new file mode 100644 index 0000000..a8cd521 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/vlans.yml @@ -0,0 +1,19 @@ +## Possible Choices: +## status: +## - active +## - reserved +## - deprecated +## +## Examples: + +# - name: vlan1 +# site: AMS 1 +# status: active +# vid: 5 +# role: Main Management +# description: VLAN 5 for MGMT +# - group: VLAN group 2 +# name: vlan2 +# site: AMS 1 +# status: active +# vid: 1300 diff --git a/etc/netbox/opt/netbox/initializers/vrfs.yml b/etc/netbox/opt/netbox/initializers/vrfs.yml new file mode 100644 index 0000000..40b9031 --- /dev/null +++ b/etc/netbox/opt/netbox/initializers/vrfs.yml @@ -0,0 +1,8 @@ +# - enforce_unique: true +# name: vrf1 +# tenant: tenant1 +# description: main VRF +# - enforce_unique: true +# name: vrf2 +# rd: "6500:6500" +# tenant: tenant2 diff --git a/etc/netbox/opt/netbox/startup_scripts/000_users.py b/etc/netbox/opt/netbox/startup_scripts/000_users.py new file mode 100644 index 0000000..a801d85 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/000_users.py @@ -0,0 +1,23 @@ +import sys + +from django.contrib.auth.models import Group, User +from startup_script_utils import load_yaml, set_permissions +from users.models import Token + +users = load_yaml('/opt/netbox/initializers/users.yml') +if users is None: + sys.exit() + +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']) + + yaml_permissions = user_details.get('permissions', []) + set_permissions(user.user_permissions, yaml_permissions) diff --git a/etc/netbox/opt/netbox/startup_scripts/010_groups.py b/etc/netbox/opt/netbox/startup_scripts/010_groups.py new file mode 100644 index 0000000..951ca96 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/010_groups.py @@ -0,0 +1,23 @@ +import sys + +from django.contrib.auth.models import Group, User +from startup_script_utils import load_yaml, set_permissions + +groups = load_yaml('/opt/netbox/initializers/groups.yml') +if groups is None: + sys.exit() + +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.get('users', []): + user = User.objects.get(username=username) + + if user: + user.groups.add(group) + + yaml_permissions = group_details.get('permissions', []) + set_permissions(group.permissions, yaml_permissions) diff --git a/etc/netbox/opt/netbox/startup_scripts/020_custom_fields.py b/etc/netbox/opt/netbox/startup_scripts/020_custom_fields.py new file mode 100644 index 0000000..2cb48a0 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/020_custom_fields.py @@ -0,0 +1,54 @@ +from extras.models import CustomField, CustomFieldChoice + +from startup_script_utils import load_yaml +import sys + +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) + +customfields = load_yaml('/opt/netbox/initializers/custom_fields.yml') + +if customfields is None: + sys.exit() + +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('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 = cf_details['type'] + + if cf_details.get('weight', 0): + custom_field.weight = cf_details['weight'] + + custom_field.save() + + for idx, choice_details in enumerate(cf_details.get('choices', [])): + choice, _ = CustomFieldChoice.objects.get_or_create( + field=custom_field, + value=choice_details['value'], + defaults={'weight': idx * 10} + ) + + print("πŸ”§ Created custom field", cf_name) diff --git a/etc/netbox/opt/netbox/startup_scripts/030_regions.py b/etc/netbox/opt/netbox/startup_scripts/030_regions.py new file mode 100644 index 0000000..0b61d89 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/030_regions.py @@ -0,0 +1,26 @@ +from dcim.models import Region +from startup_script_utils import load_yaml +import sys + +regions = load_yaml('/opt/netbox/initializers/regions.yml') + +if regions is None: + sys.exit() + +optional_assocs = { + 'parent': (Region, 'name') +} + +for params in regions: + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + region, created = Region.objects.get_or_create(**params) + + if created: + print("🌐 Created region", region.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/040_sites.py b/etc/netbox/opt/netbox/startup_scripts/040_sites.py new file mode 100644 index 0000000..828a86b --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/040_sites.py @@ -0,0 +1,41 @@ +from dcim.models import Region, Site +from extras.models import CustomField, CustomFieldValue +from tenancy.models import Tenant +from startup_script_utils import load_yaml +import sys + +sites = load_yaml('/opt/netbox/initializers/sites.yml') + +if sites is None: + sys.exit() + +optional_assocs = { + 'region': (Region, 'name'), + 'tenant': (Tenant, 'name') +} + +for params in sites: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + site, created = Site.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=site, + value=cf_value + ) + + site.custom_field_values.add(custom_field_value) + + print("πŸ“ Created site", site.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/050_manufacturers.py b/etc/netbox/opt/netbox/startup_scripts/050_manufacturers.py new file mode 100644 index 0000000..b9ebb32 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/050_manufacturers.py @@ -0,0 +1,14 @@ +from dcim.models import Manufacturer +from startup_script_utils import load_yaml +import sys + +manufacturers = load_yaml('/opt/netbox/initializers/manufacturers.yml') + +if manufacturers is None: + sys.exit() + +for params in manufacturers: + manufacturer, created = Manufacturer.objects.get_or_create(**params) + + if created: + print("🏭 Created Manufacturer", manufacturer.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/060_device_types.py b/etc/netbox/opt/netbox/startup_scripts/060_device_types.py new file mode 100644 index 0000000..e6cea93 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/060_device_types.py @@ -0,0 +1,51 @@ +from dcim.models import DeviceType, Manufacturer, Region +from tenancy.models import Tenant +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +device_types = load_yaml('/opt/netbox/initializers/device_types.yml') + +if device_types is None: + sys.exit() + +required_assocs = { + 'manufacturer': (Manufacturer, 'name') +} + +optional_assocs = { + 'region': (Region, 'name'), + 'tenant': (Tenant, 'name') +} + +for params in device_types: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in required_assocs.items(): + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + device_type, created = DeviceType.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=device_type, + value=cf_value + ) + + device_type.custom_field_values.add(custom_field_value) + + print("πŸ”‘ Created device type", device_type.manufacturer, device_type.model) diff --git a/etc/netbox/opt/netbox/startup_scripts/070_rack_roles.py b/etc/netbox/opt/netbox/startup_scripts/070_rack_roles.py new file mode 100644 index 0000000..f5aa2ce --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/070_rack_roles.py @@ -0,0 +1,23 @@ +from dcim.models import RackRole +from utilities.choices import ColorChoices + +from startup_script_utils import load_yaml +import sys + +rack_roles = load_yaml('/opt/netbox/initializers/rack_roles.yml') + +if rack_roles is None: + sys.exit() + +for params in rack_roles: + if 'color' in params: + color = params.pop('color') + + for color_tpl in ColorChoices: + if color in color_tpl: + params['color'] = color_tpl[0] + + rack_role, created = RackRole.objects.get_or_create(**params) + + if created: + print("🎨 Created rack role", rack_role.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/075_rack_groups.py b/etc/netbox/opt/netbox/startup_scripts/075_rack_groups.py new file mode 100644 index 0000000..dc5ec20 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/075_rack_groups.py @@ -0,0 +1,25 @@ +from dcim.models import Site,RackGroup +from startup_script_utils import load_yaml +import sys + +rack_groups = load_yaml('/opt/netbox/initializers/rack_groups.yml') + +if rack_groups is None: + sys.exit() + +required_assocs = { + 'site': (Site, 'name') +} + +for params in rack_groups: + + for assoc, details in required_assocs.items(): + model, field = details + query = { field: params.pop(assoc) } + params[assoc] = model.objects.get(**query) + + rack_group, created = RackGroup.objects.get_or_create(**params) + + if created: + print("🎨 Created rack group", rack_group.name) + diff --git a/etc/netbox/opt/netbox/startup_scripts/080_racks.py b/etc/netbox/opt/netbox/startup_scripts/080_racks.py new file mode 100644 index 0000000..279cb2c --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/080_racks.py @@ -0,0 +1,52 @@ +from dcim.models import Site, RackRole, Rack, RackGroup +from tenancy.models import Tenant +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +racks = load_yaml('/opt/netbox/initializers/racks.yml') + +if racks is None: + sys.exit() + +required_assocs = { + 'site': (Site, 'name') +} + +optional_assocs = { + 'role': (RackRole, 'name'), + 'tenant': (Tenant, 'name'), + 'group': (RackGroup, 'name') +} + +for params in racks: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in required_assocs.items(): + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + rack, created = Rack.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=rack, + value=cf_value + ) + + rack.custom_field_values.add(custom_field_value) + + print("πŸ”³ Created rack", rack.site, rack.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/090_device_roles.py b/etc/netbox/opt/netbox/startup_scripts/090_device_roles.py new file mode 100644 index 0000000..5225cc0 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/090_device_roles.py @@ -0,0 +1,24 @@ +from dcim.models import DeviceRole +from utilities.choices import ColorChoices + +from startup_script_utils import load_yaml +import sys + +device_roles = load_yaml('/opt/netbox/initializers/device_roles.yml') + +if device_roles is None: + sys.exit() + +for params in device_roles: + + if 'color' in params: + color = params.pop('color') + + for color_tpl in ColorChoices: + if color in color_tpl: + params['color'] = color_tpl[0] + + device_role, created = DeviceRole.objects.get_or_create(**params) + + if created: + print("🎨 Created device role", device_role.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/100_platforms.py b/etc/netbox/opt/netbox/startup_scripts/100_platforms.py new file mode 100644 index 0000000..3673230 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/100_platforms.py @@ -0,0 +1,26 @@ +from dcim.models import Manufacturer, Platform +from startup_script_utils import load_yaml +import sys + +platforms = load_yaml('/opt/netbox/initializers/platforms.yml') + +if platforms is None: + sys.exit() + +optional_assocs = { + 'manufacturer': (Manufacturer, 'name'), +} + +for params in platforms: + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + platform, created = Platform.objects.get_or_create(**params) + + if created: + print("πŸ’Ύ Created platform", platform.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/110_tenant_groups.py b/etc/netbox/opt/netbox/startup_scripts/110_tenant_groups.py new file mode 100644 index 0000000..c106d67 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/110_tenant_groups.py @@ -0,0 +1,14 @@ +from tenancy.models import TenantGroup +from startup_script_utils import load_yaml +import sys + +tenant_groups = load_yaml('/opt/netbox/initializers/tenant_groups.yml') + +if tenant_groups is None: + sys.exit() + +for params in tenant_groups: + tenant_group, created = TenantGroup.objects.get_or_create(**params) + + if created: + print("πŸ”³ Created Tenant Group", tenant_group.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/120_tenants.py b/etc/netbox/opt/netbox/startup_scripts/120_tenants.py new file mode 100644 index 0000000..121c83a --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/120_tenants.py @@ -0,0 +1,39 @@ +from tenancy.models import Tenant, TenantGroup +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +tenants = load_yaml('/opt/netbox/initializers/tenants.yml') + +if tenants is None: + sys.exit() + +optional_assocs = { + 'group': (TenantGroup, 'name') +} + +for params in tenants: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + tenant, created = Tenant.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=tenant, + value=cf_value + ) + + tenant.custom_field_values.add(custom_field_value) + + print("πŸ‘©β€πŸ’» Created Tenant", tenant.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/130_devices.py b/etc/netbox/opt/netbox/startup_scripts/130_devices.py new file mode 100644 index 0000000..7233dd0 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/130_devices.py @@ -0,0 +1,59 @@ +from dcim.models import Site, Rack, DeviceRole, DeviceType, Device, Platform +from virtualization.models import Cluster +from tenancy.models import Tenant +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +devices = load_yaml('/opt/netbox/initializers/devices.yml') + +if devices is None: + sys.exit() + +required_assocs = { + 'device_role': (DeviceRole, 'name'), + 'device_type': (DeviceType, 'model'), + 'site': (Site, 'name') +} + +optional_assocs = { + 'tenant': (Tenant, 'name'), + 'platform': (Platform, 'name'), + 'rack': (Rack, 'name'), + 'cluster': (Cluster, 'name') +} + +for params in devices: + custom_fields = params.pop('custom_fields', None) + # primary ips are handled later in `270_primary_ips.py` + params.pop('primary_ip4', None) + params.pop('primary_ip6', None) + + for assoc, details in required_assocs.items(): + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + device, created = Device.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=device, + value=cf_value + ) + + device.custom_field_values.add(custom_field_value) + + print("πŸ–₯️ Created device", device.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/140_cluster_types.py b/etc/netbox/opt/netbox/startup_scripts/140_cluster_types.py new file mode 100644 index 0000000..d39933f --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/140_cluster_types.py @@ -0,0 +1,14 @@ +from virtualization.models import ClusterType +from startup_script_utils import load_yaml +import sys + +cluster_types = load_yaml('/opt/netbox/initializers/cluster_types.yml') + +if cluster_types is None: + sys.exit() + +for params in cluster_types: + cluster_type, created = ClusterType.objects.get_or_create(**params) + + if created: + print("🧰 Created Cluster Type", cluster_type.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/150_rirs.py b/etc/netbox/opt/netbox/startup_scripts/150_rirs.py new file mode 100644 index 0000000..8bcf51f --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/150_rirs.py @@ -0,0 +1,14 @@ +from ipam.models import RIR +from startup_script_utils import load_yaml +import sys + +rirs = load_yaml('/opt/netbox/initializers/rirs.yml') + +if rirs is None: + sys.exit() + +for params in rirs: + rir, created = RIR.objects.get_or_create(**params) + + if created: + print("πŸ—ΊοΈ Created RIR", rir.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/160_aggregates.py b/etc/netbox/opt/netbox/startup_scripts/160_aggregates.py new file mode 100644 index 0000000..0ffe9b0 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/160_aggregates.py @@ -0,0 +1,42 @@ +from ipam.models import Aggregate, RIR + +from extras.models import CustomField, CustomFieldValue + +from netaddr import IPNetwork +from startup_script_utils import load_yaml +import sys + +aggregates = load_yaml('/opt/netbox/initializers/aggregates.yml') + +if aggregates is None: + sys.exit() + +required_assocs = { + 'rir': (RIR, 'name') +} + +for params in aggregates: + custom_fields = params.pop('custom_fields', None) + params['prefix'] = IPNetwork(params['prefix']) + + for assoc, details in required_assocs.items(): + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + aggregate, created = Aggregate.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=aggregate, + value=cf_value + ) + + aggregate.custom_field_values.add(custom_field_value) + + print("πŸ—žοΈ Created Aggregate", aggregate.prefix) diff --git a/etc/netbox/opt/netbox/startup_scripts/170_clusters.py b/etc/netbox/opt/netbox/startup_scripts/170_clusters.py new file mode 100644 index 0000000..a7e2065 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/170_clusters.py @@ -0,0 +1,51 @@ +from dcim.models import Site +from virtualization.models import Cluster, ClusterType, ClusterGroup +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +clusters = load_yaml('/opt/netbox/initializers/clusters.yml') + +if clusters is None: + sys.exit() + +required_assocs = { + 'type': (ClusterType, 'name') +} + +optional_assocs = { + 'site': (Site, 'name'), + 'group': (ClusterGroup, 'name') +} + +for params in clusters: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in required_assocs.items(): + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + cluster, created = Cluster.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=cluster, + value=cf_value + ) + + cluster.custom_field_values.add(custom_field_value) + + print("πŸ—„οΈ Created cluster", cluster.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/180_vrfs.py b/etc/netbox/opt/netbox/startup_scripts/180_vrfs.py new file mode 100644 index 0000000..496710d --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/180_vrfs.py @@ -0,0 +1,42 @@ +from ipam.models import VRF +from tenancy.models import Tenant + +from extras.models import CustomField, CustomFieldValue + +from startup_script_utils import load_yaml +import sys + +vrfs = load_yaml('/opt/netbox/initializers/vrfs.yml') + +if vrfs is None: + sys.exit() + +optional_assocs = { + 'tenant': (Tenant, 'name') +} + +for params in vrfs: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + vrf, created = VRF.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=vrf, + value=cf_value + ) + + vrf.custom_field_values.add(custom_field_value) + + print("πŸ“¦ Created VRF", vrf.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/190_prefix_vlan_roles.py b/etc/netbox/opt/netbox/startup_scripts/190_prefix_vlan_roles.py new file mode 100644 index 0000000..72c8eee --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/190_prefix_vlan_roles.py @@ -0,0 +1,14 @@ +from ipam.models import Role +from startup_script_utils import load_yaml +import sys + +roles = load_yaml('/opt/netbox/initializers/prefix_vlan_roles.yml') + +if roles is None: + sys.exit() + +for params in roles: + role, created = Role.objects.get_or_create(**params) + + if created: + print("⛹️‍ Created Prefix/VLAN Role", role.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/200_vlan_groups.py b/etc/netbox/opt/netbox/startup_scripts/200_vlan_groups.py new file mode 100644 index 0000000..f8dc55d --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/200_vlan_groups.py @@ -0,0 +1,40 @@ +from dcim.models import Site +from ipam.models import VLANGroup +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +vlan_groups = load_yaml('/opt/netbox/initializers/vlan_groups.yml') + +if vlan_groups is None: + sys.exit() + +optional_assocs = { + 'site': (Site, 'name') +} + +for params in vlan_groups: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + vlan_group, created = VLANGroup.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=vlan_group, + value=cf_value + ) + + vlan_group.custom_field_values.add(custom_field_value) + + print("🏘️ Created VLAN Group", vlan_group.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/210_vlans.py b/etc/netbox/opt/netbox/startup_scripts/210_vlans.py new file mode 100644 index 0000000..ceab196 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/210_vlans.py @@ -0,0 +1,45 @@ +from dcim.models import Site +from ipam.models import VLAN, VLANGroup, Role +from tenancy.models import Tenant, TenantGroup +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +vlans = load_yaml('/opt/netbox/initializers/vlans.yml') + +if vlans is None: + sys.exit() + +optional_assocs = { + 'site': (Site, 'name'), + 'tenant': (Tenant, 'name'), + 'tenant_group': (TenantGroup, 'name'), + 'group': (VLANGroup, 'name'), + 'role': (Role, 'name') +} + +for params in vlans: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + vlan, created = VLAN.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=vlan, + value=cf_value + ) + + vlan.custom_field_values.add(custom_field_value) + + print("🏠 Created VLAN", vlan.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/220_prefixes.py b/etc/netbox/opt/netbox/startup_scripts/220_prefixes.py new file mode 100644 index 0000000..b047c8c --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/220_prefixes.py @@ -0,0 +1,46 @@ +from dcim.models import Site +from ipam.models import Prefix, VLAN, Role, VRF +from tenancy.models import Tenant, TenantGroup +from extras.models import CustomField, CustomFieldValue +from netaddr import IPNetwork +from startup_script_utils import load_yaml +import sys + +prefixes = load_yaml('/opt/netbox/initializers/prefixes.yml') + +if prefixes is None: + sys.exit() + +optional_assocs = { + 'site': (Site, 'name'), + 'tenant': (Tenant, 'name'), + 'tenant_group': (TenantGroup, 'name'), + 'vlan': (VLAN, 'name'), + 'role': (Role, 'name'), + 'vrf': (VRF, 'name') +} + +for params in prefixes: + custom_fields = params.pop('custom_fields', None) + params['prefix'] = IPNetwork(params['prefix']) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + params[assoc] = model.objects.get(**query) + + prefix, created = Prefix.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=prefix, + value=cf_value + ) + prefix.custom_field_values.add(custom_field_value) + + print("πŸ“Œ Created Prefix", prefix.prefix) diff --git a/etc/netbox/opt/netbox/startup_scripts/230_virtual_machines.py b/etc/netbox/opt/netbox/startup_scripts/230_virtual_machines.py new file mode 100644 index 0000000..f138886 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/230_virtual_machines.py @@ -0,0 +1,56 @@ +from dcim.models import Site, Platform, DeviceRole +from virtualization.models import Cluster, VirtualMachine +from tenancy.models import Tenant +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +virtual_machines = load_yaml('/opt/netbox/initializers/virtual_machines.yml') + +if virtual_machines is None: + sys.exit() + +required_assocs = { + 'cluster': (Cluster, 'name') +} + +optional_assocs = { + 'tenant': (Tenant, 'name'), + 'platform': (Platform, 'name'), + 'role': (DeviceRole, 'name') +} + +for params in virtual_machines: + custom_fields = params.pop('custom_fields', None) + # primary ips are handled later in `270_primary_ips.py` + params.pop('primary_ip4', None) + params.pop('primary_ip6', None) + + for assoc, details in required_assocs.items(): + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + virtual_machine, created = VirtualMachine.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=virtual_machine, + value=cf_value + ) + + virtual_machine.custom_field_values.add(custom_field_value) + + print("πŸ–₯️ Created virtual machine", virtual_machine.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/240_virtualization_interfaces.py b/etc/netbox/opt/netbox/startup_scripts/240_virtualization_interfaces.py new file mode 100644 index 0000000..f04f30b --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/240_virtualization_interfaces.py @@ -0,0 +1,38 @@ +from virtualization.models import VirtualMachine, VMInterface +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +interfaces = load_yaml('/opt/netbox/initializers/virtualization_interfaces.yml') + +if interfaces is None: + sys.exit() + +required_assocs = { + 'virtual_machine': (VirtualMachine, 'name') +} + +for params in interfaces: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in required_assocs.items(): + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + interface, created = VMInterface.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=interface, + value=cf_value + ) + + interface.custom_field_values.add(custom_field_value) + + print("🧷 Created interface", interface.name, interface.virtual_machine.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/250_dcim_interfaces.py b/etc/netbox/opt/netbox/startup_scripts/250_dcim_interfaces.py new file mode 100644 index 0000000..51f885b --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/250_dcim_interfaces.py @@ -0,0 +1,38 @@ +from dcim.models import Interface, Device +from extras.models import CustomField, CustomFieldValue +from startup_script_utils import load_yaml +import sys + +interfaces= load_yaml('/opt/netbox/initializers/dcim_interfaces.yml') + +if interfaces is None: + sys.exit() + +required_assocs = { + 'device': (Device, 'name') +} + +for params in interfaces: + custom_fields = params.pop('custom_fields', None) + + for assoc, details in required_assocs.items(): + model, field = details + query = { field: params.pop(assoc) } + + params[assoc] = model.objects.get(**query) + + interface, created = Interface.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=interface, + value=cf_value + ) + + interface.custom_field_values.add(custom_field_value) + + print("🧷 Created interface", interface.name, interface.device.name) diff --git a/etc/netbox/opt/netbox/startup_scripts/260_ip_addresses.py b/etc/netbox/opt/netbox/startup_scripts/260_ip_addresses.py new file mode 100644 index 0000000..7d164fd --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/260_ip_addresses.py @@ -0,0 +1,69 @@ +import sys + +from dcim.models import Device, Interface +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from extras.models import CustomField, CustomFieldValue +from ipam.models import VRF, IPAddress +from netaddr import IPNetwork +from startup_script_utils import load_yaml +from tenancy.models import Tenant +from virtualization.models import VirtualMachine, VMInterface + +ip_addresses = load_yaml('/opt/netbox/initializers/ip_addresses.yml') + +if ip_addresses is None: + sys.exit() + +optional_assocs = { + 'tenant': (Tenant, 'name'), + 'vrf': (VRF, 'name'), + 'interface': (None, None) +} + +vm_interface_ct = ContentType.objects.filter(Q(app_label='virtualization', model='vminterface')).first() +interface_ct = ContentType.objects.filter(Q(app_label='dcim', model='interface')).first() + +for params in ip_addresses: + vm = params.pop('virtual_machine', None) + device = params.pop('device', None) + custom_fields = params.pop('custom_fields', None) + params['address'] = IPNetwork(params['address']) + + if vm and device: + print("IP Address can only specify one of the following: virtual_machine or device.") + sys.exit() + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + if assoc == 'interface': + if vm: + vm_id = VirtualMachine.objects.get(name=vm).id + query = { 'name': params.pop(assoc), "virtual_machine_id": vm_id } + params['assigned_object_type'] = vm_interface_ct + params['assigned_object_id'] = VMInterface.objects.get(**query).id + elif device: + dev_id = Device.objects.get(name=device).id + query = { 'name': params.pop(assoc), "device_id": dev_id } + params['assigned_object_type'] = interface_ct + params['assigned_object_id'] = Interface.objects.get(**query).id + else: + query = { field: params.pop(assoc) } + params[assoc] = model.objects.get(**query) + + ip_address, created = IPAddress.objects.get_or_create(**params) + + if created: + if custom_fields is not None: + for cf_name, cf_value in custom_fields.items(): + custom_field = CustomField.objects.get(name=cf_name) + custom_field_value = CustomFieldValue.objects.create( + field=custom_field, + obj=ip_address, + value=cf_value + ) + + ip_address.custom_field_values.add(custom_field_value) + + print("🧬 Created IP Address", ip_address.address) diff --git a/etc/netbox/opt/netbox/startup_scripts/270_primary_ips.py b/etc/netbox/opt/netbox/startup_scripts/270_primary_ips.py new file mode 100644 index 0000000..74acece --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/270_primary_ips.py @@ -0,0 +1,43 @@ +from dcim.models import Device +from ipam.models import IPAddress +from virtualization.models import VirtualMachine +from startup_script_utils import load_yaml +import sys + +def link_primary_ip(assets, asset_model): + for params in assets: + primary_ip_fields = set(params) & {'primary_ip4', 'primary_ip6'} + if not primary_ip_fields: + continue + + for assoc, details in optional_assocs.items(): + if assoc in params: + model, field = details + query = { field: params.pop(assoc) } + + try: + params[assoc] = model.objects.get(**query) + except model.DoesNotExist: + primary_ip_fields -= {assoc} + print(f"⚠️ IP Address '{query[field]}' not found") + + asset = asset_model.objects.get(name=params['name']) + for field in primary_ip_fields: + if getattr(asset, field) != params[field]: + setattr(asset, field, params[field]) + print(f"πŸ”— Define primary IP '{params[field].address}' on '{asset.name}'") + asset.save() + +devices = load_yaml('/opt/netbox/initializers/devices.yml') +virtual_machines = load_yaml('/opt/netbox/initializers/virtual_machines.yml') + +if devices is None and virtual_machines is None: + sys.exit() + +optional_assocs = { + 'primary_ip4': (IPAddress, 'address'), + 'primary_ip6': (IPAddress, 'address') +} + +link_primary_ip(devices, Device) +link_primary_ip(virtual_machines, VirtualMachine) diff --git a/etc/netbox/opt/netbox/startup_scripts/__main__.py b/etc/netbox/opt/netbox/startup_scripts/__main__.py new file mode 100644 index 0000000..dc4cdc9 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/__main__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import runpy +from os import scandir +from os.path import dirname, abspath + +this_dir = dirname(abspath(__file__)) + +def filename(f): + return f.name + +with scandir(this_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 + + print(f"▢️ Running the startup script {f.path}") + try: + runpy.run_path(f.path) + except SystemExit as e: + if e.code is not None and e.code != 0: + print(f"‼️ The startup script {f.path} returned with code {e.code}, exiting.") + raise diff --git a/etc/netbox/opt/netbox/startup_scripts/startup_script_utils/__init__.py b/etc/netbox/opt/netbox/startup_scripts/startup_script_utils/__init__.py new file mode 100644 index 0000000..c3cf28f --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/startup_script_utils/__init__.py @@ -0,0 +1,2 @@ +from .load_yaml import load_yaml +from .permissions import set_permissions diff --git a/etc/netbox/opt/netbox/startup_scripts/startup_script_utils/load_yaml.py b/etc/netbox/opt/netbox/startup_scripts/startup_script_utils/load_yaml.py new file mode 100644 index 0000000..4c16816 --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/startup_script_utils/load_yaml.py @@ -0,0 +1,10 @@ +from ruamel.yaml import YAML +from pathlib import Path + +def load_yaml(yaml_file: str): + yf = Path(yaml_file) + if not yf.is_file(): + return None + with yf.open("r") as stream: + yaml = YAML(typ="safe") + return yaml.load(stream) diff --git a/etc/netbox/opt/netbox/startup_scripts/startup_script_utils/permissions.py b/etc/netbox/opt/netbox/startup_scripts/startup_script_utils/permissions.py new file mode 100644 index 0000000..add83ee --- /dev/null +++ b/etc/netbox/opt/netbox/startup_scripts/startup_script_utils/permissions.py @@ -0,0 +1,18 @@ +from django.contrib.auth.models import Permission + + +def set_permissions(subject, permission_filters): + if subject is None or permission_filters is None: + return + subject.clear() + for permission_filter in permission_filters: + if "*" in permission_filter: + permission_filter_regex = "^" + permission_filter.replace("*", ".*") + "$" + permissions = Permission.objects.filter(codename__iregex=permission_filter_regex) + print(" ⚿ Granting", permissions.count(), "permissions matching '" + permission_filter + "'") + else: + permissions = Permission.objects.filter(codename=permission_filter) + print(" ⚿ Granting permission", permission_filter) + + for permission in permissions: + subject.add(permission)