integration: Tunneling protocols support update
[vswitchperf.git] / vswitches / ovs.py
1 # Copyright 2015-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 Open vSwitch base class
16 """
17
18 import logging
19 import os
20 import re
21 import time
22 import datetime
23 import random
24 import pexpect
25
26 from conf import settings
27 from src.ovs import OFBridge, flow_key, flow_match
28 from vswitches.vswitch import IVSwitch
29 from tools import tasks
30 from tools.module_manager import ModuleManager
31 # pylint: disable=too-many-public-methods
32 class IVSwitchOvs(IVSwitch, tasks.Process):
33     """Open vSwitch base class implementation
34
35     The method docstrings document only considerations specific to this
36     implementation. For generic information of the nature of the methods,
37     see the interface.
38     """
39     _proc_name = 'ovs-vswitchd'
40
41     def __init__(self):
42         """See IVswitch for general description
43         """
44         self._logfile = os.path.join(settings.getValue('LOG_DIR'),
45                                      settings.getValue('LOG_FILE_VSWITCHD'))
46         self._ovsdb_pidfile_path = os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'],
47                                                 "ovsdb-server.pid")
48         self._vswitchd_pidfile_path = os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'],
49                                                    "{}.pid".format(self._proc_name))
50         self._logger = logging.getLogger(__name__)
51         # sign '|' must be escaped or avoided, otherwise it is handled as 'or' by regex
52         self._expect = r'bridge.INFO.{}'.format(self._proc_name)
53         self._timeout = 30
54         self._bridges = {}
55         self._vswitchd_args = ['--pidfile=' + self._vswitchd_pidfile_path,
56                                '--overwrite-pidfile', '--log-file=' + self._logfile]
57         self._cmd = []
58         self._cmd_template = ['sudo', '-E', settings.getValue('TOOLS')['ovs-vswitchd']]
59         self._stamp = None
60         self._module_manager = ModuleManager()
61
62     def start(self):
63         """ Start ``ovsdb-server`` and ``ovs-vswitchd`` instance.
64
65         :raises: pexpect.EOF, pexpect.TIMEOUT
66         """
67         self._logger.info("Starting vswitchd...")
68
69         # insert kernel modules if required
70         if 'vswitch_modules' in settings.getValue('TOOLS'):
71             self._module_manager.insert_modules(settings.getValue('TOOLS')['vswitch_modules'])
72
73         self._cmd = self._cmd_template + self._vswitchd_args
74
75         # DB must be started before vswitchd
76         self._reset_ovsdb()
77         self._start_ovsdb()
78
79         # DB must be up before vswitchd config is altered or vswitchd started
80         time.sleep(3)
81
82         self.configure()
83
84         try:
85             tasks.Process.start(self)
86             self.relinquish()
87         except (pexpect.EOF, pexpect.TIMEOUT) as exc:
88             logging.error("Exception during VSwitch start.")
89             self._kill_ovsdb()
90             raise exc
91
92         self._logger.info("Vswitchd...Started.")
93
94     def restart(self):
95         """ Restart ``ovs-vswitchd`` instance. ``ovsdb-server`` is not restarted.
96
97         :raises: pexpect.EOF, pexpect.TIMEOUT
98         """
99         self._logger.info("Restarting vswitchd...")
100         if os.path.isfile(self._vswitchd_pidfile_path):
101             self._logger.info('Killing ovs-vswitchd...')
102             with open(self._vswitchd_pidfile_path, "r") as pidfile:
103                 vswitchd_pid = pidfile.read().strip()
104                 tasks.terminate_task(vswitchd_pid, logger=self._logger)
105
106         try:
107             tasks.Process.start(self)
108             self.relinquish()
109         except (pexpect.EOF, pexpect.TIMEOUT) as exc:
110             logging.error("Exception during VSwitch start.")
111             self._kill_ovsdb()
112             raise exc
113         self._logger.info("Vswitchd...Started.")
114
115     def configure(self):
116         """ Configure vswitchd through ovsdb if needed
117         """
118         pass
119
120     # Method could be a function
121     # pylint: disable=no-self-use
122     def get_version(self):
123         """See IVswitch for general description
124         """
125         # OVS version can be read offline
126         return []
127
128     def stop(self):
129         """See IVswitch for general description
130         """
131         for switch_name in list(self._bridges):
132             self.del_switch(switch_name)
133         self._logger.info("Terminating vswitchd...")
134         self.kill()
135         self._bridges = {}
136         self._logger.info("Vswitchd...Terminated.")
137
138     def add_switch(self, switch_name, params=None):
139         """See IVswitch for general description
140         """
141         bridge = OFBridge(switch_name)
142         bridge.create(params)
143         bridge.set_db_attribute('Open_vSwitch', '.',
144                                 'other_config:max-idle',
145                                 settings.getValue('VSWITCH_FLOW_TIMEOUT'))
146         self._bridges[switch_name] = bridge
147
148     def del_switch(self, switch_name):
149         """See IVswitch for general description
150         """
151         bridge = self._bridges[switch_name]
152         for port in list(bridge.get_ports()):
153             bridge.del_port(port)
154         self._bridges.pop(switch_name)
155         bridge.destroy()
156
157     def add_phy_port(self, switch_name):
158         """See IVswitch for general description
159         """
160         raise NotImplementedError
161
162     def add_vport(self, switch_name):
163         """See IVswitch for general description
164         """
165         raise NotImplementedError
166
167     def add_veth_pair_port(self, switch_name=None, remote_switch_name=None,
168                            local_opts=None, remote_opts=None):
169         """Creates veth-pair port between 'switch_name' and 'remote_switch_name'
170
171         """
172         if switch_name is None or remote_switch_name is None:
173             return None
174
175         bridge = self._bridges[switch_name]
176         remote_bridge = self._bridges[remote_switch_name]
177         pcount = str(self._get_port_count('type=patch'))
178         # NOTE ::: What if interface name longer than allowed width??
179         local_port_name = switch_name + '-' + remote_switch_name + '-' + pcount
180         remote_port_name = remote_switch_name + '-' + switch_name + '-' + pcount
181         local_params = ['--', 'set', 'Interface', local_port_name,
182                         'type=patch',
183                         'options:peer=' + remote_port_name]
184         remote_params = ['--', 'set', 'Interface', remote_port_name,
185                          'type=patch',
186                          'options:peer=' + local_port_name]
187
188         if local_opts is not None:
189             local_params = local_params + local_opts
190
191         if remote_opts is not None:
192             remote_params = remote_params + remote_opts
193
194         local_of_port = bridge.add_port(local_port_name, local_params)
195         remote_of_port = remote_bridge.add_port(remote_port_name, remote_params)
196         return [(local_port_name, local_of_port),
197                 (remote_port_name, remote_of_port)]
198
199     def add_tunnel_port(self, switch_name, remote_ip, tunnel_type='vxlan', params=None):
200         """Creates tunneling port
201         """
202         bridge = self._bridges[switch_name]
203         pcount = str(self._get_port_count('type=' + tunnel_type))
204         port_name = tunnel_type + pcount
205         local_params = ['--', 'set', 'Interface', port_name,
206                         'type=' + tunnel_type,
207                         'options:remote_ip=' + remote_ip]
208
209         if params is not None:
210             local_params = local_params + params
211
212         of_port = bridge.add_port(port_name, local_params)
213         return (port_name, of_port)
214
215     def get_ports(self, switch_name):
216         """See IVswitch for general description
217         """
218         bridge = self._bridges[switch_name]
219         ports = list(bridge.get_ports().items())
220         return [(name, of_port) for (name, (of_port, _)) in ports]
221
222     def del_port(self, switch_name, port_name):
223         """See IVswitch for general description
224         """
225         bridge = self._bridges[switch_name]
226         bridge.del_port(port_name)
227
228     def add_flow(self, switch_name, flow, cache='off'):
229         """See IVswitch for general description
230         """
231         bridge = self._bridges[switch_name]
232         bridge.add_flow(flow, cache=cache)
233
234     def del_flow(self, switch_name, flow=None):
235         """See IVswitch for general description
236         """
237         flow = flow or {}
238         bridge = self._bridges[switch_name]
239         bridge.del_flow(flow)
240
241     def dump_flows(self, switch_name):
242         """See IVswitch for general description
243         """
244         bridge = self._bridges[switch_name]
245         bridge.dump_flows()
246
247     def add_route(self, switch_name, network, destination):
248         """See IVswitch for general description
249         """
250         bridge = self._bridges[switch_name]
251         bridge.add_route(network, destination)
252
253     def set_tunnel_arp(self, ip_addr, mac_addr, switch_name):
254         """See IVswitch for general description
255         """
256         bridge = self._bridges[switch_name]
257         bridge.set_tunnel_arp(ip_addr, mac_addr, switch_name)
258
259     def _get_port_count(self, param):
260         """Returns the number of ports having a certain parameter
261         """
262         cnt = 0
263         for k in self._bridges:
264             pparams = [c for (_, (_, c)) in list(self._bridges[k].get_ports().items())]
265             phits = [i for i in pparams if param in i]
266             cnt += len(phits)
267
268         if cnt is None:
269             cnt = 0
270         return cnt
271
272     def disable_stp(self, switch_name):
273         """
274         Disable stp protocol on the bridge
275         :param switch_name: bridge to disable stp
276         :return: None
277         """
278         bridge = self._bridges[switch_name]
279         bridge.set_stp(False)
280         self._logger.info('Sleeping for 50 secs to allow stp to stop.')
281         time.sleep(50)  # needs time to disable
282
283     def enable_stp(self, switch_name):
284         """
285         Enable stp protocol on the bridge
286         :param switch_name: bridge to enable stp
287         :return: None
288         """
289         bridge = self._bridges[switch_name]
290         bridge.set_stp(True)
291         self._logger.info('Sleeping for 50 secs to allow stp to start.')
292         time.sleep(50)  # needs time to enable
293
294     def disable_rstp(self, switch_name):
295         """
296         Disable rstp on the bridge
297         :param switch_name: bridge to disable rstp
298         :return: None
299         """
300         bridge = self._bridges[switch_name]
301         bridge.set_rstp(False)
302         self._logger.info('Sleeping for 15 secs to allow rstp to stop.')
303         time.sleep(15)  # needs time to disable
304
305     def enable_rstp(self, switch_name):
306         """
307         Enable rstp on the bridge
308         :param switch_name: bridge to enable rstp
309         :return: None
310         """
311         bridge = self._bridges[switch_name]
312         bridge.set_rstp(True)
313         self._logger.info('Sleeping for 15 secs to allow rstp to start.')
314         time.sleep(15)  # needs time to enable
315
316     def kill(self, signal='-15', sleep=10):
317         """Kill ``ovs-vswitchd`` and ``ovs-ovsdb`` instances if they are alive.
318
319         :returns: None
320         """
321         if os.path.isfile(self._vswitchd_pidfile_path):
322             self._logger.info('Killing ovs-vswitchd...')
323             with open(self._vswitchd_pidfile_path, "r") as pidfile:
324                 vswitchd_pid = pidfile.read().strip()
325                 tasks.terminate_task(vswitchd_pid, logger=self._logger)
326
327         self._kill_ovsdb()  # ovsdb must be killed after vswitchd
328
329         # just for case, that sudo envelope has not been terminated yet
330         tasks.Process.kill(self, signal, sleep)
331
332     # helper functions
333
334     def _reset_ovsdb(self):
335         """Reset system for 'ovsdb'.
336
337         :returns: None
338         """
339         self._logger.info('Resetting system after last run...')
340
341         # create a backup of ovs_var_tmp and ovs_etc_tmp; It is
342         # essential for OVS installed from binary packages.
343         self._stamp = '{:%Y%m%d_%H%M%S}_{}'.format(datetime.datetime.now(),
344                                                    random.randrange(1000, 9999))
345         for tmp_dir in ['ovs_var_tmp', 'ovs_etc_tmp']:
346             if os.path.exists(settings.getValue('TOOLS')[tmp_dir]):
347                 orig_dir = os.path.normpath(settings.getValue('TOOLS')[tmp_dir])
348                 self._logger.info('Creating backup of %s directory...', tmp_dir)
349                 tasks.run_task(['sudo', 'mv', orig_dir, '{}.{}'.format(orig_dir, self._stamp)],
350                                self._logger)
351
352         # create fresh tmp dirs
353         tasks.run_task(['sudo', 'mkdir', '-p', settings.getValue('TOOLS')['ovs_var_tmp']], self._logger)
354         tasks.run_task(['sudo', 'mkdir', '-p', settings.getValue('TOOLS')['ovs_etc_tmp']], self._logger)
355
356         self._logger.info('System reset after last run.')
357
358     def _start_ovsdb(self):
359         """Start ``ovsdb-server`` instance.
360
361         :returns: None
362         """
363         ovsdb_tool_bin = settings.getValue('TOOLS')['ovsdb-tool']
364         tasks.run_task(['sudo', ovsdb_tool_bin, 'create',
365                         os.path.join(settings.getValue('TOOLS')['ovs_etc_tmp'], 'conf.db'),
366                         settings.getValue('TOOLS')['ovsschema']],
367                        self._logger,
368                        'Creating ovsdb configuration database...')
369
370         ovsdb_server_bin = settings.getValue('TOOLS')['ovsdb-server']
371
372         tasks.run_background_task(
373             ['sudo', ovsdb_server_bin,
374              '--remote=punix:%s' % os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'], 'db.sock'),
375              '--remote=db:Open_vSwitch,Open_vSwitch,manager_options',
376              '--pidfile=' + self._ovsdb_pidfile_path, '--overwrite-pidfile'],
377             self._logger,
378             'Starting ovsdb-server...')
379
380     def _kill_ovsdb(self):
381         """Kill ``ovsdb-server`` instance.
382
383         :returns: None
384         """
385         if os.path.isfile(self._ovsdb_pidfile_path):
386             with open(self._ovsdb_pidfile_path, "r") as pidfile:
387                 ovsdb_pid = pidfile.read().strip()
388
389             self._logger.info("Killing ovsdb with pid: %s", ovsdb_pid)
390
391             if ovsdb_pid:
392                 tasks.terminate_task(ovsdb_pid, logger=self._logger)
393
394         # restore original content of ovs_var_tmp and ovs_etc_tmp; It is
395         # essential for OVS installed from binary packages.
396         if self._stamp:
397             for tmp_dir in ['ovs_var_tmp', 'ovs_etc_tmp']:
398                 orig_dir = os.path.normpath(settings.getValue('TOOLS')[tmp_dir])
399                 if os.path.exists('{}.{}'.format(orig_dir, self._stamp)):
400                     self._logger.info('Restoring backup of %s directory...', tmp_dir)
401                     tasks.run_task(['sudo', 'rm', '-rf', orig_dir], self._logger)
402                     tasks.run_task(['sudo', 'mv', '{}.{}'.format(orig_dir, self._stamp), orig_dir],
403                                    self._logger)
404
405     @staticmethod
406     def get_db_sock_path():
407         """Method returns location of db.sock file
408
409         :returns: path to db.sock file.
410         """
411         return os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'], 'db.sock')
412
413     #
414     # validate methods required for integration testcases
415     #
416     def validate_add_switch(self, _dummy_result, switch_name, _dummy_params=None):
417         """Validate - Create a new logical switch with no ports
418         """
419         bridge = self._bridges[switch_name]
420         output = bridge.run_vsctl(['show'], check_error=True)
421         assert not output[1]  # there shouldn't be any stderr, but in case
422         assert re.search('Bridge ["\']?%s["\']?' % switch_name, output[0]) is not None
423         return True
424
425     # Method could be a function
426     # pylint: disable=no-self-use
427     def validate_del_switch(self, _dummy_result, switch_name):
428         """Validate removal of switch
429         """
430         bridge = OFBridge('tmp')
431         output = bridge.run_vsctl(['show'], check_error=True)
432         assert not output[1]  # there shouldn't be any stderr, but in case
433         assert re.search('Bridge ["\']?%s["\']?' % switch_name, output[0]) is None
434         return True
435
436     def validate_add_phy_port(self, result, switch_name):
437         """ Validate that physical port was added to bridge.
438         """
439         bridge = self._bridges[switch_name]
440         output = bridge.run_vsctl(['show'], check_error=True)
441         assert not output[1]  # there shouldn't be any stderr, but in case
442         assert re.search('Port ["\']?%s["\']?' % result[0], output[0]) is not None
443         assert re.search('Interface ["\']?%s["\']?' % result[0], output[0]) is not None
444         return True
445
446     def validate_add_vport(self, result, switch_name):
447         """ Validate that virtual port was added to bridge.
448         """
449         return self.validate_add_phy_port(result, switch_name)
450
451     def validate_del_port(self, _dummy_result, switch_name, port_name):
452         """ Validate that port_name was removed from bridge.
453         """
454         bridge = self._bridges[switch_name]
455         output = bridge.run_vsctl(['show'], check_error=True)
456         assert not output[1]  # there shouldn't be any stderr, but in case
457         assert 'Port "%s"' % port_name not in output[0]
458         return True
459
460     def validate_add_flow(self, _dummy_result, switch_name, flow, _dummy_cache='off'):
461         """ Validate insertion of the flow into the switch
462         """
463
464         if 'idle_timeout' in flow:
465             del flow['idle_timeout']
466
467         # Note: it should be possible to call `ovs-ofctl dump-flows switch flow`
468         # to verify flow insertion, but it doesn't accept the same flow syntax
469         # as add-flow, so we have to compare it the hard way
470
471         # get dump of flows and compare them one by one
472         flow_src = flow_key(flow)
473         bridge = self._bridges[switch_name]
474         output = bridge.run_ofctl(['dump-flows', switch_name], check_error=True)
475         for flow_dump in output[0].split('\n'):
476             if flow_match(flow_dump, flow_src):
477                 # flow was added correctly
478                 return True
479         return False
480
481     def validate_del_flow(self, _dummy_result, switch_name, flow=None):
482         """ Validate removal of the flow
483         """
484         if not flow:
485             # what else we can do?
486             return True
487         return not self.validate_add_flow(_dummy_result, switch_name, flow)
488
489     def validate_dump_flows(self, _dummy_result, _dummy_switch_name):
490         """ Validate call of flow dump
491         """
492         return True
493
494     def validate_disable_rstp(self, _dummy_result, switch_name):
495         """ Validate rstp disable
496         """
497         bridge = self._bridges[switch_name]
498         return 'rstp_enable         : false' in ''.join(bridge.bridge_info())
499
500     def validate_enable_rstp(self, _dummy_result, switch_name):
501         """ Validate rstp enable
502         """
503         bridge = self._bridges[switch_name]
504         return 'rstp_enable         : true' in ''.join(bridge.bridge_info())
505
506     def validate_disable_stp(self, _dummy_result, switch_name):
507         """ Validate stp disable
508         """
509         bridge = self._bridges[switch_name]
510         return 'stp_enable          : false' in ''.join(bridge.bridge_info())
511
512     def validate_enable_stp(self, _dummy_result, switch_name):
513         """ Validate stp enable
514         """
515         bridge = self._bridges[switch_name]
516         return 'stp_enable          : true' in ''.join(bridge.bridge_info())
517
518     def validate_restart(self, _dummy_result):
519         """ Validate restart
520         """
521         return True