Skip to content

Commit

Permalink
Merge pull request #343 from netbox-community/DynamicVariables
Browse files Browse the repository at this point in the history
Dynamic Configuration
  • Loading branch information
cimnine authored Oct 26, 2020
2 parents 121c3f8 + 58050e5 commit 64d82b5
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 144 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
.initializers
docker-compose.override.yml
*.pem
configuration/*
!configuration/configuration.py
!configuration/extra.py
configuration/ldap/*
!configuration/ldap/ldap_config.py
7 changes: 3 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ ARG NETBOX_PATH
COPY ${NETBOX_PATH} /opt/netbox

COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
COPY configuration/gunicorn_config.py /etc/netbox/config/
COPY docker/gunicorn_config.py /etc/netbox/
COPY docker/nginx.conf /etc/netbox-nginx/nginx.conf
COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh
COPY startup_scripts/ /opt/netbox/startup_scripts/
COPY initializers/ /opt/netbox/initializers/
COPY configuration/configuration.py /etc/netbox/config/configuration.py
COPY configuration/ /etc/netbox/config/

WORKDIR /opt/netbox/netbox

Expand All @@ -79,7 +79,7 @@ RUN mkdir static && chmod -R g+w static media

ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ]

CMD ["gunicorn", "-c /etc/netbox/config/gunicorn_config.py", "netbox.wsgi"]
CMD ["gunicorn", "-c /etc/netbox/gunicorn_config.py", "netbox.wsgi"]

LABEL ORIGINAL_TAG="" \
NETBOX_GIT_BRANCH="" \
Expand Down Expand Up @@ -122,4 +122,3 @@ RUN apk add --no-cache \
util-linux

COPY docker/ldap_config.docker.py /opt/netbox/netbox/netbox/ldap_config.py
COPY configuration/ldap_config.py /etc/netbox/config/ldap_config.py
225 changes: 125 additions & 100 deletions configuration/configuration.py

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions configuration/extra.py
Original file line number Diff line number Diff line change
@@ -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', '[email protected]'],
# ]


## 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'<marquee width="200px">This instance started on {now}.</marquee>'
48 changes: 24 additions & 24 deletions configuration/ldap_config.py → configuration/ldap/ldap_config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import ldap
import os

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=''):
def _read_secret(secret_name, default=None):
try:
f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8')
except EnvironmentError:
Expand All @@ -15,70 +15,70 @@ def read_secret(secret_name, default=''):
return f.readline().strip()

# Import and return the group type based on string name
def import_group_type(group_type_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 = os.environ.get('AUTH_LDAP_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 = os.environ.get('AUTH_LDAP_BIND_DN', '')
AUTH_LDAP_BIND_PASSWORD = read_secret('auth_ldap_bind_password', os.environ.get('AUTH_LDAP_BIND_PASSWORD', ''))
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 = os.environ.get('AUTH_LDAP_USER_DN_TEMPLATE', None)
AUTH_LDAP_USER_DN_TEMPLATE = environ.get('AUTH_LDAP_USER_DN_TEMPLATE', None)

# Enable STARTTLS for ldap authentication.
AUTH_LDAP_START_TLS = os.environ.get('AUTH_LDAP_START_TLS', 'False').lower() == 'true'
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 = os.environ.get('LDAP_IGNORE_CERT_ERRORS', 'False').lower() == 'true'
LDAP_IGNORE_CERT_ERRORS = environ.get('LDAP_IGNORE_CERT_ERRORS', 'False').lower() == 'true'

AUTH_LDAP_USER_SEARCH_BASEDN = os.environ.get('AUTH_LDAP_USER_SEARCH_BASEDN', '')
AUTH_LDAP_USER_SEARCH_ATTR = os.environ.get('AUTH_LDAP_USER_SEARCH_ATTR', 'sAMAccountName')
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 = os.environ.get('AUTH_LDAP_GROUP_SEARCH_BASEDN', '')
AUTH_LDAP_GROUP_SEARCH_CLASS = os.environ.get('AUTH_LDAP_GROUP_SEARCH_CLASS', 'group')
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(os.environ.get('AUTH_LDAP_GROUP_TYPE', 'GroupOfNamesType'))
AUTH_LDAP_GROUP_TYPE = _import_group_type(environ.get('AUTH_LDAP_GROUP_TYPE', 'GroupOfNamesType'))

# Define a group required to login.
AUTH_LDAP_REQUIRE_GROUP = os.environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', '')
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": os.environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', ''),
"is_staff": os.environ.get('AUTH_LDAP_IS_ADMIN_DN', ''),
"is_superuser": os.environ.get('AUTH_LDAP_IS_SUPERUSER_DN', '')
"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 = os.environ.get('AUTH_LDAP_FIND_GROUP_PERMS', 'True').lower() == 'true'
AUTH_LDAP_MIRROR_GROUPS = os.environ.get('AUTH_LDAP_MIRROR_GROUPS', None).lower() == 'true'
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', None).lower() == 'true'

# Cache groups for one hour to reduce LDAP traffic
AUTH_LDAP_CACHE_TIMEOUT = int(os.environ.get('AUTH_LDAP_CACHE_TIMEOUT', 3600))
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": os.environ.get('AUTH_LDAP_ATTR_FIRSTNAME', 'givenName'),
"last_name": os.environ.get('AUTH_LDAP_ATTR_LASTNAME', 'sn'),
"email": os.environ.get('AUTH_LDAP_ATTR_MAIL', 'mail')
"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')
}
79 changes: 74 additions & 5 deletions docker/configuration.docker.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,79 @@
## Generic Parts
# These functions are providing the functionality to load
# arbitrary configuration files.
#
# They can be imported by other code (see `ldap_config.py` for an example).

from os.path import abspath, isfile
from os import scandir
import importlib.util
import sys

try:
spec = importlib.util.spec_from_file_location('configuration', '/etc/netbox/config/configuration.py')
def _filename(f):
return f.name


def _import(module_name, path, loaded_configurations):
spec = importlib.util.spec_from_file_location('', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
sys.modules['netbox.configuration'] = module
except:
raise ImportError('')
sys.modules[module_name] = module

loaded_configurations.insert(0, module)

print(f"🧬 loaded config '{path}'")


def read_configurations(config_module, config_dir, main_config):
loaded_configurations = []

main_config_path = abspath(f'{config_dir}/{main_config}.py')
if isfile(main_config_path):
_import(f'{config_module}.{main_config}', main_config_path, loaded_configurations)
else:
print(f"⚠️ Main configuration '{main_config_path}' not found.")

with scandir(config_dir) as it:
for f in sorted(it, key=_filename):
if not f.is_file():
continue

if f.name.startswith('__'):
continue

if not f.name.endswith('.py'):
continue

if f.name == f'{config_dir}.py':
continue

module_name = f"{config_module}.{f.name[:-len('.py')]}".replace(".", "_")
_import(module_name, f.path, loaded_configurations)

if len(loaded_configurations) == 0:
print(f"‼️ No configuration files found in '{config_dir}'.")
raise ImportError(f"No configuration files found in '{config_dir}'.")

return loaded_configurations


## Specific Parts
# This section's code actually loads the various configuration files
# into the module with the given name.
# It contains the logic to resolve arbitrary configuration options by
# levaraging dynamic programming using `__getattr__`.


_loaded_configurations = read_configurations(
config_dir = '/etc/netbox/config/',
config_module = 'netbox.configuration',
main_config = 'configuration')


def __getattr__(name):
for config in _loaded_configurations:
try:
return getattr(config, name)
except:
pass
raise AttributeError
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
errorlog = '-'
accesslog = '-'
capture_output = False
loglevel = 'debug'
loglevel = 'info'
23 changes: 14 additions & 9 deletions docker/ldap_config.docker.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import importlib.util
import sys
from .configuration import read_configurations

try:
spec = importlib.util.spec_from_file_location('ldap_config', '/etc/netbox/config/ldap_config.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
sys.modules['netbox.ldap_config'] = module
except:
raise ImportError('')
_loaded_configurations = read_configurations(
config_dir = '/etc/netbox/config/ldap/',
config_module = 'netbox.configuration.ldap',
main_config = 'ldap_config')


def __getattr__(name):
for config in _loaded_configurations:
try:
return getattr(config, name)
except:
pass
raise AttributeError
2 changes: 1 addition & 1 deletion startup_scripts/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
def filename(f):
return f.name

with scandir(dirname(abspath(__file__))) as it:
with scandir(this_dir) as it:
for f in sorted(it, key = filename):
if not f.is_file():
continue
Expand Down

0 comments on commit 64d82b5

Please sign in to comment.