Skip to content

Commit 599b493

Browse files
authored
Merge pull request #1 from netdevops/initial
initial hier-config-cli
2 parents 184d052 + 310bc2f commit 599b493

7 files changed

Lines changed: 774 additions & 1 deletion

File tree

.github/workflows/deploy.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: deploy to pypi
2+
3+
on:
4+
release:
5+
types: [created]
6+
7+
jobs:
8+
deploy:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v2
12+
- name: Set up Python
13+
uses: actions/setup-python@v2
14+
with:
15+
python-version: '3.9'
16+
- name: Install poetry
17+
uses: snok/install-poetry@v1
18+
with:
19+
version: 1.5.1
20+
- name: Build and publish to PyPI
21+
env:
22+
TWINE_API_KEY: ${{ secrets.TWINE_API_KEY }}
23+
run: |
24+
poetry config pypi-token.pypi $TWINE_API_KEY
25+
poetry publish --build

.github/workflows/test-app.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: hier-config-cli test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version:
15+
- "3.9"
16+
- "3.10"
17+
- "3.11"
18+
- "3.12"
19+
20+
steps:
21+
- uses: actions/checkout@v2
22+
- name: Set up Python ${{ matrix.python-version }}
23+
uses: actions/setup-python@v2
24+
with:
25+
python-version: ${{ matrix.python-version }}
26+
- name: Install poetry
27+
uses: snok/install-poetry@v1
28+
with:
29+
version: 1.5.1
30+
- name: Set PYTHONPATH
31+
run: echo "PYTHONPATH=src" >> $GITHUB_ENV
32+
- name: Run tests
33+
run: |
34+
poetry install --no-interaction --no-root
35+
poetry run black --check .
36+
poetry run pytest tests/

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,19 @@
11
# hier-config-cli
2-
Hierarchical Configuration CLI
2+
3+
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.
4+
5+
## Usage
6+
7+
```python
8+
hier-config-cli [COMMAND] --platform PLATFORM --running-config PATH --generated-config PATH
9+
```
10+
11+
### Available Commands
12+
13+
* remediation
14+
* rollback
15+
* future
16+
17+
## Contributing
18+
19+
Contributions are welcome! Please open an issue or submit a pull request for features, bug fixes, or documentation improvements.

poetry.lock

Lines changed: 459 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[tool.poetry]
2+
name = "hier-config-cli"
3+
version = "0.1.0"
4+
description = ""
5+
authors = ["James Williams <james.williams@packetgeek.net>"]
6+
license = "Apache 2.0"
7+
readme = "README.md"
8+
packages = [{include = "hier_config_cli.py", from = "src"}]
9+
10+
[tool.poetry.dependencies]
11+
python = "^3.9"
12+
hier-config = "^3.1.0"
13+
click = "^8.1.7"
14+
pyyaml = "^6.0.2"
15+
16+
[tool.poetry.group.dev.dependencies]
17+
black = "^24.10.0"
18+
pytest = "^8.3.4"
19+
20+
[build-system]
21+
requires = ["poetry-core"]
22+
build-backend = "poetry.core.masonry.api"
23+
24+
[tool.poetry.scripts]
25+
hier-config-cli = "hier_config_cli:cli"

src/hier_config_cli.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import click
2+
from hier_config import Platform, WorkflowRemediation, get_hconfig
3+
from hier_config.utils import read_text_from_file
4+
5+
# Mapping for driver platforms
6+
PLATFORM_MAP = {
7+
"ios": Platform.CISCO_IOS,
8+
"nxos": Platform.CISCO_NXOS,
9+
"iosxr": Platform.CISCO_XR,
10+
"eos": Platform.ARISTA_EOS,
11+
"junos": Platform.JUNIPER_JUNOS,
12+
"vyos": Platform.VYOS,
13+
"generic": Platform.GENERIC,
14+
"hp_comware5": Platform.HP_COMWARE5,
15+
"hp_procurve": Platform.HP_PROCURVE,
16+
}
17+
18+
19+
@click.group()
20+
def cli():
21+
"""Hier Config CLI Tool"""
22+
pass
23+
24+
25+
def common_options(func):
26+
"""Reusable options for platform, running config, and generated config."""
27+
func = click.option(
28+
"--platform",
29+
type=click.Choice(PLATFORM_MAP.keys(), case_sensitive=False),
30+
required=True,
31+
help="Platform driver to use (e.g., ios, nxos, iosxr, eos, junos, vyos, generic).",
32+
)(func)
33+
func = click.option(
34+
"--running-config",
35+
type=click.Path(exists=True, readable=True),
36+
required=True,
37+
help="Path to the running configuration file.",
38+
)(func)
39+
func = click.option(
40+
"--generated-config",
41+
type=click.Path(exists=True, readable=True),
42+
required=True,
43+
help="Path to the generated (intended) configuration file.",
44+
)(func)
45+
return func
46+
47+
48+
@cli.command()
49+
@common_options
50+
def remediation(platform, running_config, generated_config):
51+
"""
52+
Generate the remediation configuration.
53+
"""
54+
platform_enum = PLATFORM_MAP[platform.lower()]
55+
running_config_text = read_text_from_file(running_config)
56+
generated_config_text = read_text_from_file(generated_config)
57+
58+
running_hconfig = get_hconfig(platform_enum, running_config_text)
59+
generated_hconfig = get_hconfig(platform_enum, generated_config_text)
60+
61+
workflow = WorkflowRemediation(running_hconfig, generated_hconfig)
62+
63+
click.echo("\n=== Remediation Configuration ===")
64+
for line in workflow.remediation_config.all_children_sorted():
65+
click.echo(line.cisco_style_text())
66+
67+
68+
@cli.command()
69+
@common_options
70+
def rollback(platform, running_config, generated_config):
71+
"""
72+
Generate the rollback configuration.
73+
"""
74+
platform_enum = PLATFORM_MAP[platform.lower()]
75+
running_config_text = read_text_from_file(running_config)
76+
generated_config_text = read_text_from_file(generated_config)
77+
78+
running_hconfig = get_hconfig(platform_enum, running_config_text)
79+
generated_hconfig = get_hconfig(platform_enum, generated_config_text)
80+
81+
workflow = WorkflowRemediation(running_hconfig, generated_hconfig)
82+
83+
click.echo("\n=== Rollback Configuration ===")
84+
for line in workflow.rollback_config.all_children_sorted():
85+
click.echo(line.cisco_style_text())
86+
87+
88+
@cli.command()
89+
@common_options
90+
def future(platform, running_config, generated_config):
91+
"""
92+
Generate the future configuration.
93+
"""
94+
platform_enum = PLATFORM_MAP[platform.lower()]
95+
running_config_text = read_text_from_file(running_config)
96+
generated_config_text = read_text_from_file(generated_config)
97+
98+
running_hconfig = get_hconfig(platform_enum, running_config_text)
99+
generated_hconfig = get_hconfig(platform_enum, generated_config_text)
100+
101+
future_config = running_hconfig.future(generated_hconfig)
102+
103+
click.echo("\n=== Future Configuration ===")
104+
for line in future_config.all_children_sorted():
105+
click.echo(line.cisco_style_text())
106+
107+
108+
@cli.command()
109+
def list_platforms():
110+
"""
111+
List all available platforms.
112+
"""
113+
click.echo("\n=== Available Platforms ===")
114+
for platform in PLATFORM_MAP.keys():
115+
click.echo(f"- {platform}")
116+
117+
118+
if __name__ == "__main__":
119+
cli()

tests/test_cli.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import pytest
2+
from click.testing import CliRunner
3+
from hier_config_cli import cli
4+
5+
6+
# Define test fixtures for mock configurations
7+
@pytest.fixture
8+
def mock_running_config(tmp_path):
9+
config_path = tmp_path / "running_config.conf"
10+
config_path.write_text("hostname test-router\ninterface Vlan1\n")
11+
return str(config_path)
12+
13+
14+
@pytest.fixture
15+
def mock_generated_config(tmp_path):
16+
config_path = tmp_path / "generated_config.conf"
17+
config_path.write_text(
18+
"hostname test-router-updated\ninterface Vlan1\n ip address 10.0.0.1 255.255.255.0\n"
19+
)
20+
return str(config_path)
21+
22+
23+
# Test `list_platforms` command
24+
def test_list_platforms():
25+
runner = CliRunner()
26+
result = runner.invoke(cli, ["list-platforms"])
27+
assert result.exit_code == 0
28+
assert "Available Platforms" in result.output
29+
assert "ios" in result.output
30+
assert "junos" in result.output
31+
32+
33+
# Test `remediation` command
34+
def test_remediation_command(mock_running_config, mock_generated_config):
35+
runner = CliRunner()
36+
result = runner.invoke(
37+
cli,
38+
[
39+
"remediation",
40+
"--platform",
41+
"ios",
42+
"--running-config",
43+
mock_running_config,
44+
"--generated-config",
45+
mock_generated_config,
46+
],
47+
)
48+
assert result.exit_code == 0
49+
assert "Remediation Configuration" in result.output
50+
assert "no hostname test-router" in result.output
51+
assert "hostname test-router-updated" in result.output
52+
53+
54+
# Test `rollback` command
55+
def test_rollback_command(mock_running_config, mock_generated_config):
56+
runner = CliRunner()
57+
result = runner.invoke(
58+
cli,
59+
[
60+
"rollback",
61+
"--platform",
62+
"ios",
63+
"--running-config",
64+
mock_running_config,
65+
"--generated-config",
66+
mock_generated_config,
67+
],
68+
)
69+
assert result.exit_code == 0
70+
assert "Rollback Configuration" in result.output
71+
assert "hostname test-router" in result.output
72+
assert "no hostname test-router-updated" in result.output
73+
74+
75+
# Test `future` command
76+
def test_future_command(mock_running_config, mock_generated_config):
77+
runner = CliRunner()
78+
result = runner.invoke(
79+
cli,
80+
[
81+
"future",
82+
"--platform",
83+
"ios",
84+
"--running-config",
85+
mock_running_config,
86+
"--generated-config",
87+
mock_generated_config,
88+
],
89+
)
90+
assert result.exit_code == 0
91+
assert "Future Configuration" in result.output
92+
assert "hostname test-router-updated" in result.output

0 commit comments

Comments
 (0)