From 0beb6ab41dca03962e3b214084fa97a9dfa6080b Mon Sep 17 00:00:00 2001 From: Jeff Kala Date: Wed, 13 Sep 2023 22:38:29 -0600 Subject: [PATCH 1/4] some flake8 fixes --- .flake8 | 3 ++- development/nautobot_config.py | 6 ++--- .../nornir_plays/config_compliance.py | 19 ++++++-------- .../nornir_plays/config_intended.py | 25 ++++++++----------- nautobot_golden_config/views.py | 7 +++--- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/.flake8 b/.flake8 index aaa63b60..3cbe47d4 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,5 @@ [flake8] # E501: Line length is enforced by Black, so flake8 doesn't need to check it # W503: Black disagrees with this rule, as does PEP 8; Black wins -ignore = E501, W503, F811, F401, F405, E203 +ignore = E501, W503 +exclude = */migrations/ diff --git a/development/nautobot_config.py b/development/nautobot_config.py index c858f2d1..e30defc8 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -4,11 +4,9 @@ import sys from django.utils.module_loading import import_string - from nautobot.core.settings import * # noqa: F403 from nautobot.core.settings_funcs import is_truthy, parse_redis_connection - # # Misc. settings # @@ -200,7 +198,7 @@ # Modify django_jinja Environment for test cases django_jinja_config = None -for template in TEMPLATES: +for template in TEMPLATES: # noqa: F405 if template["BACKEND"].startswith("django_jinja"): django_jinja_config = template @@ -213,4 +211,4 @@ jinja_options["undefined"] = "jinja2.StrictUndefined" # Import filter function to have it register filter with django_jinja -from nautobot_golden_config.tests import jinja_filters # noqa: E402 +from nautobot_golden_config.tests import jinja_filters # noqa: E402, F401 diff --git a/nautobot_golden_config/nornir_plays/config_compliance.py b/nautobot_golden_config/nornir_plays/config_compliance.py index 5ba24372..b82f6c36 100644 --- a/nautobot_golden_config/nornir_plays/config_compliance.py +++ b/nautobot_golden_config/nornir_plays/config_compliance.py @@ -3,35 +3,31 @@ import difflib import logging import os - from collections import defaultdict from datetime import datetime -from netutils.config.compliance import parser_map, section_config, _open_file_config +from nautobot_plugin_nornir.constants import NORNIR_SETTINGS +from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory +from netutils.config.compliance import _open_file_config, parser_map, section_config from nornir import InitNornir from nornir.core.plugins.inventory import InventoryPluginRegister from nornir.core.task import Result, Task - from nornir_nautobot.exceptions import NornirNautobotException from nornir_nautobot.utils.logger import NornirLogger -from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory -from nautobot_plugin_nornir.constants import NORNIR_SETTINGS - from nautobot_golden_config.choices import ComplianceRuleConfigTypeChoice +from nautobot_golden_config.models import ComplianceRule, ConfigCompliance, GoldenConfig +from nautobot_golden_config.nornir_plays.processor import ProcessGoldenConfig from nautobot_golden_config.utilities.db_management import close_threaded_db_connections -from nautobot_golden_config.models import ComplianceRule, ConfigCompliance, GoldenConfigSetting, GoldenConfig from nautobot_golden_config.utilities.helper import ( get_device_to_settings_map, get_job_filter, - verify_settings, - render_jinja_template, get_json_config, + render_jinja_template, + verify_settings, ) -from nautobot_golden_config.nornir_plays.processor import ProcessGoldenConfig from nautobot_golden_config.utilities.utils import get_platform - InventoryPluginRegister.register("nautobot-inventory", NautobotORMInventory) LOGGER = logging.getLogger(__name__) @@ -39,6 +35,7 @@ def get_rules(): """A serializer of sorts to return rule mappings as a dictionary.""" # TODO: Review if creating a proper serializer is the way to go. + ThisDoesNotConform = "dakljds" rules = defaultdict(list) for compliance_rule in ComplianceRule.objects.all(): platform = str(compliance_rule.platform.slug) diff --git a/nautobot_golden_config/nornir_plays/config_intended.py b/nautobot_golden_config/nornir_plays/config_intended.py index be5f18e9..e00518e4 100644 --- a/nautobot_golden_config/nornir_plays/config_intended.py +++ b/nautobot_golden_config/nornir_plays/config_intended.py @@ -1,35 +1,32 @@ """Nornir job for generating the intended config.""" # pylint: disable=relative-beyond-top-level -import os import logging - +import os from datetime import datetime -from nornir import InitNornir -from nornir.core.plugins.inventory import InventoryPluginRegister -from nornir.core.task import Result, Task from django.template import engines from jinja2.sandbox import SandboxedEnvironment - +from nautobot_plugin_nornir.constants import NORNIR_SETTINGS +from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory +from nautobot_plugin_nornir.utils import get_dispatcher +from nornir import InitNornir +from nornir.core.plugins.inventory import InventoryPluginRegister +from nornir.core.task import Result, Task from nornir_nautobot.exceptions import NornirNautobotException from nornir_nautobot.plugins.tasks.dispatcher import dispatcher from nornir_nautobot.utils.logger import NornirLogger -from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory -from nautobot_plugin_nornir.constants import NORNIR_SETTINGS -from nautobot_plugin_nornir.utils import get_dispatcher - +from nautobot_golden_config.models import GoldenConfig +from nautobot_golden_config.nornir_plays.processor import ProcessGoldenConfig from nautobot_golden_config.utilities.constant import PLUGIN_CFG from nautobot_golden_config.utilities.db_management import close_threaded_db_connections -from nautobot_golden_config.models import GoldenConfigSetting, GoldenConfig +from nautobot_golden_config.utilities.graphql import graph_ql_query from nautobot_golden_config.utilities.helper import ( get_device_to_settings_map, get_job_filter, - verify_settings, render_jinja_template, + verify_settings, ) -from nautobot_golden_config.utilities.graphql import graph_ql_query -from nautobot_golden_config.nornir_plays.processor import ProcessGoldenConfig InventoryPluginRegister.register("nautobot-inventory", NautobotORMInventory) LOGGER = logging.getLogger(__name__) diff --git a/nautobot_golden_config/views.py b/nautobot_golden_config/views.py index 6f6cc4c4..3d52bf2b 100644 --- a/nautobot_golden_config/views.py +++ b/nautobot_golden_config/views.py @@ -28,13 +28,14 @@ from nautobot.utilities.forms import ConfirmationForm from nautobot.utilities.utils import copy_safe_request, csv_format from nautobot.utilities.views import ContentTypePermissionRequiredMixin, ObjectPermissionRequiredMixin + from nautobot_golden_config import filters, forms, models, tables from nautobot_golden_config.api import serializers from nautobot_golden_config.jobs import DeployConfigPlans -from nautobot_golden_config.utilities.config_postprocessing import get_config_postprocessing from nautobot_golden_config.utilities import constant +from nautobot_golden_config.utilities.config_postprocessing import get_config_postprocessing from nautobot_golden_config.utilities.graphql import graph_ql_query -from nautobot_golden_config.utilities.helper import get_device_to_settings_map, add_message +from nautobot_golden_config.utilities.helper import add_message, get_device_to_settings_map LOGGER = logging.getLogger(__name__) @@ -497,7 +498,7 @@ def diff_structured_data(backup_data, intended_data): + "\n" + output[first_occurence:second_occurence] + "@@" - + output[second_occurence + 2 :] + + output[second_occurence + 2 :] # noqa: E203 ) template_name = "nautobot_golden_config/configcompliance_details.html" From d4338505ef63c1a046b5919f3f3a6906f8b34991 Mon Sep 17 00:00:00 2001 From: Jeff Kala Date: Wed, 13 Sep 2023 23:32:32 -0600 Subject: [PATCH 2/4] fix broken flake8 and pylint configs and actual issues, and update run_job.js --- .flake8 | 1 - nautobot_golden_config/jobs.py | 2 +- .../migrations/0002_custom_data.py | 2 +- .../0015_convert_sotagg_queries_part2.py | 2 +- .../0016_convert_sotagg_queries_part3.py | 4 +- .../0017_convert_sotagg_queries_part4.py | 3 +- .../0020_convert_dynamicgroup_part_2.py | 3 +- .../nornir_plays/config_compliance.py | 1 - nautobot_golden_config/static/run_job.js | 332 +++++++++--------- pyproject.toml | 3 +- 10 files changed, 165 insertions(+), 188 deletions(-) diff --git a/.flake8 b/.flake8 index 3cbe47d4..e3ba27d5 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,3 @@ # E501: Line length is enforced by Black, so flake8 doesn't need to check it # W503: Black disagrees with this rule, as does PEP 8; Black wins ignore = E501, W503 -exclude = */migrations/ diff --git a/nautobot_golden_config/jobs.py b/nautobot_golden_config/jobs.py index 84a9def1..9c2c9707 100644 --- a/nautobot_golden_config/jobs.py +++ b/nautobot_golden_config/jobs.py @@ -25,12 +25,12 @@ from nautobot_golden_config.nornir_plays.config_compliance import config_compliance from nautobot_golden_config.nornir_plays.config_deployment import config_deployment from nautobot_golden_config.nornir_plays.config_intended import config_intended +from nautobot_golden_config.utilities import constant from nautobot_golden_config.utilities.config_plan import ( config_plan_default_status, generate_config_set_from_compliance_feature, generate_config_set_from_manual, ) -from nautobot_golden_config.utilities import constant from nautobot_golden_config.utilities.git import GitRepo from nautobot_golden_config.utilities.helper import get_job_filter diff --git a/nautobot_golden_config/migrations/0002_custom_data.py b/nautobot_golden_config/migrations/0002_custom_data.py index 1435fbd8..d96c71f0 100644 --- a/nautobot_golden_config/migrations/0002_custom_data.py +++ b/nautobot_golden_config/migrations/0002_custom_data.py @@ -1,6 +1,6 @@ # Generated by Django 3.1.3 on 2021-02-22 01:27 -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): diff --git a/nautobot_golden_config/migrations/0015_convert_sotagg_queries_part2.py b/nautobot_golden_config/migrations/0015_convert_sotagg_queries_part2.py index c0b4d6c1..a1244046 100644 --- a/nautobot_golden_config/migrations/0015_convert_sotagg_queries_part2.py +++ b/nautobot_golden_config/migrations/0015_convert_sotagg_queries_part2.py @@ -1,4 +1,4 @@ -from django.db import migrations, models +from django.db import migrations def save_existing_sotagg_queries(apps, schema_editor): diff --git a/nautobot_golden_config/migrations/0016_convert_sotagg_queries_part3.py b/nautobot_golden_config/migrations/0016_convert_sotagg_queries_part3.py index 0fc55074..93c96f4f 100644 --- a/nautobot_golden_config/migrations/0016_convert_sotagg_queries_part3.py +++ b/nautobot_golden_config/migrations/0016_convert_sotagg_queries_part3.py @@ -1,7 +1,5 @@ -from datetime import date - -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/nautobot_golden_config/migrations/0017_convert_sotagg_queries_part4.py b/nautobot_golden_config/migrations/0017_convert_sotagg_queries_part4.py index c90d5b9c..1ba69ceb 100644 --- a/nautobot_golden_config/migrations/0017_convert_sotagg_queries_part4.py +++ b/nautobot_golden_config/migrations/0017_convert_sotagg_queries_part4.py @@ -1,7 +1,6 @@ -from datetime import date import logging +from datetime import date -from django.core.validators import ValidationError from django.db import migrations logger = logging.getLogger("nautobot") diff --git a/nautobot_golden_config/migrations/0020_convert_dynamicgroup_part_2.py b/nautobot_golden_config/migrations/0020_convert_dynamicgroup_part_2.py index 639d82ca..03125222 100644 --- a/nautobot_golden_config/migrations/0020_convert_dynamicgroup_part_2.py +++ b/nautobot_golden_config/migrations/0020_convert_dynamicgroup_part_2.py @@ -1,8 +1,7 @@ # Generated by Django 3.2.14 on 2022-07-11 14:18 -from django.db import migrations, models +from django.db import migrations from django.utils.text import slugify -import django.db.models.deletion def create_dynamic_groups(apps, schedma_editor): diff --git a/nautobot_golden_config/nornir_plays/config_compliance.py b/nautobot_golden_config/nornir_plays/config_compliance.py index b82f6c36..bb33f5b5 100644 --- a/nautobot_golden_config/nornir_plays/config_compliance.py +++ b/nautobot_golden_config/nornir_plays/config_compliance.py @@ -35,7 +35,6 @@ def get_rules(): """A serializer of sorts to return rule mappings as a dictionary.""" # TODO: Review if creating a proper serializer is the way to go. - ThisDoesNotConform = "dakljds" rules = defaultdict(list) for compliance_rule in ComplianceRule.objects.all(): platform = str(compliance_rule.platform.slug) diff --git a/nautobot_golden_config/static/run_job.js b/nautobot_golden_config/static/run_job.js index 3229e30e..81e286d1 100644 --- a/nautobot_golden_config/static/run_job.js +++ b/nautobot_golden_config/static/run_job.js @@ -7,207 +7,189 @@ * @param {string} jobClass - The jobs `class_path` as defined on the job detail page. * @param {Object} data - The object containing payload data to send to the job. * @param {string} redirectUrlTemplate - The redirect url to provide, you have access to jobData with the syntax like `{jobData.someKey}`, leave `undefined` if none is required. + * @param {string} callBack - The promise function to return the success or failure message, leave `undefined` no callback is required. */ -function startJob(jobClass, data, redirectUrlTemplate) { - var jobApi = `/api/extras/jobs/${jobClass}/run/`; +function startJob(jobClass, data, redirectUrlTemplate, callBack) { + var jobApi = `/api/extras/jobs/${jobClass}/run/`; - $.ajax({ - type: 'POST', - url: jobApi, - contentType: "application/json", - data: JSON.stringify(data), - dataType: 'json', - headers: { - 'X-CSRFToken': nautobot_csrf_token - }, - beforeSend: function() { - // Normalize to base as much as you can. - $('#jobStatus').html("Pending").show(); - $('#loaderImg').show(); - $('#jobResults').hide(); - $('#redirectLink').hide(); - $('#detailMessages').hide(); - }, - success: function(jobData) { - $('#jobStatus').html("Started").show(); - var jobResultUrl = "/extras/job-results/" + jobData.result.id + "/"; - $('#jobResults').html(iconLink(jobResultUrl, "mdi-open-in-new", "Job Details")).show(); - pollJobStatus(jobData.result.url); - if (typeof redirectUrlTemplate !== "undefined") { - var redirectUrl = _renderTemplate(redirectUrlTemplate, jobData); - $('#redirectLink').html(iconLink(redirectUrl, "mdi-open-in-new", "Info")); - } + if (typeof callBack === "undefined") { + var callBack = getMessage; + } + + $.ajax({ + type: 'POST', + url: jobApi, + contentType: "application/json", + data: JSON.stringify(data), + dataType: 'json', + headers: { + 'X-CSRFToken': nautobot_csrf_token + }, + beforeSend: function() { + // Normalize to base as much as you can. + $('#jobStatus').html("Pending").show(); + $('#loaderImg').show(); + $('#jobResults').hide(); + $('#redirectLink').hide(); + $('#detailMessages').hide(); }, - error: function(e) { - $("#loaderImg").hide(); - console.log("There was an error with your request..."); - console.log("error: " + JSON.stringify(e)); - $('#jobStatus').html("Failed").show(); - $('#detailMessages').show(); - $('#detailMessages').attr('class', 'alert alert-danger text-center'); - $('#detailMessages').html("Error: " + e.responseText); - } - }); + success: function(jobData) { + $('#jobStatus').html("Started").show(); + var jobResultUrl = "/extras/job-results/" + jobData.result.id + "/"; + $('#jobResults').html(iconLink(jobResultUrl, "mdi-open-in-new", "Job Details")).show(); + pollJobStatus(jobData.result.url, callBack); + if (typeof redirectUrlTemplate !== "undefined") { + var redirectUrl = _renderTemplate(redirectUrlTemplate, jobData); + $('#redirectLink').html(iconLink(redirectUrl, "mdi-open-in-new", "Info")); + } + }, + error: function(e) { + $("#loaderImg").hide(); + console.log("There was an error with your request..."); + console.log("error: " + JSON.stringify(e)); + $('#jobStatus').html("Failed").show(); + $('#detailMessages').show(); + $('#detailMessages').attr('class', 'alert alert-danger text-center'); + $('#detailMessages').html("Error: " + e.responseText); + } + }); } /** - * Polls the status of a job with the given job ID. - * - * This function makes an AJAX request to the server, - * to get the current status of the job with the specified job ID. - * It continues to poll the status until the job completes or fails. - * The job status is updated in the HTML element with ID 'jobStatus'. - * If the job encounters an error, additional error details are shown. - * The call is not made async, so that the parent call will wait until - * this is completed. - * - * @requires nautobot_csrf_token - The CSRF token obtained from Nautobot. - * @param {string} jobId - The ID of the job to poll. - * @returns {void} - */ -function pollJobStatus(jobId) { - $.ajax({ - url: jobId, - type: "GET", - async: false, - dataType: "json", - headers: { - 'X-CSRFToken': nautobot_csrf_token - }, - success: function(data) { - $('#jobStatus').html(data.status.value.charAt(0).toUpperCase() + data.status.value.slice(1)).show(); - if (["errored", "failed"].includes(data.status.value)) { - $("#loaderImg").hide(); - $('#detailMessages').show(); - $('#detailMessages').attr('class', 'alert alert-warning text-center'); - $('#detailMessages').html("Job started but failed during the Job run. This job may have partially completed. See Job Results for more details on the errors."); - } else if (["running", "pending"].includes(data.status.value)) { - // Job is still processing, continue polling - setTimeout(function() { - pollJobStatus(jobId); - }, 1000); // Poll every 1 seconds - } else if (data.status.value == "completed") { +* Polls the status of a job with the given job ID. +* +* This function makes an AJAX request to the server, +* to get the current status of the job with the specified job ID. +* It continues to poll the status until the job completes or fails. +* The job status is updated in the HTML element with ID 'jobStatus'. +* If the job encounters an error, additional error details are shown. +* The call is not made async, so that the parent call will wait until +* this is completed. +* +* @requires nautobot_csrf_token - The CSRF token obtained from Nautobot. +* @param {string} jobId - The ID of the job to poll. +* @returns {void} +*/ +function pollJobStatus(jobId, callBack) { +$.ajax({ + url: jobId, + type: "GET", + async: false, + dataType: "json", + headers: { + 'X-CSRFToken': nautobot_csrf_token + }, + success: function (data) { + $('#jobStatus').html(data.status.value.charAt(0).toUpperCase() + data.status.value.slice(1)).show(); + if (["errored", "failed"].includes(data.status.value)) { + $("#loaderImg").hide(); + $('#detailMessages').show(); + $('#detailMessages').attr('class', 'alert alert-warning text-center'); + $('#detailMessages').html("Job started but failed during the Job run. This job may have partially completed. See Job Results for more details on the errors."); + } else if (["running", "pending"].includes(data.status.value)) { + // Job is still processing, continue polling + setTimeout(function () { + pollJobStatus(jobId, callBack); + }, 1000); // Poll every 1 seconds + } else if (data.status.value == "completed") { + $("#loaderImg").hide(); + $('#detailMessages').show(); + callBack(data.id) + .then((message) => { + $('#redirectLink').show(); + $('#detailMessages').attr('class', 'alert alert-success text-center'); + $('#detailMessages').html(message) + }) + .catch((message) => { + $('#detailMessages').attr('class', 'alert alert-warning text-center'); + $('#detailMessages').html(message) + }) + } + }, + error: function(e) { $("#loaderImg").hide(); + console.log("There was an error with your request..."); + console.log("error: " + JSON.stringify(e)); $('#detailMessages').show(); - configPlanCount(data.id) - .then(function(planCount) { - $('#redirectLink').show(); - $('#detailMessages').attr('class', 'alert alert-success text-center'); - $('#detailMessages').html( - "Job Completed Successfully."+ - "
Number of Config Plans generated: " + planCount - ) - }) - .catch(function(error) { - $('#detailMessages').attr('class', 'alert alert-warning text-center'); - $('#detailMessages').html( - "Job completed successfully, but no Config Plans were generated."+ - "
If this is unexpected, please validate your input parameters." - ) - }); + $('#detailMessages').attr('class', 'alert alert-danger text-center'); + $('#detailMessages').html("Error: " + e.responseText); } - }, - error: function(e) { - $("#loaderImg").hide(); - console.log("There was an error with your request..."); - console.log("error: " + JSON.stringify(e)); - $('#detailMessages').show(); - $('#detailMessages').attr('class', 'alert alert-danger text-center'); - $('#detailMessages').html("Error: " + e.responseText); - } - }); -} - + }) +}; /** - * Converts a list of form data objects to a dictionary. - * - * @param {FormData} formData - The form data object to be converted. - * @param {string[]} listKeys - The list of keys for which values should be collected as lists. - * @returns {Object} - The dictionary representation of the form data. - */ +* Converts a list of form data objects to a dictionary. +* +* @param {FormData} formData - The form data object to be converted. +* @param {string[]} listKeys - The list of keys for which values should be collected as lists. +* @returns {Object} - The dictionary representation of the form data. +*/ function formDataToDictionary(formData, listKeys) { - const dict = {}; +const dict = {}; - formData.forEach(item => { - const { name, value } = item; - if (listKeys.includes(name)) { - if (!dict[name]) { - dict[name] = [value]; - } else { - dict[name].push(value); - } +formData.forEach(item => { + const { name, value } = item; + if (listKeys.includes(name)) { + if (!dict[name]) { + dict[name] = [value]; } else { - dict[name] = value; + dict[name].push(value); } - }); + } else { + dict[name] = value; + } +}); - return dict; +return dict; } /** - * Generates an HTML anchor link with an icon. - * - * @param {string} url - The URL to link to. - * @param {string} icon - The name of the Material Design Icon to use. - * @param {string} title - The title to display when hovering over the icon. - * @returns {string} - The HTML anchor link with the specified icon. - */ +* Generates an HTML anchor link with an icon. +* +* @param {string} url - The URL to link to. +* @param {string} icon - The name of the Material Design Icon to use. +* @param {string} title - The title to display when hovering over the icon. +* @returns {string} - The HTML anchor link with the specified icon. +*/ function iconLink(url, icon, title) { - const linkUrl = `` + - ` ` + - ` ` + - ` ` + - `` - return linkUrl +const linkUrl = `` + +` ` + +` ` + +` ` + +`` +return linkUrl } /** - * Renders a template string with placeholders replaced by corresponding values from jobData. - * - * @param {string} templateString - The template string with placeholders in the form of `{jobData.someKey}`. - * @param {Object} jobData - The object containing data to replace placeholders in the template. - * @returns {string} - The rendered string with placeholders replaced by actual values from jobData. - */ +* Renders a template string with placeholders replaced by corresponding values from jobData. +* +* @param {string} templateString - The template string with placeholders in the form of `{jobData.someKey}`. +* @param {Object} jobData - The object containing data to replace placeholders in the template. +* @returns {string} - The rendered string with placeholders replaced by actual values from jobData. +*/ function _renderTemplate(templateString, data) { - // Create a regular expression to match placeholders in the template - const placeholderRegex = /\{jobData\.([^\}]+)\}/g; +// Create a regular expression to match placeholders in the template +const placeholderRegex = /\{jobData\.([^\}]+)\}/g; - // Replace placeholders with corresponding values from jobData - const renderedString = templateString.replace(placeholderRegex, (match, key) => { - const keys = key.split("."); - let value = data; - for (const k of keys) { - if (value.hasOwnProperty(k)) { - value = value[k]; - } else { - return match; // If the key is not found, keep the original placeholder - } +// Replace placeholders with corresponding values from jobData +const renderedString = templateString.replace(placeholderRegex, (match, key) => { + const keys = key.split("."); + let value = data; + for (const k of keys) { + if (value.hasOwnProperty(k)) { + value = value[k]; + } else { + return match; // If the key is not found, keep the original placeholder } - return value; - }); + } + return value; +}); - return renderedString; +return renderedString; } - -function configPlanCount(jobResultId) { - return new Promise(function(resolve, reject) { - var configPlanApi = `/api/plugins/golden-config/config-plan/?job_result_id=${jobResultId}`; - $.ajax({ - url: configPlanApi, - type: "GET", - dataType: "json", - headers: { - 'X-CSRFToken': nautobot_csrf_token - }, - success: function(data) { - var count = data.count; - resolve(count); - }, - error: function(e) { - resolve(e); - } - }); - }); -} \ No newline at end of file +function getMessage(jobResultId) { +return new Promise((resolve) => { + resolve("Job Completed Successfully."); +}); +} diff --git a/pyproject.toml b/pyproject.toml index e85aede0..bcad57af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,8 @@ exclude = ''' [tool.pylint.master] # Include the pylint_django plugin to avoid spurious warnings about Django patterns load-plugins="pylint_django" -ignore=["jinja_filters.py", ".venv"] +ignore-patterns=["jinja_filters.py", ".venv"] +ignore-paths = '^.*/migrations/.*$' [tool.pylint.basic] # No docstrings required for private methods (Pylint default), or for test_ functions, or for inner Meta classes. From b0e37e5fe9d517a018846b7dc82d82ce17e594db Mon Sep 17 00:00:00 2001 From: Jeff Kala Date: Wed, 13 Sep 2023 23:45:59 -0600 Subject: [PATCH 3/4] add ADRs --- docs/dev/dev_adr.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/dev/dev_adr.md b/docs/dev/dev_adr.md index 88f6f593..b6404ef9 100644 --- a/docs/dev/dev_adr.md +++ b/docs/dev/dev_adr.md @@ -143,3 +143,12 @@ This function performs an additional permission validation, to check if the requ Over time device(s) platform may change; whether this is a device refresh or full replacement. A Django `post_save` signal is used on the `ConfigCompliance` model and provides a reliable and efficient way to manage configuration compliance objects. This signal deletes any `ConfigCompliance` objects that don't match the current platform. This decision was made to avoid compliance reporting inconsistencies that can arise when outdated or irrelevant objects remain in the database which were generated with the previous platform. This has a computational impact when updating a Device object's platform. This is similar to the computational impact of an SQL `cascade` option on a delete. This is largely unavoidable and should be limited in impact, such that it will only be the removal of the number of `ConfigCompliance` objects, which is no bigger than the number of `Config Features`, which is generally intended to be a small amount. + +### Configuration Deployment and Remediation + +Configuration remediation and deployments of any of the attributes based on the configuration compliance object are calculated based on the last run of the `ConfigCompliance` job. After a configuration deployment to fix any of these attributes (remediation, extras, missing) a new `ConfigCompliance` job must be run before all the compliance results will be updated. + + +### Manual ConfigPlans + +When generating a manual `ConfigPlan` the Jinja2 template render has access to Django ORM methods like `.all()`, this also means that methods like `.delete()` can be called, the `render_template` functionality used by Golden Config inherits a Jinja2 Sandbox exception that will block unsafe calls. Golden Config will simply re-raise the exception `jinja2.exceptions.SecurityError: > is not safely callable`. From 740d8134626ddb4cc257ab6c6a99fc21ad9391dd Mon Sep 17 00:00:00 2001 From: Jeff Kala Date: Thu, 14 Sep 2023 00:46:38 -0600 Subject: [PATCH 4/4] add documetnation --- docs/dev/dev_adr.md | 7 ++- docs/user/app_getting_started.md | 43 ++++++++++++++++++- .../troubleshooting/troubleshoot_general.md | 4 ++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/docs/dev/dev_adr.md b/docs/dev/dev_adr.md index b6404ef9..a8591a0b 100644 --- a/docs/dev/dev_adr.md +++ b/docs/dev/dev_adr.md @@ -146,9 +146,14 @@ This has a computational impact when updating a Device object's platform. This i ### Configuration Deployment and Remediation -Configuration remediation and deployments of any of the attributes based on the configuration compliance object are calculated based on the last run of the `ConfigCompliance` job. After a configuration deployment to fix any of these attributes (remediation, extras, missing) a new `ConfigCompliance` job must be run before all the compliance results will be updated. +Configuration remediation and deployments of any of the attributes based on the configuration compliance object are calculated based on the last run of the `ConfigCompliance` job. After a configuration deployment to fix any of these attributes (remediation, intended, missing) a new `ConfigCompliance` job must be run before all the compliance results will be updated. ### Manual ConfigPlans When generating a manual `ConfigPlan` the Jinja2 template render has access to Django ORM methods like `.all()`, this also means that methods like `.delete()` can be called, the `render_template` functionality used by Golden Config inherits a Jinja2 Sandbox exception that will block unsafe calls. Golden Config will simply re-raise the exception `jinja2.exceptions.SecurityError: > is not safely callable`. + + +### Hidden Jobs and JobButtons + +The configuration deployment and plans features of Golden Config come packaged with Jobs and JobButtons to execute the functionality. In order to to provide a repeatable and consistent behavior these Jobs and JobButtons are designed to only be executed via specialized views. They're not intended to be executed manually from the Jobs/JobButtons menus. diff --git a/docs/user/app_getting_started.md b/docs/user/app_getting_started.md index cdbb71e5..b64154af 100644 --- a/docs/user/app_getting_started.md +++ b/docs/user/app_getting_started.md @@ -126,11 +126,50 @@ Compliance requires Backups and Intended Configurations in order to be executed. # Config Plans -Coming soon +Follow the steps below to get up and running for the configuration plans element of the plugin. + +1. Enable the feature in the `PLUGIN_SETTINGS`. The configuration should have `"enable_plan": True` set in the `PLUGINS_CONFIG` dictionary for `nautobot_golden_config`. +2. Follow the steps in [Compliance](#compliance). + - Compliance is necessary if ConfigPlans will be generated utilizing any of the attributes provided by a Compliance object. + - This step may be skipped if only `manual` ConfigPlans are going to be generated. +3. Create a ConfigPlan + + 1. Navigate to `Golden Config -> Config Plans` + 2. Click on `ADD` button. + 3. Fill out the plan details and plan filters. + - The options dynamically change in the form based on the `plan type` selected. + - If the `plan type` is Intended, Remediation, Missing. + - Select the Compliance Features to use to generate the plan. If none are selected all features will be in scope. + - If the `plan type` is Manual. + - Create a manual plan to accomplish the goal. Note: Access to `obj` is available to dynamically populate fields via Jinja2 syntax. + 4. Click `Generate` + +> For in-depth details see [Navigating Config Plans](./app_feature_config_plans.md) # Config Deploy -Coming soon +Follow the steps below to get up and running for the configuration deployment element of the plugin. + +1. Enable the feature in the `PLUGIN_SETTINGS`. The configuration should have `"enable_deploy": True` set in the `PLUGINS_CONFIG` dictionary for `nautobot_golden_config`. +2. Follow the steps in [Config Plans](#config-plans). +3. Navigate to the specific ConfigPlan to deploy, or multi-select them from the ConfigPlan list view. + - If deploying from a specific ConfigPlan object. Click `Deploy` button and accept the warnings. + - If deploying from the ConfigPlan list view. Click `Deploy Selected` button and accept the warnings. +4. Interpret the results from the popup modal and navigate to the job result as needed for more details. + +> Config Deployments utilize the dispatchers from nornir-nautobot just like the other functionality of Golden Config. See [Troubleshooting Dispatchers](./troubleshooting/troubleshoot_dispatchers.md) for more details. + +# Config Remediation + +Follow the steps below to get up and running for the configuration remediation element of the plugin. + +1. Navigate to `Golden Config -> Compliance Rules`. +2. Select the rules in which you'd like to enable remediation on. +3. Edit the `Compliance Rule` and turn on the `Remediation` toggle button. +4. Run the `Compliance` job again which will generate the initial remediation plan for the feature. +5. Navigate to `Golden Config -> Config Compliance`, select the device and notice a remediation section is not present for the compliance details for the feature. + +> For in-depth details see [Navigating Config Plans](./app_feature_remediation.md) # Load Properties from Git diff --git a/docs/user/troubleshooting/troubleshoot_general.md b/docs/user/troubleshooting/troubleshoot_general.md index 934921d5..1aefdd46 100755 --- a/docs/user/troubleshooting/troubleshoot_general.md +++ b/docs/user/troubleshooting/troubleshoot_general.md @@ -64,3 +64,7 @@ If none of these troubleshooting steps helped identify the problem please visit - [Troubleshoot Credentials](./troubleshoot_credentials.md) - [Troubleshoot Dispatchers](./troubleshoot_dispatchers.md) + +### Detailed Diagrams + +Golden config flow diagrams are available on the repositories [wiki](https://github.com/nautobot/nautobot-plugin-golden-config/wiki#diagrams).