diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 50d17e9..e1780b7 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -19,8 +19,7 @@ jobs: - ./build.sh develop docker_from: - '' # use the default of the build script - - python:3.8-alpine - - python:3.9-alpine + # - python:3.10-rc-alpine # disable until dependencies work fail-fast: false runs-on: ubuntu-latest name: Builds new Netbox Docker Images diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9dbf770..32cc9ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,3 +50,34 @@ jobs: name: Logout of the Docker Registry run: docker logout "${DOCKER_REGISTRY}" if: steps.docker-build.outputs.skipped != 'true' + + # Quay.io + - id: quayio-docker-build + name: Build the image with '${{ matrix.build_cmd }}' + run: ${{ matrix.build_cmd }} + env: + DOCKER_REGISTRY: quay.io + GH_ACTION: enable + - id: quayio-registry-login + name: Login to the Quay.io Registry + run: | + echo "::add-mask::$QUAYIO_USERNAME" + echo "::add-mask::$QUAYIO_PASSWORD" + docker login -u "$QUAYIO_USERNAME" --password "${QUAYIO_PASSWORD}" "${DOCKER_REGISTRY}" + env: + DOCKER_REGISTRY: quay.io + QUAYIO_USERNAME: ${{ secrets.quayio_username }} + QUAYIO_PASSWORD: ${{ secrets.quayio_password }} + if: steps.docker-build.outputs.skipped != 'true' + - id: quayio-registry-push + name: Push the image + run: ${{ matrix.build_cmd }} --push-only + env: + DOCKER_REGISTRY: quay.io + if: steps.docker-build.outputs.skipped != 'true' + - id: quayio-registry-logout + name: Logout of the Docker Registry + run: docker logout "${DOCKER_REGISTRY}" + env: + DOCKER_REGISTRY: quay.io + if: steps.docker-build.outputs.skipped != 'true' diff --git a/README.md b/README.md index 7c4d20d..11edf52 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![GitHub license](https://img.shields.io/github/license/netbox-community/netbox-docker)][netbox-docker-license] [The Github repository](netbox-docker-github) houses the components needed to build Netbox as a Docker container. -Images are built using this code and are released to [Docker Hub][netbox-dockerhub] once a day. +Images are built using this code and are released to [Docker Hub][netbox-dockerhub] and [Quay.io][netbox-quayio] once a day. 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. @@ -18,11 +18,12 @@ Before opening an issue on Github, please join the [Network To Code][ntc-slack] [github-stargazers]: https://github.com/netbox-community/netbox-docker/stargazers [github-release]: https://github.com/netbox-community/netbox-docker/releases [netbox-docker-microbadger]: https://microbadger.com/images/netboxcommunity/netbox -[netbox-dockerhub]: https://hub.docker.com/r/netboxcommunity/netbox/tags/ +[netbox-dockerhub]: https://hub.docker.com/r/netboxcommunity/netbox/ [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 [netbox-docker-license]: https://github.com/netbox-community/netbox-docker/blob/release/LICENSE +[netbox-quayio]: https://quay.io/repository/netboxcommunity/netbox ## Docker Tags @@ -85,11 +86,13 @@ It covers advanced topics such as using files for secrets, deployment to Kuberne ## Getting Help -Please join [our Slack channel `#netbox-docker`][netbox-docker-slack] on the [Network To Code Slack][ntc-slack]. -It's free to use and there are almost always people online that can help. +Feel free to ask questions in our [Github Community][netbox-community] or join [our Slack channel `#netbox-docker`][netbox-docker-slack] on the [Network To Code Slack][ntc-slack], +which is free to use and where there are almost always people online that can help you in the Slack channel. If you need help with using Netbox or developing for it or against it's API you may find the `#netbox` channel on the same Slack instance very helpful. +[netbox-community]: https://github.com/netbox-community/netbox-docker/discussions + ## Dependencies This project relies only on *Docker* and *docker-compose* meeting these requirements: @@ -104,7 +107,7 @@ To check the version installed on your system run `docker --version` and `docker The `docker-compose.yml` file is prepared to run a specific version of Netbox, instead of `latest`. To use this feature, set and export the environment-variable `VERSION` before launching `docker-compose`, as shown below. `VERSION` may be set to the name of -[any tag of the `netboxcommunity/netbox` Docker image on Docker Hub][netbox-dockerhub]. +[any tag of the `netboxcommunity/netbox` Docker image on Docker Hub][netbox-dockerhub] or [Quay.io][netbox-quayio]. ```bash export VERSION=v2.7.1 diff --git a/VERSION b/VERSION index 894542a..1b58cc1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.26.2 +0.27.0 diff --git a/build.sh b/build.sh index 360f08c..19c56c8 100755 --- a/build.sh +++ b/build.sh @@ -49,7 +49,7 @@ if [ "${1}x" == "x" ] || [ "${1}" == "--help" ] || [ "${1}" == "-h" ]; then echo " DOCKERFILE The name of Dockerfile to use." echo " Default: Dockerfile" echo " DOCKER_FROM The base image to use." - echo " Default: 'python:3.8-alpine'" + echo " Default: 'python:3.9-alpine'" echo " DOCKER_TARGET A specific target to build." echo " It's currently not possible to pass multiple targets." echo " Default: main ldap" @@ -157,7 +157,7 @@ fi # Determining the value for DOCKER_FROM ### if [ -z "$DOCKER_FROM" ]; then - DOCKER_FROM="python:3.8-alpine" + DOCKER_FROM="python:3.9-alpine" fi ### diff --git a/configuration/configuration.py b/configuration/configuration.py index 639e1b6..36517f0 100644 --- a/configuration/configuration.py +++ b/configuration/configuration.py @@ -157,7 +157,7 @@ LOGIN_REQUIRED = environ.get('LOGIN_REQUIRED', 'False').lower() == 'true' # The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to # re-authenticate. (Default: 1209600 [14 days]) -LOGIN_TIMEOUT = environ.get('LOGIN_TIMEOUT', None) +LOGIN_TIMEOUT = int(environ.get('LOGIN_TIMEOUT', 1209600)) # Setting this to True will display a "maintenance mode" banner at the top of every page. MAINTENANCE_MODE = environ.get('MAINTENANCE_MODE', 'False').lower() == 'true' @@ -233,7 +233,7 @@ SCRIPTS_ROOT = environ.get('SCRIPTS_ROOT', '/etc/netbox/scripts') # By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use # local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only # database access.) Note that the user as which NetBox runs must have read and write permissions to this path. -SESSION_FILE_PATH = environ.get('REPORTS_ROOT', None) +SESSION_FILE_PATH = environ.get('SESSIONS_ROOT', None) # Time zone (default: UTC) TIME_ZONE = environ.get('TIME_ZONE', 'UTC') diff --git a/configuration/ldap/ldap_config.py b/configuration/ldap/ldap_config.py index 4cd5b8b..1ed599f 100644 --- a/configuration/ldap/ldap_config.py +++ b/configuration/ldap/ldap_config.py @@ -60,14 +60,17 @@ AUTH_LDAP_GROUP_SEARCH = LDAPSearch(AUTH_LDAP_GROUP_SEARCH_BASEDN, ldap.SCOPE_SU AUTH_LDAP_GROUP_TYPE = _import_group_type(environ.get('AUTH_LDAP_GROUP_TYPE', 'GroupOfNamesType')) # Define a group required to login. -AUTH_LDAP_REQUIRE_GROUP = environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', '') +AUTH_LDAP_REQUIRE_GROUP = environ.get('AUTH_LDAP_REQUIRE_GROUP_DN') # Define special user types using groups. Exercise great caution when assigning superuser status. -AUTH_LDAP_USER_FLAGS_BY_GROUP = { - "is_active": environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', ''), - "is_staff": environ.get('AUTH_LDAP_IS_ADMIN_DN', ''), - "is_superuser": environ.get('AUTH_LDAP_IS_SUPERUSER_DN', '') -} +AUTH_LDAP_USER_FLAGS_BY_GROUP = {} + +if AUTH_LDAP_REQUIRE_GROUP is not None: + AUTH_LDAP_USER_FLAGS_BY_GROUP = { + "is_active": environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', ''), + "is_staff": environ.get('AUTH_LDAP_IS_ADMIN_DN', ''), + "is_superuser": environ.get('AUTH_LDAP_IS_SUPERUSER_DN', '') + } # For more granular permissions, we can map LDAP groups to Django groups. AUTH_LDAP_FIND_GROUP_PERMS = environ.get('AUTH_LDAP_FIND_GROUP_PERMS', 'True').lower() == 'true' diff --git a/initializers/custom_fields.yml b/initializers/custom_fields.yml index 4085ab0..654fb69 100644 --- a/initializers/custom_fields.yml +++ b/initializers/custom_fields.yml @@ -43,20 +43,16 @@ # required: false # filter_logic: exact # weight: 30 +# default: First Item # 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 -# select_field_auto_weight: +# - First Item +# - Second Item +# - Third Item +# - Fifth Item +# - Fourth Item +# select_field_legacy_format: # type: select # label: Choose between items # required: false @@ -65,9 +61,9 @@ # on_objects: # - dcim.models.Device # choices: -# - value: A -# - value: B -# - value: C +# - value: A # this is the deprecated format. +# - value: B # we only use it for the tests. +# - value: C # please see above for the new format. # - value: "D like deprecated" # weight: 999 # - value: E @@ -76,7 +72,7 @@ # label: Yes Or No? # required: true # filter_logic: loose -# default: "false" # important: but "false" in quotes! +# default: "false" # important: put "false" in quotes! # weight: 90 # on_objects: # - dcim.models.Device diff --git a/initializers/device_types.yml b/initializers/device_types.yml index 9b27da3..88798b5 100644 --- a/initializers/device_types.yml +++ b/initializers/device_types.yml @@ -2,22 +2,22 @@ # manufacturer: Manufacturer 1 # slug: model-1 # u_height: 2 -# custom_fields: +# custom_field_data: # text_field: Description # - model: Model 2 # manufacturer: Manufacturer 1 # slug: model-2 -# custom_fields: +# custom_field_data: # text_field: Description # - model: Model 3 # manufacturer: Manufacturer 1 # slug: model-3 # is_full_depth: false # u_height: 0 -# custom_fields: +# custom_field_data: # text_field: Description # - model: Other # manufacturer: No Name # slug: other -# custom_fields: +# custom_field_data: # text_field: Description diff --git a/initializers/devices.yml b/initializers/devices.yml index e968503..0de0504 100644 --- a/initializers/devices.yml +++ b/initializers/devices.yml @@ -20,7 +20,7 @@ # rack: rack-01 # face: front # position: 1 -# custom_fields: +# custom_field_data: # text_field: Description # - name: server02 # device_role: server @@ -31,7 +31,7 @@ # position: 2 # primary_ip4: 10.1.1.2/24 # primary_ip6: 2001:db8:a000:1::2/64 -# custom_fields: +# custom_field_data: # text_field: Description # - name: server03 # device_role: server @@ -40,5 +40,5 @@ # rack: rack-03 # face: front # position: 3 -# custom_fields: +# custom_field_data: # text_field: Description diff --git a/initializers/racks.yml b/initializers/racks.yml index 51502de..379553d 100644 --- a/initializers/racks.yml +++ b/initializers/racks.yml @@ -20,7 +20,7 @@ # type: 4-post-cabinet # width: 19 # u_height: 47 -# custom_fields: +# custom_field_data: # text_field: Description # - site: AMS 2 # name: rack-02 @@ -28,7 +28,7 @@ # type: 4-post-cabinet # width: 19 # u_height: 47 -# custom_fields: +# custom_field_data: # text_field: Description # - site: SING 1 # name: rack-03 @@ -37,5 +37,5 @@ # type: 4-post-cabinet # width: 19 # u_height: 47 -# custom_fields: +# custom_field_data: # text_field: Description diff --git a/initializers/sites.yml b/initializers/sites.yml index f3e05ba..0015f4e 100644 --- a/initializers/sites.yml +++ b/initializers/sites.yml @@ -4,29 +4,29 @@ # status: active # facility: Amsterdam 1 # asn: 12345 -# custom_fields: -# text_field: Description +# custom_field_data: +# text_field: Description for AMS1 # - name: AMS 2 # slug: ams2 # region: Downtown # status: active # facility: Amsterdam 2 # asn: 54321 -# custom_fields: -# text_field: Description +# custom_field_data: +# text_field: Description for AMS2 # - name: AMS 3 # slug: ams3 # region: Suburbs # status: active # facility: Amsterdam 3 # asn: 67890 -# custom_fields: -# text_field: Description +# custom_field_data: +# text_field: Description for AMS3 # - name: SING 1 # slug: sing1 # region: Singapore # status: active # facility: Singapore 1 # asn: 09876 -# custom_fields: -# text_field: Description +# custom_field_data: +# text_field: Description for SING1 diff --git a/startup_scripts/000_users.py b/startup_scripts/000_users.py index a801d85..ffd4bec 100644 --- a/startup_scripts/000_users.py +++ b/startup_scripts/000_users.py @@ -1,6 +1,6 @@ import sys -from django.contrib.auth.models import Group, User +from django.contrib.auth.models import User from startup_script_utils import load_yaml, set_permissions from users.models import Token diff --git a/startup_scripts/020_custom_fields.py b/startup_scripts/020_custom_fields.py index 2cb48a0..7479a5e 100644 --- a/startup_scripts/020_custom_fields.py +++ b/startup_scripts/020_custom_fields.py @@ -1,8 +1,8 @@ -from extras.models import CustomField, CustomFieldChoice - -from startup_script_utils import load_yaml import sys +from extras.models import CustomField +from startup_script_utils import load_yaml + def get_class_for_class_path(class_path): import importlib from django.contrib.contenttypes.models import ContentType @@ -21,34 +21,38 @@ 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): + if cf_details.get('default', False): custom_field.default = cf_details['default'] - if cf_details.get('description', 0): + if cf_details.get('description', False): custom_field.description = cf_details['description'] - if cf_details.get('label', 0): + if cf_details.get('label', False): 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)) + custom_field.content_types.add(get_class_for_class_path(object_type)) - if cf_details.get('required', 0): + if cf_details.get('required', False): custom_field.required = cf_details['required'] - if cf_details.get('type', 0): + if cf_details.get('type', False): custom_field.type = cf_details['type'] - if cf_details.get('weight', 0): + if cf_details.get('weight', -1) >= 0: custom_field.weight = cf_details['weight'] + if cf_details.get('choices', False): + custom_field.choices = [] + + for choice_detail in cf_details.get('choices', []): + if isinstance(choice_detail, dict) and 'value' in choice_detail: + # legacy mode + print(f"โš ๏ธ Please migrate the choice '{choice_detail['value']}' of '{cf_name}' to the new format, as 'weight' is no longer supported!") + custom_field.choices.append(choice_detail['value']) + else: + custom_field.choices.append(choice_detail) + custom_field.save() - for idx, choice_details in enumerate(cf_details.get('choices', [])): - choice, _ = CustomFieldChoice.objects.get_or_create( - field=custom_field, - value=choice_details['value'], - defaults={'weight': idx * 10} - ) - print("๐Ÿ”ง Created custom field", cf_name) diff --git a/startup_scripts/040_sites.py b/startup_scripts/040_sites.py index 828a86b..a4f7e71 100644 --- a/startup_scripts/040_sites.py +++ b/startup_scripts/040_sites.py @@ -1,9 +1,9 @@ -from dcim.models import Region, Site -from extras.models import CustomField, CustomFieldValue -from tenancy.models import Tenant -from startup_script_utils import load_yaml import sys +from dcim.models import Region, Site +from startup_script_utils import * +from tenancy.models import Tenant + sites = load_yaml('/opt/netbox/initializers/sites.yml') if sites is None: @@ -15,7 +15,7 @@ optional_assocs = { } for params in sites: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in optional_assocs.items(): if assoc in params: @@ -27,15 +27,6 @@ for params in sites: site, created = Site.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=site, - value=cf_value - ) - - site.custom_field_values.add(custom_field_value) + set_custom_fields_values(site, custom_field_data) print("๐Ÿ“ Created site", site.name) diff --git a/startup_scripts/060_device_types.py b/startup_scripts/060_device_types.py index e6cea93..e0695b1 100644 --- a/startup_scripts/060_device_types.py +++ b/startup_scripts/060_device_types.py @@ -1,9 +1,9 @@ -from dcim.models import DeviceType, Manufacturer, Region -from tenancy.models import Tenant -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml import sys +from dcim.models import DeviceType, Manufacturer, Region +from startup_script_utils import * +from tenancy.models import Tenant + device_types = load_yaml('/opt/netbox/initializers/device_types.yml') if device_types is None: @@ -19,7 +19,7 @@ optional_assocs = { } for params in device_types: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in required_assocs.items(): model, field = details @@ -37,15 +37,6 @@ for params in device_types: device_type, created = DeviceType.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=device_type, - value=cf_value - ) - - device_type.custom_field_values.add(custom_field_value) + set_custom_fields_values(device_type, custom_field_data) print("๐Ÿ”ก Created device type", device_type.manufacturer, device_type.model) diff --git a/startup_scripts/080_racks.py b/startup_scripts/080_racks.py index 279cb2c..4504624 100644 --- a/startup_scripts/080_racks.py +++ b/startup_scripts/080_racks.py @@ -1,9 +1,9 @@ -from dcim.models import Site, RackRole, Rack, RackGroup -from tenancy.models import Tenant -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml import sys +from dcim.models import Site, RackRole, Rack, RackGroup +from startup_script_utils import * +from tenancy.models import Tenant + racks = load_yaml('/opt/netbox/initializers/racks.yml') if racks is None: @@ -20,7 +20,7 @@ optional_assocs = { } for params in racks: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in required_assocs.items(): model, field = details @@ -38,15 +38,6 @@ for params in racks: rack, created = Rack.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=rack, - value=cf_value - ) - - rack.custom_field_values.add(custom_field_value) + set_custom_fields_values(rack, custom_field_data) print("๐Ÿ”ณ Created rack", rack.site, rack.name) diff --git a/startup_scripts/120_tenants.py b/startup_scripts/120_tenants.py index 121c83a..3b3b221 100644 --- a/startup_scripts/120_tenants.py +++ b/startup_scripts/120_tenants.py @@ -1,8 +1,8 @@ -from tenancy.models import Tenant, TenantGroup -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml import sys +from startup_script_utils import * +from tenancy.models import Tenant, TenantGroup + tenants = load_yaml('/opt/netbox/initializers/tenants.yml') if tenants is None: @@ -13,7 +13,7 @@ optional_assocs = { } for params in tenants: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in optional_assocs.items(): if assoc in params: @@ -25,15 +25,6 @@ for params in tenants: 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) + set_custom_fields_values(tenant, custom_field_data) print("๐Ÿ‘ฉโ€๐Ÿ’ป Created Tenant", tenant.name) diff --git a/startup_scripts/130_devices.py b/startup_scripts/130_devices.py index 7233dd0..d73fd07 100644 --- a/startup_scripts/130_devices.py +++ b/startup_scripts/130_devices.py @@ -1,10 +1,10 @@ -from dcim.models import Site, Rack, DeviceRole, DeviceType, Device, Platform -from virtualization.models import Cluster -from tenancy.models import Tenant -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml import sys +from dcim.models import Site, Rack, DeviceRole, DeviceType, Device, Platform +from startup_script_utils import * +from tenancy.models import Tenant +from virtualization.models import Cluster + devices = load_yaml('/opt/netbox/initializers/devices.yml') if devices is None: @@ -24,7 +24,8 @@ optional_assocs = { } for params in devices: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) + # primary ips are handled later in `270_primary_ips.py` params.pop('primary_ip4', None) params.pop('primary_ip6', None) @@ -45,15 +46,6 @@ for params in devices: device, created = Device.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=device, - value=cf_value - ) - - device.custom_field_values.add(custom_field_value) + set_custom_fields_values(device, custom_field_data) print("๐Ÿ–ฅ๏ธ Created device", device.name) diff --git a/startup_scripts/160_aggregates.py b/startup_scripts/160_aggregates.py index 0ffe9b0..cc1d220 100644 --- a/startup_scripts/160_aggregates.py +++ b/startup_scripts/160_aggregates.py @@ -1,11 +1,9 @@ -from ipam.models import Aggregate, RIR - -from extras.models import CustomField, CustomFieldValue - -from netaddr import IPNetwork -from startup_script_utils import load_yaml import sys +from ipam.models import Aggregate, RIR +from netaddr import IPNetwork +from startup_script_utils import * + aggregates = load_yaml('/opt/netbox/initializers/aggregates.yml') if aggregates is None: @@ -16,7 +14,8 @@ required_assocs = { } for params in aggregates: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) + params['prefix'] = IPNetwork(params['prefix']) for assoc, details in required_assocs.items(): @@ -28,15 +27,6 @@ for params in aggregates: 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) + set_custom_fields_values(aggregate, custom_field_data) print("๐Ÿ—ž๏ธ Created Aggregate", aggregate.prefix) diff --git a/startup_scripts/170_clusters.py b/startup_scripts/170_clusters.py index a7e2065..ffd965e 100644 --- a/startup_scripts/170_clusters.py +++ b/startup_scripts/170_clusters.py @@ -1,9 +1,9 @@ -from dcim.models import Site -from virtualization.models import Cluster, ClusterType, ClusterGroup -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml import sys +from dcim.models import Site +from startup_script_utils import * +from virtualization.models import Cluster, ClusterType, ClusterGroup + clusters = load_yaml('/opt/netbox/initializers/clusters.yml') if clusters is None: @@ -19,7 +19,7 @@ optional_assocs = { } for params in clusters: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in required_assocs.items(): model, field = details @@ -37,15 +37,6 @@ for params in clusters: 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) + set_custom_fields_values(cluster, custom_field_data) print("๐Ÿ—„๏ธ Created cluster", cluster.name) diff --git a/startup_scripts/180_vrfs.py b/startup_scripts/180_vrfs.py index 496710d..2f22316 100644 --- a/startup_scripts/180_vrfs.py +++ b/startup_scripts/180_vrfs.py @@ -1,11 +1,9 @@ -from ipam.models import VRF -from tenancy.models import Tenant - -from extras.models import CustomField, CustomFieldValue - -from startup_script_utils import load_yaml import sys +from ipam.models import VRF +from startup_script_utils import * +from tenancy.models import Tenant + vrfs = load_yaml('/opt/netbox/initializers/vrfs.yml') if vrfs is None: @@ -16,7 +14,7 @@ optional_assocs = { } for params in vrfs: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in optional_assocs.items(): if assoc in params: @@ -28,15 +26,6 @@ for params in vrfs: 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) + set_custom_fields_values(vrf, custom_field_data) print("๐Ÿ“ฆ Created VRF", vrf.name) diff --git a/startup_scripts/200_vlan_groups.py b/startup_scripts/200_vlan_groups.py index f8dc55d..35c3616 100644 --- a/startup_scripts/200_vlan_groups.py +++ b/startup_scripts/200_vlan_groups.py @@ -1,8 +1,8 @@ +import sys + from dcim.models import Site from ipam.models import VLANGroup -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml -import sys +from startup_script_utils import * vlan_groups = load_yaml('/opt/netbox/initializers/vlan_groups.yml') @@ -14,7 +14,7 @@ optional_assocs = { } for params in vlan_groups: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in optional_assocs.items(): if assoc in params: @@ -26,15 +26,6 @@ for params in vlan_groups: 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) + set_custom_fields_values(vlan_group, custom_field_data) print("๐Ÿ˜๏ธ Created VLAN Group", vlan_group.name) diff --git a/startup_scripts/210_vlans.py b/startup_scripts/210_vlans.py index ceab196..7848fc1 100644 --- a/startup_scripts/210_vlans.py +++ b/startup_scripts/210_vlans.py @@ -1,9 +1,9 @@ +import sys + from dcim.models import Site from ipam.models import VLAN, VLANGroup, Role +from startup_script_utils import * from tenancy.models import Tenant, TenantGroup -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml -import sys vlans = load_yaml('/opt/netbox/initializers/vlans.yml') @@ -19,7 +19,7 @@ optional_assocs = { } for params in vlans: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in optional_assocs.items(): if assoc in params: @@ -31,15 +31,6 @@ for params in vlans: 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) + set_custom_fields_values(vlan, custom_field_data) print("๐Ÿ  Created VLAN", vlan.name) diff --git a/startup_scripts/220_prefixes.py b/startup_scripts/220_prefixes.py index b047c8c..9175521 100644 --- a/startup_scripts/220_prefixes.py +++ b/startup_scripts/220_prefixes.py @@ -1,10 +1,10 @@ +import sys + from dcim.models import Site from ipam.models import Prefix, VLAN, Role, VRF -from tenancy.models import Tenant, TenantGroup -from extras.models import CustomField, CustomFieldValue from netaddr import IPNetwork -from startup_script_utils import load_yaml -import sys +from startup_script_utils import * +from tenancy.models import Tenant, TenantGroup prefixes = load_yaml('/opt/netbox/initializers/prefixes.yml') @@ -21,7 +21,8 @@ optional_assocs = { } for params in prefixes: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) + params['prefix'] = IPNetwork(params['prefix']) for assoc, details in optional_assocs.items(): @@ -33,14 +34,6 @@ for params in prefixes: 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) + set_custom_fields_values(prefix, custom_field_data) print("๐Ÿ“Œ Created Prefix", prefix.prefix) diff --git a/startup_scripts/230_virtual_machines.py b/startup_scripts/230_virtual_machines.py index f138886..328d3ea 100644 --- a/startup_scripts/230_virtual_machines.py +++ b/startup_scripts/230_virtual_machines.py @@ -1,10 +1,10 @@ -from dcim.models import Site, Platform, DeviceRole -from virtualization.models import Cluster, VirtualMachine -from tenancy.models import Tenant -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml import sys +from dcim.models import Platform, DeviceRole +from startup_script_utils import * +from tenancy.models import Tenant +from virtualization.models import Cluster, VirtualMachine + virtual_machines = load_yaml('/opt/netbox/initializers/virtual_machines.yml') if virtual_machines is None: @@ -21,7 +21,8 @@ optional_assocs = { } for params in virtual_machines: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) + # primary ips are handled later in `270_primary_ips.py` params.pop('primary_ip4', None) params.pop('primary_ip6', None) @@ -42,15 +43,6 @@ for params in virtual_machines: 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) + set_custom_fields_values(virtual_machine, custom_field_data) print("๐Ÿ–ฅ๏ธ Created virtual machine", virtual_machine.name) diff --git a/startup_scripts/240_virtualization_interfaces.py b/startup_scripts/240_virtualization_interfaces.py index f04f30b..c6f8e50 100644 --- a/startup_scripts/240_virtualization_interfaces.py +++ b/startup_scripts/240_virtualization_interfaces.py @@ -1,8 +1,8 @@ -from virtualization.models import VirtualMachine, VMInterface -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml import sys +from startup_script_utils import * +from virtualization.models import VirtualMachine, VMInterface + interfaces = load_yaml('/opt/netbox/initializers/virtualization_interfaces.yml') if interfaces is None: @@ -13,7 +13,7 @@ required_assocs = { } for params in interfaces: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in required_assocs.items(): model, field = details @@ -24,15 +24,6 @@ for params in interfaces: interface, created = VMInterface.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) + set_custom_fields_values(interface, custom_field_data) print("๐Ÿงท Created interface", interface.name, interface.virtual_machine.name) diff --git a/startup_scripts/250_dcim_interfaces.py b/startup_scripts/250_dcim_interfaces.py index 51f885b..3b7067d 100644 --- a/startup_scripts/250_dcim_interfaces.py +++ b/startup_scripts/250_dcim_interfaces.py @@ -1,9 +1,9 @@ -from dcim.models import Interface, Device -from extras.models import CustomField, CustomFieldValue -from startup_script_utils import load_yaml import sys -interfaces= load_yaml('/opt/netbox/initializers/dcim_interfaces.yml') +from dcim.models import Interface, Device +from startup_script_utils import * + +interfaces = load_yaml('/opt/netbox/initializers/dcim_interfaces.yml') if interfaces is None: sys.exit() @@ -13,7 +13,7 @@ required_assocs = { } for params in interfaces: - custom_fields = params.pop('custom_fields', None) + custom_field_data = pop_custom_fields(params) for assoc, details in required_assocs.items(): model, field = details @@ -24,15 +24,6 @@ for params in interfaces: 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) + set_custom_fields_values(interface, custom_field_data) print("๐Ÿงท Created interface", interface.name, interface.device.name) diff --git a/startup_scripts/260_ip_addresses.py b/startup_scripts/260_ip_addresses.py index 7d164fd..a0582a2 100644 --- a/startup_scripts/260_ip_addresses.py +++ b/startup_scripts/260_ip_addresses.py @@ -3,10 +3,9 @@ import sys from dcim.models import Device, Interface from django.contrib.contenttypes.models import ContentType from django.db.models import Q -from extras.models import CustomField, CustomFieldValue from ipam.models import VRF, IPAddress from netaddr import IPNetwork -from startup_script_utils import load_yaml +from startup_script_utils import * from tenancy.models import Tenant from virtualization.models import VirtualMachine, VMInterface @@ -25,9 +24,10 @@ vm_interface_ct = ContentType.objects.filter(Q(app_label='virtualization', model interface_ct = ContentType.objects.filter(Q(app_label='dcim', model='interface')).first() for params in ip_addresses: + custom_field_data = pop_custom_fields(params) + 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: @@ -55,15 +55,6 @@ for params in ip_addresses: 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) + set_custom_fields_values(ip_address, custom_field_data) print("๐Ÿงฌ Created IP Address", ip_address.address) diff --git a/startup_scripts/startup_script_utils/__init__.py b/startup_scripts/startup_script_utils/__init__.py index c3cf28f..1e9f042 100644 --- a/startup_scripts/startup_script_utils/__init__.py +++ b/startup_scripts/startup_script_utils/__init__.py @@ -1,2 +1,3 @@ from .load_yaml import load_yaml from .permissions import set_permissions +from .custom_fields import set_custom_fields_values, pop_custom_fields diff --git a/startup_scripts/startup_script_utils/custom_fields.py b/startup_scripts/startup_script_utils/custom_fields.py new file mode 100644 index 0000000..1be7a94 --- /dev/null +++ b/startup_scripts/startup_script_utils/custom_fields.py @@ -0,0 +1,15 @@ +def set_custom_fields_values(entity, custom_field_data): + if not custom_field_data: + return + + entity.custom_field_data = custom_field_data + return entity.save() + +def pop_custom_fields(params): + if 'custom_field_data' in params: + return params.pop('custom_field_data') + elif 'custom_fields' in params: + print("โš ๏ธ Please rename 'custom_fields' to 'custom_field_data'!") + return params.pop('custom_fields') + + return None diff --git a/startup_scripts/startup_script_utils/load_yaml.py b/startup_scripts/startup_script_utils/load_yaml.py index 4c16816..ecae7af 100644 --- a/startup_scripts/startup_script_utils/load_yaml.py +++ b/startup_scripts/startup_script_utils/load_yaml.py @@ -1,5 +1,6 @@ -from ruamel.yaml import YAML from pathlib import Path +from ruamel.yaml import YAML + def load_yaml(yaml_file: str): yf = Path(yaml_file) diff --git a/test.sh b/test.sh index 26de625..fa6fa38 100755 --- a/test.sh +++ b/test.sh @@ -1,12 +1,29 @@ #!/bin/bash +# Runs the original Netbox unit tests and tests whether all initializers work. +# Usage: +# ./test.sh latest +# ./test.sh v2.9.7 +# ./test.sh develop-2.10 +# IMAGE='netboxcommunity/netbox:latest' ./test.sh +# IMAGE='netboxcommunity/netbox:v2.9.7' ./test.sh +# IMAGE='netboxcommunity/netbox:develop-2.10' ./test.sh +# export IMAGE='netboxcommunity/netbox:latest'; ./test.sh +# export IMAGE='netboxcommunity/netbox:v2.9.7'; ./test.sh +# export IMAGE='netboxcommunity/netbox:develop-2.10'; ./test.sh # exit when a command exits with an exit code != 0 set -e -# version is used by `docker-compose.yml` do determine the tag +# IMAGE is used by `docker-compose.yml` do determine the tag # of the Docker Image that is to be used -export IMAGE="${IMAGE-netboxcommunity/netbox:latest}" +if [ "${1}x" != "x" ]; then + # Use the command line argument + export IMAGE="netboxcommunity/netbox:${1}" +else + export IMAGE="${IMAGE-netboxcommunity/netbox:latest}" +fi +# Ensure that an IMAGE is defined if [ -z "${IMAGE}" ]; then echo "โš ๏ธ No image defined"