Add Honeycomb persistence tests
[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 {txqueuesconfig}
56 {pciconfig}
57 {rssconfig}
58 {nomultiseg}
59 }}
60 """
61 # End VPP configuration template.
62
63
64 class VppConfigGenerator(object):
65     """VPP Configuration File Generator"""
66
67     def __init__(self):
68         self._nodeconfig = {}
69
70     def add_pci_device(self, node, pci_device=None):
71         """Add PCI device configuration for node.
72
73         :param node: DUT node.
74         :param pci_device: PCI device (format 0000:00:00.0 or 00:00.0).
75         If none given, all PCI devices for this node as per topology will be
76         added.
77         :type node: dict
78         :type pci_device: str
79         :return: nothing
80         """
81         if node['type'] != NodeType.DUT:
82             raise ValueError('Node type is not a DUT')
83
84         if pci_device is None:
85             # No PCI device was given. Add all device from topology.
86             for port in node['interfaces'].values():
87                 port_name = port.get('name')
88                 pci_addr = Topology.get_interface_pci_addr(node, port_name)
89                 if pci_addr:
90                     self.add_pci_device(node, pci_addr)
91         else:
92             # Specific device was given.
93             hostname = Topology.get_node_hostname(node)
94
95             pattern = re.compile("^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{2}:"
96                                  "[0-9A-Fa-f]{2}\\.[0-9A-Fa-f]$")
97             if not pattern.match(pci_device):
98                 raise ValueError('PCI address {} to be added to host {} '
99                                  'is not in valid format xxxx:xx:xx.x'.
100                                  format(pci_device, hostname))
101
102             if hostname not in self._nodeconfig:
103                 self._nodeconfig[hostname] = {}
104             if 'pci_addrs' not in self._nodeconfig[hostname]:
105                 self._nodeconfig[hostname]['pci_addrs'] = []
106             self._nodeconfig[hostname]['pci_addrs'].append(pci_device)
107             logger.debug('Adding PCI device {1} to {0}'.format(hostname,
108                                                                pci_device))
109
110     def add_cpu_config(self, node, cpu_config):
111         """Add CPU configuration for node.
112
113         :param node: DUT node.
114         :param cpu_config: CPU configuration option, as a string.
115         :type node: dict
116         :type cpu_config: str
117         :return: nothing
118         """
119         if node['type'] != NodeType.DUT:
120             raise ValueError('Node type is not a DUT')
121         hostname = Topology.get_node_hostname(node)
122         if hostname not in self._nodeconfig:
123             self._nodeconfig[hostname] = {}
124         if 'cpu_config' not in self._nodeconfig[hostname]:
125             self._nodeconfig[hostname]['cpu_config'] = []
126         self._nodeconfig[hostname]['cpu_config'].append(cpu_config)
127         logger.debug('Adding {} to hostname {} CPU config'.format(hostname,
128                                                                   cpu_config))
129
130     def add_socketmem_config(self, node, socketmem_config):
131         """Add Socket Memory configuration for node.
132
133         :param node: DUT node.
134         :param socketmem_config: Socket Memory configuration option,
135         as a string.
136         :type node: dict
137         :type socketmem_config: str
138         :return: nothing
139         """
140         if node['type'] != NodeType.DUT:
141             raise ValueError('Node type is not a DUT')
142         hostname = Topology.get_node_hostname(node)
143         if hostname not in self._nodeconfig:
144             self._nodeconfig[hostname] = {}
145         self._nodeconfig[hostname]['socketmem_config'] = socketmem_config
146         logger.debug('Setting hostname {} Socket Memory config to {}'.
147                      format(hostname, socketmem_config))
148
149     def add_heapsize_config(self, node, heapsize_config):
150         """Add Heap Size configuration for node.
151
152         :param node: DUT node.
153         :param heapsize_config: Heap Size configuration, as a string.
154         :type node: dict
155         :type heapsize_config: str
156         :return: nothing
157         """
158         if node['type'] != NodeType.DUT:
159             raise ValueError('Node type is not a DUT')
160         hostname = Topology.get_node_hostname(node)
161         if hostname not in self._nodeconfig:
162             self._nodeconfig[hostname] = {}
163         self._nodeconfig[hostname]['heapsize_config'] = heapsize_config
164         logger.debug('Setting hostname {} Heap Size config to {}'.
165                      format(hostname, heapsize_config))
166
167     def add_rss_config(self, node, rss_config):
168         """Add RSS configuration for node.
169
170         :param node: DUT node.
171         :param rss_config: RSS configuration, as a string.
172         :type node: dict
173         :type rss_config: str
174         :return: nothing
175         """
176         if node['type'] != NodeType.DUT:
177             raise ValueError('Node type is not a DUT')
178         hostname = Topology.get_node_hostname(node)
179         if not hostname in self._nodeconfig:
180             self._nodeconfig[hostname] = {}
181         if not 'rss_config' in self._nodeconfig[hostname]:
182             self._nodeconfig[hostname]['rss_config'] = []
183         self._nodeconfig[hostname]['rss_config'].append(rss_config)
184         logger.debug('Setting hostname {} RSS config to {}'.\
185             format(hostname, rss_config))
186
187     def add_max_tx_queues_config(self, node, max_tx_queues_config):
188         """Add Max TX Queues configuration for node.
189
190         :param node: DUT node.
191         :param max_tx_queues_config: Max TX Queues configuration, as a string.
192         :type node: dict
193         :type max_tx_queues_config: str
194         :return: nothing
195         """
196         if node['type'] != NodeType.DUT:
197             raise ValueError('Node type is not a DUT')
198         hostname = Topology.get_node_hostname(node)
199         if not hostname in self._nodeconfig:
200             self._nodeconfig[hostname] = {}
201         if not 'max_tx_queues_config' in self._nodeconfig[hostname]:
202             self._nodeconfig[hostname]['max_tx_queues_config'] = []
203         self._nodeconfig[hostname]['max_tx_queues_config'].append(
204             max_tx_queues_config)
205         logger.debug('Setting hostname {} max_tx_queues config to {}'.\
206             format(hostname, max_tx_queues_config))
207
208     def add_no_multi_seg_config(self, node):
209         """Add No Multi Seg configuration for node.
210
211         :param node: DUT node.
212         :type node: dict
213         :return: nothing
214         """
215         if node['type'] != NodeType.DUT:
216             raise ValueError('Node type is not a DUT')
217         hostname = Topology.get_node_hostname(node)
218         if not hostname in self._nodeconfig:
219             self._nodeconfig[hostname] = {}
220         if not 'no_multi_seg_config' in self._nodeconfig[hostname]:
221             self._nodeconfig[hostname]['no_multi_seg_config'] = []
222         self._nodeconfig[hostname]['no_multi_seg_config'].append(
223             "no-multi-seg")
224         logger.debug('Setting hostname {} config with {}'.\
225             format(hostname, "no-multi-seg"))
226
227     def remove_all_pci_devices(self, node):
228         """Remove PCI device configuration from node.
229
230         :param node: DUT node.
231         :type node: dict
232         :return: nothing
233         """
234         if node['type'] != NodeType.DUT:
235             raise ValueError('Node type is not a DUT')
236         hostname = Topology.get_node_hostname(node)
237         if hostname in self._nodeconfig:
238             self._nodeconfig[hostname]['pci_addrs'] = []
239         logger.debug('Clearing all PCI devices for hostname {}.'.
240                      format(hostname))
241
242     def remove_all_cpu_config(self, node):
243         """Remove CPU configuration from node.
244
245         :param node: DUT node.
246         :type node: dict
247         :return: nothing
248         """
249         if node['type'] != NodeType.DUT:
250             raise ValueError('Node type is not a DUT')
251         hostname = Topology.get_node_hostname(node)
252         if hostname in self._nodeconfig:
253             self._nodeconfig[hostname]['cpu_config'] = []
254         logger.debug('Clearing all CPU config for hostname {}.'.
255                      format(hostname))
256
257     def remove_socketmem_config(self, node):
258         """Remove Socket Memory configuration from node.
259
260         :param node: DUT node.
261         :type node: dict
262         :return: nothing
263         """
264         if node['type'] != NodeType.DUT:
265             raise ValueError('Node type is not a DUT')
266         hostname = Topology.get_node_hostname(node)
267         if hostname in self._nodeconfig:
268             self._nodeconfig[hostname].pop('socketmem_config', None)
269         logger.debug('Clearing Socket Memory config for hostname {}.'.
270                      format(hostname))
271
272     def remove_heapsize_config(self, node):
273         """Remove Heap Size configuration from node.
274
275         :param node: DUT node.
276         :type node: dict
277         :return: nothing
278         """
279         if node['type'] != NodeType.DUT:
280             raise ValueError('Node type is not a DUT')
281         hostname = Topology.get_node_hostname(node)
282         if hostname in self._nodeconfig:
283             self._nodeconfig[hostname].pop('heapsize_config', None)
284         logger.debug('Clearing Heap Size config for hostname {}.'.
285                      format(hostname))
286
287     def remove_rss_config(self, node):
288         """Remove RSS configuration from node.
289
290         :param node: DUT node.
291         :type node: dict
292         :return: nothing
293         """
294         if node['type'] != NodeType.DUT:
295             raise ValueError('Node type is not a DUT')
296         hostname = Topology.get_node_hostname(node)
297         if hostname in self._nodeconfig:
298             self._nodeconfig[hostname]['rss_config'] = []
299         logger.debug('Clearing RSS config for hostname {}.'.\
300             format(hostname))
301
302     def remove_max_tx_queues_config(self, node):
303         """Remove Max TX Queues configuration from node.
304
305         :param node: DUT node.
306         :type node: dict
307         :return: nothing
308         """
309         if node['type'] != NodeType.DUT:
310             raise ValueError('Node type is not a DUT')
311         hostname = Topology.get_node_hostname(node)
312         if hostname in self._nodeconfig:
313             self._nodeconfig[hostname]['max_tx_queues_config'] = []
314         logger.debug('Clearing Max TX Queues config for hostname {}.'.\
315             format(hostname))
316
317     def remove_no_multi_seg_config(self, node):
318         """Remove No Multi Seg configuration from node.
319
320         :param node: DUT node.
321         :type node: dict
322         :return: nothing
323         """
324         if node['type'] != NodeType.DUT:
325             raise ValueError('Node type is not a DUT')
326         hostname = Topology.get_node_hostname(node)
327         if hostname in self._nodeconfig:
328             self._nodeconfig[hostname]['no_multi_seg_config'] = []
329         logger.debug('Clearing No Multi Seg config for hostname {}.'.\
330             format(hostname))
331
332     def apply_config(self, node, waittime=5, retries=12):
333         """Generate and apply VPP configuration for node.
334
335         Use data from calls to this class to form a startup.conf file and
336         replace /etc/vpp/startup.conf with it on node.
337
338         :param node: DUT node.
339         :param waittime: Time to wait for VPP to restart (default 5 seconds).
340         :param retries: Number of times (default 12) to re-try waiting.
341         :type node: dict
342         :type waittime: int
343         :type retries: int
344         """
345
346         if node['type'] != NodeType.DUT:
347             raise ValueError('Node type is not a DUT')
348         hostname = Topology.get_node_hostname(node)
349
350         cpuconfig = ""
351         pciconfig = ""
352         socketmemconfig = DEFAULT_SOCKETMEM_CONFIG
353         heapsizeconfig = ""
354         rssconfig = ""
355         txqueuesconfig = ""
356         nomultiseg = ""
357
358         if hostname in self._nodeconfig:
359             cfg = self._nodeconfig[hostname]
360             if 'cpu_config' in cfg:
361                 cpuconfig = "  " + "\n  ".join(cfg['cpu_config'])
362
363             if 'pci_addrs' in cfg:
364                 pciconfig = "  dev " + "\n  dev ".join(cfg['pci_addrs'])
365
366             if 'socketmem_config' in cfg:
367                 socketmemconfig = cfg['socketmem_config']
368
369             if 'heapsize_config' in cfg:
370                 heapsizeconfig = "\nheapsize {}\n".\
371                     format(cfg['heapsize_config'])
372
373             if 'rss_config' in cfg:
374                 rssconfig = "  " + "\n  ".join(cfg['rss_config'])
375
376             if 'max_tx_queues_config' in cfg:
377                 txqueuesconfig = "  " + "\n  ".join(
378                     cfg['max_tx_queues_config'])
379
380             if 'no_multi_seg_config' in cfg:
381                 nomultiseg = "  " + "\n  ".join(cfg['no_multi_seg_config'])
382
383         vppconfig = VPP_CONFIG_TEMPLATE.format(cpuconfig=cpuconfig,
384                                                pciconfig=pciconfig,
385                                                socketmemconfig=socketmemconfig,
386                                                heapsizeconfig=heapsizeconfig,
387                                                rssconfig=rssconfig,
388                                                txqueuesconfig=txqueuesconfig,
389                                                nomultiseg = nomultiseg)
390
391         logger.debug('Writing VPP config to host {}: "{}"'.format(hostname,
392                                                                   vppconfig))
393
394         ssh = SSH()
395         ssh.connect(node)
396
397         # We're using this "| sudo tee" construct because redirecting
398         # a sudo'd outut ("sudo echo xxx > /path/to/file") does not
399         # work on most platforms...
400         (ret, stdout, stderr) = \
401             ssh.exec_command('echo "{0}" | sudo tee {1}'.
402                              format(vppconfig, VPP_CONFIG_FILENAME))
403
404         if ret != 0:
405             logger.debug('Writing config file failed to node {}'.
406                          format(hostname))
407             logger.debug('stdout: {}'.format(stdout))
408             logger.debug('stderr: {}'.format(stderr))
409             raise RuntimeError('Writing config file failed to node {}'.
410                                format(hostname))
411
412         # Instead of restarting, we'll do separate start and stop
413         # actions. This way we don't care whether VPP was running
414         # to begin with.
415         ssh.exec_command('sudo initctl stop {}'.format(VPP_SERVICE_NAME))
416         (ret, stdout, stderr) = \
417             ssh.exec_command('sudo initctl start {}'.format(VPP_SERVICE_NAME))
418         if ret != 0:
419             logger.debug('Restarting VPP failed on node {}'.
420                          format(hostname))
421             logger.debug('stdout: {}'.format(stdout))
422             logger.debug('stderr: {}'.format(stderr))
423             raise RuntimeError('Restarting VPP failed on node {}'.
424                                format(hostname))
425
426         # Sleep <waittime> seconds, up to <retry> times,
427         # and verify if VPP is running.
428         vpp_is_running = False
429         retries_left = retries
430         while (not vpp_is_running) and (retries_left > 0):
431             time.sleep(waittime)
432             retries_left -= 1
433
434             # FIXME: Need to find a good way to check if VPP is operational.
435             #
436             # If VatTerminal/VatExecutor is anything like vppctl or
437             # vpp_api_test, then in case VPP is NOT running it will block for
438             # 30 seconds or so and not even return if VPP becomes alive during
439             # that time. This makes it unsuitable in this case. We either need
440             # a call that returns immediately, indicating whether VPP is
441             # healthy or not, or a call that waits (up to a defined length
442             # of time) and returns immediately if VPP is or becomes healthy.
443             (ret, stdout, stderr) = \
444                 ssh.exec_command('echo show hardware-interfaces | '
445                                  'nc 0 5002')
446
447             if ret == 0:
448                 vpp_is_running = True
449             else:
450                 logger.debug('VPP not yet running, {} retries left'.
451                              format(retries_left))
452         if retries_left == 0:
453             raise RuntimeError('VPP failed to restart on node {}'.
454                                format(hostname))
455         logger.debug('VPP interfaces found on node {}'.
456                      format(stdout))