IPsec Multi-Tunnel performance test suite
[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 import re
17 import time
18
19 from robot.api import logger
20
21 from resources.libraries.python.ssh import SSH
22 from resources.libraries.python.topology import NodeType
23 from resources.libraries.python.topology import Topology
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   dev default {{
56   {rxqueuesconfig}
57   {txqueuesconfig}
58   }}
59 {pciconfig}
60   {cryptodevconfig}
61   {uiodriverconfig}
62 {nomultiseg}
63 {enablevhostuser}
64 }}
65
66 ip6 {{
67   hash-buckets 2000000
68   heap-size 3G
69 }}
70
71 """
72 # End VPP configuration template.
73
74
75 class VppConfigGenerator(object):
76     """VPP Configuration File Generator"""
77
78     def __init__(self):
79         self._nodeconfig = {}
80
81     def add_pci_all_devices(self, node):
82         """Add all PCI devices from topology file to startup config
83
84         :param node: DUT node
85         :type node: dict
86         :return: nothing
87         """
88         for port in node['interfaces'].keys():
89             pci_addr = Topology.get_interface_pci_addr(node, port)
90             if pci_addr:
91                 self.add_pci_device(node, pci_addr)
92
93
94     def add_pci_device(self, node, *pci_devices):
95         """Add PCI device configuration for node.
96
97         :param node: DUT node.
98         :param pci_device: PCI devices (format 0000:00:00.0 or 00:00.0)
99         :type node: dict
100         :type pci_devices: tuple
101         :return: nothing
102         """
103         if node['type'] != NodeType.DUT:
104             raise ValueError('Node type is not a DUT')
105
106         # Specific device was given.
107         hostname = Topology.get_node_hostname(node)
108
109         pattern = re.compile("^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{2}:"
110                              "[0-9A-Fa-f]{2}\\.[0-9A-Fa-f]$")
111         for pci_device in pci_devices:
112             if not pattern.match(pci_device):
113                 raise ValueError('PCI address {} to be added to host {} '
114                                  'is not in valid format xxxx:xx:xx.x'.
115                                  format(pci_device, hostname))
116
117             if hostname not in self._nodeconfig:
118                 self._nodeconfig[hostname] = {}
119             if 'pci_addrs' not in self._nodeconfig[hostname]:
120                 self._nodeconfig[hostname]['pci_addrs'] = []
121             self._nodeconfig[hostname]['pci_addrs'].append(pci_device)
122             logger.debug('Adding PCI device {1} to {0}'.format(hostname,
123                                                                pci_device))
124
125     def add_cpu_config(self, node, cpu_config):
126         """Add CPU configuration for node.
127
128         :param node: DUT node.
129         :param cpu_config: CPU configuration option, as a string.
130         :type node: dict
131         :type cpu_config: str
132         :return: nothing
133         """
134         if node['type'] != NodeType.DUT:
135             raise ValueError('Node type is not a DUT')
136         hostname = Topology.get_node_hostname(node)
137         if hostname not in self._nodeconfig:
138             self._nodeconfig[hostname] = {}
139         if 'cpu_config' not in self._nodeconfig[hostname]:
140             self._nodeconfig[hostname]['cpu_config'] = []
141         self._nodeconfig[hostname]['cpu_config'].append(cpu_config)
142         logger.debug('Adding {} to hostname {} CPU config'.format(hostname,
143                                                                   cpu_config))
144
145     def add_socketmem_config(self, node, socketmem_config):
146         """Add Socket Memory configuration for node.
147
148         :param node: DUT node.
149         :param socketmem_config: Socket Memory configuration option,
150         as a string.
151         :type node: dict
152         :type socketmem_config: str
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 hostname not in self._nodeconfig:
159             self._nodeconfig[hostname] = {}
160         self._nodeconfig[hostname]['socketmem_config'] = socketmem_config
161         logger.debug('Setting hostname {} Socket Memory config to {}'.
162                      format(hostname, socketmem_config))
163
164     def add_heapsize_config(self, node, heapsize_config):
165         """Add Heap Size configuration for node.
166
167         :param node: DUT node.
168         :param heapsize_config: Heap Size configuration, as a string.
169         :type node: dict
170         :type heapsize_config: str
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 hostname not in self._nodeconfig:
177             self._nodeconfig[hostname] = {}
178         self._nodeconfig[hostname]['heapsize_config'] = heapsize_config
179         logger.debug('Setting hostname {} Heap Size config to {}'.
180                      format(hostname, heapsize_config))
181
182     def add_rxqueues_config(self, node, rxqueues_config):
183         """Add Rx Queues configuration for node.
184
185         :param node: DUT node.
186         :param rxqueues_config: Rxqueues configuration, as a string.
187         :type node: dict
188         :type rxqueues_config: str
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 not hostname in self._nodeconfig:
195             self._nodeconfig[hostname] = {}
196         if not 'rxqueues_config' in self._nodeconfig[hostname]:
197             self._nodeconfig[hostname]['rxqueues_config'] = []
198         self._nodeconfig[hostname]['rxqueues_config'].append(rxqueues_config)
199         logger.debug('Setting hostname {} rxqueues config to {}'.\
200             format(hostname, rxqueues_config))
201
202     def add_no_multi_seg_config(self, node):
203         """Add No Multi Seg configuration for node.
204
205         :param node: DUT node.
206         :type node: dict
207         :return: nothing
208         """
209         if node['type'] != NodeType.DUT:
210             raise ValueError('Node type is not a DUT')
211         hostname = Topology.get_node_hostname(node)
212         if not hostname in self._nodeconfig:
213             self._nodeconfig[hostname] = {}
214         if not 'no_multi_seg_config' in self._nodeconfig[hostname]:
215             self._nodeconfig[hostname]['no_multi_seg_config'] = []
216         self._nodeconfig[hostname]['no_multi_seg_config'].append(
217             "no-multi-seg")
218         logger.debug('Setting hostname {} config with {}'.\
219             format(hostname, "no-multi-seg"))
220
221     def add_enable_vhost_user_config(self, node):
222         """Add enable-vhost-user configuration for node.
223
224         :param node: DUT node.
225         :type node: dict
226         :return: nothing
227         """
228         if node['type'] != NodeType.DUT:
229             raise ValueError('Node type is not a DUT')
230         hostname = Topology.get_node_hostname(node)
231         if not hostname in self._nodeconfig:
232             self._nodeconfig[hostname] = {}
233         if not 'enable_vhost_user' in self._nodeconfig[hostname]:
234             self._nodeconfig[hostname]['enable_vhost_user'] = []
235         self._nodeconfig[hostname]['enable_vhost_user'].append(
236             "enable-vhost-user")
237         logger.debug('Setting hostname {} config with {}'.\
238             format(hostname, "enable-vhost-user"))
239
240     def add_cryptodev_config(self, node, count):
241         """Add cryptodev configuration for node.
242
243         :param node: DUT node.
244         :param count: Number of crypto device to add.
245         :type node: dict
246         :type count: int
247         :returns: nothing
248         :raises ValueError: If node type is not a DUT
249         """
250         if node['type'] != NodeType.DUT:
251             raise ValueError('Node type is not a DUT')
252         hostname = Topology.get_node_hostname(node)
253         if hostname not in self._nodeconfig:
254             self._nodeconfig[hostname] = {}
255
256         cryptodev = Topology.get_cryptodev(node)
257         cryptodev_config = 'enable-cryptodev'
258
259         for i in range(count):
260             cryptodev_config += ' dev {}'.format(\
261                 re.sub(r'\d.\d$', '1.'+str(i), cryptodev))
262
263         self._nodeconfig[hostname]['cryptodev_config'] = cryptodev_config
264         logger.debug('Setting hostname {} Cryptodev config to {}'.
265                      format(hostname, cryptodev_config))
266
267         uio_driver_config = 'uio-driver {}'.\
268             format(Topology.get_uio_driver(node))
269
270         self._nodeconfig[hostname]['uio_driver_config'] = uio_driver_config
271         logger.debug('Setting hostname {} uio_driver config to {}'.
272                      format(hostname, uio_driver_config))
273
274     def remove_all_pci_devices(self, node):
275         """Remove PCI device configuration from node.
276
277         :param node: DUT node.
278         :type node: dict
279         :return: nothing
280         """
281         if node['type'] != NodeType.DUT:
282             raise ValueError('Node type is not a DUT')
283         hostname = Topology.get_node_hostname(node)
284         if hostname in self._nodeconfig:
285             self._nodeconfig[hostname]['pci_addrs'] = []
286         logger.debug('Clearing all PCI devices for hostname {}.'.
287                      format(hostname))
288
289     def remove_all_cpu_config(self, node):
290         """Remove CPU configuration from node.
291
292         :param node: DUT node.
293         :type node: dict
294         :return: nothing
295         """
296         if node['type'] != NodeType.DUT:
297             raise ValueError('Node type is not a DUT')
298         hostname = Topology.get_node_hostname(node)
299         if hostname in self._nodeconfig:
300             self._nodeconfig[hostname]['cpu_config'] = []
301         logger.debug('Clearing all CPU config for hostname {}.'.
302                      format(hostname))
303
304     def remove_socketmem_config(self, node):
305         """Remove Socket Memory configuration from node.
306
307         :param node: DUT node.
308         :type node: dict
309         :return: nothing
310         """
311         if node['type'] != NodeType.DUT:
312             raise ValueError('Node type is not a DUT')
313         hostname = Topology.get_node_hostname(node)
314         if hostname in self._nodeconfig:
315             self._nodeconfig[hostname].pop('socketmem_config', None)
316         logger.debug('Clearing Socket Memory config for hostname {}.'.
317                      format(hostname))
318
319     def remove_cryptodev_config(self, node):
320         """Remove Cryptodev configuration from node.
321
322         :param node: DUT node.
323         :type node: dict
324         :returns: nothing
325         :raises ValueError: If node type is not a DUT
326         """
327         if node['type'] != NodeType.DUT:
328             raise ValueError('Node type is not a DUT')
329         hostname = Topology.get_node_hostname(node)
330         if hostname in self._nodeconfig:
331             self._nodeconfig[hostname].pop('cryptodev_config', None)
332         logger.debug('Clearing Cryptodev config for hostname {}.'.
333                      format(hostname))
334
335     def remove_heapsize_config(self, node):
336         """Remove Heap Size configuration from node.
337
338         :param node: DUT node.
339         :type node: dict
340         :return: nothing
341         """
342         if node['type'] != NodeType.DUT:
343             raise ValueError('Node type is not a DUT')
344         hostname = Topology.get_node_hostname(node)
345         if hostname in self._nodeconfig:
346             self._nodeconfig[hostname].pop('heapsize_config', None)
347         logger.debug('Clearing Heap Size config for hostname {}.'.
348                      format(hostname))
349
350     def remove_rxqueues_config(self, node):
351         """Remove Rxqueues configuration from node.
352
353         :param node: DUT node.
354         :type node: dict
355         :return: nothing
356         """
357         if node['type'] != NodeType.DUT:
358             raise ValueError('Node type is not a DUT')
359         hostname = Topology.get_node_hostname(node)
360         if hostname in self._nodeconfig:
361             self._nodeconfig[hostname]['rxqueues_config'] = []
362         logger.debug('Clearing rxqueues config for hostname {}.'.\
363             format(hostname))
364
365     def remove_no_multi_seg_config(self, node):
366         """Remove No Multi Seg configuration from node.
367
368         :param node: DUT node.
369         :type node: dict
370         :return: nothing
371         """
372         if node['type'] != NodeType.DUT:
373             raise ValueError('Node type is not a DUT')
374         hostname = Topology.get_node_hostname(node)
375         if hostname in self._nodeconfig:
376             self._nodeconfig[hostname]['no_multi_seg_config'] = []
377         logger.debug('Clearing No Multi Seg config for hostname {}.'.\
378             format(hostname))
379
380     def remove_enable_vhost_user_config(self, node):
381         """Remove enable-vhost-user configuration from node.
382
383         :param node: DUT node.
384         :type node: dict
385         :return: nothing
386         """
387         if node['type'] != NodeType.DUT:
388             raise ValueError('Node type is not a DUT')
389         hostname = Topology.get_node_hostname(node)
390         if hostname in self._nodeconfig:
391             self._nodeconfig[hostname]['enable_vhost_user'] = []
392         logger.debug('Clearing enable-vhost-user config for hostname {}.'.\
393             format(hostname))
394
395     def apply_config(self, node, waittime=5, retries=12):
396         """Generate and apply VPP configuration for node.
397
398         Use data from calls to this class to form a startup.conf file and
399         replace /etc/vpp/startup.conf with it on node.
400
401         :param node: DUT node.
402         :param waittime: Time to wait for VPP to restart (default 5 seconds).
403         :param retries: Number of times (default 12) to re-try waiting.
404         :type node: dict
405         :type waittime: int
406         :type retries: int
407         """
408
409         if node['type'] != NodeType.DUT:
410             raise ValueError('Node type is not a DUT')
411         hostname = Topology.get_node_hostname(node)
412
413         cpuconfig = ""
414         pciconfig = ""
415         socketmemconfig = DEFAULT_SOCKETMEM_CONFIG
416         heapsizeconfig = ""
417         rxqueuesconfig = ""
418         txqueuesconfig = ""
419         nomultiseg = ""
420         enablevhostuser = ""
421         cryptodevconfig = ""
422         uiodriverconfig = ""
423
424         if hostname in self._nodeconfig:
425             cfg = self._nodeconfig[hostname]
426             if 'cpu_config' in cfg:
427                 cpuconfig = "  " + "\n  ".join(cfg['cpu_config'])
428
429             if 'pci_addrs' in cfg:
430                 pciconfig = "  dev " + "\n  dev ".join(cfg['pci_addrs'])
431
432             if 'socketmem_config' in cfg:
433                 socketmemconfig = cfg['socketmem_config']
434
435             if 'cryptodev_config' in cfg:
436                 cryptodevconfig = cfg['cryptodev_config']
437
438             if 'uio_driver_config' in cfg:
439                 uiodriverconfig = cfg['uio_driver_config']
440
441             if 'heapsize_config' in cfg:
442                 heapsizeconfig = "\nheapsize {}\n".\
443                     format(cfg['heapsize_config'])
444
445             if 'rxqueues_config' in cfg:
446                 rxqueuesconfig = "  " + "\n  ".join(cfg['rxqueues_config'])
447
448             if 'no_multi_seg_config' in cfg:
449                 nomultiseg = "  " + "\n  ".join(cfg['no_multi_seg_config'])
450
451             if 'enable_vhost_user' in cfg:
452                 enablevhostuser = "  " + "\n  ".join(cfg['enable_vhost_user'])
453
454         vppconfig = VPP_CONFIG_TEMPLATE.format(cpuconfig=cpuconfig,
455                                                pciconfig=pciconfig,
456                                                cryptodevconfig=cryptodevconfig,
457                                                uiodriverconfig=uiodriverconfig,
458                                                socketmemconfig=socketmemconfig,
459                                                heapsizeconfig=heapsizeconfig,
460                                                rxqueuesconfig=rxqueuesconfig,
461                                                txqueuesconfig=txqueuesconfig,
462                                                nomultiseg=nomultiseg,
463                                                enablevhostuser=enablevhostuser)
464
465         logger.debug('Writing VPP config to host {}: "{}"'.format(hostname,
466                                                                   vppconfig))
467
468         ssh = SSH()
469         ssh.connect(node)
470
471         # We're using this "| sudo tee" construct because redirecting
472         # a sudo'd outut ("sudo echo xxx > /path/to/file") does not
473         # work on most platforms...
474         (ret, stdout, stderr) = \
475             ssh.exec_command('echo "{0}" | sudo tee {1}'.
476                              format(vppconfig, VPP_CONFIG_FILENAME))
477
478         if ret != 0:
479             logger.debug('Writing config file failed to node {}'.
480                          format(hostname))
481             logger.debug('stdout: {}'.format(stdout))
482             logger.debug('stderr: {}'.format(stderr))
483             raise RuntimeError('Writing config file failed to node {}'.
484                                format(hostname))
485
486         # Instead of restarting, we'll do separate start and stop
487         # actions. This way we don't care whether VPP was running
488         # to begin with.
489         ssh.exec_command('sudo service {} stop'.format(VPP_SERVICE_NAME))
490         (ret, stdout, stderr) = \
491             ssh.exec_command('sudo service {} start'.format(VPP_SERVICE_NAME))
492         if ret != 0:
493             logger.debug('Restarting VPP failed on node {}'.
494                          format(hostname))
495             logger.debug('stdout: {}'.format(stdout))
496             logger.debug('stderr: {}'.format(stderr))
497             raise RuntimeError('Restarting VPP failed on node {}'.
498                                format(hostname))
499
500         # Sleep <waittime> seconds, up to <retry> times,
501         # and verify if VPP is running.
502         vpp_is_running = False
503         retries_left = retries
504         while (not vpp_is_running) and (retries_left > 0):
505             time.sleep(waittime)
506             retries_left -= 1
507
508             # FIXME: Need to find a good way to check if VPP is operational.
509             #
510             # If VatTerminal/VatExecutor is anything like vppctl or
511             # vpp_api_test, then in case VPP is NOT running it will block for
512             # 30 seconds or so and not even return if VPP becomes alive during
513             # that time. This makes it unsuitable in this case. We either need
514             # a call that returns immediately, indicating whether VPP is
515             # healthy or not, or a call that waits (up to a defined length
516             # of time) and returns immediately if VPP is or becomes healthy.
517             (ret, stdout, stderr) = \
518                 ssh.exec_command('echo show hardware-interfaces | '
519                                  'nc 0 5002')
520
521             if ret == 0:
522                 vpp_is_running = True
523             else:
524                 logger.debug('VPP not yet running, {} retries left'.
525                              format(retries_left))
526         if retries_left == 0:
527             raise RuntimeError('VPP failed to restart on node {}'.
528                                format(hostname))
529         logger.debug('VPP interfaces found on node {}'.
530                      format(stdout))