diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9756d69..9912258f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,8 +79,8 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.10"] # ["3.8"] - nautobot-version: ["2.0.0-rc.2"] # ["1.5.3"] + python-version: ["3.11"] + nautobot-version: ["2.0.0-rc.2"] env: INVOKE_NAUTOBOT_GOLDEN_CONFIG_PYTHON_VER: "${{ matrix.python-version }}" INVOKE_NAUTOBOT_GOLDEN_CONFIG_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" @@ -116,7 +116,7 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.10"] # ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] db-backend: ["postgresql"] nautobot-version: ["2.0.0-rc.2"] # ["latest"] # The include is a method to limit the amount of jobs ran. This essentially @@ -125,13 +125,13 @@ jobs: include: - python-version: "3.11" db-backend: "postgresql" - nautobot-version: "2.0.0-rc.2" # "1.5.3" - - python-version: "3.10" # "3.7" + nautobot-version: "2.0.0-rc.2" + - python-version: "3.11" + db-backend: "mysql" + nautobot-version: "2.0.0-rc.2" + - python-version: "3.8" db-backend: "mysql" - nautobot-version: "2.0.0-rc.2" # "1.5.3" - #- python-version: "3.10" - # db-backend: "mysql" - # nautobot-version: "latest" + nautobot-version: "latest" runs-on: "ubuntu-20.04" env: INVOKE_NAUTOBOT_GOLDEN_CONFIG_PYTHON_VER: "${{ matrix.python-version }}" @@ -206,7 +206,7 @@ jobs: - name: "Set up Python" uses: "actions/setup-python@v2" with: - python-version: "3.10" # "3.9" + python-version: "3.11" - name: "Install Python Packages" run: "pip install poetry" - name: "Set env" diff --git a/.readthedocs.yaml b/.readthedocs.yaml index a9d358ef..a6334706 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,7 +10,7 @@ version: 2 build: os: "ubuntu-22.04" tools: - python: "3.10" + python: "3.11" mkdocs: configuration: "mkdocs.yml" diff --git a/development/Dockerfile b/development/Dockerfile index 7afbab89..7b596e27 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -6,11 +6,11 @@ # ------------------------------------------------------------------------------------- # !!! USE CAUTION WHEN MODIFYING LINES BELOW -# Accepts a desired Nautobot version as build argument, default to 1.5.3 +# Accepts a desired Nautobot version as build argument, default to 2.0.0 ARG NAUTOBOT_VER="2.0.0-rc.2" -# Accepts a desired Python version as build argument, default to 3.7 -ARG PYTHON_VER="3.10" +# Accepts a desired Python version as build argument, default to 3.11 +ARG PYTHON_VER="3.11" # Retrieve published development image of Nautobot base which should include most CI dependencies FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} diff --git a/development/docker-compose.dev.yml b/development/docker-compose.dev.yml index 5987d5fb..b95ecb81 100644 --- a/development/docker-compose.dev.yml +++ b/development/docker-compose.dev.yml @@ -12,6 +12,13 @@ services: volumes: - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - "../:/source" + # Helper method to mount on top of the python implementations, assuming you are using py3.11 and + # have all of your projects in the same directory. Uncomment out as required. + # - "../../netutils/netutils:/opt/nautobot/lib/python3.11/site-packages/netutils" + # - "../../nornir-nautobot/nornir_nautobot:/opt/nautobot/lib/python3.11/site-packages/nornir_nautobot" + # - "../../nautobot-plugin-nornir/nautobot_plugin_nornir:/opt/nautobot/lib/python3.11/site-packages/nautobot_plugin_nornir" + # - "../../nautobot/nautobot:/opt/nautobot/lib/python3.11/site-packages/nautobot" + docs: entrypoint: "mkdocs serve -v -a 0.0.0.0:8080" ports: @@ -26,6 +33,12 @@ services: volumes: - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - "../:/source" + # Helper method to mount on top of the python implementations, assuming you are using py3.11 and + # have all of your projects in the same directory. Uncomment out as required. + # - "../../netutils/netutils:/opt/nautobot/lib/python3.11/site-packages/netutils" + # - "../../nornir-nautobot/nornir_nautobot:/opt/nautobot/lib/python3.11/site-packages/nornir_nautobot" + # - "../../nautobot-plugin-nornir/nautobot_plugin_nornir:/opt/nautobot/lib/python3.11/site-packages/nautobot_plugin_nornir" + # - "../../nautobot/nautobot:/opt/nautobot/lib/python3.11/site-packages/nautobot" # To expose postgres or redis to the host uncomment the following # postgres: # ports: diff --git a/docs/admin/release_notes/version_1.4.md b/docs/admin/release_notes/version_1.4.md index 06251351..d03fc888 100755 --- a/docs/admin/release_notes/version_1.4.md +++ b/docs/admin/release_notes/version_1.4.md @@ -6,55 +6,55 @@ ### Changed -- [519](https://github.com/nautobot/nautobot-plugin-golden-config/pull/519) - docs-only: large fixes and template troubleshooting section. +- [#519](https://github.com/nautobot/nautobot-plugin-golden-config/pull/519) - docs-only: large fixes and template troubleshooting section. ### Fixed -- [492](https://github.com/nautobot/nautobot-plugin-golden-config/pull/492) - Fix count of in scope devices on settings detail view. -- [498](https://github.com/nautobot/nautobot-plugin-golden-config/pull/498) - Fix deepdiff dependency. -- [501](https://github.com/nautobot/nautobot-plugin-golden-config/pull/501) - Update docs for adding CustomField data with datasources. -- [503](https://github.com/nautobot/nautobot-plugin-golden-config/pull/503) - Switch from deprecated FilterSet to new FilterSetMixin. -- [504](https://github.com/nautobot/nautobot-plugin-golden-config/pull/504) - Fix extend queryfilter to export. -- [511](https://github.com/nautobot/nautobot-plugin-golden-config/pull/511) - Fix `log_failure` function missing argument. -- [523](https://github.com/nautobot/nautobot-plugin-golden-config/pull/523) - Fix docs site by pinning dev dependencies. -- [530](https://github.com/nautobot/nautobot-plugin-golden-config/pull/530) - Fix, removing ConfigCompliance model import from 0005 migration. +- [#492](https://github.com/nautobot/nautobot-plugin-golden-config/pull/492) - Fix count of in scope devices on settings detail view. +- [#498](https://github.com/nautobot/nautobot-plugin-golden-config/pull/498) - Fix deepdiff dependency. +- [#501](https://github.com/nautobot/nautobot-plugin-golden-config/pull/501) - Update docs for adding CustomField data with datasources. +- [#503](https://github.com/nautobot/nautobot-plugin-golden-config/pull/503) - Switch from deprecated FilterSet to new FilterSetMixin. +- [#504](https://github.com/nautobot/nautobot-plugin-golden-config/pull/504) - Fix extend queryfilter to export. +- [#511](https://github.com/nautobot/nautobot-plugin-golden-config/pull/511) - Fix `log_failure` function missing argument. +- [#523](https://github.com/nautobot/nautobot-plugin-golden-config/pull/523) - Fix docs site by pinning dev dependencies. +- [#530](https://github.com/nautobot/nautobot-plugin-golden-config/pull/530) - Fix, removing ConfigCompliance model import from 0005 migration. ## v1.4.1 - 2023-05 ### Fixed -- [488](https://github.com/nautobot/nautobot-plugin-golden-config/pull/488) - Fix Golden Config Settings Buttons. +- [#488](https://github.com/nautobot/nautobot-plugin-golden-config/pull/488) - Fix Golden Config Settings Buttons. ## v1.4.0 - 2023-05 ### Added -- [445](https://github.com/nautobot/nautobot-plugin-golden-config/pull/445) - Add validation for Settings sot_agg_query. -- [449](https://github.com/nautobot/nautobot-plugin-golden-config/pull/449) - Allows for custom kwargs to `get_secret_by_secret_group_slug`. -- [470](https://github.com/nautobot/nautobot-plugin-golden-config/pull/470) - Enhance UI settings detail object view. -- [473](https://github.com/nautobot/nautobot-plugin-golden-config/pull/473) - Add status selection field to job filtering. -- [480](https://github.com/nautobot/nautobot-plugin-golden-config/pull/480) - Add compliance summary to default tenant view. +- [#445](https://github.com/nautobot/nautobot-plugin-golden-config/pull/445) - Add validation for Settings sot_agg_query. +- [#449](https://github.com/nautobot/nautobot-plugin-golden-config/pull/449) - Allows for custom kwargs to `get_secret_by_secret_group_slug`. +- [#470](https://github.com/nautobot/nautobot-plugin-golden-config/pull/470) - Enhance UI settings detail object view. +- [#473](https://github.com/nautobot/nautobot-plugin-golden-config/pull/473) - Add status selection field to job filtering. +- [#480](https://github.com/nautobot/nautobot-plugin-golden-config/pull/480) - Add compliance summary to default tenant view. ### Changed -- [414](https://github.com/nautobot/nautobot-plugin-golden-config/pull/414) - Update application description for UI. -- [407](https://github.com/nautobot/nautobot-plugin-golden-config/pull/407) - Update branching policy in contributing docs. -- [417](https://github.com/nautobot/nautobot-plugin-golden-config/pull/417) - Changed extends base.html to extends generic/object_detail.html. -- [434](https://github.com/nautobot/nautobot-plugin-golden-config/pull/434) - Upgrade deepdiff dependency to 6.2.0. -- [451](https://github.com/nautobot/nautobot-plugin-golden-config/pull/451) - Tune Dependabot. -- [459](https://github.com/nautobot/nautobot-plugin-golden-config/pull/459) - Update tasks.py to meet current standards. -- [464](https://github.com/nautobot/nautobot-plugin-golden-config/pull/464) - Update ordering on compliance views. -- [471](https://github.com/nautobot/nautobot-plugin-golden-config/pull/471) - Migrate to using NautobotUIViewset and other initial 2.x prep work. -- [481](https://github.com/nautobot/nautobot-plugin-golden-config/pull/481) - Update filtersets for rack-group to extend proper TreeNode parent. +- [#414](https://github.com/nautobot/nautobot-plugin-golden-config/pull/414) - Update application description for UI. +- [#407](https://github.com/nautobot/nautobot-plugin-golden-config/pull/407) - Update branching policy in contributing docs. +- [#417](https://github.com/nautobot/nautobot-plugin-golden-config/pull/417) - Changed extends base.html to extends generic/object_detail.html. +- [#434](https://github.com/nautobot/nautobot-plugin-golden-config/pull/434) - Upgrade deepdiff dependency to 6.2.0. +- [#451](https://github.com/nautobot/nautobot-plugin-golden-config/pull/451) - Tune Dependabot. +- [#459](https://github.com/nautobot/nautobot-plugin-golden-config/pull/459) - Update tasks.py to meet current standards. +- [#464](https://github.com/nautobot/nautobot-plugin-golden-config/pull/464) - Update ordering on compliance views. +- [#471](https://github.com/nautobot/nautobot-plugin-golden-config/pull/471) - Migrate to using NautobotUIViewset and other initial 2.x prep work. +- [#481](https://github.com/nautobot/nautobot-plugin-golden-config/pull/481) - Update filtersets for rack-group to extend proper TreeNode parent. ### Fixed -- [436](https://github.com/nautobot/nautobot-plugin-golden-config/pull/436) - Update FAQ for how compliance works. -- [444](https://github.com/nautobot/nautobot-plugin-golden-config/pull/444) - `app_faq.md` references incorrect `Cisco IOS XR` platform slug. -- [446](https://github.com/nautobot/nautobot-plugin-golden-config/pull/446) - Fix mysql not working in github actions. -- [450](https://github.com/nautobot/nautobot-plugin-golden-config/pull/450) - Make ConfigReplace export match import. -- [456](https://github.com/nautobot/nautobot-plugin-golden-config/pull/456) - Fix postprocessing to use Sandbox Jinja2 environment. -- [461](https://github.com/nautobot/nautobot-plugin-golden-config/pull/461) - Moves dependabot config to proper location. -- [463](https://github.com/nautobot/nautobot-plugin-golden-config/pull/463) - Fix Json render in compliance reporting template. -- [468](https://github.com/nautobot/nautobot-plugin-golden-config/pull/468) - Fix GoldenConfig list view and csv export. -- [474](https://github.com/nautobot/nautobot-plugin-golden-config/pull/474) - Docs update: Fix multiple typos. +- [#436](https://github.com/nautobot/nautobot-plugin-golden-config/pull/436) - Update FAQ for how compliance works. +- [#444](https://github.com/nautobot/nautobot-plugin-golden-config/pull/444) - `app_faq.md` references incorrect `Cisco IOS XR` platform slug. +- [#446](https://github.com/nautobot/nautobot-plugin-golden-config/pull/446) - Fix mysql not working in github actions. +- [#450](https://github.com/nautobot/nautobot-plugin-golden-config/pull/450) - Make ConfigReplace export match import. +- [#456](https://github.com/nautobot/nautobot-plugin-golden-config/pull/456) - Fix postprocessing to use Sandbox Jinja2 environment. +- [#461](https://github.com/nautobot/nautobot-plugin-golden-config/pull/461) - Moves dependabot config to proper location. +- [#463](https://github.com/nautobot/nautobot-plugin-golden-config/pull/463) - Fix Json render in compliance reporting template. +- [#468](https://github.com/nautobot/nautobot-plugin-golden-config/pull/468) - Fix GoldenConfig list view and csv export. +- [#474](https://github.com/nautobot/nautobot-plugin-golden-config/pull/474) - Docs update: Fix multiple typos. diff --git a/docs/admin/release_notes/version_1.5.md b/docs/admin/release_notes/version_1.5.md index 7d4be037..46e0d40f 100755 --- a/docs/admin/release_notes/version_1.5.md +++ b/docs/admin/release_notes/version_1.5.md @@ -11,17 +11,17 @@ ### Added -- [455](https://github.com/nautobot/nautobot-plugin-golden-config/pull/455) - Add metrics for Golden Config plugin. -- [485](https://github.com/nautobot/nautobot-plugin-golden-config/pull/485) - Custom compliance for CLI and JSON rules. -- [487](https://github.com/nautobot/nautobot-plugin-golden-config/pull/487) - Implement native JSON support. -- [527](https://github.com/nautobot/nautobot-plugin-golden-config/pull/527) - Add the ability to update Jinja environment setting from nautobot_config. -- [558](https://github.com/nautobot/nautobot-plugin-golden-config/pull/558) - Updated Filters for various models, including adding an experimental `_isnull` on DateTime objects. +- [#455](https://github.com/nautobot/nautobot-plugin-golden-config/pull/455) - Add metrics for Golden Config plugin. +- [#485](https://github.com/nautobot/nautobot-plugin-golden-config/pull/485) - Custom compliance for CLI and JSON rules. +- [#487](https://github.com/nautobot/nautobot-plugin-golden-config/pull/487) - Implement native JSON support. +- [#527](https://github.com/nautobot/nautobot-plugin-golden-config/pull/527) - Add the ability to update Jinja environment setting from nautobot_config. +- [#558](https://github.com/nautobot/nautobot-plugin-golden-config/pull/558) - Updated Filters for various models, including adding an experimental `_isnull` on DateTime objects. ### Changed -- [485](https://github.com/nautobot/nautobot-plugin-golden-config/pull/485) - Changed the behavior of custom compliance to a boolean vs toggle between cli, json, and custom. +- [#485](https://github.com/nautobot/nautobot-plugin-golden-config/pull/485) - Changed the behavior of custom compliance to a boolean vs toggle between cli, json, and custom. ### Fixed -- [505](https://github.com/nautobot/nautobot-plugin-golden-config/pull/505) - fixes imports and choice definitions in the compliance nornir play. -- [513](https://github.com/nautobot/nautobot-plugin-golden-config/pull/513) - Fixed issue with native JSON support with `get_config_element` function. +- [#505](https://github.com/nautobot/nautobot-plugin-golden-config/pull/505) - fixes imports and choice definitions in the compliance nornir play. +- [#513](https://github.com/nautobot/nautobot-plugin-golden-config/pull/513) - Fixed issue with native JSON support with `get_config_element` function. diff --git a/docs/dev/dev_environment.md b/docs/dev/dev_environment.md index e2c8a06a..1d55d564 100644 --- a/docs/dev/dev_environment.md +++ b/docs/dev/dev_environment.md @@ -15,7 +15,7 @@ The [Invoke](http://www.pyinvoke.org/) library is used to provide some helper co - `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: latest) - `project_name`: the default docker compose project name (default: `nautobot_golden_config`) -- `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.8) +- `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.11) - `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) - `compose_dir`: the full path to a directory containing the project compose files - `compose_files`: a list of compose files applied in order (see [Multiple Compose files](https://docs.docker.com/compose/extends/#multiple-compose-files) for more information) @@ -187,7 +187,7 @@ The first thing you need to do is build the necessary Docker image for Nautobot #14 exporting layers #14 exporting layers 1.2s done #14 writing image sha256:2d524bc1665327faa0d34001b0a9d2ccf450612bf8feeb969312e96a2d3e3503 done -#14 naming to docker.io/nautobot-golden-config/nautobot:latest-py3.7 done +#14 naming to docker.io/nautobot-golden-config/nautobot:latest-py3.11 done ``` ### Invoke - Starting the Development Environment @@ -218,9 +218,9 @@ This will start all of the Docker containers used for hosting Nautobot. You shou ```bash ➜ docker ps ****CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -ee90fbfabd77 nautobot-golden-config/nautobot:latest-py3.7 "nautobot-server rqw…" 16 seconds ago Up 13 seconds nautobot_golden_config_worker_1 -b8adb781d013 nautobot-golden-config/nautobot:latest-py3.7 "/docker-entrypoint.…" 20 seconds ago Up 15 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp nautobot_golden_config_nautobot_1 -d64ebd60675d nautobot-golden-config/nautobot:latest-py3.7 "mkdocs serve -v -a …" 25 seconds ago Up 18 seconds 0.0.0.0:8001->8080/tcp, :::8001->8080/tcp nautobot_golden_config_docs_1 +ee90fbfabd77 nautobot-golden-config/nautobot:latest-py3.11 "nautobot-server rqw…" 16 seconds ago Up 13 seconds nautobot_golden_config_worker_1 +b8adb781d013 nautobot-golden-config/nautobot:latest-py3.11 "/docker-entrypoint.…" 20 seconds ago Up 15 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp nautobot_golden_config_nautobot_1 +d64ebd60675d nautobot-golden-config/nautobot:latest-py3.11 "mkdocs serve -v -a …" 25 seconds ago Up 18 seconds 0.0.0.0:8001->8080/tcp, :::8001->8080/tcp nautobot_golden_config_docs_1 e72d63129b36 postgres:13-alpine "docker-entrypoint.s…" 25 seconds ago Up 19 seconds 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp nautobot_golden_config_postgres_1 96c6ff66997c redis:6-alpine "docker-entrypoint.s…" 25 seconds ago Up 21 seconds 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp nautobot_golden_config_redis_1 ``` @@ -399,7 +399,7 @@ namespace.configure( { "nautobot_golden_config": { ... - "python_ver": "3.7", + "python_ver": "3.11", ... } } diff --git a/docs/user/troubleshooting/troubleshoot_dispatchers.md b/docs/user/troubleshooting/troubleshoot_dispatchers.md index 2bc03fda..ed044d5b 100755 --- a/docs/user/troubleshooting/troubleshoot_dispatchers.md +++ b/docs/user/troubleshooting/troubleshoot_dispatchers.md @@ -14,6 +14,8 @@ This occurs when a Golden Config job is executed with a Nautobot `platform`, and How is the dispatcher loaded? +TODO: 2.0: Change to custom_dispatcher + 1. Job initializes Nornir and the method is called with `get_dispatcher()` function from Nautobot-Plugin-Nornir. 2. Nornir initialization looks in the DEFAULT_DISPATCHER map for the platform network_driver from [nornir-nautobot](https://github.com/nautobot/nornir-nautobot/blob/64baa8a24d21d9ec14c32be569e2b51cd0bd1cd1/nornir_nautobot/plugins/tasks/dispatcher/__init__.py#L12) mapping. 3. Merge this mapping with anything directly configured in Golden Config [dispatcher mapping](). diff --git a/nautobot_golden_config/api/serializers.py b/nautobot_golden_config/api/serializers.py index 5de9b7b2..a21fcc3d 100644 --- a/nautobot_golden_config/api/serializers.py +++ b/nautobot_golden_config/api/serializers.py @@ -2,16 +2,10 @@ # pylint: disable=too-many-ancestors from rest_framework import serializers -from nautobot.apps.api import WritableNestedSerializer -from nautobot.extras.api.fields import StatusSerializerField -from nautobot.extras.api.serializers import TaggedObjectSerializer -from nautobot.extras.api.nested_serializers import NestedDynamicGroupSerializer from nautobot.extras.api.mixins import TaggedModelSerializerMixin -from nautobot.extras.models import Status -from nautobot.dcim.api.nested_serializers import NestedDeviceSerializer from nautobot.dcim.api.serializers import DeviceSerializer from nautobot.dcim.models import Device -from nautobot.extras.api.serializers import NautobotModelSerializer, StatusModelSerializerMixin +from nautobot.core.api.serializers import NautobotModelSerializer from nautobot_golden_config import models @@ -78,7 +72,7 @@ class GoldenConfigSettingSerializer(NautobotModelSerializer, TaggedModelSerializ url = serializers.HyperlinkedIdentityField( view_name="plugins-api:nautobot_golden_config-api:goldenconfigsetting-detail" ) - # TODO: What is correct for this with the removal of nested serializers? + # TODO: 2.0: What is correct for this with the removal of nested serializers? # dynamic_group = NestedDynamicGroupSerializer(required=False) class Meta: @@ -120,7 +114,7 @@ class ConfigToPushSerializer(DeviceSerializer): class Meta(DeviceSerializer): """Extend the Device serializer with the configuration after postprocessing.""" - # TODO: Fix fields to work with Device moving to a string "__all__" + # TODO: 2.0: Fix fields to work with Device moving to a string "__all__" # fields = DeviceSerializer.Meta.fields + ["config"] fields = "__all__" model = Device @@ -133,7 +127,7 @@ def get_config(self, obj): return get_config_postprocessing(config_details, request) -class RemediationSettingSerializer(NautobotModelSerializer, TaggedObjectSerializer): +class RemediationSettingSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): """Serializer for RemediationSetting object.""" url = serializers.HyperlinkedIdentityField( @@ -148,12 +142,10 @@ class Meta: fields = "__all__" -class ConfigPlanSerializer(NautobotModelSerializer, TaggedObjectSerializer, StatusModelSerializerMixin): +class ConfigPlanSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): """Serializer for ConfigPlan object.""" url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_golden_config-api:configplan-detail") - device = NestedDeviceSerializer(required=False) - status = StatusSerializerField(required=False, queryset=Status.objects.all()) class Meta: """Set Meta Data for ConfigPlan, will serialize all fields.""" @@ -161,15 +153,3 @@ class Meta: model = models.ConfigPlan fields = "__all__" read_only_fields = ["device", "plan_type", "feature", "config_set"] - - -class NestedConfigPlanSerializer(WritableNestedSerializer): - """Nested serializer for ConfigPlan object.""" - - url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_golden_config-api:configplan-detail") - - class Meta: - """Set Meta Data for ConfigPlan, will serialize brief fields.""" - - model = models.ConfigPlan - fields = ["id", "url", "device", "plan_type"] diff --git a/nautobot_golden_config/filters.py b/nautobot_golden_config/filters.py index 95af7527..1d69c3d1 100644 --- a/nautobot_golden_config/filters.py +++ b/nautobot_golden_config/filters.py @@ -3,24 +3,17 @@ import django_filters from django.db.models import Q -from nautobot.core.filters import ( - BaseFilterSet, - MultiValueDateTimeFilter, - NameSlugSearchFilterSet, - TagFilter, - TreeNodeMultipleChoiceFilter, -) +from nautobot.core.filters import BaseFilterSet, MultiValueDateTimeFilter, TagFilter, TreeNodeMultipleChoiceFilter from nautobot.dcim.models import Device, DeviceType, Manufacturer, Platform, Rack, RackGroup, Location from nautobot.dcim.filters import DeviceFilterSet -from nautobot.extras.filters import NaturalKeyOrPKMultipleChoiceFilter, StatusFilter -from nautobot.extras.filters import NautobotFilterSet -from nautobot.extras.models import Status, Role +from nautobot.extras.filters import NaturalKeyOrPKMultipleChoiceFilter, NautobotFilterSet, StatusFilter +from nautobot.extras.models import JobResult, Role, Status from nautobot.tenancy.models import Tenant, TenantGroup from nautobot_golden_config import models -# TODO: DeviceFilterSet has bugs in regards to Location in 2.0.0-rc.2 +# TODO: 2.0: DeviceFilterSet has bugs in regards to Location in 2.0.0-rc.2 class GoldenConfigDeviceFilterSet(DeviceFilterSet): # pylint: disable=too-many-ancestors """Filter capabilities that extend the standard DeviceFilterSet.""" @@ -119,7 +112,7 @@ def _get_filter_lookup_dict(existing_filter): ) role = NaturalKeyOrPKMultipleChoiceFilter( field_name="device__role", - queryset=Role.objects.all(), # TODO: How does change to Role model affect this? + queryset=Role.objects.all(), # TODO: 2.0: How does change to Role model affect this? to_field_name="name", label="Role (name or ID)", ) @@ -318,7 +311,7 @@ class Meta: fields = ["id", "name", "slug", "weight", "backup_repository", "intended_repository", "jinja_repository"] -class RemediationSettingFilterSet(BaseFilterSet, NameSlugSearchFilterSet): +class RemediationSettingFilterSet(BaseFilterSet): """Inherits Base Class CustomFieldModelFilterSet.""" q = django_filters.CharFilter( @@ -356,7 +349,7 @@ class Meta: fields = ["id", "platform", "remediation_type"] -class ConfigPlanFilterSet(BaseFilterSet, NameSlugSearchFilterSet): +class ConfigPlanFilterSet(BaseFilterSet): """Inherits Base Class BaseFilterSet.""" q = django_filters.CharFilter( diff --git a/nautobot_golden_config/forms.py b/nautobot_golden_config/forms.py index ae71a4d7..9d2db033 100644 --- a/nautobot_golden_config/forms.py +++ b/nautobot_golden_config/forms.py @@ -8,9 +8,10 @@ import nautobot.core.forms as core_forms from nautobot.dcim.models import Device, Platform, Location, DeviceType, Manufacturer, Rack, RackGroup from nautobot.extras.forms import NautobotFilterForm, NautobotBulkEditForm, NautobotModelForm -from nautobot.extras.models import Status, GitRepository, DynamicGroup, Role +from nautobot.extras.models import DynamicGroup, GitRepository, JobResult, Role, Status, Tag from nautobot.tenancy.models import Tenant, TenantGroup -import nautobot.utilities.forms as utilities_forms + +# import nautobot.utilities.forms as core_forms from nautobot_golden_config import models from nautobot_golden_config.choices import ComplianceRuleConfigTypeChoice, ConfigPlanTypeChoice, RemediationTypeChoice @@ -96,7 +97,7 @@ class ConfigComplianceFilterForm(NautobotFilterForm): role = core_forms.DynamicModelMultipleChoiceField( queryset=Role.objects.all(), to_field_name="name", - required=False, # TODO: Test with change to Role model + required=False, # TODO: 2.0: Test with change to Role model ) manufacturer = core_forms.DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), to_field_name="name", required=False, label="Manufacturer" @@ -176,11 +177,11 @@ class ComplianceRuleBulkEditForm(NautobotBulkEditForm): description = forms.CharField(max_length=200, required=False) config_type = forms.ChoiceField( required=False, - choices=utilities_forms.add_blank_choice(ComplianceRuleConfigTypeChoice), + choices=core_forms.add_blank_choice(ComplianceRuleConfigTypeChoice), ) - config_ordered = forms.NullBooleanField(required=False, widget=utilities_forms.BulkEditNullBooleanSelect()) - custom_compliance = forms.NullBooleanField(required=False, widget=utilities_forms.BulkEditNullBooleanSelect()) - config_remediation = forms.NullBooleanField(required=False, widget=utilities_forms.BulkEditNullBooleanSelect()) + config_ordered = forms.NullBooleanField(required=False, widget=core_forms.BulkEditNullBooleanSelect()) + custom_compliance = forms.NullBooleanField(required=False, widget=core_forms.BulkEditNullBooleanSelect()) + config_remediation = forms.NullBooleanField(required=False, widget=core_forms.BulkEditNullBooleanSelect()) class Meta: """Boilerplate form Meta data for ComplianceRule.""" @@ -194,7 +195,7 @@ class Meta: class ComplianceFeatureForm(NautobotModelForm): """Filter Form for ComplianceFeature instances.""" - slug = core_forms.fields.SlugField() # TODO: Remove slugs + slug = core_forms.fields.SlugField() # TODO: 2.1: Change from slugs once django-pivot is figured out class Meta: """Boilerplate form Meta data for compliance feature.""" @@ -321,8 +322,8 @@ class Meta: class GoldenConfigSettingForm(NautobotModelForm): """Filter Form for GoldenConfigSettingForm instances.""" - slug = core_forms.fields.SlugField() # TODO: Remove slugs - dynamic_group = core_forms.DynamicModelChoiceField(queryset=DynamicGroup.objects.all(), required=False) + slug = core_forms.fields.SlugField() # TODO: 2.1: Remove slugs + dynamic_group = core_forms.DynamicModelChoiceField(queryset=DynamicGroup.objects.all()) class Meta: """Filter Form Meta Data for GoldenConfigSettingForm instances.""" @@ -401,24 +402,14 @@ class RemediationSettingFilterForm(NautobotFilterForm): model = models.RemediationSetting q = forms.CharField(required=False, label="Search") - platform = utilities_forms.DynamicModelMultipleChoiceField( + platform = core_forms.DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), required=False, display_field="name", to_field_name="name" ) remediation_type = forms.ChoiceField( - choices=add_blank_choice(RemediationTypeChoice), required=False, label="Remediation Type" + choices=core_forms.add_blank_choice(RemediationTypeChoice), required=False, label="Remediation Type" ) -class RemediationSettingCSVForm(extras_forms.CustomFieldModelCSVForm): - """CSV Form for RemediationSetting instances.""" - - class Meta: - """Boilerplate form Meta data for RemediationSetting.""" - - model = models.RemediationSetting - fields = models.RemediationSetting.csv_headers - - class RemediationSettingBulkEditForm(NautobotBulkEditForm): """BulkEdit form for RemediationSetting instances.""" @@ -439,14 +430,15 @@ class Meta: class ConfigPlanForm(NautobotModelForm): """Form for ConfigPlan instances.""" - plan_type = forms.ChoiceField(choices=add_blank_choice(ConfigPlanTypeChoice), required=True, label="Plan Type") - change_control_id = forms.CharField(required=False, label="Change Control ID") - change_control_url = forms.URLField(required=False, label="Change Control URL") + plan_type = forms.ChoiceField( + choices=core_forms.add_blank_choice(ConfigPlanTypeChoice), required=True, label="Plan Type" + ) + change_control_id = forms.CharField(label="Change Control ID") + change_control_url = forms.URLField(label="Change Control URL") - feature = utilities_forms.DynamicModelMultipleChoiceField( + feature = core_forms.DynamicModelMultipleChoiceField( queryset=models.ComplianceFeature.objects.all(), display_field="name", - required=False, help_text="Note: Selecting no features will generate plans for all applicable features.", ) commands = forms.CharField( @@ -457,27 +449,26 @@ class ConfigPlanForm(NautobotModelForm): "You can also reference the device object with obj.
" "For example: hostname {{ obj.name }} or ip address {{ obj.primary_ip4.host }}" ), - required=True, ) - tenant_group = utilities_forms.DynamicModelMultipleChoiceField(queryset=TenantGroup.objects.all(), required=False) - tenant = utilities_forms.DynamicModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False) + tenant_group = core_forms.DynamicModelMultipleChoiceField(queryset=TenantGroup.objects.all()) + tenant = core_forms.DynamicModelMultipleChoiceField(queryset=Tenant.objects.all()) # Requires https://github.com/nautobot/nautobot-plugin-golden-config/issues/430 - # location = utilities_forms.DynamicModelMultipleChoiceField(queryset=Location.objects.all(), required=False) - region = utilities_forms.DynamicModelMultipleChoiceField(queryset=Region.objects.all(), required=False) - site = utilities_forms.DynamicModelMultipleChoiceField(queryset=Site.objects.all(), required=False) - rack_group = utilities_forms.DynamicModelMultipleChoiceField(queryset=RackGroup.objects.all(), required=False) - rack = utilities_forms.DynamicModelMultipleChoiceField(queryset=Rack.objects.all(), required=False) - role = utilities_forms.DynamicModelMultipleChoiceField(queryset=DeviceRole.objects.all(), required=False) - manufacturer = utilities_forms.DynamicModelMultipleChoiceField(queryset=Manufacturer.objects.all(), required=False) - platform = utilities_forms.DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), required=False) - device_type = utilities_forms.DynamicModelMultipleChoiceField(queryset=DeviceType.objects.all(), required=False) - device = utilities_forms.DynamicModelMultipleChoiceField(queryset=Device.objects.all(), required=False) - tag = utilities_forms.DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), query_params={"content_types": "dcim.device"}, required=False - ) - status = utilities_forms.DynamicModelMultipleChoiceField( - queryset=Status.objects.all(), query_params={"content_types": "dcim.device"}, required=False + location = core_forms.DynamicModelMultipleChoiceField(queryset=Location.objects.all()) + # region = core_forms.DynamicModelMultipleChoiceField(queryset=Region.objects.all()) + # site = core_forms.DynamicModelMultipleChoiceField(queryset=Site.objects.all()) + rack_group = core_forms.DynamicModelMultipleChoiceField(queryset=RackGroup.objects.all()) + rack = core_forms.DynamicModelMultipleChoiceField(queryset=Rack.objects.all()) + role = core_forms.DynamicModelMultipleChoiceField(queryset=Role.objects.all()) + manufacturer = core_forms.DynamicModelMultipleChoiceField(queryset=Manufacturer.objects.all()) + platform = core_forms.DynamicModelMultipleChoiceField(queryset=Platform.objects.all()) + device_type = core_forms.DynamicModelMultipleChoiceField(queryset=DeviceType.objects.all()) + device = core_forms.DynamicModelMultipleChoiceField(queryset=Device.objects.all()) + tag = core_forms.DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), query_params={"content_types": "dcim.device"} + ) + status = core_forms.DynamicModelMultipleChoiceField( + queryset=Status.objects.all(), query_params={"content_types": "dcim.device"} ) def __init__(self, *args, **kwargs): @@ -508,9 +499,7 @@ class Meta: "feature", "commands", "tenant", - # "location", Requires https://github.com/nautobot/nautobot-plugin-golden-config/issues/430 - "region", - "site", + "location", # Requires https://github.com/nautobot/nautobot-plugin-golden-config/issues/430 "rack_group", "rack", "role", @@ -526,15 +515,14 @@ class Meta: class ConfigPlanUpdateForm(NautobotModelForm): """Form for ConfigPlan instances.""" - change_control_id = forms.CharField(required=False, label="Change Control ID") - change_control_url = forms.URLField(required=False, label="Change Control URL") - status = utilities_forms.DynamicModelChoiceField( + change_control_id = forms.CharField(label="Change Control ID") + change_control_url = forms.URLField(label="Change Control URL") + status = core_forms.DynamicModelChoiceField( queryset=Status.objects.all(), query_params={"content_types": models.ConfigPlan._meta.label_lower}, - required=False, ) - tag = utilities_forms.DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), query_params={"content_types": "dcim.device"}, required=False + tag = core_forms.DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), query_params={"content_types": "dcim.device"} ) class Meta: @@ -555,15 +543,18 @@ class ConfigPlanFilterForm(NautobotFilterForm): model = models.ConfigPlan q = forms.CharField(required=False, label="Search") - device_id = utilities_forms.DynamicModelMultipleChoiceField( + device_id = core_forms.DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, null_option="None", label="Device" ) - created__lte = forms.DateTimeField(label="Created Before", required=False, widget=utilities_forms.DatePicker()) - created__gte = forms.DateTimeField(label="Created After", required=False, widget=utilities_forms.DatePicker()) + created__lte = forms.DateTimeField(label="Created Before", required=False, widget=core_forms.DatePicker()) + created__gte = forms.DateTimeField(label="Created After", required=False, widget=core_forms.DatePicker()) plan_type = forms.ChoiceField( - choices=utilities_forms.add_blank_choice(ConfigPlanTypeChoice), required=False, widget=forms.Select(), label="Plan Type" + choices=core_forms.add_blank_choice(ConfigPlanTypeChoice), + required=False, + widget=forms.Select(), + label="Plan Type", ) - feature = utilities_forms.DynamicModelMultipleChoiceField( + feature = core_forms.DynamicModelMultipleChoiceField( queryset=models.ComplianceFeature.objects.all(), required=False, null_option="None", @@ -571,14 +562,14 @@ class ConfigPlanFilterForm(NautobotFilterForm): to_field_name="name", ) change_control_id = forms.CharField(required=False, label="Change Control ID") - job_result_id = utilities_forms.DynamicModelMultipleChoiceField( + job_result_id = core_forms.DynamicModelMultipleChoiceField( queryset=JobResult.objects.all(), query_params={"nautobot_golden_config_config_plan_null": True}, label="Job Result", required=False, display_field="id", ) - status = utilities_forms.DynamicModelMultipleChoiceField( + status = core_forms.DynamicModelMultipleChoiceField( required=False, queryset=Status.objects.all(), query_params={"content_types": models.ConfigPlan._meta.label_lower}, @@ -586,14 +577,14 @@ class ConfigPlanFilterForm(NautobotFilterForm): label="Status", to_field_name="name", ) - tag = utilities_forms.TagFilterField(model) + tag = core_forms.TagFilterField(model) -class ConfigPlanBulkEditForm(core_forms.TagsBulkEditFormMixin, NautobotBulkEditForm): +class ConfigPlanBulkEditForm(NautobotBulkEditForm): """BulkEdit form for ConfigPlan instances.""" pk = forms.ModelMultipleChoiceField(queryset=models.ConfigPlan.objects.all(), widget=forms.MultipleHiddenInput) - status = utilities_forms.DynamicModelChoiceField( + status = core_forms.DynamicModelChoiceField( queryset=Status.objects.all(), query_params={"content_types": models.ConfigPlan._meta.label_lower}, required=False, diff --git a/nautobot_golden_config/jobs.py b/nautobot_golden_config/jobs.py index 6d2578c2..09afa3c3 100644 --- a/nautobot_golden_config/jobs.py +++ b/nautobot_golden_config/jobs.py @@ -1,5 +1,5 @@ """Jobs to run backups, intended config, and compliance.""" -# pylint: disable=too-many-function-args +# pylint: disable=too-many-function-args,logging-fstring-interpolation from datetime import datetime @@ -63,20 +63,7 @@ def get_refreshed_repos(job_obj, repo_type, data=None): return repositories -def commit_check(method): - """Decorator to check if a "dry-run" attempt was made.""" - - def inner(obj, data, commit): - """Decorator bolierplate code.""" - msg = "Dry-run mode is not supported, please set the commit flag to proceed." - if not commit: - raise ValueError(msg) - return method(obj, data, commit) - - return inner - - -# TODO: Does changing region/site to location affect nornir jobs? +# TODO: 2.0: Does changing region/site to location affect nornir jobs? class FormEntry: # pylint disable=too-few-public-method @@ -87,7 +74,7 @@ class FormEntry: # pylint disable=too-few-public-method location = MultiObjectVar(model=Location, required=False) rack_group = MultiObjectVar(model=RackGroup, required=False) rack = MultiObjectVar(model=Rack, required=False) - role = MultiObjectVar(model=Role, required=False) # TODO: How does change to Role model affect this? + role = MultiObjectVar(model=Role, required=False) # TODO: 2.0: How does change to Role model affect this? manufacturer = MultiObjectVar(model=Manufacturer, required=False) platform = MultiObjectVar(model=Platform, required=False) device_type = MultiObjectVar(model=DeviceType, required=False, display_field="display_name") @@ -106,8 +93,6 @@ class FormEntry: # pylint disable=too-few-public-method class ComplianceJob(Job, FormEntry): """Job to to run the compliance engine.""" - # TODO: Remove these as they are already defined via inheritence - tenant_group = FormEntry.tenant_group tenant = FormEntry.tenant location = FormEntry.location @@ -128,23 +113,17 @@ class Meta: name = "Perform Configuration Compliance" description = "Run configuration compliance on your network infrastructure." - @commit_check - # TODO: Fix pylint arguments-differ during Job 2.x migration - def run(self, data, commit): # pylint: disable=too-many-branches,arguments-differ + def run(self, **data): # pylint: disable=too-many-branches """Run config compliance report script.""" # pylint: disable=unused-argument - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Starting compliance job.") # pylint: disable=no-member + self.logger.debug("Starting compliance job.") - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Refreshing intended configuration git repository.") # pylint: disable=no-member + self.logger.debug("Refreshing intended configuration git repository.") get_refreshed_repos(job_obj=self, repo_type="intended_repository", data=data) - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Refreshing backup configuration git repository.") # pylint: disable=no-member + self.logger.debug("Refreshing backup configuration git repository.") get_refreshed_repos(job_obj=self, repo_type="backup_repository", data=data) - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Starting config compliance nornir play.") # pylint: disable=no-member + self.logger.debug("Starting config compliance nornir play.") config_compliance(self, data) @@ -171,34 +150,27 @@ class Meta: name = "Generate Intended Configurations" description = "Generate the configuration for your intended state." - @commit_check - # TODO: Fix pylint arguments-differ,unused-argument during Job 2.x migration - def run(self, data, commit): # pylint: disable=arguments-differ,unused-argument + def run(self, **data): """Run config generation script.""" - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Starting intended job.") # pylint: disable=no-member + self.logger.debug("Starting intended job.") now = datetime.now() - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Pull Jinja template repos.") # pylint: disable=no-member + self.logger.debug("Pull Jinja template repos.") get_refreshed_repos(job_obj=self, repo_type="jinja_repository", data=data) - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Pull Intended config repos.") # pylint: disable=no-member + self.logger.debug("Pull Intended config repos.") # Instantiate a GitRepo object for each GitRepository in GoldenConfigSettings. intended_repos = get_refreshed_repos(job_obj=self, repo_type="intended_repository", data=data) - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug( # pylint: disable=no-member + self.logger.debug( "Building device settings mapping and running intended config nornir play." ) config_intended(self, data) # Commit / Push each repo after job is completed. for intended_repo in intended_repos: - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug(f"Push new intended configs to repo {intended_repo.url}.") # pylint: disable=no-member + self.logger.debug(f"Push new intended configs to repo {intended_repo.url}.") intended_repo.commit_with_added(f"INTENDED CONFIG CREATION JOB - {now}") intended_repo.push() @@ -226,30 +198,23 @@ class Meta: name = "Backup Configurations" description = "Backup the configurations of your network devices." - @commit_check - # TODO: Fix pylint arguments-differ,unused-argument during Job 2.x migration - def run(self, data, commit): # pylint: disable=arguments-differ,unused-argument + def run(self, **data): """Run config backup process.""" - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Starting backup job.") # pylint: disable=no-member + self.logger.debug("Starting backup job.") now = datetime.now() - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Pull Backup config repo.") # pylint: disable=no-member + self.logger.debug("Pull Backup config repo.") # Instantiate a GitRepo object for each GitRepository in GoldenConfigSettings. backup_repos = get_refreshed_repos(job_obj=self, repo_type="backup_repository", data=data) - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug(f"Starting backup jobs to the following repos: {backup_repos}") # pylint: disable=no-member + self.logger.debug(f"Starting backup jobs to the following repos: {backup_repos}") - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug("Starting config backup nornir play.") # pylint: disable=no-member + self.logger.debug("Starting config backup nornir play.") config_backup(self, data) # Commit / Push each repo after job is completed. for backup_repo in backup_repos: - # TODO: Fix pylint no-member during Job 2.x migration - self.log_debug(f"Pushing Backup config repo {backup_repo.url}.") # pylint: disable=no-member + self.logger.debug(f"Pushing Backup config repo {backup_repo.url}.") backup_repo.commit_with_added(f"BACKUP JOB {now}") backup_repo.push() @@ -266,9 +231,7 @@ class Meta: name = "Execute All Golden Configuration Jobs - Single Device" description = "Process to run all Golden Configuration jobs configured." - @commit_check - # TODO: Fix pylint arguments-differ,unused-argument during Job 2.x migration - def run(self, data, commit): # pylint: disable=arguments-differ,unused-argument + def run(self, **data): """Run all jobs.""" if ENABLE_INTENDED: IntendedJob().run.__func__(self, data, True) # pylint: disable=too-many-function-args @@ -301,9 +264,7 @@ class Meta: name = "Execute All Golden Configuration Jobs - Multiple Device" description = "Process to run all Golden Configuration jobs configured against multiple devices." - @commit_check - # TODO: Fix pylint arguments-differ,unused-argument during Job 2.x migration - def run(self, data, commit): # pylint: disable=arguments-differ,unused-argument + def run(self, **data): """Run all jobs.""" if ENABLE_INTENDED: IntendedJob().run.__func__(self, data, True) # pylint: disable=too-many-function-args @@ -319,8 +280,7 @@ class GenerateConfigPlans(Job, FormEntry): # Device QS Filters tenant_group = FormEntry.tenant_group tenant = FormEntry.tenant - region = FormEntry.region - site = FormEntry.site + location = FormEntry.location rack_group = FormEntry.rack_group rack = FormEntry.rack role = FormEntry.role @@ -369,9 +329,9 @@ def _validate_inputs(self, data): self._feature = ComplianceFeature.objects.all() if self._plan_type in ["manual"]: if not self._commands: - self.log_failure("No commands entered for config plan generation.") - return False - return True + error_msg = "No commands entered for config plan generation." + self.logger.error(error_msg) + raise ValueError(error_msg) def _generate_config_plan_from_feature(self): """Generate config plans from features.""" @@ -387,7 +347,7 @@ def _generate_config_plan_from_feature(self): if not config_sets: _features = ", ".join([str(feat) for feat in self._feature]) - self.log_debug(f"Device `{device}` does not have `{self._plan_type}` configs for `{_features}`.") + self.logger.debug(f"Device `{device}` does not have `{self._plan_type}` configs for `{_features}`.") continue config_plan = ConfigPlan.objects.create( device=device, @@ -401,18 +361,20 @@ def _generate_config_plan_from_feature(self): config_plan.feature.set(features) config_plan.validated_save() _features = ", ".join([str(feat) for feat in features]) - self.log_success(obj=config_plan, message=f"Config plan created for `{device}` with feature `{_features}`.") + self.logger.info(obj=config_plan, message=f"Config plan created for `{device}` with feature `{_features}`.") def _generate_config_plan_from_manual(self): """Generate config plans from manual.""" default_context = { "request": self.request, - "user": self.request.user, + "user": self.user, } for device in self._device_qs: config_set = generate_config_set_from_manual(device, self._commands, context=default_context) if not config_set: - self.log_debug(f"Device {self.device} did not return a rendered config set from the provided commands.") + self.logger.debug( + f"Device {self.device} did not return a rendered config set from the provided commands." + ) continue config_plan = ConfigPlan.objects.create( device=device, @@ -423,27 +385,28 @@ def _generate_config_plan_from_manual(self): status=self._status, job_result=self.job_result, ) - self.log_success(obj=config_plan, message=f"Config plan created for {device} with manual commands.") + self.logger.info(obj=config_plan, message=f"Config plan created for {device} with manual commands.") - def run(self, data, commit): + def run(self, **data): """Run config plan generation process.""" - self.log_debug("Starting config plan generation job.") - if not self._validate_inputs(data): - return + self.logger.debug("Starting config plan generation job.") + self._validate_inputs(data) try: self._device_qs = get_job_filter(data) - except NornirNautobotException as exc: - self.log_failure(str(exc)) - return + except NornirNautobotException as error: + error_msg = str(error) + self.logger.error(error_msg) + raise NornirNautobotException(error_msg) if self._plan_type in ["intended", "missing", "remediation"]: - self.log_debug("Starting config plan generation for compliance features.") + self.logger.debug("Starting config plan generation for compliance features.") self._generate_config_plan_from_feature() elif self._plan_type in ["manual"]: - self.log_debug("Starting config plan generation for manual commands.") + self.logger.debug("Starting config plan generation for manual commands.") self._generate_config_plan_from_manual() else: - self.log_failure(f"Unknown config plan type {self._plan_type}.") - return + error_msg = f"Unknown config plan type {self._plan_type}." + self.logger.error() + raise ValueError(error_msg) class DeployConfigPlans(Job): @@ -458,13 +421,10 @@ class Meta: name = "Deploy Config Plans" description = "Deploy config plans to devices." - def run(self, data, commit): + def run(self, **data): # pylint: disable=arguments-differ """Run config plan deployment process.""" - self.log_debug("Starting config plan deployment job.") - config_deployment(self, data, commit) - if commit and not self.failed: - config_plan_qs = data["config_plan"] - config_plan_qs.delete() + self.logger.debug("Starting config plan deployment job.") + config_deployment(self, **data) class DeployConfigPlanJobButtonReceiver(JobButtonReceiver): @@ -477,11 +437,9 @@ class Meta: def receive_job_button(self, obj): """Run config plan deployment process.""" - self.log_debug("Starting config plan deployment job.") + self.logger.debug("Starting config plan deployment job.") data = {"debug": False, "config_plan": ConfigPlan.objects.filter(id=obj.id)} - config_deployment(self, data, commit=True) - if not self.failed: - obj.delete() + config_deployment(self, **data=True) # Conditionally allow jobs based on whether or not turned on. diff --git a/nautobot_golden_config/management/commands/run_config_backup.py b/nautobot_golden_config/management/commands/run_config_backup.py deleted file mode 100644 index 348c3949..00000000 --- a/nautobot_golden_config/management/commands/run_config_backup.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Add the run_config_backup command to nautobot-server.""" - -from django.core.management.base import BaseCommand -from nautobot.extras.jobs import get_job - -from nautobot_golden_config.utilities.management import job_runner - - -class Command(BaseCommand): - """Boilerplate Command to inherit from BaseCommand.""" - - help = "Run Config Backup from Golden Config Plugin." - - def add_arguments(self, parser): - """Add arguments for run_config_backup.""" - parser.add_argument("-u", "--user", type=str, required=True, help="User to run the Job as.") - parser.add_argument("-d", "--device", type=str, help="Define a uniquely defined device name") - - def handle(self, *args, **kwargs): - """Add handler for run_config_backup.""" - job_class = get_job("plugins/nautobot_golden_config.jobs/BackupJob") - job_runner(self, job_class, kwargs.get("device"), kwargs.get("user")) diff --git a/nautobot_golden_config/management/commands/run_config_compliance.py b/nautobot_golden_config/management/commands/run_config_compliance.py deleted file mode 100644 index 488ecccc..00000000 --- a/nautobot_golden_config/management/commands/run_config_compliance.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Add the run_config_compliance command to nautobot-server.""" - -from django.core.management.base import BaseCommand -from nautobot.extras.jobs import get_job - -from nautobot_golden_config.utilities.management import job_runner - - -class Command(BaseCommand): - """Boilerplate Command to inherit from BaseCommand.""" - - help = "Run Config Compliance Job from Golden Config Plugin." - - def add_arguments(self, parser): - """Add arguments for run_config_compliance.""" - parser.add_argument("-u", "--user", type=str, required=True, help="User to run the Job as.") - parser.add_argument("-d", "--device", type=str, help="Define a uniquely defined device name") - - def handle(self, *args, **kwargs): - """Add handler for run_config_compliance.""" - job_class = get_job("plugins/nautobot_golden_config.jobs/ComplianceJob") - job_runner(self, job_class, kwargs.get("device"), kwargs.get("user")) diff --git a/nautobot_golden_config/management/commands/run_generate_config.py b/nautobot_golden_config/management/commands/run_generate_config.py deleted file mode 100644 index 68c70c25..00000000 --- a/nautobot_golden_config/management/commands/run_generate_config.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Add the run_generate_config command to nautobot-server.""" - -from django.core.management.base import BaseCommand -from nautobot.extras.jobs import get_job - -from nautobot_golden_config.utilities.management import job_runner - - -class Command(BaseCommand): - """Boilerplate Command to inherit from BaseCommand.""" - - help = "Run Job to generate your intended configuration from Golden Config Plugin." - - def add_arguments(self, parser): - """Add arguments for run_generate_config.""" - parser.add_argument("-u", "--user", type=str, required=True, help="User to run the Job as.") - parser.add_argument("-d", "--device", type=str, help="Define a uniquely defined device name") - - def handle(self, *args, **kwargs): - """Add handler for run_generate_config.""" - job_class = get_job("plugins/nautobot_golden_config.jobs/IntendedJob") - job_runner(self, job_class, kwargs.get("device"), kwargs.get("user")) diff --git a/nautobot_golden_config/models.py b/nautobot_golden_config/models.py index aa41bac4..345c690f 100644 --- a/nautobot_golden_config/models.py +++ b/nautobot_golden_config/models.py @@ -7,14 +7,11 @@ from django.core.exceptions import ValidationError from django.db import models from django.utils.module_loading import import_string -from django.utils.text import slugify from hier_config import Host as HierConfigHost from nautobot.core.models.generics import PrimaryModel from nautobot.extras.models import ObjectChange -from nautobot.extras.models import DynamicGroup, ObjectChange -from nautobot.extras.models.statuses import StatusField from nautobot.extras.utils import extras_features from nautobot.core.models.utils import serialize_object, serialize_object_v2 from netutils.config.compliance import feature_compliance @@ -225,36 +222,30 @@ def __str__(self): class ComplianceRule(PrimaryModel): # pylint: disable=too-many-ancestors """ComplianceRule details.""" - feature = models.ForeignKey(to="ComplianceFeature", on_delete=models.CASCADE, blank=False, related_name="feature") + feature = models.ForeignKey(to="ComplianceFeature", on_delete=models.CASCADE, related_name="feature") platform = models.ForeignKey( to="dcim.Platform", on_delete=models.CASCADE, related_name="compliance_rules", - null=False, - blank=False, ) description = models.CharField( max_length=200, blank=True, ) config_ordered = models.BooleanField( - null=False, - blank=False, verbose_name="Configured Ordered", help_text="Whether or not the configuration order matters, such as in ACLs.", + default=False, ) config_remediation = models.BooleanField( default=False, - null=False, - blank=False, verbose_name="Config Remediation", help_text="Whether or not the config remediation is executed for this compliance rule.", ) match_config = models.TextField( - null=True, blank=True, verbose_name="Config to Match", help_text="The config to match that is matched based on the parent most configuration. E.g.: For CLI `router bgp` or `ntp`. For JSON this is a top level key name.", @@ -305,18 +296,18 @@ def clean(self): class ConfigCompliance(PrimaryModel): # pylint: disable=too-many-ancestors """Configuration compliance details.""" - device = models.ForeignKey(to="dcim.Device", on_delete=models.CASCADE, help_text="The device", blank=False) - rule = models.ForeignKey(to="ComplianceRule", on_delete=models.CASCADE, blank=False, related_name="rule") - compliance = models.BooleanField(null=True, blank=True) + device = models.ForeignKey(to="dcim.Device", on_delete=models.CASCADE, help_text="The device") + rule = models.ForeignKey(to="ComplianceRule", on_delete=models.CASCADE, related_name="rule") + compliance = models.BooleanField(blank=True) actual = models.JSONField(blank=True, help_text="Actual Configuration for feature") intended = models.JSONField(blank=True, help_text="Intended Configuration for feature") # these three are config snippets exposed for the ConfigDeployment. - remediation = models.JSONField(blank=True, null=True, help_text="Remediation Configuration for the device") + remediation = models.JSONField(blank=True, help_text="Remediation Configuration for the device") missing = models.JSONField(blank=True, help_text="Configuration that should be on the device.") extra = models.JSONField(blank=True, help_text="Configuration that should not be on the device.") - ordered = models.BooleanField(default=True) + ordered = models.BooleanField(default=False) # Used for django-pivot, both compliance and compliance_int should be set. - compliance_int = models.IntegerField(null=True, blank=True) + compliance_int = models.IntegerField(blank=True) def to_objectchange( self, action, *, related_object=None, object_data_extra=None, object_data_exclude=None @@ -382,6 +373,7 @@ def save(self, *args, **kwargs): """The actual configuration compliance happens here, but the details for actual compliance job would be found in FUNC_MAPPER.""" self.compliance_on_save() self.remediation_on_save() + self.full_clean() super().save(*args, **kwargs) @@ -405,16 +397,16 @@ class GoldenConfig(PrimaryModel): # pylint: disable=too-many-ancestors blank=False, ) backup_config = models.TextField(blank=True, help_text="Full backup config for device.") - backup_last_attempt_date = models.DateTimeField(null=True) - backup_last_success_date = models.DateTimeField(null=True) + backup_last_attempt_date = models.DateTimeField(null=True, blank=True) + backup_last_success_date = models.DateTimeField(null=True, blank=True) intended_config = models.TextField(blank=True, help_text="Intended config for the device.") - intended_last_attempt_date = models.DateTimeField(null=True) - intended_last_success_date = models.DateTimeField(null=True) + intended_last_attempt_date = models.DateTimeField(null=True, blank=True) + intended_last_success_date = models.DateTimeField(null=True, blank=True) compliance_config = models.TextField(blank=True, help_text="Full config diff for device.") - compliance_last_attempt_date = models.DateTimeField(null=True) - compliance_last_success_date = models.DateTimeField(null=True) + compliance_last_attempt_date = models.DateTimeField(null=True, blank=True) + compliance_last_success_date = models.DateTimeField(null=True, blank=True) def to_objectchange( self, action, *, related_object=None, object_data_extra=None, object_data_exclude=None @@ -447,16 +439,16 @@ def __str__(self): class GoldenConfigSetting(PrimaryModel): # pylint: disable=too-many-ancestors """GoldenConfigSetting Model definition. This provides global configs instead of via configs.py.""" - name = models.CharField(max_length=100, unique=True, blank=False) - slug = models.SlugField(max_length=100, unique=True, blank=False) - weight = models.PositiveSmallIntegerField(default=1000, blank=False) + name = models.CharField(max_length=100, unique=True) + slug = models.SlugField(max_length=100, unique=True) + weight = models.PositiveSmallIntegerField(default=1000) description = models.CharField( max_length=200, blank=True, ) backup_repository = models.ForeignKey( to="extras.GitRepository", - on_delete=models.SET_NULL, + on_delete=models.PROTECT, null=True, blank=True, related_name="backup_repository", @@ -464,14 +456,13 @@ class GoldenConfigSetting(PrimaryModel): # pylint: disable=too-many-ancestors ) backup_path_template = models.CharField( max_length=255, - null=False, blank=True, verbose_name="Backup Path in Jinja Template Form", help_text="The Jinja path representation of where the backup file will be found. The variable `obj` is available as the device instance object of a given device, as is the case for all Jinja templates. e.g. `{{obj.location.name}}/{{obj.name}}.cfg`", ) intended_repository = models.ForeignKey( to="extras.GitRepository", - on_delete=models.SET_NULL, + on_delete=models.PROTECT, null=True, blank=True, related_name="intended_repository", @@ -479,14 +470,13 @@ class GoldenConfigSetting(PrimaryModel): # pylint: disable=too-many-ancestors ) intended_path_template = models.CharField( max_length=255, - null=False, blank=True, verbose_name="Intended Path in Jinja Template Form", help_text="The Jinja path representation of where the generated file will be places. e.g. `{{obj.location.name}}/{{obj.name}}.cfg`", ) jinja_repository = models.ForeignKey( to="extras.GitRepository", - on_delete=models.SET_NULL, + on_delete=models.PROTECT, null=True, blank=True, related_name="jinja_template", @@ -494,13 +484,11 @@ class GoldenConfigSetting(PrimaryModel): # pylint: disable=too-many-ancestors ) jinja_path_template = models.CharField( max_length=255, - null=False, blank=True, verbose_name="Template Path in Jinja Template Form", help_text="The Jinja path representation of where the Jinja template can be found. e.g. `{{obj.platform.network_driver}}.j2`", ) backup_test_connectivity = models.BooleanField( - null=False, default=True, verbose_name="Backup Test", help_text="Whether or not to pretest the connectivity of the device by verifying there is a resolvable IP that can connect to port 22.", @@ -570,13 +558,11 @@ def get_url_to_filtered_device_list(self): class ConfigRemove(PrimaryModel): # pylint: disable=too-many-ancestors """ConfigRemove for Regex Line Removals from Backup Configuration Model definition.""" - name = models.CharField(max_length=255, null=False, blank=False) + name = models.CharField(max_length=255) platform = models.ForeignKey( to="dcim.Platform", on_delete=models.CASCADE, related_name="backup_line_remove", - null=False, - blank=False, ) description = models.CharField( max_length=200, @@ -613,13 +599,11 @@ def __str__(self): class ConfigReplace(PrimaryModel): # pylint: disable=too-many-ancestors """ConfigReplace for Regex Line Replacements from Backup Configuration Model definition.""" - name = models.CharField(max_length=255, null=False, blank=False) + name = models.CharField(max_length=255) platform = models.ForeignKey( to="dcim.Platform", on_delete=models.CASCADE, related_name="backup_line_replace", - null=False, - blank=False, ) description = models.CharField( max_length=200, @@ -660,8 +644,6 @@ class RemediationSetting(PrimaryModel): # pylint: disable=too-many-ancestors to="dcim.Platform", on_delete=models.CASCADE, related_name="remediation_settings", - null=False, - blank=False, ) remediation_type = models.CharField( @@ -697,11 +679,7 @@ def to_csv(self): def __str__(self): """Return a sane string representation of the instance.""" - return str(self.platform.slug) - - def get_absolute_url(self): - """Absolute url for the RemediationRule instance.""" - return reverse("plugins:nautobot_golden_config:remediationsetting", args=[self.pk]) + return str(self.platform.name) @extras_features( @@ -733,19 +711,15 @@ class ConfigPlan(PrimaryModel): # pylint: disable=too-many-ancestors to="extras.JobResult", on_delete=models.CASCADE, related_name="config_plan", - null=False, - blank=False, verbose_name="Job Result", ) change_control_id = models.CharField( max_length=50, blank=True, - null=True, verbose_name="Change Control ID", help_text="Change Control ID for this configuration plan.", ) change_control_url = models.URLField(blank=True, verbose_name="Change Control URL") - status = StatusField(blank=True, null=True, on_delete=models.PROTECT) class Meta: """Meta information for ConfigPlan model.""" @@ -755,7 +729,3 @@ class Meta: def __str__(self): """Return a simple string if model is called.""" return f"{self.device.name}-{self.plan_type}-{self.created}" - - def get_absolute_url(self): - """Return absolute URL for instance.""" - return reverse("plugins:nautobot_golden_config:configplan", args=[self.pk]) diff --git a/nautobot_golden_config/navigation.py b/nautobot_golden_config/navigation.py index 5f6b3d63..befec9c2 100644 --- a/nautobot_golden_config/navigation.py +++ b/nautobot_golden_config/navigation.py @@ -1,6 +1,7 @@ """Add the configuration compliance buttons to the Plugins Navigation.""" -from nautobot.core.apps import NavMenuGroup, NavMenuItem, NavMenuTab +from nautobot.apps.ui import NavMenuAddButton, NavMenuGroup, NavMenuItem, NavMenuTab + from nautobot_golden_config.utilities.constant import ENABLE_COMPLIANCE, ENABLE_BACKUP items_operate = [ @@ -56,11 +57,8 @@ name="Config Plans", permissions=["nautobot_golden_config.view_configplan"], buttons=( - NavMenuButton( + NavMenuAddButton( link="plugins:nautobot_golden_config:configplan_add", - title="Generate Config Plan", - icon_class="mdi mdi-plus-thick", - button_class=ButtonColorChoices.GREEN, permissions=["nautobot_golden_config.add_configplan"], ), ), @@ -93,11 +91,8 @@ name="Remediation Settings", permissions=["nautobot_golden_config.view_remediationsetting"], buttons=( - NavMenuButton( + NavMenuAddButton( link="plugins:nautobot_golden_config:remediationsetting_add", - title="Remediation Settings", - icon_class="mdi mdi-plus-thick", - button_class=ButtonColorChoices.GREEN, permissions=["nautobot_golden_config.add_remediationsetting"], ), ), diff --git a/nautobot_golden_config/nornir_plays/config_backup.py b/nautobot_golden_config/nornir_plays/config_backup.py index 9aec8ec3..c30fbcc4 100644 --- a/nautobot_golden_config/nornir_plays/config_backup.py +++ b/nautobot_golden_config/nornir_plays/config_backup.py @@ -7,12 +7,12 @@ from nornir.core.task import Result, Task from nornir.core.plugins.inventory import InventoryPluginRegister +from nornir_nautobot.exceptions import NornirNautobotException from nornir_nautobot.plugins.tasks.dispatcher import dispatcher from nornir_nautobot.utils.logger import NornirLogger from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory from nautobot_plugin_nornir.constants import NORNIR_SETTINGS -from nautobot_plugin_nornir.utils import get_dispatcher from nautobot_golden_config.utilities.db_management import close_threaded_db_connections from nautobot_golden_config.utilities.helper import ( @@ -63,29 +63,31 @@ def run_backup( # pylint: disable=too-many-arguments if settings.backup_test_connectivity is not False: task.run( task=dispatcher, - name="TEST CONNECTIVITY", method="check_connectivity", - obj=obj, logger=logger, - default_drivers_mapping=get_dispatcher(), + obj=obj, + framework="napalm", + custom_dispatcher={}, + name="TEST CONNECTIVITY", ) running_config = task.run( task=dispatcher, - name="SAVE BACKUP CONFIGURATION TO FILE", method="get_config", obj=obj, logger=logger, + framework="napalm", + custom_dispatcher={}, + name="SAVE BACKUP CONFIGURATION TO FILE", backup_file=backup_file, - remove_lines=remove_regex_dict.get(obj.platform.netwokr_driver, []), - substitute_lines=replace_regex_dict.get(obj.platform.netwokr_driver, []), - default_drivers_mapping=get_dispatcher(), + remove_lines=remove_regex_dict.get(obj.platform.network_driver, []), + substitute_lines=replace_regex_dict.get(obj.platform.network_driver, []), )[1].result["config"] backup_obj.backup_last_success_date = task.host.defaults.data["now"] backup_obj.backup_config = running_config backup_obj.save() - logger.log_success(obj, "Successfully extracted running configuration from device.") + logger.log_info(obj, "Successfully extracted running configuration from device.") return Result(host=task.host, result=running_config) @@ -143,7 +145,8 @@ def config_backup(job_result, data): logger.log_debug("Completed configuration from devices.") except Exception as err: - logger.log_failure(None, err) - raise + error_msg = f"E3001: {err}" + logger.log_error(error_msg) + raise NornirNautobotException(error_msg) logger.log_debug("Completed configuration backup job for devices.") diff --git a/nautobot_golden_config/nornir_plays/config_compliance.py b/nautobot_golden_config/nornir_plays/config_compliance.py index 63c377c8..66fdb420 100644 --- a/nautobot_golden_config/nornir_plays/config_compliance.py +++ b/nautobot_golden_config/nornir_plays/config_compliance.py @@ -38,7 +38,7 @@ def get_rules(): """A serializer of sorts to return rule mappings as a dictionary.""" - # TODO: Review if creating a proper serializer is the way to go. + # TODO: Future: Review if creating a proper serializer is the way to go. rules = defaultdict(list) for compliance_rule in ComplianceRule.objects.all(): platform = str(compliance_rule.platform.network_driver) @@ -64,8 +64,9 @@ def get_config_element(rule, config, obj, logger): config_json = get_json_config(config) if not config_json: - logger.log_failure(obj, "Unable to interpret configuration as JSON.") - raise NornirNautobotException("Unable to interpret configuration as JSON.") + error_msg = "E3002: Unable to interpret configuration as JSON." + logger.log_error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) if rule["obj"].match_config: config_element = {k: config_json.get(k) for k in rule["obj"].match_config.splitlines() if k in config_json} @@ -74,19 +75,16 @@ def get_config_element(rule, config, obj, logger): elif rule["obj"].config_type == ComplianceRuleConfigTypeChoice.TYPE_CLI: if get_platform(obj.platform.network_driver) not in parser_map.keys(): - logger.log_failure( - obj, - f"There is currently no CLI-config parser support for platform network_driver `{get_platform(obj.platform.network_driver)}`, preemptively failed.", - ) - raise NornirNautobotException( - f"There is currently no CLI-config parser support for platform network_driver `{get_platform(obj.platform.network_driver)}`, preemptively failed." - ) + error_msg = f"E3003: There is currently no CLI-config parser support for platform network_driver `{get_platform(obj.platform.network_driver)}`, preemptively failed." + logger.log_error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) config_element = section_config(rule, config, get_platform(obj.platform.network_driver)) else: - logger.log_failure(obj, f"There rule type ({rule['obj'].config_type}) is not recognized.") - raise NornirNautobotException(f"There rule type ({rule['obj'].config_type}) is not recognized.") + error_msg = f"E3004: There rule type ({rule['obj'].config_type}) is not recognized." + logger.log_error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) return config_element @@ -129,28 +127,26 @@ def run_compliance( # pylint: disable=too-many-arguments,too-many-locals intended_file = os.path.join(intended_directory, intended_path_template_obj) if not os.path.exists(intended_file): - logger.log_failure(obj, f"Unable to locate intended file for device at {intended_file}, preemptively failed.") - raise NornirNautobotException( - f"Unable to locate intended file for device at {intended_file}, preemptively failed." - ) + error_msg = f"E3005: Unable to locate intended file for device at {intended_file}, preemptively failed." + logger.log_error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) backup_directory = settings.backup_repository.filesystem_path backup_template = render_jinja_template(obj, logger, settings.backup_path_template) backup_file = os.path.join(backup_directory, backup_template) if not os.path.exists(backup_file): - logger.log_failure(obj, f"Unable to locate backup file for device at {backup_file}, preemptively failed.") - raise NornirNautobotException(f"Unable to locate backup file for device at {backup_file}, preemptively failed.") + error_msg = f"E3006: Unable to locate backup file for device at {backup_file}, preemptively failed." + logger.log_error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) platform = obj.platform.network_driver if not rules.get(platform): - logger.log_failure( - obj, - f"There is no defined `Configuration Rule` for platform network_driver `{platform}`, preemptively failed.", - ) - raise NornirNautobotException( - f"There is no defined `Configuration Rule` for platform network_driver `{platform}`, preemptively failed." + error_msg = ( + f"E3007: There is no defined `Configuration Rule` for platform network_driver `{platform}`, preemptively failed." ) + logger.log_error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) backup_cfg = _open_file_config(backup_file) intended_cfg = _open_file_config(intended_file) @@ -174,7 +170,7 @@ def run_compliance( # pylint: disable=too-many-arguments,too-many-locals compliance_obj.compliance_last_success_date = task.host.defaults.data["now"] compliance_obj.compliance_config = "\n".join(diff_files(backup_file, intended_file)) compliance_obj.save() - logger.log_success(obj, "Successfully tested compliance job.") + logger.log_info(obj, "Successfully tested compliance job.") return Result(host=task.host) @@ -218,7 +214,8 @@ def config_compliance(job_result, data): ) except Exception as err: - logger.log_failure(None, err) - raise + error_msg = f"E3009: {err}" + logger.log_error(error_msg) + raise NornirNautobotException(error_msg) logger.log_debug("Completed compliance job for devices.") diff --git a/nautobot_golden_config/nornir_plays/config_deployment.py b/nautobot_golden_config/nornir_plays/config_deployment.py index 1862f7ea..a563dde3 100644 --- a/nautobot_golden_config/nornir_plays/config_deployment.py +++ b/nautobot_golden_config/nornir_plays/config_deployment.py @@ -1,15 +1,19 @@ """Nornir job for deploying configurations.""" from datetime import datetime -from nautobot.dcim.models import Device -from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory -from nautobot_plugin_nornir.constants import NORNIR_SETTINGS -from nautobot_plugin_nornir.utils import get_dispatcher + from nornir import InitNornir from nornir.core.task import Result, Task from nornir.core.plugins.inventory import InventoryPluginRegister + +from nornir_nautobot.exceptions import NornirNautobotException from nornir_nautobot.plugins.tasks.dispatcher import dispatcher from nornir_nautobot.utils.logger import NornirLogger +from nautobot.dcim.models import Device + +from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory +from nautobot_plugin_nornir.constants import NORNIR_SETTINGS + from nautobot_golden_config.nornir_plays.processor import ProcessGoldenConfig InventoryPluginRegister.register("nautobot-inventory", NautobotORMInventory) @@ -21,20 +25,20 @@ def run_deployment(task: Task, logger: NornirLogger, commit: bool, config_plan_q plans_to_deploy = config_plan_qs.filter(device=obj) consolidated_config_set = "\n".join(plans_to_deploy.values_list("config_set", flat=True)) logger.log_debug(f"Consolidated config set: {consolidated_config_set}") - # TODO: We should add post-processing rendering here + # TODO: Future: We should add post-processing rendering here # after https://github.com/nautobot/nautobot-plugin-golden-config/issues/443 if commit: result = task.run( task=dispatcher, - name="DEPLOY CONFIG TO DEVICE", method="merge_config", obj=obj, logger=logger, + custom_dispatcher={}, + name="DEPLOY CONFIG TO DEVICE", config=consolidated_config_set, - default_drivers_mapping=get_dispatcher(), )[1].result["result"] - logger.log_success(obj=obj, message="Successfully deployed configuration to device.") + logger.log_info(obj=obj, message="Successfully deployed configuration to device.") else: result = None logger.log_info(obj=obj, message="Commit not enabled. Configuration not deployed to device.") @@ -50,11 +54,10 @@ def config_deployment(job_result, data, commit): config_plan_qs = data["config_plan"] if config_plan_qs.filter(status__slug="not-approved").exists(): - logger.log_failure( - obj=None, - message="Cannot deploy configuration(s). One or more config plans are not approved.", - ) - raise ValueError("Cannot deploy configuration(s). One or more config plans are not approved.") + error_msg = "E3010: Cannot deploy configuration(s). One or more config plans are not approved." + logger.log_error(error_msg) + raise NornirNautobotException(error_msg) + device_qs = Device.objects.filter(config_plan__in=config_plan_qs).distinct() try: @@ -81,7 +84,8 @@ def config_deployment(job_result, data, commit): config_plan_qs=config_plan_qs, ) except Exception as err: - logger.log_failure(obj=None, message=f"Failed to initialize Nornir: {err}") - raise + error_msg = f"E3011: {err}" + logger.log_error(error_msg) + raise NornirNautobotException(error_msg) logger.log_debug("Completed configuration deployment.") diff --git a/nautobot_golden_config/nornir_plays/config_intended.py b/nautobot_golden_config/nornir_plays/config_intended.py index be5f18e9..8835d8c5 100644 --- a/nautobot_golden_config/nornir_plays/config_intended.py +++ b/nautobot_golden_config/nornir_plays/config_intended.py @@ -17,7 +17,6 @@ from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory from nautobot_plugin_nornir.constants import NORNIR_SETTINGS -from nautobot_plugin_nornir.utils import get_dispatcher from nautobot_golden_config.utilities.constant import PLUGIN_CFG from nautobot_golden_config.utilities.db_management import close_threaded_db_connections @@ -75,10 +74,10 @@ def run_template( # pylint: disable=too-many-arguments jinja_template = render_jinja_template(obj, logger, settings.jinja_path_template) status, device_data = graph_ql_query(nautobot_job.request, obj, settings.sot_agg_query.query) if status != 200: - logger.log_failure(obj, f"The GraphQL query return a status of {str(status)} with error of {str(device_data)}") - raise NornirNautobotException( - f"The GraphQL query return a status of {str(status)} with error of {str(device_data)}" - ) + error_msg = f"E3012: The GraphQL query return a status of {str(status)} with error of {str(device_data)}" + logger.log_error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) + task.host.data.update(device_data) generated_config = task.run( @@ -90,7 +89,7 @@ def run_template( # pylint: disable=too-many-arguments jinja_template=jinja_template, jinja_root_path=settings.jinja_repository.filesystem_path, output_file_location=output_file_location, - default_drivers_mapping=get_dispatcher(), + custom_dispatcher={}, jinja_filters=jinja_env.filters, jinja_env=jinja_env, )[1].result["config"] @@ -98,7 +97,7 @@ def run_template( # pylint: disable=too-many-arguments intended_obj.intended_config = generated_config intended_obj.save() - logger.log_success(obj, "Successfully generated the intended configuration.") + logger.log_info(obj, "Successfully generated the intended configuration.") return Result(host=task.host, result=generated_config) @@ -151,5 +150,6 @@ def config_intended(nautobot_job, data): ) except Exception as err: - logger.log_failure(None, err) - raise + error_msg = f"E3013: {err}" + logger.log_error(error_msg) + raise NornirNautobotException(error_msg) diff --git a/nautobot_golden_config/nornir_plays/processor.py b/nautobot_golden_config/nornir_plays/processor.py index fa4afdeb..1f2c1c41 100644 --- a/nautobot_golden_config/nornir_plays/processor.py +++ b/nautobot_golden_config/nornir_plays/processor.py @@ -32,4 +32,5 @@ def task_instance_completed(self, task: Task, host: Host, result: MultiResult) - for level_2_result in level_1_result.exception.result: if isinstance(level_2_result.exception, NornirNautobotException): return - self.logger.log_failure(task.host.data["obj"], f"{task.name} failed: {result.exception}") + self.logger.log_error(f"{task.name} failed: {result.exception}", extra={"object": task.host.data["obj"]}) + # TODO 2.0: update the state???? diff --git a/nautobot_golden_config/templates/nautobot_golden_config/compliance_device_report.html b/nautobot_golden_config/templates/nautobot_golden_config/compliance_device_report.html index caf6dc3c..d2184c63 100644 --- a/nautobot_golden_config/templates/nautobot_golden_config/compliance_device_report.html +++ b/nautobot_golden_config/templates/nautobot_golden_config/compliance_device_report.html @@ -188,7 +188,7 @@

{% block title %}Configuration Compliance - {{ device.name }}{% endblock %}< {% endfor %} {% endblock %} - + {% block javascript %}