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