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