Skip to content

Hier Config V3 Upgrade #902

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

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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/902.changed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Upgraded from hier_config v2.2.2 to v3.2.2, which is a breaking change from the hier_config side. The hier_config implementation was updated to reflect hier_config v3.
2 changes: 1 addition & 1 deletion docs/user/app_feature_remediation.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Default Hier config options can be used or customized on a per platform basis, a
![Hier Options Customization](../images/remediation_hier_edit_options.png)

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/
https://hier-config.readthedocs.io/en/latest/

### Custom Config Remediation Type

Expand Down
30 changes: 19 additions & 11 deletions nautobot_golden_config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from django.db import models
from django.db.models.manager import BaseManager
from django.utils.module_loading import import_string
from hier_config import Host as HierConfigHost
from hier_config import WorkflowRemediation, get_hconfig
from hier_config.utils import hconfig_v2_os_v3_platform_mapper, load_hconfig_v2_options
from nautobot.apps.models import RestrictedQuerySet
from nautobot.apps.utils import render_jinja2
from nautobot.core.models.generics import PrimaryModel
Expand Down Expand Up @@ -177,32 +178,39 @@ def _verify_get_custom_compliance_data(compliance_details):

def _get_hierconfig_remediation(obj):
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be great to document and standardize and document inputs in this method

"""Returns the remediating config."""
hierconfig_os = obj.device.platform.network_driver_mappings["hier_config"]
hierconfig_os = obj.device.platform.network_driver_mappings.get("hier_config")
Copy link
Author

Choose a reason for hiding this comment

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

While testing the hier config v3 update, I noticed that if the network_driver_mappings wasn't created, because of an unsupported driver, then this would cause an exception that isn't handled. By changing this to .get, it produces a default value of None, which would cause the ValidationError below and handled appropriately.


if not hierconfig_os:
raise ValidationError(f"platform {obj.network_driver} is not supported by hierconfig.")
raise ValidationError(f"platform {obj.device.platform.network_driver} is not supported by hierconfig.")
Copy link
Author

Choose a reason for hiding this comment

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

While testing this function, I noticed that obj.network_driver is not the correct syntax, it should be obj.device.platform.network_driver. This would cause a separate exception that is not handled.


try:
remediation_setting_obj = RemediationSetting.objects.get(platform=obj.rule.platform)
except Exception as err: # pylint: disable=broad-except:
raise ValidationError(f"Platform {obj.network_driver} has no Remediation Settings defined.") from err
raise ValidationError(
f"Platform {obj.device.platform.network_driver} has no Remediation Settings defined."
Copy link
Author

Choose a reason for hiding this comment

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

While testing this function, I noticed that obj.network_driver is not the correct syntax, it should be obj.device.platform.network_driver. This would cause a separate exception that is not handled.

Copy link
Contributor

Choose a reason for hiding this comment

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

RemediationSetting is set per Platform, so probably we don't need network_driver here

Copy link
Author

Choose a reason for hiding this comment

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

The network driver is specifically used to map to a hier config driver in netutils.

) from err

remediation_options = remediation_setting_obj.remediation_options

try:
hc_kwargs = {"hostname": obj.device.name, "os": hierconfig_os}
hierconfig_os = hconfig_v2_os_v3_platform_mapper(hierconfig_os)

if remediation_options:
hc_kwargs.update(hconfig_options=remediation_options)
host = HierConfigHost(**hc_kwargs)
hierconfig_os = load_hconfig_v2_options(remediation_options, hierconfig_os)

hierconfig_running_config = get_hconfig(hierconfig_os, obj.actual)
hierconfig_intended_config = get_hconfig(hierconfig_os, obj.intended)
hierconfig_wfr = WorkflowRemediation(
hierconfig_running_config,
hierconfig_intended_config,
)

except Exception as err: # pylint: disable=broad-except:
raise Exception( # pylint: disable=broad-exception-raised
f"Cannot instantiate HierConfig on {obj.device.name}, check Device, Platform and Hier Options."
) from err

host.load_generated_config(obj.intended)
host.load_running_config(obj.actual)
host.remediation_config()
remediation_config = host.remediation_config_filtered_text(include_tags={}, exclude_tags={})
remediation_config = hierconfig_wfr.remediation_config_filtered_text(include_tags={}, exclude_tags={})

return remediation_config

Expand Down
110 changes: 110 additions & 0 deletions nautobot_golden_config/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Unit tests for nautobot_golden_config models."""

from unittest.mock import patch

from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models.deletion import ProtectedError
Expand All @@ -15,6 +17,7 @@
ConfigReplace,
GoldenConfigSetting,
RemediationSetting,
_get_hierconfig_remediation,
)
from nautobot_golden_config.tests.conftest import create_git_repos

Expand Down Expand Up @@ -575,3 +578,110 @@ def test_create_remediation_setting_default_values(self):
self.assertEqual(remediation_setting.platform, self.platform)
self.assertEqual(remediation_setting.remediation_type, RemediationTypeChoice.TYPE_HIERCONFIG)
self.assertEqual(remediation_setting.remediation_options, {})


class GetHierConfigRemediationTestCase(TestCase):
"""Test _get_hierconfig_remediation function."""

def test_successful_remediation(self):
"""Test successful remediation generation."""
device = create_device()
compliance_rule_cli = create_feature_rule_cli_with_remediation(device)

RemediationSetting.objects.create(
platform=device.platform,
remediation_type=RemediationTypeChoice.TYPE_HIERCONFIG,
remediation_options={},
)
config_compliance = ConfigCompliance(
device=device,
rule=compliance_rule_cli,
actual="interface Ethernet1\n no shutdown",
intended="interface Ethernet1\n description Test\n no shutdown\n",
)
remediation = _get_hierconfig_remediation(config_compliance)

self.assertIsInstance(remediation, str)
self.assertIn("interface Ethernet1\n description Test", remediation)

def test_platform_not_supported_by_hierconfig(self):
"""Test error when platform is not supported by hierconfig."""
device = create_device()
device.platform.network_driver = "unsupported_driver"
device.platform.save()

compliance_rule_cli = create_feature_rule_cli_with_remediation(device)

config_compliance = ConfigCompliance(
device=device,
rule=compliance_rule_cli,
actual="interface Ethernet1\n no shutdown",
intended="interface Ethernet1\n description Test\n no shutdown\n",
)

# Validate that calling the function raises a ValidationError
with self.assertRaises(ValidationError) as context:
_get_hierconfig_remediation(config_compliance)

self.assertIn("not supported by hierconfig", str(context.exception))

def test_no_remediation_settings_defined(self):
"""Test error when no remediation settings are defined for the platform."""
device = create_device()
compliance_rule_cli = create_feature_rule_cli_with_remediation(device)

config_compliance = ConfigCompliance(
device=device,
rule=compliance_rule_cli,
actual="interface Ethernet1\n no shutdown",
intended="interface Ethernet1\n description Test\n no shutdown\n",
)
# Make sure no remediation settings exist for this platform
RemediationSetting.objects.filter(platform=device.platform).delete()

with self.assertRaises(ValidationError) as context:
_get_hierconfig_remediation(config_compliance)

self.assertIn("has no Remediation Settings defined", str(context.exception))

@patch("nautobot_golden_config.models.hconfig_v2_os_v3_platform_mapper")
@patch("nautobot_golden_config.models.get_hconfig")
@patch("nautobot_golden_config.models.WorkflowRemediation")
def test_hierconfig_instantiation_error(self, mock_workflow_remediation, mock_get_hconfig, mock_mapper):
"""Test error when HierConfig instantiation fails."""
device = create_device()
compliance_rule_cli = create_feature_rule_cli_with_remediation(device)

RemediationSetting.objects.create(
platform=device.platform,
remediation_type=RemediationTypeChoice.TYPE_HIERCONFIG,
)

# Set up mocks to raise an exception
mock_mapper.return_value = "ios"
mock_get_hconfig.side_effect = Exception("Test exception")

# We won't reach the WorkflowRemediation instantiation, but configure it anyway
# to satisfy pylint
mock_instance = mock_workflow_remediation.return_value
mock_instance.remediation_config_filtered_text.return_value = "mock remediation"

# Create a mock ConfigCompliance object
config_compliance = ConfigCompliance(
device=device,
rule=compliance_rule_cli,
actual="interface Ethernet1\n no shutdown",
intended="interface Ethernet1\n description Test\n no shutdown\n",
)

# Validate that calling the function raises an Exception
with self.assertRaises(Exception) as context:
_get_hierconfig_remediation(config_compliance)

self.assertIn("Cannot instantiate HierConfig", str(context.exception))

# Verify mock usage
mock_mapper.assert_called_once_with("ios")
mock_get_hconfig.assert_called_once()
# WorkflowRemediation should never be called since get_hconfig raises exception
mock_workflow_remediation.assert_not_called()
Loading