aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2021-01-27 04:41:00 +0000
committerMike Crute <mike@crute.us>2021-01-27 04:41:00 +0000
commit763b810ca1a9d755205e49b1246025b83abb5132 (patch)
tree923df4e956b36ef95e86ff9c51c5bbad26fd076f
parentcd833bc2852ec204fbaecde7ada56798eba005da (diff)
downloaddockerfiles-763b810ca1a9d755205e49b1246025b83abb5132.tar.bz2
dockerfiles-763b810ca1a9d755205e49b1246025b83abb5132.tar.xz
dockerfiles-763b810ca1a9d755205e49b1246025b83abb5132.zip
Add netbox
-rw-r--r--netbox/Dockerfile73
-rw-r--r--netbox/Makefile24
-rw-r--r--netbox/config-patch1.diff26
-rw-r--r--netbox/config-patch2.diff89
-rw-r--r--netbox/django-driver.py69
-rw-r--r--netbox/django-vault-client.py84
-rwxr-xr-xnetbox/entrypoint.sh22
-rwxr-xr-xnetbox/etc/service/netbox-rq/log/run3
-rwxr-xr-xnetbox/etc/service/netbox-rq/run23
-rwxr-xr-xnetbox/etc/service/uwsgi/log/run3
-rwxr-xr-xnetbox/etc/service/uwsgi/run23
-rw-r--r--netbox/etc/uwsgi/netbox.ini15
-rw-r--r--netbox/settings-patch.diff11
13 files changed, 465 insertions, 0 deletions
diff --git a/netbox/Dockerfile b/netbox/Dockerfile
new file mode 100644
index 0000000..022aebf
--- /dev/null
+++ b/netbox/Dockerfile
@@ -0,0 +1,73 @@
1FROM alpine:latest
2LABEL maintainer="Mike Crute <mike@crute.us>"
3
4ARG netbox_version
5
6ADD config-patch1.diff /config-patch1.diff
7ADD config-patch2.diff /config-patch2.diff
8ADD settings-patch.diff /settings-patch.diff
9
10RUN set -euxo pipefail; \
11 \
12 apk --no-cache add \
13 build-base \
14 dumb-init \
15 jpeg-dev \
16 libffi-dev \
17 libxml2-dev \
18 libxslt-dev \
19 openssl-dev \
20 postgresql-dev \
21 py3-pip \
22 python3-dev \
23 runit \
24 su-exec \
25 uwsgi \
26 uwsgi-python \
27 zlib-dev \
28 ; \
29 cd /tmp; \
30 wget "https://github.com/netbox-community/netbox/archive/v${netbox_version}.tar.gz"; \
31 tar -xvf "v${netbox_version}.tar.gz" -C /opt; \
32 rm "v${netbox_version}.tar.gz"; \
33 mv /opt/netbox-${netbox_version}/ /opt/netbox/; \
34 \
35 cd /; \
36 cp /opt/netbox/netbox/netbox/configuration.example.py /opt/netbox/netbox/netbox/configuration.py; \
37 patch -p1 < /config-patch1.diff; \
38 rm /config-patch1.diff; \
39 \
40 addgroup -S netbox; \
41 adduser -S -G netbox netbox; \
42 chown -R netbox:netbox /opt/netbox/netbox/media; \
43 \
44 cd /opt/netbox; \
45 pip3 install wheel; \
46 pip3 install -r requirements.txt; \
47 \
48 python3 netbox/manage.py collectstatic --no-input; \
49 \
50 cd /; \
51 cp /opt/netbox/netbox/netbox/configuration.example.py /opt/netbox/netbox/netbox/configuration.py; \
52 patch -p1 < /config-patch2.diff; \
53 rm /config-patch2.diff; \
54 \
55 patch -p1 < /settings-patch.diff; \
56 rm /settings-patch.diff; \
57 \
58 mkdir -p /usr/lib/python3.8/site-packages/django/db/backends/postgresqlvault; \
59 touch /usr/lib/python3.8/site-packages/django/db/backends/postgresqlvault/__init__.py; \
60 \
61 rm -rf /root/.cache; \
62 apk --no-cache del --purge \
63 build-base \
64 ;
65
66ADD django-vault-client.py /usr/lib/python3.8/site-packages/django/contrib/vault_client.py
67ADD django-driver.py /usr/lib/python3.8/site-packages/django/db/backends/postgresqlvault/base.py
68ADD etc/ /etc/
69ADD entrypoint.sh /entrypoint.sh
70
71STOPSIGNAL SIGHUP
72ENTRYPOINT [ "/entrypoint.sh" ]
73CMD [ "/usr/bin/dumb-init", "/sbin/runsvdir", "/etc/service" ]
diff --git a/netbox/Makefile b/netbox/Makefile
new file mode 100644
index 0000000..ffe7e64
--- /dev/null
+++ b/netbox/Makefile
@@ -0,0 +1,24 @@
1VERSION=2.10.3
2IMAGE=docker.crute.me/netbox:$(VERSION)
3LATEST=$(subst :$(VERSION),,$(IMAGE)):latest
4
5all:
6 #docker pull alpine:latest
7 docker build \
8 --build-arg=netbox_version=$(VERSION) \
9 -t $(IMAGE) .
10
11all-no-cache:
12 docker build --no-cache -t $(IMAGE) .
13
14run:
15 docker run -d \
16 -p 9110:9000 \
17 -p 9111:9001 \
18 -v /srv/code:/srv/code \
19 $(IMAGE)
20
21publish:
22 docker push $(IMAGE)
23 docker tag $(IMAGE) $(LATEST)
24 docker push $(LATEST)
diff --git a/netbox/config-patch1.diff b/netbox/config-patch1.diff
new file mode 100644
index 0000000..be5b068
--- /dev/null
+++ b/netbox/config-patch1.diff
@@ -0,0 +1,26 @@
1--- a/opt/netbox/netbox/netbox/configuration.py
2+++ b/opt/netbox/netbox/netbox/configuration.py
3@@ -4,11 +4,13 @@
4 # #
5 #########################
6
7+import urllib3; urllib3.disable_warnings()
8+
9 # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
10 # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
11 #
12 # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
13-ALLOWED_HOSTS = []
14+ALLOWED_HOSTS = ['*']
15
16 # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
17 # https://docs.djangoproject.com/en/stable/ref/settings/#databases
18@@ -51,7 +53,7 @@
19 # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
20 # symbols. NetBox will not run without this defined. For more information, see
21 # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
22-SECRET_KEY = ''
23+SECRET_KEY = 'dummy setup value will get cleared after setup run'
24
25
26 #########################
diff --git a/netbox/config-patch2.diff b/netbox/config-patch2.diff
new file mode 100644
index 0000000..5983cc1
--- /dev/null
+++ b/netbox/config-patch2.diff
@@ -0,0 +1,89 @@
1--- a/opt/netbox/netbox/netbox/configuration.py
2+++ b/opt/netbox/netbox/netbox/configuration.py
3@@ -4,21 +4,35 @@
4 # #
5 #########################
6
7+import os
8+from django.contrib.vault_client import SimpleVaultClient
9+
10+
11+def _is_affirmative(value):
12+ value = "" if not value else value
13+ return value.lower() in ["yes", "true", "on", "1"]
14+
15+
16 # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
17 # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
18 #
19 # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
20-ALLOWED_HOSTS = []
21+ALLOWED_HOSTS = ['*']
22
23 # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
24 # https://docs.djangoproject.com/en/stable/ref/settings/#databases
25+port = os.getenv("NETBOX_DB_PORT")
26 DATABASE = {
27- 'NAME': 'netbox', # Database name
28- 'USER': '', # PostgreSQL username
29- 'PASSWORD': '', # PostgreSQL password
30- 'HOST': 'localhost', # Database server
31- 'PORT': '', # Database port (leave blank for default)
32- 'CONN_MAX_AGE': 300, # Max database connection age
33+ 'NAME': os.getenv("NETBOX_DB_NAME"),
34+ 'HOST': os.getenv("NETBOX_DB_HOST"),
35+ 'PORT': int(port) if port else "",
36+ 'CONN_MAX_AGE': 300,
37+ "VAULT_SKIP_VERIFY": os.getenv("VAULT_SKIP_VERIFY"),
38+ "VAULT_ADDR": os.getenv("VAULT_ADDR"),
39+ "VAULT_TOKEN": os.getenv("VAULT_TOKEN"),
40+ "VAULT_DB_ROLE_NAME": os.getenv("VAULT_DB_ROLE_NAME"),
41+ "VAULT_ROLE_ID": os.getenv("VAULT_ROLE_ID"),
42+ "VAULT_SECRET_ID": os.getenv("VAULT_SECRET_ID"),
43 }
44
45 # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
46@@ -26,23 +40,23 @@
47 # to use two separate database IDs.
48 REDIS = {
49 'tasks': {
50- 'HOST': 'localhost',
51+ 'HOST': os.getenv("NETBOX_REDIS_HOST"),
52 'PORT': 6379,
53 # Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
54 # 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
55 # 'SENTINEL_SERVICE': 'netbox',
56 'PASSWORD': '',
57- 'DATABASE': 0,
58+ 'DATABASE': int(os.getenv("NETBOX_REDIS_TASK_DB")),
59 'SSL': False,
60 },
61 'caching': {
62- 'HOST': 'localhost',
63+ 'HOST': os.getenv("NETBOX_REDIS_HOST"),
64 'PORT': 6379,
65 # Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
66 # 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
67 # 'SENTINEL_SERVICE': 'netbox',
68 'PASSWORD': '',
69- 'DATABASE': 1,
70+ 'DATABASE': int(os.getenv("NETBOX_REDIS_CACHE_DB")),
71 'SSL': False,
72 }
73 }
74@@ -51,7 +65,14 @@
75 # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
76 # symbols. NetBox will not run without this defined. For more information, see
77 # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
78-SECRET_KEY = ''
79+vc = SimpleVaultClient(
80+ os.getenv("VAULT_ADDR"),
81+ os.getenv("VAULT_ROLE_ID"),
82+ os.getenv("VAULT_SECRET_ID"),
83+ ssl_verify=not _is_affirmative(os.getenv("VAULT_SKIP_VERIFY"))
84+)
85+SECRET_KEY = vc.get_kv_secret(os.getenv("NETBOX_VAULT_SECRET_NAME"), "key")
86+del vc
87
88
89 #########################
diff --git a/netbox/django-driver.py b/netbox/django-driver.py
new file mode 100644
index 0000000..65a9136
--- /dev/null
+++ b/netbox/django-driver.py
@@ -0,0 +1,69 @@
1import threading
2from datetime import datetime, timedelta
3
4from django.core.exceptions import ImproperlyConfigured
5from django.contrib.vault_client import SimpleVaultClient, Credential
6from django.db.backends.postgresql.base import DatabaseWrapper as OrigWrapper
7
8
9def _is_affirmative(value):
10 value = "" if not value else value
11 return value.lower() in ["yes", "true", "on", "1"]
12
13
14def _must_get(store, key):
15 value = store.get(key)
16
17 if not value:
18 raise ImproperlyConfigured(
19 f"Database parameter {key} is required but not set.")
20
21 return value
22
23
24class DatabaseWrapper(OrigWrapper):
25
26 def __init__(self, *args, **kwargs):
27 super().__init__(*args, **kwargs)
28 self._vault_cache_lock = threading.Lock()
29 self._vault_cred_cache = Credential.empty()
30
31 def close_if_unusable_or_obsolete(self):
32 super().close_if_unusable_or_obsolete()
33
34 if self.connection is None:
35 return
36
37 with self._vault_cache_lock:
38 if not self._vault_cred_cache.is_valid:
39 self.close()
40
41 # All of this is done under lock
42 def _get_vault_cred(self):
43 print("Getting credentials from vault")
44 params = self.settings_dict
45
46 verify = not _is_affirmative(params.get("VAULT_SKIP_VERIFY"))
47 url = _must_get(params, "VAULT_ADDR")
48 token = params.get("VAULT_TOKEN")
49 db_role_name = _must_get(params, "VAULT_DB_ROLE_NAME")
50 role_id = _must_get(params, "VAULT_ROLE_ID")
51 role_secret = _must_get(params, "VAULT_SECRET_ID")
52
53 client = SimpleVaultClient(url, role_id, role_secret, verify)
54
55 self._vault_cred_cache = client.get_db_credential(db_role_name)
56
57 def get_connection_params(self):
58 conn_params = super().get_connection_params()
59
60 # Do the fetch under lock to prevent multiple threads from piling onto
61 # the vault server
62 with self._vault_cache_lock:
63 if not self._vault_cred_cache.is_valid:
64 self._get_vault_cred()
65
66 conn_params["user"] = self._vault_cred_cache.username
67 conn_params["password"] = self._vault_cred_cache.password
68
69 return conn_params
diff --git a/netbox/django-vault-client.py b/netbox/django-vault-client.py
new file mode 100644
index 0000000..e699db3
--- /dev/null
+++ b/netbox/django-vault-client.py
@@ -0,0 +1,84 @@
1import os
2import ssl
3import json
4from urllib import request, parse
5from datetime import datetime, timedelta
6
7
8class Credential:
9
10 def __init__(self, data):
11 self.username = data["data"]["username"]
12 self.password = data["data"]["password"]
13
14 # Leave 2 minutes in case we encounter a failure
15 _duration = timedelta(seconds=data["lease_duration"] - 120)
16 self.expires = datetime.now() + _duration
17
18 @classmethod
19 def empty(cls):
20 return cls({
21 "lease_duration": -1,
22 "data": { "username": "", "password": "" }
23 })
24
25 @property
26 def is_valid(self):
27 return self.expires > datetime.now()
28
29
30
31class SimpleVaultClient:
32
33 def __init__(self, base_url, role_id, role_secret, ssl_verify=True):
34 self.base_url = base_url
35 self.ssl_verify = ssl_verify
36 self.role_id = role_id
37 self.role_secret = role_secret
38
39 env_token = os.getenv("VAULT_TOKEN")
40 if env_token:
41 self._token = env_token
42 self._token_expires = None # Token is assumed to never expire
43 else:
44 self._token = None
45 self._token_expires = datetime.now() + timedelta(seconds=-1)
46
47 def _login_approle(self, role_id, secret):
48 res = self._make_request("auth/approle/login", auth=False, data={
49 "role_id": role_id,
50 "secret_id": secret,
51 })
52
53 self._token = res["auth"]["client_token"]
54 self._token_expires = datetime.now() + \
55 timedelta(seconds=res["auth"]["lease_duration"])
56
57 @property
58 def _token_is_expired(self):
59 return self._token_expires and \
60 self._token_expires < (datetime.now() + timedelta(seconds=120))
61
62 def _auth_as_needed(self):
63 if self._token_is_expired:
64 self._login_approle(self.role_id, self.role_secret)
65
66 def _make_request(self, url, data=None, auth=True):
67 context = ssl._create_unverified_context() \
68 if not self.ssl_verify else None
69
70 data = json.dumps(data).encode("utf-8") if data else None
71 headers = { "X-Vault-Token": self._token } if auth else {}
72
73 url = parse.urljoin(self.base_url, parse.urljoin("/v1/", url))
74 req = request.Request(url, headers=headers, data=data)
75 res = request.urlopen(req, context=context)
76 return json.load(res)
77
78 def get_kv_secret(self, path, key):
79 self._auth_as_needed()
80 return self._make_request(f"kv/data/{path}")["data"]["data"][key]
81
82 def get_db_credential(self, role):
83 self._auth_as_needed()
84 return Credential(self._make_request(f"database/creds/{role}"))
diff --git a/netbox/entrypoint.sh b/netbox/entrypoint.sh
new file mode 100755
index 0000000..a4f844c
--- /dev/null
+++ b/netbox/entrypoint.sh
@@ -0,0 +1,22 @@
1#!/bin/sh
2
3set -e
4
5cd "/opt/netbox"
6
7# Apply any database migrations
8/sbin/su-exec netbox python3 netbox/manage.py migrate
9
10# Trace any missing cable paths (not typically needed)
11/sbin/su-exec netbox python3 netbox/manage.py trace_paths --no-input
12
13# Delete any stale content types
14/sbin/su-exec netbox python3 netbox/manage.py remove_stale_contenttypes --no-input
15
16# Delete any expired user sessions
17/sbin/su-exec netbox python3 netbox/manage.py clearsessions
18
19# Clear all cached data
20/sbin/su-exec netbox python3 netbox/manage.py invalidate all
21
22exec "$@"
diff --git a/netbox/etc/service/netbox-rq/log/run b/netbox/etc/service/netbox-rq/log/run
new file mode 100755
index 0000000..6193824
--- /dev/null
+++ b/netbox/etc/service/netbox-rq/log/run
@@ -0,0 +1,3 @@
1#!/bin/sh
2
3cat -
diff --git a/netbox/etc/service/netbox-rq/run b/netbox/etc/service/netbox-rq/run
new file mode 100755
index 0000000..aa4b675
--- /dev/null
+++ b/netbox/etc/service/netbox-rq/run
@@ -0,0 +1,23 @@
1#!/bin/sh
2
3# runsv sends us a TERM but uwsgi will only shutdown cleanly
4# if it receives an INT so we need to translate the signal
5# properly for uwsgi
6trap 'kill -INT $PID' TERM
7
8/sbin/su-exec netbox /usr/bin/python3 /opt/netbox/netbox/manage.py rqworker &
9
10PID=$!
11
12# wait for uwsgi, will get cancelled when runsv TERMs us and
13# the trap will get executed next, unless something goes wrong
14# and uwsgi fails then this wait will run
15wait $PID
16
17# if something went wrong then unregister the trap because it
18# won't have a target
19trap - TERM
20
21# waiting on a dead process will return the return code of the
22# processes original exit
23wait $PID
diff --git a/netbox/etc/service/uwsgi/log/run b/netbox/etc/service/uwsgi/log/run
new file mode 100755
index 0000000..6193824
--- /dev/null
+++ b/netbox/etc/service/uwsgi/log/run
@@ -0,0 +1,3 @@
1#!/bin/sh
2
3cat -
diff --git a/netbox/etc/service/uwsgi/run b/netbox/etc/service/uwsgi/run
new file mode 100755
index 0000000..e24ede7
--- /dev/null
+++ b/netbox/etc/service/uwsgi/run
@@ -0,0 +1,23 @@
1#!/bin/sh
2
3# runsv sends us a TERM but uwsgi will only shutdown cleanly
4# if it receives an INT so we need to translate the signal
5# properly for uwsgi
6trap 'kill -INT $PID' TERM
7
8/usr/sbin/uwsgi --ini /etc/uwsgi/netbox.ini &
9
10PID=$!
11
12# wait for uwsgi, will get cancelled when runsv TERMs us and
13# the trap will get executed next, unless something goes wrong
14# and uwsgi fails then this wait will run
15wait $PID
16
17# if something went wrong then unregister the trap because it
18# won't have a target
19trap - TERM
20
21# waiting on a dead process will return the return code of the
22# processes original exit
23wait $PID
diff --git a/netbox/etc/uwsgi/netbox.ini b/netbox/etc/uwsgi/netbox.ini
new file mode 100644
index 0000000..8431c6f
--- /dev/null
+++ b/netbox/etc/uwsgi/netbox.ini
@@ -0,0 +1,15 @@
1[uwsgi]
2plugin = python
3master = true
4no-orphans = true
5socket = [::]:9000
6uid = netbox
7gid = netbox
8mime-file = /etc/mime.types
9chdir = /opt/netbox/netbox
10pythonpath = /opt/netbox/netbox
11workers = 2
12wsgi-file = netbox/wsgi.py
13harakiri = 300
14offload-threads = 4
15static-map = /static=/opt/netbox/netbox/static
diff --git a/netbox/settings-patch.diff b/netbox/settings-patch.diff
new file mode 100644
index 0000000..78510ad
--- /dev/null
+++ b/netbox/settings-patch.diff
@@ -0,0 +1,11 @@
1--- /opt/netbox/netbox/netbox/settings.py
2+++ /opt/netbox/netbox/netbox/settings.py
3@@ -147,7 +147,7 @@
4 })
5 else:
6 DATABASE.update({
7- 'ENGINE': 'django.db.backends.postgresql'
8+ 'ENGINE': 'django.db.backends.postgresqlvault'
9 })
10
11 DATABASES = {