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

mvp config plan post processing #874

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
2 changes: 2 additions & 0 deletions changes/875.added
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added Config Plan Post Processing to Config plan detail view.
Added Config plan post processing to configuration deployment stage.
15 changes: 9 additions & 6 deletions nautobot_golden_config/nornir_plays/config_deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
from datetime import datetime

from django.contrib.auth import get_user_model
from django.utils.timezone import make_aware
from nautobot.dcim.models import Device
from nautobot.extras.models import Status
Expand All @@ -16,6 +17,7 @@
from nornir_nautobot.plugins.tasks.dispatcher import dispatcher

from nautobot_golden_config.nornir_plays.processor import ProcessGoldenConfig
from nautobot_golden_config.utilities.config_postprocessing import get_config_postprocessing
from nautobot_golden_config.utilities.constant import DEFAULT_DEPLOY_STATUS
from nautobot_golden_config.utilities.db_management import close_threaded_db_connections
from nautobot_golden_config.utilities.helper import dispatch_params
Expand All @@ -25,24 +27,23 @@


@close_threaded_db_connections
def run_deployment(task: Task, logger: logging.Logger, config_plan_qs, deploy_job_result) -> Result:
def run_deployment(task: Task, logger: logging.Logger, config_plan_qs, deploy_job_result, job_request) -> Result:
"""Deploy configurations to device."""
obj = task.host.data["obj"]
plans_to_deploy = config_plan_qs.filter(device=obj)
plans_to_deploy.update(deploy_result=deploy_job_result)
consolidated_config_set = "\n".join(plans_to_deploy.values_list("config_set", flat=True))
logger.debug(f"Consolidated config set: {consolidated_config_set}")
# TODO: Future: We should add post-processing rendering here
# after https://github.com/nautobot/nautobot-app-golden-config/issues/443

logger.debug("Executing post-processing on the config set")
post_config = get_config_postprocessing(plans_to_deploy, job_request)
plans_to_deploy.update(status=Status.objects.get(name="In Progress"))
try:
result = task.run(
task=dispatcher,
name="DEPLOY CONFIG TO DEVICE",
obj=obj,
logger=logger,
config=consolidated_config_set,
config=post_config,
**dispatch_params("merge_config", obj.platform.network_driver, logger),
)[1]
task_changed, task_result, task_failed = result.changed, result.result, result.failed
Expand Down Expand Up @@ -93,7 +94,8 @@ def config_deployment(job):
logger.error(error_msg)
raise NornirNautobotException(error_msg)
device_qs = Device.objects.filter(config_plan__in=config_plan_qs).distinct()

User = get_user_model() # pylint: disable=invalid-name
job.request.user = User.objects.get(id=job.celery_kwargs["nautobot_job_user_id"])
try:
with InitNornir(
runner=NORNIR_SETTINGS.get("runner"),
Expand All @@ -116,6 +118,7 @@ def config_deployment(job):
logger=logger,
config_plan_qs=config_plan_qs,
deploy_job_result=job.job_result,
job_request=job.request,
)
except Exception as error:
error_msg = f"`E3001:` General Exception handler, original error message ```{error}```"
Expand Down
28 changes: 21 additions & 7 deletions nautobot_golden_config/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,28 @@

<!-- Modal body -->
<div class="modal-body">
<span id="config_set_{{ record.pk }}"><pre>{{ record.config_set }}</pre></span>
<span class="config_hover_button">
<button type="button" class="btn btn-inline btn-default hover_copy_button" data-clipboard-action='copy' data-clipboard-target="#config_set_{{ record.pk }}">
<span class="mdi mdi-content-copy"></span>
</button>
</span>
<table class="table table-hover panel-body attr-table table-responsive table-wrapper">
<tr>
<td>Config Set</td>
<td>
<span id="config_set_{{ record.pk }}"><pre>{{ record.config_set }}</pre></span>
<span class="config_hover_button">
<button type="button" class="btn btn-inline btn-default hover_copy_button" data-clipboard-action='copy' data-clipboard-target="#config_set_{{ record.pk }}">
<span class="mdi mdi-content-copy"></span>
</button>
</span>
</td>
</tr>
<tr>
<td>Postprocessed Config Set</td>
<td>
<a href="{% url 'plugins:nautobot_golden_config:goldenconfig_postprocessing' pk=record.device.id %}?config_plan_id={{ record.id }}" target="_blank">
<i class="mdi mdi-text-box-check" title="Config Plan after Postprocessing"></i>
</a>
</td>
</tr>
</table>
</div>

<!-- Modal footer -->
<div class="modal-footer">
<button id="close" type="button" class="btn btn-default" data-dismiss="modal">Close</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends 'generic/object_detail.html' %}
{% load helpers %}
{% load static %}

{% block extra_styles %}
<style>
Expand Down Expand Up @@ -88,6 +89,18 @@
</span>
</td>
</tr>
<tr>
<td>Postprocessed Config Set</td>
<td>
<a href="{% url 'plugins:nautobot_golden_config:goldenconfig_postprocessing' pk=object.device.id %}?config_plan_id={{ object.id }}&modal=true" data-toggle="modal" data-target="#ccppModal">
<i class="mdi mdi-text-box-check"></i>
</a>
<div class="modal fade" id="ccppModal" tabindex="-1" role="dialog" aria-labelledby="ccppModalLabel">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-content">
</div>
</div>
</td>
</tr>
</table>
</div>
{% endblock %}
36 changes: 26 additions & 10 deletions nautobot_golden_config/utilities/config_postprocessing.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Functions related to prepare configuration with postprocessing."""

from functools import partial
from typing import Optional
from typing import Optional, Union

from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest
from django.utils.module_loading import import_string
from jinja2 import exceptions as jinja_errors
from jinja2.sandbox import SandboxedEnvironment
from nautobot.core.models.querysets import RestrictedQuerySet
from nautobot.dcim.models import Device
from nautobot.extras.choices import SecretsGroupAccessTypeChoices
from nautobot.extras.models.secrets import SecretsGroup
Expand Down Expand Up @@ -61,7 +62,9 @@ def _get_device_agg_data(device, request):
return device_data


def render_secrets(config_postprocessing: str, configs: models.GoldenConfig, request: HttpRequest) -> str:
def render_secrets(
config_postprocessing: str, configs: Union[models.GoldenConfig, models.ConfigPlan], request: HttpRequest
) -> str:
"""Renders secrets using the get_secrets filter.

This method is defined to render an already rendered intended configuration, but which have used the Jinja
Expand Down Expand Up @@ -102,7 +105,14 @@ def render_secrets(config_postprocessing: str, configs: models.GoldenConfig, req
except jinja_errors.TemplateAssertionError as error:
return f"Jinja encountered an TemplateAssertionError: '{error}'; check the template for correctness"

device_data = _get_device_agg_data(configs.device, request)
dev = None
if isinstance(configs, RestrictedQuerySet):
if isinstance(configs.first(), models.ConfigPlan):
dev = configs.first().device
else:
# If its a single config plan or intended config post-processing you can get the device from the object.
dev = configs.device
device_data = _get_device_agg_data(dev, request)

try:
return template.render(device_data)
Expand All @@ -121,7 +131,7 @@ def render_secrets(config_postprocessing: str, configs: models.GoldenConfig, req
) from error


def get_config_postprocessing(configs: models.GoldenConfig, request: HttpRequest) -> str:
def get_config_postprocessing(configs: Union[models.GoldenConfig, models.ConfigPlan], request: HttpRequest) -> str:
"""Renders final configuration artifact from intended configuration.

It chains multiple callables to transform an intended configuration into a configuration that can be pushed.
Expand All @@ -135,12 +145,18 @@ def get_config_postprocessing(configs: models.GoldenConfig, request: HttpRequest
if not ENABLE_POSTPROCESSING:
return "Generation of intended configurations postprocessing it is not enabled, check your app configuration."

config_postprocessing = configs.intended_config
if not config_postprocessing:
return (
"No intended configuration is available. Before rendering the configuration with postprocessing, "
"you need to generate the intended configuration."
)
if isinstance(configs, models.ConfigPlan):
config_postprocessing = configs.config_set
elif isinstance(configs, models.GoldenConfig):
config_postprocessing = configs.intended_config
if not config_postprocessing:
return (
"No intended configuration is available. Before rendering the configuration with postprocessing, "
"you need to generate the intended configuration."
)
else:
if isinstance(configs.first(), models.ConfigPlan):
config_postprocessing = "\n".join(configs.values_list("config_set", flat=True))

# Available functions to create the final intended configuration, in string dotted format
# The order is important because, if not changed by the `postprocessing_subscribed`, is going
Expand Down
6 changes: 4 additions & 2 deletions nautobot_golden_config/utilities/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
from nautobot.core.utils.data import render_jinja2
from nautobot.dcim.filters import DeviceFilterSet
from nautobot.dcim.models import Device
from nautobot.extras.models import Job
from nautobot.extras.choices import DynamicGroupTypeChoices
from nautobot.extras.models import Job
from nornir_nautobot.exceptions import NornirNautobotException

from nautobot_golden_config import config as app_config
Expand Down Expand Up @@ -71,7 +71,9 @@ def get_job_filter(data=None):

raw_qs = Q()
# If scope is set to {} do not loop as all devices are in scope.
if not models.GoldenConfigSetting.objects.filter(dynamic_group__filter__iexact="{}", dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER).exists():
if not models.GoldenConfigSetting.objects.filter(
dynamic_group__filter__iexact="{}", dynamic_group__group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER
).exists():
for obj in models.GoldenConfigSetting.objects.all():
raw_qs = raw_qs | obj.dynamic_group.generate_query()

Expand Down
5 changes: 4 additions & 1 deletion nautobot_golden_config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ def get_extra_context(self, request, instance=None, **kwargs):

def _pre_helper(self, pk, request):
self.device = Device.objects.get(pk=pk)
self.config_details = models.GoldenConfig.objects.filter(device=self.device).first()
if request.GET.get("config_plan_id"):
self.config_details = models.ConfigPlan.objects.get(id=request.GET.get("config_plan_id"))
else:
self.config_details = models.GoldenConfig.objects.filter(device=self.device).first()
self.action_template_name = "nautobot_golden_config/goldenconfig_details.html"
self.structured_format = "json"
self.is_modal = False
Expand Down
Loading