Skip to content

Commit 64d82b5

Browse files
authored
Merge pull request #343 from netbox-community/DynamicVariables
Dynamic Configuration
2 parents 121c3f8 + 58050e5 commit 64d82b5

File tree

9 files changed

+302
-144
lines changed

9 files changed

+302
-144
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
.initializers
44
docker-compose.override.yml
55
*.pem
6+
configuration/*
7+
!configuration/configuration.py
8+
!configuration/extra.py
9+
configuration/ldap/*
10+
!configuration/ldap/ldap_config.py

Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ ARG NETBOX_PATH
6161
COPY ${NETBOX_PATH} /opt/netbox
6262

6363
COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
64-
COPY configuration/gunicorn_config.py /etc/netbox/config/
64+
COPY docker/gunicorn_config.py /etc/netbox/
6565
COPY docker/nginx.conf /etc/netbox-nginx/nginx.conf
6666
COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh
6767
COPY startup_scripts/ /opt/netbox/startup_scripts/
6868
COPY initializers/ /opt/netbox/initializers/
69-
COPY configuration/configuration.py /etc/netbox/config/configuration.py
69+
COPY configuration/ /etc/netbox/config/
7070

7171
WORKDIR /opt/netbox/netbox
7272

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

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

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

8484
LABEL ORIGINAL_TAG="" \
8585
NETBOX_GIT_BRANCH="" \
@@ -122,4 +122,3 @@ RUN apk add --no-cache \
122122
util-linux
123123

124124
COPY docker/ldap_config.docker.py /opt/netbox/netbox/netbox/ldap_config.py
125-
COPY configuration/ldap_config.py /etc/netbox/config/ldap_config.py

configuration/configuration.py

Lines changed: 125 additions & 100 deletions
Large diffs are not rendered by default.

configuration/extra.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
####
2+
## This file contains extra configuration options that can't be configured
3+
## directly through environment variables.
4+
####
5+
6+
## Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
7+
## application errors (assuming correct email settings are provided).
8+
# ADMINS = [
9+
# # ['John Doe', 'jdoe@example.com'],
10+
# ]
11+
12+
13+
## URL schemes that are allowed within links in NetBox
14+
# ALLOWED_URL_SCHEMES = (
15+
# 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
16+
# )
17+
18+
19+
## NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
20+
## be provided as a dictionary.
21+
# NAPALM_ARGS = {}
22+
23+
24+
## Enable installed plugins. Add the name of each plugin to the list.
25+
# from netbox.configuration.configuration import PLUGINS
26+
# PLUGINS.append('my_plugin')
27+
28+
## Plugins configuration settings. These settings are used by various plugins that the user may have installed.
29+
## Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings.
30+
# from netbox.configuration.configuration import PLUGINS_CONFIG
31+
# PLUGINS_CONFIG['my_plugin'] = {
32+
# 'foo': 'bar',
33+
# 'buzz': 'bazz'
34+
# }
35+
36+
37+
## Remote authentication support
38+
# REMOTE_AUTH_DEFAULT_PERMISSIONS = {}
39+
40+
41+
## By default uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the
42+
## class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example:
43+
# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'
44+
# STORAGE_CONFIG = {
45+
# 'AWS_ACCESS_KEY_ID': 'Key ID',
46+
# 'AWS_SECRET_ACCESS_KEY': 'Secret',
47+
# 'AWS_STORAGE_BUCKET_NAME': 'netbox',
48+
# 'AWS_S3_REGION_NAME': 'eu-west-1',
49+
# }
50+
51+
52+
## This file can contain arbitrary Python code, e.g.:
53+
# from datetime import datetime
54+
# now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
55+
# BANNER_TOP = f'<marquee width="200px">This instance started on {now}.</marquee>'
Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import ldap
2-
import os
32

43
from django_auth_ldap.config import LDAPSearch
54
from importlib import import_module
5+
from os import environ
66

77
# Read secret from file
8-
def read_secret(secret_name, default=''):
8+
def _read_secret(secret_name, default=None):
99
try:
1010
f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8')
1111
except EnvironmentError:
@@ -15,70 +15,70 @@ def read_secret(secret_name, default=''):
1515
return f.readline().strip()
1616

1717
# Import and return the group type based on string name
18-
def import_group_type(group_type_name):
18+
def _import_group_type(group_type_name):
1919
mod = import_module('django_auth_ldap.config')
2020
try:
2121
return getattr(mod, group_type_name)()
2222
except:
2323
return None
2424

2525
# Server URI
26-
AUTH_LDAP_SERVER_URI = os.environ.get('AUTH_LDAP_SERVER_URI', '')
26+
AUTH_LDAP_SERVER_URI = environ.get('AUTH_LDAP_SERVER_URI', '')
2727

2828
# The following may be needed if you are binding to Active Directory.
2929
AUTH_LDAP_CONNECTION_OPTIONS = {
3030
ldap.OPT_REFERRALS: 0
3131
}
3232

3333
# Set the DN and password for the NetBox service account.
34-
AUTH_LDAP_BIND_DN = os.environ.get('AUTH_LDAP_BIND_DN', '')
35-
AUTH_LDAP_BIND_PASSWORD = read_secret('auth_ldap_bind_password', os.environ.get('AUTH_LDAP_BIND_PASSWORD', ''))
34+
AUTH_LDAP_BIND_DN = environ.get('AUTH_LDAP_BIND_DN', '')
35+
AUTH_LDAP_BIND_PASSWORD = _read_secret('auth_ldap_bind_password', environ.get('AUTH_LDAP_BIND_PASSWORD', ''))
3636

3737
# Set a string template that describes any user’s distinguished name based on the username.
38-
AUTH_LDAP_USER_DN_TEMPLATE = os.environ.get('AUTH_LDAP_USER_DN_TEMPLATE', None)
38+
AUTH_LDAP_USER_DN_TEMPLATE = environ.get('AUTH_LDAP_USER_DN_TEMPLATE', None)
3939

4040
# Enable STARTTLS for ldap authentication.
41-
AUTH_LDAP_START_TLS = os.environ.get('AUTH_LDAP_START_TLS', 'False').lower() == 'true'
41+
AUTH_LDAP_START_TLS = environ.get('AUTH_LDAP_START_TLS', 'False').lower() == 'true'
4242

4343
# Include this setting if you want to ignore certificate errors. This might be needed to accept a self-signed cert.
4444
# Note that this is a NetBox-specific setting which sets:
4545
# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
46-
LDAP_IGNORE_CERT_ERRORS = os.environ.get('LDAP_IGNORE_CERT_ERRORS', 'False').lower() == 'true'
46+
LDAP_IGNORE_CERT_ERRORS = environ.get('LDAP_IGNORE_CERT_ERRORS', 'False').lower() == 'true'
4747

48-
AUTH_LDAP_USER_SEARCH_BASEDN = os.environ.get('AUTH_LDAP_USER_SEARCH_BASEDN', '')
49-
AUTH_LDAP_USER_SEARCH_ATTR = os.environ.get('AUTH_LDAP_USER_SEARCH_ATTR', 'sAMAccountName')
48+
AUTH_LDAP_USER_SEARCH_BASEDN = environ.get('AUTH_LDAP_USER_SEARCH_BASEDN', '')
49+
AUTH_LDAP_USER_SEARCH_ATTR = environ.get('AUTH_LDAP_USER_SEARCH_ATTR', 'sAMAccountName')
5050
AUTH_LDAP_USER_SEARCH = LDAPSearch(AUTH_LDAP_USER_SEARCH_BASEDN,
5151
ldap.SCOPE_SUBTREE,
5252
"(" + AUTH_LDAP_USER_SEARCH_ATTR + "=%(user)s)")
5353

5454
# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group
5555
# heirarchy.
56-
AUTH_LDAP_GROUP_SEARCH_BASEDN = os.environ.get('AUTH_LDAP_GROUP_SEARCH_BASEDN', '')
57-
AUTH_LDAP_GROUP_SEARCH_CLASS = os.environ.get('AUTH_LDAP_GROUP_SEARCH_CLASS', 'group')
56+
AUTH_LDAP_GROUP_SEARCH_BASEDN = environ.get('AUTH_LDAP_GROUP_SEARCH_BASEDN', '')
57+
AUTH_LDAP_GROUP_SEARCH_CLASS = environ.get('AUTH_LDAP_GROUP_SEARCH_CLASS', 'group')
5858
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(AUTH_LDAP_GROUP_SEARCH_BASEDN, ldap.SCOPE_SUBTREE,
5959
"(objectClass=" + AUTH_LDAP_GROUP_SEARCH_CLASS + ")")
60-
AUTH_LDAP_GROUP_TYPE = import_group_type(os.environ.get('AUTH_LDAP_GROUP_TYPE', 'GroupOfNamesType'))
60+
AUTH_LDAP_GROUP_TYPE = _import_group_type(environ.get('AUTH_LDAP_GROUP_TYPE', 'GroupOfNamesType'))
6161

6262
# Define a group required to login.
63-
AUTH_LDAP_REQUIRE_GROUP = os.environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', '')
63+
AUTH_LDAP_REQUIRE_GROUP = environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', '')
6464

6565
# Define special user types using groups. Exercise great caution when assigning superuser status.
6666
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
67-
"is_active": os.environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', ''),
68-
"is_staff": os.environ.get('AUTH_LDAP_IS_ADMIN_DN', ''),
69-
"is_superuser": os.environ.get('AUTH_LDAP_IS_SUPERUSER_DN', '')
67+
"is_active": environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', ''),
68+
"is_staff": environ.get('AUTH_LDAP_IS_ADMIN_DN', ''),
69+
"is_superuser": environ.get('AUTH_LDAP_IS_SUPERUSER_DN', '')
7070
}
7171

7272
# For more granular permissions, we can map LDAP groups to Django groups.
73-
AUTH_LDAP_FIND_GROUP_PERMS = os.environ.get('AUTH_LDAP_FIND_GROUP_PERMS', 'True').lower() == 'true'
74-
AUTH_LDAP_MIRROR_GROUPS = os.environ.get('AUTH_LDAP_MIRROR_GROUPS', None).lower() == 'true'
73+
AUTH_LDAP_FIND_GROUP_PERMS = environ.get('AUTH_LDAP_FIND_GROUP_PERMS', 'True').lower() == 'true'
74+
AUTH_LDAP_MIRROR_GROUPS = environ.get('AUTH_LDAP_MIRROR_GROUPS', None).lower() == 'true'
7575

7676
# Cache groups for one hour to reduce LDAP traffic
77-
AUTH_LDAP_CACHE_TIMEOUT = int(os.environ.get('AUTH_LDAP_CACHE_TIMEOUT', 3600))
77+
AUTH_LDAP_CACHE_TIMEOUT = int(environ.get('AUTH_LDAP_CACHE_TIMEOUT', 3600))
7878

7979
# Populate the Django user from the LDAP directory.
8080
AUTH_LDAP_USER_ATTR_MAP = {
81-
"first_name": os.environ.get('AUTH_LDAP_ATTR_FIRSTNAME', 'givenName'),
82-
"last_name": os.environ.get('AUTH_LDAP_ATTR_LASTNAME', 'sn'),
83-
"email": os.environ.get('AUTH_LDAP_ATTR_MAIL', 'mail')
81+
"first_name": environ.get('AUTH_LDAP_ATTR_FIRSTNAME', 'givenName'),
82+
"last_name": environ.get('AUTH_LDAP_ATTR_LASTNAME', 'sn'),
83+
"email": environ.get('AUTH_LDAP_ATTR_MAIL', 'mail')
8484
}

docker/configuration.docker.py

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,79 @@
1+
## Generic Parts
2+
# These functions are providing the functionality to load
3+
# arbitrary configuration files.
4+
#
5+
# They can be imported by other code (see `ldap_config.py` for an example).
6+
7+
from os.path import abspath, isfile
8+
from os import scandir
19
import importlib.util
210
import sys
311

4-
try:
5-
spec = importlib.util.spec_from_file_location('configuration', '/etc/netbox/config/configuration.py')
12+
def _filename(f):
13+
return f.name
14+
15+
16+
def _import(module_name, path, loaded_configurations):
17+
spec = importlib.util.spec_from_file_location('', path)
618
module = importlib.util.module_from_spec(spec)
719
spec.loader.exec_module(module)
8-
sys.modules['netbox.configuration'] = module
9-
except:
10-
raise ImportError('')
20+
sys.modules[module_name] = module
21+
22+
loaded_configurations.insert(0, module)
23+
24+
print(f"🧬 loaded config '{path}'")
25+
26+
27+
def read_configurations(config_module, config_dir, main_config):
28+
loaded_configurations = []
29+
30+
main_config_path = abspath(f'{config_dir}/{main_config}.py')
31+
if isfile(main_config_path):
32+
_import(f'{config_module}.{main_config}', main_config_path, loaded_configurations)
33+
else:
34+
print(f"⚠️ Main configuration '{main_config_path}' not found.")
35+
36+
with scandir(config_dir) as it:
37+
for f in sorted(it, key=_filename):
38+
if not f.is_file():
39+
continue
40+
41+
if f.name.startswith('__'):
42+
continue
43+
44+
if not f.name.endswith('.py'):
45+
continue
46+
47+
if f.name == f'{config_dir}.py':
48+
continue
49+
50+
module_name = f"{config_module}.{f.name[:-len('.py')]}".replace(".", "_")
51+
_import(module_name, f.path, loaded_configurations)
52+
53+
if len(loaded_configurations) == 0:
54+
print(f"‼️ No configuration files found in '{config_dir}'.")
55+
raise ImportError(f"No configuration files found in '{config_dir}'.")
56+
57+
return loaded_configurations
58+
59+
60+
## Specific Parts
61+
# This section's code actually loads the various configuration files
62+
# into the module with the given name.
63+
# It contains the logic to resolve arbitrary configuration options by
64+
# levaraging dynamic programming using `__getattr__`.
65+
66+
67+
_loaded_configurations = read_configurations(
68+
config_dir = '/etc/netbox/config/',
69+
config_module = 'netbox.configuration',
70+
main_config = 'configuration')
71+
72+
73+
def __getattr__(name):
74+
for config in _loaded_configurations:
75+
try:
76+
return getattr(config, name)
77+
except:
78+
pass
79+
raise AttributeError

configuration/gunicorn_config.py renamed to docker/gunicorn_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
errorlog = '-'
66
accesslog = '-'
77
capture_output = False
8-
loglevel = 'debug'
8+
loglevel = 'info'

docker/ldap_config.docker.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import importlib.util
2-
import sys
1+
from .configuration import read_configurations
32

4-
try:
5-
spec = importlib.util.spec_from_file_location('ldap_config', '/etc/netbox/config/ldap_config.py')
6-
module = importlib.util.module_from_spec(spec)
7-
spec.loader.exec_module(module)
8-
sys.modules['netbox.ldap_config'] = module
9-
except:
10-
raise ImportError('')
3+
_loaded_configurations = read_configurations(
4+
config_dir = '/etc/netbox/config/ldap/',
5+
config_module = 'netbox.configuration.ldap',
6+
main_config = 'ldap_config')
7+
8+
9+
def __getattr__(name):
10+
for config in _loaded_configurations:
11+
try:
12+
return getattr(config, name)
13+
except:
14+
pass
15+
raise AttributeError

startup_scripts/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
def filename(f):
1010
return f.name
1111

12-
with scandir(dirname(abspath(__file__))) as it:
12+
with scandir(this_dir) as it:
1313
for f in sorted(it, key = filename):
1414
if not f.is_file():
1515
continue

0 commit comments

Comments
 (0)