diff --git a/netnir/__init__.py b/netnir/__init__.py index 48f70d6..b736448 100644 --- a/netnir/__init__.py +++ b/netnir/__init__.py @@ -3,4 +3,4 @@ doesn't exist, netnir will create the default config and folders. """ -__version__ = "0.0.20" +__version__ = "0.0.21" diff --git a/netnir/cli.py b/netnir/cli.py index a9736f5..b448d0e 100644 --- a/netnir/cli.py +++ b/netnir/cli.py @@ -21,7 +21,7 @@ def __init__(self): self.plugins = NETNIR_CONFIG["plugins"] self.parser = MyParser(prog="netnir") self.parser.add_argument( - "--version", default=False, action="store_true", help="display version" + "--version", default=False, action="store_true", help="display version", ) subparsers = self.parser.add_subparsers(title="netnir commands", dest="command") self.loaded_plugins = plugins_import(tasks=self.plugins, subparsers=subparsers) diff --git a/netnir/core/inventory.py b/netnir/core/inventory.py index b91a8a9..e41ba56 100644 --- a/netnir/core/inventory.py +++ b/netnir/core/inventory.py @@ -14,7 +14,7 @@ def __init__(self, **kwargs): initialize the NornirInventory class and load the data into nornir """ super().__init__( - hosts=self.nhosts(), groups=self.ngroups(), defaults=self.ndefaults() + hosts=self.nhosts(), groups=self.ngroups(), defaults=self.ndefaults(), ) def nhosts(self): diff --git a/netnir/helpers/common/args.py b/netnir/helpers/common/args.py index 3963634..0ed10a2 100644 --- a/netnir/helpers/common/args.py +++ b/netnir/helpers/common/args.py @@ -174,3 +174,23 @@ def netconf_capabilities(parser, required: bool = False): nargs="?", const=True, ) + + +def config_to_yang(parser, required: bool = False) -> None: + """ + common argument to convert configuration text to a yang model via the + --config-to-yang flag. + """ + parser.add_argument( + "--config-to-yang", help="convert configuration text to a yang model", + ) + + +def yang_to_config(parser, required: bool = False) -> None: + """ + common argument to convert yang model to configuration text via the + --yang-to-config flag. + """ + parser.add_argument( + "--yang-to-config", help="convert configuration text to a yang model", + ) diff --git a/netnir/helpers/defaults.py b/netnir/helpers/defaults.py index a4edc28..5fde4d1 100644 --- a/netnir/helpers/defaults.py +++ b/netnir/helpers/defaults.py @@ -6,31 +6,31 @@ "output": "./output", "hier": "./conf/hier", }, - "domain": "example.net", + "domain": None, "nornir": {"config": "./conf/nornir.yaml"}, "plugins": { "user": { - "class": "netnir.core.tasks.user.User", + "class": "netnir.plugins.tasks.user.User", "description": "netnir user commands", }, "inventory": { - "class": "netnir.core.tasks.inventory.Inventory", + "class": "netnir.plugins.tasks.inventory.Inventory", "description": "inventory search command", }, "cp": { - "class": "netnir.core.tasks.config_plan.ConfigPlan", + "class": "netnir.plugins.tasks.config_plan.ConfigPlan", "description": "config plan commands", }, "ssh": { - "class": "netnir.core.tasks.ssh.Ssh", + "class": "netnir.plugins.tasks.ssh.Ssh", "description": "command and config execution over SSH", }, "netconf": { - "class": "netnir.core.tasks.netconf.NetConf", + "class": "netnir.plugins.tasks.netconf.NetConf", "description": "command and config execution over NETCONF", }, "fetch": { - "class": "netnir.core.tasks.fetch.Fetch", + "class": "netnir.plugins.tasks.fetch.Fetch", "description": "fetch commands", }, }, diff --git a/netnir/plugins/hier.py b/netnir/plugins/hier.py index 9843f3c..3f976c5 100644 --- a/netnir/plugins/hier.py +++ b/netnir/plugins/hier.py @@ -49,7 +49,7 @@ def hier_host( ) host = Host( - hostname=task.host.name, os=operating_system, hconfig_options=hier_options + hostname=task.host.name, os=operating_system, hconfig_options=hier_options, ) host.load_config_from( config_type="running", name=running_config, load_file=load_file diff --git a/netnir/plugins/rosetta.py b/netnir/plugins/rosetta.py new file mode 100644 index 0000000..c630985 --- /dev/null +++ b/netnir/plugins/rosetta.py @@ -0,0 +1,43 @@ +def convert_config_to_yang(operating_system: str, config_file: str): + """ + convert a config string into a yang model + + :param operating_system: type str + :param config_file: configuration file from CWD + + :returns: YANG output in JSON format + """ + from ntc_rosetta import get_driver + import json + + with open(config_file) as f: + config_string = f.read() + + os_driver = get_driver(operating_system, "openconfig") + driver = os_driver() + parsed = driver.parse(native={"dev_conf": config_string}) + + return json.dumps(parsed.raw_value(), indent=2) + + +def convert_yang_to_config(operating_system: str, yang_file: str): + """ + convert a yang model to configuration text + + :param operating_system: type str + :param yang_file: yang file from CWD + + :returns: configuration text + """ + from ntc_rosetta import get_driver + import json + + with open(yang_file) as f: + yang_string = f.read() + yang_model = json.loads(yang_string) + + os_driver = get_driver(operating_system, "openconfig") + driver = os_driver() + config_text = driver.translate(candidate=yang_model) + + return config_text diff --git a/netnir/core/tasks/__init__.py b/netnir/plugins/tasks/__init__.py similarity index 100% rename from netnir/core/tasks/__init__.py rename to netnir/plugins/tasks/__init__.py diff --git a/netnir/core/tasks/config_plan.py b/netnir/plugins/tasks/config_plan.py similarity index 95% rename from netnir/core/tasks/config_plan.py rename to netnir/plugins/tasks/config_plan.py index 29810ea..ba44b8a 100644 --- a/netnir/core/tasks/config_plan.py +++ b/netnir/plugins/tasks/config_plan.py @@ -62,7 +62,7 @@ def run(self): to_console=self._verbose()["to_console"], ) output_writer(nornir_results=results, output_file="compiled.conf") - print_result(results) + print_result(result=results, severity_level=self._verbose()["level"]) if self.args.compile: return results @@ -77,7 +77,7 @@ def run(self): to_console=self._verbose()["to_console"], ) output_writer(nornir_results=results, output_file="running.conf") - print_result(results) + print_result(result=results, severity_level=self._verbose()["level"]) results = self.nr.run( task=hier_host, @@ -94,7 +94,6 @@ def run(self): to_console=self._verbose()["to_console"], ) output_writer(nornir_results=results, output_file="remediation.conf") - print_result(result=results, severity_level=self._verbose()["level"]) return results diff --git a/netnir/core/tasks/fetch/__init__.py b/netnir/plugins/tasks/fetch/__init__.py similarity index 86% rename from netnir/core/tasks/fetch/__init__.py rename to netnir/plugins/tasks/fetch/__init__.py index 0b2cfad..23538d4 100644 --- a/netnir/core/tasks/fetch/__init__.py +++ b/netnir/plugins/tasks/fetch/__init__.py @@ -9,7 +9,7 @@ class Fetch(SubCommandParser): title = "fetch commands" tasks = { "config": { - "class": "netnir.core.tasks.fetch.config.FetchConfig", + "class": "netnir.plugins.tasks.fetch.config.FetchConfig", "description": "fetch current config from a network device", }, } diff --git a/netnir/core/tasks/fetch/config.py b/netnir/plugins/tasks/fetch/config.py similarity index 100% rename from netnir/core/tasks/fetch/config.py rename to netnir/plugins/tasks/fetch/config.py diff --git a/netnir/core/tasks/inventory.py b/netnir/plugins/tasks/inventory.py similarity index 100% rename from netnir/core/tasks/inventory.py rename to netnir/plugins/tasks/inventory.py diff --git a/netnir/core/tasks/netconf.py b/netnir/plugins/tasks/netconf.py similarity index 100% rename from netnir/core/tasks/netconf.py rename to netnir/plugins/tasks/netconf.py diff --git a/netnir/core/tasks/ssh.py b/netnir/plugins/tasks/ssh.py similarity index 100% rename from netnir/core/tasks/ssh.py rename to netnir/plugins/tasks/ssh.py diff --git a/netnir/core/tasks/user.py b/netnir/plugins/tasks/user.py similarity index 100% rename from netnir/core/tasks/user.py rename to netnir/plugins/tasks/user.py diff --git a/setup.cfg b/setup.cfg index 8a8d786..a96a96c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,9 +7,8 @@ filterwarnings = addopts = -vv --cov=netnir tests/ + --cov-report term-missing --black netnir --flake8 netnir python_files = tests/*/*.py flake8-max-line-length = 127 -flake8-ignore = - netnir/core/__init__.py F401 \ No newline at end of file diff --git a/setup.py b/setup.py index 2f0fcc1..60258fb 100755 --- a/setup.py +++ b/setup.py @@ -17,6 +17,8 @@ "hier_config==v1.6.1", "keyring>=21.2.1", "keyrings.alt>=3.4.0", + "ntc-rosetta>=v0.2.0", + "dataclasses<0.7.0,>=0.6.0", ], tests_require=[ "pytest", diff --git a/tests/data/config.json b/tests/data/config.json new file mode 100644 index 0000000..ccd79db --- /dev/null +++ b/tests/data/config.json @@ -0,0 +1,45 @@ +{ + "openconfig-interfaces:interfaces": { + "interface": [ + { + "name": "FastEthernet1", + "config": { + "name": "FastEthernet1", + "type": "iana-if-type:ethernetCsmacd", + "description": "This is Fa1", + "enabled": false + } + } + ] + }, + "openconfig-network-instance:network-instances": { + "network-instance": [ + { + "name": "default", + "config": { + "name": "default" + }, + "vlans": { + "vlan": [ + { + "vlan-id": 10, + "config": { + "vlan-id": 10, + "name": "prod", + "status": "ACTIVE" + } + }, + { + "vlan-id": 20, + "config": { + "vlan-id": 20, + "name": "dev", + "status": "SUSPENDED" + } + } + ] + } + } + ] + } +} diff --git a/tests/data/config.txt b/tests/data/config.txt new file mode 100644 index 0000000..a99648d --- /dev/null +++ b/tests/data/config.txt @@ -0,0 +1,15 @@ +interface FastEthernet1 + description This is Fa1 + shutdown + exit +! +vlan 10 + name prod + no shutdown + exit +! +vlan 20 + name dev + shutdown + exit +! diff --git a/tests/data/netnir.yaml b/tests/data/netnir.yaml index 00fda26..44f53b6 100644 --- a/tests/data/netnir.yaml +++ b/tests/data/netnir.yaml @@ -13,20 +13,20 @@ nornir: plugins: cp: - class: netnir.core.tasks.config_plan.ConfigPlan + class: netnir.plugins.tasks.config_plan.ConfigPlan description: "config plan commands" fetch: - class: netnir.core.tasks.fetch.Fetch + class: netnir.plugins.tasks.fetch.Fetch description: fetch commands inventory: - class: netnir.core.tasks.inventory.Inventory + class: netnir.plugins.tasks.inventory.Inventory description: "inventory search command" user: - class: netnir.core.tasks.user.User + class: netnir.plugins.tasks.user.User description: "netnir user commands" ssh: - class: netnir.core.tasks.ssh.Ssh + class: netnir.plugins.tasks.ssh.Ssh description: "command and config execution over SSH" netconf: - class: netnir.core.tasks.netconf.NetConf + class: netnir.plugins.tasks.netconf.NetConf description: "command and config execution over NETCONF" diff --git a/tests/netnir/helpers/test_filter_type.py b/tests/netnir/helpers/test_filter_type.py index ec28240..e006c58 100644 --- a/tests/netnir/helpers/test_filter_type.py +++ b/tests/netnir/helpers/test_filter_type.py @@ -1,6 +1,12 @@ def test_filter_type(initial_setup): from netnir.helpers import filter_type - assert filter_type(host="router.dc1") == {"type": "host", "data": "router.dc1"} + assert filter_type(host="router.dc1") == { + "type": "host", + "data": "router.dc1", + } assert filter_type(group="dc1") == {"type": "group", "data": "dc1"} - assert filter_type(filter="os:iosxr") == {"type": "filter", "data": "os:iosxr"} + assert filter_type(filter="os:iosxr") == { + "type": "filter", + "data": "os:iosxr", + } diff --git a/tests/netnir/helpers/test_netnir_config.py b/tests/netnir/helpers/test_netnir_config.py index d539ada..1dd797b 100644 --- a/tests/netnir/helpers/test_netnir_config.py +++ b/tests/netnir/helpers/test_netnir_config.py @@ -13,27 +13,27 @@ def test_netnir_config(initial_setup): "nornir": {"config": "./tests/data/nornir.yaml"}, "plugins": { "user": { - "class": "netnir.core.tasks.user.User", + "class": "netnir.plugins.tasks.user.User", "description": "netnir user commands", }, "inventory": { - "class": "netnir.core.tasks.inventory.Inventory", + "class": "netnir.plugins.tasks.inventory.Inventory", "description": "inventory search command", }, "cp": { - "class": "netnir.core.tasks.config_plan.ConfigPlan", + "class": "netnir.plugins.tasks.config_plan.ConfigPlan", "description": "config plan commands", }, "ssh": { - "class": "netnir.core.tasks.ssh.Ssh", + "class": "netnir.plugins.tasks.ssh.Ssh", "description": "command and config execution over SSH", }, "netconf": { - "class": "netnir.core.tasks.netconf.NetConf", + "class": "netnir.plugins.tasks.netconf.NetConf", "description": "command and config execution over NETCONF", }, "fetch": { - "class": "netnir.core.tasks.fetch.Fetch", + "class": "netnir.plugins.tasks.fetch.Fetch", "description": "fetch commands", }, }, diff --git a/tests/netnir/core/tasks/test_tasks_config_plan.py b/tests/netnir/plugins/tasks/test_tasks_config_plan.py similarity index 87% rename from tests/netnir/core/tasks/test_tasks_config_plan.py rename to tests/netnir/plugins/tasks/test_tasks_config_plan.py index 9da10b9..897d349 100644 --- a/tests/netnir/core/tasks/test_tasks_config_plan.py +++ b/tests/netnir/plugins/tasks/test_tasks_config_plan.py @@ -1,5 +1,5 @@ def test_tasks_config_plan(): - from netnir.core.tasks.config_plan import ConfigPlan + from netnir.plugins.tasks.config_plan import ConfigPlan from netnir.helpers.common.args import ( filter_group, filter_host, @@ -8,6 +8,8 @@ def test_tasks_config_plan(): verbose, num_workers, make_changes, + config_to_yang, + yang_to_config, ) import argparse @@ -19,6 +21,8 @@ def test_tasks_config_plan(): verbose(parser) num_workers(parser) make_changes(parser) + config_to_yang(parser) + yang_to_config(parser) parser.add_argument("--compile", const=True, nargs="?") parser.add_argument("--include-tags", action="append") parser.add_argument("--exclude-tags", action="append") diff --git a/tests/netnir/core/tasks/test_tasks_inventory.py b/tests/netnir/plugins/tasks/test_tasks_inventory.py similarity index 94% rename from tests/netnir/core/tasks/test_tasks_inventory.py rename to tests/netnir/plugins/tasks/test_tasks_inventory.py index 0b69adf..53ab594 100644 --- a/tests/netnir/core/tasks/test_tasks_inventory.py +++ b/tests/netnir/plugins/tasks/test_tasks_inventory.py @@ -1,5 +1,5 @@ def test_tasks_inventory(): - from netnir.core.tasks.inventory import Inventory + from netnir.plugins.tasks.inventory import Inventory from netnir.helpers.common.args import ( filter_group, filter_hosts, diff --git a/tests/netnir/plugins/tasks/test_tasks_rosetta.py b/tests/netnir/plugins/tasks/test_tasks_rosetta.py new file mode 100644 index 0000000..be5c863 --- /dev/null +++ b/tests/netnir/plugins/tasks/test_tasks_rosetta.py @@ -0,0 +1,22 @@ +def test_rosetta_config_to_yang(): + from netnir.plugins.rosetta import convert_config_to_yang + import json + + with open("./tests/data/config.json") as f: + data = f.read() + model = json.loads(data) + + config = json.loads(convert_config_to_yang("ios", "./tests/data/config.txt")) + + assert config == model + + +def test_rosetta_yang_to_config(): + from netnir.plugins.rosetta import convert_yang_to_config + + with open("./tests/data/config.txt") as f: + config = f.read() + + model = convert_yang_to_config("ios", "./tests/data/config.json") + + assert model == config