Merge "vpp: Initial support of VPP vSwitch"
[vswitchperf.git] / vswitches / vpp_dpdk_vhost.py
1 # Copyright 2017 Intel Corporation.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #   http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """VSPERF VPP implementation using DPDK and vhostuser vports
16 """
17
18 import logging
19 import os
20 import copy
21 import re
22 import pexpect
23
24 from src.dpdk import dpdk
25 from conf import settings as S
26 from vswitches.vswitch import IVSwitch
27 from tools import tasks
28
29 # pylint: disable=too-many-public-methods
30 class VppDpdkVhost(IVSwitch, tasks.Process):
31     """ VPP with DPDK support
32     """
33     _proc_name = 'vpp'
34     _bridge_idx_counter = 100
35
36     def __init__(self):
37         """See IVswitch for general description
38         """
39         self._logfile = os.path.join(S.getValue('LOG_DIR'),
40                                      S.getValue('LOG_FILE_VPP'))
41         self._logger = logging.getLogger(__name__)
42         self._expect = r'vpp#'
43         self._timeout = 30
44         self._vswitch_args = []
45         self._cmd = []
46         self._cmd_template = ['sudo', '-E', S.getValue('TOOLS')['vpp']]
47         self._stamp = None
48         self._logger = logging.getLogger(__name__)
49         self._phy_ports = []
50         self._virt_ports = []
51         self._switches = {}
52
53         # configure DPDK NICs
54         tmp_args = copy.deepcopy(S.getValue('VSWITCH_VPP_ARGS'))
55         if 'dpdk' not in tmp_args:
56             tmp_args['dpdk'] = []
57
58         # override socket-mem settings
59         for tmp_arg in tmp_args['dpdk']:
60             if tmp_arg.startswith('socket-mem'):
61                 tmp_args['dpdk'].remove(tmp_arg)
62         tmp_args['dpdk'].append('socket-mem ' +
63                                 ','.join(S.getValue('DPDK_SOCKET_MEM')))
64
65         for nic in S.getValue('NICS'):
66             tmp_args['dpdk'].append("dev {}".format(nic['pci']))
67         self._vswitch_args = self._process_vpp_args(tmp_args)
68
69     def _get_nic_info(self, key='Name'):
70         """Read NIC info from VPP and return NIC details in a dictionary
71            indexed by given ``key``
72
73         :param key: Name of the key to be used for indexing result dictionary
74
75         :returns: Dictionary with NIC infos including their PCI addresses
76         """
77         result = {}
78         output = self.run_vppctl(['show', 'hardware', 'brief'])
79         # parse output and store basic info about NICS
80         ifaces = output[0].split('\n')
81         keys = ifaces[0].split()
82         keys.append('Pci')
83         keyidx = keys.index(key)
84         for iface in ifaces[1:]:
85             tmpif = iface.split()
86             # get PCI address of given interface
87             output = self.run_vppctl(['show', 'hardware', tmpif[1], 'detail'])
88             match = re.search(r'pci address:\s*([\d:\.]+)', output[0])
89             if match:
90                 # normalize PCI address, e.g. 0000:05:10.01 => 0000:05:10.1
91                 tmp_pci = match.group(1).split('.')
92                 tmp_pci[1] = str(int(tmp_pci[1]))
93                 tmpif.append('.'.join(tmp_pci))
94             else:
95                 tmpif.append(None)
96             # store only NICs with reasonable index
97             if tmpif[keyidx] is not None:
98                 result[tmpif[keyidx]] = dict(zip(keys, tmpif))
99
100         return result
101
102     def _process_vpp_args(self, args):
103         """Produce VPP CLI args from input dictionary ``args``
104         """
105         cli_args = []
106         for cfg_key in args:
107             cli_args.append(cfg_key)
108             cli_args.append("{{ {} }}".format(' '.join(args[cfg_key])))
109
110         self._logger.debug("VPP CLI args: %s", cli_args)
111         return cli_args
112
113
114     def start(self):
115         """Activates DPDK kernel modules and starts VPP
116
117         :raises: pexpect.EOF, pexpect.TIMEOUT
118         """
119         dpdk.init()
120         self._logger.info("Starting VPP...")
121
122         self._cmd = self._cmd_template + self._vswitch_args
123
124         try:
125             tasks.Process.start(self)
126             self.relinquish()
127         except (pexpect.EOF, pexpect.TIMEOUT) as exc:
128             logging.error("Exception during VPP start.")
129             raise exc
130
131         self._logger.info("VPP...Started.")
132
133     def stop(self):
134         """See IVswitch for general description
135
136         Kills VPP and removes DPDK kernel modules.
137         """
138         self._logger.info("Terminating VPP...")
139         self.kill()
140         self._logger.info("VPP...Terminated.")
141         dpdk.cleanup()
142
143     def kill(self, signal='-15', sleep=10):
144         """See IVswitch for general description
145
146         Kills ``vpp``
147         """
148         # try to get VPP pid
149         output = self.run_vppctl(['show', 'version', 'verbose'])
150         match = re.search(r'Current PID:\s*([0-9]+)', output[0])
151         if match:
152             vpp_pid = match.group(1)
153             tasks.terminate_task(vpp_pid, logger=self._logger)
154
155         # in case, that pid was not detected or sudo envelope
156         # has not been terminated yet
157         tasks.Process.kill(self, signal, sleep)
158
159     def add_switch(self, switch_name, dummy_params=None):
160         """See IVswitch for general description
161         """
162         if switch_name in self._switches:
163             self._logger.warning("switch %s already exists...", switch_name)
164         else:
165             self._switches[switch_name] = self._bridge_idx_counter
166             self._bridge_idx_counter += 1
167
168     def del_switch(self, switch_name):
169         """See IVswitch for general description
170         """
171         if switch_name in self._switches:
172             del self._switches[switch_name]
173
174     def add_phy_port(self, dummy_switch_name):
175         """See IVswitch for general description
176         :raises: RuntimeError
177         """
178         # get list of physical interfaces with PCI addresses
179         vpp_nics = self._get_nic_info(key='Pci')
180         # check if there are any NICs left
181         if len(self._phy_ports) >= len(S.getValue('NICS')):
182             raise RuntimeError('All available NICs are already configured!')
183
184         nic = S.getValue('NICS')[len(self._phy_ports)]
185         if not nic['pci'] in vpp_nics:
186             raise RuntimeError('VPP cannot access nic with PCI address: {}'.format(nic['pci']))
187         nic_name = vpp_nics[nic['pci']]['Name']
188         self._phy_ports.append(nic_name)
189         self.run_vppctl(['set', 'int', 'state', nic_name, 'up'])
190         return (nic_name, vpp_nics[nic['pci']]['Idx'])
191
192     def add_vport(self, dummy_switch_name):
193         """See IVswitch for general description
194         """
195         socket_name = S.getValue('TOOLS')['ovs_var_tmp'] + 'dpdkvhostuser' + str(len(self._virt_ports))
196         output = self.run_vppctl(['create', 'vhost-user', 'socket', socket_name, 'server'] +
197                                  S.getValue('VSWITCH_VPP_VHOSTUSER_ARGS'))
198         nic_name = output[0]
199         self._virt_ports.append(nic_name)
200         self.run_vppctl(['set', 'int', 'state', nic_name, 'up'])
201         return (nic_name, None)
202
203     def del_port(self, switch_name, port_name):
204         """See IVswitch for general description
205         """
206         if port_name in self._phy_ports:
207             self.run_vppctl(['set', 'int', 'state', port_name, 'down'])
208             self._phy_ports.remove(port_name)
209         elif port_name in self._virt_ports:
210             self.run_vppctl(['set', 'int', 'state', port_name, 'down'])
211             self.run_vppctl(['delete', 'vhost-user', port_name])
212             self._virt_ports.remove(port_name)
213         else:
214             self._logger.warning("Port %s is not configured.", port_name)
215
216     def add_l2patch(self, port1, port2, bidir=False):
217         """Create l2patch connection between given ports
218         """
219         self.run_vppctl(['test', 'l2patch', 'rx', port1, 'tx', port2])
220         if bidir:
221             self.run_vppctl(['test', 'l2patch', 'rx', port2, 'tx', port1])
222
223     def add_xconnect(self, port1, port2, bidir=False):
224         """Create l2patch connection between given ports
225         """
226         self.run_vppctl(['set', 'interface', 'l2', 'xconnect', port1, port2])
227         if bidir:
228             self.run_vppctl(['set', 'interface', 'l2', 'xconnect', port2, port1])
229
230     def add_bridge(self, switch_name, port1, port2, dummy_bidir=False):
231         """Add given ports to bridge ``switch_name``
232         """
233         self.run_vppctl(['set', 'interface', 'l2', 'bridge', port1,
234                          str(self._switches[switch_name])])
235         self.run_vppctl(['set', 'interface', 'l2', 'bridge', port2,
236                          str(self._switches[switch_name])])
237
238     def add_connection(self, switch_name, port1, port2, bidir=False):
239         """See IVswitch for general description
240
241         :raises: RuntimeError
242         """
243         mode = S.getValue('VSWITCH_VPP_L2_CONNECT_MODE')
244         if mode == 'l2patch':
245             self.add_l2patch(port1, port2, bidir)
246         elif mode == 'xconnect':
247             self.add_xconnect(port1, port2, bidir)
248         elif mode == 'bridge':
249             self.add_bridge(switch_name, port1, port2)
250         else:
251             raise RuntimeError('VPP: Unsupported l2 connection mode detected %s', mode)
252
253     def del_l2patch(self, port1, port2, bidir=False):
254         """Remove l2patch connection between given ports
255
256         :param port1: port to be used in connection
257         :param port2: port to be used in connection
258         :param bidir: switch between uni and bidirectional traffic
259         """
260         self.run_vppctl(['test', 'l2patch', 'rx', port1, 'tx', port2, 'del'])
261         if bidir:
262             self.run_vppctl(['test', 'l2patch', 'rx', port2, 'tx', port1, 'del'])
263
264     def del_xconnect(self, dummy_port1, dummy_port2, dummy_bidir=False):
265         """Remove xconnect connection between given ports
266         """
267         self._logger.warning('VPP: Removal of l2 xconnect is not implemented.')
268
269     def del_bridge(self, dummy_switch_name, dummy_port1, dummy_port2):
270         """Remove given ports from the bridge
271         """
272         self._logger.warning('VPP: Removal of interfaces from bridge is not implemented.')
273
274     def del_connection(self, switch_name, port1, port2, bidir=False):
275         """See IVswitch for general description
276
277         :raises: RuntimeError
278         """
279         mode = S.getValue('VSWITCH_VPP_L2_CONNECT_MODE')
280         if mode == 'l2patch':
281             self.del_l2patch(port1, port2, bidir)
282         elif mode == 'xconnect':
283             self.del_xconnect(port1, port2, bidir)
284         elif mode == 'bridge':
285             self.del_bridge(switch_name, port1, port2)
286         else:
287             raise RuntimeError('VPP: Unsupported l2 connection mode detected %s', mode)
288
289     def dump_l2patch(self):
290         """Dump l2patch connections
291         """
292         self.run_vppctl(['show', 'l2patch'])
293
294     def dump_xconnect(self):
295         """Dump l2 xconnect connections
296         """
297         self._logger.warning("VPP: Dump of l2 xconnections is not supported.")
298
299     def dump_bridge(self, switch_name):
300         """Show bridge details
301
302         :param switch_name: switch on which to operate
303         """
304         self.run_vppctl(['show', 'bridge-domain', str(self._switches[switch_name]), 'int'])
305
306     def dump_connections(self, switch_name):
307         """See IVswitch for general description
308
309         :raises: RuntimeError
310         """
311         mode = S.getValue('VSWITCH_VPP_L2_CONNECT_MODE')
312         if mode == 'l2patch':
313             self.dump_l2patch()
314         elif mode == 'xconnect':
315             self.dump_xconnect()
316         elif mode == 'bridge':
317             self.dump_bridge(switch_name)
318         else:
319             raise RuntimeError('VPP: Unsupported l2 connection mode detected %s', mode)
320
321     def run_vppctl(self, args, check_error=False):
322         """Run ``vppctl`` with supplied arguments.
323
324         :param args: Arguments to pass to ``vppctl``
325         :param check_error: Throw exception on error
326
327         :return: None
328         """
329         cmd = ['sudo', S.getValue('TOOLS')['vppctl']] + args
330         return tasks.run_task(cmd, self._logger, 'Running vppctl...', check_error)
331
332     #
333     # Validate methods
334     #
335     def validate_add_switch(self, dummy_result, switch_name, dummy_params=None):
336         """Validate - Create a new logical switch with no ports
337         """
338         return switch_name in self._switches
339
340     def validate_del_switch(self, dummy_result, switch_name):
341         """Validate removal of switch
342         """
343         return not self.validate_add_switch(dummy_result, switch_name)
344
345     def validate_add_phy_port(self, result, dummy_switch_name):
346         """ Validate that physical port was added to bridge.
347         """
348         return result[0] in self._phy_ports
349
350     def validate_add_vport(self, result, dummy_switch_name):
351         """ Validate that virtual port was added to bridge.
352         """
353         return result[0] in self._virt_ports
354
355     def validate_del_port(self, dummy_result, dummy_switch_name, port_name):
356         """ Validate that port_name was removed from bridge.
357         """
358         return not (port_name in self._phy_ports or port_name in self._virt_ports)
359
360     # pylint: disable=no-self-use
361     def validate_run_vppctl(self, result, dummy_args, dummy_check_error=False):
362         """validate execution of ``vppctl`` with supplied arguments.
363         """
364         # there shouldn't be any stderr
365         return not result[1]
366
367     #
368     # Non implemented methods
369     #
370     def add_flow(self, switch_name, flow, cache='off'):
371         """See IVswitch for general description
372         """
373         raise NotImplementedError()
374
375     def del_flow(self, switch_name, flow=None):
376         """See IVswitch for general description
377         """
378         raise NotImplementedError()
379
380     def dump_flows(self, switch_name):
381         """See IVswitch for general description
382         """
383         raise NotImplementedError()
384
385     def add_route(self, switch_name, network, destination):
386         """See IVswitch for general description
387         """
388         raise NotImplementedError()
389
390     def set_tunnel_arp(self, ip_addr, mac_addr, switch_name):
391         """See IVswitch for general description
392         """
393         raise NotImplementedError()
394
395     def add_tunnel_port(self, switch_name, remote_ip, tunnel_type='vxlan', params=None):
396         """See IVswitch for general description
397         """
398         raise NotImplementedError()
399
400     def get_ports(self, switch_name):
401         """See IVswitch for general description
402         """
403         raise NotImplementedError()