Update of VPP_STABLE_VER files + quick fix for gre create tunnel
[csit.git] / resources / libraries / python / CpuUtils.py
1 # Copyright (c) 2019 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 """CPU utilities library."""
15
16 from robot.libraries.BuiltIn import BuiltIn
17
18 from resources.libraries.python.Constants import Constants
19 from resources.libraries.python.ssh import exec_cmd_no_error
20 from resources.libraries.python.topology import Topology
21
22 __all__ = ["CpuUtils"]
23
24
25 class CpuUtils(object):
26     """CPU utilities"""
27
28     # Number of threads per core.
29     NR_OF_THREADS = 2
30
31     @staticmethod
32     def __str2int(string):
33         """Conversion from string to integer, 0 in case of empty string.
34
35         :param string: Input string.
36         :type string: str
37         :returns: Integer converted from string, 0 in case of ValueError.
38         :rtype: int
39         """
40         try:
41             return int(string)
42         except ValueError:
43             return 0
44
45     @staticmethod
46     def is_smt_enabled(cpu_info):
47         """Uses CPU mapping to find out if SMT is enabled or not. If SMT is
48         enabled, the L1d,L1i,L2,L3 setting is the same for two processors. These
49         two processors are two threads of one core.
50
51         :param cpu_info: CPU info, the output of "lscpu -p".
52         :type cpu_info: list
53         :returns: True if SMT is enabled, False if SMT is disabled.
54         :rtype: bool
55         """
56         cpu_mems = [item[-4:] for item in cpu_info]
57         cpu_mems_len = len(cpu_mems) / CpuUtils.NR_OF_THREADS
58         count = 0
59         for cpu_mem in cpu_mems[:cpu_mems_len]:
60             if cpu_mem in cpu_mems[cpu_mems_len:]:
61                 count += 1
62         return bool(count == cpu_mems_len)
63
64     @staticmethod
65     def get_cpu_layout_from_all_nodes(nodes):
66         """Retrieve cpu layout from all nodes, assuming all nodes
67            are Linux nodes.
68
69         :param nodes: DICT__nodes from Topology.DICT__nodes.
70         :type nodes: dict
71         :raises RuntimeError: If the ssh command "lscpu -p" fails.
72         """
73         for node in nodes.values():
74             stdout, _ = exec_cmd_no_error(node, 'lscpu -p')
75             node['cpuinfo'] = list()
76             for line in stdout.split("\n"):
77                 if line and line[0] != "#":
78                     node['cpuinfo'].append([CpuUtils.__str2int(x) for x in
79                                             line.split(",")])
80
81     @staticmethod
82     def cpu_node_count(node):
83         """Return count of numa nodes.
84
85         :param node: Targeted node.
86         :type node: dict
87         :returns: Count of numa nodes.
88         :rtype: int
89         :raises RuntimeError: If node cpuinfo is not available.
90         """
91         cpu_info = node.get("cpuinfo")
92         if cpu_info is not None:
93             return node["cpuinfo"][-1][3] + 1
94         else:
95             raise RuntimeError("Node cpuinfo not available.")
96
97     @staticmethod
98     def cpu_list_per_node(node, cpu_node, smt_used=False):
99         """Return node related list of CPU numbers.
100
101         :param node: Node dictionary with cpuinfo.
102         :param cpu_node: Numa node number.
103         :param smt_used: True - we want to use SMT, otherwise false.
104         :type node: dict
105         :type cpu_node: int
106         :type smt_used: bool
107         :returns: List of cpu numbers related to numa from argument.
108         :rtype: list of int
109         :raises RuntimeError: If node cpuinfo is not available
110             or if SMT is not enabled.
111         """
112         cpu_node = int(cpu_node)
113         cpu_info = node.get("cpuinfo")
114         if cpu_info is None:
115             raise RuntimeError("Node cpuinfo not available.")
116
117         smt_enabled = CpuUtils.is_smt_enabled(cpu_info)
118         if not smt_enabled and smt_used:
119             raise RuntimeError("SMT is not enabled.")
120
121         cpu_list = []
122         for cpu in cpu_info:
123             if cpu[3] == cpu_node:
124                 cpu_list.append(cpu[0])
125
126         if not smt_enabled or smt_enabled and smt_used:
127             pass
128
129         if smt_enabled and not smt_used:
130             cpu_list_len = len(cpu_list)
131             cpu_list = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS]
132
133         return cpu_list
134
135     @staticmethod
136     def cpu_slice_of_list_per_node(node, cpu_node, skip_cnt=0, cpu_cnt=0,
137                                    smt_used=False):
138         """Return string of node related list of CPU numbers.
139
140         :param node: Node dictionary with cpuinfo.
141         :param cpu_node: Numa node number.
142         :param skip_cnt: Skip first "skip_cnt" CPUs.
143         :param cpu_cnt: Count of cpus to return, if 0 then return all.
144         :param smt_used: True - we want to use SMT, otherwise false.
145         :type node: dict
146         :type cpu_node: int
147         :type skip_cnt: int
148         :type cpu_cnt: int
149         :type smt_used: bool
150         :returns: Cpu numbers related to numa from argument.
151         :rtype: list
152         :raises RuntimeError: If we require more cpus than available.
153         """
154         cpu_list = CpuUtils.cpu_list_per_node(node, cpu_node, smt_used)
155
156         cpu_list_len = len(cpu_list)
157         if cpu_cnt + skip_cnt > cpu_list_len:
158             raise RuntimeError("cpu_cnt + skip_cnt > length(cpu list).")
159
160         if cpu_cnt == 0:
161             cpu_cnt = cpu_list_len - skip_cnt
162
163         if smt_used:
164             cpu_list_0 = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS]
165             cpu_list_1 = cpu_list[cpu_list_len / CpuUtils.NR_OF_THREADS:]
166             cpu_list = [cpu for cpu in cpu_list_0[skip_cnt:skip_cnt + cpu_cnt]]
167             cpu_list_ex = [cpu for cpu in
168                            cpu_list_1[skip_cnt:skip_cnt + cpu_cnt]]
169             cpu_list.extend(cpu_list_ex)
170         else:
171             cpu_list = [cpu for cpu in cpu_list[skip_cnt:skip_cnt + cpu_cnt]]
172
173         return cpu_list
174
175     @staticmethod
176     def cpu_list_per_node_str(node, cpu_node, skip_cnt=0, cpu_cnt=0, sep=",",
177                               smt_used=False):
178         """Return string of node related list of CPU numbers.
179
180         :param node: Node dictionary with cpuinfo.
181         :param cpu_node: Numa node number.
182         :param skip_cnt: Skip first "skip_cnt" CPUs.
183         :param cpu_cnt: Count of cpus to return, if 0 then return all.
184         :param sep: Separator, default: 1,2,3,4,....
185         :param smt_used: True - we want to use SMT, otherwise false.
186         :type node: dict
187         :type cpu_node: int
188         :type skip_cnt: int
189         :type cpu_cnt: int
190         :type sep: str
191         :type smt_used: bool
192         :returns: Cpu numbers related to numa from argument.
193         :rtype: str
194         """
195         cpu_list = CpuUtils.cpu_slice_of_list_per_node(node, cpu_node,
196                                                        skip_cnt=skip_cnt,
197                                                        cpu_cnt=cpu_cnt,
198                                                        smt_used=smt_used)
199         return sep.join(str(cpu) for cpu in cpu_list)
200
201     @staticmethod
202     def cpu_range_per_node_str(node, cpu_node, skip_cnt=0, cpu_cnt=0, sep="-",
203                                smt_used=False):
204         """Return string of node related range of CPU numbers, e.g. 0-4.
205
206         :param node: Node dictionary with cpuinfo.
207         :param cpu_node: Numa node number.
208         :param skip_cnt: Skip first "skip_cnt" CPUs.
209         :param cpu_cnt: Count of cpus to return, if 0 then return all.
210         :param sep: Separator, default: "-".
211         :param smt_used: True - we want to use SMT, otherwise false.
212         :type node: dict
213         :type cpu_node: int
214         :type skip_cnt: int
215         :type cpu_cnt: int
216         :type sep: str
217         :type smt_used: bool
218         :returns: String of node related range of CPU numbers.
219         :rtype: str
220         """
221         cpu_list = CpuUtils.cpu_slice_of_list_per_node(node, cpu_node,
222                                                        skip_cnt=skip_cnt,
223                                                        cpu_cnt=cpu_cnt,
224                                                        smt_used=smt_used)
225         if smt_used:
226             cpu_list_len = len(cpu_list)
227             cpu_list_0 = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS]
228             cpu_list_1 = cpu_list[cpu_list_len / CpuUtils.NR_OF_THREADS:]
229             cpu_range = "{}{}{},{}{}{}".format(cpu_list_0[0], sep,
230                                                cpu_list_0[-1],
231                                                cpu_list_1[0], sep,
232                                                cpu_list_1[-1])
233         else:
234             cpu_range = "{}{}{}".format(cpu_list[0], sep, cpu_list[-1])
235
236         return cpu_range
237
238     @staticmethod
239     def cpu_slice_of_list_for_nf(node, cpu_node, nf_chains=1, nf_nodes=1,
240                                  nf_chain=1, nf_node=1, nf_dtc=1, nf_mtcr=2,
241                                  nf_dtcr=1, skip_cnt=0):
242         """Return list of DUT node related list of CPU numbers. The main
243         computing unit is physical core count.
244
245         :param node: DUT node.
246         :param cpu_node: Numa node number.
247         :param nf_chains: Number of NF chains.
248         :param nf_nodes: Number of NF nodes in chain.
249         :param nf_chain: Chain number indexed from 1.
250         :param nf_node: Node number indexed from 1.
251         :param vs_dtc: Amount of physical cores for vswitch dataplane.
252         :param nf_dtc: Amount of physical cores for NF dataplane.
253         :param nf_mtcr: NF main thread per core ratio.
254         :param nf_dtcr: NF dataplane thread per core ratio.
255         :param skip_cnt: Skip first "skip_cnt" CPUs.
256         :type node: dict
257         :param cpu_node: int.
258         :type nf_chains: int
259         :type nf_nodes: int
260         :type nf_chain: int
261         :type nf_node: int
262         :type vs_dtc: int
263         :type nf_dtc: int or float
264         :type nf_mtcr: int
265         :type nf_dtcr: int
266         :type skip_cnt: int
267         :returns: List of CPUs allocated to NF.
268         :rtype: list
269         :raises RuntimeError: If we require more cpus than available or if
270         placement is not possible due to wrong parameters.
271         """
272         if nf_chain - 1 >= nf_chains:
273             raise RuntimeError("ChainID is higher than total number of chains!")
274         if nf_node - 1 >= nf_nodes:
275             raise RuntimeError("NodeID is higher than chain nodes!")
276
277         smt_used = CpuUtils.is_smt_enabled(node['cpuinfo'])
278         cpu_list = CpuUtils.cpu_list_per_node(node, cpu_node, smt_used)
279         # CPU thread sibling offset.
280         sib = len(cpu_list) / CpuUtils.NR_OF_THREADS
281
282         if not smt_used and not isinstance(nf_dtc, int):
283             raise RuntimeError("Cannot allocate if SMT is not enabled!")
284         # TODO: Workaround as we are using physical core as main unit, we must
285         # adjust number of physical dataplane cores in case of float for further
286         # array referencing. As rounding method in Py2.7 and Py3.x differs, we
287         # are using static mapping. This can be rewritten using flat arrays and
288         # different logic (from Physical core unit to Logical core unit).
289         dtc = 1 if not isinstance(nf_dtc, int) else nf_dtc
290
291         mt_req = ((nf_chains * nf_nodes) + nf_mtcr - 1) / nf_mtcr
292         dt_req = ((nf_chains * nf_nodes) + nf_dtcr - 1) / nf_dtcr
293         cpu_req = skip_cnt + mt_req + dt_req
294
295         if smt_used and cpu_req > len(cpu_list) / CpuUtils.NR_OF_THREADS:
296             raise RuntimeError("Not enough CPU cores available for placement!")
297         elif not smt_used and cpu_req > len(cpu_list):
298             raise RuntimeError("Not enough CPU cores available for placement!")
299
300         offset = (nf_node - 1) + (nf_chain - 1) * nf_nodes
301         try:
302             mt_odd = (offset / mt_req) & 1
303             mt_skip = skip_cnt + (offset % mt_req)
304             dt_odd = (offset / dt_req) & 1
305             dt_skip = skip_cnt + mt_req + (offset % dt_req) * dtc
306         except ZeroDivisionError:
307             raise RuntimeError("Invalid placement combination!")
308         if smt_used:
309             mt_list = [cpu for cpu in cpu_list[mt_skip+sib:mt_skip+sib + 1]] \
310                 if mt_odd else [cpu for cpu in cpu_list[mt_skip:mt_skip + 1]]
311             dt_list = [cpu for cpu in cpu_list[dt_skip+sib:dt_skip+sib + dtc]] \
312                 if dt_odd else [cpu for cpu in cpu_list[dt_skip:dt_skip + dtc]]
313             if isinstance(nf_dtc, int):
314                 dt_list = \
315                     [cpu for cpu in cpu_list[dt_skip:dt_skip + dtc]]
316                 dt_list += \
317                     [cpu for cpu in cpu_list[dt_skip+sib:dt_skip+sib + dtc]]
318         else:
319             mt_list = [cpu for cpu in cpu_list[mt_skip:mt_skip + 1]]
320             dt_list = [cpu for cpu in cpu_list[dt_skip:dt_skip + dtc]]
321
322         return mt_list + dt_list
323
324     @staticmethod
325     def get_affinity_nf(nodes, node, nf_chains=1, nf_nodes=1, nf_chain=1,
326                         nf_node=1, vs_dtc=1, nf_dtc=1, nf_mtcr=2, nf_dtcr=1):
327
328         """Get affinity of NF (network function). Result will be used to compute
329         the amount of CPUs and also affinity.
330
331         :param nodes: Physical topology nodes.
332         :param node: SUT node.
333         :param nf_chains: Number of NF chains.
334         :param nf_nodes: Number of NF nodes in chain.
335         :param nf_chain: Chain number indexed from 1.
336         :param nf_node: Node number indexed from 1.
337         :param vs_dtc: Amount of physical cores for vswitch dataplane.
338         :param nf_dtc: Amount of physical cores for NF dataplane.
339         :param nf_mtcr: NF main thread per core ratio.
340         :param nf_dtcr: NF dataplane thread per core ratio.
341         :type nodes: dict
342         :type node: dict
343         :type nf_chains: int
344         :type nf_nodes: int
345         :type nf_chain: int
346         :type nf_node: int
347         :type vs_dtc: int
348         :type nf_dtc: int or float
349         :type nf_mtcr: int
350         :type nf_dtcr: int
351         :returns: List of CPUs allocated to NF.
352         :rtype: list
353         """
354         skip_cnt = Constants.CPU_CNT_SYSTEM + Constants.CPU_CNT_MAIN + vs_dtc
355
356         interface_list = []
357         interface_list.append(
358             BuiltIn().get_variable_value('${{{node}_if1}}'.format(node=node)))
359         interface_list.append(
360             BuiltIn().get_variable_value('${{{node}_if2}}'.format(node=node)))
361
362         cpu_node = Topology.get_interfaces_numa_node(
363             nodes[node], *interface_list)
364
365         return CpuUtils.cpu_slice_of_list_for_nf(
366             node=nodes[node], cpu_node=cpu_node, nf_chains=nf_chains,
367             nf_nodes=nf_nodes, nf_chain=nf_chain, nf_node=nf_node,
368             nf_mtcr=nf_mtcr, nf_dtcr=nf_dtcr, nf_dtc=nf_dtc, skip_cnt=skip_cnt)
369