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