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