Skip to content

Commit 17e3839

Browse files
Merge pull request #106 from network-unit-testing-system/105-poor-performance-on-big-test-portfolio-using-remote-nornir-inventory
add option to not cache the inventory
2 parents 4190a5f + 7e96494 commit 17e3839

7 files changed

Lines changed: 408 additions & 0 deletions

File tree

docs/source/advanced/cli.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Advanced Options
2+
================
3+
4+
Nuts can be tweaked if the default options are not sufficient.
5+
6+
Custom Nornir Inventory
7+
-----------------------
8+
9+
If you already have a nornir configuration and inventory for your network you can reuse it by passing the parameter ``--nornir-config`` to the pytest command:
10+
11+
.. code:: shell
12+
13+
$ pytest tests/test-definition-ping.yaml --nornir-config path/to/nr-config.yaml
14+
15+
16+
It will default back to the local directory.
17+
18+
19+
Disable caching of the Nornir Inventory
20+
---------------------------------------
21+
22+
For performance reasons the Nornir Inventory will be internally cached. If you wish to disable this behavior pass the parameter ``--nornir-cache-disable`` to the pytest command:
23+
24+
.. code:: shell
25+
26+
$ pytest tests/test-definition-ping.yaml --nornir-cache-disable
27+
28+

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Contents
5555
Test Bundles <testbundles/alltestbundles>
5656
Writing Custom Tests <dev/writetests>
5757
Test Reports <reports/reports>
58+
Advanced Options <advanced/cli>
5859

5960

6061
Indices and tables

nuts/context.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
from nornir.core import Nornir
99
from nornir.core.task import AggregatedResult, Result
1010
from nornir.core.filter import F
11+
from nornir.core.plugins.inventory import InventoryPluginRegister
1112

1213
from nuts.helpers.errors import NutsSetupError
1314
from nuts.helpers.result import AbstractResultExtractor
1415
from nuts.helpers.filters import filter_hosts, get_filter_object
16+
from nuts.helpers.cache import serialize_inventory, CacheInventory
1517

1618

1719
class NutsContext:
@@ -105,17 +107,44 @@ def __init__(
105107
self.nornir: Optional[Nornir] = None
106108

107109
def initialize(self) -> None:
110+
"""
111+
Checks if inventory should be cached, then use global inventory otherwise
112+
regenerate it continuously.
113+
"""
108114
if self.pytestconfig:
109115
config_file = pathlib.Path(
110116
self.pytestconfig.getoption("nornir_configuration")
111117
)
112118
else:
113119
config_file = pathlib.Path(self.DEFAULT_NORNIR_CONFIG_FILE)
114120

121+
if self.pytestconfig and self.pytestconfig.cache:
122+
if nornir_inventory := self.pytestconfig.cache.get(
123+
"nuts/NORNIR_CACHE", None
124+
):
125+
InventoryPluginRegister.register("NutsCacheInventory", CacheInventory)
126+
127+
self.nornir = InitNornir(
128+
config_file=str(config_file),
129+
logging={"enabled": False},
130+
inventory={
131+
"plugin": "NutsCacheInventory",
132+
"options": nornir_inventory,
133+
},
134+
)
135+
return
136+
115137
self.nornir = InitNornir(
116138
config_file=str(config_file),
117139
logging={"enabled": False},
118140
)
141+
if self.pytestconfig and not self.pytestconfig.getoption(
142+
"nornir_cache_disabled"
143+
):
144+
# pytest cash needs json encodable values
145+
inventory = serialize_inventory(self.nornir.inventory)
146+
if self.pytestconfig and self.pytestconfig.cache:
147+
self.pytestconfig.cache.set("nuts/NORNIR_CACHE", inventory)
119148

120149
def nuts_task(self) -> Callable[..., Result]:
121150
"""

nuts/helpers/cache.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Any, Dict
2+
3+
4+
from nornir.core.inventory import Group, Groups, Host, Hosts, Inventory, ParentGroups
5+
from nornir.plugins.inventory.simple import _get_defaults, _get_inventory_element
6+
7+
8+
def serialize_inventory(inventory: Inventory) -> Dict[str, Dict[str, Any]]:
9+
data = {
10+
"hosts": {host: data.dict() for host, data in inventory.hosts.items()},
11+
"groups": {group: data.dict() for group, data in inventory.groups.items()},
12+
"defaults": inventory.defaults.dict(),
13+
}
14+
return data
15+
16+
17+
class CacheInventory:
18+
def __init__(
19+
self,
20+
hosts: Dict[str, Dict[str, Any]],
21+
groups: Dict[str, Dict[str, Any]],
22+
defaults: Dict[str, Any],
23+
) -> None:
24+
"""
25+
CacheInventory inspired by the SimpleInventory.
26+
27+
Args:
28+
29+
hosts: Dict with host name and host.dict() data
30+
groups: Dict with group name and group.dict() data
31+
default: defaults.dict() dict data
32+
"""
33+
34+
self.hosts_dict = hosts
35+
self.groups_dict = groups
36+
self.defaults_dict = defaults
37+
38+
def load(self) -> Inventory:
39+
40+
defaults = _get_defaults(self.defaults_dict)
41+
42+
hosts = Hosts()
43+
44+
for n, h in self.hosts_dict.items():
45+
hosts[n] = _get_inventory_element(Host, h, n, defaults)
46+
47+
groups = Groups()
48+
49+
for n, g in self.groups_dict.items():
50+
groups[n] = _get_inventory_element(Group, g, n, defaults)
51+
52+
for group in groups.values():
53+
group.groups = ParentGroups([groups[str(g)] for g in group.groups])
54+
55+
for host in hosts.values():
56+
host.groups = ParentGroups([groups[str(g)] for g in host.groups])
57+
58+
return Inventory(hosts=hosts, groups=groups, defaults=defaults)

nuts/plugin.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,29 @@ def pytest_addoption(parser: Parser) -> None:
103103
metavar="NORNIR_CONFIG",
104104
help="nuts nornir configuration file. Default is nr-config.yaml",
105105
)
106+
107+
group.addoption(
108+
"--nornir-cache-disable",
109+
action="store_true",
110+
dest="nornir_cache_disabled",
111+
default=False,
112+
help="disable caching of nornir inventory between executions",
113+
)
114+
115+
group.addoption(
116+
"--nornir-cached-inventory",
117+
action="store_true",
118+
dest="nornir_cached_inventory",
119+
default=False,
120+
help="Uses the chached inventory from the last executions if possible",
121+
)
122+
123+
124+
def pytest_sessionstart(session: Session) -> None:
125+
"""Called after the ``Session`` object has been created
126+
and before performing collection and entering the run test loop.
127+
128+
:param pytest.Session session: The pytest session object.
129+
"""
130+
if not session.config.getoption("nornir_cached_inventory") and session.config.cache:
131+
session.config.cache.set("nuts/NORNIR_CACHE", None)

tests/helpers/test_cache.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
from nornir.core.inventory import Inventory, Hosts, Groups, Defaults, Host, Group
2+
from typing import Any, Dict
3+
4+
from nuts.helpers.cache import CacheInventory, serialize_inventory
5+
6+
7+
def test_serialize_inventory():
8+
hosts = Hosts(
9+
{
10+
"host1": Host(
11+
name="host1", hostname="host1.example.com", data={"key": "value"}
12+
)
13+
}
14+
)
15+
groups = Groups({"group1": Group(name="group1", data={"key": "value"})})
16+
defaults = Defaults(hostname="hostname", data={"key": "value"})
17+
inventory = Inventory(hosts=hosts, groups=groups, defaults=defaults)
18+
19+
serialized = serialize_inventory(inventory)
20+
21+
expected = {
22+
"hosts": {
23+
"host1": {
24+
"name": "host1",
25+
"hostname": "host1.example.com",
26+
"port": None,
27+
"username": None,
28+
"password": None,
29+
"platform": None,
30+
"data": {"key": "value"},
31+
"connection_options": {},
32+
"groups": [],
33+
}
34+
},
35+
"groups": {
36+
"group1": {
37+
"connection_options": {},
38+
"data": {"key": "value"},
39+
"hostname": None,
40+
"groups": [],
41+
"name": "group1",
42+
"password": None,
43+
"platform": None,
44+
"port": None,
45+
"username": None,
46+
}
47+
},
48+
"defaults": {
49+
"connection_options": {},
50+
"data": {"key": "value"},
51+
"hostname": "hostname",
52+
"password": None,
53+
"platform": None,
54+
"port": None,
55+
"username": None,
56+
},
57+
}
58+
59+
assert serialized == expected
60+
61+
62+
def test_fail_serialize_inventory():
63+
hosts = Hosts(
64+
{
65+
"host1": Host(
66+
name="host1", hostname="host1.example.com", data={"key": "value"}
67+
)
68+
}
69+
)
70+
groups = Groups({"group1": Group(name="group1", data={"key": "value"})})
71+
defaults = Defaults(hostname="hostname", data={"key": "value"})
72+
inventory = Inventory(hosts=hosts, groups=groups, defaults=defaults)
73+
74+
serialized = serialize_inventory(inventory)
75+
76+
expected = {
77+
"hosts": {
78+
"host12": {
79+
"name": "host1",
80+
"hostname": "host1.example.com",
81+
"port": None,
82+
"username": None,
83+
"password": None,
84+
"platform": None,
85+
"data": {"key": "value"},
86+
"connection_options": {},
87+
"groups": [],
88+
}
89+
},
90+
"groups": {
91+
"group12": {
92+
"connection_options": {},
93+
"data": {"key": "value"},
94+
"hostname": None,
95+
"groups": [],
96+
"name": "group1",
97+
"password": None,
98+
"platform": None,
99+
"port": None,
100+
"username": None,
101+
}
102+
},
103+
"defaults": {
104+
"connection_options": {},
105+
"data": {"key": "value"},
106+
"hostname": "hostname",
107+
"password": None,
108+
"platform": None,
109+
"port": None,
110+
"username": "User",
111+
},
112+
}
113+
114+
assert serialized != expected
115+
116+
117+
def test_cache_inventory_load():
118+
hosts_dict = {
119+
"host1": {
120+
"name": "host1",
121+
"hostname": "host1.example.com",
122+
"data": {"key": "value"},
123+
"groups": [],
124+
"defaults": {"key": "value"},
125+
}
126+
}
127+
groups_dict = {
128+
"group1": {
129+
"name": "group1",
130+
"data": {"key": "value"},
131+
"groups": [],
132+
"defaults": {"key": "value"},
133+
}
134+
}
135+
defaults_dict = {"hostname": "hostname", "data": {"key": "value"}}
136+
137+
cache_inventory = CacheInventory(
138+
hosts=hosts_dict, groups=groups_dict, defaults=defaults_dict
139+
)
140+
141+
inventory = cache_inventory.load()
142+
143+
assert inventory.hosts["host1"].hostname == "host1.example.com"
144+
assert inventory.hosts["host1"].data == {"key": "value"}
145+
assert inventory.groups["group1"].data == {"key": "value"}
146+
assert inventory.defaults.hostname == "hostname"
147+
assert inventory.defaults.hostname != "not the right hostname"
148+
assert inventory.defaults.data == {"key": "value"}
149+
150+
151+
def test_empty_cache_inventory_load():
152+
hosts_dict: Dict[str, Dict[str, Any]] = {}
153+
groups_dict: Dict[str, Dict[str, Any]] = {}
154+
defaults_dict: Dict[str, Any] = {}
155+
156+
cache_inventory = CacheInventory(
157+
hosts=hosts_dict, groups=groups_dict, defaults=defaults_dict
158+
)
159+
160+
inventory = cache_inventory.load()
161+
162+
assert inventory is not None
163+
assert inventory.hosts is not None
164+
assert inventory.groups is not None
165+
assert inventory.defaults is not None

0 commit comments

Comments
 (0)