diff --git a/docs/config-view.md b/docs/config-view.md new file mode 100644 index 0000000..b9f9df1 --- /dev/null +++ b/docs/config-view.md @@ -0,0 +1,90 @@ +# Config View + +A Config View is an abstraction layer for network device configurations. It provides a structured, Pythonic way to interact with and extract information from raw configuration data. Config Views are especially useful for standardizing how configuration elements are accessed across different platforms and devices. + +The framework uses a combination of abstract base classes (e.g., `ConfigViewInterfaceBase`, `HConfigViewBase`) and platform-specific implementations (e.g., `ConfigViewInterfaceCiscoIOS`, `HConfigViewCiscoIOS`) to provide a unified interface for interacting with configurations while accounting for the unique syntax and semantics of each vendor or platform. + +## Why Use Config Views? + +1. **Vendor Abstraction:** Network devices from different vendors (Cisco, Arista, Juniper, etc.) have varied configuration formats. Config Views standardize access, making it easier to work across platforms. + +2. **Simplified Interface:** Accessing configuration data becomes more intuitive through Python properties and methods rather than manually parsing text. + +3. **Extensibility:** Easily extendable to support new platforms or devices by implementing platform-specific subclasses. + +4. **Error Reduction:** Encapsulates parsing logic, reducing the risk of errors due to configuration syntax differences. + +## Available Config Views + +| **Property/Method** | **Type** | **Description** | +|-----------------------------|--------------------------------|------------------------------------------------------------------------------| +| `bundle_interface_views` | `Iterable` | Yields interfaces configured as bundles. | +| `config` | `HConfig` | Root configuration object. | +| `dot1q_mode_from_vlans` | `Callable` | Determines 802.1Q mode based on VLANs and tagging. | +| `hostname` | `Optional[str]` | Retrieves the device hostname. | +| `interface_names_mentioned` | `frozenset[str]` | Set of all interface names mentioned. | +| `interface_view_by_name` | `Callable` | Returns view of a specific interface by name. | +| `interface_views` | `Iterable` | Yields all interface views. | +| `interfaces` | `Iterable[HConfigChild]` | Yields raw configuration objects for all interfaces. | +| `interfaces_names` | `Iterable[str]` | Yields the names of all interfaces. | +| `ipv4_default_gw` | `Optional[IPv4Address]` | Retrieves the IPv4 default gateway. | +| `location` | `str` | Returns the SNMP location. | +| `module_numbers` | `Iterable[int]` | Yields module numbers from interfaces. | +| `stack_members` | `Iterable[StackMember]` | Yields stack members configured on the device. | +| `vlan_ids` | `frozenset[int]` | Set of VLAN IDs configured. | +| `vlans` | `Iterable[Vlan]` | Yields VLAN objects, including ID and name. | + +## Example: Cisco IOS Config View + +### Step 1: Parse Configuration + +Assume we have a Cisco IOS configuration file as a string. + +```python +from hier_config import Platform, get_hconfig + + +raw_config = """ +hostname router1 +interface GigabitEthernet0/1 + description Uplink to Switch + switchport access vlan 10 + ip address 192.168.1.1 255.255.255.0 + shutdown +! +vlan 10 + name DATA +""" + +hconfig = get_hconfig(Platform.CISCO_IOS, raw_config) +``` + +### Step 2: Create Config View + +```python +from hier_config.platforms.cisco_ios.view import HConfigViewCiscoIOS + + +config_view = HConfigViewCiscoIOS(hconfig) +``` + +### Step 3: Access Configuration Details + +Access properties to interact with the configuration programmatically: + +```python +# Get the hostname +print(config_view.hostname) # Output: router1 + +# List all interface names +print(list(config_view.interfaces_names)) # Output: ['GigabitEthernet0/1'] + +# Check if an interface is enabled +for interface_view in config_view.interface_views: + print(interface_view.name, "Enabled:", interface_view.enabled) + +# Get all VLANs +for vlan in config_view.vlans: + print(f"VLAN {vlan.id}: {vlan.name}") + +``` \ No newline at end of file diff --git a/docs/custom-workflows.md b/docs/custom-workflows.md new file mode 100644 index 0000000..0b18c9c --- /dev/null +++ b/docs/custom-workflows.md @@ -0,0 +1,221 @@ +# Creating Custom Workflows + +Certain scenarios demand remediation strategies that go beyond the standard negation and idempotency workflows Hier Config is designed to handle. To address these edge cases, Hier Config allows for custom remediation workflows that integrate seamlessly with the existing remediation process. + +---- + +## Building a Remediation Workflow + +1. Importing Modules and Loading Configurations + +Start by importing the necessary modules and loading the running and intended configurations for comparison. + +```python +from hier_config import WorkflowRemediation, get_hconfig, Platform +from hier_config.utils import load_device_config +``` + +Load the configurations from files: + +```python +running_config = load_device_config("./tests/fixtures/running_config_acl.conf") +generated_config = load_device_config("./tests/fixtures/generated_config_acl.conf") +``` + +These configurations represent the current and desired states of the device. + +---- + +2. Initializing the Workflow Remediation Object: + +Initialize the WorkflowRemediation object for a Cisco IOS platform: + + +```python +wfr = WorkflowRemediation( + running_config=get_hconfig(Platform.CISCO_IOS, running_config), + generated_config=get_hconfig(Platform.CISCO_IOS, generated_config) +) +``` +This object manages the remediation workflow between the running and generated configurations. + +---- + +## Extracting and Analyzing Remediation Sections + +### Example: Access-List Custom Remediation + +**Current (Running) Configuration** + +```python +print(wfr.running_config.get_child(startswith="ip access-list")) +``` + +Output: + +``` +ip access-list extended TEST + 12 permit ip 10.0.0.0 0.0.0.7 any + exit +``` + +**Intended (Generated) Configuration** + +```python +print(wfr.generated_config.get_child(startswith="ip access-list")) +``` + +Output: + +``` +ip access-list extended TEST + 10 permit ip 10.0.1.0 0.0.0.255 any + 20 permit ip 10.0.0.0 0.0.0.7 any + exit +``` + +**Default Remediation Configuration** + +```python +print(wfr.remediation_config.get_child(startswith="ip access-list")) +``` + +Output: + +``` +ip access-list extended TEST + no 12 permit ip 10.0.0.0 0.0.0.7 any + 10 permit ip 10.0.1.0 0.0.0.255 any + 20 permit ip 10.0.0.0 0.0.0.7 any + exit +``` + +#### Issues with the Default Remediation: + +1. **Invalid Command:** `no 12 permit ip 10.0.0.0 0.0.0.7 any` is invalid in Cisco IOS. The valid command is `no 12`. +2. **Risk of Lockout:** Removing a line currently matched by traffic could cause a connectivity outage. +3. **Unnecessary Changes:** `permit ip 10.0.0.0 0.0.0.7 any` is a valid line aside from sequence numbers. In large ACLs, this might be unnecessary to delete and re-add. + +---- + +#### Goals for Safe Access-List Remediation + +To avoid outages during production changes: + +1. **Resequence the ACL:** Adjust sequence numbers using the ip access-list resequence command. + * For demonstration, resequence to align 12 to 20. +2. **Temporary Allow-All:** Add a temporary rule (1 permit ip any any) to prevent lockouts. +2. **Cleanup:** Remove the temporary rule (no 1) after applying the changes. + +---- + +## Building the Custom Remediation + +1. Create a Custom `HConfig` Object + +```python +from hier_config import HConfig + +custom_remediation = HConfig(wfr.running_config.driver) +``` + +2. Add Resequencing and Extract ACL Remediation + +```python +custom_remediation.add_child("ip access-list resequence TEST 10 10") +custom_remediation.add_child("ip access-list extended TEST") +remediation = wfr.remediation_config.get_child(equals="ip access-list extended TEST") +``` + +3. Build the Custom ACL Remediation + +```python +acl = custom_remediation.get_child(equals="ip access-list extended TEST") +acl.add_child("1 permit ip any any") # Temporary allow-all + +for line in remediation.all_children(): + if line.text.startswith("no "): + # Adjust invalid sequence negation + parts = line.text.split() + rounded_number = round(int(parts[1]), -1) + acl.add_child(f"{parts[0]} {rounded_number}") + else: + acl.add_child(line.text) + +acl.add_child("no 1") # Cleanup temporary rule +``` + +### Output of Custom Remediation + +```python +print(custom_remediation) +``` + +Output: + +``` +ip access-list resequence TEST 10 10 +ip access-list extended TEST + 1 permit ip any any + no 10 + 10 permit ip 10.0.1.0 0.0.0.255 any + 20 permit ip 10.0.0.0 0.0.0.7 any + no 1 + exit +``` + +## Applying the Custom Remediation + +### Remove Invalid Remediation + +```python +invalid_remediation = wfr.remediation_config.get_child(equals="ip access-list extended TEST") +wfr.remediation_config.delete_child(invalid_remediation) +``` + +### Add Custom Remediation + +```python +wfr.remediation_config.merge(custom_remediation) +``` + +### Output of Updated Remediation + +```python +print(wfr.remediation_config) +``` + +Output: + +``` +vlan 3 + name switch_mgmt_10.0.3.0/24 + exit +vlan 4 + name switch_mgmt_10.0.4.0/24 + exit +interface Vlan2 + mtu 9000 + ip access-group TEST in + no shutdown + exit +interface Vlan3 + description switch_mgmt_10.0.3.0/24 + ip address 10.0.3.1 255.255.0.0 + exit +interface Vlan4 + mtu 9000 + description switch_mgmt_10.0.4.0/24 + ip address 10.0.4.1 255.255.0.0 + ip access-group TEST in + no shutdown + exit +ip access-list resequence TEST 10 10 +ip access-list extended TEST + 1 permit ip any any + no 10 + 10 permit ip 10.0.1.0 0.0.0.255 any + 20 permit ip 10.0.0.0 0.0.0.7 any + no 1 + exit +``` \ No newline at end of file diff --git a/docs/drivers.md b/docs/drivers.md new file mode 100644 index 0000000..2089af9 --- /dev/null +++ b/docs/drivers.md @@ -0,0 +1,606 @@ +# Drivers in Hier Config + +Drivers represent a modern approach to handling operating system-specific options within Hier Config. Prior to version 3, Hier Config utilized `options` or `hconfig_options`, which were defined as dictionaries, to specify OS-specific parameters. Starting with version 3, these options have been replaced by drivers, which are implemented as Pydantic models and loaded as Python classes, offering improved structure and validation. + +> **Note:** Many of the options available in the Hier Config version 3 driver format are similar to those in the version 2 options format. However, some options have been removed because they are no longer used in version 3, or their names have been updated for consistency or clarity. + +## What is a Driver? + +A driver in Hier Config defines a structured and systematic approach to managing operating system-specific configurations for network devices. It acts as a framework that encapsulates the rules, transformations, and behaviors required to process and normalize device configurations. + +Drivers provide a consistent way to handle configurations by applying a set of specialized logic, including: + +1. **Negation Handling**: Ensures commands are properly negated or reset according to the operating system's syntax and behavior, maintaining consistency in enabling or disabling features. + +2. **Sectional Exiting Rules**: Defines how to navigate in and out of hierarchical configuration sections, ensuring commands are logically grouped and the configuration maintains its structural integrity. + +3. **Command Ordering**: Establishes the sequence in which commands should be applied based on dependencies or importance, preventing conflicts or misconfigurations during deployment. + +4. **Line Substitutions**: Cleans up unnecessary or temporary data in configurations, such as metadata, system-generated comments, or obsolete commands, resulting in a streamlined and standardized output. + +5. **Idempotency Management**: Identifies and enforces commands that should not be duplicated, ensuring repeated application of the configuration does not lead to redundant or conflicting entries. + +6. **Post-Processing Callbacks**: Performs additional adjustments or enhancements after the initial configuration is processed, such as refining access control lists or applying custom transformations specific to the device's operating system. + +By defining these rules and behaviors in a reusable way, a driver enables Hier Config to adapt seamlessly to different operating systems while maintaining a consistent interface for configuration management. This abstraction allows users to work with configurations in a predictable and efficient manner, regardless of the underlying system-specific requirements. + +--- + +## Built-In Drivers in Hier Config + +The following drivers are included in Hier Config: + +- **ARISTA_EOS** +- **CISCO_IOS** +- **CISCO_XR** +- **CISCO_NXOS** +- **GENERIC** +- **HP_COMWARE5** +- **HP_PROCURVE** +- **JUNIPER_JUNOS** +- **VYOS** + +To activate a driver, use the `get_hconfig_driver` utility provided by Hier Config: + +```python +from hier_config import get_hconfig_driver, Platform + +# Example: Activating the CISCO_IOS driver +driver = get_hconfig_driver(Platform.CISCO_IOS) +``` + +### Structure of Each Section and How Rules Are Built + +In Hier Config, the rules within a driver are organized into sections, each targeting a specific aspect of device configuration processing. These sections use Pydantic models to define the behavior and ensure consistency. Here's a breakdown of each section and its associated models: + +--- + +### 1. Negation Rules +**Purpose**: Define how to negate commands or reset them to a default state. + +- **Models**: + - **`NegationDefaultWithRule`**: + - `match_rules`: A tuple of `MatchRule` objects defining the conditions under which the rule applies. + - `use`: The text to use as the negation command. + + - **`NegationDefaultWhenRule`**: + - `match_rules`: A tuple of `MatchRule` objects for matching conditions where negation is default. + +--- + +### 2. Sectional Exiting +**Purpose**: Manage hierarchical configuration sections by defining commands for properly exiting each section. + +- **Models**: + - **`SectionalExitingRule`**: + - `match_rules`: A tuple of `MatchRule` objects defining the section's boundaries. + - `exit_text`: The command used to exit the section. + +--- + +### 3. Ordering +**Purpose**: Assign weights to commands to control the order of operations during configuration application. + +- **Models**: + - **`OrderingRule`**: + - `match_rules`: A tuple of `MatchRule` objects defining the commands to be ordered. + - `weight`: An integer determining the order (lower weights are processed earlier). + +--- + +### 4. Per-Line Substitutions +**Purpose**: Modify or clean up specific lines in the configuration. + +- **Models**: + - **`PerLineSubRule`**: + - `search`: A string or regex to search for. + - `replace`: The replacement text. + + - **`FullTextSubRule`**: + - Similar to `PerLineSubRule`, but applies to the entire text rather than individual lines. + +--- + +### 5. Idempotent Commands +**Purpose**: Ensure commands are not repeated unnecessarily in the configuration. + +- **Models**: + - **`IdempotentCommandsRule`**: + - `match_rules`: A tuple of `MatchRule` objects defining idempotent commands. + + - **`IdempotentCommandsAvoidRule`**: + - `match_rules`: Specifies commands that should be avoided during idempotency checks. + +--- + +### 6. Post-Processing Callbacks +**Purpose**: Apply additional transformations after initial configuration processing. + +- **Implementation**: + - A list of functions or methods called after the driver rules are applied, enabling custom logic specific to the platform. + +--- + +### 7. Tagging and Overwriting +**Purpose**: Apply tags to configuration lines or define overwriting behavior for specific sections. + +- **Models**: + - **`TagRule`**: + - `match_rules`: A tuple of `MatchRule` objects defining the lines to tag. + - `apply_tags`: A frozenset of tags to apply. + + - **`SectionalOverwriteRule`**: + - `match_rules`: Defines sections that can be overwritten. + + - **`SectionalOverwriteNoNegateRule`**: + - Similar to `SectionalOverwriteRule`, but prevents negation. + +--- + +### 8. Indentation Adjustments +**Purpose**: Define start and end points for adjusting indentation within configurations. + +- **Models**: + - **`IndentAdjustRule`**: + - `start_expression`: Regex or text marking the start of an adjustment. + - `end_expression`: Regex or text marking the end of an adjustment. + +--- + +### 9. Match Rules +**Purpose**: Provide a flexible way to define conditions for matching configuration lines. + +- **Models**: + - **`MatchRule`**: + - `equals`: Matches lines that are exactly equal. + - `startswith`: Matches lines that start with the specified text or tuple of text. + - `endswith`: Matches lines that end with the specified text or tuple of text. + - `contains`: Matches lines that contain the specified text or tuple of text. + - `re_search`: Matches lines using a regular expression. + +--- + +### 10. Instance Metadata +**Purpose**: Manage metadata for configuration instances, such as tags and comments. + +- **Models**: + - **`Instance`**: + - `id`: A unique positive integer identifier. + - `comments`: A frozenset of comments. + - `tags`: A frozenset of tags. + +--- + +### 11. Dumping Configuration +**Purpose**: Represent and handle the output of processed configuration lines. + +- **Models**: + - **`DumpLine`**: + - `depth`: Indicates the hierarchy level of the line. + - `text`: The configuration text. + - `tags`: A frozenset of tags associated with the line. + - `comments`: A frozenset of comments associated with the line. + - `new_in_config`: A boolean indicating if the line is new. + + - **`Dump`**: + - `lines`: A tuple of `DumpLine` objects representing the processed configuration. + +--- + +### General Rule-Building Patterns + +1. **Define Matching Conditions**: + - Use `MatchRule` to specify conditions for each rule, ensuring flexible and precise control over which configuration lines a rule applies to. + +2. **Apply Context-Specific Logic**: + - Use specialized models like `SectionalExitingRule` or `IdempotentCommandsRule` to tailor behavior to hierarchical or idempotency-related scenarios. + +3. **Maintain Immutability**: + - All models use Pydantic’s immutability and validation to enforce the integrity of rules and configurations. + +This structure ensures that drivers are modular, extensible, and capable of handling diverse configuration scenarios across different platforms. + +## Customizing Existing Drivers + +This guide provides two examples of how to extend the rules for a Cisco IOS driver in Hier Config. The first example involves subclassing the driver to customize and add rules. The second example demonstrates extending the driver rules dynamically after the driver has already been instantiated. + +--- + +### Example 1: Subclassing the Driver to Extend Rules + +In this approach, you create a new class that subclasses the base Cisco IOS driver and overrides its `_instantiate_rules` method to customize the rules. + +```python +from hier_config.models import ( + MatchRule, + NegationDefaultWithRule, + SectionalExitingRule, + OrderingRule, + PerLineSubRule, + IdempotentCommandsRule, +) +from hier_config.drivers.cisco_ios import HConfigDriverCiscoIOS + + +class ExtendedHConfigDriverCiscoIOS(HConfigDriverCiscoIOS): + @staticmethod + def _instantiate_rules(): + # Start with the base rules + base_rules = super()._instantiate_rules() + + # Extend negation rules + base_rules.negate_with.append( + NegationDefaultWithRule( + match_rules=(MatchRule(startswith="ip route "),), + use="no ip route" + ) + ) + + # Extend sectional exiting rules + base_rules.sectional_exiting.append( + SectionalExitingRule( + match_rules=( + MatchRule(startswith="policy-map"), + MatchRule(startswith="class"), + ), + exit_text="exit", + ) + ) + + # Add additional ordering rules + base_rules.ordering.append( + OrderingRule( + match_rules=( + MatchRule(startswith="access-list"), + MatchRule(startswith="permit "), + ), + weight=50, + ) + ) + + # Add new per-line substitutions + base_rules.per_line_sub.append( + PerLineSubRule( + search="^!.*Generated by system.*$", replace="" + ) + ) + + # Add new idempotent commands + base_rules.idempotent_commands.append( + IdempotentCommandsRule( + match_rules=( + MatchRule(startswith="interface "), + MatchRule(startswith="speed "), + ) + ) + ) + + return base_rules +``` + +#### Using the Subclassed Driver + +```python +from hier_config import Platform + +# Example function to activate the extended driver +def get_extended_hconfig_driver(platform: Platform): + if platform == Platform.CISCO_IOS: + return ExtendedHConfigDriverCiscoIOS() + raise ValueError(f"Unsupported platform: {platform}") + +# Activate the extended driver +driver = get_extended_hconfig_driver(Platform.CISCO_IOS) +``` + +### Example 2: Dynamically Extending Rules for an Instantiated Driver + +If you already have the driver instantiated, you can modify its rules dynamically by directly appending to the appropriate sections. + +```python +from hier_config import get_hconfig_driver, Platform +from hier_config.models import ( + MatchRule, + NegationDefaultWithRule, + SectionalExitingRule, + OrderingRule, + PerLineSubRule, + IdempotentCommandsRule, +) + +# Instantiate the driver +driver = get_hconfig_driver(Platform.CISCO_IOS) + +# Dynamically extend negation rules +driver.rules.negate_with.append( + NegationDefaultWithRule( + match_rules=(MatchRule(startswith="ip route "),), + use="no ip route" + ) +) + +# Dynamically extend sectional exiting rules +driver.rules.sectional_exiting.append( + SectionalExitingRule( + match_rules=( + MatchRule(startswith="policy-map"), + MatchRule(startswith="class"), + ), + exit_text="exit", + ) +) + +# Add additional ordering rules dynamically +driver.rules.ordering.append( + OrderingRule( + match_rules=( + MatchRule(startswith="access-list"), + MatchRule(startswith="permit "), + ), + weight=50, + ) +) + +# Add new per-line substitutions dynamically +driver.rules.per_line_sub.append( + PerLineSubRule( + search="^!.*Generated by system.*$", replace="" + ) +) + +# Add new idempotent commands dynamically +driver.rules.idempotent_commands.append( + IdempotentCommandsRule( + match_rules=( + MatchRule(startswith="interface "), + MatchRule(startswith="speed "), + ) + ) +) +``` + +#### Explanation + +* **Dynamic Rule Extension:** You directly modify the driver.rules attributes to append new rules dynamically. +* **Flexibility:** This approach is useful when the driver is instantiated by external code, and subclassing is not feasible. + +Both approaches allow you to extend the functionality of the Cisco IOS driver: + +1. **Subclassing:** Recommended for reusable, modular extensions where the driver logic can be encapsulated in a new class. +2. **Dynamic Modification:** Useful when the driver is instantiated dynamically, and you need to modify the rules at runtime. + +## Creating a Custom Driver + +This guide walks you through the process of creating a custom driver using the `HConfigDriverBase` class from the `hier_config.platforms.driver_base` module. Custom drivers allow you to define operating system-specific rules and behaviors for managing device configurations. + +--- + +### Overview of `HConfigDriverBase` + +The `HConfigDriverBase` class provides a foundation for defining driver-specific rules and behaviors. It encapsulates configuration rules and methods for handling idempotency, negation, and more. You will extend this class to create a new driver. + +Key Components: +1. **`HConfigDriverRules`**: A collection of rules for handling configuration logic. +2. **Methods to Override**: Define custom behavior by overriding the `_instantiate_rules` method. +3. **Properties**: Adjust behavior for negation and declaration prefixes. + +--- + +### Steps to Create a Custom Driver + +#### Step 1: Subclass `HConfigDriverBase` +Begin by subclassing `HConfigDriverBase` to define a new driver. + +```python +from hier_config.platforms.driver_base import HConfigDriverBase, HConfigDriverRules +from hier_config.models import ( + MatchRule, + NegationDefaultWithRule, + SectionalExitingRule, + OrderingRule, + PerLineSubRule, + IdempotentCommandsRule, +) + + +class CustomHConfigDriver(HConfigDriverBase): + """Custom driver for a specific operating system.""" + + @staticmethod + def _instantiate_rules() -> HConfigDriverRules: + """Define the rules for this custom driver.""" + return HConfigDriverRules( + negate_with=[ + NegationDefaultWithRule( + match_rules=(MatchRule(startswith="ip route "),), + use="no ip route" + ) + ], + sectional_exiting=[ + SectionalExitingRule( + match_rules=( + MatchRule(startswith="policy-map"), + MatchRule(startswith="class"), + ), + exit_text="exit" + ) + ], + ordering=[ + OrderingRule( + match_rules=(MatchRule(startswith="interface"),), + weight=10 + ) + ], + per_line_sub=[ + PerLineSubRule( + search="^!.*Generated by system.*$", + replace="" + ) + ], + idempotent_commands=[ + IdempotentCommandsRule( + match_rules=(MatchRule(startswith="interface"),) + ) + ], + ) +``` + +#### Step 2: Customize Negation or Declaration Prefixes (Optional) +Override the `negation_prefix` or `declaration_prefix` properties to customize their behavior. + +```python + @property + def negation_prefix(self) -> str: + """Customize the negation prefix.""" + return "disable " + + @property + def declaration_prefix(self) -> str: + """Customize the declaration prefix.""" + return "enable " +``` + +#### Step 3: Use the Custom Driver + +This section describes how to use the custom driver by extending the `get_hconfig_driver` function and adding a new platform to the `Platform` model. It also covers how to load the driver into Hier Config and utilize it for remediation workflows. + +--- + +##### 1. Extend `get_hconfig_driver` to Include the Custom Driver + +First, modify the `get_hconfig_driver` function to include the new custom driver: + +```python +from hier_config.platforms.driver_base import HConfigDriverBase +from hier_config import get_hconfig_driver +from .custom_driver import CustomHConfigDriver # Import your custom driver +from hier_config.models import Platform + +def get_custom_hconfig_driver(platform: Union[CustomPlatform,Platform]) -> HConfigDriverBase: + """Create base options on an OS level.""" + if platform == CustomPlatform.CUSTOM_DRIVER: + return CustomHConfigDriver() + return get_hconfig_driver(platform) +``` + +##### 2. Create a custom `Platform` to Include the Custom Driver + +```python +from enum import Enum, auto + +class CustomPlatform(str, Enum): + CUSTOM_PLATFORM = auto() +``` + +##### 3. Load the Driver into `HConfig` + +```python +from .custom_platform import CustomPlatform +from hier_config import get_hconfig +from hier_config.utils import load_device_config + +# Load running and intended configurations from files +running_config_text = load_device_config("./tests/fixtures/running_config.conf") +generated_config_text = load_device_config("./tests/fixtures/remediation_config.conf") + +# Create HConfig objects for running and intended configurations +running_config = get_hconfig(CustomPlatform.CUSTOM_DRIVER, running_config_text) +generated_config = get_hconfig(CustomPlatform.CUSTOM_DRIVER, generated_config_text) +``` + +##### 4. Instantiate a `WorkflowRemediation` + +```python +from hier_config import WorkflowRemediation + +# Instantiate the remediation workflow +workflow = WorkflowRemediation(running_config, generated_config) +``` + + +### Key Methods in HConfigDriverBase + +1. `idempotent_for`: + * Matches configurations against idempotent rules to prevent duplication. + +```python +def idempotent_for( + self, + config: HConfigChild, + other_children: Iterable[HConfigChild], +) -> Optional[HConfigChild]: + ... +``` + +2. `negate_with`: + * Provides a negation command based on rules. + +```python +def negate_with(self, config: HConfigChild) -> Optional[str]: + ... +``` + +3. `swap_negation`: + * Toggles the negation of a command. + +```python +def swap_negation(self, child: HConfigChild) -> HConfigChild: + ... +``` + +4. Properties: + * `negation_prefix`: Default is `"no "`. + * `declaration_prefix`: Default is `""`. + +### Example Rule Definitions + +#### Negation Rules +Define commands that require specific negation handling: + +```python +negate_with=[ + NegationDefaultWithRule( + match_rules=(MatchRule(startswith="ip route "),), + use="no ip route" + ) +] +``` + +#### Sectional Exiting +Define how to exit specific configuration sections: + +```python +sectional_exiting=[ + SectionalExitingRule( + match_rules=( + MatchRule(startswith="policy-map"), + MatchRule(startswith="class"), + ), + exit_text="exit" + ) +] +``` + +#### Command Ordering +Set the execution order of specific commands: + +```python +ordering=[ + OrderingRule( + match_rules=(MatchRule(startswith="interface"),), + weight=10 + ) +] +``` + +#### Per-Line Substitution +Clean up unwanted lines in the configuration: + +```python +per_line_sub=[ + PerLineSubRule( + search="^!.*Generated by system.*$", + replace="" + ) +] +``` diff --git a/docs/future-config.md b/docs/future-config.md new file mode 100644 index 0000000..672973d --- /dev/null +++ b/docs/future-config.md @@ -0,0 +1,116 @@ +# Future Config + +The Future Config feature, introduced in version 2.2.0, attempts to predict the state of the running configuration after a change is applied. + +This feature is useful in scenarios where you need to determine the anticipated configuration state following a change, such as: +- Verifying that a configuration change was successfully applied to a device + - For example, checking if the post-change configuration matches the predicted future configuration +- Generating a future-state configuration that can be analyzed by tools like Batfish to assess the potential impact of a change +- Building rollback configurations: once the future configuration state is known, a rollback configuration can be generated by simply creating the remediation in reverse `(rollback = future.config_to_get_to(running))`. + - When building rollbacks for a series of configuration changes, you can use the future configuration from each change as input for the subsequent change. For example, use the future configuration after Change 1 as the input for determining the future configuration after Change 2, and so on. + ```shell + post_change_1_config = running_config.future(change_1_config) + change_1_rollback_config = post_change_1_config.config_to_get_to(running_config) + post_change_2_config = post_change_1_config.future(change_2_config) + change_2_rollback_config = post_change_2_config.config_to_get_to(post_change_1_config) + ... + ``` + +Currently, this algorithm does not account for: +- negate a numbered ACL when removing an item +- sectional exiting +- negate with +- idempotent command avoid +- idempotent_acl_check +- and likely others + +```bash +>>> from hier_config import get_hconfig, Platform +>>> from hier_config.utils import load_device_config +>>> + +>>> running_config_text = load_device_config("./tests/fixtures/running_config.conf") +>>> generated_config_text = load_device_config("./tests/fixtures/remediation_config_without_tags.conf") +>>> +>>> running_config = get_hconfig(Platform.CISCO_IOS, running_config_text) +>>> remediation_config = get_hconfig(Platform.CISCO_IOS, remediation_config_text) +>>> +>>> print("Running Config") +Running Config +>>> for line in running_config.all_children(): +... print(line.cisco_style_text()) +... +hostname aggr-example.rtr +ip access-list extended TEST + 10 permit ip 10.0.0.0 0.0.0.7 any +vlan 2 + name switch_mgmt_10.0.2.0/24 +vlan 3 + name switch_mgmt_10.0.4.0/24 +interface Vlan2 + descripton switch_10.0.2.0/24 + ip address 10.0.2.1 255.255.255.0 + shutdown +interface Vlan3 + mtu 9000 + description switch_mgmt_10.0.4.0/24 + ip address 10.0.4.1 255.255.0.0 + ip access-group TEST in + no shutdown +>>> +>>> print("Remediation Config") +Remediation Config +>>> for line in remediation_config.all_children(): +... print(line.cisco_style_text()) +... +vlan 3 + name switch_mgmt_10.0.3.0/24 +vlan 4 + name switch_mgmt_10.0.4.0/24 +interface Vlan2 + mtu 9000 + ip access-group TEST in + no shutdown +interface Vlan3 + description switch_mgmt_10.0.3.0/24 + ip address 10.0.3.1 255.255.0.0 +interface Vlan4 + mtu 9000 + description switch_mgmt_10.0.4.0/24 + ip address 10.0.4.1 255.255.0.0 + ip access-group TEST in + no shutdown +>>> +>>> print("Future Config") +Future Config +>>> for line in running_config.future(remediation_config).all_children(): +... print(line.cisco_style_text()) +... +vlan 3 + name switch_mgmt_10.0.3.0/24 +vlan 4 + name switch_mgmt_10.0.4.0/24 +interface Vlan2 + mtu 9000 + ip access-group TEST in + descripton switch_10.0.2.0/24 + ip address 10.0.2.1 255.255.255.0 +interface Vlan3 + description switch_mgmt_10.0.3.0/24 + ip address 10.0.3.1 255.255.0.0 + mtu 9000 + ip access-group TEST in + no shutdown +interface Vlan4 + mtu 9000 + description switch_mgmt_10.0.4.0/24 + ip address 10.0.4.1 255.255.0.0 + ip access-group TEST in + no shutdown +hostname aggr-example.rtr +ip access-list extended TEST + 10 permit ip 10.0.0.0 0.0.0.7 any +vlan 2 + name switch_mgmt_10.0.2.0/24 +>>> +``` \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md index 9ab78a1..9ced331 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -7,7 +7,9 @@ Hier Config is a Python library that assists with remediating network configurat To use `WorkflowRemediation`, you’ll import it along with `get_hconfig` (for generating configuration objects) and `Platform` (for specifying the operating system driver). ```python -from hier_config import get_hconfig, Platform, WorkflowRemediation +>>> from hier_config import WorkflowRemediation, get_hconfig, Platform +>>> from hier_config.utils import load_device_config +>>> ``` With the Host class imported, it can be utilized to create host objects. @@ -18,14 +20,14 @@ Use `get_hconfig` to create HConfig objects for both the running and intended co ```python # Define running and intended configurations as strings -running_config_text = open("./tests/fixtures/running_config.conf").read() -generated_config_text = open("./tests/fixtures/generated_config.conf").read() - +>>> running_config_text = load_device_config("./tests/fixtures/running_config.conf") +>>> generated_config_text = load_device_config("./tests/fixtures/remediation_config.conf") +>>> # Create HConfig objects for running and intended configurations -running_config = get_hconfig(Platform.CISCO_IOS, running_config_text) -generated_config = get_hconfig(Platform.CISCO_IOS, generated_config_text) - +>>> running_config = get_hconfig(Platform.CISCO_IOS, running_config_text) +>>> generated_config = get_hconfig(Platform.CISCO_IOS, generated_config_text) +>>> ``` ## Step 3: Initializing WorkflowRemediation and Generating Remediation @@ -34,18 +36,20 @@ With the HConfig objects created, initialize `WorkflowRemediation` to calculate ```python # Initialize WorkflowRemediation with the running and intended configurations -workflow = WorkflowRemediation(running_config, generated_config) +>>> workflow = WorkflowRemediation(running_config, generated_config) +>>> ``` -### Generating the Remediation Configuration +## Generating the Remediation Configuration -The `remediation_config` attribute generates the configuration needed to apply the intended changes to the device. +The `remediation_config` attribute generates the configuration needed to apply the intended changes to the device. Use `all_children_sorted()` to display the configuration in a readable format: ```python -print(workflow.remediation_config) -``` - -```text +>>> print("Remediation configuration:") +Remediation configuration: +>>> for line in workflow.remediation_config.all_children_sorted(): +... print(line.cisco_style_text()) +... vlan 3 name switch_mgmt_10.0.3.0/24 vlan 4 @@ -63,17 +67,20 @@ interface Vlan4 ip address 10.0.4.1 255.255.0.0 ip access-group TEST in no shutdown +>>> ``` -### Generating the Rollback Configuration +## Generating the Rollback Configuration Similarly, the `rollback_config` attribute generates a configuration that can revert the changes, restoring the device to its original state. ```python -print(workflow.rollback_config) -``` - -```text +# Generate and display the rollback configuration +>>> print("Rollback configuration:") +Rollback configuration: +>>> for line in workflow.rollback_config.all_children_sorted(): +... print(line.cisco_style_text()) +... no vlan 4 no interface Vlan4 vlan 3 @@ -85,4 +92,5 @@ interface Vlan2 interface Vlan3 description switch_mgmt_10.0.4.0/24 ip address 10.0.4.1 255.255.0.0 +>>> ``` \ No newline at end of file diff --git a/docs/install.md b/docs/install.md index 1b8ebb1..651eb2d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,11 +1,11 @@ # Install hier_config -> Hierarchical Configuration requires a minimum Python version of 3.8. +> Hierarchical Configuration requires a minimum Python version of 3.9. Hierarchical Configuration can be installed directly from GitHub or with pip: ## Pip -1. Install from [PyPI](https://pypi.org/project/hier-config/): `pip install hier-config` +1. Install from PyPI: `pip install hier-config` ## Github 1. [Install Poetry](https://python-poetry.org/docs/#installation) diff --git a/docs/junos-style-syntax-remediation.md b/docs/junos-style-syntax-remediation.md new file mode 100644 index 0000000..9126930 --- /dev/null +++ b/docs/junos-style-syntax-remediation.md @@ -0,0 +1,157 @@ +# JunOS-style Syntax Remediation +Operating systems that use "set"-based syntax can now be remediated experimentally. Below is an example of a JunOS-style remediation. + +```bash +$ cat ./tests/fixtures/running_config_flat_junos.conf +set system host-name aggr-example.rtr + +set firewall family inet filter TEST term 1 from source-address 10.0.0.0/29 +set firewall family inet filter TEST term 1 then accept + +set vlans switch_mgmt_10.0.2.0/24 vlan-id 2 +set vlans switch_mgmt_10.0.2.0/24 l3-interface irb.2 + +set vlans switch_mgmt_10.0.4.0/24 vlan-id 3 +set vlans switch_mgmt_10.0.4.0/24 l3-interface irb.3 + +set interfaces irb unit 2 family inet address 10.0.2.1/24 +set interfaces irb unit 2 family inet description "switch_10.0.2.0/24" +set interfaces irb unit 2 family inet disable + +set interfaces irb unit 3 family inet address 10.0.4.1/16 +set interfaces irb unit 3 family inet filter input TEST +set interfaces irb unit 3 family inet mtu 9000 +set interfaces irb unit 3 family inet description "switch_mgmt_10.0.4.0/24" + + +$ python3 +>>> from hier_config import WorkflowRemediation, get_hconfig, Platform +>>> from hier_config.utils import load_device_config +>>> +>>> running_config_text = load_device_config("./tests/fixtures/running_config_flat_junos.conf") +>>> generated_config_text = load_device_config("./tests/fixtures/generated_config_flat_junos.conf") +# Create HConfig objects for the running and generated configurations using JunOS syntax +>>> running_config = get_hconfig(Platform.JUNIPER_JUNOS, running_config_text) +>>> generated_config = get_hconfig(Platform.JUNIPER_JUNOS, generated_config_text) +>>> +# Initialize WorkflowRemediation with the running and generated configurations +>>> workflow = WorkflowRemediation(running_config, generated_config) +>>> +# Generate and display the remediation configuration +>>> print("Remediation configuration:") +Remediation configuration: +>>> print(str(workflow.remediation_config)) +delete vlans switch_mgmt_10.0.4.0/24 vlan-id 3 +delete vlans switch_mgmt_10.0.4.0/24 l3-interface irb.3 +delete interfaces irb unit 2 family inet disable +delete interfaces irb unit 3 family inet address 10.0.4.1/16 +delete interfaces irb unit 3 family inet description "switch_mgmt_10.0.4.0/24" +set vlans switch_mgmt_10.0.3.0/24 vlan-id 3 +set vlans switch_mgmt_10.0.3.0/24 l3-interface irb.3 +set vlans switch_mgmt_10.0.4.0/24 vlan-id 4 +set vlans switch_mgmt_10.0.4.0/24 l3-interface irb.4 +set interfaces irb unit 2 family inet filter input TEST +set interfaces irb unit 2 family inet mtu 9000 +set interfaces irb unit 3 family inet address 10.0.3.1/16 +set interfaces irb unit 3 family inet description "switch_mgmt_10.0.3.0/24" +set interfaces irb unit 4 family inet address 10.0.4.1/16 +set interfaces irb unit 4 family inet filter input TEST +set interfaces irb unit 4 family inet mtu 9000 +set interfaces irb unit 4 family inet description "switch_mgmt_10.0.4.0/24" +>>> +``` + +Configurations loaded into Hier Config with Juniper-style syntax are converted to a flat, `set`-based format. Remediation steps are then generated using this `set` syntax. + +```bash +$ cat ./tests/fixtures/running_config_junos.conf +system { + host-name aggr-example.rtr; +} + +firewall { + family inet { + filter TEST { + term 1 { + from { + source-address 10.0.0.0/29; + } + then { + accept; + } + } + } + } +} + +vlans { + switch_mgmt_10.0.2.0/24 { + vlan-id 2; + l3-interface irb.2; + } + switch_mgmt_10.0.4.0/24 { + vlan-id 3; + l3-interface irb.3; + } +} + +interfaces { + irb { + unit 2 { + family inet { + address 10.0.2.1/24; + description "switch_10.0.2.0/24"; + disable; + } + } + unit 3 { + family inet { + address 10.0.4.1/16; + filter { + input TEST; + } + mtu 9000; + description "switch_mgmt_10.0.4.0/24"; + } + } + } +} + +$ python3 +>>> from hier_config import WorkflowRemediation, get_hconfig, Platform +>>> from hier_config.utils import load_device_config +>>> +>>> running_config_text = load_device_config("./tests/fixtures/running_config_junos.conf") +>>> generated_config_text = load_device_config("./tests/fixtures/generated_config_junos.conf") +# Create HConfig objects for the running and generated configurations using JunOS syntax +>>> running_config = get_hconfig(Platform.JUNIPER_JUNOS, running_config_text) +>>> generated_config = get_hconfig(Platform.JUNIPER_JUNOS, generated_config_text) +>>> +# Initialize WorkflowRemediation with the running and generated configurations +>>> workflow = WorkflowRemediation(running_config, generated_config) +>>> +# Generate and display the remediation configuration +>>> print("Remediation configuration:") +Remediation configuration: +>>> print(str(workflow.remediation_config)) +delete vlans switch_mgmt_10.0.4.0/24 vlan-id 3 +delete vlans switch_mgmt_10.0.4.0/24 l3-interface irb.3 +delete interfaces irb unit 2 family inet description "switch_10.0.2.0/24" +delete interfaces irb unit 2 family inet disable +delete interfaces irb unit 3 family inet address 10.0.4.1/16 +delete interfaces irb unit 3 family inet description "switch_mgmt_10.0.4.0/24" +set vlans switch_mgmt_10.0.3.0/24 vlan-id 3 +set vlans switch_mgmt_10.0.3.0/24 l3-interface irb.3 +set vlans switch_mgmt_10.0.4.0/24 vlan-id 4 +set vlans switch_mgmt_10.0.4.0/24 l3-interface irb.4 +set interfaces irb unit 2 family inet filter input TEST +set interfaces irb unit 2 family inet mtu 9000 +set interfaces irb unit 2 family inet description "switch_mgmt_10.0.2.0/24" +set interfaces irb unit 3 family inet address 10.0.3.1/16 +set interfaces irb unit 3 family inet description "switch_mgmt_10.0.3.0/24" +set interfaces irb unit 4 family inet address 10.0.4.1/16 +set interfaces irb unit 4 family inet filter input TEST +set interfaces irb unit 4 family inet mtu 9000 +set interfaces irb unit 4 family inet description "switch_mgmt_10.0.4.0/24" +>>> +``` \ No newline at end of file diff --git a/docs/advanced-topics.md b/docs/tags.md similarity index 86% rename from docs/advanced-topics.md rename to docs/tags.md index 7b92e7b..101f48e 100644 --- a/docs/advanced-topics.md +++ b/docs/tags.md @@ -1,4 +1,4 @@ -# Advanced Topics +# Working with Tags ## MatchRules @@ -109,24 +109,15 @@ With the tags loaded, you can create a targeted remediation based on those tags #!/usr/bin/env python3 # Import necessary libraries -import yaml -from pydantic import TypeAdapter -from hier_config import WorkflowRemediation, get_hconfig -from hier_config.models import Platform, TagRule +from hier_config import WorkflowRemediation, get_hconfig, Platform +from hier_config.utils import load_device_config, load_hier_config_tags # Load the running and generated configurations from files -with open("./tests/fixtures/running_config.conf") as f: - running_config = f.read() - -with open("./tests/fixtures/generated_config.conf") as f: - generated_config = f.read() +running_config = load_device_config("./tests/fixtures/running_config.conf") +generated_config = load_device_config("./tests/fixtures/generated_config.conf") # Load tag rules from a file -with open("./tests/fixtures/tag_rules_ios.yml") as f: - tags = yaml.safe_load(f) - -# Validate and format tags using the TagRule model -tag_rules = TypeAdapter(tuple[TagRule, ...]).validate_python(tags) +tags = load_hier_config_tags("./tests/fixtures/tag_rules_ios.yml") # Initialize a WorkflowRemediation object with the running and intended configurations wfr = WorkflowRemediation( @@ -135,7 +126,10 @@ wfr = WorkflowRemediation( ) # Apply the tag rules to filter remediation steps by tags -wfr.apply_remediation_tag_rules(tag_rules) +wfr.apply_remediation_tag_rules(tags) + +# Generate the remediation steps +wfr.remediation_config # Display remediation steps filtered to include only the "ntp" tag print(wfr.remediation_config_filtered_text(include_tags={"ntp"}, exclude_tags={})) @@ -148,10 +142,4 @@ no ntp server 192.0.2.1 prefer version 2 ip name-server 1.1.1.1 ip name-server 8.8.8.8 ntp server time.nist.gov -``` - -## Drivers - -## Custom hier_config Workflows - -Coming soon... +``` \ No newline at end of file diff --git a/docs/unified-diff.md b/docs/unified-diff.md new file mode 100644 index 0000000..75d07a6 --- /dev/null +++ b/docs/unified-diff.md @@ -0,0 +1,42 @@ +# Unified diff + +The Unified Diff feature, introduced in version 2.1.0, provides output similar to `difflib.unified_diff()` but with added awareness of out-of-order lines and parent-child relationships in the Hier Config model of configurations being compared. + +This feature is particularly useful when comparing configurations from two network devices, such as redundant pairs, or when validating differences between running and intended configurations. + +Currently, the algorithm does not account for duplicate child entries (e.g., multiple `endif` statements in an IOS-XR route-policy) or enforce command order in sections where it may be critical, such as Access Control Lists (ACLs). For accurate ordering in ACLs, sequence numbers should be used if command order is important. + +```bash +>>> from hier_config import get_hconfig, Platform +>>> from pprint import pprint +>>> +>>> running_config_text = load_device_config("./tests/fixtures/running_config.conf") +>>> generated_config_text = load_device_config("./tests/fixtures/generated_config.conf") +>>> +>>> running_config = get_hconfig(Platform.CISCO_IOS, running_config_text) +>>> generated_config = get_hconfig(Platform.CISCO_IOS, generated_config_text) +>>> +>>> pprint(list(running_config.unified_diff(generated_config))) +['vlan 3', + ' - name switch_mgmt_10.0.4.0/24', + ' + name switch_mgmt_10.0.3.0/24', + 'interface Vlan2', + ' - shutdown', + ' + mtu 9000', + ' + ip access-group TEST in', + ' + no shutdown', + 'interface Vlan3', + ' - description switch_mgmt_10.0.4.0/24', + ' - ip address 10.0.4.1 255.255.0.0', + ' + description switch_mgmt_10.0.3.0/24', + ' + ip address 10.0.3.1 255.255.0.0', + '+ vlan 4', + ' + name switch_mgmt_10.0.4.0/24', + '+ interface Vlan4', + ' + mtu 9000', + ' + description switch_mgmt_10.0.4.0/24', + ' + ip address 10.0.4.1 255.255.0.0', + ' + ip access-group TEST in', + ' + no shutdown'] +>>> +``` \ No newline at end of file diff --git a/hier_config/utils.py b/hier_config/utils.py new file mode 100644 index 0000000..3219ca5 --- /dev/null +++ b/hier_config/utils.py @@ -0,0 +1,33 @@ +from pathlib import Path + +import yaml +from pydantic import TypeAdapter + +from hier_config.models import TagRule + + +def load_device_config(file_path: str) -> str: + """Reads a device configuration file and loads its contents into memory. + + Args: + file_path (str): The path to the configuration file. + + Returns: + str: The configuration file contents as a string. + + """ + return Path(file_path).read_text(encoding="utf-8") + + +def load_hier_config_tags(tags_file: str) -> tuple[TagRule, ...]: + """Loads and validates Hier Config tags from a YAML file. + + Args: + tags_file (str): Path to the YAML file containing the tags. + + Returns: + Tuple[TagRule, ...]: A tuple of validated TagRule objects. + + """ + tags_data = yaml.safe_load(Path(tags_file).read_text(encoding="utf-8")) + return TypeAdapter(tuple[TagRule, ...]).validate_python(tags_data) diff --git a/mkdocs.yml b/mkdocs.yml index c829397..fc8a703 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: Hierarchical Configuration -site_url: https://hier_config.readthedocs.io/ +site_url: https://hier-config.readthedocs.io/ repo_url: https://github.com/netdevops/hier_config theme: name: readthedocs @@ -8,5 +8,10 @@ nav: - Home: index.md - Install: install.md - Getting Started: getting-started.md -- Advanced Topics: advanced-topics.md -- Experimental Features: experimental-features.md +- Config View: config-view.md +- Drivers: drivers.md +- Custom Workflows: custom-workflows.md +- Future Config: future-config.md +- JunOS Style Syntax Remediation: junos-style-syntax-remediation.md +- Unified Diff: unified-diff.md +- Working with Tags: tags.md diff --git a/tests/fixtures/generated_config_acl.conf b/tests/fixtures/generated_config_acl.conf new file mode 100644 index 0000000..c5d441c --- /dev/null +++ b/tests/fixtures/generated_config_acl.conf @@ -0,0 +1,35 @@ +hostname aggr-example.rtr +! +ip access-list extended TEST + 10 permit ip 10.0.1.0 0.0.0.255 any + 20 permit ip 10.0.0.0 0.0.0.7 any +! +vlan 2 + name switch_mgmt_10.0.2.0/24 +! +vlan 3 + name switch_mgmt_10.0.3.0/24 +! +vlan 4 + name switch_mgmt_10.0.4.0/24 +! +interface Vlan2 + mtu 9000 + descripton switch_10.0.2.0/24 + ip address 10.0.2.1 255.255.255.0 + ip access-group TEST in + no shutdown +! +interface Vlan3 + mtu 9000 + description switch_mgmt_10.0.3.0/24 + ip address 10.0.3.1 255.255.0.0 + ip access-group TEST in + no shutdown +! +interface Vlan4 + mtu 9000 + description switch_mgmt_10.0.4.0/24 + ip address 10.0.4.1 255.255.0.0 + ip access-group TEST in + no shutdown diff --git a/tests/fixtures/running_config_acl.conf b/tests/fixtures/running_config_acl.conf new file mode 100644 index 0000000..c4fe45b --- /dev/null +++ b/tests/fixtures/running_config_acl.conf @@ -0,0 +1,22 @@ +hostname aggr-example.rtr +! +ip access-list extended TEST + 12 permit ip 10.0.0.0 0.0.0.7 any +! +vlan 2 + name switch_mgmt_10.0.2.0/24 +! +vlan 3 + name switch_mgmt_10.0.4.0/24 +! +interface Vlan2 + descripton switch_10.0.2.0/24 + ip address 10.0.2.1 255.255.255.0 + shutdown +! +interface Vlan3 + mtu 9000 + description switch_mgmt_10.0.4.0/24 + ip address 10.0.4.1 255.255.0.0 + ip access-group TEST in + no shutdown diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..91682b7 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,83 @@ +from pathlib import Path + +import pytest +import yaml +from pydantic import ValidationError + +from hier_config.models import TagRule +from hier_config.utils import load_device_config, load_hier_config_tags + +TAGS_FILE_PATH = "./tests/fixtures/tag_rules_ios.yml" + + +@pytest.fixture +def temporary_file_fixture(tmp_path: Path) -> tuple[Path, str]: + file_path = tmp_path / "temp_config.conf" + content = "interface GigabitEthernet0/1\n ip address 192.168.1.1 255.255.255.0\n no shutdown" + file_path.write_text(content) + return file_path, content + + +def test_load_device_config_success(temporary_file_fixture: tuple[Path, str]) -> None: + """Test that the function successfully loads a valid configuration file.""" + # pylint: disable=redefined-outer-name + file_path, expected_content = temporary_file_fixture + result = load_device_config(str(file_path)) + assert result == expected_content, "File content should match expected content." + + +def test_load_device_config_file_not_found() -> None: + """Test that the function raises FileNotFoundError when the file does not exist.""" + with pytest.raises(FileNotFoundError): + load_device_config("non_existent_file.conf") + + +def test_load_device_config_empty_file(tmp_path: Path) -> None: + """Test that the function correctly handles an empty configuration file.""" + empty_file = tmp_path / "empty.conf" + empty_file.write_text("") + result = load_device_config(str(empty_file)) + assert not result, "Empty file should return an empty string." + + +def test_load_hier_config_tags_success() -> None: + """Test that valid tags from the tag_rules_ios.yml file load and validate successfully.""" + result = load_hier_config_tags(TAGS_FILE_PATH) + + assert isinstance(result, tuple), "Result should be a tuple of TagRule objects." + assert len(result) == 4, "There should be four TagRule objects." + assert isinstance(result[0], TagRule), "Each element should be a TagRule object." + assert result[0].apply_tags == { + "safe" + }, "First tag should have 'safe' as an applied tag." + assert result[3].apply_tags == { + "manual" + }, "Last tag should have 'manual' as an applied tag." + + +def test_load_hier_config_tags_file_not_found() -> None: + """Test that the function raises FileNotFoundError for a missing file.""" + with pytest.raises(FileNotFoundError): + load_hier_config_tags("non_existent_file.yml") + + +def test_load_hier_config_tags_invalid_yaml(tmp_path: Path) -> None: + """Test that the function raises yaml.YAMLError for invalid YAML syntax.""" + invalid_file = tmp_path / "invalid_tags.yml" + invalid_file.write_text(""" + - match_rules: + - equals: no ip http server + apply_tags [safe] # Missing colon causes a syntax error + """) + + with pytest.raises(yaml.YAMLError): + load_hier_config_tags(str(invalid_file)) + + +def test_load_hier_config_tags_empty_file(tmp_path: Path) -> None: + """Test that the function raises ValidationError for an empty YAML file.""" + empty_file = tmp_path / "empty.yml" + empty_file.write_text("") + + with pytest.raises(ValidationError): + load_hier_config_tags(str(empty_file))