From b1032bee392c413eac90485e31fcc28fe2aa94be Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Tue, 14 Nov 2023 15:02:33 +0400 Subject: [PATCH 01/13] Change URLs in Usage to docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2955f8b..c0b66ab 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ systemctl restart netbox ## Usage -Read this [doc](docs/colliecting-diffs.md) about collecting diffs, for configuration management read [this](docs/configuratiom-management.md) +Read this [doc](https://miaow2.github.io/netbox-config-diff/colliecting-diffs/) about collecting diffs, for configuration management read [this](https://miaow2.github.io/netbox-config-diff/configuratiom-management/) ## Screenshots From d70debcbd2e5f5ba0fb868871517fbe71481e866 Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Tue, 12 Dec 2023 12:26:08 +0300 Subject: [PATCH 02/13] Update project description --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0ae9cf3..42a1404 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires = [ [project] name = "netbox-config-diff" -description = "Find diff between the intended device configuration and actual." +description = "Push rendered device configurations from NetBox to devices and apply them." readme = "README.md" keywords = [ "netbox", From 04476cd659445244a76536bb1ab58ee99f6c0b58 Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Tue, 12 Dec 2023 12:32:17 +0300 Subject: [PATCH 03/13] Add video presentation --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index c0b66ab..c8718b6 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,12 @@ systemctl restart netbox ## Usage Read this [doc](https://miaow2.github.io/netbox-config-diff/colliecting-diffs/) about collecting diffs, for configuration management read [this](https://miaow2.github.io/netbox-config-diff/configuratiom-management/) + +## Video + +My presention about plugin at October NetBox community call (19.10.2023). + +[![October NetBox community call](https://img.youtube.com/vi/B4uhtYh278o/0.jpg)](https://youtu.be/B4uhtYh278o?t=425) ## Screenshots From f83d730e73f649c26a0b5fa988125aba6e25fbe6 Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Sat, 27 Jan 2024 21:52:20 +0800 Subject: [PATCH 04/13] Closes #50: Add template field to ConfigDiffScript (#51) * Add name template for searching in DataSource * Add docs for templating files name in DataSource --- .github/workflows/commit.yaml | 2 +- docs/colliecting-diffs.md | 11 +++++++++++ docs/media/screenshots/script.png | Bin 42952 -> 43979 bytes netbox_config_diff/compliance/base.py | 21 ++++++++++++++++++--- netbox_config_diff/models/data_models.py | 5 ++++- tests/conftest.py | 1 + 6 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 4731536..0effea7 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -31,7 +31,7 @@ jobs: strategy: max-parallel: 10 matrix: - netbox_version: ["v3.5.9", "v3.6.4"] + netbox_version: ["v3.5.9", "v3.6.9"] steps: - name: Checkout uses: actions/checkout@v3 diff --git a/docs/colliecting-diffs.md b/docs/colliecting-diffs.md index 54ddae6..01c6b15 100644 --- a/docs/colliecting-diffs.md +++ b/docs/colliecting-diffs.md @@ -42,6 +42,17 @@ If you have configs in NetBox DataSource, you can define it, the script instead !!! note Only synced DataSources are acceptable +If in your DataSource config names are different from the hostnames of the devices, you can specify config name with Jinja2 template in `Name template` field. + Reference device with `{{ object }}` variable. + +For example, config name is virtual chassis name plus `config` (`switchname-config`) and your devices names are `switchname1`, `switchname2` and etc. + +You can define Jinja2 template with logic to use virtual chassis name if device is in chassis, else use device name: + +``` +{% if object.virtual_chassis %}{{ object.virtual_chassis.name }}-config{% else %}{{ object.name }}{% endif %} +``` + ![Screenshot of the script](media/screenshots/script.png) ## Results diff --git a/docs/media/screenshots/script.png b/docs/media/screenshots/script.png index b0fe6768dd02257a5e7687f0145db798db6f53e1..498aeb677cbad6e7aaccb457212fbfd443ce353f 100644 GIT binary patch literal 43979 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 JnUQPMe*j#fDX;(l diff --git a/netbox_config_diff/compliance/base.py b/netbox_config_diff/compliance/base.py index 2e2c3f8..fb9702a 100644 --- a/netbox_config_diff/compliance/base.py +++ b/netbox_config_diff/compliance/base.py @@ -9,10 +9,11 @@ from dcim.models import Device, DeviceRole, Site from django.conf import settings from django.db.models import Q -from extras.scripts import MultiObjectVar, ObjectVar +from extras.scripts import MultiObjectVar, ObjectVar, TextVar from jinja2.exceptions import TemplateError from netutils.config.compliance import diff_network_config from utilities.exceptions import AbortScript +from utilities.utils import render_jinja2 from netbox_config_diff.models import ConplianceDeviceDataClass @@ -52,6 +53,11 @@ class ConfigDiffBase(SecretsMixin): }, description="Define synced DataSource, if you want compare configs stored in it wihout connecting to devices", ) + name_template = TextVar( + required=False, + description="Jinja2 template code for the device name in Data source. " + "Reference the object as {{ object }}.", + ) def run_script(self, data: dict) -> None: devices = self.validate_data(data) @@ -155,17 +161,26 @@ def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterat auth_secondary=auth_secondary, rendered_config=rendered_config, error=error, + device=device, ) def get_config_from_datasource(self, devices: list[ConplianceDeviceDataClass]) -> None: for device in devices: - if df := DataFile.objects.filter(source=self.data["data_source"], path__icontains=device.name).first(): + if self.data["name_template"]: + try: + device_name = render_jinja2(self.data["name_template"], {"object": device.device}).strip() + except Exception as e: + self.log_failure(f"Error in rendering data source name for {device.name}: {e}, using device name.") + device_name = device.name + else: + device_name = device.name + if df := DataFile.objects.filter(source=self.data["data_source"], path__icontains=device_name).first(): if config := df.data_as_string: device.actual_config = config else: device.error = f"Data in file {df} is broken, skiping device {device.name}" else: - device.error = f"Not found file in DataSource for device {device.name}" + device.error = f"Not found file in DataSource for name {device_name}" def get_actual_configs(self, devices: list[ConplianceDeviceDataClass]) -> None: if self.data["data_source"]: diff --git a/netbox_config_diff/models/data_models.py b/netbox_config_diff/models/data_models.py index 43162ce..1567bbc 100644 --- a/netbox_config_diff/models/data_models.py +++ b/netbox_config_diff/models/data_models.py @@ -1,6 +1,7 @@ import traceback from dataclasses import dataclass +from dcim.models import Device from scrapli import AsyncScrapli from netbox_config_diff.choices import ConfigComplianceStatusChoices @@ -113,10 +114,12 @@ def send_to_db(self) -> None: class ConplianceDeviceDataClass(BaseDeviceDataClass): command: str + device: Device | None = None - def __init__(self, command: str, **kwargs) -> None: + def __init__(self, command: str, device: Device, **kwargs) -> None: super().__init__(**kwargs) self.command = command + self.device = device async def get_actual_config(self) -> None: if self.error is not None: diff --git a/tests/conftest.py b/tests/conftest.py index 692d0a9..da9e32f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,6 +90,7 @@ def factory(**fields: Unpack["DeviceDataClassData"]) -> "DeviceDataClassData": "password": faker.password(), "auth_strict_key": False, "transport": "asyncssh", + "device": None, } return data | fields From 63dd590e81d99e31fdaa921f72a30b14c698f61e Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Sun, 28 Jan 2024 20:41:03 +0800 Subject: [PATCH 05/13] Closes #47: Move plugin to separete menu item in navbar (#52) * Move plugin into separate menu item * Add tab for devices with their config compliance --- docs/colliecting-diffs.md | 2 +- docs/media/screenshots/navbar.png | Bin 7416 -> 9572 bytes .../0008_alter_configcompliance_device.py | 21 +++++ netbox_config_diff/models/models.py | 2 +- netbox_config_diff/navigation.py | 51 +++++----- .../netbox_config_diff/configcompliance.html | 88 ++---------------- .../configcompliance/base.html | 13 --- .../configcompliance/config.html | 2 +- .../configcompliance/data.html | 81 ++++++++++++++++ .../configcompliance/missing_extra.html | 2 +- netbox_config_diff/views/compliance.py | 42 ++++++++- 11 files changed, 183 insertions(+), 121 deletions(-) create mode 100644 netbox_config_diff/migrations/0008_alter_configcompliance_device.py delete mode 100644 netbox_config_diff/templates/netbox_config_diff/configcompliance/base.html create mode 100644 netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html diff --git a/docs/colliecting-diffs.md b/docs/colliecting-diffs.md index 01c6b15..b5cd95f 100644 --- a/docs/colliecting-diffs.md +++ b/docs/colliecting-diffs.md @@ -1,7 +1,7 @@ # Usage -Under `Plugins` navbar menu you can find plugin +In navbar serach for `Config Diff Plugin` menu ![Screenshot of navbar](media/screenshots/navbar.png) diff --git a/docs/media/screenshots/navbar.png b/docs/media/screenshots/navbar.png index 167889fb96de411977a00702581a6b43b89d5675..7b0dba25187e8ceef34907df25a115b800ab72c8 100644 GIT binary patch literal 9572 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 literal 7416 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 xJ?Pd4OalR+?w0&0;lD{ PluginMenuButton: ) -menu_items = ( - PluginMenuItem( - link="plugins:netbox_config_diff:platformsetting_list", - link_text="Platform Settings", - buttons=[get_add_button("platformsetting")], - permissions=["netbox_config_diff.view_platformsetting"], - ), - PluginMenuItem( - link="plugins:netbox_config_diff:configcompliance_list", - link_text="Config Compliances", - buttons=[], - permissions=["netbox_config_diff.view_configcompliance"], - ), - PluginMenuItem( - link="plugins:netbox_config_diff:configurationrequest_list", - link_text="Configuration Requests", - buttons=[get_add_button("configurationrequest")], - permissions=["netbox_config_diff.view_configurationrequest"], - ), +def get_menu_item(model: str, verbose_name: str, add_button: bool = True) -> PluginMenuItem: + return PluginMenuItem( + link=f"plugins:netbox_config_diff:{model}_list", + link_text=verbose_name, + buttons=[get_add_button(model)] if add_button else [], + permissions=[f"netbox_config_diff.view_{model}"], + ) + + +compliance_items = ( + get_menu_item("platformsetting", "Platform Settings"), + get_menu_item("configcompliance", "Config Compliances", add_button=False), +) + +config_items = ( + get_menu_item("configurationrequest", "Configuration Requests"), + get_menu_item("substitute", "Substitutes"), PluginMenuItem( link="plugins:netbox_config_diff:configurationrequest_job_list", link_text="Jobs", buttons=[], permissions=["core.view_job"], ), - PluginMenuItem( - link="plugins:netbox_config_diff:substitute_list", - link_text="Substitutes", - buttons=[get_add_button("substitute")], - permissions=["netbox_config_diff.view_substitute"], +) + +menu = PluginMenu( + label="Config Diff Plugin", + groups=( + ("Compliance", compliance_items), + ("Config Management", config_items), ), + icon_class="mdi mdi-vector-difference", ) diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance.html index 50bc5d6..2505fc7 100644 --- a/netbox_config_diff/templates/netbox_config_diff/configcompliance.html +++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance.html @@ -1,81 +1,13 @@ -{% extends "netbox_config_diff/configcompliance/base.html" %} -{% load static %} +{% extends "generic/object.html" %} +{% load buttons %} +{% load perms %} -{% block content %} -
-
-
-
{{ object|meta:"verbose_name"|bettertitle }}
-
- - - - - - - - - -
Device{{ object.device|linkify }}
Status{% badge object.get_status_display bg_color=object.get_status_color %}
-
-
+{% block controls %} +
+
+ {% if request.user|can_delete:object %} + {% delete_button object %} + {% endif %}
- {% if object.error %} -
-
-
Error
-
-
{{ object.error }}
-
-
-
- {% endif %}
- {% if object.diff %} -
-
-
-
Diff
-
-
-
-
- {% endif %} -{% endblock content %} - -{% block javascript %} - - -{% endblock javascript %} +{% endblock controls %} diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/base.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/base.html deleted file mode 100644 index 8fdcc6f..0000000 --- a/netbox_config_diff/templates/netbox_config_diff/configcompliance/base.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "generic/object.html" %} -{% load buttons %} -{% load perms %} - -{% block controls %} -
-
- {% if request.user|can_delete:object %} - {% delete_button object %} - {% endif %} -
-
-{% endblock controls %} \ No newline at end of file diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html index 8bc2722..ae2c864 100644 --- a/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html +++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html @@ -1,4 +1,4 @@ -{% extends "netbox_config_diff/configcompliance/base.html" %} +{% extends "netbox_config_diff/configcompliance.html" %} {% block title %}{{ object }} - {{ header }}{% endblock %} diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html new file mode 100644 index 0000000..5c20e42 --- /dev/null +++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html @@ -0,0 +1,81 @@ +{% extends base_template %} +{% load static %} + +{% block content %} +
+
+
+
{{ instance|meta:"verbose_name"|bettertitle }}
+
+ + + + + + + + + +
Device{{ instance.device|linkify }}
Status{% badge instance.get_status_display bg_color=instance.get_status_color %}
+
+
+
+ {% if instance.error %} +
+
+
Error
+
+
{{ instance.error }}
+
+
+
+ {% endif %} +
+ {% if instance.diff %} +
+
+
+
Diff
+
+
+
+
+ {% endif %} +{% endblock content %} + +{% block javascript %} + + +{% endblock javascript %} diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html index c7692fd..de94dd9 100644 --- a/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html +++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html @@ -1,4 +1,4 @@ -{% extends "netbox_config_diff/configcompliance/base.html" %} +{% extends "netbox_config_diff/configcompliance.html" %} {% block title %}{{ object }} - Missing/Extra{% endblock %} diff --git a/netbox_config_diff/views/compliance.py b/netbox_config_diff/views/compliance.py index fbe5c95..2a4d49c 100644 --- a/netbox_config_diff/views/compliance.py +++ b/netbox_config_diff/views/compliance.py @@ -1,5 +1,6 @@ +from dcim.models import Device from django.http import HttpResponse -from django.shortcuts import render +from django.shortcuts import redirect, render from django.utils.translation import gettext as _ from netbox.views import generic from utilities.views import ViewTab, register_model_view @@ -51,6 +52,14 @@ def get_extra_context(self, request, instance): @register_model_view(ConfigCompliance) class ConfigComplianceView(generic.ObjectView): queryset = ConfigCompliance.objects.all() + base_template = "netbox_config_diff/configcompliance.html" + template_name = "netbox_config_diff/configcompliance/data.html" + + def get_extra_context(self, request, instance): + return { + "instance": instance, + "base_template": self.base_template, + } @register_model_view(ConfigCompliance, "rendered-config") @@ -113,6 +122,37 @@ def get(self, request, **kwargs): ) +@register_model_view(Device, "config_compliance", "config-compliance") +class ConfigComplianceDeviceView(generic.ObjectView): + queryset = Device.objects.all() + base_template = "dcim/device/base.html" + template_name = "netbox_config_diff/configcompliance/data.html" + tab = ViewTab( + label=_("Config Compliance"), + weight=2110, + badge=lambda obj: 1 if hasattr(obj, "config_compliance") else 0, + hide_if_empty=True, + ) + + def get(self, request, **kwargs): + instance = self.get_object(**kwargs) + + if not hasattr(instance, "config_compliance"): + return redirect("dcim:device", pk=instance.pk) + + return render( + request, + self.get_template_name(), + { + "object": instance, + "instance": instance.config_compliance, + "tab": self.tab, + "base_template": self.base_template, + **self.get_extra_context(request, instance), + }, + ) + + class ConfigComplianceListView(generic.ObjectListView): queryset = ConfigCompliance.objects.prefetch_related("device") filterset = ConfigComplianceFilterSet From 5dd31cff42251881ed3e55f841a536e91258ccdc Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Thu, 1 Feb 2024 02:40:40 +0800 Subject: [PATCH 06/13] Closes #53: Add netbox-rq to installation process docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8718b6..0d7795c 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ python manage.py collectstatic --noinput Restart NetBox service: ```bash -systemctl restart netbox +systemctl restart netbox netbox-rq ``` From 79c3ec34f193deccbaa3330ab0dd8571969b5cd2 Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Tue, 6 Feb 2024 12:58:09 +0400 Subject: [PATCH 07/13] Changelog for 2.2.0 version --- docs/changelog.md | 6 ++++++ netbox_config_diff/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 1766866..07af73b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # Changelog +## 2.2.0 (2024-02-06) + +* [#47](https://github.com/miaow2/netbox-config-diff/issues/47) Move plugin to separete menu item in navbar and add tab for devices with compliance result +* [#50](https://github.com/miaow2/netbox-config-diff/issues/50) Add template field for device name in DataSource to ConfigDiffScript +* [#53](https://github.com/miaow2/netbox-config-diff/issues/53) Add netbox-rq to installation process docs + ## 2.1.0 (2023-10-26) * [#35](https://github.com/miaow2/netbox-config-diff/issues/35) Add ability to define password for accessing priviliged exec mode diff --git a/netbox_config_diff/__init__.py b/netbox_config_diff/__init__.py index 3cd5c9e..f687423 100644 --- a/netbox_config_diff/__init__.py +++ b/netbox_config_diff/__init__.py @@ -2,7 +2,7 @@ __author__ = "Artem Kotik" __email__ = "miaow2@yandex.ru" -__version__ = "2.1.0" +__version__ = "2.2.0" class ConfigDiffConfig(PluginConfig): From a757ac70f61f1253445c24cc265abf4a00b2de96 Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Wed, 10 Apr 2024 02:09:00 +0800 Subject: [PATCH 08/13] Handle junipers templates with set commands (#58) --- netbox_config_diff/configurator/base.py | 2 +- netbox_config_diff/configurator/platforms.py | 73 +++++++++++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/netbox_config_diff/configurator/base.py b/netbox_config_diff/configurator/base.py index a6635a2..38bc94b 100644 --- a/netbox_config_diff/configurator/base.py +++ b/netbox_config_diff/configurator/base.py @@ -126,7 +126,7 @@ async def _collect_one_diff(self, device: ConfiguratorDeviceDataClass) -> None: ) device.rendered_config = rendered_config else: - actual_config = await conn.get_config() + actual_config = await conn.get_config(config_template=device.rendered_config) device.actual_config = conn.clean_config(actual_config.result) device.diff = get_unified_diff(device.rendered_config, device.actual_config, device.name) diff --git a/netbox_config_diff/configurator/platforms.py b/netbox_config_diff/configurator/platforms.py index d4ffe97..45eb5a2 100644 --- a/netbox_config_diff/configurator/platforms.py +++ b/netbox_config_diff/configurator/platforms.py @@ -1,5 +1,5 @@ import re -from typing import Pattern +from typing import Any, Pattern from scrapli_cfg.exceptions import TemplateError from scrapli_cfg.platform.core.arista_eos import AsyncScrapliCfgEOS @@ -94,13 +94,17 @@ async def render_substituted_config( """ self.logger.info("fetching configuration and replacing with provided substitutes") - source_config = await self.get_config(source=source) + source_config = await self.get_config(config_template=config_template, source=source) return source_config, self._render_substituted_config( config_template=config_template, substitutes=substitutes, source_config=source_config.result, ) + async def get_config(self, **kwargs) -> ScrapliCfgResponse: + kwargs.pop("config_template", None) + return await super().get_config(**kwargs) + class CustomAsyncScrapliCfgEOS(CustomScrapliCfg, AsyncScrapliCfgEOS): pass @@ -119,4 +123,67 @@ class CustomAsyncScrapliCfgNXOS(CustomScrapliCfg, AsyncScrapliCfgNXOS): class CustomAsyncScrapliCfgJunos(CustomScrapliCfg, AsyncScrapliCfgJunos): - pass + is_set_config = False + + async def get_config(self, config_template: str, source: str = "running") -> ScrapliCfgResponse: + response = self._pre_get_config(source=source) + + command = "show configuration" + if re.findall(r"^set\s+", config_template, flags=re.I | re.M): + self.is_set_config = True + command += " | display set" + + if self._in_configuration_session is True: + config_result = await self.conn.send_config(config=f"run {command}") + else: + config_result = await self.conn.send_command(command=command) + + return self._post_get_config( + response=response, + source=source, + scrapli_responses=[config_result], + result=config_result.result, + ) + + async def load_config(self, config: str, replace: bool = False, **kwargs: Any) -> ScrapliCfgResponse: + """ + Load configuration to a device + + Supported kwargs: + set: bool indicating config is a "set" style config (ignored if replace is True) + + Args: + config: string of the configuration to load + replace: replace the configuration or not, if false configuration will be loaded as a + merge operation + kwargs: additional kwargs that the implementing classes may need for their platform, + see above for junos supported kwargs + + Returns: + ScrapliCfgResponse: response object + + Raises: + N/A + + """ + response = self._pre_load_config(config=config) + + config = self._prepare_load_config(config=config, replace=replace) + + config_result = await self.conn.send_config(config=config, privilege_level="root_shell") + + if self.is_set_config is True: + load_config = f"load set {self.filesystem}{self.candidate_config_filename}" + else: + if self._replace is True: + load_config = f"load override {self.filesystem}{self.candidate_config_filename}" + else: + load_config = f"load merge {self.filesystem}{self.candidate_config_filename}" + + load_result = await self.conn.send_config(config=load_config) + self._in_configuration_session = True + + return self._post_load_config( + response=response, + scrapli_responses=[config_result, load_result], + ) From cd091be94cb50df55ce1ab721e7132376753194a Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Wed, 10 Apr 2024 04:45:18 +0800 Subject: [PATCH 09/13] Reverse columns in diff (#59) --- netbox_config_diff/compliance/utils.py | 2 +- tests/test_compliance_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox_config_diff/compliance/utils.py b/netbox_config_diff/compliance/utils.py index c0aa4af..1da8fc7 100644 --- a/netbox_config_diff/compliance/utils.py +++ b/netbox_config_diff/compliance/utils.py @@ -30,8 +30,8 @@ def __init__(self, choices, *args, **kwargs): def get_unified_diff(rendered_config: str, actual_config: str, device: str) -> str: diff = unified_diff( - rendered_config.strip().splitlines(), actual_config.splitlines(), + rendered_config.strip().splitlines(), fromfiledate=device, tofiledate=device, lineterm="", diff --git a/tests/test_compliance_utils.py b/tests/test_compliance_utils.py index 4178a26..b69b285 100644 --- a/tests/test_compliance_utils.py +++ b/tests/test_compliance_utils.py @@ -56,4 +56,4 @@ def test_exclude_lines(regex: str, expected: str) -> None: ids=["diff", "no diff"], ) def test_get_unified_diff(render: str, actual: str, expected: str) -> None: - assert get_unified_diff(render, actual, "test-1") == expected + assert get_unified_diff(actual, render, "test-1") == expected From bbf82930337cf6c8fd15a2e034bc7c27358efa7a Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Wed, 10 Apr 2024 18:22:22 +0400 Subject: [PATCH 10/13] Add python 3.12 and Netbox 3.7.5 in CI --- .github/workflows/commit.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 0effea7..23b3ca3 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 10 matrix: - python: ["3.10", "3.11"] + python: ["3.10", "3.11", "3.12"] steps: - name: Checkout uses: actions/checkout@v3 @@ -31,7 +31,7 @@ jobs: strategy: max-parallel: 10 matrix: - netbox_version: ["v3.5.9", "v3.6.9"] + netbox_version: ["v3.5.9", "v3.6.9", "v3.7.5"] steps: - name: Checkout uses: actions/checkout@v3 From 834e88e8ba2865da53f19a3f8c2bd0a561679168 Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Thu, 11 Apr 2024 17:00:43 +0400 Subject: [PATCH 11/13] Add warning about using set commands in Juniper --- docs/configuratiom-management.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/configuratiom-management.md b/docs/configuratiom-management.md index 52d9a42..a28413e 100644 --- a/docs/configuratiom-management.md +++ b/docs/configuratiom-management.md @@ -12,6 +12,12 @@ Supported platforms: Plugin using [scrapli-cfg](https://github.com/scrapli/scrapli_cfg) for this feature. +!!! warning + If you use Juniper and render config in set commands, please read next info. + Plugin uses `load override` command to load config to a device, set commands load with `load set`. + With `load set` commnad you can't replace all config, because this command uses `merge` action. + So, please, be careful when using set commands in rendering config and pushig it with plugin, it can have unexpected side effects. + ## Substitutes If you render not full configuration, it is acceptable to pull missing config sections from the actual configuration to render full configuration. From 1ca2303ef810b83155e8c08c025baa82e0ad4242 Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Thu, 11 Apr 2024 17:01:55 +0400 Subject: [PATCH 12/13] Version 2.3.0 --- README.md | 2 +- netbox_config_diff/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0d7795c..dc1ebe2 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ This is possible thanks to the scrapli_cfg. Read [Scrapli](https://github.com/sc | NetBox Version | Plugin Version | |----------------|----------------| -| 3.5, 3.6 | =>0.1.0 | +| 3.5, 3.6, 3.7 | =>0.1.0 | ## Installing diff --git a/netbox_config_diff/__init__.py b/netbox_config_diff/__init__.py index f687423..b4d2835 100644 --- a/netbox_config_diff/__init__.py +++ b/netbox_config_diff/__init__.py @@ -2,7 +2,7 @@ __author__ = "Artem Kotik" __email__ = "miaow2@yandex.ru" -__version__ = "2.2.0" +__version__ = "2.3.0" class ConfigDiffConfig(PluginConfig): From 7d52fa322e02aed32861edc4d56e3b13b23a0f0d Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Sun, 12 May 2024 23:37:58 +0800 Subject: [PATCH 13/13] Closes #63: Add patch commands for configuration --- .github/workflows/commit.yaml | 2 +- docs/colliecting-diffs.md | 47 +++++++++--- docs/media/screenshots/compliance-patch.png | Bin 0 -> 26518 bytes netbox_config_diff/api/serializers.py | 1 + netbox_config_diff/compliance/base.py | 5 +- netbox_config_diff/compliance/utils.py | 17 +++++ netbox_config_diff/configurator/base.py | 5 +- .../migrations/0009_configcompliance_patch.py | 16 ++++ netbox_config_diff/models/data_models.py | 2 + netbox_config_diff/models/models.py | 3 + .../configcompliance/config.html | 3 +- .../configcompliance/missing_extra.html | 32 +------- .../configcompliance/patch.html | 11 +++ .../netbox_config_diff/inc/commands_card.html | 16 ++++ netbox_config_diff/views/base.py | 41 ++++++++++- netbox_config_diff/views/compliance.py | 69 ++++++++---------- requirements/base.txt | 1 + tests/test_compliance.py | 1 + 18 files changed, 185 insertions(+), 87 deletions(-) create mode 100644 docs/media/screenshots/compliance-patch.png create mode 100644 netbox_config_diff/migrations/0009_configcompliance_patch.py create mode 100644 netbox_config_diff/templates/netbox_config_diff/configcompliance/patch.html create mode 100644 netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 23b3ca3..6a12548 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -31,7 +31,7 @@ jobs: strategy: max-parallel: 10 matrix: - netbox_version: ["v3.5.9", "v3.6.9", "v3.7.5"] + netbox_version: ["v3.5.9", "v3.6.9", "v3.7.8"] steps: - name: Checkout uses: actions/checkout@v3 diff --git a/docs/colliecting-diffs.md b/docs/colliecting-diffs.md index b5cd95f..565c0bf 100644 --- a/docs/colliecting-diffs.md +++ b/docs/colliecting-diffs.md @@ -63,6 +63,41 @@ After script is done you can find results in `Config Compliances` menu. Each dev Also result is storing rendered and actual configurations from devices. +Compliance finished with error + +![Screenshot of the compliance error](media/screenshots/compliance-error.png) + +Render diff between configurations + +![Screenshot of diff](media/screenshots/compliance-diff.png) + +No diff + +![Screenshot of the compliance ok](media/screenshots/compliance-ok.png) + +### Patch commands + +With [hier_config](https://github.com/netdevops/hier_config) library you are able to take a actual configuration of a network device, compare it to its rendered configuration, + and build the remediation steps necessary to bring a device into spec with its intended configuration. + +![Screenshot of the patch commands](media/screenshots/compliance-patch.png) + +Supported platforms: + +* Arista EOS (arista_eos) +* Cisco IOS-XE (cisco_iosxe) +* Cisco IOS-XR (cisco_iosxr) +* Cisco NX-OS (cisco_nxos) + +However, any NOS that utilizes a CLI syntax that is structured in a similar fasion to IOS should work mostly out of the box. + +NOS's that utilize a `set` based CLI syntax has been added as experimental functionality: + +* Juniper JunOS (juniper_junos) +* VyOS (vyos_vyos) + +### Missing/extra + With the help of [netutils](https://github.com/networktocode/netutils) library plugin stores missing and extra config lines. ![Screenshot of the missing/extra lines](media/screenshots/compliance-missing-extra.png) @@ -81,15 +116,3 @@ Supported platforms for missing/extra lines: * Nokia SROS (nokia_sros) * PaloAlto PanOS (paloalto_panos) * Ruckus FastIron (ruckus_fastiron) - -Compliance finished with error - -![Screenshot of the compliance error](media/screenshots/compliance-error.png) - -Render diff between configurations - -![Screenshot of diff](media/screenshots/compliance-diff.png) - -No diff - -![Screenshot of the compliance ok](media/screenshots/compliance-ok.png) diff --git a/docs/media/screenshots/compliance-patch.png b/docs/media/screenshots/compliance-patch.png new file mode 100644 index 0000000000000000000000000000000000000000..b70509e99f0cd94be2e5ffe2170e7ba04a2101c8 GIT binary patch literal 26518 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 PFKDRUSFKQf^!mR5U4O<6 literal 0 HcmV?d00001 diff --git a/netbox_config_diff/api/serializers.py b/netbox_config_diff/api/serializers.py index c636a28..9b3a12d 100644 --- a/netbox_config_diff/api/serializers.py +++ b/netbox_config_diff/api/serializers.py @@ -29,6 +29,7 @@ class Meta: "diff", "rendered_config", "actual_config", + "patch", "missing", "extra", "created", diff --git a/netbox_config_diff/compliance/base.py b/netbox_config_diff/compliance/base.py index fb9702a..48a945c 100644 --- a/netbox_config_diff/compliance/base.py +++ b/netbox_config_diff/compliance/base.py @@ -18,7 +18,7 @@ from netbox_config_diff.models import ConplianceDeviceDataClass from .secrets import SecretsMixin -from .utils import PLATFORM_MAPPING, CustomChoiceVar, exclude_lines, get_unified_diff +from .utils import PLATFORM_MAPPING, CustomChoiceVar, exclude_lines, get_remediation_commands, get_unified_diff class ConfigDiffBase(SecretsMixin): @@ -204,3 +204,6 @@ def get_diff(self, devices: list[ConplianceDeviceDataClass]) -> None: device.extra = diff_network_config( cleaned_config, device.rendered_config, PLATFORM_MAPPING[device.platform] ) + device.patch = get_remediation_commands( + device.name, device.platform, cleaned_config, device.rendered_config + ) diff --git a/netbox_config_diff/compliance/utils.py b/netbox_config_diff/compliance/utils.py index 1da8fc7..7451a71 100644 --- a/netbox_config_diff/compliance/utils.py +++ b/netbox_config_diff/compliance/utils.py @@ -3,6 +3,7 @@ from django.forms import ChoiceField from extras.scripts import ScriptVariable +from hier_config import Host PLATFORM_MAPPING = { "arista_eos": "arista_eos", @@ -19,6 +20,15 @@ "ruckus_fastiron": "ruckus_fastiron", } +REMEDIATION_MAPPING = { + "arista_eos": "eos", + "cisco_iosxe": "ios", + "cisco_iosxr": "iosxr", + "cisco_nxos": "nxos", + "juniper_junos": "junos", + "vyos_vyos": "vyos", +} + class CustomChoiceVar(ScriptVariable): form_field = ChoiceField @@ -43,3 +53,10 @@ def exclude_lines(text: str, regexs: list) -> str: for item in regexs: text = re.sub(item, "", text, flags=re.I | re.M) return text.strip() + + +def get_remediation_commands(name: str, platform: str, actual_config: str, rendered_config: str) -> str: + host = Host(hostname=name, os=REMEDIATION_MAPPING.get(platform, "ios")) + host.load_running_config(config_text=actual_config) + host.load_generated_config(config_text=rendered_config) + return host.remediation_config_filtered_text(include_tags={}, exclude_tags={}) diff --git a/netbox_config_diff/configurator/base.py b/netbox_config_diff/configurator/base.py index 38bc94b..21c81df 100644 --- a/netbox_config_diff/configurator/base.py +++ b/netbox_config_diff/configurator/base.py @@ -14,7 +14,7 @@ from utilities.utils import NetBoxFakeRequest from netbox_config_diff.compliance.secrets import SecretsMixin -from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_unified_diff +from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_remediation_commands, get_unified_diff from netbox_config_diff.configurator.exceptions import DeviceConfigurationError, DeviceValidationError from netbox_config_diff.configurator.utils import CustomLogger from netbox_config_diff.constants import ACCEPTABLE_DRIVERS @@ -137,6 +137,9 @@ async def _collect_one_diff(self, device: ConfiguratorDeviceDataClass) -> None: device.extra = diff_network_config( device.actual_config, device.rendered_config, PLATFORM_MAPPING[device.platform] ) + device.patch = get_remediation_commands( + device.name, device.platform, device.actual_config, device.rendered_config + ) self.logger.log_info(f"Got diff from {device.name}") except Exception: error = traceback.format_exc() diff --git a/netbox_config_diff/migrations/0009_configcompliance_patch.py b/netbox_config_diff/migrations/0009_configcompliance_patch.py new file mode 100644 index 0000000..5ad5620 --- /dev/null +++ b/netbox_config_diff/migrations/0009_configcompliance_patch.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("netbox_config_diff", "0008_alter_configcompliance_device"), + ] + + operations = [ + migrations.AddField( + model_name="configcompliance", + name="patch", + field=models.TextField(blank=True), + ), + ] diff --git a/netbox_config_diff/models/data_models.py b/netbox_config_diff/models/data_models.py index 1567bbc..99539a4 100644 --- a/netbox_config_diff/models/data_models.py +++ b/netbox_config_diff/models/data_models.py @@ -23,6 +23,7 @@ class BaseDeviceDataClass: diff: str = "" missing: str | None = None extra: str | None = None + patch: str | None = None error: str = "" config_error: str | None = None auth_strict_key: bool = False @@ -99,6 +100,7 @@ def to_db(self) -> dict: "actual_config": self.actual_config or "", "missing": self.missing or "", "extra": self.extra or "", + "patch": self.patch or "", } def send_to_db(self) -> None: diff --git a/netbox_config_diff/models/models.py b/netbox_config_diff/models/models.py index 10b0f73..8fd7493 100644 --- a/netbox_config_diff/models/models.py +++ b/netbox_config_diff/models/models.py @@ -49,6 +49,9 @@ class ConfigCompliance(AbsoluteURLMixin, ChangeLoggingMixin, models.Model): extra = models.TextField( blank=True, ) + patch = models.TextField( + blank=True, + ) objects = RestrictedQuerySet.as_manager() diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html index ae2c864..874799a 100644 --- a/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html +++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html @@ -8,6 +8,7 @@
+ {% copy_content config_field %} Download @@ -15,7 +16,7 @@
{{ header }}
{% if config %} -
{{ config }}
+
{{ config }}
{% else %}
No configuration
{% endif %} diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html index de94dd9..5de7f9e 100644 --- a/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html +++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html @@ -5,38 +5,10 @@ {% block content %}
-
-
- -
Missing
-
- {% if object.missing %} -
{{ object.missing }}
- {% else %} -
No lines
- {% endif %} -
+ {% include 'netbox_config_diff/inc/commands_card.html' with data=object.missing header='Missing' pre_id='missing' %}
-
-
- -
Extra
-
- {% if object.extra %} -
{{ object.extra }}
- {% else %} -
No lines
- {% endif %} -
+ {% include 'netbox_config_diff/inc/commands_card.html' with data=object.extra header='Extra' pre_id='extra' %}
{% endblock %} diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/patch.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/patch.html new file mode 100644 index 0000000..ac77fe7 --- /dev/null +++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance/patch.html @@ -0,0 +1,11 @@ +{% extends "netbox_config_diff/configcompliance.html" %} + +{% block title %}{{ object }} - Patch commands{% endblock %} + +{% block content %} +
+
+ {% include 'netbox_config_diff/inc/commands_card.html' with data=object.patch header='Patch commands' pre_id='patch' %} +
+
+{% endblock %} diff --git a/netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html b/netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html new file mode 100644 index 0000000..dae39c8 --- /dev/null +++ b/netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html @@ -0,0 +1,16 @@ +
+
+
+ {% copy_content pre_id %} + + Download + +
+
{{ header }}
+
+ {% if data %} +
{{ data }}
+ {% else %} +
No commands
+ {% endif %} +
diff --git a/netbox_config_diff/views/base.py b/netbox_config_diff/views/base.py index 43cc8de..b01a7b8 100644 --- a/netbox_config_diff/views/base.py +++ b/netbox_config_diff/views/base.py @@ -1,5 +1,7 @@ +from django.http import HttpResponse +from django.shortcuts import render from django.urls import reverse -from netbox.views.generic import ObjectDeleteView, ObjectEditView +from netbox.views.generic import ObjectDeleteView, ObjectEditView, ObjectView class BaseObjectDeleteView(ObjectDeleteView): @@ -11,3 +13,40 @@ class BaseObjectEditView(ObjectEditView): @property def default_return_url(self) -> str: return f"plugins:netbox_config_diff:{self.queryset.model._meta.model_name}_list" + + +class BaseExportView(ObjectView): + def export_parts(self, name, lines, suffix): + response = HttpResponse(lines, content_type="text") + filename = f"{name}_{suffix}.txt" + response["Content-Disposition"] = f'attachment; filename="{filename}"' + return response + + +class BaseConfigComplianceConfigView(BaseExportView): + config_field = None + template_header = None + + def get(self, request, **kwargs): + instance = self.get_object(**kwargs) + context = self.get_extra_context(request, instance) + + if request.GET.get("export"): + return self.export_parts(instance.device.name, context["config"], self.config_field) + + return render( + request, + self.get_template_name(), + { + "object": instance, + "tab": self.tab, + **context, + }, + ) + + def get_extra_context(self, request, instance): + return { + "header": self.template_header, + "config": getattr(instance, self.config_field), + "config_field": self.config_field, + } diff --git a/netbox_config_diff/views/compliance.py b/netbox_config_diff/views/compliance.py index 2a4d49c..a5b0d5b 100644 --- a/netbox_config_diff/views/compliance.py +++ b/netbox_config_diff/views/compliance.py @@ -1,5 +1,4 @@ from dcim.models import Device -from django.http import HttpResponse from django.shortcuts import redirect, render from django.utils.translation import gettext as _ from netbox.views import generic @@ -15,38 +14,7 @@ from netbox_config_diff.models import ConfigCompliance, PlatformSetting from netbox_config_diff.tables import ConfigComplianceTable, PlatformSettingTable -from .base import BaseObjectDeleteView, BaseObjectEditView - - -class BaseConfigComplianceConfigView(generic.ObjectView): - config_field = None - template_header = None - - def get(self, request, **kwargs): - instance = self.get_object(**kwargs) - context = self.get_extra_context(request, instance) - - if request.GET.get("export"): - response = HttpResponse(context["config"], content_type="text") - filename = f"{instance.device.name}_{self.config_field}.txt" - response["Content-Disposition"] = f'attachment; filename="{filename}"' - return response - - return render( - request, - self.get_template_name(), - { - "object": instance, - "tab": self.tab, - **context, - }, - ) - - def get_extra_context(self, request, instance): - return { - "header": self.template_header, - "config": getattr(instance, self.config_field), - } +from .base import BaseConfigComplianceConfigView, BaseExportView, BaseObjectDeleteView, BaseObjectEditView @register_model_view(ConfigCompliance) @@ -87,7 +55,7 @@ class ConfigComplianceActualConfigView(BaseConfigComplianceConfigView): @register_model_view(ConfigCompliance, "missing-extra") -class ConfigComplianceMissingExtraConfigView(generic.ObjectView): +class ConfigComplianceMissingExtraConfigView(BaseExportView): queryset = ConfigCompliance.objects.all() template_name = "netbox_config_diff/configcompliance/missing_extra.html" tab = ViewTab( @@ -95,12 +63,6 @@ class ConfigComplianceMissingExtraConfigView(generic.ObjectView): weight=520, ) - def export_parts(self, name, lines, suffix): - response = HttpResponse(lines, content_type="text") - filename = f"{name}_{suffix}.txt" - response["Content-Disposition"] = f'attachment; filename="{filename}"' - return response - def get(self, request, **kwargs): instance = self.get_object(**kwargs) context = self.get_extra_context(request, instance) @@ -122,6 +84,33 @@ def get(self, request, **kwargs): ) +@register_model_view(ConfigCompliance, "patch") +class ConfigCompliancePatchView(BaseExportView): + queryset = ConfigCompliance.objects.all() + template_name = "netbox_config_diff/configcompliance/patch.html" + tab = ViewTab( + label=_("Patch"), + weight=515, + ) + + def get(self, request, **kwargs): + instance = self.get_object(**kwargs) + context = self.get_extra_context(request, instance) + + if request.GET.get("export_patch"): + return self.export_parts(instance.device.name, instance.patch, "patch") + + return render( + request, + self.get_template_name(), + { + "object": instance, + "tab": self.tab, + **context, + }, + ) + + @register_model_view(Device, "config_compliance", "config-compliance") class ConfigComplianceDeviceView(generic.ObjectView): queryset = Device.objects.all() diff --git a/requirements/base.txt b/requirements/base.txt index 80304ec..770e18b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,3 +1,4 @@ +hier-config==2.2.3 netutils==1.5.0 scrapli[asyncssh]==2023.07.30 scrapli-cfg==2023.07.30 diff --git a/tests/test_compliance.py b/tests/test_compliance.py index 04f2ff8..fdbc376 100644 --- a/tests/test_compliance.py +++ b/tests/test_compliance.py @@ -155,4 +155,5 @@ def test_devicedataclass_to_db( "actual_config": "", "missing": "", "extra": "", + "patch": "", }