From 43cb9f7e50d1a38f9c95c741aa832a0b57560f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ma=CC=88der?= Date: Fri, 16 Feb 2018 10:25:26 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Include=20Initializers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initializers are startup scripts for common tasks like creating custom fields. These are problems many users of Netbox Docker potentially face and are therefore worth sharing. --- Dockerfile | 12 +++-- README.md | 50 +++++++++++++++++++-- docker-compose.yml | 3 +- docker/docker-entrypoint.sh | 6 ++- docker/startup_scripts/.gitkeep | 0 initializers/custom_fields.yml | 66 ++++++++++++++++++++++++++++ initializers/groups.yml | 9 ++++ initializers/users.yml | 6 +++ startup_scripts/00_users.py | 20 +++++++++ startup_scripts/10_groups.py | 19 ++++++++ startup_scripts/20_custom_fields.py | 68 +++++++++++++++++++++++++++++ 11 files changed, 249 insertions(+), 10 deletions(-) delete mode 100644 docker/startup_scripts/.gitkeep create mode 100644 initializers/custom_fields.yml create mode 100644 initializers/groups.yml create mode 100644 initializers/users.yml create mode 100644 startup_scripts/00_users.py create mode 100644 startup_scripts/10_groups.py create mode 100644 startup_scripts/20_custom_fields.py diff --git a/Dockerfile b/Dockerfile index 02cdc08..e134bc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,11 @@ RUN apk add --no-cache \ postgresql-dev \ wget -RUN pip install gunicorn +RUN pip install \ +# gunicorn is used for launching netbox + gunicorn \ +# ruamel is used in startup_scripts + ruamel.yaml WORKDIR /opt @@ -31,11 +35,13 @@ RUN pip install -r requirements.txt COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py COPY docker/gunicorn_config.py /opt/netbox/ COPY docker/nginx.conf /etc/netbox-nginx/nginx.conf +COPY docker/docker-entrypoint.sh docker-entrypoint.sh +COPY startup_scripts/ ./ +COPY initializers/ ./ WORKDIR /opt/netbox/netbox -COPY docker/docker-entrypoint.sh /docker-entrypoint.sh -ENTRYPOINT [ "/docker-entrypoint.sh" ] +ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ] VOLUME ["/etc/netbox-nginx/"] diff --git a/README.md b/README.md index 478dbe4..f3e4735 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,11 @@ For example defining `ALLOWED_HOSTS=localhost ::1 127.0.0.1` would allows access [compose-env]: https://docs.docker.com/compose/environment-variables/ -### Custom Initialisation Code (e.g. Automatically Setting Up Custom Fields) +### Custom Initialization Code (e.g. Automatically Setting Up Custom Fields) -When using `docker-compose`, all the python scripts present in `docker/startup_scripts` will automatically be executed after the application boots in the context of `./manage.py`. +When using `docker-compose`, all the python scripts present in `/opt/netbox/startup_scripts` will automatically be executed after the application boots in the context of `./manage.py`. -That mechanism can be used for many things, and in particular to load Netbox custom fields: +That mechanism can be used for many things, e.g. to create Netbox custom fields: ```python # docker/startup_scripts/load_custom_fields.py @@ -94,12 +94,54 @@ if created: my_custom_field.obj_type.add(device_type) ``` +#### Initializers + +Initializers are built-in startup scripts for defining Netbox custom fields, groups and users. +All you need to do is to mount you own `initializers` folder ([see `docker-compose.yml`][netbox-docker-compose]). +Look at the [`initializers` folder][netbox-docker-initializers] to learn how the files must look like. + +Here's an example for defining a custom field: + +```yaml +# initializers/custom_fields.yml +text_field: + type: text + label: Custom Text + description: Enter text in a text field. + required: false + filterable: true + weight: 0 + on_objects: + - dcim.models.Device + - dcim.models.Rack + - ipam.models.IPAddress + - ipam.models.Prefix + - tenancy.models.Tenant + - virtualization.models.VirtualMachine +``` + +[netbox-docker-initializers]: https://github.com/ninech/netbox-docker/tree/master/initializers +[netbox-docker-compose]: https://github.com/ninech/netbox-docker/blob/master/docker-compose.yml + +#### Custom Docker Image + +You can also build your own Netbox Docker image containing your own startup scripts, custom fields, users and groups +like this: + +``` +ARG VERSION=latest +FROM ninech/netbox:$VERSION + +COPY startup_scripts/ /opt/netbox/startup_scripts/ +COPY initializers/ /opt/netbox/initializers/ +``` + ### Production The default settings are optimized for (local) development environments. You should therefore adjust the configuration for production setups, at least the following variables: -* `ALLOWED_HOSTS`: Add all URLs that lead to your netbox instance. +* `ALLOWED_HOSTS`: Add all URLs that lead to your Netbox instance. * `DB_*`: Use a persistent database. * `EMAIL_*`: Use your own mailserver. * `MAX_PAGE_SIZE`: Use the recommended default of 1000. diff --git a/docker-compose.yml b/docker-compose.yml index 34e4572..d92e507 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,8 @@ services: - postgres env_file: netbox.env volumes: - - ./docker/startup_scripts:/opt/netbox/netbox/startup_scripts + - ./startup_scripts:/opt/netbox/startup_scripts + - ./initializers:/opt/netbox/initializers - netbox-nginx-config:/etc/netbox-nginx/ - netbox-static-files:/opt/netbox/netbox/static - netbox-media-files:/opt/netbox/netbox/media diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index ff22cad..bcf418f 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -39,7 +39,8 @@ if not User.objects.filter(username='${SUPERUSER_NAME}'): Token.objects.create(user=u, key='${SUPERUSER_API_TOKEN}') END -for script in $(ls startup_scripts/*.py 2> /dev/null); do +for script in /opt/netbox/startup_scripts/*.py; do + echo "⚙️ Executing '$script'" ./manage.py shell --plain < "${script}" done @@ -48,5 +49,6 @@ done echo "✅ Initialisation is done." -# launch whatever is passed by docker via RUN +# launch whatever is passed by docker +# (i.e. the RUN instruction in the Dockerfile) exec ${@} diff --git a/docker/startup_scripts/.gitkeep b/docker/startup_scripts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/initializers/custom_fields.yml b/initializers/custom_fields.yml new file mode 100644 index 0000000..ccde9a0 --- /dev/null +++ b/initializers/custom_fields.yml @@ -0,0 +1,66 @@ +# text_field: +# type: text +# label: Custom Text +# description: Enter text in a text field. +# required: false +# filterable: true +# weight: 0 +# on_objects: +# - dcim.models.Device +# - dcim.models.Rack +# - ipam.models.IPAddress +# - ipam.models.Prefix +# - tenancy.models.Tenant +# - virtualization.models.VirtualMachine +# integer_field: +# type: integer +# label: Custom Number +# description: Enter numbers into an integer field. +# required: true +# filterable: true +# weight: 10 +# on_objects: +# - tenancy.models.Tenant +# selection_field: +# type: selection +# label: Choose between items +# required: false +# filterable: true +# weight: 30 +# on_objects: +# - dcim.models.Device +# choices: +# - value: First Item +# weight: 10 +# - value: Second Item +# weight: 20 +# - value: Third Item +# weight: 30 +# - value: Fifth Item +# weight: 50 +# - value: Fourth Item +# weight: 40 +# boolean_field: +# type: boolean +# label: Yes Or No? +# required: true +# filterable: true +# default: "false" # important: but "false" in quotes! +# weight: 90 +# on_objects: +# - dcim.models.Device +# url_field: +# type: url +# label: Hyperlink +# description: Link to something nice. +# required: true +# filterable: false +# on_objects: +# - tenancy.models.Tenant +# date_field: +# type: date +# label: Important Date +# required: false +# filterable: false +# on_objects: +# - dcim.models.Device diff --git a/initializers/groups.yml b/initializers/groups.yml new file mode 100644 index 0000000..1f4a5a7 --- /dev/null +++ b/initializers/groups.yml @@ -0,0 +1,9 @@ +# applications: +# users: +# - technical_user +# readers: +# users: +# - reader +# writers: +# users: +# - writer diff --git a/initializers/users.yml b/initializers/users.yml new file mode 100644 index 0000000..ed57fef --- /dev/null +++ b/initializers/users.yml @@ -0,0 +1,6 @@ +# technical_user: +# api_token: 0123456789technicaluser789abcdef01234567 # must be looooong! +# reader: +# password: reader +# writer: +# password: writer diff --git a/startup_scripts/00_users.py b/startup_scripts/00_users.py new file mode 100644 index 0000000..1db0ebe --- /dev/null +++ b/startup_scripts/00_users.py @@ -0,0 +1,20 @@ +from django.contrib.auth.models import Group, User +from users.models import Token + +from ruamel.yaml import YAML + +with open('/opt/netbox/initializers/users.yml', 'r') as stream: + yaml=YAML(typ='safe') + users = yaml.load(stream) + + if users is not None: + for username, user_details in users.items(): + if not User.objects.filter(username=username): + user = User.objects.create_user( + username = username, + password = user_details.get('password', 0) or User.objects.make_random_password) + + print("👤 Created user ",username) + + if user_details.get('api_token', 0): + Token.objects.create(user=user, key=user_details['api_token']) diff --git a/startup_scripts/10_groups.py b/startup_scripts/10_groups.py new file mode 100644 index 0000000..7932874 --- /dev/null +++ b/startup_scripts/10_groups.py @@ -0,0 +1,19 @@ +from django.contrib.auth.models import Group, User +from ruamel.yaml import YAML + +with open('/opt/netbox/initializers/groups.yml', 'r') as stream: + yaml=YAML(typ='safe') + groups = yaml.load(stream) + + if groups is not None: + for groupname, group_details in groups.items(): + group, created = Group.objects.get_or_create(name=groupname) + + if created: + print("👥 Created group", groupname) + + for username in group_details['users']: + user = User.objects.get(username=username) + + if user: + user.groups.add(group) diff --git a/startup_scripts/20_custom_fields.py b/startup_scripts/20_custom_fields.py new file mode 100644 index 0000000..5c40e37 --- /dev/null +++ b/startup_scripts/20_custom_fields.py @@ -0,0 +1,68 @@ +from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_URL, CF_TYPE_SELECT +from extras.models import CustomField, CustomFieldChoice + +from ruamel.yaml import YAML + +text_to_fields = { + 'boolean': CF_TYPE_BOOLEAN, + 'date': CF_TYPE_DATE, + 'integer': CF_TYPE_INTEGER, + 'selection': CF_TYPE_SELECT, + 'text': CF_TYPE_TEXT, + 'url': CF_TYPE_URL, +} + +def get_class_for_class_path(class_path): + import importlib + from django.contrib.contenttypes.models import ContentType + + module_name, class_name = class_path.rsplit(".", 1) + module = importlib.import_module(module_name) + clazz = getattr(module, class_name) + return ContentType.objects.get_for_model(clazz) + +with open('/opt/netbox/initializers/custom_fields.yml', 'r') as stream: + yaml = YAML(typ='safe') + customfields = yaml.load(stream) + + if customfields is not None: + for cf_name, cf_details in customfields.items(): + custom_field, created = CustomField.objects.get_or_create(name = cf_name) + + if created: + if cf_details.get('default', 0): + custom_field.default = cf_details['default'] + + if cf_details.get('description', 0): + custom_field.description = cf_details['description'] + + if cf_details.get('filterable', 0): + custom_field.is_filterables = cf_details['filterable'] + + if cf_details.get('label', 0): + custom_field.label = cf_details['label'] + + for object_type in cf_details.get('on_objects', []): + custom_field.obj_type.add(get_class_for_class_path(object_type)) + + if cf_details.get('required', 0): + custom_field.required = cf_details['required'] + + if cf_details.get('type', 0): + custom_field.type = text_to_fields[cf_details['type']] + + if cf_details.get('weight', 0): + custom_field.weight = cf_details['weight'] + + custom_field.save() + + for choice_details in cf_details.get('choices', []): + choice = CustomFieldChoice.objects.create( + field=custom_field, + value=choice_details['value']) + + if choice_details.get('weight', 0): + choice.weight = choice_details['weight'] + choice.save() + + print("🔧 Created custom field", cf_name) From bab8618650ca1b1566f3c567a751ba7bebb25185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ma=CC=88der?= Date: Fri, 16 Feb 2018 10:30:26 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9D=87=EF=B8=8F=20Make=20relevant=20moun?= =?UTF-8?q?ts=20read-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some mounts are not supposed to be writable by a container. The docker-compose file has been improved in that direction in order to enforce this. --- docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d92e507..18c80b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,8 @@ services: - postgres env_file: netbox.env volumes: - - ./startup_scripts:/opt/netbox/startup_scripts - - ./initializers:/opt/netbox/initializers + - ./startup_scripts:/opt/netbox/startup_scripts:ro + - ./initializers:/opt/netbox/initializers:ro - netbox-nginx-config:/etc/netbox-nginx/ - netbox-static-files:/opt/netbox/netbox/static - netbox-media-files:/opt/netbox/netbox/media @@ -24,8 +24,8 @@ services: ports: - 8080 volumes: - - netbox-static-files:/opt/netbox/netbox/static - - netbox-nginx-config:/etc/netbox-nginx/ + - netbox-static-files:/opt/netbox/netbox/static:ro + - netbox-nginx-config:/etc/netbox-nginx/:ro postgres: image: postgres:9.6-alpine env_file: postgres.env From ba2b49339fed76bf8c64ea666b17f195582b8e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ma=CC=88der?= Date: Fri, 16 Feb 2018 10:34:33 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Updates=20Dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Postgres 9.6 -> 10.2 * Nginx 1.11 -> 1.13 --- docker-compose.test.yml | 2 +- docker-compose.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 9177dd2..c0722d3 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -13,7 +13,7 @@ services: - ./manage.py - test postgres: - image: postgres:9.6-alpine + image: postgres:10.2-alpine env_file: postgres.env volumes: netbox-static-files: diff --git a/docker-compose.yml b/docker-compose.yml index 18c80b0..0600b61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: - netbox-media-files:/opt/netbox/netbox/media - netbox-report-files:/opt/netbox/netbox/reports nginx: - image: nginx:1.11-alpine + image: nginx:1.13-alpine command: nginx -g 'daemon off;' -c /etc/netbox-nginx/nginx.conf depends_on: - netbox @@ -27,7 +27,7 @@ services: - netbox-static-files:/opt/netbox/netbox/static:ro - netbox-nginx-config:/etc/netbox-nginx/:ro postgres: - image: postgres:9.6-alpine + image: postgres:10.2-alpine env_file: postgres.env volumes: - netbox-postgres-data:/var/lib/postgresql/data From 2d6388b48912bc5bf736374e0f993388b27c6c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ma=CC=88der?= Date: Fri, 16 Feb 2018 10:49:34 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9E=20Be=20More=20Explicit=20On=20?= =?UTF-8?q?Paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 4 ++++ Dockerfile | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ce32f24 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.github +.travis.yml +build* +*.env diff --git a/Dockerfile b/Dockerfile index e134bc3..7d657ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,8 +36,8 @@ COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py COPY docker/gunicorn_config.py /opt/netbox/ COPY docker/nginx.conf /etc/netbox-nginx/nginx.conf COPY docker/docker-entrypoint.sh docker-entrypoint.sh -COPY startup_scripts/ ./ -COPY initializers/ ./ +COPY startup_scripts/ /opt/netbox/startup_scripts/ +COPY initializers/ /opt/netbox/initializers/ WORKDIR /opt/netbox/netbox