NodePath: Make path computation deterministic
[csit.git] / resources / libraries / python / NodePath.py
1 # Copyright (c) 2020 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Path utilities library for nodes in the topology."""
15
16 from resources.libraries.python.topology import Topology
17
18
19 class NodePath:
20     """Path utilities for nodes in the topology.
21
22     :Example:
23
24     node1--link1-->node2--link2-->node3--link3-->node2--link4-->node1
25     RobotFramework:
26     | Library | resources/libraries/python/NodePath.py
27
28     | Path test
29     | | [Arguments] | ${node1} | ${node2} | ${node3}
30     | | Append Node | ${nodes1}
31     | | Append Node | ${nodes2}
32     | | Append Nodes | ${nodes3} | ${nodes2}
33     | | Append Node | ${nodes1}
34     | | Compute Path | ${FALSE}
35     | | ${first_int} | ${node}= | First Interface
36     | | ${last_int} | ${node}= | Last Interface
37     | | ${first_ingress} | ${node}= | First Ingress Interface
38     | | ${last_egress} | ${node}= | Last Egress Interface
39     | | ${next} | ${node}= | Next Interface
40
41     Python:
42     >>> from NodePath import NodePath
43     >>> path = NodePath()
44     >>> path.append_node(node1)
45     >>> path.append_node(node2)
46     >>> path.append_nodes(node3, node2)
47     >>> path.append_node(node1)
48     >>> path.compute_path()
49     >>> (interface, node) = path.first_interface()
50     >>> (interface, node) = path.last_interface()
51     >>> (interface, node) = path.first_ingress_interface()
52     >>> (interface, node) = path.last_egress_interface()
53     >>> (interface, node) = path.next_interface()
54     """
55
56     def __init__(self):
57         self._nodes = []
58         self._nodes_filter = []
59         self._links = []
60         self._path = []
61         self._path_iter = []
62
63     def append_node(self, node, filter_list=None):
64         """Append node to the path.
65
66         :param node: Node to append to the path.
67         :param filter_list: Filter criteria list.
68         :type node: dict
69         :type filter_list: list of strings
70         """
71         self._nodes_filter.append(filter_list)
72         self._nodes.append(node)
73
74     def append_nodes(self, *nodes):
75         """Append nodes to the path.
76
77         :param nodes: Nodes to append to the path.
78         :type nodes: dict
79
80         .. note:: Node order does matter.
81         """
82         for node in nodes:
83             self.append_node(node)
84
85     def clear_path(self):
86         """Clear path."""
87         self._nodes = []
88         self._nodes_filter = []
89         self._links = []
90         self._path = []
91         self._path_iter = []
92
93     def compute_path(self, always_same_link=True):
94         """Compute path for added nodes.
95
96         .. note:: First add at least two nodes to the topology.
97
98         :param always_same_link: If True use always same link between two nodes
99             in path. If False use different link (if available)
100             between two nodes if one link was used before.
101         :type always_same_link: bool
102         :raises RuntimeError: If not enough nodes for path.
103         """
104         nodes = self._nodes
105         if len(nodes) < 2:
106             raise RuntimeError(u"Not enough nodes to compute path")
107
108         for idx in range(0, len(nodes) - 1):
109             topo = Topology()
110             node1 = nodes[idx]
111             node2 = nodes[idx + 1]
112             n1_list = self._nodes_filter[idx]
113             n2_list = self._nodes_filter[idx + 1]
114             links = topo.get_active_connecting_links(
115                 node1, node2, filter_list_node1=n1_list,
116                 filter_list_node2=n2_list
117             )
118             if not links:
119                 raise RuntimeError(
120                     f"No link between {node1[u'host']} and {node2[u'host']}"
121                 )
122
123             # Not using set operations, as we need deterministic order.
124             if always_same_link:
125                 l_set = [link for link in links if link in self._links]
126             else:
127                 l_set = [link for link in links if link not in self._links]
128                 if not l_set:
129                     raise RuntimeError(
130                         f"No free link between {node1[u'host']} and "
131                         f"{node2[u'host']}, all links already used"
132                     )
133
134             if not l_set:
135                 link = links[0]
136             else:
137                 link = l_set[0]
138
139             self._links.append(link)
140             interface1 = topo.get_interface_by_link_name(node1, link)
141             interface2 = topo.get_interface_by_link_name(node2, link)
142             self._path.append((interface1, node1))
143             self._path.append((interface2, node2))
144
145         self._path_iter.extend(self._path)
146         self._path_iter.reverse()
147
148     def next_interface(self):
149         """Path interface iterator.
150
151         :returns: Interface and node or None if not next interface.
152         :rtype: tuple (str, dict)
153
154         .. note:: Call compute_path before.
155         """
156         if not self._path_iter:
157             return None, None
158         return self._path_iter.pop()
159
160     def first_interface(self):
161         """Return first interface on the path.
162
163         :returns: Interface and node.
164         :rtype: tuple (str, dict)
165
166         .. note:: Call compute_path before.
167         """
168         if not self._path:
169             raise RuntimeError(u"No path for topology")
170         return self._path[0]
171
172     def last_interface(self):
173         """Return last interface on the path.
174
175         :returns: Interface and node.
176         :rtype: tuple (str, dict)
177
178         .. note:: Call compute_path before.
179         """
180         if not self._path:
181             raise RuntimeError(u"No path for topology")
182         return self._path[-1]
183
184     def first_ingress_interface(self):
185         """Return first ingress interface on the path.
186
187         :returns: Interface and node.
188         :rtype: tuple (str, dict)
189
190         .. note:: Call compute_path before.
191         """
192         if not self._path:
193             raise RuntimeError(u"No path for topology")
194         return self._path[1]
195
196     def last_egress_interface(self):
197         """Return last egress interface on the path.
198
199         :returns: Interface and node.
200         :rtype: tuple (str, dict)
201
202         .. note:: Call compute_path before.
203         """
204         if not self._path:
205             raise RuntimeError(u"No path for topology")
206         return self._path[-2]