be7b77dfa8fa19551e84610dd19d47bb9a5a6606
[vswitchperf.git] / vswitches / ovs.py
1 # Copyright 2015-2018 Intel Corporation, Tieto and Others.
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         bridge.del_flow({})
153         for port in list(bridge.get_ports()):
154             bridge.del_port(port)
155         self._bridges.pop(switch_name)
156         bridge.destroy()
157
158     def add_phy_port(self, switch_name):
159         """See IVswitch for general description
160         """
161         raise NotImplementedError
162
163     def add_vport(self, switch_name):
164         """See IVswitch for general description
165         """
166         raise NotImplementedError
167
168     def add_veth_pair_port(self, switch_name=None, remote_switch_name=None,
169                            local_opts=None, remote_opts=None):
170         """Creates veth-pair port between 'switch_name' and 'remote_switch_name'
171
172         """
173         if switch_name is None or remote_switch_name is None:
174             return None
175
176         bridge = self._bridges[switch_name]
177         remote_bridge = self._bridges[remote_switch_name]
178         pcount = str(self._get_port_count('type=patch'))
179         # NOTE ::: What if interface name longer than allowed width??
180         local_port_name = switch_name + '-' + remote_switch_name + '-' + pcount
181         remote_port_name = remote_switch_name + '-' + switch_name + '-' + pcount
182         local_params = ['--', 'set', 'Interface', local_port_name,
183                         'type=patch',
184                         'options:peer=' + remote_port_name]
185         remote_params = ['--', 'set', 'Interface', remote_port_name,
186                          'type=patch',
187                          'options:peer=' + local_port_name]
188
189         if local_opts is not None:
190             local_params = local_params + local_opts
191
192         if remote_opts is not None:
193             remote_params = remote_params + remote_opts
194
195         local_of_port = bridge.add_port(local_port_name, local_params)
196         remote_of_port = remote_bridge.add_port(remote_port_name, remote_params)
197         return [(local_port_name, local_of_port),
198                 (remote_port_name, remote_of_port)]
199
200     def add_tunnel_port(self, switch_name, remote_ip, tunnel_type='vxlan', params=None):
201         """Creates tunneling port
202         """
203         bridge = self._bridges[switch_name]
204         pcount = str(self._get_port_count('type=' + tunnel_type))
205         port_name = tunnel_type + pcount
206         local_params = ['--', 'set', 'Interface', port_name,
207                         'type=' + tunnel_type,
208                         'options:remote_ip=' + remote_ip]
209
210         if params is not None:
211             local_params = local_params + params
212
213         of_port = bridge.add_port(port_name, local_params)
214         return (port_name, of_port)
215
216     def get_ports(self, switch_name):
217         """See IVswitch for general description
218         """
219         bridge = self._bridges[switch_name]
220         ports = list(bridge.get_ports().items())
221         return [(name, of_port) for (name, (of_port, _)) in ports]
222
223     def del_port(self, switch_name, port_name):
224         """See IVswitch for general description
225         """
226         bridge = self._bridges[switch_name]
227         bridge.del_port(port_name)
228
229     def add_flow(self, switch_name, flow, cache='off'):
230         """See IVswitch for general description
231         """
232         bridge = self._bridges[switch_name]
233         bridge.add_flow(flow, cache=cache)
234
235     def del_flow(self, switch_name, flow=None):
236         """See IVswitch for general description
237         """
238         flow = flow or {}
239         bridge = self._bridges[switch_name]
240         bridge.del_flow(flow)
241
242     def dump_flows(self, switch_name):
243         """See IVswitch for general description
244         """
245         bridge = self._bridges[switch_name]
246         bridge.dump_flows()
247
248     def add_route(self, switch_name, network, destination):
249         """See IVswitch for general description
250         """
251         bridge = self._bridges[switch_name]
252         bridge.add_route(network, destination)
253
254     def set_tunnel_arp(self, ip_addr, mac_addr, switch_name):
255         """See IVswitch for general description
256         """
257         bridge = self._bridges[switch_name]
258         bridge.set_tunnel_arp(ip_addr, mac_addr, switch_name)
259
260     def _get_port_count(self, param):
261         """Returns the number of ports having a certain parameter
262         """
263         cnt = 0
264         for k in self._bridges:
265             pparams = [c for (_, (_, c)) in list(self._bridges[k].get_ports().items())]
266             phits = [i for i in pparams if param in i]
267             cnt += len(phits)
268
269         if cnt is None:
270             cnt = 0
271         return cnt
272
273     def disable_stp(self, switch_name):
274         """
275         Disable stp protocol on the bridge
276         :param switch_name: bridge to disable stp
277         :return: None
278         """
279         bridge = self._bridges[switch_name]
280         bridge.set_stp(False)
281         self._logger.info('Sleeping for 50 secs to allow stp to stop.')
282         time.sleep(50)  # needs time to disable
283
284     def enable_stp(self, switch_name):
285         """
286         Enable stp protocol on the bridge
287         :param switch_name: bridge to enable stp
288         :return: None
289         """
290         bridge = self._bridges[switch_name]
291         bridge.set_stp(True)
292         self._logger.info('Sleeping for 50 secs to allow stp to start.')
293         time.sleep(50)  # needs time to enable
294
295     def disable_rstp(self, switch_name):
296         """
297         Disable rstp on the bridge
298         :param switch_name: bridge to disable rstp
299         :return: None
300         """
301         bridge = self._bridges[switch_name]
302         bridge.set_rstp(False)
303         self._logger.info('Sleeping for 15 secs to allow rstp to stop.')
304         time.sleep(15)  # needs time to disable
305
306     def enable_rstp(self, switch_name):
307         """
308         Enable rstp on the bridge
309         :param switch_name: bridge to enable rstp
310         :return: None
311         """
312         bridge = self._bridges[switch_name]
313         bridge.set_rstp(True)
314         self._logger.info('Sleeping for 15 secs to allow rstp to start.')
315         time.sleep(15)  # needs time to enable
316
317     def kill(self, signal='-15', sleep=10):
318         """Kill ``ovs-vswitchd`` and ``ovs-ovsdb`` instances if they are alive.
319
320         :returns: None
321         """
322         if os.path.isfile(self._vswitchd_pidfile_path):
323             self._logger.info('Killing ovs-vswitchd...')
324             with open(self._vswitchd_pidfile_path, "r") as pidfile:
325                 vswitchd_pid = pidfile.read().strip()
326                 tasks.terminate_task(vswitchd_pid, logger=self._logger)
327
328         self._kill_ovsdb()  # ovsdb must be killed after vswitchd
329
330         # just for case, that sudo envelope has not been terminated yet
331         tasks.Process.kill(self, signal, sleep)
332
333     # helper functions
334
335     def _reset_ovsdb(self):
336         """Reset system for 'ovsdb'.
337
338         :returns: None
339         """
340         self._logger.info('Resetting system after last run...')
341
342         # create a backup of ovs_var_tmp and ovs_etc_tmp; It is
343         # essential for OVS installed from binary packages.
344         self._stamp = '{:%Y%m%d_%H%M%S}_{}'.format(datetime.datetime.now(),
345                                                    random.randrange(1000, 9999))
346         for tmp_dir in ['ovs_var_tmp', 'ovs_etc_tmp']:
347             if os.path.exists(settings.getValue('TOOLS')[tmp_dir]):
348                 orig_dir = os.path.normpath(settings.getValue('TOOLS')[tmp_dir])
349                 self._logger.info('Creating backup of %s directory...', tmp_dir)
350                 tasks.run_task(['sudo', 'mv', orig_dir, '{}.{}'.format(orig_dir, self._stamp)],
351                                self._logger)
352
353         # create fresh tmp dirs
354         tasks.run_task(['sudo', 'mkdir', '-p', settings.getValue('TOOLS')['ovs_var_tmp']], self._logger)
355         tasks.run_task(['sudo', 'mkdir', '-p', settings.getValue('TOOLS')['ovs_etc_tmp']], self._logger)
356
357         self._logger.info('System reset after last run.')
358
359     def _start_ovsdb(self):
360         """Start ``ovsdb-server`` instance.
361
362         :returns: None
363         """
364         ovsdb_tool_bin = settings.getValue('TOOLS')['ovsdb-tool']
365         tasks.run_task(['sudo', ovsdb_tool_bin, 'create',
366                         os.path.join(settings.getValue('TOOLS')['ovs_etc_tmp'], 'conf.db'),
367                         settings.getValue('TOOLS')['ovsschema']],
368                        self._logger,
369                        'Creating ovsdb configuration database...')
370
371         ovsdb_server_bin = settings.getValue('TOOLS')['ovsdb-server']
372
373         tasks.run_background_task(
374             ['sudo', ovsdb_server_bin,
375              '--remote=punix:%s' % os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'], 'db.sock'),
376              '--remote=db:Open_vSwitch,Open_vSwitch,manager_options',
377              '--pidfile=' + self._ovsdb_pidfile_path, '--overwrite-pidfile'],
378             self._logger,
379             'Starting ovsdb-server...')
380
381     def _kill_ovsdb(self):
382         """Kill ``ovsdb-server`` instance.
383
384         :returns: None
385         """
386         if os.path.isfile(self._ovsdb_pidfile_path):
387             with open(self._ovsdb_pidfile_path, "r") as pidfile:
388                 ovsdb_pid = pidfile.read().strip()
389
390             self._logger.info("Killing ovsdb with pid: %s", ovsdb_pid)
391
392             if ovsdb_pid:
393                 tasks.terminate_task(ovsdb_pid, logger=self._logger)
394
395         # restore original content of ovs_var_tmp and ovs_etc_tmp; It is
396         # essential for OVS installed from binary packages.
397         if self._stamp:
398             for tmp_dir in ['ovs_var_tmp', 'ovs_etc_tmp']:
399                 orig_dir = os.path.normpath(settings.getValue('TOOLS')[tmp_dir])
400                 if os.path.exists('{}.{}'.format(orig_dir, self._stamp)):
401                     self._logger.info('Restoring backup of %s directory...', tmp_dir)
402                     tasks.run_task(['sudo', 'rm', '-rf', orig_dir], self._logger)
403                     tasks.run_task(['sudo', 'mv', '{}.{}'.format(orig_dir, self._stamp), orig_dir],
404                                    self._logger)
405
406     @staticmethod
407     def get_db_sock_path():
408         """Method returns location of db.sock file
409
410         :returns: path to db.sock file.
411         """
412         return os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'], 'db.sock')
413
414     #
415     # validate methods required for integration testcases
416     #
417     def validate_add_switch(self, _dummy_result, switch_name, _dummy_params=None):
418         """Validate - Create a new logical switch with no ports
419         """
420         bridge = self._bridges[switch_name]
421         output = bridge.run_vsctl(['show'], check_error=True)
422         assert not output[1]  # there shouldn't be any stderr, but in case
423         assert re.search('Bridge ["\']?%s["\']?' % switch_name, output[0]) is not None
424         return True
425
426     # Method could be a function
427     # pylint: disable=no-self-use
428     def validate_del_switch(self, _dummy_result, switch_name):
429         """Validate removal of switch
430         """
431         bridge = OFBridge('tmp')
432         output = bridge.run_vsctl(['show'], check_error=True)
433         assert not output[1]  # there shouldn't be any stderr, but in case
434         assert re.search('Bridge ["\']?%s["\']?' % switch_name, output[0]) is None
435         return True
436
437     def validate_add_phy_port(self, result, switch_name):
438         """ Validate that physical port was added to bridge.
439         """
440         bridge = self._bridges[switch_name]
441         output = bridge.run_vsctl(['show'], check_error=True)
442         assert not output[1]  # there shouldn't be any stderr, but in case
443         assert re.search('Port ["\']?%s["\']?' % result[0], output[0]) is not None
444         assert re.search('Interface ["\']?%s["\']?' % result[0], output[0]) is not None
445         return True
446
447     def validate_add_vport(self, result, switch_name):
448         """ Validate that virtual port was added to bridge.
449         """
450         return self.validate_add_phy_port(result, switch_name)
451
452     def validate_del_port(self, _dummy_result, switch_name, port_name):
453         """ Validate that port_name was removed from bridge.
454         """
455         bridge = self._bridges[switch_name]
456         output = bridge.run_vsctl(['show'], check_error=True)
457         assert not output[1]  # there shouldn't be any stderr, but in case
458         assert 'Port "%s"' % port_name not in output[0]
459         return True
460
461     def validate_add_flow(self, _dummy_result, switch_name, flow, _dummy_cache='off'):
462         """ Validate insertion of the flow into the switch
463         """
464
465         if 'idle_timeout' in flow:
466             del flow['idle_timeout']
467
468         # Note: it should be possible to call `ovs-ofctl dump-flows switch flow`
469         # to verify flow insertion, but it doesn't accept the same flow syntax
470         # as add-flow, so we have to compare it the hard way
471
472         # get dump of flows and compare them one by one
473         flow_src = flow_key(flow)
474         bridge = self._bridges[switch_name]
475         output = bridge.run_ofctl(['dump-flows', switch_name], check_error=True)
476         for flow_dump in output[0].split('\n'):
477             if flow_match(flow_dump, flow_src):
478                 # flow was added correctly
479                 return True
480         return False
481
482     def validate_del_flow(self, _dummy_result, switch_name, flow=None):
483         """ Validate removal of the flow
484         """
485         if not flow:
486             # what else we can do?
487             return True
488         return not self.validate_add_flow(_dummy_result, switch_name, flow)
489
490     def validate_dump_flows(self, _dummy_result, _dummy_switch_name):
491         """ Validate call of flow dump
492         """
493         return True
494
495     def validate_disable_rstp(self, _dummy_result, switch_name):
496         """ Validate rstp disable
497         """
498         bridge = self._bridges[switch_name]
499         return 'rstp_enable         : false' in ''.join(bridge.bridge_info())
500
501     def validate_enable_rstp(self, _dummy_result, switch_name):
502         """ Validate rstp enable
503         """
504         bridge = self._bridges[switch_name]
505         return 'rstp_enable         : true' in ''.join(bridge.bridge_info())
506
507     def validate_disable_stp(self, _dummy_result, switch_name):
508         """ Validate stp disable
509         """
510         bridge = self._bridges[switch_name]
511         return 'stp_enable          : false' in ''.join(bridge.bridge_info())
512
513     def validate_enable_stp(self, _dummy_result, switch_name):
514         """ Validate stp enable
515         """
516         bridge = self._bridges[switch_name]
517         return 'stp_enable          : true' in ''.join(bridge.bridge_info())
518
519     def validate_restart(self, _dummy_result):
520         """ Validate restart
521         """
522         return True