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