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

improvements to data resolution #623

Merged
merged 6 commits into from
Jan 11, 2021
Merged
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
2 changes: 1 addition & 1 deletion docs/tutorial/inventory.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@
{
"data": {
"text/plain": [
"'acme.local'"
"'global.local'"
]
},
"execution_count": 13,
Expand Down
76 changes: 59 additions & 17 deletions nornir/core/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ def schema(cls) -> Dict[str, Any]:

def dict(self) -> Dict[str, Any]:
return {
"hostname": self.hostname,
"port": self.port,
"username": self.username,
"password": self.password,
"platform": self.platform,
"hostname": object.__getattribute__(self, "hostname"),
"port": object.__getattribute__(self, "port"),
"username": object.__getattribute__(self, "username"),
"password": object.__getattribute__(self, "password"),
"platform": object.__getattribute__(self, "platform"),
}


Expand Down Expand Up @@ -162,6 +162,45 @@ def dict(self) -> Dict[str, Any]:
**super().dict(),
}

def extended_groups(self) -> List["Group"]:
"""
Returns the groups this host belongs to by virtue of inheritance.

This list is ordered based on the inheritance rules and groups are not
duplicated. For instance, given a host with the following groups:

hostA:
groups:
- group_a
- group_b

group_a:
groups:
- group_1
- group_2
group_b:
groups:
- group_2
- group_3

group_1:
groups:
- group_X

this will return [group_a, group_1, group_X, group_2, group_b, group_3]
"""
groups: List["Group"] = []

for g in self.groups:
if g not in groups:
groups.append(g)

for sg in g.extended_groups():
if sg not in groups:
groups.append(sg)

return groups


class Defaults(BaseAttributes):
__slots__ = ("data", "connection_options")
Expand Down Expand Up @@ -234,14 +273,17 @@ def __init__(
connection_options=connection_options,
)

def _resolve_data(self) -> Dict[str, Any]:
def extended_data(self) -> Dict[str, Any]:
"""
Returns the data associated with the object including inherited data
"""
processed = []
result = {}
for k, v in self.data.items():
processed.append(k)
result[k] = v
for g in self.groups:
for k, v in g.items():
for g in self.extended_groups():
for k, v in g.data.items():
if k not in processed:
processed.append(k)
result[k] = v
Expand Down Expand Up @@ -270,18 +312,18 @@ def dict(self) -> Dict[str, Any]:

def keys(self) -> KeysView[str]:
"""Returns the keys of the attribute ``data`` and of the parent(s) groups."""
return self._resolve_data().keys()
return self.extended_data().keys()

def values(self) -> ValuesView[Any]:
"""Returns the values of the attribute ``data`` and of the parent(s) groups."""
return self._resolve_data().values()
return self.extended_data().values()

def items(self) -> ItemsView[str, Any]:
"""
Returns all the data accessible from a device, including
the one inherited from parent groups
"""
return self._resolve_data().items()
return self.extended_data().items()

def has_parent_group(self, group: Union[str, "Group"]) -> bool:
"""Returns whether the object is a child of the :obj:`Group` ``group``"""
Expand All @@ -308,9 +350,9 @@ def __getitem__(self, item: str) -> Any:
return self.data[item]

except KeyError:
for g in self.groups:
for g in self.extended_groups():
try:
r = g[item]
r = g.data[item]
return r
except KeyError:
continue
Expand All @@ -326,8 +368,8 @@ def __getattribute__(self, name: str) -> Any:
return object.__getattribute__(self, name)
v = object.__getattribute__(self, name)
if v is None:
for g in self.groups:
r = getattr(g, name)
for g in self.extended_groups():
r = object.__getattribute__(g, name)
if r is not None:
return r

Expand All @@ -342,10 +384,10 @@ def __setitem__(self, item: str, value: Any) -> None:
self.data[item] = value

def __len__(self) -> int:
return len(self._resolve_data().keys())
return len(self.extended_data().keys())

def __iter__(self) -> Iterator[str]:
return self.data.__iter__()
return self.extended_data().__iter__()

def __str__(self) -> str:
return self.name
Expand Down
30 changes: 26 additions & 4 deletions tests/core/test_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ def test_combined(self, nornir):
f = F(site="site2") | (F(role="www") & F(my_var="comes_from_dev1.group_1"))
filtered = sorted(list((nornir.inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1", "dev3.group_2", "dev4.group_2"]
assert filtered == [
"dev1.group_1",
"dev3.group_2",
"dev4.group_2",
"dev6.group_3",
]

f = (F(site="site2") | F(role="www")) & F(my_var="comes_from_dev1.group_1")
filtered = sorted(list((nornir.inventory.filter(f).hosts.keys())))
Expand All @@ -41,7 +46,12 @@ def test_negate(self, nornir):
f = ~F(groups__contains="group_1")
filtered = sorted(list((nornir.inventory.filter(f).hosts.keys())))

assert filtered == ["dev3.group_2", "dev4.group_2", "dev5.no_group"]
assert filtered == [
"dev3.group_2",
"dev4.group_2",
"dev5.no_group",
"dev6.group_3",
]

def test_negate_and_second_negate(self, nornir):
f = F(site="site1") & ~F(role="www")
Expand All @@ -58,6 +68,7 @@ def test_negate_or_both_negate(self, nornir):
"dev3.group_2",
"dev4.group_2",
"dev5.no_group",
"dev6.group_3",
]

def test_nested_data_a_string(self, nornir):
Expand Down Expand Up @@ -93,6 +104,7 @@ def test_nested_data_a_dict_doesnt_contain(self, nornir):
"dev3.group_2",
"dev4.group_2",
"dev5.no_group",
"dev6.group_3",
]

def test_nested_data_a_list_contains(self, nornir):
Expand All @@ -105,7 +117,12 @@ def test_filtering_by_callable_has_parent_group(self, nornir):
f = F(has_parent_group="parent_group")
filtered = sorted(list((nornir.inventory.filter(f).hosts.keys())))

assert filtered == ["dev1.group_1", "dev2.group_1", "dev4.group_2"]
assert filtered == [
"dev1.group_1",
"dev2.group_1",
"dev4.group_2",
"dev6.group_3",
]

def test_filtering_by_attribute_name(self, nornir):
f = F(name="dev1.group_1")
Expand All @@ -117,7 +134,12 @@ def test_filtering_string_in_list(self, nornir):
f = F(platform__in=["linux", "mock"])
filtered = sorted(list((nornir.inventory.filter(f).hosts.keys())))

assert filtered == ["dev3.group_2", "dev4.group_2", "dev5.no_group"]
assert filtered == [
"dev3.group_2",
"dev4.group_2",
"dev5.no_group",
"dev6.group_3",
]

def test_filtering_list_any(self, nornir):
f = F(nested_data__a_list__any=[1, 3])
Expand Down
Loading