Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Inventory data plugin #888

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions nornir/core/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,30 @@ def dict(self) -> Dict[str, Any]:
}


class InventoryDataConfig(object):
__slots__ = "plugin", "options"

class Parameters:
plugin = Parameter(
typ=str, default="InventoryDataDict", envvar="NORNIR_INVENTORY_DATA_PLUGIN"
)
options = Parameter(default={}, envvar="NORNIR_INVENTORY_DATA_OPTIONS")

def __init__(
self,
plugin: Optional[str] = None,
options: Optional[Dict[str, Any]] = None,
) -> None:
self.plugin = self.Parameters.plugin.resolve(plugin)
self.options = self.Parameters.options.resolve(options) or {}

def dict(self) -> Dict[str, Any]:
return {
"plugin": self.plugin,
"options": self.options,
}


class LoggingConfig(object):
__slots__ = "enabled", "level", "log_file", "format", "to_console", "loggers"

Expand Down Expand Up @@ -245,20 +269,23 @@ class Config(object):
"runner",
"ssh",
"inventory",
"inventory_data",
"logging",
"user_defined",
)

def __init__(
self,
inventory: Optional[InventoryConfig] = None,
inventory_data: Optional[InventoryDataConfig] = None,
ssh: Optional[SSHConfig] = None,
logging: Optional[LoggingConfig] = None,
core: Optional[CoreConfig] = None,
runner: Optional[RunnerConfig] = None,
user_defined: Optional[Dict[str, Any]] = None,
) -> None:
self.inventory = inventory or InventoryConfig()
self.inventory_data = inventory_data or InventoryDataConfig()
self.ssh = ssh or SSHConfig()
self.logging = logging or LoggingConfig()
self.core = core or CoreConfig()
Expand All @@ -269,6 +296,7 @@ def __init__(
def from_dict(
cls,
inventory: Optional[Dict[str, Any]] = None,
inventory_data: Optional[Dict[str, Any]] = None,
ssh: Optional[Dict[str, Any]] = None,
logging: Optional[Dict[str, Any]] = None,
core: Optional[Dict[str, Any]] = None,
Expand All @@ -277,6 +305,7 @@ def from_dict(
) -> "Config":
return cls(
inventory=InventoryConfig(**inventory or {}),
inventory_data=InventoryDataConfig(**inventory_data or {}),
ssh=SSHConfig(**ssh or {}),
logging=LoggingConfig(**logging or {}),
core=CoreConfig(**core or {}),
Expand All @@ -289,13 +318,15 @@ def from_file(
cls,
config_file: str,
inventory: Optional[Dict[str, Any]] = None,
inventory_data: Optional[Dict[str, Any]] = None,
ssh: Optional[Dict[str, Any]] = None,
logging: Optional[Dict[str, Any]] = None,
core: Optional[Dict[str, Any]] = None,
runner: Optional[Dict[str, Any]] = None,
user_defined: Optional[Dict[str, Any]] = None,
) -> "Config":
inventory = inventory or {}
inventory_data = inventory_data or {}
ssh = ssh or {}
logging = logging or {}
core = core or {}
Expand All @@ -306,6 +337,9 @@ def from_file(
data = yml.load(f)
return cls(
inventory=InventoryConfig(**{**data.get("inventory", {}), **inventory}),
inventory_data=InventoryDataConfig(
**{**data.get("inventory_data", {}), **inventory_data}
),
ssh=SSHConfig(**{**data.get("ssh", {}), **ssh}),
logging=LoggingConfig(**{**data.get("logging", {}), **logging}),
core=CoreConfig(**{**data.get("core", {}), **core}),
Expand All @@ -316,6 +350,7 @@ def from_file(
def dict(self) -> Dict[str, Any]:
return {
"inventory": self.inventory.dict(),
"inventory_data": self.inventory_data.dict(),
"ssh": self.ssh.dict(),
"logging": self.logging.dict(),
"core": self.core.dict(),
Expand Down
24 changes: 22 additions & 2 deletions nornir/core/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,26 @@
from nornir.core.configuration import Config
from nornir.core.exceptions import ConnectionAlreadyOpen, ConnectionNotOpen
from nornir.core.plugins.connections import ConnectionPlugin, ConnectionPluginRegister
from nornir.core.plugins.inventory_data import (
InventoryData,
InventoryDataPluginRegister,
)

HostOrGroup = TypeVar("HostOrGroup", "Host", "Group")


def _init_inventory_data(
data: Optional[Dict[str, Any]] = None, configuration: Optional[Config] = None
) -> Union[Dict[str, Any], InventoryData]:
if not configuration:
configuration = Config()
InventoryDataPluginRegister.auto_register()
inventory_data_plugin = InventoryDataPluginRegister.get_plugin(
configuration.inventory_data.plugin
)
return inventory_data_plugin(**configuration.inventory_data.options).load(data)


class BaseAttributes(object):
__slots__ = ("hostname", "port", "username", "password", "platform")

Expand Down Expand Up @@ -125,9 +141,10 @@ def __init__(
groups: Optional[ParentGroups] = None,
data: Optional[Dict[str, Any]] = None,
connection_options: Optional[Dict[str, ConnectionOptions]] = None,
configuration: Optional[Config] = None,
) -> None:
self.groups = groups or ParentGroups()
self.data = data or {}
self.data = _init_inventory_data(data, configuration=configuration)
self.connection_options = connection_options or {}
super().__init__(
hostname=hostname,
Expand Down Expand Up @@ -208,8 +225,9 @@ def __init__(
platform: Optional[str] = None,
data: Optional[Dict[str, Any]] = None,
connection_options: Optional[Dict[str, ConnectionOptions]] = None,
configuration: Optional[Config] = None,
) -> None:
self.data = data or {}
self.data = _init_inventory_data(data, configuration=configuration)
self.connection_options = connection_options or {}
super().__init__(
hostname=hostname,
Expand Down Expand Up @@ -252,6 +270,7 @@ def __init__(
data: Optional[Dict[str, Any]] = None,
connection_options: Optional[Dict[str, ConnectionOptions]] = None,
defaults: Optional[Defaults] = None,
configuration: Optional[Config] = None,
) -> None:
self.name = name
self.defaults = defaults or Defaults(None, None, None, None, None, None, None)
Expand All @@ -265,6 +284,7 @@ def __init__(
groups=groups,
data=data,
connection_options=connection_options,
configuration=configuration,
)

def extended_data(self) -> Dict[str, Any]:
Expand Down
80 changes: 80 additions & 0 deletions nornir/core/plugins/inventory_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import (
Any,
Dict,
ItemsView,
KeysView,
Optional,
Protocol,
Type,
Union,
ValuesView,
)

from nornir.core.plugins.register import PluginRegister

INVENTORY_DATA_PLUGIN_PATH = "nornir.plugins.inventory_data"


class InventoryData(Protocol):
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
This method configures the plugin
"""
raise NotImplementedError("needs to be implemented by the plugin")

def __getitem__(self, key: str) -> Any:
"""
This method configures the plugin
"""
raise NotImplementedError("needs to be implemented by the plugin")

def get(self, key: str, default: Any = None) -> Any:
"""
This method configures the plugin
"""
raise NotImplementedError("needs to be implemented by the plugin")

def __setitem__(self, key: str, value: Any) -> None:
"""
This method configures the plugin
"""
raise NotImplementedError("needs to be implemented by the plugin")

def keys(self) -> KeysView[str]:
"""
This method configures the plugin
"""
raise NotImplementedError("needs to be implemented by the plugin")

def values(self) -> ValuesView[Any]:
"""
This method configures the plugin
"""
raise NotImplementedError("needs to be implemented by the plugin")

def items(self) -> ItemsView[str, Any]:
"""
This method configures the plugin
"""
raise NotImplementedError("needs to be implemented by the plugin")


class InventoryDataPlugin(Protocol):
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
This method configures the plugin
"""
raise NotImplementedError("needs to be implemented by the plugin")

def load(
self, data: Optional[Dict[str, Any]] = None
) -> Union[Dict[str, Any], InventoryData]:
"""
Returns the object containing the data
"""
raise NotImplementedError("needs to be implemented by the plugin")


InventoryDataPluginRegister: PluginRegister[Type[InventoryDataPlugin]] = PluginRegister(
INVENTORY_DATA_PLUGIN_PATH
)
16 changes: 14 additions & 2 deletions nornir/init_nornir.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any
import inspect
from typing import Any, Union

from nornir.core import Nornir
from nornir.core.configuration import Config
Expand All @@ -17,7 +18,18 @@ def load_inventory(
) -> Inventory:
InventoryPluginRegister.auto_register()
inventory_plugin = InventoryPluginRegister.get_plugin(config.inventory.plugin)
inv = inventory_plugin(**config.inventory.options).load()
inventory_plugin_params = config.inventory.options.copy()

init_params = inspect.signature(inventory_plugin).parameters
if "configuration" in init_params:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is most likely not working yet. The idea would be to detect if the Inventory plugin allows passing down the configuration settings to the Host, Group and Defaults Inventory objects. This would allow us to avoid breaking existing inventory plugins.

config_parameter = init_params["configuration"]
if config_parameter.annotation is not inspect.Parameter.empty and (
config_parameter.annotation == Config
or config_parameter.annotation == Union[Config, None]
):
inventory_plugin_params.update({"configuration": config})

inv = inventory_plugin(**inventory_plugin_params).load()

if config.inventory.transform_function:
TransformFunctionRegister.auto_register()
Expand Down
27 changes: 21 additions & 6 deletions nornir/plugins/inventory/simple.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
import pathlib
from typing import Any, Dict, Type
from typing import Any, Dict, Optional, Type

import ruamel.yaml

from nornir.core.configuration import Config
from nornir.core.inventory import (
ConnectionOptions,
Defaults,
Expand Down Expand Up @@ -33,7 +34,9 @@ def _get_connection_options(data: Dict[str, Any]) -> Dict[str, ConnectionOptions
return cp


def _get_defaults(data: Dict[str, Any]) -> Defaults:
def _get_defaults(
data: Dict[str, Any], configuration: Optional[Config] = None
) -> Defaults:
return Defaults(
hostname=data.get("hostname"),
port=data.get("port"),
Expand All @@ -42,11 +45,16 @@ def _get_defaults(data: Dict[str, Any]) -> Defaults:
platform=data.get("platform"),
data=data.get("data"),
connection_options=_get_connection_options(data.get("connection_options", {})),
configuration=configuration,
)


def _get_inventory_element(
typ: Type[HostOrGroup], data: Dict[str, Any], name: str, defaults: Defaults
typ: Type[HostOrGroup],
data: Dict[str, Any],
name: str,
defaults: Defaults,
configuration: Optional[Config] = None,
) -> HostOrGroup:
return typ(
name=name,
Expand All @@ -61,6 +69,7 @@ def _get_inventory_element(
), # this is a hack, we will convert it later to the correct type
defaults=defaults,
connection_options=_get_connection_options(data.get("connection_options", {})),
configuration=configuration,
)


Expand All @@ -71,6 +80,7 @@ def __init__(
group_file: str = "groups.yaml",
defaults_file: str = "defaults.yaml",
encoding: str = "utf-8",
configuration: Optional[Config] = None,
) -> None:
"""
SimpleInventory is an inventory plugin that loads data from YAML files.
Expand All @@ -90,14 +100,15 @@ def __init__(
self.group_file = pathlib.Path(group_file).expanduser()
self.defaults_file = pathlib.Path(defaults_file).expanduser()
self.encoding = encoding
self._config = configuration

def load(self) -> Inventory:
yml = ruamel.yaml.YAML(typ="safe")

if self.defaults_file.exists():
with open(self.defaults_file, "r", encoding=self.encoding) as f:
defaults_dict = yml.load(f) or {}
defaults = _get_defaults(defaults_dict)
defaults = _get_defaults(defaults_dict, configuration=self._config)
else:
defaults = Defaults()

Expand All @@ -106,15 +117,19 @@ def load(self) -> Inventory:
hosts_dict = yml.load(f)

for n, h in hosts_dict.items():
hosts[n] = _get_inventory_element(Host, h, n, defaults)
hosts[n] = _get_inventory_element(
Host, h, n, defaults, configuration=self._config
)

groups = Groups()
if self.group_file.exists():
with open(self.group_file, "r", encoding=self.encoding) as f:
groups_dict = yml.load(f) or {}

for n, g in groups_dict.items():
groups[n] = _get_inventory_element(Group, g, n, defaults)
groups[n] = _get_inventory_element(
Group, g, n, defaults, configuration=self._config
)

for g in groups.values():
g.groups = ParentGroups([groups[g] for g in g.groups])
Expand Down
3 changes: 3 additions & 0 deletions nornir/plugins/inventory_data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .dictionary import InventoryDataDict

__all__ = ("InventoryDataDict",)
Loading
Loading