Fix Tap failing tests
[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 resources.libraries.python.ssh import SSH
17
18 __all__ = ["CpuUtils"]
19
20
21 class CpuUtils(object):
22     """CPU utilities"""
23
24     # Number of threads per core.
25     NR_OF_THREADS = 2
26
27     @staticmethod
28     def __str2int(string):
29         """Conversion from string to integer, 0 in case of empty string.
30
31         :param string: Input string.
32         :type string: str
33         :returns: Integer converted from string, 0 in case of ValueError.
34         :rtype: int
35         """
36         try:
37             return int(string)
38         except ValueError:
39             return 0
40
41     @staticmethod
42     def is_smt_enabled(cpu_info):
43         """Uses CPU mapping to find out if SMT is enabled or not. If SMT is
44         enabled, the L1d,L1i,L2,L3 setting is the same for two processors. These
45         two processors are two threads of one core.
46
47         :param cpu_info: CPU info, the output of "lscpu -p".
48         :type cpu_info: list
49         :returns: True if SMT is enabled, False if SMT is disabled.
50         :rtype: bool
51         """
52         cpu_mems = [item[-4:] for item in cpu_info]
53         cpu_mems_len = len(cpu_mems) / CpuUtils.NR_OF_THREADS
54         count = 0
55         for cpu_mem in cpu_mems[:cpu_mems_len]:
56             if cpu_mem in cpu_mems[cpu_mems_len:]:
57                 count += 1
58         return bool(count == cpu_mems_len)
59
60     @staticmethod
61     def get_cpu_layout_from_all_nodes(nodes):
62         """Retrieve cpu layout from all nodes, assuming all nodes
63            are Linux nodes.
64
65         :param nodes: DICT__nodes from Topology.DICT__nodes.
66         :type nodes: dict
67         :raises RuntimeError: If the ssh command "lscpu -p" fails.
68         """
69         ssh = SSH()
70         for node in nodes.values():
71             ssh.connect(node)
72             cmd = "lscpu -p"
73             ret, stdout, stderr = ssh.exec_command(cmd)
74 #           parsing of "lscpu -p" output:
75 #           # CPU,Core,Socket,Node,,L1d,L1i,L2,L3
76 #           0,0,0,0,,0,0,0,0
77 #           1,1,0,0,,1,1,1,0
78             if ret != 0:
79                 raise RuntimeError(
80                     "Failed to execute ssh command, ret: {} err: {}".format(
81                         ret, stderr))
82             node['cpuinfo'] = list()
83             for line in stdout.split("\n"):
84                 if line and line[0] != "#":
85                     node['cpuinfo'].append([CpuUtils.__str2int(x) for x in
86                                             line.split(",")])
87
88     @staticmethod
89     def cpu_node_count(node):
90         """Return count of numa nodes.
91
92         :param node: Targeted node.
93         :type node: dict
94         :returns: Count of numa nodes.
95         :rtype: int
96         :raises RuntimeError: If node cpuinfo is not available.
97         """
98         cpu_info = node.get("cpuinfo")
99         if cpu_info is not None:
100             return node["cpuinfo"][-1][3] + 1
101         else:
102             raise RuntimeError("Node cpuinfo not available.")
103
104     @staticmethod
105     def cpu_list_per_node(node, cpu_node, smt_used=False):
106         """Return node related list of CPU numbers.
107
108         :param node: Node dictionary with cpuinfo.
109         :param cpu_node: Numa node number.
110         :param smt_used: True - we want to use SMT, otherwise false.
111         :type node: dict
112         :type cpu_node: int
113         :type smt_used: bool
114         :returns: List of cpu numbers related to numa from argument.
115         :rtype: list of int
116         :raises RuntimeError: If node cpuinfo is not available
117             or if SMT is not enabled.
118         """
119         cpu_node = int(cpu_node)
120         cpu_info = node.get("cpuinfo")
121         if cpu_info is None:
122             raise RuntimeError("Node cpuinfo not available.")
123
124         smt_enabled = CpuUtils.is_smt_enabled(cpu_info)
125         if not smt_enabled and smt_used:
126             raise RuntimeError("SMT is not enabled.")
127
128         cpu_list = []
129         for cpu in cpu_info:
130             if cpu[3] == cpu_node:
131                 cpu_list.append(cpu[0])
132
133         if not smt_enabled or smt_enabled and smt_used:
134             pass
135
136         if smt_enabled and not smt_used:
137             cpu_list_len = len(cpu_list)
138             cpu_list = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS]
139
140         return cpu_list
141
142     @staticmethod
143     def cpu_slice_of_list_per_node(node, cpu_node, skip_cnt=0, cpu_cnt=0,
144                                    smt_used=False):
145         """Return string of node related list of CPU numbers.
146
147         :param node: Node dictionary with cpuinfo.
148         :param cpu_node: Numa node number.
149         :param skip_cnt: Skip first "skip_cnt" CPUs.
150         :param cpu_cnt: Count of cpus to return, if 0 then return all.
151         :param smt_used: True - we want to use SMT, otherwise false.
152         :type node: dict
153         :type cpu_node: int
154         :type skip_cnt: int
155         :type cpu_cnt: int
156         :type smt_used: bool
157         :returns: Cpu numbers related to numa from argument.
158         :rtype: list
159         :raises RuntimeError: If we require more cpus than available.
160         """
161         cpu_list = CpuUtils.cpu_list_per_node(node, cpu_node, smt_used)
162
163         cpu_list_len = len(cpu_list)
164         if cpu_cnt + skip_cnt > cpu_list_len:
165             raise RuntimeError("cpu_cnt + skip_cnt > length(cpu list).")
166
167         if cpu_cnt == 0:
168             cpu_cnt = cpu_list_len - skip_cnt
169
170         if smt_used:
171             cpu_list_0 = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS]
172             cpu_list_1 = cpu_list[cpu_list_len / CpuUtils.NR_OF_THREADS:]
173             cpu_list = [cpu for cpu in cpu_list_0[skip_cnt:skip_cnt + cpu_cnt]]
174             cpu_list_ex = [cpu for cpu in
175                            cpu_list_1[skip_cnt:skip_cnt + cpu_cnt]]
176             cpu_list.extend(cpu_list_ex)
177         else:
178             cpu_list = [cpu for cpu in cpu_list[skip_cnt:skip_cnt + cpu_cnt]]
179
180         return cpu_list
181
182     @staticmethod
183     def cpu_list_per_node_str(node, cpu_node, skip_cnt=0, cpu_cnt=0, sep=",",
184                               smt_used=False):
185         """Return string of node related list of CPU numbers.
186
187         :param node: Node dictionary with cpuinfo.
188         :param cpu_node: Numa node number.
189         :param skip_cnt: Skip first "skip_cnt" CPUs.
190         :param cpu_cnt: Count of cpus to return, if 0 then return all.
191         :param sep: Separator, default: 1,2,3,4,....
192         :param smt_used: True - we want to use SMT, otherwise false.
193         :type node: dict
194         :type cpu_node: int
195         :type skip_cnt: int
196         :type cpu_cnt: int
197         :type sep: str
198         :type smt_used: bool
199         :returns: Cpu numbers related to numa from argument.
200         :rtype: str
201         """
202         cpu_list = CpuUtils.cpu_slice_of_list_per_node(node, cpu_node,
203                                                        skip_cnt=skip_cnt,
204                                                        cpu_cnt=cpu_cnt,
205                                                        smt_used=smt_used)
206         return sep.join(str(cpu) for cpu in cpu_list)
207
208     @staticmethod
209     def cpu_range_per_node_str(node, cpu_node, skip_cnt=0, cpu_cnt=0, sep="-",
210                                smt_used=False):
211         """Return string of node related range of CPU numbers, e.g. 0-4.
212
213         :param node: Node dictionary with cpuinfo.
214         :param cpu_node: Numa node number.
215         :param skip_cnt: Skip first "skip_cnt" CPUs.
216         :param cpu_cnt: Count of cpus to return, if 0 then return all.
217         :param sep: Separator, default: "-".
218         :param smt_used: True - we want to use SMT, otherwise false.
219         :type node: dict
220         :type cpu_node: int
221         :type skip_cnt: int
222         :type cpu_cnt: int
223         :type sep: str
224         :type smt_used: bool
225         :returns: String of node related range of CPU numbers.
226         :rtype: str
227         """
228         cpu_list = CpuUtils.cpu_slice_of_list_per_node(node, cpu_node,
229                                                        skip_cnt=skip_cnt,
230                                                        cpu_cnt=cpu_cnt,
231                                                        smt_used=smt_used)
232         if smt_used:
233             cpu_list_len = len(cpu_list)
234             cpu_list_0 = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS]
235             cpu_list_1 = cpu_list[cpu_list_len / CpuUtils.NR_OF_THREADS:]
236             cpu_range = "{}{}{},{}{}{}".format(cpu_list_0[0], sep,
237                                                cpu_list_0[-1],
238                                                cpu_list_1[0], sep,
239                                                cpu_list_1[-1])
240         else:
241             cpu_range = "{}{}{}".format(cpu_list[0], sep, cpu_list[-1])
242
243         return cpu_range
244
245     @staticmethod
246     def cpu_slice_of_list_for_nf(**kwargs):
247         """Return list of node related list of CPU numbers.
248
249         :param kwargs: Key-value pairs used to compute placement.
250         :type kwargs: dict
251         :returns: Cpu numbers related to numa from argument.
252         :rtype: list
253         :raises RuntimeError: If we require more cpus than available or if
254         placement is not possible due to wrong parameters.
255         """
256         if kwargs['chain_id'] - 1 >= kwargs['chains']:
257             raise RuntimeError("ChainID is higher than total number of chains!")
258         if kwargs['node_id'] - 1 >= kwargs['nodeness']:
259             raise RuntimeError("NodeID is higher than chain nodeness!")
260
261         smt_used = CpuUtils.is_smt_enabled(kwargs['node']['cpuinfo'])
262         cpu_list = CpuUtils.cpu_list_per_node(kwargs['node'],
263                                               kwargs['cpu_node'], smt_used)
264         cpu_list_len = len(cpu_list)
265
266         mt_req = ((kwargs['chains'] * kwargs['nodeness']) + kwargs['mtcr'] - 1)\
267             / kwargs['mtcr']
268         dt_req = ((kwargs['chains'] * kwargs['nodeness']) + kwargs['dtcr'] - 1)\
269             / kwargs['dtcr']
270
271         cpu_req = kwargs['skip_cnt'] + mt_req + dt_req
272         if smt_used and cpu_req > cpu_list_len / CpuUtils.NR_OF_THREADS:
273             raise RuntimeError("Not enough CPU cores available for placement!")
274         elif not smt_used and cpu_req > cpu_list_len:
275             raise RuntimeError("Not enough CPU cores available for placement!")
276
277         offset = (kwargs['node_id'] - 1) + (kwargs['chain_id'] - 1)\
278             * kwargs['nodeness']
279         dtc = kwargs['dtc']
280         try:
281             mt_odd = (offset / mt_req) & 1
282             mt_skip = kwargs['skip_cnt'] + (offset % mt_req)
283             dt_skip = kwargs['skip_cnt'] + mt_req + (offset % dt_req) * dtc
284         except ZeroDivisionError:
285             raise RuntimeError("Invalid placement combination!")
286
287         if smt_used:
288             cpu_list_0 = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS]
289             cpu_list_1 = cpu_list[cpu_list_len / CpuUtils.NR_OF_THREADS:]
290
291             mt_cpu_list = [cpu for cpu in cpu_list_1[mt_skip:mt_skip + 1]] \
292                 if mt_odd else [cpu for cpu in cpu_list_0[mt_skip:mt_skip + 1]]
293
294             dt_cpu_list = [cpu for cpu in cpu_list_0[dt_skip:dt_skip + dtc]]
295             dt_cpu_list += [cpu for cpu in cpu_list_1[dt_skip:dt_skip + dtc]]
296         else:
297             mt_cpu_list = [cpu for cpu in cpu_list[mt_skip:mt_skip + 1]]
298             dt_cpu_list = [cpu for cpu in cpu_list[dt_skip:dt_skip + dtc]]
299
300         return mt_cpu_list + dt_cpu_list