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