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