Skip to content

Commit 9a304e3

Browse files
committed
Add patch commands to compliance
1 parent 21704dd commit 9a304e3

File tree

15 files changed

+157
-75
lines changed

15 files changed

+157
-75
lines changed

.github/workflows/commit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
strategy:
3232
max-parallel: 10
3333
matrix:
34-
netbox_version: ["v3.5.9", "v3.6.9", "v3.7.5"]
34+
netbox_version: ["v3.5.9", "v3.6.9", "v3.7.8"]
3535
steps:
3636
- name: Checkout
3737
uses: actions/checkout@v3

netbox_config_diff/api/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Meta:
2929
"diff",
3030
"rendered_config",
3131
"actual_config",
32+
"patch",
3233
"missing",
3334
"extra",
3435
"created",

netbox_config_diff/compliance/base.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@
1818
from netbox_config_diff.models import ConplianceDeviceDataClass
1919

2020
from .secrets import SecretsMixin
21-
from .utils import PLATFORM_MAPPING, CustomChoiceVar, exclude_lines, get_unified_diff
21+
from .utils import (
22+
PLATFORM_MAPPING,
23+
REMEDIATION_MAPPING,
24+
CustomChoiceVar,
25+
exclude_lines,
26+
get_remediation_commands,
27+
get_unified_diff,
28+
)
2229

2330

2431
class ConfigDiffBase(SecretsMixin):
@@ -204,3 +211,7 @@ def get_diff(self, devices: list[ConplianceDeviceDataClass]) -> None:
204211
device.extra = diff_network_config(
205212
cleaned_config, device.rendered_config, PLATFORM_MAPPING[device.platform]
206213
)
214+
if device.platform in REMEDIATION_MAPPING:
215+
device.patch = get_remediation_commands(
216+
device.name, device.platform, cleaned_config, device.rendered_config
217+
)

netbox_config_diff/compliance/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.forms import ChoiceField
55
from extras.scripts import ScriptVariable
6+
from hier_config import Host
67

78
PLATFORM_MAPPING = {
89
"arista_eos": "arista_eos",
@@ -19,6 +20,15 @@
1920
"ruckus_fastiron": "ruckus_fastiron",
2021
}
2122

23+
REMEDIATION_MAPPING = {
24+
"arista_eos": "eos",
25+
"cisco_iosxe": "ios",
26+
"cisco_iosxr": "iosxr",
27+
"cisco_nxos": "nxos",
28+
"juniper_junos": "junos",
29+
"vyos_vyos": "vyos",
30+
}
31+
2232

2333
class CustomChoiceVar(ScriptVariable):
2434
form_field = ChoiceField
@@ -43,3 +53,10 @@ def exclude_lines(text: str, regexs: list) -> str:
4353
for item in regexs:
4454
text = re.sub(item, "", text, flags=re.I | re.M)
4555
return text.strip()
56+
57+
58+
def get_remediation_commands(name: str, platform: str, actual_config: str, rendered_config: str) -> str:
59+
host = Host(hostname=name, os=REMEDIATION_MAPPING.get(platform))
60+
host.load_running_config(config_text=actual_config)
61+
host.load_generated_config(config_text=rendered_config)
62+
return host.remediation_config_filtered_text(include_tags={}, exclude_tags={})

netbox_config_diff/configurator/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from utilities.utils import NetBoxFakeRequest
1515

1616
from netbox_config_diff.compliance.secrets import SecretsMixin
17-
from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_unified_diff
17+
from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_remediation_commands, get_unified_diff
1818
from netbox_config_diff.configurator.exceptions import DeviceConfigurationError, DeviceValidationError
1919
from netbox_config_diff.configurator.utils import CustomLogger
2020
from netbox_config_diff.constants import ACCEPTABLE_DRIVERS
@@ -137,6 +137,9 @@ async def _collect_one_diff(self, device: ConfiguratorDeviceDataClass) -> None:
137137
device.extra = diff_network_config(
138138
device.actual_config, device.rendered_config, PLATFORM_MAPPING[device.platform]
139139
)
140+
device.patch = get_remediation_commands(
141+
device.name, device.platform, device.actual_config, device.rendered_config
142+
)
140143
self.logger.log_info(f"Got diff from {device.name}")
141144
except Exception:
142145
error = traceback.format_exc()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.db import migrations, models
2+
3+
4+
class Migration(migrations.Migration):
5+
6+
dependencies = [
7+
("netbox_config_diff", "0008_alter_configcompliance_device"),
8+
]
9+
10+
operations = [
11+
migrations.AddField(
12+
model_name="configcompliance",
13+
name="patch",
14+
field=models.TextField(blank=True),
15+
),
16+
]

netbox_config_diff/models/data_models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class BaseDeviceDataClass:
2323
diff: str = ""
2424
missing: str | None = None
2525
extra: str | None = None
26+
patch: str | None = None
2627
error: str = ""
2728
config_error: str | None = None
2829
auth_strict_key: bool = False
@@ -99,6 +100,7 @@ def to_db(self) -> dict:
99100
"actual_config": self.actual_config or "",
100101
"missing": self.missing or "",
101102
"extra": self.extra or "",
103+
"patch": self.patch or "",
102104
}
103105

104106
def send_to_db(self) -> None:

netbox_config_diff/models/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class ConfigCompliance(AbsoluteURLMixin, ChangeLoggingMixin, models.Model):
4949
extra = models.TextField(
5050
blank=True,
5151
)
52+
patch = models.TextField(
53+
blank=True,
54+
)
5255

5356
objects = RestrictedQuerySet.as_manager()
5457

netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
<div class="card">
99
<div class="card-header">
1010
<div class="float-end">
11+
{% copy_content config_field %}
1112
<a href="?export=True" class="btn btn-sm btn-primary" role="button">
1213
<i class="mdi mdi-download" aria-hidden="true"></i> Download
1314
</a>
1415
</div>
1516
<h5>{{ header }}</h5>
1617
</div>
1718
{% if config %}
18-
<pre class="card-body">{{ config }}</pre>
19+
<pre class="card-body" id="{{ config_field }}">{{ config }}</pre>
1920
{% else %}
2021
<div class="card-body text-muted">No configuration</div>
2122
{% endif %}

netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,10 @@
55
{% block content %}
66
<div class="row">
77
<div class="col col-md-6">
8-
<div class="card">
9-
<div class="card-header">
10-
<div class="float-end">
11-
<a href="?export_missing=True" class="btn btn-sm btn-primary" role="button">
12-
<i class="mdi mdi-download" aria-hidden="true"></i> Download
13-
</a>
14-
</div>
15-
<h5>Missing</h5>
16-
</div>
17-
{% if object.missing %}
18-
<pre class="card-body">{{ object.missing }}</pre>
19-
{% else %}
20-
<div class="card-body text-muted">No lines</div>
21-
{% endif %}
22-
</div>
8+
{% include 'netbox_config_diff/inc/commands_card.html' with data=object.missing header='Missing' pre_id='missing' %}
239
</div>
2410
<div class="col col-md-6">
25-
<div class="card">
26-
<div class="card-header">
27-
<div class="float-end">
28-
<a href="?export_extra=True" class="btn btn-sm btn-primary" role="button">
29-
<i class="mdi mdi-download" aria-hidden="true"></i> Download
30-
</a>
31-
</div>
32-
<h5>Extra</h5>
33-
</div>
34-
{% if object.extra %}
35-
<pre class="card-body">{{ object.extra }}</pre>
36-
{% else %}
37-
<div class="card-body text-muted">No lines</div>
38-
{% endif %}
39-
</div>
11+
{% include 'netbox_config_diff/inc/commands_card.html' with data=object.extra header='Extra' pre_id='extra' %}
4012
</div>
4113
</div>
4214
{% endblock %}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{% extends "netbox_config_diff/configcompliance.html" %}
2+
3+
{% block title %}{{ object }} - Patch commands{% endblock %}
4+
5+
{% block content %}
6+
<div class="row">
7+
<div class="col col-md-6">
8+
{% include 'netbox_config_diff/inc/commands_card.html' with data=object.patch header='Patch commands' pre_id='patch' %}
9+
</div>
10+
</div>
11+
{% endblock %}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<div class="card">
2+
<div class="card-header">
3+
<div class="float-end">
4+
{% copy_content pre_id %}
5+
<a href="?export_{{ pre_id }}=True" class="btn btn-sm btn-primary" role="button">
6+
<i class="mdi mdi-download" aria-hidden="true"></i> Download
7+
</a>
8+
</div>
9+
<h5>{{ header }}</h5>
10+
</div>
11+
{% if data %}
12+
<pre class="card-body" id="{{ pre_id }}">{{ data }}</pre>
13+
{% else %}
14+
<div class="card-body text-muted">No commands</div>
15+
{% endif %}
16+
</div>

netbox_config_diff/views/base.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from django.http import HttpResponse
2+
from django.shortcuts import render
13
from django.urls import reverse
2-
from netbox.views.generic import ObjectDeleteView, ObjectEditView
4+
from netbox.views.generic import ObjectDeleteView, ObjectEditView, ObjectView
35

46

57
class BaseObjectDeleteView(ObjectDeleteView):
@@ -11,3 +13,40 @@ class BaseObjectEditView(ObjectEditView):
1113
@property
1214
def default_return_url(self) -> str:
1315
return f"plugins:netbox_config_diff:{self.queryset.model._meta.model_name}_list"
16+
17+
18+
class BaseExportView(ObjectView):
19+
def export_parts(self, name, lines, suffix):
20+
response = HttpResponse(lines, content_type="text")
21+
filename = f"{name}_{suffix}.txt"
22+
response["Content-Disposition"] = f'attachment; filename="{filename}"'
23+
return response
24+
25+
26+
class BaseConfigComplianceConfigView(BaseExportView):
27+
config_field = None
28+
template_header = None
29+
30+
def get(self, request, **kwargs):
31+
instance = self.get_object(**kwargs)
32+
context = self.get_extra_context(request, instance)
33+
34+
if request.GET.get("export"):
35+
return self.export_parts(instance.device.name, context["config"], self.config_field)
36+
37+
return render(
38+
request,
39+
self.get_template_name(),
40+
{
41+
"object": instance,
42+
"tab": self.tab,
43+
**context,
44+
},
45+
)
46+
47+
def get_extra_context(self, request, instance):
48+
return {
49+
"header": self.template_header,
50+
"config": getattr(instance, self.config_field),
51+
"config_field": self.config_field,
52+
}

netbox_config_diff/views/compliance.py

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from dcim.models import Device
2-
from django.http import HttpResponse
32
from django.shortcuts import redirect, render
43
from django.utils.translation import gettext as _
54
from netbox.views import generic
@@ -15,38 +14,7 @@
1514
from netbox_config_diff.models import ConfigCompliance, PlatformSetting
1615
from netbox_config_diff.tables import ConfigComplianceTable, PlatformSettingTable
1716

18-
from .base import BaseObjectDeleteView, BaseObjectEditView
19-
20-
21-
class BaseConfigComplianceConfigView(generic.ObjectView):
22-
config_field = None
23-
template_header = None
24-
25-
def get(self, request, **kwargs):
26-
instance = self.get_object(**kwargs)
27-
context = self.get_extra_context(request, instance)
28-
29-
if request.GET.get("export"):
30-
response = HttpResponse(context["config"], content_type="text")
31-
filename = f"{instance.device.name}_{self.config_field}.txt"
32-
response["Content-Disposition"] = f'attachment; filename="{filename}"'
33-
return response
34-
35-
return render(
36-
request,
37-
self.get_template_name(),
38-
{
39-
"object": instance,
40-
"tab": self.tab,
41-
**context,
42-
},
43-
)
44-
45-
def get_extra_context(self, request, instance):
46-
return {
47-
"header": self.template_header,
48-
"config": getattr(instance, self.config_field),
49-
}
17+
from .base import BaseConfigComplianceConfigView, BaseExportView, BaseObjectDeleteView, BaseObjectEditView
5018

5119

5220
@register_model_view(ConfigCompliance)
@@ -87,20 +55,14 @@ class ConfigComplianceActualConfigView(BaseConfigComplianceConfigView):
8755

8856

8957
@register_model_view(ConfigCompliance, "missing-extra")
90-
class ConfigComplianceMissingExtraConfigView(generic.ObjectView):
58+
class ConfigComplianceMissingExtraConfigView(BaseExportView):
9159
queryset = ConfigCompliance.objects.all()
9260
template_name = "netbox_config_diff/configcompliance/missing_extra.html"
9361
tab = ViewTab(
9462
label=_("Missing/Extra"),
9563
weight=520,
9664
)
9765

98-
def export_parts(self, name, lines, suffix):
99-
response = HttpResponse(lines, content_type="text")
100-
filename = f"{name}_{suffix}.txt"
101-
response["Content-Disposition"] = f'attachment; filename="{filename}"'
102-
return response
103-
10466
def get(self, request, **kwargs):
10567
instance = self.get_object(**kwargs)
10668
context = self.get_extra_context(request, instance)
@@ -122,6 +84,33 @@ def get(self, request, **kwargs):
12284
)
12385

12486

87+
@register_model_view(ConfigCompliance, "patch")
88+
class ConfigCompliancePatchView(BaseExportView):
89+
queryset = ConfigCompliance.objects.all()
90+
template_name = "netbox_config_diff/configcompliance/patch.html"
91+
tab = ViewTab(
92+
label=_("Patch"),
93+
weight=515,
94+
)
95+
96+
def get(self, request, **kwargs):
97+
instance = self.get_object(**kwargs)
98+
context = self.get_extra_context(request, instance)
99+
100+
if request.GET.get("export_patch"):
101+
return self.export_parts(instance.device.name, instance.patch, "patch")
102+
103+
return render(
104+
request,
105+
self.get_template_name(),
106+
{
107+
"object": instance,
108+
"tab": self.tab,
109+
**context,
110+
},
111+
)
112+
113+
125114
@register_model_view(Device, "config_compliance", "config-compliance")
126115
class ConfigComplianceDeviceView(generic.ObjectView):
127116
queryset = Device.objects.all()

requirements/base.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
hier-config==2.2.3
12
netutils==1.5.0
23
scrapli[asyncssh]==2023.07.30
34
scrapli-cfg==2023.07.30

0 commit comments

Comments
 (0)