diff --git a/nornir/plugins/inventory/__init__.py b/nornir/plugins/inventory/__init__.py index e69de29b..35cdf7f4 100644 --- a/nornir/plugins/inventory/__init__.py +++ b/nornir/plugins/inventory/__init__.py @@ -0,0 +1,3 @@ +from .simple import SimpleInventory + +__all__ = ("SimpleInventory",) diff --git a/nornir/plugins/inventory/simple.py b/nornir/plugins/inventory/simple.py new file mode 100644 index 00000000..ec32afce --- /dev/null +++ b/nornir/plugins/inventory/simple.py @@ -0,0 +1,122 @@ +import logging +import pathlib +from typing import Any, Dict, Type + +from nornir.core.inventory import ( + Inventory, + Group, + Groups, + Host, + Hosts, + Defaults, + ConnectionOptions, + HostOrGroup, + ParentGroups, +) + +import ruamel.yaml + +logger = logging.getLogger(__name__) + + +def _get_connection_options(data: Dict[str, Any]) -> Dict[str, ConnectionOptions]: + cp = {} + for cn, c in data.items(): + cp[cn] = ConnectionOptions( + hostname=c.get("hostname"), + port=c.get("port"), + username=c.get("username"), + password=c.get("password"), + platform=c.get("platform"), + extras=c.get("extras"), + ) + return cp + + +def _get_defaults(data: Dict[str, Any]) -> Defaults: + return Defaults( + hostname=data.get("hostname"), + port=data.get("port"), + username=data.get("username"), + password=data.get("password"), + platform=data.get("platform"), + data=data.get("data"), + connection_options=_get_connection_options(data.get("connection_options", {})), + ) + + +def _get_inventory_element( + typ: Type[HostOrGroup], data: Dict[str, Any], name: str, defaults: Defaults +) -> HostOrGroup: + return typ( + name=name, + hostname=data.get("hostname"), + port=data.get("port"), + username=data.get("username"), + password=data.get("password"), + platform=data.get("platform"), + data=data.get("data"), + groups=data.get( + "groups" + ), # this is a hack, we will convert it later to the correct type + defaults=defaults, + connection_options=_get_connection_options(data.get("connection_options", {})), + ) + + +class SimpleInventory: + def __init__( + self, + host_file: str = "hosts.yaml", + group_file: str = "groups.yaml", + defaults_file: str = "defaults.yaml", + ) -> None: + """ + SimpleInventory is an inventory plugin that loads data from YAML files. + The YAML files follow the same structure as the native objects + + Args: + + host_file: path to file with hosts definition + group_file: path to file with groups definition. If + it doesn't exist it will be skipped + defaults_file: path to file with defaults definition. + If it doesn't exist it will be skipped + """ + + self.host_file = pathlib.Path(host_file) + self.group_file = pathlib.Path(group_file) + self.defaults_file = pathlib.Path(defaults_file) + + def load(self) -> Inventory: + yml = ruamel.yaml.YAML(typ="safe") + + if self.defaults_file.exists(): + with open(self.defaults_file, "r") as f: + defaults_dict = yml.load(f) + defaults = _get_defaults(defaults_dict) + else: + defaults = Defaults() + + hosts = Hosts() + with open(self.host_file, "r") as f: + hosts_dict = yml.load(f) + + for n, h in hosts_dict.items(): + hosts[n] = _get_inventory_element(Host, h, n, defaults) + + groups = Groups() + if self.group_file.exists(): + with open(self.group_file, "r") as f: + groups_dict = yml.load(f) + + for n, g in groups_dict.items(): + groups[n] = _get_inventory_element(Group, g, n, defaults) + + for h in hosts.values(): + h.groups = ParentGroups([groups[g] for g in h.groups]) + + for g in groups.values(): + g.groups = ParentGroups([groups[g] for g in g.groups]) + + return Inventory(hosts=hosts, groups=groups, defaults=defaults) diff --git a/pyproject.toml b/pyproject.toml index 35cdb9c5..1f9208fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,9 @@ build-backend = "poetry.masonry.api" "serial" = "nornir.plugins.runners:SerialRunner" "threaded" = "nornir.plugins.runners:ThreadedRunner" +[tool.poetry.plugins."nornir.plugins.inventory"] +"SimpleInventory" = "nornir.plugins.inventory.simple:SimpleInventory" + [tool.poetry] name = "nornir" version = "3.0.0a3" diff --git a/tests/core/test_registered_plugins.py b/tests/core/test_registered_plugins.py new file mode 100644 index 00000000..cc83d345 --- /dev/null +++ b/tests/core/test_registered_plugins.py @@ -0,0 +1,22 @@ +from nornir.core.plugins.inventory import InventoryPluginRegister +from nornir.core.plugins.runners import RunnersPluginRegister + +from nornir.plugins.inventory import SimpleInventory +from nornir.plugins.runners import SerialRunner, ThreadedRunner + + +class Test: + def test_registered_runners(self): + RunnersPluginRegister.deregister_all() + RunnersPluginRegister.auto_register() + assert RunnersPluginRegister.available == { + "threaded": ThreadedRunner, + "serial": SerialRunner, + } + + def test_registered_inventory(self): + InventoryPluginRegister.deregister_all() + InventoryPluginRegister.auto_register() + assert InventoryPluginRegister.available == { + "SimpleInventory": SimpleInventory, + } diff --git a/tests/plugins/inventory/__init__.py b/tests/plugins/inventory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/inventory/data/defaults.yaml b/tests/plugins/inventory/data/defaults.yaml new file mode 100644 index 00000000..9f5852a2 --- /dev/null +++ b/tests/plugins/inventory/data/defaults.yaml @@ -0,0 +1,18 @@ +--- +port: +hostname: +username: root +password: docker +platform: linux +data: + my_var: comes_from_defaults + only_default: only_defined_in_default +connection_options: + dummy: + hostname: dummy_from_defaults + port: + username: + password: + platform: + extras: + blah: from_defaults diff --git a/tests/plugins/inventory/data/groups.yaml b/tests/plugins/inventory/data/groups.yaml new file mode 100644 index 00000000..bf996b1e --- /dev/null +++ b/tests/plugins/inventory/data/groups.yaml @@ -0,0 +1,60 @@ +--- +parent_group: + port: + hostname: + username: + password: from_parent_group + platform: + data: + a_var: blah + a_false_var: false + groups: [] + connection_options: + dummy: + hostname: dummy_from_parent_group + port: + username: + password: + platform: + extras: + blah: from_group + dummy2: + hostname: dummy2_from_parent_group + port: + username: + password: + platform: + extras: + blah: from_group +group_1: + port: + hostname: + username: + password: from_group1 + platform: + data: + my_var: comes_from_group_1 + site: site1 + groups: + - parent_group + connection_options: {} +group_2: + port: + hostname: + username: + password: + platform: + data: + site: site2 + groups: [] + connection_options: {} +group_3: + port: + hostname: + username: + password: + platform: + groups: [] + data: + site: site2 + connection_options: {} diff --git a/tests/plugins/inventory/data/hosts.yaml b/tests/plugins/inventory/data/hosts.yaml new file mode 100644 index 00000000..8f11e479 --- /dev/null +++ b/tests/plugins/inventory/data/hosts.yaml @@ -0,0 +1,121 @@ +--- +dev1.group_1: + port: 65020 + hostname: localhost + username: + password: a_password + platform: eos + data: + my_var: comes_from_dev1.group_1 + www_server: nginx + role: www + nested_data: + a_dict: + a: 1 + b: 2 + a_list: [1, 2] + a_string: asdasd + groups: + - group_1 + connection_options: + paramiko: + port: 65020 + hostname: + username: root + password: docker + platform: linux + extras: {} + dummy: + hostname: dummy_from_host + port: + username: + password: + platform: + extras: + blah: from_host +dev2.group_1: + port: 65021 + hostname: localhost + username: + password: + platform: junos + data: + role: db + nested_data: + a_dict: + b: 2 + c: 3 + a_list: [2, 3] + a_string: qwe + groups: + - group_1 + connection_options: + paramiko: + port: + hostname: + username: root + password: docker + platform: linux + extras: {} + dummy2: + hostname: + port: + username: dummy2_from_host + password: + platform: + extras: +dev3.group_2: + port: 65022 + hostname: localhost + username: + password: + platform: linux + data: + www_server: apache + role: www + groups: + - group_2 + connection_options: + nornir_napalm.napalm: + platform: mock + hostname: + port: + username: + password: + extras: {} +dev4.group_2: + port: 65023 + hostname: localhost + username: + password: + platform: linux + data: + my_var: comes_from_dev4.group_2 + role: db + groups: + - parent_group + - group_2 + connection_options: + paramiko: + port: + hostname: localhost + username: root + password: docker + platform: linux + extras: {} + netmiko: + port: + hostname: localhost + username: root + password: docker + platform: linux + extras: {} +dev5.no_group: + port: 65024 + hostname: localhost + username: + password: + platform: linux + data: {} + groups: [] + connection_options: {} diff --git a/tests/plugins/inventory/data/netconf_hosts.yaml b/tests/plugins/inventory/data/netconf_hosts.yaml new file mode 100644 index 00000000..b5947828 --- /dev/null +++ b/tests/plugins/inventory/data/netconf_hosts.yaml @@ -0,0 +1,13 @@ +--- +netconf1.no_group: + hostname: localhost + username: netconf + password: netconf + port: 65025 + connection_options: + netconf: + extras: + allow_agent: False + hostkey_verify: False + look_for_keys: False + ssh_config: null diff --git a/tests/plugins/inventory/test_simple_inventory.py b/tests/plugins/inventory/test_simple_inventory.py new file mode 100644 index 00000000..2c2548fe --- /dev/null +++ b/tests/plugins/inventory/test_simple_inventory.py @@ -0,0 +1,233 @@ +import os + +from nornir.plugins.inventory import SimpleInventory + +dir_path = os.path.dirname(os.path.realpath(__file__)) + + +class Test: + def test(self): + host_file = f"{dir_path}/data/hosts.yaml" + group_file = f"{dir_path}/data/groups.yaml" + defaults_file = f"{dir_path}/data/defaults.yaml" + + inv = SimpleInventory(host_file, group_file, defaults_file).load() + assert inv.dict() == { + "defaults": { + "connection_options": { + "dummy": { + "extras": {"blah": "from_defaults"}, + "hostname": "dummy_from_defaults", + "password": None, + "platform": None, + "port": None, + "username": None, + } + }, + "data": { + "my_var": "comes_from_defaults", + "only_default": "only_defined_in_default", + }, + "hostname": None, + "password": "docker", + "platform": "linux", + "port": None, + "username": "root", + }, + "groups": { + "group_1": { + "connection_options": {}, + "data": {"my_var": "comes_from_group_1", "site": "site1"}, + "groups": ["parent_group"], + "hostname": None, + "name": "group_1", + "password": "from_group1", + "platform": "linux", + "port": None, + "username": "root", + }, + "group_2": { + "connection_options": {}, + "data": {"site": "site2"}, + "groups": [], + "hostname": None, + "name": "group_2", + "password": "docker", + "platform": "linux", + "port": None, + "username": "root", + }, + "group_3": { + "connection_options": {}, + "data": {"site": "site2"}, + "groups": [], + "hostname": None, + "name": "group_3", + "password": "docker", + "platform": "linux", + "port": None, + "username": "root", + }, + "parent_group": { + "connection_options": { + "dummy": { + "extras": {"blah": "from_group"}, + "hostname": "dummy_from_parent_group", + "password": None, + "platform": None, + "port": None, + "username": None, + }, + "dummy2": { + "extras": {"blah": "from_group"}, + "hostname": "dummy2_from_parent_group", + "password": None, + "platform": None, + "port": None, + "username": None, + }, + }, + "data": {"a_false_var": False, "a_var": "blah"}, + "groups": [], + "hostname": None, + "name": "parent_group", + "password": "from_parent_group", + "platform": "linux", + "port": None, + "username": "root", + }, + }, + "hosts": { + "dev1.group_1": { + "connection_options": { + "dummy": { + "extras": {"blah": "from_host"}, + "hostname": "dummy_from_host", + "password": None, + "platform": None, + "port": None, + "username": None, + }, + "paramiko": { + "extras": {}, + "hostname": None, + "password": "docker", + "platform": "linux", + "port": 65020, + "username": "root", + }, + }, + "data": { + "my_var": "comes_from_dev1.group_1", + "nested_data": { + "a_dict": {"a": 1, "b": 2}, + "a_list": [1, 2], + "a_string": "asdasd", + }, + "role": "www", + "www_server": "nginx", + }, + "groups": ["group_1"], + "hostname": "localhost", + "name": "dev1.group_1", + "password": "a_password", + "platform": "eos", + "port": 65020, + "username": "root", + }, + "dev2.group_1": { + "connection_options": { + "dummy2": { + "extras": None, + "hostname": None, + "password": None, + "platform": None, + "port": None, + "username": "dummy2_from_host", + }, + "paramiko": { + "extras": {}, + "hostname": None, + "password": "docker", + "platform": "linux", + "port": None, + "username": "root", + }, + }, + "data": { + "nested_data": { + "a_dict": {"b": 2, "c": 3}, + "a_list": [2, 3], + "a_string": "qwe", + }, + "role": "db", + }, + "groups": ["group_1"], + "hostname": "localhost", + "name": "dev2.group_1", + "password": "from_group1", + "platform": "junos", + "port": 65021, + "username": "root", + }, + "dev3.group_2": { + "connection_options": { + "nornir_napalm.napalm": { + "extras": {}, + "hostname": None, + "password": None, + "platform": "mock", + "port": None, + "username": None, + } + }, + "data": {"role": "www", "www_server": "apache"}, + "groups": ["group_2"], + "hostname": "localhost", + "name": "dev3.group_2", + "password": "docker", + "platform": "linux", + "port": 65022, + "username": "root", + }, + "dev4.group_2": { + "connection_options": { + "netmiko": { + "extras": {}, + "hostname": "localhost", + "password": "docker", + "platform": "linux", + "port": None, + "username": "root", + }, + "paramiko": { + "extras": {}, + "hostname": "localhost", + "password": "docker", + "platform": "linux", + "port": None, + "username": "root", + }, + }, + "data": {"my_var": "comes_from_dev4.group_2", "role": "db"}, + "groups": ["parent_group", "group_2"], + "hostname": "localhost", + "name": "dev4.group_2", + "password": "from_parent_group", + "platform": "linux", + "port": 65023, + "username": "root", + }, + "dev5.no_group": { + "connection_options": {}, + "data": {}, + "groups": [], + "hostname": "localhost", + "name": "dev5.no_group", + "password": "docker", + "platform": "linux", + "port": 65024, + "username": "root", + }, + }, + }