Multicore VPP setup for performance testing
[csit.git] / resources / libraries / python / VppConfigGenerator.py
1 # Copyright (c) 2016 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 """VPP Configuration File Generator library"""
15
16 from robot.api import logger
17
18 from resources.libraries.python.ssh import SSH
19 from resources.libraries.python.topology import NodeType
20 from resources.libraries.python.topology import Topology
21
22 import re
23 import time
24
25 __all__ = ['VppConfigGenerator']
26
27 #
28 # VPP configuration template.
29 # TODO: Do we need a better place for this? Somewhere in an external
30 # (template) file?
31 # Note: We're going to pass this through Python string Formatter, so
32 # any literal curly braces need to be escaped.
33 #
34 VPP_SERVICE_NAME = "vpp"
35 VPP_CONFIG_FILENAME = "/etc/vpp/startup.conf"
36 DEFAULT_SOCKETMEM_CONFIG = "1024,1024"
37 VPP_CONFIG_TEMPLATE = """
38 unix {{
39   nodaemon
40   log /tmp/vpe.log
41   cli-listen localhost:5002
42   full-coredump
43 }}
44
45 api-trace {{
46   on
47 }}
48 {heapsizeconfig}
49 cpu {{
50 {cpuconfig}
51 }}
52
53 dpdk {{
54   socket-mem {socketmemconfig}
55 {pciconfig}
56 {rssconfig}
57 }}
58 """
59 # End VPP configuration template.
60
61
62 class VppConfigGenerator(object):
63     """VPP Configuration File Generator"""
64
65     def __init__(self):
66         self._nodeconfig = {}
67
68     def add_pci_device(self, node, pci_device=None):
69         """Add PCI device configuration for node.
70
71         :param node: DUT node
72         :param pci_device: PCI device (format 0000:00:00.0 or 00:00.0).
73         If none given, all PCI devices for this node as per topology will be
74         added.
75         :type node: dict
76         :type pci_device: string
77         :return: nothing
78         """
79         if node['type'] != NodeType.DUT:
80             raise ValueError('Node type is not a DUT')
81
82         if pci_device is None:
83             # No PCI device was given. Add all device from topology.
84             for port in node['interfaces'].values():
85                 port_name = port.get('name')
86                 pci_addr = Topology.get_interface_pci_addr(node, port_name)
87                 if pci_addr:
88                     self.add_pci_device(node, pci_addr)
89         else:
90             # Specific device was given.
91             hostname = Topology.get_node_hostname(node)
92
93             pattern = re.compile("^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{2}:"\
94                 "[0-9A-Fa-f]{2}\\.[0-9A-Fa-f]$")
95             if not pattern.match(pci_device):
96                 raise ValueError('PCI address {} to be added to host {} '\
97                     'is not in valid format xxxx:xx:xx.x'.\
98                     format(pci_device, hostname))
99
100             if not hostname in self._nodeconfig:
101                 self._nodeconfig[hostname] = {}
102             if not 'pci_addrs' in self._nodeconfig[hostname]:
103                 self._nodeconfig[hostname]['pci_addrs'] = []
104             self._nodeconfig[hostname]['pci_addrs'].append(pci_device)
105             logger.debug('Adding PCI device {1} to {0}'.format(hostname,\
106                pci_device))
107
108     def add_cpu_config(self, node, cpu_config):
109         """Add CPU configuration for node.
110
111         :param node: DUT node
112         :param cpu_config: CPU configuration option, as a string
113         :type node: dict
114         :type cpu_config: string
115         :return: nothing
116         """
117         if node['type'] != NodeType.DUT:
118             raise ValueError('Node type is not a DUT')
119         hostname = Topology.get_node_hostname(node)
120         if not hostname in self._nodeconfig:
121             self._nodeconfig[hostname] = {}
122         if not 'cpu_config' in self._nodeconfig[hostname]:
123             self._nodeconfig[hostname]['cpu_config'] = []
124         self._nodeconfig[hostname]['cpu_config'].append(cpu_config)
125         logger.debug('Adding {} to hostname {} CPU config'.format(hostname, \
126             cpu_config))
127
128     def add_socketmem_config(self, node, socketmem_config):
129         """Add Socket Memory configuration for node.
130
131         :param node: DUT node
132         :param socketmem_config: Socket Memory configuration option, as a string
133         :type node: dict
134         :type cpu_config: string
135         :return: nothing
136         """
137         if node['type'] != NodeType.DUT:
138             raise ValueError('Node type is not a DUT')
139         hostname = Topology.get_node_hostname(node)
140         if not hostname in self._nodeconfig:
141             self._nodeconfig[hostname] = {}
142         self._nodeconfig[hostname]['socketmem_config'] = socketmem_config
143         logger.debug('Setting hostname {} Socket Memory config to {}'.\
144             format(hostname, socketmem_config))
145
146     def add_heapsize_config(self, node, heapsize_config):
147         """Add Heap Size configuration for node.
148
149         :param node: DUT node
150         :param heapsize_config: Heap Size configuration, as a string
151         :type node: dict
152         :type cpu_config: string
153         :return: nothing
154         """
155         if node['type'] != NodeType.DUT:
156             raise ValueError('Node type is not a DUT')
157         hostname = Topology.get_node_hostname(node)
158         if not hostname in self._nodeconfig:
159             self._nodeconfig[hostname] = {}
160         self._nodeconfig[hostname]['heapsize_config'] = heapsize_config
161         logger.debug('Setting hostname {} Heap Size config to {}'.\
162             format(hostname, heapsize_config))
163
164     def add_rss_config(self, node, rss_config):
165         """Add RSS configuration for node.
166
167         :param node: DUT node
168         :param rss_config: RSS configuration, as a string
169         :type node: dict
170         :type rss_config: string
171         :return: nothing
172         """
173         if node['type'] != NodeType.DUT:
174             raise ValueError('Node type is not a DUT')
175         hostname = Topology.get_node_hostname(node)
176         if not hostname in self._nodeconfig:
177             self._nodeconfig[hostname] = {}
178         if not 'rss_config' in self._nodeconfig[hostname]:
179             self._nodeconfig[hostname]['rss_config'] = []
180         self._nodeconfig[hostname]['rss_config'].append(rss_config)
181         logger.debug('Setting hostname {} RSS config to {}'.\
182             format(hostname, rss_config))
183
184     def remove_all_pci_devices(self, node):
185         """Remove PCI device configuration from node.
186
187         :param node: DUT node
188         :type: node: dict
189         :return: nothing
190         """
191         if node['type'] != NodeType.DUT:
192             raise ValueError('Node type is not a DUT')
193         hostname = Topology.get_node_hostname(node)
194         if hostname in self._nodeconfig:
195             self._nodeconfig[hostname]['pci_addrs'] = []
196         logger.debug('Clearing all PCI devices for hostname {}.'.\
197             format(hostname))
198
199     def remove_all_cpu_config(self, node):
200         """Remove CPU configuration from node.
201
202         :param node: DUT node
203         :type: node: dict
204         :return: nothing
205         """
206         if node['type'] != NodeType.DUT:
207             raise ValueError('Node type is not a DUT')
208         hostname = Topology.get_node_hostname(node)
209         if hostname in self._nodeconfig:
210             self._nodeconfig[hostname]['cpu_config'] = []
211         logger.debug('Clearing all CPU config for hostname {}.'.\
212             format(hostname))
213
214     def remove_socketmem_config(self, node):
215         """Remove Socket Memory configuration from node.
216
217         :param node: DUT node
218         :type: node: dict
219         :return: nothing
220         """
221         if node['type'] != NodeType.DUT:
222             raise ValueError('Node type is not a DUT')
223         hostname = Topology.get_node_hostname(node)
224         if hostname in self._nodeconfig:
225             self._nodeconfig[hostname].pop('socketmem_config', None)
226         logger.debug('Clearing Socket Memory config for hostname {}.'.\
227             format(hostname))
228
229     def remove_heapsize_config(self, node):
230         """Remove Heap Size configuration from node.
231
232         :param node: DUT node
233         :type: node: dict
234         :return: nothing
235         """
236         if node['type'] != NodeType.DUT:
237             raise ValueError('Node type is not a DUT')
238         hostname = Topology.get_node_hostname(node)
239         if hostname in self._nodeconfig:
240             self._nodeconfig[hostname].pop('heapsize_config', None)
241         logger.debug('Clearing Heap Size config for hostname {}.'.\
242             format(hostname))
243
244     def remove_rss_config(self, node):
245         """Remove RSS configuration from node.
246
247         :param node: DUT node
248         :type: node: dict
249         :return: nothing
250         """
251         if node['type'] != NodeType.DUT:
252             raise ValueError('Node type is not a DUT')
253         hostname = Topology.get_node_hostname(node)
254         if hostname in self._nodeconfig:
255             self._nodeconfig[hostname]['rss_config'] = []
256         logger.debug('Clearing RSS config for hostname {}.'.\
257             format(hostname))
258
259     def apply_config(self, node, waittime=5, retries=12):
260         """Generate and apply VPP configuration for node.
261
262         Use data from calls to this class to form a startup.conf file and
263         replace /etc/vpp/startup.conf with it on node.
264
265         :param node: DUT node
266         :param waittime: time to wait for VPP to restart (default 5 seconds)
267         :param retries: number of times (default 12) to re-try waiting
268         :type node: dict
269         :type waittime: int
270         :type retries: int
271         """
272
273         if node['type'] != NodeType.DUT:
274             raise ValueError('Node type is not a DUT')
275         hostname = Topology.get_node_hostname(node)
276
277         cpuconfig = ""
278         pciconfig = ""
279         socketmemconfig = DEFAULT_SOCKETMEM_CONFIG
280         heapsizeconfig = ""
281         rssconfig = ""
282
283         if hostname in self._nodeconfig:
284             cfg = self._nodeconfig[hostname]
285             if 'cpu_config' in cfg:
286                 cpuconfig = "  " + "\n  ".join(cfg['cpu_config'])
287
288             if 'pci_addrs' in cfg:
289                 pciconfig = "  dev " + "\n  dev ".join(cfg['pci_addrs'])
290
291             if 'socketmem_config' in cfg:
292                 socketmemconfig = cfg['socketmem_config']
293
294             if 'heapsize_config' in cfg:
295                 heapsizeconfig = "\nheapsize {}\n".\
296                     format(cfg['heapsize_config'])
297
298             if 'rss_config' in cfg:
299                 rssconfig = "  " + "\n  ".join(cfg['rss_config'])
300
301         vppconfig = VPP_CONFIG_TEMPLATE.format(cpuconfig=cpuconfig,
302                                                pciconfig=pciconfig,
303                                                socketmemconfig=socketmemconfig,
304                                                heapsizeconfig=heapsizeconfig,
305                                                rssconfig=rssconfig)
306
307         logger.debug('Writing VPP config to host {}: "{}"'.format(hostname,\
308                vppconfig))
309
310         ssh = SSH()
311         ssh.connect(node)
312
313         # We're using this "| sudo tee" construct because redirecting
314         # a sudo'd outut ("sudo echo xxx > /path/to/file") does not
315         # work on most platforms...
316         (ret, stdout, stderr) = \
317             ssh.exec_command('echo "{0}" | sudo tee {1}'.\
318             format(vppconfig, VPP_CONFIG_FILENAME))
319
320         if ret != 0:
321             logger.debug('Writing config file failed to node {}'.\
322                 format(hostname))
323             logger.debug('stdout: {}'.format(stdout))
324             logger.debug('stderr: {}'.format(stderr))
325             raise RuntimeError('Writing config file failed to node {}'.\
326                 format(hostname))
327
328         # Instead of restarting, we'll do separate start and stop
329         # actions. This way we don't care whether VPP was running
330         # to begin with.
331         ssh.exec_command('sudo initctl stop {}'.format(VPP_SERVICE_NAME))
332         (ret, stdout, stderr) = \
333             ssh.exec_command('sudo initctl start {}'.format(VPP_SERVICE_NAME))
334         if ret != 0:
335             logger.debug('Restarting VPP failed on node {}'.\
336                 format(hostname))
337             logger.debug('stdout: {}'.format(stdout))
338             logger.debug('stderr: {}'.format(stderr))
339             raise RuntimeError('Restarting VPP failed on node {}'.\
340                 format(hostname))
341
342         # Sleep <waittime> seconds, up to <retry> times,
343         # and verify if VPP is running.
344         vpp_is_running = False
345         retries_left = retries
346         while (not vpp_is_running) and (retries_left > 0):
347             time.sleep(waittime)
348             retries_left -= 1
349
350             # FIXME: Need to find a good way to check if VPP is operational.
351             #
352             # If VatTerminal/VatExecutor is anything like vppctl or
353             # vpp_api_test, then in case VPP is NOT running it will block for
354             # 30 seconds or so and not even return if VPP becomes alive during
355             # that time. This makes it unsuitable in this case. We either need
356             # a call that returns immediately, indicating whether VPP is
357             # healthy or not, or a call that waits (up to a defined length
358             # of time) and returns immediately if VPP is or becomes healthy.
359             (ret, stdout, stderr) = \
360                 ssh.exec_command('echo show hardware-interfaces | '\
361                     'nc 0 5002')
362
363             if ret == 0:
364                 vpp_is_running = True
365             else:
366                 logger.debug('VPP not yet running, {} retries left'.\
367                     format(retries_left))
368         if retries_left == 0:
369             raise RuntimeError('VPP failed to restart on node {}'.\
370                 format(hostname))
371         logger.debug('VPP interfaces found on node {}'.\
372            format(stdout))