Skip to content

Commit

Permalink
Add patch commands to compliance
Browse files Browse the repository at this point in the history
  • Loading branch information
miaow2 committed May 12, 2024
1 parent 21704dd commit 9a304e3
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions netbox_config_diff/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Meta:
"diff",
"rendered_config",
"actual_config",
"patch",
"missing",
"extra",
"created",
Expand Down
13 changes: 12 additions & 1 deletion netbox_config_diff/compliance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@
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,
REMEDIATION_MAPPING,
CustomChoiceVar,
exclude_lines,
get_remediation_commands,
get_unified_diff,
)


class ConfigDiffBase(SecretsMixin):
Expand Down Expand Up @@ -204,3 +211,7 @@ def get_diff(self, devices: list[ConplianceDeviceDataClass]) -> None:
device.extra = diff_network_config(
cleaned_config, device.rendered_config, PLATFORM_MAPPING[device.platform]
)
if device.platform in REMEDIATION_MAPPING:
device.patch = get_remediation_commands(
device.name, device.platform, cleaned_config, device.rendered_config
)
17 changes: 17 additions & 0 deletions netbox_config_diff/compliance/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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))
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={})
5 changes: 4 additions & 1 deletion netbox_config_diff/configurator/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
16 changes: 16 additions & 0 deletions netbox_config_diff/migrations/0009_configcompliance_patch.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
2 changes: 2 additions & 0 deletions netbox_config_diff/models/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions netbox_config_diff/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ class ConfigCompliance(AbsoluteURLMixin, ChangeLoggingMixin, models.Model):
extra = models.TextField(
blank=True,
)
patch = models.TextField(
blank=True,
)

objects = RestrictedQuerySet.as_manager()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
<div class="card">
<div class="card-header">
<div class="float-end">
{% copy_content config_field %}
<a href="?export=True" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> Download
</a>
</div>
<h5>{{ header }}</h5>
</div>
{% if config %}
<pre class="card-body">{{ config }}</pre>
<pre class="card-body" id="{{ config_field }}">{{ config }}</pre>
{% else %}
<div class="card-body text-muted">No configuration</div>
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,10 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
<div class="float-end">
<a href="?export_missing=True" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> Download
</a>
</div>
<h5>Missing</h5>
</div>
{% if object.missing %}
<pre class="card-body">{{ object.missing }}</pre>
{% else %}
<div class="card-body text-muted">No lines</div>
{% endif %}
</div>
{% include 'netbox_config_diff/inc/commands_card.html' with data=object.missing header='Missing' pre_id='missing' %}
</div>
<div class="col col-md-6">
<div class="card">
<div class="card-header">
<div class="float-end">
<a href="?export_extra=True" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> Download
</a>
</div>
<h5>Extra</h5>
</div>
{% if object.extra %}
<pre class="card-body">{{ object.extra }}</pre>
{% else %}
<div class="card-body text-muted">No lines</div>
{% endif %}
</div>
{% include 'netbox_config_diff/inc/commands_card.html' with data=object.extra header='Extra' pre_id='extra' %}
</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% extends "netbox_config_diff/configcompliance.html" %}

{% block title %}{{ object }} - Patch commands{% endblock %}

{% block content %}
<div class="row">
<div class="col col-md-6">
{% include 'netbox_config_diff/inc/commands_card.html' with data=object.patch header='Patch commands' pre_id='patch' %}
</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="card">
<div class="card-header">
<div class="float-end">
{% copy_content pre_id %}
<a href="?export_{{ pre_id }}=True" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> Download
</a>
</div>
<h5>{{ header }}</h5>
</div>
{% if data %}
<pre class="card-body" id="{{ pre_id }}">{{ data }}</pre>
{% else %}
<div class="card-body text-muted">No commands</div>
{% endif %}
</div>
41 changes: 40 additions & 1 deletion netbox_config_diff/views/base.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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,
}
69 changes: 29 additions & 40 deletions netbox_config_diff/views/compliance.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -87,20 +55,14 @@ 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(
label=_("Missing/Extra"),
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)
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
hier-config==2.2.3
netutils==1.5.0
scrapli[asyncssh]==2023.07.30
scrapli-cfg==2023.07.30
Expand Down

0 comments on commit 9a304e3

Please sign in to comment.