11"""Let a device ping another device."""
22from enum import Enum
3- from typing import Dict , List , Callable
3+ from typing import Dict , Callable , Any , List
44
55import pytest
66from nornir .core import Task
77from nornir .core .filter import F
8- from nornir .core .task import Result , MultiResult , AggregatedResult
8+ from nornir .core .task import Result , AggregatedResult
99from nornir_napalm .plugins .tasks import napalm_ping
1010
1111from 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
1525class 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
3580CONTEXT = PingContext
@@ -38,52 +83,29 @@ def transform_result(self, general_result: AggregatedResult) -> Dict[str, Dict[s
3883@pytest .mark .usefixtures ("check_nuts_result" )
3984class TestNapalmPing :
4085 @pytest .fixture
41- def single_result (self , nornir_nuts_ctx : NornirNutsContext , host : str , destination : str ) -> NutsResult :
42- transformed_result = nornir_nuts_ctx .transformed_result ()
43- assert host in transformed_result , f"Host { host } not found in aggregated result."
44- assert destination in transformed_result [host ], f"Destination { destination } not found in result."
45- return transformed_result [host ][destination ]
86+ def single_result (self , nuts_ctx : NornirNutsContext , host : str , destination : str ) -> NutsResult :
87+ assert host in nuts_ctx .transformed_result , f"Host { host } not found in aggregated result."
88+ assert destination in nuts_ctx .transformed_result [host ], f"Destination { destination } not found in result."
89+ return nuts_ctx .transformed_result [host ][destination ]
4690
4791 @pytest .mark .nuts ("host,destination,expected" )
4892 def test_ping (self , single_result , expected ):
4993 assert single_result .result .name == expected
5094
5195
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 )
96+ def _map_result_to_enum (result : Dict [str , Dict [Any , Any ]], max_drop : int ) -> Ping :
97+ """
98+ Evaluates the ping that has been conducted with nornir and matches it
99+ to a Ping-Enum which can be either FAIL, SUCCESS or FLAPPING.
84100
101+ FAIL: Packet loss equals probes sent.
102+ SUCCESS: Packet loss is below or equal max_drop.
103+ FLAPPING: Everything else.
85104
86- def _map_result_to_enum (result : dict , max_drop : int ) -> Ping :
105+ :param result: a single nornir Result
106+ :param max_drop: max_drop threshold
107+ :return: evaluated ping result
108+ """
87109 if result ["success" ]["packet_loss" ] == result ["success" ]["probes_sent" ]:
88110 return Ping .FAIL
89111 if result ["success" ]["packet_loss" ] <= max_drop :
0 commit comments