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/README.md b/README.md
index 2955f8b..0d7795c 100644
--- a/README.md
+++ b/README.md
@@ -97,13 +97,19 @@ python manage.py collectstatic --noinput
Restart NetBox service:
```bash
-systemctl restart netbox
+systemctl restart netbox netbox-rq
```
## 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/)
+
+## 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
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/docs/colliecting-diffs.md b/docs/colliecting-diffs.md
index 54ddae6..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)
@@ -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/navbar.png b/docs/media/screenshots/navbar.png
index 167889f..7b0dba2 100644
Binary files a/docs/media/screenshots/navbar.png and b/docs/media/screenshots/navbar.png differ
diff --git a/docs/media/screenshots/script.png b/docs/media/screenshots/script.png
index b0fe676..498aeb6 100644
Binary files a/docs/media/screenshots/script.png and b/docs/media/screenshots/script.png differ
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):
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/migrations/0008_alter_configcompliance_device.py b/netbox_config_diff/migrations/0008_alter_configcompliance_device.py
new file mode 100644
index 0000000..1972edc
--- /dev/null
+++ b/netbox_config_diff/migrations/0008_alter_configcompliance_device.py
@@ -0,0 +1,21 @@
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("dcim", "0181_rename_device_role_device_role"),
+ ("netbox_config_diff", "0007_configurationrequest"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="configcompliance",
+ name="device",
+ field=models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="config_compliance",
+ to="dcim.device",
+ ),
+ ),
+ ]
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/netbox_config_diff/models/models.py b/netbox_config_diff/models/models.py
index 4bb0dfd..10b0f73 100644
--- a/netbox_config_diff/models/models.py
+++ b/netbox_config_diff/models/models.py
@@ -24,7 +24,7 @@ class ConfigCompliance(AbsoluteURLMixin, ChangeLoggingMixin, models.Model):
device = models.OneToOneField(
to="dcim.Device",
on_delete=models.CASCADE,
- related_name="config_compliamce",
+ related_name="config_compliance",
)
status = models.CharField(
max_length=50,
diff --git a/netbox_config_diff/navigation.py b/netbox_config_diff/navigation.py
index ddada5d..c52f406 100644
--- a/netbox_config_diff/navigation.py
+++ b/netbox_config_diff/navigation.py
@@ -1,4 +1,4 @@
-from extras.plugins import PluginMenuButton, PluginMenuItem
+from extras.plugins import PluginMenu, PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
@@ -12,35 +12,36 @@ def get_add_button(model: str) -> 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 %}
-
-
-
-
-
-
-
- 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 %}
-
- {% endif %}
- {% if object.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 %}
+
+
+
+
+
+
+
+ Device |
+ {{ instance.device|linkify }} |
+
+
+ Status |
+ {% badge instance.get_status_display bg_color=instance.get_status_color %} |
+
+
+
+
+
+ {% if instance.error %}
+
+ {% endif %}
+
+ {% if instance.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
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",
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