diff --git a/docs/user/app_feature_remediation.md b/docs/user/app_feature_remediation.md index 4cde54f00..a4d5ac559 100644 --- a/docs/user/app_feature_remediation.md +++ b/docs/user/app_feature_remediation.md @@ -26,6 +26,7 @@ The type of remediation to be performed in a particular platform is defined by n Network device operating systems (Nautobot Platforms) can consume two different types of remediation, namely: - **HIERCONFIG remediation (CLI - hierarchical)** +- **Dynamic HIERCONFIG remediation** - **Custom Remediation** ![Remediation Platform Settings](../images/remediation_settings_per_platform.png) @@ -48,6 +49,33 @@ Default Hier config options can be used or customized on a per platform basis, a For additional information on how to customize Hier Config options, please refer to the Hierarchical Configuration development guide: https://netdevops.io/hier_config/advanced-topics/ +### Dynamic Hier Config Remediation Type + +The Dynamic Hier Config remediation option is an additional lever to customize the default Hier Config remediation config without the need to rewrite the entire Hier Config flow in a custom remediation function. Dynamic remeditaion takes the form of modules imported from a Git repository. Specifically within the root folder of the repository, there should be a folder called `hier_config_dynamic_remediations`, and within that folder should be an `__init__.py` and any dynamic remediations that have been created. The folder structure for the Git repository should be similiar to the following: + +```shell +├── hier_config_dynamic_remediations +│ ├── __init__.py // necessary to discover the functions dynamically +│ ├── file_1.py +│ ├── file_2.py +│ └── file_3.py +``` + +Each dynamic remediation file should have a function called `remediation` that accepts a `remediation_config` argument. The `remediation_config` argument is the `hier_config.root.HConfig` object that is created when the difference between the backup and intended config is generated by the **Perform Configuration Compliance** job. Any changes should be made directly to that arugment. + +For example, the following code would remove any sequence numbers from a Cisco IOS ACL config block: + +```python +def remediation(remediation_config): + lineage = remediation_config.get_child("startswith", "ip access-list") + for line in lineage.all_children(): + line_items = line.text.split() + index = 1 if line.text.startswith("no") else 0 + line_items.remove(line_items[index]) + lineage.del_child_by_text(line.text) + lineage.add_child(" ".join(line_items)) +``` + ### Custom Config Remediation Type When a Network Operating System delivers configuration data in a format that is not CLI/Hierarchical, we can still perform remediation by using the Custom Remediation options. Custom Remediation is defined within a Python function that takes as input a Configuration Compliance object and returns a Remediation Field. diff --git a/nautobot_golden_config/choices.py b/nautobot_golden_config/choices.py index 7531b279f..7b9fcc2fe 100644 --- a/nautobot_golden_config/choices.py +++ b/nautobot_golden_config/choices.py @@ -20,10 +20,12 @@ class RemediationTypeChoice(ChoiceSet): """Choiceset used by RemediationSetting.""" TYPE_HIERCONFIG = "hierconfig" + TYPE_DYNAMIC_HIERCONFIG = "dynamic_hierconfig" TYPE_CUSTOM = "custom_remediation" CHOICES = ( (TYPE_HIERCONFIG, "HIERCONFIG"), + (TYPE_DYNAMIC_HIERCONFIG, "DYNAMIC_HIERCONFIG"), (TYPE_CUSTOM, "CUSTOM_REMEDIATION"), ) diff --git a/nautobot_golden_config/datasources.py b/nautobot_golden_config/datasources.py index c8953736c..8dab28d78 100644 --- a/nautobot_golden_config/datasources.py +++ b/nautobot_golden_config/datasources.py @@ -36,6 +36,32 @@ def refresh_git_backup(repository_record, job_result, delete=False): # pylint: ) +def refresh_git_gc_dynamic_remediations(repository_record, job_result, delete=False): # pylint: disable=unused-argument + """Callback for gitrepository updates on Hier Config Dynamic Remediation repo.""" + job_result.log( + "Successfully Pulled git repo test", + level_choice=LogLevelChoices.LOG_DEBUG, + ) + + dynamic_remediation_path = os.path.join(repository_record.filesystem_path, "hier_config_dynamic_remediations") + if not os.path.isdir(dynamic_remediation_path): + job_result.log( + f"Skipping sync for {dynamic_remediation_path} because directory doesn't exist.", + level_choice=LogLevelChoices.LOG_INFO, + ) + return + + for root, _, files in os.walk(dynamic_remediation_path): + for file_name in files: + if not file_name.endswith(".py") or "__init__" in file_name: + continue + # file_info.append({"root": root, "file_name": file_name}) + job_result.log( + f"File {file_name} found in repository.", + level_choice=LogLevelChoices.LOG_INFO + ) + + def refresh_git_gc_properties(repository_record, job_result, delete=False): # pylint: disable=unused-argument """Callback for gitrepository updates on Git Configuration repo. @@ -244,6 +270,18 @@ def update_git_gc_properties(golden_config_path, job_result, gc_config_item): # ), ) ) +if ENABLE_COMPLIANCE: + datasource_contents.append( + ( + "extras.gitrepository", + DatasourceContent( + name="Hier Config Dynamic Remediations", + content_identifier="nautobot_golden_config.hierconfigdynamicremediations", + icon="mdi-file-code", + callback=refresh_git_gc_dynamic_remediations, + ), + ) + ) datasource_contents.append( ( diff --git a/nautobot_golden_config/models.py b/nautobot_golden_config/models.py index 924f51582..57042c93e 100644 --- a/nautobot_golden_config/models.py +++ b/nautobot_golden_config/models.py @@ -2,6 +2,8 @@ import json import logging +import pkgutil +import sys from deepdiff import DeepDiff from django.core.exceptions import ValidationError @@ -11,7 +13,7 @@ from nautobot.core.models.generics import PrimaryModel from nautobot.core.models.utils import serialize_object, serialize_object_v2 from nautobot.dcim.models import Device -from nautobot.extras.models import ObjectChange +from nautobot.extras.models import ObjectChange, GitRepository from nautobot.extras.models.statuses import StatusField from nautobot.extras.utils import extras_features from netutils.config.compliance import feature_compliance @@ -198,7 +200,19 @@ def _get_hierconfig_remediation(obj): host.load_generated_config(obj.intended) host.load_running_config(obj.actual) - host.remediation_config() + rem = host.remediation_config() + + if remediation_setting_obj.remediation_type == RemediationTypeChoice.TYPE_DYNAMIC_HIERCONFIG: + repos = GitRepository.objects.filter(provided_contents__contains="nautobot_golden_config.hierconfigdynamicremediations") + for repo in repos: + for importer, discovered_module_name, _ in pkgutil.iter_modules( + [f"{repo.filesystem_path}/hier_config_dynamic_remediations"] + ): + if "__init__" in discovered_module_name: + continue + module = importer.find_module(discovered_module_name).load_module(discovered_module_name) + module.remediation(rem) + remediation_config = host.remediation_config_filtered_text(include_tags={}, exclude_tags={}) return remediation_config @@ -210,6 +224,7 @@ def _get_hierconfig_remediation(obj): ComplianceRuleConfigTypeChoice.TYPE_JSON: _get_json_compliance, ComplianceRuleConfigTypeChoice.TYPE_XML: _get_xml_compliance, RemediationTypeChoice.TYPE_HIERCONFIG: _get_hierconfig_remediation, + RemediationTypeChoice.TYPE_DYNAMIC_HIERCONFIG: _get_hierconfig_remediation, } # The below conditionally add the custom provided compliance type for custom_function, custom_type in CUSTOM_FUNCTIONS.items():