5dfc314dbf856ea46eeb6dc6c7c61810f51f85f5
[vswitchperf.git] / vswitches / vpp_dpdk_vhost.py
1 # Copyright 2017-2018 Intel Corporation., Tieto
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 os
19 import copy
20 import re
21 import pexpect
22
23 from src.dpdk import dpdk
24 from conf import settings as S
25 from vswitches.vswitch import IVSwitch
26 from tools import tasks
27 from tools.version import Version
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         super().__init__()
40         name, ext = os.path.splitext(S.getValue('LOG_FILE_VPP'))
41         rename_vpplf = "{name}_{uid}{ex}".format(name=name,
42                                                  uid=S.getValue(
43                                                      'LOG_TIMESTAMP'),
44                                                  ex=ext)
45         self._logfile = os.path.join(S.getValue('RESULTS_PATH'), rename_vpplf)
46         self._expect = r'vpp#'
47         self._cmd_template = ['sudo', '-E', S.getValue('TOOLS')['vpp']]
48         self._phy_ports = []
49         self._virt_ports = []
50         self._vpp_ctl = ['sudo', S.getValue('TOOLS')['vppctl']]
51
52         # configure DPDK NICs
53         tmp_args = copy.deepcopy(S.getValue('VSWITCH_VPP_ARGS'))
54         if 'dpdk' not in tmp_args:
55             tmp_args['dpdk'] = []
56
57         # override socket-mem settings
58         for tmp_arg in tmp_args['dpdk']:
59             if tmp_arg.startswith('socket-mem'):
60                 tmp_args['dpdk'].remove(tmp_arg)
61         tmp_args['dpdk'].append('socket-mem ' +
62                                 ','.join(S.getValue('DPDK_SOCKET_MEM')))
63
64         # create directory for vhostuser sockets if needed
65         if not os.path.exists(S.getValue('TOOLS')['ovs_var_tmp']):
66             tasks.run_task(['sudo', 'mkdir', '-p',
67                             S.getValue('TOOLS')['ovs_var_tmp']], self._logger)
68
69         # configure path to the plugins
70         tmp_args['plugin_path'] = S.getValue('TOOLS')['vpp_plugin_path']
71
72         # cli sock file must be used for VPP 17.10 and newer
73         if S.getValue('VSWITCH_VPP_CLI_SOCK'):
74             self._vpp_ctl += ['-s', S.getValue('VSWITCH_VPP_CLI_SOCK')]
75             tmp_args['unix'].append('cli-listen {}'.format(
76                 S.getValue('VSWITCH_VPP_CLI_SOCK')))
77
78         mqs = int(S.getValue('VSWITCH_DPDK_MULTI_QUEUES'))
79         tmp_rxqs = ''
80         if mqs:
81             tmp_rxqs = " {{ num-rx-queues {} }}".format(mqs)
82
83         # configure physical ports
84         for nic in S.getValue('NICS'):
85             tmp_args['dpdk'].append("dev {}{}".format(nic['pci'], tmp_rxqs))
86         self._vswitch_args = self._process_vpp_args(tmp_args)
87
88     def _get_nic_info(self, key='Name'):
89         """Read NIC info from VPP and return NIC details in a dictionary
90            indexed by given ``key``
91
92         :param key: Name of the key to be used for indexing result dictionary
93
94         :returns: Dictionary with NIC infos including their PCI addresses
95         """
96         result = {}
97         output = self.run_vppctl(['show', 'hardware', 'brief'])
98         # parse output and store basic info about NICS
99         ifaces = output[0].split('\n')
100         keys = ifaces[0].split()
101         keys.append('Pci')
102         keyidx = keys.index(key)
103         for iface in ifaces[1:]:
104             tmpif = iface.split()
105             if not tmpif:
106                 continue
107             # get PCI address of given interface
108             output = self.run_vppctl(['show', 'hardware', tmpif[1], 'detail'])
109             match = re.search(r'pci address:\s*([\d:\.]+)', output[0])
110             if match:
111                 # normalize PCI address, e.g. 0000:05:10.01 => 0000:05:10.1
112                 tmp_pci = match.group(1).split('.')
113                 tmp_pci[1] = str(int(tmp_pci[1]))
114                 tmpif.append('.'.join(tmp_pci))
115             else:
116                 tmpif.append(None)
117             # store only NICs with reasonable index
118             if tmpif[keyidx] is not None:
119                 result[tmpif[keyidx]] = dict(zip(keys, tmpif))
120
121         return result
122
123     def _process_vpp_args(self, args):
124         """Produce VPP CLI args from input dictionary ``args``
125         """
126         cli_args = []
127         for cfg_key in args:
128             cli_args.append(cfg_key)
129             if isinstance(args[cfg_key], str):
130                 cli_args.append(args[cfg_key])
131             else:
132                 cli_args.append("{{ {} }}".format(' '.join(args[cfg_key])))
133
134         self._logger.debug("VPP CLI args: %s", cli_args)
135         return cli_args
136
137     def start(self):
138         """Activates DPDK kernel modules and starts VPP
139
140         :raises: pexpect.EOF, pexpect.TIMEOUT
141         """
142         dpdk.init()
143         self._logger.info("Starting VPP...")
144
145         self._cmd = self._cmd_template + self._vswitch_args
146
147         try:
148             tasks.Process.start(self)
149             self.relinquish()
150         except (pexpect.EOF, pexpect.TIMEOUT) as exc:
151             self._logger.error("Exception during VPP start.")
152             raise exc
153
154         self._logger.info("VPP...Started.")
155
156     def stop(self):
157         """See IVswitch for general description
158
159         Kills VPP and removes DPDK kernel modules.
160         """
161         self._logger.info("Terminating VPP...")
162         self.kill()
163         self._logger.info("VPP...Terminated.")
164         dpdk.cleanup()
165
166     def kill(self, signal='-15', sleep=10):
167         """See IVswitch for general description
168
169         Kills ``vpp``
170         """
171         if self.is_running():
172             # try to get VPP pid
173             output = self.run_vppctl(['show', 'version', 'verbose'])
174             match = re.search(r'Current PID:\s*([0-9]+)', output[0])
175             if match:
176                 vpp_pid = match.group(1)
177                 tasks.terminate_task(vpp_pid, logger=self._logger)
178
179             # in case, that pid was not detected or sudo envelope
180             # has not been terminated yet
181             tasks.Process.kill(self, signal, sleep)
182
183     def get_version(self):
184         """See IVswitch for general description
185         """
186         versions = []
187         output = self.run_vppctl(['show', 'version', 'verbose'])
188         if output[1]:
189             self._logger.warning("VPP version can not be read!")
190             return versions
191
192         match = re.search(r'Version:\s*(.+)', output[0])
193         if match:
194             versions.append(Version(S.getValue('VSWITCH'), match.group(1)))
195
196         match = re.search(r'DPDK Version:\s*DPDK (.+)', output[0])
197         if match:
198             versions.append(Version('dpdk', match.group(1)))
199
200         return versions
201
202     def add_switch(self, switch_name, dummy_params=None):
203         """See IVswitch for general description
204         """
205         # pylint: disable=unused-argument
206         if switch_name in self._switches:
207             self._logger.warning("switch %s already exists...", switch_name)
208         else:
209             self._switches[switch_name] = self._bridge_idx_counter
210             self._bridge_idx_counter += 1
211
212     def del_switch(self, switch_name):
213         """See IVswitch for general description
214         """
215         if switch_name in self._switches:
216             del self._switches[switch_name]
217
218     def add_phy_port(self, dummy_switch_name):
219         """See IVswitch for general description
220         :raises: RuntimeError
221         """
222         # pylint: disable=unused-argument
223         # get list of physical interfaces with PCI addresses
224         vpp_nics = self._get_nic_info(key='Pci')
225         # check if there are any NICs left
226         if len(self._phy_ports) >= len(S.getValue('NICS')):
227             raise RuntimeError("Can't add phy port! There are only {} ports defined "
228                                "by WHITELIST_NICS parameter!".format(len(S.getValue('NICS'))))
229
230         nic = S.getValue('NICS')[len(self._phy_ports)]
231         if not nic['pci'] in vpp_nics:
232             raise RuntimeError('VPP cannot access nic with PCI address: {}'.format(nic['pci']))
233         nic_name = vpp_nics[nic['pci']]['Name']
234         self._phy_ports.append(nic_name)
235         self.run_vppctl(['set', 'int', 'state', nic_name, 'up'])
236         return (nic_name, vpp_nics[nic['pci']]['Idx'])
237
238     def add_vport(self, dummy_switch_name):
239         """See IVswitch for general description
240         """
241         # pylint: disable=unused-argument
242         socket_name = S.getValue('TOOLS')['ovs_var_tmp'] + 'dpdkvhostuser' + str(len(self._virt_ports))
243         if S.getValue('VSWITCH_VHOSTUSER_SERVER_MODE'):
244             mode = ['server']
245         else:
246             mode = []
247         output = self.run_vppctl(['create', 'vhost-user', 'socket', socket_name] + mode +
248                                  S.getValue('VSWITCH_VPP_VHOSTUSER_ARGS'))
249         if output[0].find('returned') >= 0:
250             raise RuntimeError('VPP VhostUser interface cannot be created.')
251         nic_name = output[0].strip()
252         self._virt_ports.append(nic_name)
253         self.run_vppctl(['set', 'int', 'state', nic_name, 'up'])
254         return (nic_name, None)
255
256     def del_port(self, switch_name, port_name):
257         """See IVswitch for general description
258         """
259         if port_name in self._phy_ports:
260             self.run_vppctl(['set', 'int', 'state', port_name, 'down'])
261             self._phy_ports.remove(port_name)
262         elif port_name in self._virt_ports:
263             self.run_vppctl(['set', 'int', 'state', port_name, 'down'])
264             self.run_vppctl(['delete', 'vhost-user', port_name])
265             self._virt_ports.remove(port_name)
266         else:
267             self._logger.warning("Port %s is not configured.", port_name)
268
269     def add_l2patch(self, port1, port2):
270         """Create l2patch connection between given ports
271         """
272         self.run_vppctl(['test', 'l2patch', 'rx', port1, 'tx', port2])
273
274     def add_xconnect(self, port1, port2):
275         """Create l2patch connection between given ports
276         """
277         self.run_vppctl(['set', 'interface', 'l2', 'xconnect', port1, port2])
278
279     def add_bridge(self, switch_name, port1, port2):
280         """Add given ports to bridge ``switch_name``
281         """
282         self.run_vppctl(['set', 'interface', 'l2', 'bridge', port1,
283                          str(self._switches[switch_name])])
284         self.run_vppctl(['set', 'interface', 'l2', 'bridge', port2,
285                          str(self._switches[switch_name])])
286
287     def add_connection(self, switch_name, port1, port2, traffic=None):
288         """See IVswitch for general description
289
290         :raises: RuntimeError
291         """
292         if traffic:
293             self._logger.warning("VPP add_connection() does not support 'traffic' options.")
294
295         mode = S.getValue('VSWITCH_VPP_L2_CONNECT_MODE')
296         if mode == 'l2patch':
297             self.add_l2patch(port1, port2)
298         elif mode == 'xconnect':
299             self.add_xconnect(port1, port2)
300         elif mode == 'bridge':
301             self.add_bridge(switch_name, port1, port2)
302         else:
303             raise RuntimeError('VPP: Unsupported l2 connection mode detected %s' % mode)
304
305     def del_l2patch(self, port1, port2):
306         """Remove l2patch connection between given ports
307
308         :param port1: port to be used in connection
309         :param port2: port to be used in connection
310         """
311         self.run_vppctl(['test', 'l2patch', 'rx', port1, 'tx', port2, 'del'])
312
313     def del_xconnect(self, port1, port2):
314         """Remove xconnect connection between given ports
315         """
316         self.run_vppctl(['set', 'interface', 'l3', port1])
317         self.run_vppctl(['set', 'interface', 'l3', port2])
318
319     def del_bridge(self, _dummy_switch_name, port1, port2):
320         """Remove given ports from the bridge
321         """
322         self.run_vppctl(['set', 'interface', 'l3', port1])
323         self.run_vppctl(['set', 'interface', 'l3', port2])
324
325     def del_connection(self, switch_name, port1=None, port2=None):
326         """See IVswitch for general description
327
328         :raises: RuntimeError
329         """
330         if port1 and port2:
331             mode = S.getValue('VSWITCH_VPP_L2_CONNECT_MODE')
332             if mode == 'l2patch':
333                 self.del_l2patch(port1, port2)
334             elif mode == 'xconnect':
335                 self.del_xconnect(port1, port2)
336             elif mode == 'bridge':
337                 self.del_bridge(switch_name, port1, port2)
338             else:
339                 raise RuntimeError('VPP: Unsupported l2 connection mode detected %s' % mode)
340
341     def dump_l2patch(self):
342         """Dump l2patch connections
343         """
344         self.run_vppctl(['show', 'l2patch'])
345
346     def dump_xconnect(self):
347         """Dump l2 xconnect connections
348         """
349         self.run_vppctl(['show', 'mode'] + self._phy_ports + self._virt_ports)
350
351     def dump_bridge(self, switch_name):
352         """Show bridge details
353
354         :param switch_name: switch on which to operate
355         """
356         self.run_vppctl(['show', 'bridge-domain', str(self._switches[switch_name]), 'int'])
357
358     def dump_connections(self, switch_name):
359         """See IVswitch for general description
360
361         :raises: RuntimeError
362         """
363         mode = S.getValue('VSWITCH_VPP_L2_CONNECT_MODE')
364         if mode == 'l2patch':
365             self.dump_l2patch()
366         elif mode == 'xconnect':
367             self.dump_xconnect()
368         elif mode == 'bridge':
369             self.dump_bridge(switch_name)
370         else:
371             raise RuntimeError('VPP: Unsupported l2 connection mode detected %s' % mode)
372
373     def run_vppctl(self, args, check_error=False):
374         """Run ``vppctl`` with supplied arguments.
375
376         :param args: Arguments to pass to ``vppctl``
377         :param check_error: Throw exception on error
378
379         :return: None
380         """
381         cmd = self._vpp_ctl + args
382         return tasks.run_task(cmd, self._logger, 'Running vppctl...', check_error)
383
384     #
385     # Validate methods
386     #
387     def validate_add_switch(self, _dummy_result, switch_name, _dummy_params=None):
388         """Validate - Create a new logical switch with no ports
389         """
390         return switch_name in self._switches
391
392     def validate_del_switch(self, _dummy_result, switch_name):
393         """Validate removal of switch
394         """
395         return not self.validate_add_switch(_dummy_result, switch_name)
396
397     def validate_add_phy_port(self, result, _dummy_switch_name):
398         """ Validate that physical port was added to bridge.
399         """
400         return result[0] in self._phy_ports
401
402     def validate_add_vport(self, result, _dummy_switch_name):
403         """ Validate that virtual port was added to bridge.
404         """
405         return result[0] in self._virt_ports
406
407     def validate_del_port(self, _dummy_result, _dummy_switch_name, port_name):
408         """ Validate that port_name was removed from bridge.
409         """
410         return not (port_name in self._phy_ports or port_name in self._virt_ports)
411
412     # pylint: disable=no-self-use
413     def validate_add_connection(self, _dummy_result, _dummy_switch_name, _dummy_port1,
414                                 _dummy_port2, _dummy_traffic=None):
415         """ Validate that connection was added
416         """
417         return True
418
419     def validate_del_connection(self, _dummy_result, _dummy_switch_name, _dummy_port1,
420                                 _dummy_port2):
421         """ Validate that connection was deleted
422         """
423         return True
424
425     def validate_dump_connections(self, _dummy_result, _dummy_switch_name):
426         """ Validate dump connections call
427         """
428         return True
429
430     def validate_run_vppctl(self, result, _dummy_args, _dummy_check_error=False):
431         """validate execution of ``vppctl`` with supplied arguments.
432         """
433         # there shouldn't be any stderr
434         return not result[1]
435
436     #
437     # Non implemented methods
438     #
439     def add_route(self, switch_name, network, destination):
440         """See IVswitch for general description
441         """
442         raise NotImplementedError()
443
444     def set_tunnel_arp(self, ip_addr, mac_addr, switch_name):
445         """See IVswitch for general description
446         """
447         raise NotImplementedError()
448
449     def add_tunnel_port(self, switch_name, remote_ip, tunnel_type='vxlan', params=None):
450         """See IVswitch for general description
451         """
452         raise NotImplementedError()
453
454     def get_ports(self, switch_name):
455         """See IVswitch for general description
456         """
457         raise NotImplementedError()