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