Skip to content

Commit

Permalink
Closes #63: Add patch commands for configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
miaow2 authored May 12, 2024
1 parent 21704dd commit 7d52fa3
Show file tree
Hide file tree
Showing 18 changed files with 185 additions and 87 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
47 changes: 35 additions & 12 deletions docs/colliecting-diffs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Binary file added docs/media/screenshots/compliance-patch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
5 changes: 4 additions & 1 deletion netbox_config_diff/compliance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
)
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, "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={})
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,
}
Loading

0 comments on commit 7d52fa3

Please sign in to comment.