Skip to content

Commit

Permalink
Merge pull request #837 from nautobot/Release-v2.2.1
Browse files Browse the repository at this point in the history
Release v2.2.1
  • Loading branch information
smk4664 authored Nov 28, 2024
2 parents 60ea5b0 + a7c17e1 commit 6f12a32
Show file tree
Hide file tree
Showing 15 changed files with 672 additions and 506 deletions.
11 changes: 11 additions & 0 deletions docs/admin/release_notes/version_2.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ This document describes all new features and changes in the release. The format
- Added Python 3.12 support.
- Added REST API endpoint for Jinja as first part of journey towards a jinja live editor.

## [v2.2.1 (2024-11-27)](https://github.com/nautobot/nautobot-app-golden-config/releases/tag/v2.2.1)

### Added

- [#827](https://github.com/nautobot/nautobot-app-golden-config/issues/827) - Added a web ui for Jinja template developers to render intended configurations.

### Fixed

- [#831](https://github.com/nautobot/nautobot-app-golden-config/issues/831) - Resolved issue with tests failing in Nautobot 2.3.11.
- [#835](https://github.com/nautobot/nautobot-app-golden-config/issues/835) - Resolved error when accessing the Golden Config Settings list view in Nautobot v2.3.11 and higher.

## [v2.2.0 (2024-11-04)](https://github.com/nautobot/nautobot-app-golden-config/releases/tag/v2.2.0)

### Added
Expand Down
Binary file added 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 added 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.
20 changes: 9 additions & 11 deletions docs/user/app_feature_intended.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,22 @@ In these examples, `/services.j2`, `/ntp.j2`, etc. could contain the actual Jinj

### Developing Intended Configuration Templates

To help developers create the Jinja2 templates for generating the intended configuration, the app provides a REST API at `/api/plugins/golden-config/generate-intended-config/`. This API accepts two query parameters: `device_id` and `git_repository_id`. It returns the rendered configuration for the specified device using the templates from the given Git repository. This feature allows developers to test their configuration templates using a custom `GitRepository` without running a full intended configuration job.
To help developers create the Jinja2 templates for generating a device's intended configuration, the app provides a REST API at `/api/plugins/golden-config/generate-intended-config/` and a simple web UI at `/plugins/golden-config/generate-intended-config/`. The REST API accepts a query parameter for `device_id` and returns the rendered configuration for the specified device using the templates from the device's golden config `jinja_repository` Git repository. This feature allows developers to test their configuration templates without running a full "intended configuration" job.

Here's an example of how to request the rendered configuration for a device:
Here's an example of how to request the rendered configuration for a device using the REST API:

```no-highlight
GET /api/plugins/golden-config/generate-intended-config/?device_id=231b8765-054d-4abe-bdbf-cd60e049cd8d&git_repository_id=82c051e0-d0a9-4008-948a-936a409c654a
curl -s -X GET \
-H "Accept: application/json" \
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. This is the intended workflow for developers:
The returned response will contain the rendered configuration for the specified device. The web UI provides a simple form to input the device and displays the rendered configuration when submitted.

- Create a new branch in the intended configuration repository.
- Modify the Jinja2 templates in that new branch.
- Add a new `GitRepository` in Nautobot that points to the new branch and sync the repository.
- NOTE: Do not select the "jinja templates" option under the "Provides" field when creating the `GitRepository`. Nautobot does not allow multiple `GitRepository` instances with an identical URL and "Provided Content". This API ignores the "Provided Content" field for this reason.
- Don't forget to associate credentials required to access the repository using the "Secrets Group" field.
- Use the API to render the configuration for a device, using the new `GitRepository`.
![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)

Calling this API endpoint automatically performs a `git pull`, retrieving the latest commit from the branch before rendering the template.
Calling this API endpoint automatically performs a `git pull`, retrieving the latest commit from the Jinja2 templates Git repository before rendering the template.

Note that this API is only intended to render Jinja2 templates and does not apply any [configuration post-processing](./app_feature_config_postprocessing.md).

Expand Down
4 changes: 2 additions & 2 deletions nautobot_golden_config/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,5 @@ class Meta:
class GenerateIntendedConfigSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""Serializer for GenerateIntendedConfigView."""

intended_config = serializers.CharField()
intended_config_lines = serializers.ListField(child=serializers.CharField())
intended_config = serializers.CharField(read_only=True)
intended_config_lines = serializers.ListField(read_only=True, child=serializers.CharField())
25 changes: 14 additions & 11 deletions nautobot_golden_config/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from nautobot.dcim.models import Device
from nautobot.extras.api.views import NautobotModelViewSet, NotesViewSetMixin
from nautobot.extras.datasources.git import ensure_git_repository
from nautobot.extras.models import GitRepository
from nautobot_plugin_nornir.constants import NORNIR_SETTINGS
from nornir import InitNornir
from nornir_nautobot.plugins.tasks.dispatcher import dispatcher
Expand Down Expand Up @@ -233,25 +232,21 @@ def _get_jinja_template_path(self, settings, device, git_repository):
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
),
OpenApiParameter(
name="git_repository_id",
required=True,
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
),
]
)
def get(self, request, *args, **kwargs):
"""Generate intended configuration for a Device with an arbitrary GitRepository."""
"""Generate intended configuration for a Device."""
device = self._get_object(request, Device, "device_id")
git_repository = self._get_object(request, GitRepository, "git_repository_id")
settings = models.GoldenConfigSetting.objects.get_for_device(device)
if not settings:
raise GenerateIntendedConfigException("No Golden Config settings found for this device")
if not settings.sot_agg_query:
raise GenerateIntendedConfigException("Golden Config settings sot_agg_query not set")
if not settings.jinja_repository:
raise GenerateIntendedConfigException("Golden Config settings jinja_repository not set")

try:
git_repository = settings.jinja_repository
ensure_git_repository(git_repository)
except Exception as exc:
raise GenerateIntendedConfigException("Error trying to sync git repository") from exc
Expand All @@ -268,7 +263,7 @@ def get(self, request, *args, **kwargs):
graphql_data=context,
)
except Exception as exc:
raise GenerateIntendedConfigException("Error rendering Jinja template") from exc
raise GenerateIntendedConfigException(f"Error rendering Jinja template: {exc}") from exc
return Response(
data={
"intended_config": intended_config,
Expand Down Expand Up @@ -315,4 +310,12 @@ def _render_config_nornir_serial(self, device, jinja_template, jinja_root_path,
"generate_config", device.platform.network_driver, logging.getLogger(dispatch_params.__module__)
),
)
return results[device.name][1][1][0].result["config"]
if results[device.name].failed:
if results[device.name].exception: # pylint: disable=no-else-raise
raise results[device.name].exception
else:
raise GenerateIntendedConfigException(
f"Error generating intended config for {device.name}: {results[device.name].result}"
)
else:
return results[device.name][1][1][0].result["config"]
10 changes: 10 additions & 0 deletions nautobot_golden_config/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,13 @@ class Meta:
"change_control_url",
"tags",
]


class GenerateIntendedConfigForm(django_forms.Form):
"""Form for generating intended configuration."""

device = forms.DynamicModelChoiceField(
queryset=Device.objects.all(),
required=True,
label="Device",
)
1 change: 1 addition & 0 deletions nautobot_golden_config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ class GoldenConfigSetting(PrimaryModel): # pylint: disable=too-many-ancestors
on_delete=models.PROTECT,
related_name="golden_config_setting",
)
is_dynamic_group_associable_model = False

objects = GoldenConfigSettingManager()

Expand Down
11 changes: 11 additions & 0 deletions nautobot_golden_config/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@
groups=(
NavMenuGroup(name="Manage", weight=100, items=tuple(items_operate)),
NavMenuGroup(name="Setup", weight=100, items=tuple(items_setup)),
NavMenuGroup(
name="Tools",
weight=300,
items=(
NavMenuItem(
link="plugins:nautobot_golden_config:generate_intended_config",
name="Generate Intended Config",
permissions=["dcim.view_device", "extras.view_gitrepository"],
),
),
),
),
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{% extends "base.html" %}
{% load form_helpers %}
{% load helpers %}
{% load static %}

{% block extra_styles %}
<style type="text/css">
.button-container {
margin-bottom: 24px;
}
</style>
{% endblock extra_styles %}

{% block content %}
<form class="form form-horizontal" onsubmit="handleFormSubmit(event)">
<div class="row">
<div class="col-lg-6 col-md-6">
<div class="panel panel-default">
<div class="panel-heading"><strong>{% block title %}Generate Intended Configuration{% endblock title %}</strong></div>
<div class="panel-body">
<p>
This tool is <strong>intended for template developers</strong>. Production configuration generation should be initiated from the
<a href="{% url 'plugins:nautobot_golden_config:goldenconfig_list' %}">Config Overview</a> page.
</p>
<p>
This will render the configuration for the selected device using Jinja templates from the golden config <code>jinja_repository</code>
Git repository for that device.
This feature allows developers to test their configuration templates without running a full "intended configuration" job. See the
<a href="{% static 'nautobot_golden_config/docs/user/app_feature_intended.html' %}#developing-intended-configuration-templates">
developing intended configuration templates
</a> documentation for more information.
</p>
<p>
<strong>Note:</strong>
This will perform a <code>git pull</code> on the golden config Jinja template repository to ensure the latest templates are used.
</p>
{% render_field form.device %}
{% render_field form.git_repository %}
</div>
</div>
<div class="button-container text-right">
<button type="submit" class="btn btn-primary">Render</button>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
<div class="col-lg-6 col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Intended Configuration</strong>
<button type="button" class="btn btn-inline btn-default copy-rendered-config" data-clipboard-target="#rendered_config">
<span class="mdi mdi-content-copy"></span>
</button>
</div>
<div class="panel-body">
<textarea readonly="readonly" cols="40" rows="10" class="form-control" placeholder="Rendered Config" id="rendered_config"></textarea>
</div>
</div>
</div>
</div>
</form>
{% endblock content %}

{% block javascript %}
{{ block.super }}
<script>
new ClipboardJS('.copy-rendered-config');
const sanitize = function(string) {
return string.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
};
async function handleFormSubmit(event) {
event.preventDefault(); // Prevent default form submission

try {
const rendered_config = document.getElementById("rendered_config");
rendered_config.innerHTML = "Loading...";
const device = document.getElementById("id_device").value;
const url = "{% url 'plugins-api:nautobot_golden_config-api:generate_intended_config' %}";
const data = {device_id: device};
const query_params = new URLSearchParams(data).toString();
const response = await fetch(url + "?" + query_params, {
method: "GET",
headers: {"Content-Type": "application/json"}
});
const responseData = await response.json();
if (!response.ok) {
const msg = responseData.detail ? responseData.detail : response.statusText;
rendered_config.innerHTML = sanitize(`An error occurred:\n\n${msg}`);
} else {
rendered_config.innerHTML = sanitize(responseData.intended_config);
}
} catch (error) {
rendered_config.innerHTML = sanitize(`An error occurred:\n\n${error.message}`);
}
}
</script>
{% endblock javascript %}
Loading

0 comments on commit 6f12a32

Please sign in to comment.