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

Added a web ui to render intended configurations #827

Merged
merged 7 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions changes/827.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a web ui for Jinja template developers to render intended configurations from templates in an arbitrary git repository.
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.
11 changes: 8 additions & 3 deletions docs/user/app_feature_intended.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,20 @@ 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 the 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 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.

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
```

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 Git repository and displays the rendered configuration when submitted.

![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)

This is the intended workflow for Jinja Template developers:

- Create a new branch in the intended configuration repository.
- Modify the Jinja2 templates in that new branch.
Expand Down
15 changes: 15 additions & 0 deletions nautobot_golden_config/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,18 @@ 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",
)
git_repository = forms.DynamicModelChoiceField(
queryset=GitRepository.objects.all(),
required=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud.. can we make this not required and if not set figure out what the default git repo is this device is?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the majority of cases, I don't think people will need to switch this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because different devices can have different golden config settings, and different golden config settings can have different git repositories, we would have no way of showing the user in this view what repo was used to generate the template.

Either:

  • They would have to back-solve which golden config setting applied and then which git repository
  • We will have to enhance the API response to provide more information than it does today

label="Git Repository",
)
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,92 @@
{% 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 renders the configuration for the specified device using the Jinja templates from the given Git repository.
This feature allows developers to test their configuration templates using a custom <code>GitRepository</code> 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>
{% render_field form.device %}
{% render_field form.git_repository %}
</div>
</div>
<div class="button-container text-right">
<button type="submit" class="btn btn-primary">Submit</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 git_repository = document.getElementById("id_git_repository").value;
const url = "{% url 'plugins-api:nautobot_golden_config-api:generate_intended_config' %}";
const data = {
device_id: device,
git_repository_id: git_repository,
};
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: ${msg}`);
} else {
rendered_config.innerHTML = sanitize(responseData.intended_config);
}
} catch (error) {
rendered_config.innerHTML = sanitize(`An error occurred: ${error.message}`);
}
}
</script>
{% endblock javascript %}
1 change: 1 addition & 0 deletions nautobot_golden_config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@
urlpatterns = [
path("config-compliance/overview/", views.ConfigComplianceOverview.as_view(), name="configcompliance_overview"),
path("config-plan/bulk_deploy/", views.ConfigPlanBulkDeploy.as_view(), name="configplan_bulk-deploy"),
path("generate-intended-config/", views.GenerateIntendedConfigView.as_view(), name="generate_intended_config"),
path("docs/", RedirectView.as_view(url=static("nautobot_golden_config/docs/index.html")), name="docs"),
] + router.urls
16 changes: 15 additions & 1 deletion nautobot_golden_config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@

import yaml
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count, ExpressionWrapper, F, FloatField, Max, Q
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.html import format_html
from django.utils.timezone import make_aware
from django.views.generic import View
from django.views.generic import TemplateView, View
from django_pivot.pivot import pivot
from nautobot.apps import views
from nautobot.core.views import generic
Expand Down Expand Up @@ -585,3 +586,16 @@ def post(self, request):
**job.job_class.serialize_data(request),
)
return redirect(job_result.get_absolute_url())


class GenerateIntendedConfigView(PermissionRequiredMixin, TemplateView):
"""View to generate the intended configuration."""

template_name = "nautobot_golden_config/generate_intended_config.html"
permission_required = ["dcim.view_device", "extras.view_gitrepository"]
Copy link
Contributor

@smk4664 smk4664 Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not also check permissions for GoldenConfig? If I am not mistaken, a user would need to have view permissions for Golden Config in order to see the Intended Configuration.

Suggested change
permission_required = ["dcim.view_device", "extras.view_gitrepository"]
permission_required = ["dcim.view_device", "extras.view_gitrepository", "nautobot_golden_config.view_goldenconfig"]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see a user wanting to require a permission to use this but I don't think view_goldenconfig is the correct permission. Perhaps we need to implement a new permission similar to run_job?


def get_context_data(self, **kwargs):
"""Get the context data for the view."""
context = super().get_context_data(**kwargs)
context["form"] = forms.GenerateIntendedConfigForm()
return context