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