Kubernetes: Infrastructure For K8S Net testing.
[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             if 'Link' in iface or 'local' in iface:
108                 continue
109             # get PCI address of given interface
110             output = self.run_vppctl(['show', 'hardware', tmpif[1], 'detail'])
111             lines = output[0].split('\n')
112             #match = re.search(r'pci address:\s*([\d:\.]+)', output[0])
113             match = ''
114             for line in lines:
115                 if "pci:" in line:
116                     match = line.split(' ')[6]
117             if match:
118                 # normalize PCI address, e.g. 0000:05:10.01 => 0000:05:10.1
119                 tmp_pci = match.split('.')
120                 # tmp_pci = match.group(1).split('.')
121                 tmp_pci[1] = str(int(tmp_pci[1]))
122                 tmpif.append('.'.join(tmp_pci))
123             else:
124                 tmpif.append(None)
125             # store only NICs with reasonable index
126             if tmpif[keyidx] is not None:
127                 result[tmpif[keyidx]] = dict(zip(keys, tmpif))
128
129         return result
130
131     def _process_vpp_args(self, args):
132         """Produce VPP CLI args from input dictionary ``args``
133         """
134         cli_args = []
135         for cfg_key in args:
136             cli_args.append(cfg_key)
137             if isinstance(args[cfg_key], str):
138                 cli_args.append(args[cfg_key])
139             else:
140                 cli_args.append("{{ {} }}".format(' '.join(args[cfg_key])))
141
142         self._logger.debug("VPP CLI args: %s", cli_args)
143         return cli_args
144
145     def start(self):
146         """Activates DPDK kernel modules and starts VPP
147
148         :raises: pexpect.EOF, pexpect.TIMEOUT
149         """
150         dpdk.init()
151         self._logger.info("Starting VPP...")
152
153         self._cmd = self._cmd_template + self._vswitch_args
154
155         try:
156             tasks.Process.start(self)
157             self.relinquish()
158         except (pexpect.EOF, pexpect.TIMEOUT) as exc:
159             self._logger.error("Exception during VPP start.")
160             raise exc
161
162         self._logger.info("VPP...Started.")
163
164     def stop(self):
165         """See IVswitch for general description
166
167         Kills VPP and removes DPDK kernel modules.
168         """
169         self._logger.info("Terminating VPP...")
170         self.kill()
171         self._logger.info("VPP...Terminated.")
172         dpdk.cleanup()
173
174     def kill(self, signal='-15', sleep=10):
175         """See IVswitch for general description
176
177         Kills ``vpp``
178         """
179         if self.is_running():
180             # try to get VPP pid
181             output = self.run_vppctl(['show', 'version', 'verbose'])
182             match = re.search(r'Current PID:\s*([0-9]+)', output[0])
183             if match:
184                 vpp_pid = match.group(1)
185                 tasks.terminate_task(vpp_pid, logger=self._logger)
186
187             # in case, that pid was not detected or sudo envelope
188             # has not been terminated yet
189             tasks.Process.kill(self, signal, sleep)
190
191     def get_version(self):
192         """See IVswitch for general description
193         """
194         versions = []
195         output = self.run_vppctl(['show', 'version', 'verbose'])
196         if output[1]:
197             self._logger.warning("VPP version can not be read!")
198             return versions
199
200         match = re.search(r'Version:\s*(.+)', output[0])
201         if match:
202             versions.append(Version(S.getValue('VSWITCH'), match.group(1)))
203
204         match = re.search(r'DPDK Version:\s*DPDK (.+)', output[0])
205         if match:
206             versions.append(Version('dpdk', match.group(1)))
207
208         return versions
209
210     def add_switch(self, switch_name, dummy_params=None):
211         """See IVswitch for general description
212         """
213         # pylint: disable=unused-argument
214         if switch_name in self._switches:
215             self._logger.warning("switch %s already exists...", switch_name)
216         else:
217             self._switches[switch_name] = self._bridge_idx_counter
218             self._bridge_idx_counter += 1
219
220     def del_switch(self, switch_name):
221         """See IVswitch for general description
222         """
223         if switch_name in self._switches:
224             del self._switches[switch_name]
225
226     def add_phy_port(self, dummy_switch_name):
227         """See IVswitch for general description
228         :raises: RuntimeError
229         """
230         # pylint: disable=unused-argument
231         # get list of physical interfaces with PCI addresses
232         vpp_nics = self._get_nic_info(key='Pci')
233         # check if there are any NICs left
234         if len(self._phy_ports) >= len(S.getValue('NICS')):
235             raise RuntimeError("Can't add phy port! There are only {} ports defined "
236                                "by WHITELIST_NICS parameter!".format(len(S.getValue('NICS'))))
237
238         nic = S.getValue('NICS')[len(self._phy_ports)]
239         if not nic['pci'] in vpp_nics:
240             raise RuntimeError('VPP cannot access nic with PCI address: {}'.format(nic['pci']))
241         nic_name = vpp_nics[nic['pci']]['Name']
242         self._phy_ports.append(nic_name)
243         self.run_vppctl(['set', 'int', 'state', nic_name, 'up'])
244         return (nic_name, vpp_nics[nic['pci']]['Idx'])
245
246     def add_vport(self, dummy_switch_name):
247         """See IVswitch for general description
248         """
249         # pylint: disable=unused-argument
250         socket_name = S.getValue('TOOLS')['ovs_var_tmp'] + 'dpdkvhostuser' + str(len(self._virt_ports))
251         if S.getValue('VSWITCH_VHOSTUSER_SERVER_MODE'):
252             mode = ['server']
253         else:
254             mode = []
255         output = self.run_vppctl(['create', 'vhost-user', 'socket', socket_name] + mode +
256                                  S.getValue('VSWITCH_VPP_VHOSTUSER_ARGS'))
257         if output[0].find('returned') >= 0:
258             raise RuntimeError('VPP VhostUser interface cannot be created.')
259         nic_name = output[0].strip()
260         self._virt_ports.append(nic_name)
261         self.run_vppctl(['set', 'int', 'state', nic_name, 'up'])
262         return (nic_name, None)
263
264     def del_port(self, switch_name, port_name):
265         """See IVswitch for general description
266         """
267         if port_name in self._phy_ports:
268             self.run_vppctl(['set', 'int', 'state', port_name, 'down'])
269             self._phy_ports.remove(port_name)
270         elif port_name in self._virt_ports:
271             self.run_vppctl(['set', 'int', 'state', port_name, 'down'])
272             self.run_vppctl(['delete', 'vhost-user', port_name])
273             self._virt_ports.remove(port_name)
274         else:
275             self._logger.warning("Port %s is not configured.", port_name)
276
277     def add_l2patch(self, port1, port2):
278         """Create l2patch connection between given ports
279         """
280         self.run_vppctl(['test', 'l2patch', 'rx', port1, 'tx', port2])
281
282     def add_xconnect(self, port1, port2):
283         """Create l2patch connection between given ports
284         """
285         self.run_vppctl(['set', 'interface', 'l2', 'xconnect', port1, port2])
286
287     def add_bridge(self, switch_name, port1, port2):
288         """Add given ports to bridge ``switch_name``
289         """
290         self.run_vppctl(['set', 'interface', 'l2', 'bridge', port1,
291                          str(self._switches[switch_name])])
292         self.run_vppctl(['set', 'interface', 'l2', 'bridge', port2,
293                          str(self._switches[switch_name])])
294
295     def add_connection(self, switch_name, port1, port2, traffic=None):
296         """See IVswitch for general description
297
298         :raises: RuntimeError
299         """
300         if traffic:
301             self._logger.warning("VPP add_connection() does not support 'traffic' options.")
302
303         mode = S.getValue('VSWITCH_VPP_L2_CONNECT_MODE')
304         if mode == 'l2patch':
305             self.add_l2patch(port1, port2)
306         elif mode == 'xconnect':
307             self.add_xconnect(port1, port2)
308         elif mode == 'bridge':
309             self.add_bridge(switch_name, port1, port2)
310         else:
311             raise RuntimeError('VPP: Unsupported l2 connection mode detected %s' % mode)
312
313     def del_l2patch(self, port1, port2):
314         """Remove l2patch connection between given ports
315
316         :param port1: port to be used in connection
317         :param port2: port to be used in connection
318         """
319         self.run_vppctl(['test', 'l2patch', 'rx', port1, 'tx', port2, 'del'])
320
321     def del_xconnect(self, port1, port2):
322         """Remove xconnect connection between given ports
323         """
324         self.run_vppctl(['set', 'interface', 'l3', port1])
325         self.run_vppctl(['set', 'interface', 'l3', port2])
326
327     def del_bridge(self, _dummy_switch_name, port1, port2):
328         """Remove given ports from the bridge
329         """
330         self.run_vppctl(['set', 'interface', 'l3', port1])
331         self.run_vppctl(['set', 'interface', 'l3', port2])
332
333     def del_connection(self, switch_name, port1=None, port2=None):
334         """See IVswitch for general description
335
336         :raises: RuntimeError
337         """
338         if port1 and port2:
339             mode = S.getValue('VSWITCH_VPP_L2_CONNECT_MODE')
340             if mode == 'l2patch':
341                 self.del_l2patch(port1, port2)
342             elif mode == 'xconnect':
343                 self.del_xconnect(port1, port2)
344             elif mode == 'bridge':
345                 self.del_bridge(switch_name, port1, port2)
346             else:
347                 raise RuntimeError('VPP: Unsupported l2 connection mode detected %s' % mode)
348
349     def dump_l2patch(self):
350         """Dump l2patch connections
351         """
352         self.run_vppctl(['show', 'l2patch'])
353
354     def dump_xconnect(self):
355         """Dump l2 xconnect connections
356         """
357         self.run_vppctl(['show', 'mode'] + self._phy_ports + self._virt_ports)
358
359     def dump_bridge(self, switch_name):
360         """Show bridge details
361
362         :param switch_name: switch on which to operate
363         """
364         self.run_vppctl(['show', 'bridge-domain', str(self._switches[switch_name]), 'int'])
365
366     def dump_connections(self, switch_name):
367         """See IVswitch for general description
368
369         :raises: RuntimeError
370         """
371         mode = S.getValue('VSWITCH_VPP_L2_CONNECT_MODE')
372         if mode == 'l2patch':
373             self.dump_l2patch()
374         elif mode == 'xconnect':
375             self.dump_xconnect()
376         elif mode == 'bridge':
377             self.dump_bridge(switch_name)
378         else:
379             raise RuntimeError('VPP: Unsupported l2 connection mode detected %s' % mode)
380
381     def run_vppctl(self, args, check_error=False):
382         """Run ``vppctl`` with supplied arguments.
383
384         :param args: Arguments to pass to ``vppctl``
385         :param check_error: Throw exception on error
386
387         :return: None
388         """
389         cmd = self._vpp_ctl + args
390         return tasks.run_task(cmd, self._logger, 'Running vppctl...', check_error)
391
392     #
393     # Validate methods
394     #
395     def validate_add_switch(self, _dummy_result, switch_name, _dummy_params=None):
396         """Validate - Create a new logical switch with no ports
397         """
398         return switch_name in self._switches
399
400     def validate_del_switch(self, _dummy_result, switch_name):
401         """Validate removal of switch
402         """
403         return not self.validate_add_switch(_dummy_result, switch_name)
404
405     def validate_add_phy_port(self, result, _dummy_switch_name):
406         """ Validate that physical port was added to bridge.
407         """
408         return result[0] in self._phy_ports
409
410     def validate_add_vport(self, result, _dummy_switch_name):
411         """ Validate that virtual port was added to bridge.
412         """
413         return result[0] in self._virt_ports
414
415     def validate_del_port(self, _dummy_result, _dummy_switch_name, port_name):
416         """ Validate that port_name was removed from bridge.
417         """
418         return not (port_name in self._phy_ports or port_name in self._virt_ports)
419
420     # pylint: disable=no-self-use
421     def validate_add_connection(self, _dummy_result, _dummy_switch_name, _dummy_port1,
422                                 _dummy_port2, _dummy_traffic=None):
423         """ Validate that connection was added
424         """
425         return True
426
427     def validate_del_connection(self, _dummy_result, _dummy_switch_name, _dummy_port1,
428                                 _dummy_port2):
429         """ Validate that connection was deleted
430         """
431         return True
432
433     def validate_dump_connections(self, _dummy_result, _dummy_switch_name):
434         """ Validate dump connections call
435         """
436         return True
437
438     def validate_run_vppctl(self, result, _dummy_args, _dummy_check_error=False):
439         """validate execution of ``vppctl`` with supplied arguments.
440         """
441         # there shouldn't be any stderr
442         return not result[1]
443
444     #
445     # Non implemented methods
446     #
447     def add_route(self, switch_name, network, destination):
448         """See IVswitch for general description
449         """
450         raise NotImplementedError()
451
452     def set_tunnel_arp(self, ip_addr, mac_addr, switch_name):
453         """See IVswitch for general description
454         """
455         raise NotImplementedError()
456
457     def add_tunnel_port(self, switch_name, remote_ip, tunnel_type='vxlan', params=None):
458         """See IVswitch for general description
459         """
460         raise NotImplementedError()
461
462     def get_ports(self, switch_name):
463         """See IVswitch for general description
464         """
465         return self._phy_ports