Skip to content

Commit

Permalink
Merge branch 'develop' into gc-app-settings
Browse files Browse the repository at this point in the history
  • Loading branch information
jtdub authored Dec 13, 2024
2 parents a3d7a71 + f203463 commit 4d97ec8
Show file tree
Hide file tree
Showing 15 changed files with 159 additions and 80 deletions.
1 change: 1 addition & 0 deletions changes/844.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a diff output to the "Generate Intended Config" view and associated REST API.
Binary file modified docs/images/generate-intended-config-ui-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/generate-intended-config-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion docs/user/app_feature_intended.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ curl -s -X GET \
http://nautobot/api/plugins/golden-config/generate-intended-config/?device_id=231b8765-054d-4abe-bdbf-cd60e049cd8d
```

The returned response will contain the rendered configuration for the specified device and the GraphQL data that was used. The web UI provides a simple form to input the device and displays the rendered configuration when submitted.
The returned response will contain the rendered configuration for the specified device, the GraphQL data that was used, and if applicable, a diff of the most recent intended config that was generated by the **Intended Configuration** job. The web UI provides a simple form to interact with this REST API. You can access the web UI by clicking on "Generate Intended Config" in the "Tools" section of the Golden Config navigation menu.

For more advanced use cases, the REST API and web UI also accept a `graphql_query_id` parameter to specify a custom GraphQL query to use when rendering the configuration. If a `graphql_query_id` is not provided, the default query configured in the Device's Golden Config settings will be used.

![Intended Configuration Web UI](../images/generate-intended-config-ui.png#only-light)
![Intended Configuration Web UI](../images/generate-intended-config-ui-dark.png#only-dark)
Expand Down
2 changes: 2 additions & 0 deletions nautobot_golden_config/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,5 @@ class GenerateIntendedConfigSerializer(serializers.Serializer): # pylint: disab
intended_config = serializers.CharField(read_only=True)
intended_config_lines = serializers.ListField(read_only=True, child=serializers.CharField())
graphql_data = serializers.JSONField(read_only=True)
diff = serializers.CharField(read_only=True)
diff_lines = serializers.ListField(read_only=True, child=serializers.CharField())
26 changes: 26 additions & 0 deletions nautobot_golden_config/api/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""View for Golden Config APIs."""

import datetime
import difflib
import json
import logging
from pathlib import Path
Expand Down Expand Up @@ -203,6 +204,26 @@ class GenerateIntendedConfigView(NautobotAPIVersionMixin, GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = serializers.GenerateIntendedConfigSerializer

def _get_diff(self, device, intended_config):
"""Generate a unified diff between the provided config and the intended config stored on the Device's GoldenConfig.intended_config."""
diff = None
try:
golden_config = device.goldenconfig
if golden_config.intended_last_success_date is not None:
prior_intended_config = golden_config.intended_config
diff = "".join(
difflib.unified_diff(
prior_intended_config.splitlines(keepends=True),
intended_config.splitlines(keepends=True),
fromfile="prior intended config",
tofile="rendered config",
)
)
except models.GoldenConfig.DoesNotExist:
pass

return diff

def _get_object(self, request, model, query_param):
"""Get the requested model instance, restricted to requesting user."""
pk = request.query_params.get(query_param)
Expand Down Expand Up @@ -287,11 +308,16 @@ def get(self, request, *args, **kwargs):
)
except Exception as exc:
raise GenerateIntendedConfigException(f"Error rendering Jinja template: {exc}") from exc

diff = self._get_diff(device, intended_config)

return Response(
data={
"intended_config": intended_config,
"intended_config_lines": intended_config.split("\n"),
"graphql_data": graphql_data,
"diff": diff,
"diff_lines": diff.split("\n") if diff else [],
},
status=status.HTTP_200_OK,
)
Expand Down
2 changes: 1 addition & 1 deletion nautobot_golden_config/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
{% if record.config_type == 'json' %}
<i class="mdi mdi-circle-small"></i>
{% else %}
<a href="{% url 'extras:job_run_by_class_path' class_path='nautobot_golden_config.jobs.AllGoldenConfig' %}?device={{ record.device.pk }}"
<a href="{% url 'extras:job_run_by_class_path' class_path='nautobot_golden_config.jobs.AllGoldenConfig' %}?device={{ record.device.pk }}">
<span class="text-primary">
<i class="mdi mdi-play-circle" title="Execute All Golden Config Jobs"></i>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

{% block title %} {{ object }} - Config Compliance {% endblock %}

{% block content %}
{% block extra_styles %}
{{ block.super }}
<style>
#compliance-content pre {
font-size: 10px;
Expand Down Expand Up @@ -56,12 +57,15 @@
transform: scale(1.1);
}
</style>
{% endblock extra_styles %}

{% block content %}
{% block navigation %}
<div class="panel panel-default" style="width:100%">
<div class="panel-heading"><strong>Feature Navigation</strong>
<a href="{% url 'plugins:nautobot_golden_config:configcompliance_devicetab' pk=device.pk %}?tab={{ active_tab }}&compliance=compliant" class="btn btn-success">Compliant</a>
<a href="{% url 'plugins:nautobot_golden_config:configcompliance_devicetab' pk=device.pk %}?tab={{ active_tab }}&compliance=non-compliant" class="btn btn-danger">Non-Compliant</a>
<a href="{% url 'plugins:nautobot_golden_config:configcompliance_devicetab' pk=device.pk %}?tab={{ active_tab }}" class="btn btn-info">Clear</a>
<div class="panel-heading"><strong>Feature Navigation</strong>
<a href="{% url 'plugins:nautobot_golden_config:configcompliance_devicetab' pk=device.pk %}?tab={{ active_tab }}&compliance=compliant" class="btn btn-success">Compliant</a>
<a href="{% url 'plugins:nautobot_golden_config:configcompliance_devicetab' pk=device.pk %}?tab={{ active_tab }}&compliance=non-compliant" class="btn btn-danger">Non-Compliant</a>
<a href="{% url 'plugins:nautobot_golden_config:configcompliance_devicetab' pk=device.pk %}?tab={{ active_tab }}" class="btn btn-info">Clear</a>
</div>
<div id="navigation">
<table>
Expand Down Expand Up @@ -203,4 +207,4 @@
</table>
</div>
{% endfor %}
{% endblock %}
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{% block title %}Overview Reports{% endblock %}

{% block breadcrumbs %}
<li><a href="{% url 'plugins:nautobot_golden_config:configcompliance_overview' %}">Overview Reports</a></li>
<li><a href="{% url 'plugins:nautobot_golden_config:configcompliance_overview' %}">Overview Reports</a></li>
{% block extra_breadcrumbs %}{% endblock extra_breadcrumbs %}
{% endblock breadcrumbs %}

Expand Down Expand Up @@ -74,5 +74,6 @@ <h3 class="text-center m-2 p-3">Feature Summary</h3>
{% table_config_form table table_name="ObjectTable" %}
{% endblock %}
{% block javascript %}
{{ block.super }}
<script src="{% static 'js/tableconfig.js' %}"></script>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@
{% load helpers %}
{% load static %}

{% block content_left_page %}
<script src="{% static 'clipboard.js-2.0.9/clipboard.min.js' %}"></script>
<script>
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function (e) {});
clipboard.on('error', function (e) {});
</script>
{% block extra_styles %}
<style>
#config-output pre {
white-space: pre-wrap; /* CSS 2.1 */
Expand All @@ -18,6 +12,9 @@
word-wrap: break-word; /* IE >= 5.5 */
}
</style>
{% endblock extra_styles %}

{% block content_left_page %}
<div id="config-output" class="panel panel-default">
<div class="panel-heading">
<strong>Details</strong>
Expand Down Expand Up @@ -90,4 +87,13 @@
</div>
{% include 'inc/custom_fields_panel.html' %}
{% include 'inc/relationships_panel.html' %}
{% endblock %}
{% endblock %}

{% block javascript %}
{{ block.super }}
<script>
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function (e) {});
clipboard.on('error', function (e) {});
</script>
{% endblock javascript %}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
{% endblock %}

{% block javascript %}
{{ block.super }}
<script src="{% static 'toggle_fields.js' %}"></script>
<script src="{% static 'run_job.js' %}"></script>
<script>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{% extends 'generic/object_detail.html' %}
{% load helpers %}

{% block content_left_page %}
<style>
{% block extra_styles %}
<style>
.table-wrapper{
display: block;
overflow-x: auto;
white-space: nowrap;
width: 100%;
}
</style>
{% endblock extra_styles %}

{% block content_left_page %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Config Plan Details</strong>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@
{% load static %}

{% block extra_styles %}
<link rel="stylesheet" type="text/css" href="{% static 'nautobot_golden_config/diff2html-3.4.43/diff2html.min.css' %}"/>
<style type="text/css">
.d2h-tag {
display: none;
}
.d2h-file-name {
visibility: hidden;
}
.d2h-moved {
visibility: hidden;
}
.d2h-file-list-title {
display: none;
}
.button-container {
margin-bottom: 24px;
}
Expand Down Expand Up @@ -71,6 +84,7 @@
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active" id="id_intended_config_tab"><a href="#id_intended_config_tab_content" aria-controls="id_rendered_config_tabs" role="tab" data-toggle="tab">Configuration</a></li>
<li role="presentation" class="disabled" id="id_graphql_data_tab"><a href="#id_graphql_data_tab_content" aria-controls="id_graphql_data_tab_content" role="tab" data-toggle="tab">GraphQL Data</a></li>
<li role="presentation" class="disabled" id="id_diff_tab"><a href="#id_diff_tab_content" aria-controls="id_diff_tab_content" role="tab" data-toggle="tab">Diff</a></li>
</ul>
<div class="tab-content" id="id_rendered_config_tabs">
<div class="tab-pane active" id="id_intended_config_tab_content">
Expand All @@ -83,6 +97,9 @@
<div class="tab-pane" id="id_graphql_data_tab_content">
<pre><code class="language-json" id="id_graphql_data_code_block" contenteditable="true" onbeforeinput="return false"></code></pre>
</div>
<div class="tab-pane" id="id_diff_tab_content">
<div id="id_diff_render"></div>
</div>
</div>
</div>
</div>
Expand All @@ -93,6 +110,7 @@

{% block javascript %}
{{ block.super }}
<script type="text/javascript" src="{% static 'nautobot_golden_config/diff2html-3.4.43/diff2html.min.js' %}"></script>
<script>
// When a device is selected, populate the GraphQL query field with the default query for that device
async function handleDeviceFieldSelect(event) {
Expand Down Expand Up @@ -139,20 +157,24 @@
async function handleFormSubmit(event) {
event.preventDefault(); // Prevent default form submission

try {
const rendered_config_code_block = document.getElementById("id_rendered_config_code_block");
const device_id = document.getElementById("id_device").value;
const graphql_query_id = document.getElementById("id_graphql_query").value;
const url = "{% url 'plugins-api:nautobot_golden_config-api:generate_intended_config' %}";
const graphql_data_code_block = document.getElementById("id_graphql_data_code_block");
const graphql_data_tab = document.getElementById("id_graphql_data_tab");
const rendered_config_code_block = document.getElementById("id_rendered_config_code_block");
const device_id = document.getElementById("id_device").value;
const graphql_query_id = document.getElementById("id_graphql_query").value;
const url = "{% url 'plugins-api:nautobot_golden_config-api:generate_intended_config' %}";
const graphql_data_tab = document.getElementById("id_graphql_data_tab");
const graphql_data_code_block = document.getElementById("id_graphql_data_code_block");
const diff_tab = document.getElementById("id_diff_tab");
const diff_render_div = document.getElementById("id_diff_render");

try {
rendered_config_code_block.innerHTML = "Loading...";

// switch to the intended config tab and disable the graphql data tab
// switch to the intended config tab and disable the graphql data and diff tabs
$("#id_intended_config_tab a").tab("show");
graphql_data_tab.classList.add("disabled");
graphql_data_tab.classList.remove("active");
diff_tab.classList.add("disabled");
diff_tab.classList.remove("active");

// fetch the intended config
const data = {device_id: device_id, graphql_query_id: graphql_query_id};
Expand All @@ -174,8 +196,23 @@
delete graphql_data_code_block.dataset.highlighted;
hljs.highlightElement(graphql_data_code_block);

// enable the graphql data tab
id_graphql_data_tab.classList.remove("disabled");
// render the diff
if (responseData.diff == null) {
diff_render_div.innerHTML = "<p>No intended configuration available to diff against. You may need to run the intended configuration job first.</p>";
// output a message if no diff
} else if (responseData.diff === "") {
diff_render_div.innerHTML = "<p>No changes detected.</p>";
} else {
diff_render_div.innerHTML = Diff2Html.html(responseData.diff, {
drawFileList: true,
matching: "lines",
outputFormat: "line-by-line",
});
}

// enable the graphql data and diff tabs
graphql_data_tab.classList.remove("disabled");
diff_tab.classList.remove("disabled");
}
} catch (error) {
rendered_config_code_block.innerHTML = sanitize(`An error occurred:\n\n${error.message}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
<!-- Reference https://cdn.jsdelivr.net/npm/diff2html/bundles/ and obtain files and version folder name -->
<link rel="stylesheet" type="text/css" href="{% static 'nautobot_golden_config/diff2html-3.4.43/diff2html.min.css' %}"/>
<script type="text/javascript" src="{% static 'nautobot_golden_config/diff2html-3.4.43/diff2html.min.js' %}"></script>
<script src="{% static 'clipboard.js-2.0.9/clipboard.min.js' %}"></script>
<script>
<script>
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function (e) {});
clipboard.on('error', function (e) {});
Expand All @@ -15,7 +14,10 @@
}
.d2h-file-collapse {
display:none;
}
}
#diffoutput {
display: none;
}
</style>
<h1>{{ title_name }} - {{ device_name }}
{% if format != 'diff' %}
Expand All @@ -32,36 +34,28 @@ <h1>{{ title_name }} - {{ device_name }}
{% if format in 'json,yaml' %}
<pre id="{{ title_name|slugify }}_{{ device_name|slugify }}">{{ output }}</pre>
{% elif format == 'diff' %}
<div id="diffoutput" value="{{ output }}"></div>
<div id="diffoutput">{{ output }}</div>
<div id="diffrender"></div>
{% else %}
<pre id="{{ title_name|slugify }}_{{ device_name|slugify }}">{{ output }}</pre>
{% endif %}
</div>
</div>
<script type="text/javascript">
var str_input = document.getElementById("diffoutput").getAttribute("value");
var isModal = "{{ is_modal }}";
if (isModal === "True") {
var diffHtml = Diff2Html.html(str_input, {
var is_modal = {{ is_modal|lower }};
async function renderDiff() {
const str_input = document.getElementById("diffoutput").innerText;
const outputFormat = is_modal ? "line-by-line" : "side-by-side";
const diffHtml = Diff2Html.html(str_input, {
drawFileList: true,
matching: 'lines',
outputFormat: 'line_by_line',
}
)
// TODO: Fixed, but didn't update querySelectorAll
let elements = document.querySelectorAll('#diffrender');
elements.forEach((element) => {
element.innerHTML = diffHtml;
});
} else {
document.addEventListener('DOMContentLoaded', () => {
var diffHtml = Diff2Html.html(str_input, {
drawFileList: true,
matching: 'lines',
outputFormat: 'side-by-side',
});
document.getElementById('diffrender').innerHTML = diffHtml;
outputFormat: outputFormat,
});
document.getElementById('diffrender').innerHTML = diffHtml;
}
if (is_modal) {
renderDiff();
} else {
document.addEventListener("DOMContentLoaded", renderDiff);
}
</script>
</script>
Loading

0 comments on commit 4d97ec8

Please sign in to comment.