Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 2.4.0 #65

Merged
merged 16 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading