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