diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2a4d3e0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,47 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +# Python files +[*.py] +indent_style = space +indent_size = 4 +max_line_length = 100 + +# YAML files +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +# TOML files +[*.toml] +indent_style = space +indent_size = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false +max_line_length = 120 + +# Shell scripts +[*.sh] +indent_style = space +indent_size = 2 + +# JSON files +[*.json] +indent_style = space +indent_size = 2 + +# Configuration files +[*.conf] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index e2e9292..971ce41 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,20 +4,36 @@ on: release: types: [created] +permissions: + contents: read + jobs: deploy: runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/hier-config-cli/ + permissions: + id-token: write # Required for trusted publishing + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' + - name: Install poetry uses: snok/install-poetry@v1 with: - version: 1.5.1 - - name: Build and publish to PyPI + version: 1.8.2 + + - name: Build package + run: | + poetry build + + - name: Publish to PyPI env: TWINE_API_KEY: ${{ secrets.TWINE_API_KEY }} run: | diff --git a/.github/workflows/test-app.yaml b/.github/workflows/test-app.yaml index 940eb92..83bbc58 100644 --- a/.github/workflows/test-app.yaml +++ b/.github/workflows/test-app.yaml @@ -7,30 +7,70 @@ on: branches: [main] jobs: - build: + test: runs-on: ubuntu-latest strategy: matrix: python-version: - - "3.9" - "3.10" - "3.11" - "3.12" + - "3.13" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install poetry uses: snok/install-poetry@v1 with: - version: 1.5.1 - - name: Set PYTHONPATH - run: echo "PYTHONPATH=src" >> $GITHUB_ENV - - name: Run tests + version: 1.8.2 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pypoetry + key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry- + + - name: Install dependencies + run: | + poetry install --no-interaction + + - name: Run black formatter check + run: | + poetry run black --check src/ tests/ + + - name: Run ruff linter run: | - poetry install --no-interaction --no-root - poetry run black --check . - poetry run pytest tests/ \ No newline at end of file + poetry run ruff check src/ tests/ + + - name: Run mypy type checker + run: | + poetry run mypy src/ + + - name: Run tests with coverage + run: | + poetry run pytest --cov=hier_config_cli --cov-report=xml --cov-report=term-missing + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + if: matrix.python-version == '3.12' + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + if: matrix.python-version == '3.12' + with: + name: coverage-report + path: htmlcov/ + retention-days: 7 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..64cf748 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,80 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.2.0] - 2026-01-25 + +### Added +- Added support for Fortinet FortiOS platform +- Added `--format` option to output results in JSON, YAML, or text formats +- Added `--output` / `-o` flag to save output to file +- Added `list-platforms` command to show all available platforms +- Added `version` command to display tool version +- Added comprehensive error handling for file operations and platform validation +- Added verbose logging support with `-v` (INFO) and `-vv` (DEBUG) flags +- Added complete type hints throughout codebase +- Added comprehensive test suite with 20+ test cases including: + - Error handling tests + - All platform tests + - Output format tests (text, JSON, YAML) + - File output tests + - Verbose logging tests +- Added comprehensive documentation: + - Detailed README with installation instructions and examples + - CONTRIBUTING.md with development guidelines + - SECURITY.md with security policy + - Examples directory with sample configurations + - Integration examples for Nornir, Ansible, and CI/CD +- Added development tools configuration: + - Black formatter configuration + - Ruff linter configuration + - Mypy type checker configuration + - Pytest with coverage reporting +- Added `py.typed` marker file for PEP 561 compliance + +### Changed +- **BREAKING**: Converted to proper Python package structure with `__init__.py` +- **BREAKING**: Entry point now uses `hier_config_cli:cli` instead of module path +- Refactored duplicate code in commands into shared `process_configs()` function +- Fixed platform-specific output formatting (no longer hardcodes `cisco_style_text()` for all platforms) +- Improved command help text with detailed descriptions and examples +- Enhanced pyproject.toml with comprehensive metadata and classifiers +- Updated Python version support to 3.9-3.13 +- Improved error messages to be more descriptive and actionable + +### Fixed +- Fixed incorrect output format for Juniper JunOS configurations +- Fixed missing platform support for Fortinet FortiOS +- Fixed lack of error handling for missing or unreadable configuration files +- Fixed unused PyYAML dependency (now actively used for YAML output) + +### Removed +- Removed code duplication across remediation, rollback, and future commands + +## [0.1.0] - 2024-12-XX + +### Added +- Initial release +- Basic remediation, rollback, and future commands +- Support for major network platforms: + - Cisco IOS + - Cisco NX-OS + - Cisco IOS XR + - Arista EOS + - Juniper JunOS + - VyOS + - HP Comware5 + - HP ProCurve + - Generic platform +- GitHub Actions CI/CD pipeline +- Basic test coverage +- Apache 2.0 license + +[Unreleased]: https://github.com/netdevops/hier-config-cli/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/netdevops/hier-config-cli/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/netdevops/hier-config-cli/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2542646 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,327 @@ +# Contributing to Hier Config CLI + +Thank you for your interest in contributing to Hier Config CLI! This document provides guidelines and instructions for contributing to the project. + +## Code of Conduct + +By participating in this project, you agree to maintain a respectful and inclusive environment for all contributors. + +## How to Contribute + +### Reporting Bugs + +Before creating bug reports, please check existing issues to avoid duplicates. When creating a bug report, include: + +- **Clear title and description** +- **Steps to reproduce** the issue +- **Expected behavior** vs actual behavior +- **Environment details** (OS, Python version, tool version) +- **Sample configurations** if applicable (sanitized of sensitive data) +- **Error messages** or stack traces + +Example: +```markdown +**Bug**: Remediation fails on Juniper configs with curly braces + +**To Reproduce**: +1. Run: `hier-config-cli remediation --platform junos --running-config r.conf --generated-config g.conf` +2. See error: ... + +**Expected**: Should generate remediation commands +**Actual**: Raises ValueError + +**Environment**: Python 3.11, hier-config-cli 0.2.0, Ubuntu 22.04 +``` + +### Suggesting Enhancements + +Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion: + +- **Use a clear and descriptive title** +- **Provide detailed description** of the proposed functionality +- **Explain why this enhancement would be useful** +- **Provide examples** of how it would be used +- **List alternatives** you've considered + +### Pull Requests + +1. **Fork the repository** and create your branch from `main` +2. **Follow the development workflow** (see below) +3. **Write tests** for new functionality +4. **Ensure all tests pass** and code quality checks succeed +5. **Update documentation** as needed +6. **Submit the pull request** with a clear description + +#### Pull Request Process + +1. Update the README.md or relevant documentation with details of changes +2. Add entries to CHANGELOG.md under "Unreleased" section +3. Ensure the test suite passes (`pytest`) +4. Ensure code quality checks pass (`black`, `ruff`, `mypy`) +5. Update type hints for any new functions or modified signatures +6. Request review from maintainers + +## Development Workflow + +### Setting Up Development Environment + +```bash +# Clone your fork +git clone https://github.com/YOUR-USERNAME/hier-config-cli.git +cd hier-config-cli + +# Add upstream remote +git remote add upstream https://github.com/netdevops/hier-config-cli.git + +# Install dependencies +poetry install + +# Activate virtual environment +poetry shell +``` + +### Creating a Branch + +```bash +# Update your fork +git fetch upstream +git checkout main +git merge upstream/main + +# Create feature branch +git checkout -b feature/your-feature-name +# or +git checkout -b fix/bug-description +``` + +### Making Changes + +1. **Write code** following the project style guidelines +2. **Add tests** for new functionality +3. **Run tests** to ensure everything works +4. **Check code quality** with linters and type checker + +```bash +# Run tests +pytest + +# Check test coverage +pytest --cov=hier_config_cli --cov-report=html + +# Format code with black +black src/ tests/ + +# Lint with ruff +ruff check src/ tests/ + +# Type check with mypy +mypy src/ + +# Run all checks at once +black src/ tests/ && ruff check src/ tests/ && mypy src/ && pytest +``` + +### Committing Changes + +Write clear, concise commit messages following conventional commits: + +```bash +# Feature +git commit -m "feat: add support for FortiOS platform" + +# Bug fix +git commit -m "fix: correct Junos output formatting" + +# Documentation +git commit -m "docs: update installation instructions" + +# Tests +git commit -m "test: add error handling tests" + +# Refactor +git commit -m "refactor: consolidate duplicate command code" +``` + +### Submitting Pull Request + +```bash +# Push to your fork +git push origin feature/your-feature-name + +# Create pull request on GitHub +# Include: +# - Clear description of changes +# - Reference to related issues +# - Screenshots if applicable +# - Checklist of completed items +``` + +## Coding Standards + +### Python Style Guide + +- Follow [PEP 8](https://peps.python.org/pep-0008/) style guide +- Use [Black](https://github.com/psf/black) for code formatting (line length: 100) +- Use [Ruff](https://github.com/astral-sh/ruff) for linting +- Use [mypy](https://mypy.readthedocs.io/) for type checking + +### Code Organization + +- Keep functions focused and single-purpose +- Use descriptive variable and function names +- Add docstrings to all public functions and classes +- Include type hints for all function signatures +- Avoid code duplication (DRY principle) + +### Documentation Standards + +#### Docstrings + +Use Google-style docstrings: + +```python +def process_configs( + platform_str: str, + running_config_path: str, + generated_config_path: str, + operation: str, +) -> tuple[HConfig, Platform]: + """Process configuration files and return the result. + + Args: + platform_str: Platform name string + running_config_path: Path to running configuration + generated_config_path: Path to generated configuration + operation: Operation type (remediation, rollback, future) + + Returns: + Tuple of (result HConfig, Platform enum) + + Raises: + click.ClickException: If processing fails + """ +``` + +#### Type Hints + +Always include type hints: + +```python +from typing import Optional +from pathlib import Path + +def save_output(content: str, filepath: Optional[Path] = None) -> None: + """Save output to file.""" + if filepath: + filepath.write_text(content) +``` + +### Testing Standards + +- Write tests for all new functionality +- Maintain or improve code coverage +- Test both happy paths and error cases +- Use descriptive test names +- Include docstrings in test functions + +```python +def test_remediation_with_invalid_platform( + mock_running_config: str, + mock_generated_config: str +) -> None: + """Test that remediation fails gracefully with invalid platform.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", "invalid_platform", + "--running-config", mock_running_config, + "--generated-config", mock_generated_config, + ], + ) + assert result.exit_code != 0 + assert "Unknown platform" in result.output +``` + +## Project Structure + +``` +hier-config-cli/ +├── src/ +│ └── hier_config_cli/ +│ ├── __main__.py # Main CLI code +│ └── py.typed # Type hints marker +├── tests/ +│ └── test_cli.py # Test suite +├── examples/ # Example configurations +│ ├── cisco_ios_running.conf +│ ├── cisco_ios_intended.conf +│ └── README.md +├── .github/ +│ └── workflows/ # CI/CD workflows +├── pyproject.toml # Project configuration +├── README.md # Main documentation +├── CONTRIBUTING.md # This file +├── CHANGELOG.md # Version history +├── SECURITY.md # Security policy +└── LICENSE # License file +``` + +## Adding New Platforms + +To add support for a new network platform: + +1. Add the platform to `PLATFORM_MAP` in `src/hier_config_cli/__main__.py` +2. Ensure proper output formatting in `get_output_text()` function +3. Add test cases for the new platform +4. Update documentation (README.md, examples) +5. Add example configuration files if available + +Example: +```python +PLATFORM_MAP = { + # ... existing platforms ... + "new_platform": Platform.NEW_PLATFORM, +} +``` + +## Adding New Features + +When adding new features: + +1. **Discuss first** by opening an issue to get feedback +2. **Design carefully** considering backwards compatibility +3. **Write tests** covering new functionality +4. **Document thoroughly** in code and README +5. **Update CHANGELOG** with your changes + +## Release Process + +Maintainers handle releases. The process: + +1. Update version in `pyproject.toml` and `src/hier_config_cli/__main__.py` +2. Update CHANGELOG.md with release notes +3. Create git tag for release +4. GitHub Actions automatically publishes to PyPI + +## Getting Help + +- **Questions**: Open a [GitHub Discussion](https://github.com/netdevops/hier-config-cli/discussions) +- **Bugs**: Open a [GitHub Issue](https://github.com/netdevops/hier-config-cli/issues) +- **Chat**: Join our community channels (if available) + +## Recognition + +Contributors will be recognized in: +- GitHub contributors list +- CHANGELOG.md for significant contributions +- Project README (for major features) + +## License + +By contributing, you agree that your contributions will be licensed under the Apache License 2.0. + +--- + +Thank you for contributing to Hier Config CLI! Your efforts help make network automation better for everyone. diff --git a/README.md b/README.md index 2778de8..417bc0b 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,399 @@ -# hier-config-cli +# Hier Config CLI -hier-config-cli is a command-line interface tool built on top of the [Hier Config](https://github.com/netdevops/hier_config) library. It enables network engineers to analyze, remediate, and predict configurations for network devices across multiple platforms. +[![PyPI version](https://badge.fury.io/py/hier-config-cli.svg)](https://badge.fury.io/py/hier-config-cli) +[![Python Versions](https://img.shields.io/pypi/pyversions/hier-config-cli.svg)](https://pypi.org/project/hier-config-cli/) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Tests](https://github.com/netdevops/hier-config-cli/workflows/test-app/badge.svg)](https://github.com/netdevops/hier-config-cli/actions) -## Usage +A powerful command-line interface tool for network configuration analysis, remediation, and rollback built on top of the [Hier Config](https://github.com/netdevops/hier_config) library. + +## Overview + +**hier-config-cli** helps network engineers analyze configuration differences, generate remediation commands, prepare rollback procedures, and predict configuration states across multiple network platforms. It's an essential tool for network automation workflows, change management, and configuration compliance. + +### Key Features + +- 🔄 **Remediation Generation**: Automatically generate commands to transform running config into intended config +- ⏮️ **Rollback Planning**: Create rollback procedures before making changes +- 🔮 **Future State Prediction**: Preview the complete configuration after applying changes +- 🌐 **Multi-Platform Support**: Works with Cisco, Juniper, Arista, HP, Fortinet, VyOS, and more +- 📊 **Multiple Output Formats**: Export as text, JSON, or YAML +- 🎯 **Type-Safe**: Fully typed Python code with mypy support +- ✅ **Well-Tested**: Comprehensive test suite with high code coverage +- 📝 **Detailed Logging**: Verbose and debug modes for troubleshooting + +## Installation + +### Via pip (recommended) + +```bash +pip install hier-config-cli +``` + +### Via Poetry + +```bash +poetry add hier-config-cli +``` + +### From Source + +```bash +git clone https://github.com/netdevops/hier-config-cli.git +cd hier-config-cli +poetry install +``` + +## Quick Start + +### 1. List Available Platforms + +```bash +hier-config-cli list-platforms +``` + +Output: +``` +=== Available Platforms === + eos + fortios + generic + hp_comware5 + hp_procurve + ios + iosxr + junos + nxos + vyos +``` + +### 2. Generate Remediation Configuration + +Compare your running configuration with the intended configuration and generate the commands needed to remediate: + +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf +``` + +### 3. Generate Rollback Configuration + +Prepare rollback commands before making changes: + +```bash +hier-config-cli rollback \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf +``` + +### 4. Preview Future Configuration + +See what the complete configuration will look like after applying changes: + +```bash +hier-config-cli future \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf +``` + +## Usage Examples + +### Basic Remediation + +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config examples/cisco_ios_running.conf \ + --generated-config examples/cisco_ios_intended.conf +``` + +Output: +``` +=== Remediation Configuration === +no hostname router-01 +hostname router-01-updated +interface GigabitEthernet0/0 + no description WAN Interface + description WAN Interface - Updated +interface Vlan20 + description Guest VLAN + ip address 10.0.20.1 255.255.255.0 +router ospf 1 + network 10.0.20.0 0.0.0.255 area 0 +ntp server 192.0.2.1 +``` + +### Export to JSON + +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format json +``` + +### Export to YAML + +```bash +hier-config-cli remediation \ + --platform nxos \ + --running-config running.conf \ + --generated-config intended.conf \ + --format yaml +``` + +### Save Output to File + +```bash +hier-config-cli remediation \ + --platform iosxr \ + --running-config running.conf \ + --generated-config intended.conf \ + --output remediation_commands.txt +``` + +### Enable Verbose Logging + +```bash +# INFO level logging +hier-config-cli -v remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf + +# DEBUG level logging +hier-config-cli -vv remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf +``` + +## Commands + +### `remediation` + +Generates the commands needed to transform the running configuration into the intended configuration. + +**Options:** +- `--platform`: Target platform (required) - see `list-platforms` for options +- `--running-config`: Path to running configuration file (required) +- `--generated-config`: Path to intended/generated configuration file (required) +- `--format`: Output format - `text`, `json`, or `yaml` (default: text) +- `--output, -o`: Write output to file instead of stdout + +### `rollback` + +Generates the commands needed to revert from the intended configuration back to the running configuration. Useful for preparing rollback procedures before making changes. + +**Options:** Same as `remediation` + +### `future` + +Predicts what the complete configuration will look like after applying the intended configuration to the running configuration. + +**Options:** Same as `remediation` + +### `list-platforms` + +Lists all supported network platforms. + +### `version` + +Shows the installed version of hier-config-cli. + +## Supported Platforms + +| Platform | Code | Description | +|----------|------|-------------| +| Cisco IOS | `ios` | Cisco IOS routers and switches | +| Cisco NX-OS | `nxos` | Cisco Nexus switches | +| Cisco IOS XR | `iosxr` | Cisco IOS XR routers | +| Arista EOS | `eos` | Arista switches | +| Juniper JunOS | `junos` | Juniper routers and switches | +| VyOS | `vyos` | VyOS routers | +| Fortinet FortiOS | `fortios` | Fortinet firewalls | +| HP Comware5 | `hp_comware5` | HP Comware5 switches | +| HP ProCurve | `hp_procurve` | HP ProCurve switches | +| Generic | `generic` | Generic/unknown platform | + +## Integration Examples + +### With Nornir ```python -hier-config-cli [COMMAND] --platform PLATFORM --running-config PATH --generated-config PATH +from nornir import InitNornir +from nornir.core.task import Task +import subprocess + +def generate_remediation(task: Task) -> None: + """Generate remediation for a device.""" + result = subprocess.run( + [ + "hier-config-cli", + "remediation", + "--platform", task.host.platform, + "--running-config", f"configs/{task.host.name}_running.conf", + "--generated-config", f"configs/{task.host.name}_intended.conf", + "--output", f"remediation/{task.host.name}_remediation.txt", + ], + capture_output=True, + text=True, + ) + return result.stdout + +nr = InitNornir(config_file="config.yaml") +results = nr.run(task=generate_remediation) +``` + +### With Ansible + +```yaml +- name: Generate configuration remediation + hosts: network_devices + tasks: + - name: Run hier-config-cli remediation + command: > + hier-config-cli remediation + --platform {{ platform }} + --running-config /tmp/{{ inventory_hostname }}_running.conf + --generated-config /tmp/{{ inventory_hostname }}_intended.conf + --output /tmp/{{ inventory_hostname }}_remediation.txt + register: remediation_result + + - name: Display remediation + debug: + msg: "{{ remediation_result.stdout }}" +``` + +### In CI/CD Pipeline + +```yaml +# .github/workflows/config-validation.yml +name: Validate Network Configs + +on: [push, pull_request] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install hier-config-cli + run: pip install hier-config-cli + + - name: Generate remediation + run: | + hier-config-cli remediation \ + --platform ios \ + --running-config prod/running.conf \ + --generated-config staging/intended.conf \ + --output remediation.txt + + - name: Upload remediation + uses: actions/upload-artifact@v4 + with: + name: remediation-config + path: remediation.txt +``` + +## Development + +### Prerequisites + +- Python 3.9 or higher +- Poetry (for dependency management) + +### Setup Development Environment + +```bash +# Clone the repository +git clone https://github.com/netdevops/hier-config-cli.git +cd hier-config-cli + +# Install dependencies +poetry install + +# Activate virtual environment +poetry shell ``` -### Available Commands +### Running Tests + +```bash +# Run all tests +pytest + +# Run with coverage +pytest --cov=hier_config_cli --cov-report=html -* remediation -* rollback -* future +# Run specific test file +pytest tests/test_cli.py + +# Run with verbose output +pytest -v +``` + +### Code Quality + +```bash +# Format code with black +black src/ tests/ + +# Lint with ruff +ruff check src/ tests/ + +# Type check with mypy +mypy src/ + +# Run all quality checks +black src/ tests/ && ruff check src/ tests/ && mypy src/ && pytest +``` ## Contributing -Contributions are welcome! Please open an issue or submit a pull request for features, bug fixes, or documentation improvements. +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on: + +- Code of conduct +- Development workflow +- Pull request process +- Coding standards + +## Security + +For security issues, please see [SECURITY.md](SECURITY.md) for our security policy and how to report vulnerabilities. + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes between versions. + +## License + +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. + +## Links + +- **Documentation**: [https://github.com/netdevops/hier-config-cli](https://github.com/netdevops/hier-config-cli) +- **PyPI**: [https://pypi.org/project/hier-config-cli/](https://pypi.org/project/hier-config-cli/) +- **Issues**: [https://github.com/netdevops/hier-config-cli/issues](https://github.com/netdevops/hier-config-cli/issues) +- **Hier Config Library**: [https://github.com/netdevops/hier_config](https://github.com/netdevops/hier_config) + +## Acknowledgments + +Built on top of the excellent [Hier Config](https://github.com/netdevops/hier_config) library by James Williams and contributors. + +## Support + +- 📫 Open an issue: [GitHub Issues](https://github.com/netdevops/hier-config-cli/issues) +- 💬 Discussions: [GitHub Discussions](https://github.com/netdevops/hier-config-cli/discussions) +- 📧 Email: james.williams@packetgeek.net diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e1d9bdd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,240 @@ +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities in the following versions: + +| Version | Supported | +| ------- | ------------------ | +| 0.2.x | :white_check_mark: | +| 0.1.x | :x: | + +## Reporting a Vulnerability + +We take the security of hier-config-cli seriously. If you believe you have found a security vulnerability, please report it to us as described below. + +### Please DO NOT: + +- Open a public GitHub issue for security vulnerabilities +- Disclose the vulnerability publicly before it has been addressed + +### Please DO: + +**Report security vulnerabilities by emailing: james.williams@packetgeek.net** + +Include the following information in your report: + +1. **Description** of the vulnerability +2. **Steps to reproduce** the issue +3. **Potential impact** of the vulnerability +4. **Affected versions** (if known) +5. **Suggested fix** (if you have one) +6. **Your contact information** for follow-up questions + +### What to Expect + +- **Acknowledgment**: We will acknowledge receipt of your vulnerability report within 48 hours +- **Updates**: We will send you regular updates about our progress (at least every 5 business days) +- **Disclosure timeline**: We aim to address critical vulnerabilities within 30 days +- **Credit**: If you wish, we will credit you in the security advisory when the vulnerability is disclosed + +## Security Best Practices + +When using hier-config-cli, follow these security best practices: + +### 1. Protect Configuration Files + +Network device configurations often contain sensitive information: + +- **Passwords and secrets**: Sanitize configs before sharing or storing in version control +- **IP addresses**: Be cautious about exposing internal network topology +- **Community strings**: Remove SNMP community strings from configs +- **Authentication keys**: Strip out authentication keys and certificates + +Example of sanitizing a config: +```bash +# Before sharing, replace sensitive data +sed -i 's/enable secret .*/enable secret /' config.txt +sed -i 's/username .* password .*/username admin password /' config.txt +``` + +### 2. File Permissions + +Ensure configuration files have appropriate permissions: + +```bash +# Set restrictive permissions on config files +chmod 600 configs/*.conf + +# Ensure output directory is protected +chmod 700 output/ +``` + +### 3. Use Environment Variables + +For automation workflows, avoid hardcoding paths or credentials: + +```bash +# Good - use environment variables +export RUNNING_CONFIG_PATH=/secure/path/running.conf +hier-config-cli remediation --platform ios \ + --running-config "$RUNNING_CONFIG_PATH" \ + --generated-config "$GENERATED_CONFIG_PATH" + +# Bad - hardcoded paths in scripts +hier-config-cli remediation --platform ios \ + --running-config /tmp/production-router.conf \ + --generated-config /tmp/new-config.conf +``` + +### 4. Validate Input Files + +Always validate configuration files come from trusted sources: + +```python +import hashlib + +def verify_config_hash(filepath, expected_hash): + """Verify configuration file integrity.""" + sha256_hash = hashlib.sha256() + with open(filepath, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() == expected_hash +``` + +### 5. Secure CI/CD Pipelines + +When using hier-config-cli in CI/CD: + +- Store configs in encrypted CI/CD secrets +- Use temporary files that are cleaned up after use +- Limit permissions of CI/CD service accounts +- Audit who has access to configuration files +- Enable branch protection rules + +Example GitHub Actions security: +```yaml +jobs: + config-check: + runs-on: ubuntu-latest + permissions: + contents: read # Minimal permissions + steps: + - uses: actions/checkout@v4 + + - name: Process configs + env: + RUNNING_CONFIG: ${{ secrets.RUNNING_CONFIG }} + run: | + echo "$RUNNING_CONFIG" > /tmp/running.conf + chmod 600 /tmp/running.conf + + hier-config-cli remediation \ + --platform ios \ + --running-config /tmp/running.conf \ + --generated-config configs/intended.conf + + # Clean up + shred -vfz /tmp/running.conf +``` + +### 6. Output Handling + +Be cautious with command output: + +```bash +# Don't accidentally log sensitive configs +hier-config-cli remediation ... > remediation.txt +chmod 600 remediation.txt + +# In production, consider encrypting output +hier-config-cli remediation ... | gpg -e -r admin@example.com > remediation.txt.gpg +``` + +## Known Security Considerations + +### 1. Configuration Data Sensitivity + +This tool processes network device configurations that may contain: +- Passwords and authentication credentials +- SNMP community strings +- Encryption keys +- Internal network topology +- Security policies + +**Mitigation**: Always sanitize configurations before sharing or storing in version control. + +### 2. File System Access + +The tool requires read access to configuration files and write access for output files. + +**Mitigation**: Run with least privilege necessary and use restrictive file permissions. + +### 3. Dependency Security + +We depend on third-party libraries (click, hier-config, pyyaml). + +**Mitigation**: +- We regularly update dependencies +- Use `poetry` for dependency management with lock files +- Monitor for security advisories via GitHub Dependabot + +### 4. Command Injection + +The tool does not execute shell commands based on configuration content. + +**Note**: When integrating with other tools, ensure proper input validation. + +## Security Update Process + +When a security vulnerability is confirmed: + +1. We develop and test a fix +2. We prepare a security advisory +3. We release a patched version +4. We publish the security advisory +5. We notify users via GitHub security advisories + +## Vulnerability Disclosure Policy + +We follow coordinated vulnerability disclosure: + +- **Private reporting period**: 90 days from initial report +- **Public disclosure**: After patch is released or 90 days, whichever comes first +- **Early disclosure**: May occur if vulnerability is being actively exploited + +## Security Tools + +We use the following tools to maintain security: + +- **Dependabot**: Automated dependency updates +- **Safety**: Python dependency vulnerability scanning (can be added) +- **Bandit**: Python security linter (can be added) +- **CodeQL**: Static analysis via GitHub (can be enabled) + +To run security checks locally: + +```bash +# Install security tools +pip install safety bandit + +# Check for known vulnerabilities in dependencies +safety check + +# Run security linter +bandit -r src/ + +# Check for outdated dependencies +poetry show --outdated +``` + +## Contact + +For security issues: james.williams@packetgeek.net + +For general questions: [GitHub Issues](https://github.com/netdevops/hier-config-cli/issues) + +--- + +Thank you for helping keep hier-config-cli and its users safe! diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..ecfcfa6 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,104 @@ +# Examples + +This directory contains example configuration files for various network platforms to demonstrate the capabilities of hier-config-cli. + +## Cisco IOS Example + +The `cisco_ios_running.conf` and `cisco_ios_intended.conf` files demonstrate a typical use case: + +- **Running config**: Current device configuration +- **Intended config**: Desired device configuration with updates + +### Generate Remediation Configuration + +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config examples/cisco_ios_running.conf \ + --generated-config examples/cisco_ios_intended.conf +``` + +This will output the commands needed to transform the running configuration into the intended configuration: + +``` +=== Remediation Configuration === +no hostname router-01 +hostname router-01-updated +interface GigabitEthernet0/0 + no description WAN Interface + description WAN Interface - Updated +interface Vlan20 + description Guest VLAN + ip address 10.0.20.1 255.255.255.0 +router ospf 1 + network 10.0.20.0 0.0.0.255 area 0 +ntp server 192.0.2.1 +``` + +### Generate Rollback Configuration + +```bash +hier-config-cli rollback \ + --platform ios \ + --running-config examples/cisco_ios_running.conf \ + --generated-config examples/cisco_ios_intended.conf +``` + +This will output the commands needed to rollback from the intended configuration to the running configuration. + +### Generate Future Configuration + +```bash +hier-config-cli future \ + --platform ios \ + --running-config examples/cisco_ios_running.conf \ + --generated-config examples/cisco_ios_intended.conf +``` + +This will show what the complete configuration will look like after applying the remediation. + +### Output Formats + +You can export configurations in different formats: + +**JSON:** +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config examples/cisco_ios_running.conf \ + --generated-config examples/cisco_ios_intended.conf \ + --format json +``` + +**YAML:** +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config examples/cisco_ios_running.conf \ + --generated-config examples/cisco_ios_intended.conf \ + --format yaml +``` + +**Save to File:** +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config examples/cisco_ios_running.conf \ + --generated-config examples/cisco_ios_intended.conf \ + --output remediation.txt +``` + +## Other Platforms + +The tool supports multiple platforms. See `hier-config-cli list-platforms` for all available options: + +- ios (Cisco IOS) +- nxos (Cisco NX-OS) +- iosxr (Cisco IOS XR) +- eos (Arista EOS) +- junos (Juniper JunOS) +- vyos (VyOS) +- fortios (Fortinet FortiOS) +- hp_comware5 (HP Comware5) +- hp_procurve (HP ProCurve) +- generic (Generic platform) diff --git a/examples/cisco_ios_intended.conf b/examples/cisco_ios_intended.conf new file mode 100644 index 0000000..a41ea7c --- /dev/null +++ b/examples/cisco_ios_intended.conf @@ -0,0 +1,39 @@ +! +hostname router-01-updated +! +interface GigabitEthernet0/0 + description WAN Interface - Updated + ip address 203.0.113.1 255.255.255.252 + duplex auto + speed auto + no shutdown +! +interface GigabitEthernet0/1 + description LAN Interface + ip address 192.168.1.1 255.255.255.0 + duplex auto + speed auto + no shutdown +! +interface Vlan10 + description Management VLAN + ip address 10.0.10.1 255.255.255.0 +! +interface Vlan20 + description Guest VLAN + ip address 10.0.20.1 255.255.255.0 +! +router ospf 1 + network 192.168.1.0 0.0.0.255 area 0 + network 10.0.10.0 0.0.0.255 area 0 + network 10.0.20.0 0.0.0.255 area 0 +! +ip route 0.0.0.0 0.0.0.0 203.0.113.2 +! +line vty 0 4 + login local + transport input ssh +! +ntp server 192.0.2.1 +! +end diff --git a/examples/cisco_ios_running.conf b/examples/cisco_ios_running.conf new file mode 100644 index 0000000..bde30ef --- /dev/null +++ b/examples/cisco_ios_running.conf @@ -0,0 +1,32 @@ +! +hostname router-01 +! +interface GigabitEthernet0/0 + description WAN Interface + ip address 203.0.113.1 255.255.255.252 + duplex auto + speed auto + no shutdown +! +interface GigabitEthernet0/1 + description LAN Interface + ip address 192.168.1.1 255.255.255.0 + duplex auto + speed auto + no shutdown +! +interface Vlan10 + description Management VLAN + ip address 10.0.10.1 255.255.255.0 +! +router ospf 1 + network 192.168.1.0 0.0.0.255 area 0 + network 10.0.10.0 0.0.0.255 area 0 +! +ip route 0.0.0.0 0.0.0.0 203.0.113.2 +! +line vty 0 4 + login local + transport input ssh +! +end diff --git a/poetry.lock b/poetry.lock index dfc45c1..8eb4010 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,6 +6,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -17,6 +18,7 @@ version = "24.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -63,6 +65,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -77,10 +80,132 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} + +[[package]] +name = "coverage" +version = "7.10.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, + {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, + {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, + {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, + {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, + {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, + {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, + {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, + {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, + {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, + {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, + {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, + {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, + {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, + {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, + {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, + {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, + {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, + {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, + {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, + {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, + {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, + {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, + {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, + {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, + {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, + {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, + {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, + {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, + {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, + {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, + {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, + {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, + {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, + {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, + {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, + {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, + {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, + {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, + {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "exceptiongroup" @@ -88,6 +213,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -98,13 +225,14 @@ test = ["pytest (>=6)"] [[package]] name = "hier-config" -version = "3.1.0" +version = "3.4.0" description = "A network configuration query and comparison library, used to build remediation configurations." optional = false -python-versions = "<4.0,>=3.9.0" +python-versions = "<4.0,>=3.10.0" +groups = ["main"] files = [ - {file = "hier_config-3.1.0-py3-none-any.whl", hash = "sha256:3d547396b53b97b8d160aadf9a550a15663df085fefbb9bac4126fa62d6fbfc8"}, - {file = "hier_config-3.1.0.tar.gz", hash = "sha256:857128118ee619544a3026aba339ebabd8472a6abc4da68e17a8938238b3352f"}, + {file = "hier_config-3.4.0-py3-none-any.whl", hash = "sha256:c26f374fc28983ca665c78039a62fd31b77a4bd906c75b4d5b068a36612aa6b1"}, + {file = "hier_config-3.4.0.tar.gz", hash = "sha256:27f58074dd43141c5ca7c5b8d095c9e54bb18ca63536fcf31a6bc2dcde24ec36"}, ] [package.dependencies] @@ -116,17 +244,168 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "librt" +version = "0.7.8" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d"}, + {file = "librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b"}, + {file = "librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d"}, + {file = "librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d"}, + {file = "librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c"}, + {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c"}, + {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d"}, + {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0"}, + {file = "librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85"}, + {file = "librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c"}, + {file = "librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f"}, + {file = "librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac"}, + {file = "librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c"}, + {file = "librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8"}, + {file = "librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff"}, + {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3"}, + {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75"}, + {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873"}, + {file = "librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7"}, + {file = "librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c"}, + {file = "librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232"}, + {file = "librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63"}, + {file = "librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93"}, + {file = "librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592"}, + {file = "librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850"}, + {file = "librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62"}, + {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b"}, + {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714"}, + {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449"}, + {file = "librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac"}, + {file = "librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708"}, + {file = "librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0"}, + {file = "librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc"}, + {file = "librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2"}, + {file = "librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3"}, + {file = "librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6"}, + {file = "librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d"}, + {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e"}, + {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca"}, + {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93"}, + {file = "librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951"}, + {file = "librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34"}, + {file = "librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09"}, + {file = "librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418"}, + {file = "librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611"}, + {file = "librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758"}, + {file = "librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea"}, + {file = "librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac"}, + {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398"}, + {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81"}, + {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83"}, + {file = "librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d"}, + {file = "librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44"}, + {file = "librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce"}, + {file = "librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f"}, + {file = "librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde"}, + {file = "librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e"}, + {file = "librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b"}, + {file = "librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666"}, + {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581"}, + {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a"}, + {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca"}, + {file = "librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365"}, + {file = "librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32"}, + {file = "librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06"}, + {file = "librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6"}, + {file = "librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b"}, + {file = "librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c"}, + {file = "librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5"}, + {file = "librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71"}, + {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e"}, + {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63"}, + {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94"}, + {file = "librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb"}, + {file = "librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be"}, + {file = "librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862"}, +] + +[[package]] +name = "mypy" +version = "1.19.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, +] + +[package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -138,6 +417,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -149,6 +429,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -160,6 +441,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -176,6 +458,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -191,6 +474,7 @@ version = "2.10.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, @@ -203,7 +487,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -211,6 +495,7 @@ version = "2.27.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, @@ -323,6 +608,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -339,12 +625,33 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "6.3.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749"}, + {file = "pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=6.2.5" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -401,12 +708,42 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "ruff" +version = "0.8.6" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3"}, + {file = "ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1"}, + {file = "ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117"}, + {file = "ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe"}, + {file = "ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d"}, + {file = "ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a"}, + {file = "ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76"}, + {file = "ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764"}, + {file = "ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"}, + {file = "ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162"}, + {file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"}, +] + [[package]] name = "tomli" version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -442,18 +779,31 @@ files = [ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"}, + {file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [metadata] -lock-version = "2.0" -python-versions = "^3.9" -content-hash = "858b59390625c3206d8019b63f4f9aa93c78d372b137c1098a8efb7ea3b45724" +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "75121bdb2b8a06217874d64b039b16d5dde10d61b59b70fa96029a0f38df9051" diff --git a/pyproject.toml b/pyproject.toml index 0c8cf3a..3412ced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,42 @@ [tool.poetry] name = "hier-config-cli" -version = "0.1.0" -description = "" +version = "0.2.0" +description = "A command-line interface tool for network configuration analysis, remediation, and rollback built on top of the Hier Config library" authors = ["James Williams "] -license = "Apache 2.0" +license = "Apache-2.0" readme = "README.md" -packages = [{include = "hier_config_cli.py", from = "src"}] +homepage = "https://github.com/netdevops/hier-config-cli" +repository = "https://github.com/netdevops/hier-config-cli" +keywords = ["network", "automation", "configuration", "cisco", "juniper", "arista"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: System :: Networking", + "Topic :: System :: Systems Administration", +] +packages = [{include = "hier_config_cli", from = "src"}] +include = ["src/hier_config_cli/py.typed"] [tool.poetry.dependencies] -python = "^3.9" -hier-config = "^3.1.0" +python = "^3.10" +hier-config = "^3.3.0" click = "^8.1.7" pyyaml = "^6.0.2" [tool.poetry.group.dev.dependencies] black = "^24.10.0" pytest = "^8.3.4" +pytest-cov = "^6.0.0" +mypy = "^1.13.0" +ruff = "^0.8.4" +types-pyyaml = "^6.0.12" [build-system] requires = ["poetry-core"] @@ -23,3 +44,64 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] hier-config-cli = "hier_config_cli:cli" + +[tool.black] +line-length = 100 +target-version = ["py310", "py311", "py312", "py313"] + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [] + +[tool.mypy] +python_version = "3.10" +warn_return_any = false +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = false +strict_equality = true + +# Allow untyped third-party libraries +[[tool.mypy.overrides]] +module = [ + "hier_config.*", + "click.*", +] +ignore_missing_imports = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "--cov=hier_config_cli --cov-report=term-missing --cov-report=html --cov-report=xml" + +[tool.coverage.run] +source = ["src"] +omit = ["*/tests/*", "*/__pycache__/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/src/hier_config_cli.py b/src/hier_config_cli.py deleted file mode 100644 index adbd531..0000000 --- a/src/hier_config_cli.py +++ /dev/null @@ -1,119 +0,0 @@ -import click -from hier_config import Platform, WorkflowRemediation, get_hconfig -from hier_config.utils import read_text_from_file - -# Mapping for driver platforms -PLATFORM_MAP = { - "ios": Platform.CISCO_IOS, - "nxos": Platform.CISCO_NXOS, - "iosxr": Platform.CISCO_XR, - "eos": Platform.ARISTA_EOS, - "junos": Platform.JUNIPER_JUNOS, - "vyos": Platform.VYOS, - "generic": Platform.GENERIC, - "hp_comware5": Platform.HP_COMWARE5, - "hp_procurve": Platform.HP_PROCURVE, -} - - -@click.group() -def cli(): - """Hier Config CLI Tool""" - pass - - -def common_options(func): - """Reusable options for platform, running config, and generated config.""" - func = click.option( - "--platform", - type=click.Choice(PLATFORM_MAP.keys(), case_sensitive=False), - required=True, - help="Platform driver to use (e.g., ios, nxos, iosxr, eos, junos, vyos, generic).", - )(func) - func = click.option( - "--running-config", - type=click.Path(exists=True, readable=True), - required=True, - help="Path to the running configuration file.", - )(func) - func = click.option( - "--generated-config", - type=click.Path(exists=True, readable=True), - required=True, - help="Path to the generated (intended) configuration file.", - )(func) - return func - - -@cli.command() -@common_options -def remediation(platform, running_config, generated_config): - """ - Generate the remediation configuration. - """ - platform_enum = PLATFORM_MAP[platform.lower()] - running_config_text = read_text_from_file(running_config) - generated_config_text = read_text_from_file(generated_config) - - running_hconfig = get_hconfig(platform_enum, running_config_text) - generated_hconfig = get_hconfig(platform_enum, generated_config_text) - - workflow = WorkflowRemediation(running_hconfig, generated_hconfig) - - click.echo("\n=== Remediation Configuration ===") - for line in workflow.remediation_config.all_children_sorted(): - click.echo(line.cisco_style_text()) - - -@cli.command() -@common_options -def rollback(platform, running_config, generated_config): - """ - Generate the rollback configuration. - """ - platform_enum = PLATFORM_MAP[platform.lower()] - running_config_text = read_text_from_file(running_config) - generated_config_text = read_text_from_file(generated_config) - - running_hconfig = get_hconfig(platform_enum, running_config_text) - generated_hconfig = get_hconfig(platform_enum, generated_config_text) - - workflow = WorkflowRemediation(running_hconfig, generated_hconfig) - - click.echo("\n=== Rollback Configuration ===") - for line in workflow.rollback_config.all_children_sorted(): - click.echo(line.cisco_style_text()) - - -@cli.command() -@common_options -def future(platform, running_config, generated_config): - """ - Generate the future configuration. - """ - platform_enum = PLATFORM_MAP[platform.lower()] - running_config_text = read_text_from_file(running_config) - generated_config_text = read_text_from_file(generated_config) - - running_hconfig = get_hconfig(platform_enum, running_config_text) - generated_hconfig = get_hconfig(platform_enum, generated_config_text) - - future_config = running_hconfig.future(generated_hconfig) - - click.echo("\n=== Future Configuration ===") - for line in future_config.all_children_sorted(): - click.echo(line.cisco_style_text()) - - -@cli.command() -def list_platforms(): - """ - List all available platforms. - """ - click.echo("\n=== Available Platforms ===") - for platform in PLATFORM_MAP.keys(): - click.echo(f"- {platform}") - - -if __name__ == "__main__": - cli() diff --git a/src/hier_config_cli/__init__.py b/src/hier_config_cli/__init__.py new file mode 100644 index 0000000..ff35659 --- /dev/null +++ b/src/hier_config_cli/__init__.py @@ -0,0 +1,9 @@ +"""Hier Config CLI - A command-line interface for network configuration analysis. + +This package provides a CLI tool for analyzing network device configurations, +generating remediation steps, rollback configurations, and predicting future states. +""" + +from hier_config_cli.__main__ import __version__, cli + +__all__ = ["cli", "__version__"] diff --git a/src/hier_config_cli/__main__.py b/src/hier_config_cli/__main__.py new file mode 100644 index 0000000..5d0018d --- /dev/null +++ b/src/hier_config_cli/__main__.py @@ -0,0 +1,374 @@ +"""Hier Config CLI Tool - A command-line interface for network configuration analysis.""" + +import json +import logging +import sys +from collections.abc import Callable +from pathlib import Path +from typing import Any, TypeVar + +import click +import yaml +from hier_config import HConfig, Platform, WorkflowRemediation, get_hconfig +from hier_config.utils import read_text_from_file + +F = TypeVar("F", bound=Callable[..., Any]) + +__version__ = "0.2.0" + +# Mapping for driver platforms - includes all hier-config supported platforms +PLATFORM_MAP = { + "ios": Platform.CISCO_IOS, + "nxos": Platform.CISCO_NXOS, + "iosxr": Platform.CISCO_XR, + "eos": Platform.ARISTA_EOS, + "junos": Platform.JUNIPER_JUNOS, + "vyos": Platform.VYOS, + "fortios": Platform.FORTINET_FORTIOS, + "generic": Platform.GENERIC, + "hp_comware5": Platform.HP_COMWARE5, + "hp_procurve": Platform.HP_PROCURVE, +} + +# Configure logging +logger = logging.getLogger(__name__) + + +def setup_logging(verbose: int) -> None: + """Configure logging based on verbosity level. + + Args: + verbose: Verbosity level (0=WARNING, 1=INFO, 2=DEBUG) + """ + level = logging.WARNING + if verbose == 1: + level = logging.INFO + elif verbose >= 2: + level = logging.DEBUG + + logging.basicConfig( + level=level, + format="%(levelname)s: %(message)s", + stream=sys.stderr, + ) + + +def get_output_text(hconfig: HConfig, platform: Platform) -> str: + """Get text output from HConfig based on platform. + + Args: + hconfig: The hierarchical configuration object + platform: The platform type + + Returns: + Formatted configuration text appropriate for the platform + """ + lines = [] + for line in hconfig.all_children_sorted(): + # Use platform-appropriate text formatting + if platform in (Platform.JUNIPER_JUNOS,): + # Juniper uses curly braces and different syntax + lines.append(line.text) + else: + # Cisco-style platforms (IOS, NXOS, XR, EOS, etc.) + lines.append(line.cisco_style_text()) + return "\n".join(lines) + + +def format_output(hconfig: HConfig, platform: Platform, output_format: str) -> str: + """Format configuration output in the requested format. + + Args: + hconfig: The hierarchical configuration object + platform: The platform type + output_format: Output format (text, json, yaml) + + Returns: + Formatted output string + + Raises: + ValueError: If output format is not supported + """ + if output_format == "text": + return get_output_text(hconfig, platform) + elif output_format == "json": + config_dict = {"config": get_output_text(hconfig, platform).split("\n")} + return json.dumps(config_dict, indent=2) + elif output_format == "yaml": + config_dict = {"config": get_output_text(hconfig, platform).split("\n")} + return yaml.dump(config_dict, default_flow_style=False) + else: + raise ValueError(f"Unsupported output format: {output_format}") + + +def process_configs( + platform_str: str, + running_config_path: str, + generated_config_path: str, + operation: str, +) -> tuple[HConfig, Platform]: + """Process configuration files and return the result. + + Args: + platform_str: Platform name string + running_config_path: Path to running configuration + generated_config_path: Path to generated configuration + operation: Operation type (remediation, rollback, future) + + Returns: + Tuple of (result HConfig, Platform enum) + + Raises: + click.ClickException: If processing fails + """ + try: + platform_enum = PLATFORM_MAP[platform_str.lower()] + logger.info(f"Using platform: {platform_str}") + except KeyError: + raise click.ClickException( + f"Unknown platform: {platform_str}. " + f"Use 'list-platforms' to see available platforms." + ) from None + + try: + logger.info(f"Reading running config from: {running_config_path}") + running_config_text = read_text_from_file(running_config_path) + except FileNotFoundError: + raise click.ClickException( + f"Running config file not found: {running_config_path}" + ) from None + except PermissionError: + raise click.ClickException( + f"Permission denied reading running config: {running_config_path}" + ) from None + except Exception as e: + raise click.ClickException(f"Error reading running config: {e}") from e + + try: + logger.info(f"Reading generated config from: {generated_config_path}") + generated_config_text = read_text_from_file(generated_config_path) + except FileNotFoundError: + raise click.ClickException( + f"Generated config file not found: {generated_config_path}" + ) from None + except PermissionError: + raise click.ClickException( + f"Permission denied reading generated config: {generated_config_path}" + ) from None + except Exception as e: + raise click.ClickException(f"Error reading generated config: {e}") from e + + try: + logger.info("Parsing configurations") + running_hconfig = get_hconfig(platform_enum, running_config_text) + generated_hconfig = get_hconfig(platform_enum, generated_config_text) + except Exception as e: + raise click.ClickException(f"Error parsing configuration: {e}") from e + + try: + logger.info(f"Generating {operation} configuration") + if operation == "future": + result = running_hconfig.future(generated_hconfig) + else: + workflow = WorkflowRemediation(running_hconfig, generated_hconfig) + result = ( + workflow.remediation_config + if operation == "remediation" + else workflow.rollback_config + ) + except Exception as e: + raise click.ClickException(f"Error generating {operation}: {e}") from e + + return result, platform_enum + + +@click.group() +@click.option( + "-v", + "--verbose", + count=True, + help="Increase verbosity (use -v for INFO, -vv for DEBUG)", +) +@click.pass_context +def cli(ctx: click.Context, verbose: int) -> None: + """Hier Config CLI Tool - Network configuration analysis and remediation. + + This tool provides commands to analyze network device configurations, + generate remediation steps, rollback configurations, and predict future states. + """ + ctx.ensure_object(dict) + ctx.obj["verbose"] = verbose + setup_logging(verbose) + + +def common_options(func: F) -> F: + """Reusable options for platform, running config, and generated config.""" + func = click.option( + "--platform", + type=click.Choice(list(PLATFORM_MAP.keys()), case_sensitive=False), + required=True, + help="Platform driver to use (e.g., ios, nxos, iosxr, eos, junos, vyos, fortios, generic).", + )(func) + func = click.option( + "--running-config", + type=click.Path(exists=True, readable=True), + required=True, + help="Path to the running configuration file.", + )(func) + func = click.option( + "--generated-config", + type=click.Path(exists=True, readable=True), + required=True, + help="Path to the generated (intended) configuration file.", + )(func) + func = click.option( + "--format", + "output_format", + type=click.Choice(["text", "json", "yaml"], case_sensitive=False), + default="text", + help="Output format (default: text).", + )(func) + func = click.option( + "--output", + "-o", + "output_file", + type=click.Path(), + default=None, + help="Write output to file instead of stdout.", + )(func) + return func + + +@cli.command() +@common_options +def remediation( + platform: str, + running_config: str, + generated_config: str, + output_format: str, + output_file: str | None, +) -> None: + """Generate the remediation configuration. + + Compares the running configuration with the generated (intended) configuration + and produces the commands needed to transform the running config into the + generated config. + + Example: + hier-config-cli remediation --platform ios \\ + --running-config running.conf --generated-config intended.conf + """ + result, platform_enum = process_configs( + platform, running_config, generated_config, "remediation" + ) + + try: + output = format_output(result, platform_enum, output_format) + except ValueError as e: + raise click.ClickException(str(e)) from e + + if output_file: + try: + Path(output_file).write_text(output) + click.echo(f"Remediation configuration written to: {output_file}", err=True) + except Exception as e: + raise click.ClickException(f"Error writing output file: {e}") from e + else: + click.echo("\n=== Remediation Configuration ===") + click.echo(output) + + +@cli.command() +@common_options +def rollback( + platform: str, + running_config: str, + generated_config: str, + output_format: str, + output_file: str | None, +) -> None: + """Generate the rollback configuration. + + Produces the commands needed to revert from the generated configuration + back to the running configuration. This is useful for preparing rollback + procedures before making changes. + + Example: + hier-config-cli rollback --platform ios \\ + --running-config running.conf --generated-config intended.conf + """ + result, platform_enum = process_configs(platform, running_config, generated_config, "rollback") + + try: + output = format_output(result, platform_enum, output_format) + except ValueError as e: + raise click.ClickException(str(e)) from e + + if output_file: + try: + Path(output_file).write_text(output) + click.echo(f"Rollback configuration written to: {output_file}", err=True) + except Exception as e: + raise click.ClickException(f"Error writing output file: {e}") from e + else: + click.echo("\n=== Rollback Configuration ===") + click.echo(output) + + +@cli.command() +@common_options +def future( + platform: str, + running_config: str, + generated_config: str, + output_format: str, + output_file: str | None, +) -> None: + """Generate the future configuration. + + Predicts what the complete configuration will look like after applying + the generated configuration to the running configuration. + + Example: + hier-config-cli future --platform ios \\ + --running-config running.conf --generated-config intended.conf + """ + result, platform_enum = process_configs(platform, running_config, generated_config, "future") + + try: + output = format_output(result, platform_enum, output_format) + except ValueError as e: + raise click.ClickException(str(e)) from e + + if output_file: + try: + Path(output_file).write_text(output) + click.echo(f"Future configuration written to: {output_file}", err=True) + except Exception as e: + raise click.ClickException(f"Error writing output file: {e}") from e + else: + click.echo("\n=== Future Configuration ===") + click.echo(output) + + +@cli.command() +def list_platforms() -> None: + """List all available platforms. + + Shows all supported network device platforms that can be used + with the --platform option. + """ + click.echo("\n=== Available Platforms ===") + for platform in sorted(PLATFORM_MAP.keys()): + click.echo(f" {platform}") + click.echo() + + +@cli.command() +def version() -> None: + """Show the version and exit.""" + click.echo(f"hier-config-cli version {__version__}") + + +if __name__ == "__main__": + cli() diff --git a/src/hier_config_cli/py.typed b/src/hier_config_cli/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cli.py b/tests/test_cli.py index 2a43bd8..7e3151c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,37 +1,110 @@ +"""Comprehensive tests for hier-config-cli.""" + +import json +from pathlib import Path + import pytest +import yaml from click.testing import CliRunner + from hier_config_cli import cli # Define test fixtures for mock configurations @pytest.fixture -def mock_running_config(tmp_path): +def mock_running_config(tmp_path: Path) -> str: + """Create a mock running configuration file. + + Args: + tmp_path: Pytest temporary directory fixture + + Returns: + Path to the mock running config file + """ config_path = tmp_path / "running_config.conf" - config_path.write_text("hostname test-router\ninterface Vlan1\n") + config_path.write_text( + "hostname test-router\ninterface Vlan1\n ip address 10.0.0.1 255.255.255.0\n" + ) return str(config_path) @pytest.fixture -def mock_generated_config(tmp_path): +def mock_generated_config(tmp_path: Path) -> str: + """Create a mock generated configuration file. + + Args: + tmp_path: Pytest temporary directory fixture + + Returns: + Path to the mock generated config file + """ config_path = tmp_path / "generated_config.conf" config_path.write_text( - "hostname test-router-updated\ninterface Vlan1\n ip address 10.0.0.1 255.255.255.0\n" + "hostname test-router-updated\n" + "interface Vlan1\n" + " ip address 10.0.0.1 255.255.255.0\n" + "interface Vlan2\n" + " ip address 10.0.1.1 255.255.255.0\n" ) return str(config_path) +@pytest.fixture +def mock_junos_running_config(tmp_path: Path) -> str: + """Create a mock Junos running configuration file. + + Args: + tmp_path: Pytest temporary directory fixture + + Returns: + Path to the mock Junos running config file + """ + config_path = tmp_path / "junos_running.conf" + config_path.write_text("system {\n host-name test-router;\n}\n") + return str(config_path) + + +@pytest.fixture +def mock_junos_generated_config(tmp_path: Path) -> str: + """Create a mock Junos generated configuration file. + + Args: + tmp_path: Pytest temporary directory fixture + + Returns: + Path to the mock Junos generated config file + """ + config_path = tmp_path / "junos_generated.conf" + config_path.write_text("system {\n host-name test-router-updated;\n}\n") + return str(config_path) + + +# Test `version` command +def test_version_command() -> None: + """Test the version command.""" + runner = CliRunner() + result = runner.invoke(cli, ["version"]) + assert result.exit_code == 0 + assert "hier-config-cli version" in result.output + assert "0.2.0" in result.output + + # Test `list_platforms` command -def test_list_platforms(): +def test_list_platforms() -> None: + """Test listing available platforms.""" runner = CliRunner() result = runner.invoke(cli, ["list-platforms"]) assert result.exit_code == 0 assert "Available Platforms" in result.output assert "ios" in result.output assert "junos" in result.output + assert "fortios" in result.output + assert "nxos" in result.output -# Test `remediation` command -def test_remediation_command(mock_running_config, mock_generated_config): +# Test `remediation` command - happy path +def test_remediation_command(mock_running_config: str, mock_generated_config: str) -> None: + """Test remediation command with valid inputs.""" runner = CliRunner() result = runner.invoke( cli, @@ -51,8 +124,93 @@ def test_remediation_command(mock_running_config, mock_generated_config): assert "hostname test-router-updated" in result.output +# Test `remediation` command - JSON output +def test_remediation_json_output(mock_running_config: str, mock_generated_config: str) -> None: + """Test remediation command with JSON output format.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", + "ios", + "--running-config", + mock_running_config, + "--generated-config", + mock_generated_config, + "--format", + "json", + ], + ) + assert result.exit_code == 0 + # Parse JSON to verify it's valid + output_lines = result.output.split("\n") + json_start = next(i for i, line in enumerate(output_lines) if line.strip().startswith("{")) + json_output = "\n".join(output_lines[json_start:]) + data = json.loads(json_output) + assert "config" in data + assert isinstance(data["config"], list) + + +# Test `remediation` command - YAML output +def test_remediation_yaml_output(mock_running_config: str, mock_generated_config: str) -> None: + """Test remediation command with YAML output format.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", + "ios", + "--running-config", + mock_running_config, + "--generated-config", + mock_generated_config, + "--format", + "yaml", + ], + ) + assert result.exit_code == 0 + # Parse YAML to verify it's valid + output_lines = result.output.split("\n") + yaml_start = next(i for i, line in enumerate(output_lines) if "config:" in line) + yaml_output = "\n".join(output_lines[yaml_start:]) + data = yaml.safe_load(yaml_output) + assert "config" in data + assert isinstance(data["config"], list) + + +# Test `remediation` command - output to file +def test_remediation_output_file( + mock_running_config: str, mock_generated_config: str, tmp_path: Path +) -> None: + """Test remediation command with output to file.""" + output_file = tmp_path / "output.txt" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", + "ios", + "--running-config", + mock_running_config, + "--generated-config", + mock_generated_config, + "--output", + str(output_file), + ], + ) + assert result.exit_code == 0 + assert output_file.exists() + content = output_file.read_text() + assert "no hostname test-router" in content + assert "hostname test-router-updated" in content + + # Test `rollback` command -def test_rollback_command(mock_running_config, mock_generated_config): +def test_rollback_command(mock_running_config: str, mock_generated_config: str) -> None: + """Test rollback command with valid inputs.""" runner = CliRunner() result = runner.invoke( cli, @@ -73,7 +231,8 @@ def test_rollback_command(mock_running_config, mock_generated_config): # Test `future` command -def test_future_command(mock_running_config, mock_generated_config): +def test_future_command(mock_running_config: str, mock_generated_config: str) -> None: + """Test future command with valid inputs.""" runner = CliRunner() result = runner.invoke( cli, @@ -90,3 +249,203 @@ def test_future_command(mock_running_config, mock_generated_config): assert result.exit_code == 0 assert "Future Configuration" in result.output assert "hostname test-router-updated" in result.output + + +# Error handling tests +def test_invalid_platform(mock_running_config: str, mock_generated_config: str) -> None: + """Test error handling for invalid platform.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", + "invalid_platform", + "--running-config", + mock_running_config, + "--generated-config", + mock_generated_config, + ], + ) + assert result.exit_code != 0 + + +def test_missing_running_config(mock_generated_config: str) -> None: + """Test error handling for missing running config file.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", + "ios", + "--running-config", + "/nonexistent/file.conf", + "--generated-config", + mock_generated_config, + ], + ) + assert result.exit_code != 0 + assert "does not exist" in result.output + + +def test_missing_generated_config(mock_running_config: str) -> None: + """Test error handling for missing generated config file.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", + "ios", + "--running-config", + mock_running_config, + "--generated-config", + "/nonexistent/file.conf", + ], + ) + assert result.exit_code != 0 + assert "does not exist" in result.output + + +def test_unreadable_file(tmp_path: Path, mock_generated_config: str) -> None: + """Test error handling for unreadable config file.""" + unreadable_file = tmp_path / "unreadable.conf" + unreadable_file.write_text("hostname test") + unreadable_file.chmod(0o000) + + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", + "ios", + "--running-config", + str(unreadable_file), + "--generated-config", + mock_generated_config, + ], + ) + + # Clean up permissions + unreadable_file.chmod(0o644) + + assert result.exit_code != 0 + + +# Test verbose logging +def test_verbose_logging(mock_running_config: str, mock_generated_config: str) -> None: + """Test verbose logging output.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "-v", + "remediation", + "--platform", + "ios", + "--running-config", + mock_running_config, + "--generated-config", + mock_generated_config, + ], + ) + assert result.exit_code == 0 + + +def test_very_verbose_logging(mock_running_config: str, mock_generated_config: str) -> None: + """Test very verbose (debug) logging output.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "-vv", + "remediation", + "--platform", + "ios", + "--running-config", + mock_running_config, + "--generated-config", + mock_generated_config, + ], + ) + assert result.exit_code == 0 + + +# Test all platforms +@pytest.mark.parametrize( + "platform", + [ + "ios", + "nxos", + "iosxr", + "eos", + "junos", + "vyos", + "fortios", + "generic", + "hp_comware5", + "hp_procurve", + ], +) +def test_all_platforms(platform: str, mock_running_config: str, mock_generated_config: str) -> None: + """Test remediation command works with all supported platforms.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", + platform, + "--running-config", + mock_running_config, + "--generated-config", + mock_generated_config, + ], + ) + assert result.exit_code == 0 + + +# Test Junos platform with appropriate configs +def test_junos_platform_specific( + mock_junos_running_config: str, mock_junos_generated_config: str +) -> None: + """Test Junos platform with Junos-specific configurations.""" + runner = CliRunner() + result = runner.invoke( + cli, + [ + "remediation", + "--platform", + "junos", + "--running-config", + mock_junos_running_config, + "--generated-config", + mock_junos_generated_config, + ], + ) + assert result.exit_code == 0 + + +# Test help text +def test_help_command() -> None: + """Test help command output.""" + runner = CliRunner() + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + assert "Hier Config CLI Tool" in result.output + assert "remediation" in result.output + assert "rollback" in result.output + assert "future" in result.output + + +def test_remediation_help() -> None: + """Test remediation command help output.""" + runner = CliRunner() + result = runner.invoke(cli, ["remediation", "--help"]) + assert result.exit_code == 0 + assert "Generate the remediation configuration" in result.output + assert "--platform" in result.output + assert "--running-config" in result.output + assert "--generated-config" in result.output