Skip to content

Commit 7238fca

Browse files
committed
Merge branch '53-extract-and-refactor-duplicated-code' into 'master'
extract and refactor duplicated code Closes #53 See merge request ins/nettowel/nettowel-nuts!24
2 parents 2b647c0 + f003dfd commit 7238fca

28 files changed

Lines changed: 706 additions & 573 deletions

nuts/base_tests/napalm_bgp_neighbors.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from nornir_napalm.plugins.tasks import napalm_get
88

99
from nuts.context import NornirNutsContext
10-
from nuts.helpers.result import nuts_result_wrapper, NutsResult
10+
from nuts.helpers.filters import filter_hosts
11+
from nuts.helpers.result import map_host_to_nutsresult, NutsResult
1112

1213

1314
class BgpNeighborsContext(NornirNutsContext):
@@ -18,13 +19,10 @@ def nuts_arguments(self) -> Dict[str, List]:
1819
return {"getters": ["bgp_neighbors"]}
1920

2021
def nornir_filter(self) -> F:
21-
hosts = {entry["host"] for entry in self.nuts_parameters["test_data"]}
22-
return F(name__any=hosts)
22+
return filter_hosts(self.nuts_parameters["test_data"])
2323

2424
def transform_result(self, general_result: AggregatedResult) -> Dict[str, NutsResult]:
25-
return {
26-
host: nuts_result_wrapper(result, self._transform_host_results) for host, result in general_result.items()
27-
}
25+
return map_host_to_nutsresult(general_result, self._transform_host_results)
2826

2927
def _transform_host_results(self, single_result: MultiResult) -> dict:
3028
assert single_result[0].result is not None

nuts/base_tests/napalm_get_users.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
from nornir.core.task import MultiResult, AggregatedResult
77
from nornir_napalm.plugins.tasks import napalm_get
88

9+
910
from nuts.context import NornirNutsContext
10-
from nuts.helpers.result import nuts_result_wrapper, NutsResult
11+
from nuts.helpers.filters import filter_hosts
12+
from nuts.helpers.result import map_host_to_nutsresult, NutsResult
1113

1214

1315
class UsersContext(NornirNutsContext):
@@ -18,13 +20,10 @@ def nuts_arguments(self) -> Dict[str, List[str]]:
1820
return {"getters": ["users"]}
1921

2022
def nornir_filter(self) -> F:
21-
hosts = {entry["host"] for entry in self.nuts_parameters["test_data"]}
22-
return F(name__any=hosts)
23+
return filter_hosts(self.nuts_parameters["test_data"])
2324

2425
def transform_result(self, general_result: AggregatedResult) -> Dict[str, NutsResult]:
25-
return {
26-
host: nuts_result_wrapper(result, self._transform_host_results) for host, result in general_result.items()
27-
}
26+
return map_host_to_nutsresult(general_result, self._transform_host_results)
2827

2928
def _transform_host_results(self, single_result: MultiResult) -> Dict[str, Dict]:
3029
assert single_result[0].result is not None

nuts/base_tests/napalm_interfaces.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
from nornir.core.task import MultiResult, AggregatedResult
77
from nornir_napalm.plugins.tasks import napalm_get
88

9+
910
from nuts.context import NornirNutsContext
10-
from nuts.helpers.converters import InterfaceNameConverter
11-
from nuts.helpers.result import nuts_result_wrapper, NutsResult
11+
from nuts.helpers.filters import filter_hosts
12+
from nuts.helpers.result import NutsResult, map_host_to_nutsresult
1213

1314

1415
class InterfacesContext(NornirNutsContext):
@@ -19,13 +20,10 @@ def nuts_arguments(self) -> Dict[str, List[str]]:
1920
return {"getters": ["interfaces"]}
2021

2122
def nornir_filter(self) -> F:
22-
hosts = {entry["host"] for entry in self.nuts_parameters["test_data"]}
23-
return F(name__any=hosts)
23+
return filter_hosts(self.nuts_parameters["test_data"])
2424

2525
def transform_result(self, general_result: AggregatedResult) -> Dict[str, NutsResult]:
26-
return {
27-
host: nuts_result_wrapper(result, self._transform_host_results) for host, result in general_result.items()
28-
}
26+
return map_host_to_nutsresult(general_result, self._transform_host_results)
2927

3028
def _transform_host_results(self, single_result: MultiResult) -> dict:
3129
assert single_result[0].result is not None

nuts/base_tests/napalm_lldp_neighbors.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
from nuts.context import NornirNutsContext
1010
from nuts.helpers.converters import InterfaceNameConverter
11-
from nuts.helpers.result import nuts_result_wrapper, NutsResult
11+
from nuts.helpers.filters import filter_hosts
12+
from nuts.helpers.result import NutsResult, map_host_to_nutsresult
1213

1314

1415
class LldpNeighborsContext(NornirNutsContext):
@@ -19,13 +20,10 @@ def nuts_arguments(self) -> Dict[str, List]:
1920
return {"getters": ["lldp_neighbors_detail"]}
2021

2122
def nornir_filter(self) -> F:
22-
hosts = {entry["host"] for entry in self.nuts_parameters["test_data"]}
23-
return F(name__any=hosts)
23+
return filter_hosts(self.nuts_parameters["test_data"])
2424

2525
def transform_result(self, general_result: AggregatedResult) -> Dict[str, NutsResult]:
26-
return {
27-
host: nuts_result_wrapper(result, self._transform_host_results) for host, result in general_result.items()
28-
}
26+
return map_host_to_nutsresult(general_result, self._transform_host_results)
2927

3028
def _transform_host_results(self, single_result: MultiResult) -> dict:
3129
assert single_result[0].result is not None

nuts/base_tests/napalm_network_instances.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from nornir.core.task import MultiResult, AggregatedResult
77
from nornir_napalm.plugins.tasks import napalm_get
88

9-
from nuts.helpers.result import nuts_result_wrapper, NutsResult
9+
from nuts.helpers.filters import filter_hosts
10+
from nuts.helpers.result import nuts_result_wrapper, NutsResult, map_host_to_nutsresult
1011
from nuts.context import NornirNutsContext
1112

1213

@@ -18,15 +19,12 @@ def nuts_arguments(self) -> Dict[str, List[str]]:
1819
return {"getters": ["network_instances"]}
1920

2021
def nornir_filter(self) -> F:
21-
hosts = {entry["host"] for entry in self.nuts_parameters["test_data"]}
22-
return F(name__any=hosts)
22+
return filter_hosts(self.nuts_parameters["test_data"])
2323

2424
def transform_result(self, general_result: AggregatedResult) -> Dict[str, NutsResult]:
25-
return {
26-
host: nuts_result_wrapper(result, self._transform_single_result) for host, result in general_result.items()
27-
}
25+
return map_host_to_nutsresult(general_result, self._transform_host_results)
2826

29-
def _transform_single_result(self, single_result: MultiResult) -> dict:
27+
def _transform_host_results(self, single_result: MultiResult) -> dict:
3028
assert single_result[0].result is not None
3129
task_result = single_result[0].result
3230
network_instances = task_result["network_instances"]

nuts/base_tests/napalm_ping.py

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,80 @@
11
"""Let a device ping another device."""
22
from enum import Enum
3-
from typing import Dict, List, Callable
3+
from typing import Dict, Callable, Any, List
44

55
import pytest
66
from nornir.core import Task
77
from nornir.core.filter import F
8-
from nornir.core.task import Result, MultiResult, AggregatedResult
8+
from nornir.core.task import Result, AggregatedResult
99
from nornir_napalm.plugins.tasks import napalm_ping
1010

1111
from nuts.context import NornirNutsContext
12-
from nuts.helpers.result import nuts_result_wrapper, NutsResult
12+
from nuts.helpers.filters import filter_hosts
13+
from nuts.helpers.result import (
14+
NutsResult,
15+
map_host_to_dest_to_nutsresult,
16+
)
17+
18+
19+
class Ping(Enum):
20+
FAIL = 0
21+
SUCCESS = 1
22+
FLAPPING = 2
1323

1424

1525
class PingContext(NornirNutsContext):
1626
def nuts_task(self) -> Callable:
17-
return napalm_ping_multi_host
18-
19-
def nuts_arguments(self) -> dict:
20-
args = super().nuts_arguments()
21-
args["destinations_per_host"] = _destinations_per_host(self.nuts_parameters["test_data"])
22-
return args
27+
return self.napalm_ping_multi_dests
2328

2429
def nornir_filter(self) -> F:
25-
hosts = {entry["host"] for entry in self.nuts_parameters["test_data"]}
26-
return F(name__any=hosts)
30+
return filter_hosts(self.nuts_parameters["test_data"])
2731

2832
def transform_result(self, general_result: AggregatedResult) -> Dict[str, Dict[str, NutsResult]]:
29-
test_data = self.nuts_parameters["test_data"]
30-
return {
31-
host: _parse_ping_results(host, task_results, test_data) for host, task_results in general_result.items()
32-
}
33+
return map_host_to_dest_to_nutsresult(general_result, self._transform_single_entry)
34+
35+
def _transform_single_entry(self, single_result: Result) -> Ping:
36+
assert single_result.host is not None
37+
assert single_result.result is not None
38+
max_drop = self._allowed_max_drop_for_destination(single_result.host.name, single_result.destination) # type: ignore[attr-defined] # see below
39+
return _map_result_to_enum(single_result.result, max_drop)
40+
41+
def _allowed_max_drop_for_destination(self, host: str, dest: str) -> int:
42+
"""
43+
Matches the host-destination pair from a nornir task to the
44+
host-destination pair from test_data and retrieves its
45+
max_drop value that has been defined for that pair.
46+
47+
:param host: host entry from the nornir task
48+
:param dest: destination that was pinged by a host from the nornir task
49+
:return: max_drop value from test_data
50+
"""
51+
test_data: List[Dict[str, Any]] = self.nuts_parameters["test_data"]
52+
for entry in test_data:
53+
if entry["host"] == host and entry["destination"] == dest:
54+
return entry["max_drop"]
55+
return 0
56+
57+
def napalm_ping_multi_dests(self, task: Task, **kwargs) -> Result:
58+
"""
59+
One host pings all destinations as defined in the test bundle.
60+
61+
Note: The destination is not included in the nornir result if the ping fails.
62+
Therefore we cannot know which destination was not reachable,
63+
so we must patch the destination onto the result object to know later which
64+
host-destination pair actually failed.
65+
66+
:param task: nornir task for ping
67+
:param kwargs: arguments from the test bundle for the napalm ping task, such as
68+
count, ttl, timeout
69+
:return: all pinged destinations per host
70+
"""
71+
destinations_per_hosts = [
72+
entry["destination"] for entry in self.nuts_parameters["test_data"] if entry["host"] == task.host.name
73+
]
74+
for destination in destinations_per_hosts:
75+
result = task.run(task=napalm_ping, dest=destination, **kwargs)
76+
result[0].destination = destination # type: ignore[attr-defined]
77+
return Result(host=task.host, result="All pings executed")
3378

3479

3580
CONTEXT = PingContext
@@ -49,41 +94,19 @@ def test_ping(self, single_result, expected):
4994
assert single_result.result.name == expected
5095

5196

52-
class Ping(Enum):
53-
FAIL = 0
54-
SUCCESS = 1
55-
FLAPPING = 2
56-
57-
58-
def napalm_ping_multi_host(task: Task, destinations_per_host, **kwargs) -> Result:
59-
destinations = destinations_per_host(task.host.name)
60-
for destination in destinations:
61-
result = task.run(task=napalm_ping, dest=destination, **kwargs)
62-
result[0].destination = destination # type: ignore[attr-defined]
63-
return Result(host=task.host, result="All pings executed")
64-
65-
66-
def _destinations_per_host(test_data):
67-
return lambda host_name: [entry["destination"] for entry in test_data if entry["host"] == host_name]
68-
69-
70-
def _parse_ping_results(host: str, task_results: MultiResult, test_data: List[dict]) -> dict:
71-
maxdrop_per_destination = {
72-
entry["destination"]: entry.get("max_drop", 0) for entry in test_data if entry["host"] == host
73-
}
74-
return {
75-
ping_task.destination: nuts_result_wrapper( # type: ignore[attr-defined]
76-
ping_task, _get_transform_single_entry(maxdrop_per_destination[ping_task.destination]) # type: ignore[attr-defined]
77-
)
78-
for ping_task in task_results[1:]
79-
}
80-
81-
82-
def _get_transform_single_entry(max_drop):
83-
return lambda ping_task: _map_result_to_enum(ping_task.result, max_drop)
97+
def _map_result_to_enum(result: Dict[str, Dict[Any, Any]], max_drop: int) -> Ping:
98+
"""
99+
Evaluates the ping that has been conducted with nornir and matches it
100+
to a Ping-Enum which can be either FAIL, SUCCESS or FLAPPING.
84101
102+
FAIL: Packet loss equals probes sent.
103+
SUCCESS: Packet loss is below or equal max_drop.
104+
FLAPPING: Everything else.
85105
86-
def _map_result_to_enum(result: dict, max_drop: int) -> Ping:
106+
:param result: a single nornir Result
107+
:param max_drop: max_drop threshold
108+
:return: evaluated ping result
109+
"""
87110
if result["success"]["packet_loss"] == result["success"]["probes_sent"]:
88111
return Ping.FAIL
89112
if result["success"]["packet_loss"] <= max_drop:

nuts/base_tests/netmiko_cdp_neighbors.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from nornir.core.task import MultiResult, AggregatedResult
77
from nornir_netmiko import netmiko_send_command
88

9-
from nuts.helpers.result import nuts_result_wrapper, NutsResult
9+
from nuts.helpers.filters import filter_hosts
10+
from nuts.helpers.result import NutsResult, map_host_to_nutsresult
1011
from nuts.context import NornirNutsContext
1112

1213

@@ -18,17 +19,14 @@ def nuts_arguments(self) -> dict:
1819
return {"command_string": "show cdp neighbors detail", "use_textfsm": True}
1920

2021
def nornir_filter(self) -> F:
21-
hosts = {entry["host"] for entry in self.nuts_parameters["test_data"]}
22-
return F(name__any=hosts)
22+
return filter_hosts(self.nuts_parameters["test_data"])
2323

2424
def _transform_host_results(self, host_results: MultiResult) -> dict:
2525
assert host_results[0].result is not None
2626
return {neighbor["destination_host"]: neighbor for neighbor in host_results[0].result}
2727

2828
def transform_result(self, general_result: AggregatedResult) -> Dict[str, NutsResult]:
29-
return {
30-
host: nuts_result_wrapper(result, self._transform_host_results) for host, result in general_result.items()
31-
}
29+
return map_host_to_nutsresult(general_result, self._transform_host_results)
3230

3331

3432
CONTEXT = CdpNeighborsContext

0 commit comments

Comments
 (0)