Compare commits

...

27 Commits

Author SHA1 Message Date
19728fa3e6 prepare 0.18.1 2019-10-13 14:03:56 +02:00
e1f054b6b5 Merge branch 'fix-160' 2019-10-13 14:03:34 +02:00
79d349133e fix typo 2019-10-13 14:03:22 +02:00
cb8007e41d Adds a hyperlink for the #netbox-docker channel 2019-10-12 15:16:17 +02:00
b790796f9b Prepare Version 0.18.0 2019-10-12 14:56:03 +02:00
20c234a96e Introduce $SKIP_SUPERUSER
This adds a new variable to skip the creation of the superuser.
That is useful for LDAP and for production environments.

Fixes #160
2019-10-12 14:49:40 +02:00
5ae5977717 Merge branch 'axarriola-master' 2019-10-12 14:27:11 +02:00
e894cdaa06 Reformat initializers and add some IPv6 examples 2019-10-12 14:23:41 +02:00
5defc38294 Add missing break 2019-10-12 14:23:11 +02:00
61c0a9b519 Merge branch 'master' of https://github.com/axarriola/netbox-docker into axarriola-master 2019-10-12 14:02:35 +02:00
992a8f1d5f More typos. Tested all changes. 2019-10-11 18:08:22 +02:00
2d25907cba Fixing typos. 2019-10-11 17:30:38 +02:00
8a40c6e0a3 Merge branch 'master' of https://github.com/netbox-community/netbox-docker 2019-10-11 17:06:18 +02:00
ed8e339bfe Merge pull request #162 from netbox-community/cleanup
Remove debug statement
2019-10-11 16:32:07 +02:00
4b1514f8d3 Remove debug statement 2019-10-11 16:31:56 +02:00
2044f685cf Merge pull request #161 from netbox-community/fix-platform-initializer
Updating the initializer platform examples
2019-10-11 16:15:19 +02:00
86de0d850b Updating the initializer platform examples
rpc client was removed from netbox
2019-10-11 16:15:03 +02:00
4e1ac2392d Added newline and breaks. 2019-10-11 15:46:32 +02:00
723d4744a4 Merge branch 'master' of https://github.com/axarriola/netbox-docker into axarriola-master 2019-10-11 14:31:29 +02:00
821d6c8672 Fixed further requirements. 2019-10-10 17:35:06 +02:00
0b5214d247 Fixed aesthetics. 2019-10-10 16:52:29 +02:00
aca448d180 Merge branch 'bootc-helm-chart' 2019-10-09 12:42:01 +02:00
6051092a59 Move large parts of the documentation in the README to the wiki 2019-10-09 12:40:40 +02:00
03b52f9074 Merge branch 'helm-chart' of https://github.com/bootc/netbox-docker into bootc-helm-chart 2019-10-09 12:40:29 +02:00
ce9158eb07 Update README.md 2019-07-30 13:45:08 +02:00
3e1f688f78 Add a link to my Helm chart
I've written and will continue to maintain a Helm chart to aid in
deploying NetBox on Kubernetes. This adds a link to the README file so
that people who may be interested in it can find it.
2019-07-20 16:51:51 -03:00
4cb5b9f20d Added more startup_scripts and initializers examples. 2019-07-04 14:10:56 +02:00
39 changed files with 863 additions and 364 deletions

339
README.md
View File

@ -1,13 +1,14 @@
# netbox-docker
[The Github repository](netbox-docker-github) houses the components needed to build Netbox as a Docker container.
Images are built using this code are released to [Docker Hub][netbox-dockerhub] every night.
Images are built using this code and are released to [Docker Hub][netbox-dockerhub] once a day.
Questions? Before opening an issue on Github, please join the [Network To Code][ntc-slack] Slack and ask for help in our `#netbox-docker` channel.
Do you have any questions? Before opening an issue on Github, please join the [Network To Code][ntc-slack] Slack and ask for help in our [`#netbox-docker`][netbox-docker-slack] channel.
[netbox-dockerhub]: https://hub.docker.com/r/netboxcommunity/netbox/tags/
[netbox-docker-github]: https://github.com/netbox-community/netbox-docker/
[netbox-docker-github]: https://github.com/netbox-community/netbox-docker/
[ntc-slack]: http://slack.networktocode.com/
[netbox-docker-slack]: https://slack.com/app_redirect?channel=netbox-docker&team=T09LQ7E9E
## Quickstart
@ -53,174 +54,12 @@ This project relies only on *Docker* and *docker-compose* meeting this requireme
To ensure this, compare the output of `docker --version` and `docker-compose --version` with the requirements above.
## Configuration
## Reference Documentation
You can configure the app using environment variables.
These are defined in `netbox.env`.
Read [Environment Variables in Compose][compose-env] to understand about the various possibilities to overwrite these variables.
(The easiest solution being simply adjusting that file.)
Please refer [to the wiki][wiki] for further information on how to use this Netbox Docker image properly.
It covers advanced topics such as using secret files, deployment to Kubernetes as well as NAPALM and LDAP configuration.
To find all possible variables, have a look at the [configuration.py][docker-config] and [docker-entrypoint.sh][entrypoint] files.
Generally, the environment variables are called the same as their respective Netbox configuration variables.
Variables which are arrays are usually composed by putting all the values into the same environment variables with the values separated by a whitespace ("` `").
For example defining `ALLOWED_HOSTS=localhost ::1 127.0.0.1` would allows access to Netbox through `http://localhost:8080`, `http://[::1]:8080` and `http://127.0.0.1:8080`.
[compose-env]: https://docs.docker.com/compose/environment-variables/
### 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, space separated. E.g. `ALLOWED_HOSTS=netbox.mycorp.com server042.mycorp.com 2a02:123::42 10.0.0.42 localhost ::1 127.0.0.1` (It's good advice to always allow localhost connections for easy debugging, i.e. `localhost ::1 127.0.0.1`.)
* `DB_*`: Use your own persistent database. Don't use the default passwords!
* `EMAIL_*`: Use your own mailserver.
* `MAX_PAGE_SIZE`: Use the recommended default of 1000.
* `SUPERUSER_*`: Only define those variables during the initial setup, and drop them once the DB is set up. Don't use the default passwords!
* `REDIS_*`: Use your own persistent redis. Don't use the default passwords!
### Running on Docker Swarm / Kubernetes / OpenShift
You may run this image in a cluster such as Docker Swarm, Kubernetes or OpenShift, but this is advanced level.
In this case, we encourage you to statically configure Netbox by starting from [Netbox's example config file][default-config], and mounting it into your container in the directory `/etc/netbox/config/` using the mechanism provided by your container platform (i.e. [Docker Swarm configs][swarm-config], [Kubernetes ConfigMap][k8s-config], [OpenShift ConfigMaps][openshift-config]).
But if you rather continue to configure your application through environment variables, you may continue to use [the built-in configuration file][docker-config].
We discourage storing secrets in environment variables, as environment variable are passed on to all sub-processes and may leak easily into other systems, e.g. error collecting tools that often collect all environment variables whenever an error occurs.
Therefore we *strongly advise* to make use of the secrets mechanism provided by your container platform (i.e. [Docker Swarm secrets][swarm-secrets], [Kubernetes secrets][k8s-secrets], [OpenShift secrets][openshift-secrets]).
[The configuration file][docker-config] and [the entrypoint script][entrypoint] try to load the following secrets from the respective files.
If a secret is defined by an environment variable and in the respective file at the same time, then the value from the environment variable is used.
* `SUPERUSER_PASSWORD`: `/run/secrets/superuser_password`
* `SUPERUSER_API_TOKEN`: `/run/secrets/superuser_api_token`
* `DB_PASSWORD`: `/run/secrets/db_password`
* `SECRET_KEY`: `/run/secrets/secret_key`
* `EMAIL_PASSWORD`: `/run/secrets/email_password`
* `NAPALM_PASSWORD`: `/run/secrets/napalm_password`
* `REDIS_PASSWORD`: `/run/secrets/redis_password`
* `AUTH_LDAP_BIND_PASSWORD`: `/run/secrets/auth_ldap_bind_password`
Please also consider [the advice about running Netbox in production](#production) above!
[docker-config]: https://github.com/netbox-community/netbox-docker/blob/master/configuration/configuration.py
[default-config]: https://github.com/netbox-community/netbox/blob/develop/netbox/netbox/configuration.example.py
[entrypoint]: https://github.com/netbox-community/netbox-docker/blob/master/docker/docker-entrypoint.sh
[swarm-config]: https://docs.docker.com/engine/swarm/configs/
[swarm-secrets]: https://docs.docker.com/engine/swarm/secrets/
[openshift-config]: https://docs.openshift.org/latest/dev_guide/configmaps.html
[openshift-secrets]: https://docs.openshift.org/latest/dev_guide/secrets.html
[k8s-secrets]: https://kubernetes.io/docs/concepts/configuration/secret/
[k8s-config]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/
### NAPALM Configuration
Since v2.1.0 NAPALM has been tightly integrated into Netbox.
NAPALM allows Netbox to fetch live data from devices and return it to a requester via its REST API.
To learn more about what NAPALM is and how it works, please see the documentation from the [libary itself][napalm-doc] or the documentation from [Netbox][netbox-napalm-doc] on how it is integrated.
To enable this functionality, simply complete the following lines in `netbox.env` (or appropriate secrets mechanism) :
* `NAPALM_USERNAME`: A common username that can be utilized for connecting to network devices in your environment.
* `NAPALM_PASSWORD`: The password to use in combintation with the username to connect to network devices.
* `NAPALM_TIMEOUT`: A value to use for when an attempt to connect to a device will timeout if no response has been recieved.
However, if you don't need this functionality, leave these blank.
[napalm-doc]: http://napalm.readthedocs.io/en/latest/index.html
[netbox-napalm-doc]: https://netbox.readthedocs.io/en/latest/configuration/optional-settings/#napalm_username
### Customizable Reporting
Netbox includes [customized reporting][netbox-reports-doc] that allows the user to write Python code and determine the validity of the data within Netbox.
The `REPORTS_ROOT` variable is setup as a mapped directory within this Docker container to `/reports/` and includes the example directly from the documentation for `devices.py`.
However, it has been renamed to `devices.py.example` which prevents Netbox from recognizing it as a valid report.
This was done to avoid unnessary issues from being opened when the default does not work for someone's expectations.
To re-enable this default report, simply rename `devices.py.example` to `devices.py` and browse within the WebUI to `/extras/reports/`.
You can also dynamically add any other report to this same directory and Netbox will be able to see it without restarting the container.
[netbox-reports-doc]: https://netbox.readthedocs.io/en/stable/additional-features/reports/
### Custom Initialization Code (e.g. Automatically Setting Up Custom Fields)
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`.
The execution of the startup scripts can be prevented by setting the environment variable `SKIP_STARTUP_SCRIPTS` to `true`, e.g. in the file `env/netbox.env`.
That mechanism can be used for many things, e.g. to create Netbox custom fields:
```python
# docker/startup_scripts/load_custom_fields.py
from django.contrib.contenttypes.models import ContentType
from extras.models import CF_TYPE_TEXT, CustomField
from dcim.models import Device
from dcim.models import DeviceType
device = ContentType.objects.get_for_model(Device)
device_type = ContentType.objects.get_for_model(DeviceType)
my_custom_field, created = CustomField.objects.get_or_create(
type=CF_TYPE_TEXT,
name='my_custom_field',
description='My own custom field'
)
if created:
my_custom_field.obj_type.add(device)
my_custom_field.obj_type.add(device_type)
```
#### Initializers
Initializers are built-in startup scripts for defining Netbox custom fields, groups, users and many other resources.
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
filter_logic: loose
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/netbox-community/netbox-docker/tree/master/initializers
[netbox-docker-compose]: https://github.com/netbox-community/netbox-docker/blob/master/docker-compose.yml
##### Available Groups for User/Group initializers
To get an up-to-date list about all the available permissions, run the following command.
```bash
# Make sure the 'netbox' container is already running! If unsure, run `docker-compose up -d`
echo "from django.contrib.auth.models import Permission\nfor p in Permission.objects.all():\n print(p.codename);" | docker-compose exec -T netbox ./manage.py shell
```
#### Custom Docker Image
You can also build your own Netbox Docker image containing your own startup scripts, custom fields, users and groups
like this:
```Dockerfile
ARG VERSION=latest
FROM netboxcommunity/netbox:$VERSION
COPY startup_scripts/ /opt/netbox/startup_scripts/
COPY initializers/ /opt/netbox/initializers/
```
[wiki]: https://github.com/netbox-community/netbox-docker/wiki/
## Netbox Version
@ -251,174 +90,16 @@ This can increase the build speed if you're just adjusting the config, for examp
[git-ref]: https://git-scm.com/book/en/v2/Git-Internals-Git-References
[netbox-github]: https://github.com/netbox-community/netbox/releases
### LDAP enabled variant
The images tagged with "-ldap" contain anything necessary to authenticate against an LDAP or Active Directory server.
The default configuration `ldap_config.py` is prepared for use with an Active Directory server.
Custom values can be injected using environment variables, similar to the main configuration mechanisms.
## Troubleshooting
This section is a collection of some common issues and how to resolve them.
If your issue is not here, look through [the existing issues][issues] and eventually create a new issue.
[issues]: (https://github.com/netbox-community/netbox-docker/issues)
### Docker Compose basics
* You can see all running containers belonging to this project using `docker-compose ps`.
* You can see the logs by running `docker-compose logs -f`.
Running `docker-compose logs -f netbox` will just show the logs for netbox.
* You can stop everything using `docker-compose stop`.
* You can clean up everything using `docker-compose down -v --remove-orphans`. **This will also remove any related data.**
* You can enter the shell of the running Netbox container using `docker-compose exec netbox /bin/bash`. Now you have access to `./manage.py`, e.g. to reset a password.
* To access the database run `docker-compose exec postgres sh -c 'psql -U $POSTGRES_USER $POSTGRES_DB'`
* To create a database backup run `docker-compose exec postgres sh -c 'pg_dump -cU $POSTGRES_USER $POSTGRES_DB' | gzip > db_dump.sql.gz`
* To restore that database backup run `gunzip -c db_dump.sql.gz | docker exec -i $(docker-compose ps -q postgres) sh -c 'psql -U $POSTGRES_USER $POSTGRES_DB'`.
### Nginx doesn't start
As a first step, stop your docker-compose setup.
Then locate the `netbox-nginx-config` volume and remove it:
```bash
# Stop your local netbox-docker installation
$ docker-compose down
# Find the volume
$ docker volume ls | grep netbox-nginx-config
local netbox-docker_netbox-nginx-config
# Remove the volume
$ docker volume rm netbox-docker_netbox-nginx-config
netbox-docker_netbox-nginx-config
```
Now start everything up again.
If this didn't help, try to see if there's anything in the logs indicating why nginx doesn't start:
```bash
docker-compose logs -f nginx
```
### Getting a "Bad Request (400)"
> When connecting to the Netbox instance, I get a "Bad Request (400)" error.
This usually happens when the `ALLOWED_HOSTS` variable is not set correctly.
### How to upgrade
> How do I update to a newer version of netbox?
It should be sufficient to pull the latest image from Docker Hub, stopping the container and starting it up again:
```bash
docker-compose pull netbox
docker-compose stop netbox netbox-worker
docker-compose rm -f netbox netbox-worker
docker-compose up -d netbox netbox-worker
```
### Webhooks don't work
First make sure that the webhooks feature is enabled in your Netbox configuration and that a redis host is defined.
Check `netbox.env` if the following variables are defined:
```bash
WEBHOOKS_ENABLED=true
REDIS_HOST=redis
```
Then make sure that the `redis` container and at least one `netbox-worker` are running.
```bash
# check the container status
$ docker-compose ps
Name Command State Ports
--------------------------------------------------------------------------------------------------------
netbox-docker_netbox-worker_1 /opt/netbox/docker-entrypo ... Up
netbox-docker_netbox_1 /opt/netbox/docker-entrypo ... Up
netbox-docker_nginx_1 nginx -c /etc/netbox-nginx ... Up 80/tcp, 0.0.0.0:32776->8080/tcp
netbox-docker_postgres_1 docker-entrypoint.sh postgres Up 5432/tcp
netbox-docker_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
# connect to redis and send PING command:
$ docker-compose run --rm -T redis sh -c 'redis-cli -h redis -a $REDIS_PASSWORD ping'
Warning: Using a password with '-a' option on the command line interface may not be safe.
PONG
```
If `redis` and the `netbox-worker` are not available, make sure you have updated your `docker-compose.yml` file!
Everything's up and running? Then check the log of `netbox-worker` and/or `redis`:
```bash
docker-compose logs -f netbox-worker
docker-compose logs -f redis
```
Still no clue? You can connect to the `redis` container and have it report any command that is currently executed on the server:
```bash
docker-compose run --rm -T redis sh -c 'redis-cli -h redis -a $REDIS_PASSWORD monitor'
# Hit CTRL-C a few times to leave
```
If you don't see anything happening after you triggered a webhook, double-check the configuration of the `netbox` and the `netbox-worker` containers and also check the configuration of your webhook in the admin interface of Netbox.
## Breaking Changes
From time to time it might become necessary to re-engineer the structure of this setup.
Things like the `docker-compose.yml` file or your Kubernetes or OpenShift configurations have to be adjusted as a consequence.
Since April 2018 each image built from this repo contains a `NETBOX_DOCKER_PROJECT_VERSION` label.
You can check the label of your local image by running `docker inspect netboxcommunity/netbox:v2.3.1 --format "{{json .ContainerConfig.Labels}}"`.
Compare the version with the list below to check whether a breaking change was introduced with that version.
The following is a list of breaking changes of the `netbox-docker` project:
Please read [the release notes][releases] carefully when updating to a new image version.
* 0.17.0: Updated the python image to `python:3.7-alpine3.10` in [#144][144]. Fixed the permissions and group scripts for Netbox 2.6. in [#148][148].
* 0.16.0: Update the Netbox URL from "github.com/digitalocean/netbox" to "github.com/netbox-community/netbox"
* 0.15.0: Update for Netbox v2.6.0.
The `configuration/configuration.py` file has been updated to match the file from Netbox.
`CORS_ORIGIN_WHITELIST` has a new default value of `http://localhost`.
To provide a nice development environment, `CORS_ORIGIN_ALLOW_ALL` added to `env/netbox.env` with a default value of `True`.
There are also new options:
* `REDIS_CACHE_DATABASE`
* `CACHE_TIMEOUT` (set to 0 to disable caching)
* `CHANGELOG_RETENTION`
* `CORS_ORIGIN_REGEX_WHITELIST` (space separated list of regular expressions)
* `EXEMPT_VIEW_PERMISSIONS` (space separated list)
* `METRICS_ENABLED`
* 0.14.0: Improved caching strategy [#137][137] [#136][136].
New `AUTH_LDAP_GROUP_TYPE` environment variable [#135][135].
* 0.13.0: `AUTH_LDAP_BIND_PASSWORD` can now be extracted into a secrets file. [#133][133]
* 0.12.0: A new flag `REDIS_SSL=false` was added to the `env/netbox.env` file. [#129][129]
* 0.11.0: The docker-compose file now marks volumes as shared (`:z`). This should prevent SELinux problems [#131][131]
* 0.9.0: Upgrade to at least 2.1.5
* 0.8.0: Alpine linux was upgraded to 3.9 [#126][126]
* 0.7.0: The value of the `MAX_PAGE_SIZE` environment variable was changed to `1000`, which is the default of Netbox.
* 0.6.0: The naming of the default startup_scripts were changed.
If you overwrite them, you may need to adjust these scripts.
* 0.5.0: Alpine was updated to 3.8, `*.env` moved to `/env` folder
* 0.4.0: In order to use Netbox webhooks you need to add Redis and a netbox-worker to your docker-compose.yml.
* 0.3.0: Field `filterable: <boolean` was replaced with field `filter_logic: loose/exact/disabled`. It will default to `CF_FILTER_LOOSE=loose` when not defined.
* 0.2.0: Re-organized paths: `/etc/netbox -> /etc/netbox/config` and `/etc/reports -> /etc/netbox/reports`. Fixes [#54][54].
* 0.1.0: Introduction of the `NETBOX_DOCKER_PROJECT_VERSION`. (Not a breaking change per se.)
[54]: https://github.com/netbox-community/netbox-docker/issues/54
[126]: https://github.com/netbox-community/netbox-docker/pull/126
[131]: https://github.com/netbox-community/netbox-docker/pull/131
[129]: https://github.com/netbox-community/netbox-docker/pull/129
[133]: https://github.com/netbox-community/netbox-docker/pull/133
[135]: https://github.com/netbox-community/netbox-docker/pull/135
[136]: https://github.com/netbox-community/netbox-docker/pull/136
[137]: https://github.com/netbox-community/netbox-docker/pull/137
[144]: https://github.com/netbox-community/netbox-docker/pull/144
[148]: https://github.com/netbox-community/netbox-docker/pull/148
[releases]: https://github.com/netbox-community/netbox-docker/releases
## Rebuilding & Publishing images

View File

@ -1 +1 @@
0.17.1
0.18.1

View File

@ -7,31 +7,31 @@ while ! ./manage.py migrate 2>&1; do
sleep 3
done
# create superuser silently
if [ -z ${SUPERUSER_NAME+x} ]; then
SUPERUSER_NAME='admin'
fi
if [ -z ${SUPERUSER_EMAIL+x} ]; then
SUPERUSER_EMAIL='admin@example.com'
fi
if [ -z ${SUPERUSER_PASSWORD+x} ]; then
if [ -f "/run/secrets/superuser_password" ]; then
SUPERUSER_PASSWORD="$(< /run/secrets/superuser_password)"
else
SUPERUSER_PASSWORD='admin'
if [ "$SKIP_SUPERUSER" == "true" ]; then
echo "↩️ Skip creating the superuser"
else
if [ -z ${SUPERUSER_NAME+x} ]; then
SUPERUSER_NAME='admin'
fi
fi
if [ -z ${SUPERUSER_API_TOKEN+x} ]; then
if [ -f "/run/secrets/superuser_api_token" ]; then
SUPERUSER_API_TOKEN="$(< /run/secrets/superuser_api_token)"
else
SUPERUSER_API_TOKEN='0123456789abcdef0123456789abcdef01234567'
if [ -z ${SUPERUSER_EMAIL+x} ]; then
SUPERUSER_EMAIL='admin@example.com'
fi
if [ -z ${SUPERUSER_PASSWORD+x} ]; then
if [ -f "/run/secrets/superuser_password" ]; then
SUPERUSER_PASSWORD="$(< /run/secrets/superuser_password)"
else
SUPERUSER_PASSWORD='admin'
fi
fi
if [ -z ${SUPERUSER_API_TOKEN+x} ]; then
if [ -f "/run/secrets/superuser_api_token" ]; then
SUPERUSER_API_TOKEN="$(< /run/secrets/superuser_api_token)"
else
SUPERUSER_API_TOKEN='0123456789abcdef0123456789abcdef01234567'
fi
fi
fi
echo "💡 Username: ${SUPERUSER_NAME}, E-Mail: ${SUPERUSER_EMAIL}"
./manage.py shell --interface python << END
./manage.py shell --interface python << END
from django.contrib.auth.models import User
from users.models import Token
if not User.objects.filter(username='${SUPERUSER_NAME}'):
@ -39,8 +39,11 @@ if not User.objects.filter(username='${SUPERUSER_NAME}'):
Token.objects.create(user=u, key='${SUPERUSER_API_TOKEN}')
END
echo "💡 Superuser Username: ${SUPERUSER_NAME}, E-Mail: ${SUPERUSER_EMAIL}"
fi
if [ "$SKIP_STARTUP_SCRIPTS" == "true" ]; then
echo " Skipping startup scripts"
echo "↩️ Skipping startup scripts"
else
for script in /opt/netbox/startup_scripts/*.py; do
echo "⚙️ Executing '$script'"
@ -55,4 +58,6 @@ echo "✅ Initialisation is done."
# launch whatever is passed by docker
# (i.e. the RUN instruction in the Dockerfile)
exec ${@}
#
# shellcheck disable=SC2068
exec $@

4
env/netbox.env vendored
View File

@ -20,7 +20,9 @@ REDIS_DATABASE=0
REDIS_CACHE_DATABASE=1
REDIS_SSL=false
SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
SUPERUSER_NAME=admin
SKIP_STARTUP_SCRIPTS=false
SKIP_SUPERUSER=true
SUPERUSER_NAME=admin2
SUPERUSER_EMAIL=admin@example.com
SUPERUSER_PASSWORD=admin
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567

View File

@ -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

View File

@ -0,0 +1,2 @@
# - name: Hyper-V
# slug: hyper-v

View File

@ -0,0 +1,5 @@
# - name: cluster1
# type: Hyper-V
# - name: cluster2
# type: Hyper-V
# site: SING 1

View File

@ -0,0 +1,8 @@
# - device: server01
# enabled: true
# type: Virtual
# name: to-server02
# - device: server02
# enabled: true
# type: Virtual
# name: to-server01

View File

@ -17,7 +17,7 @@
# custom_fields:
# text_field: Description
# - model: Other
# manufacturer: NoName
# manufacturer: No Name
# slug: other
# custom_fields:
# text_field: Description

View File

@ -0,0 +1,26 @@
# - 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

View File

@ -2,5 +2,5 @@
# slug: manufacturer-1
# - name: Manufacturer 2
# slug: manufacturer-2
# - name: NoName
# slug: noname
# - name: No Name
# slug: no-name

View File

@ -1,19 +1,15 @@
# # Allowed rpc clients are: juniper-junos, cisco-ios, opengear
# - name: Platform 1
# slug: platform-1
# manufacturer: Manufacturer 1
# napalm_driver: driver1
# napalm_args: "{'arg1': 'value1', 'arg2': 'value2'}"
# rpc_client: juniper-junos
# - name: Platform 2
# slug: platform-2
# manufacturer: Manufacturer 2
# napalm_driver: driver2
# napalm_args: "{'arg1': 'value1', 'arg2': 'value2'}"
# rpc_client: opengear
# - name: Platform 3
# slug: platform-3
# manufacturer: NoName
# manufacturer: No Name
# napalm_driver: driver3
# napalm_args: "{'arg1': 'value1', 'arg2': 'value2'}"
# rpc_client: juniper-junos

View File

@ -0,0 +1,2 @@
# - name: Main Management
# slug: main-management

20
initializers/prefixes.yml Normal file
View File

@ -0,0 +1,20 @@
# - 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

9
initializers/rirs.yml Normal file
View File

@ -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

View File

@ -0,0 +1,4 @@
# - name: Tenant Group 1
# slug: tenant-group-1
# - name: Tenant Group 2
# slug: tenant-group-2

5
initializers/tenants.yml Normal file
View File

@ -0,0 +1,5 @@
# - name: tenant1
# slug: tenant1
# - name: tenant2
# slug: tenant2
# group: Tenant Group 2

View File

@ -0,0 +1,18 @@
# - 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
# status: Active
# tenant: tenant1
# vcpus: 8

View File

@ -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

View File

@ -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

11
initializers/vlans.yml Normal file
View File

@ -0,0 +1,11 @@
# - 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

8
initializers/vrfs.yml Normal file
View File

@ -0,0 +1,8 @@
# - enforce_unique: true
# name: vrf1
# tenant: tenant1
# description: main VRF
# - enforce_unique: true
# name: vrf2
# rd: "6500:6500"
# tenant: tenant2

View File

@ -27,7 +27,6 @@ with file.open('r') as stream:
group_permissions = group_details.get('permissions', [])
if group_permissions:
group.permissions.clear()
print("Permissions:", group.permissions.all())
for permission_codename in group_details.get('permissions', []):
for permission in Permission.objects.filter(codename=permission_codename):
group.permissions.add(permission)

View File

@ -0,0 +1,19 @@
from tenancy.models import TenantGroup
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/tenant_groups.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
tenant_groups = yaml.load(stream)
if tenant_groups is not None:
for params in tenant_groups:
tenant_group, created = TenantGroup.objects.get_or_create(**params)
if created:
print("🔳 Created Tenant Group", tenant_group.name)

View File

@ -0,0 +1,45 @@
from tenancy.models import Tenant, TenantGroup
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/tenants.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
tenants = yaml.load(stream)
optional_assocs = {
'group': (TenantGroup, 'name')
}
if tenants is not None:
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)

View File

@ -53,6 +53,7 @@ with file.open('r') as stream:
for rack_face in RACK_FACE_CHOICES:
if params['face'] in rack_face:
params['face'] = rack_face[0]
break
device, created = Device.objects.get_or_create(**params)

View File

@ -0,0 +1,19 @@
from virtualization.models import ClusterType
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/cluster_types.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
cluster_types = yaml.load(stream)
if cluster_types is not None:
for params in cluster_types:
cluster_type, created = ClusterType.objects.get_or_create(**params)
if created:
print("🧰 Created Cluster Type", cluster_type.name)

View File

@ -0,0 +1,19 @@
from ipam.models import RIR
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/rirs.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
rirs = yaml.load(stream)
if rirs is not None:
for params in rirs:
rir, created = RIR.objects.get_or_create(**params)
if created:
print("🗺️ Created RIR", rir.name)

View File

@ -0,0 +1,46 @@
from ipam.models import Aggregate, RIR
from ruamel.yaml import YAML
from extras.models import CustomField, CustomFieldValue
from netaddr import IPNetwork
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/aggregates.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
aggregates = yaml.load(stream)
required_assocs = {
'rir': (RIR, 'name')
}
if aggregates is not None:
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)

View File

@ -0,0 +1,57 @@
from dcim.models import Site
from virtualization.models import Cluster, ClusterType, ClusterGroup
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/clusters.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
clusters = yaml.load(stream)
required_assocs = {
'type': (ClusterType, 'name')
}
optional_assocs = {
'site': (Site, 'name'),
'group': (ClusterGroup, 'name')
}
if clusters is not None:
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)

View File

@ -0,0 +1,46 @@
from ipam.models import VRF
from tenancy.models import Tenant
from ruamel.yaml import YAML
from extras.models import CustomField, CustomFieldValue
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/vrfs.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
vrfs = yaml.load(stream)
optional_assocs = {
'tenant': (Tenant, 'name')
}
if vrfs is not None:
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)

View File

@ -0,0 +1,19 @@
from ipam.models import Role
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/prefix_vlan_roles.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
roles = yaml.load(stream)
if roles is not None:
for params in roles:
role, created = Role.objects.get_or_create(**params)
if created:
print("⛹️‍ Created Prefix/VLAN Role", role.name)

View File

@ -0,0 +1,46 @@
from dcim.models import Site
from ipam.models import VLANGroup
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/vlan_groups.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
vlan_groups = yaml.load(stream)
optional_assocs = {
'site': (Site, 'name')
}
if vlan_groups is not None:
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)

View File

@ -0,0 +1,58 @@
from dcim.models import Site
from ipam.models import VLAN, VLANGroup, Role
from ipam.constants import VLAN_STATUS_CHOICES
from tenancy.models import Tenant, TenantGroup
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/vlans.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
vlans = yaml.load(stream)
optional_assocs = {
'site': (Site, 'name'),
'tenant': (Tenant, 'name'),
'tenant_group': (TenantGroup, 'name'),
'group': (VLANGroup, 'name'),
'role': (Role, 'name')
}
if vlans is not None:
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)
if 'status' in params:
for vlan_status in VLAN_STATUS_CHOICES:
if params['status'] in vlan_status:
params['status'] = vlan_status[0]
break
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)

View File

@ -0,0 +1,61 @@
from dcim.models import Site
from ipam.models import Prefix, VLAN, Role, VRF
from ipam.constants import PREFIX_STATUS_CHOICES
from tenancy.models import Tenant, TenantGroup
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from netaddr import IPNetwork
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/prefixes.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
prefixes = yaml.load(stream)
optional_assocs = {
'site': (Site, 'name'),
'tenant': (Tenant, 'name'),
'tenant_group': (TenantGroup, 'name'),
'vlan': (VLAN, 'name'),
'role': (Role, 'name'),
'vrf': (VRF, 'name')
}
if prefixes is not None:
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)
if 'status' in params:
for prefix_status in PREFIX_STATUS_CHOICES:
if params['status'] in prefix_status:
params['status'] = prefix_status[0]
break
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)

View File

@ -0,0 +1,66 @@
from dcim.models import Site, Platform, DeviceRole
from virtualization.models import Cluster, VirtualMachine
from virtualization.constants import VM_STATUS_CHOICES
from tenancy.models import Tenant
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/virtual_machines.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
virtual_machines = yaml.load(stream)
required_assocs = {
'cluster': (Cluster, 'name')
}
optional_assocs = {
'tenant': (Tenant, 'name'),
'platform': (Platform, 'name'),
'role': (DeviceRole, 'name')
}
if virtual_machines is not None:
for params in virtual_machines:
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)
if 'status' in params:
for vm_status in VM_STATUS_CHOICES:
if params['status'] in vm_status:
params['status'] = vm_status[0]
break
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)

View File

@ -0,0 +1,45 @@
from dcim.models import Interface
from virtualization.models import VirtualMachine
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/virtualization_interfaces.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
interfaces = yaml.load(stream)
required_assocs = {
'virtual_machine': (VirtualMachine, 'name')
}
if interfaces is not None:
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.virtual_machine.name)

View File

@ -0,0 +1,55 @@
from dcim.models import Interface, Device
from dcim.constants import IFACE_TYPE_CHOICES
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/dcim_interfaces.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
interfaces = yaml.load(stream)
required_assocs = {
'device': (Device, 'name')
}
if interfaces is not None:
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)
if 'type' in params:
for outer_list in IFACE_TYPE_CHOICES:
for type_choices in outer_list[1]:
if params['type'] in type_choices:
params['type'] = type_choices[0]
break
else:
continue
break
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)

View File

@ -0,0 +1,72 @@
from ipam.models import IPAddress, VRF
from ipam.constants import IPADDRESS_STATUS_CHOICES
from dcim.models import Device, Interface
from virtualization.models import VirtualMachine
from tenancy.models import Tenant
from extras.models import CustomField, CustomFieldValue
from ruamel.yaml import YAML
from netaddr import IPNetwork
from pathlib import Path
import sys
file = Path('/opt/netbox/initializers/ip_addresses.yml')
if not file.is_file():
sys.exit()
with file.open('r') as stream:
yaml = YAML(typ='safe')
ip_addresses = yaml.load(stream)
optional_assocs = {
'tenant': (Tenant, 'name'),
'vrf': (VRF, 'name'),
'interface': (Interface, 'name')
}
if ip_addresses is not None:
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 = { field: params.pop(assoc), "virtual_machine_id": vm_id }
elif device:
dev_id = Device.objects.get(name=device).id
query = { field: params.pop(assoc), "device_id": dev_id }
else:
query = { field: params.pop(assoc) }
params[assoc] = model.objects.get(**query)
if 'status' in params:
for ip_status in IPADDRESS_STATUS_CHOICES:
if params['status'] in ip_status:
params['status'] = ip_status[0]
break
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)