1 # Copyright 2015-2018 Intel Corporation., Tieto
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 """VSPERF Open vSwitch base class
27 from conf import settings
28 from src.ovs import OFBridge, flow_key, flow_match
29 from vswitches.vswitch import IVSwitch
30 from tools import tasks
31 from tools.module_manager import ModuleManager
33 # enable caching of flows if their number exceeds given limit
34 _CACHE_FLOWS_LIMIT = 10
36 # pylint: disable=too-many-public-methods
37 class IVSwitchOvs(IVSwitch, tasks.Process):
38 """Open vSwitch base class implementation
40 The method docstrings document only considerations specific to this
41 implementation. For generic information of the nature of the methods,
44 _proc_name = 'ovs-vswitchd'
47 """See IVswitch for general description
51 name, ext = os.path.splitext(settings.getValue('LOG_FILE_VSWITCHD'))
52 rename_vswitchd = "{name}_{uid}{ex}".format(name=name,
53 uid=settings.getValue('LOG_TIMESTAMP'),
55 self._logfile = os.path.join(settings.getValue('RESULTS_PATH'), rename_vswitchd)
56 self._ovsdb_pidfile_path = os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'],
58 self._vswitchd_pidfile_path = os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'],
59 "{}.pid".format(self._proc_name))
60 # sign '|' must be escaped or avoided, otherwise it is handled as 'or' by regex
61 self._expect = r'bridge.INFO.{}'.format(self._proc_name)
62 self._vswitchd_args = ['--pidfile=' + self._vswitchd_pidfile_path,
63 '--overwrite-pidfile', '--log-file=' + self._logfile]
64 self._cmd_template = ['sudo', '-E', settings.getValue('TOOLS')['ovs-vswitchd']]
65 self._module_manager = ModuleManager()
66 self._flow_template = settings.getValue('OVS_FLOW_TEMPLATE').copy()
67 self._flow_actions = ['output:{}']
69 # if routing tables are enabled, then flows should go into table 1
70 # see design document for details about Routing Tables feature
71 if settings.getValue('OVS_ROUTING_TABLES'):
72 # flows should be added into table 1
73 self._flow_template.update({'table':'1', 'priority':'1'})
74 # and chosen port will be propagated via metadata
75 self._flow_actions = ['write_actions(output:{})',
80 """ Start ``ovsdb-server`` and ``ovs-vswitchd`` instance.
82 :raises: pexpect.EOF, pexpect.TIMEOUT
84 self._logger.info("Starting vswitchd...")
86 # insert kernel modules if required
87 if 'vswitch_modules' in settings.getValue('TOOLS'):
88 self._module_manager.insert_modules(settings.getValue('TOOLS')['vswitch_modules'])
90 self._cmd = self._cmd_template + self._vswitchd_args
92 # DB must be started before vswitchd
96 # DB must be up before vswitchd config is altered or vswitchd started
102 tasks.Process.start(self)
104 except (pexpect.EOF, pexpect.TIMEOUT) as exc:
105 self._logger.error("Exception during VSwitch start.")
109 self._logger.info("Vswitchd...Started.")
112 """ Restart ``ovs-vswitchd`` instance. ``ovsdb-server`` is not restarted.
114 :raises: pexpect.EOF, pexpect.TIMEOUT
116 self._logger.info("Restarting vswitchd...")
117 if os.path.isfile(self._vswitchd_pidfile_path):
118 self._logger.info('Killing ovs-vswitchd...')
119 with open(self._vswitchd_pidfile_path, "r") as pidfile:
120 vswitchd_pid = pidfile.read().strip()
121 tasks.terminate_task(vswitchd_pid, logger=self._logger)
124 tasks.Process.start(self)
126 except (pexpect.EOF, pexpect.TIMEOUT) as exc:
127 self._logger.error("Exception during VSwitch start.")
130 self._logger.info("Vswitchd...Started.")
133 """ Configure vswitchd through ovsdb if needed
137 # Method could be a function
138 # pylint: disable=no-self-use
139 def get_version(self):
140 """See IVswitch for general description
142 # OVS version can be read offline
146 """See IVswitch for general description
148 for switch_name in list(self._switches):
149 self.del_switch(switch_name)
150 self._logger.info("Terminating vswitchd...")
153 self._logger.info("Vswitchd...Terminated.")
155 def add_switch(self, switch_name, params=None):
156 """See IVswitch for general description
158 # create and configure new ovs bridge and delete all default flows
159 bridge = OFBridge(switch_name)
160 bridge.create(params)
162 bridge.set_db_attribute('Open_vSwitch', '.',
163 'other_config:max-idle',
164 settings.getValue('VSWITCH_FLOW_TIMEOUT'))
165 self._switches[switch_name] = bridge
166 if settings.getValue('OVS_ROUTING_TABLES'):
167 # table#0 - flows designed to force 5 & 13 tuple matches go here
168 flow = {'table':'0', 'priority':'1', 'actions': ['goto_table:1']}
169 bridge.add_flow(flow)
171 # table#1 - flows to route packets between ports goes here. The
172 # chosen port is communicated to subsequent tables by setting the
173 # metadata value to the egress port number
175 # A placeholder - flows are added into this table by deployments
176 # or by TestSteps via add_connection() method
178 # table#2 - frame modification table. Frame modification flow rules are
179 # isolated in this table so that they can be turned on or off
180 # without affecting the routing or tuple-matching flow rules.
181 flow = {'table':'2', 'priority':'1', 'actions': ['goto_table:3']}
182 bridge.add_flow(flow)
184 # table#3 - egress table
185 # (NOTE) Billy O'Mahony - the drop action here actually required in
186 # order to egress the packet. This is the subject of a thread on
187 # ovs-discuss 2015-06-30.
188 flow = {'table':'3', 'priority':'1', 'actions': ['drop']}
189 bridge.add_flow(flow)
191 def del_switch(self, switch_name):
192 """See IVswitch for general description
194 bridge = self._switches[switch_name]
196 for port in list(bridge.get_ports()):
197 bridge.del_port(port)
198 self._switches.pop(switch_name)
201 def add_phy_port(self, switch_name):
202 """See IVswitch for general description
204 raise NotImplementedError
206 def add_vport(self, switch_name):
207 """See IVswitch for general description
209 raise NotImplementedError
211 def add_veth_pair_port(self, switch_name=None, remote_switch_name=None,
212 local_opts=None, remote_opts=None):
213 """Creates veth-pair port between 'switch_name' and 'remote_switch_name'
216 if switch_name is None or remote_switch_name is None:
219 bridge = self._switches[switch_name]
220 remote_bridge = self._switches[remote_switch_name]
221 pcount = str(self._get_port_count('type=patch'))
222 # NOTE ::: What if interface name longer than allowed width??
223 local_port_name = switch_name + '-' + remote_switch_name + '-' + pcount
224 remote_port_name = remote_switch_name + '-' + switch_name + '-' + pcount
225 local_params = ['--', 'set', 'Interface', local_port_name,
227 'options:peer=' + remote_port_name]
228 remote_params = ['--', 'set', 'Interface', remote_port_name,
230 'options:peer=' + local_port_name]
232 if local_opts is not None:
233 local_params = local_params + local_opts
235 if remote_opts is not None:
236 remote_params = remote_params + remote_opts
238 local_of_port = bridge.add_port(local_port_name, local_params)
239 remote_of_port = remote_bridge.add_port(remote_port_name, remote_params)
240 return [(local_port_name, local_of_port),
241 (remote_port_name, remote_of_port)]
243 def add_tunnel_port(self, switch_name, remote_ip, tunnel_type='vxlan', params=None):
244 """Creates tunneling port
246 bridge = self._switches[switch_name]
247 pcount = str(self._get_port_count('type=' + tunnel_type))
248 port_name = tunnel_type + pcount
249 local_params = ['--', 'set', 'Interface', port_name,
250 'type=' + tunnel_type,
251 'options:remote_ip=' + remote_ip]
253 if params is not None:
254 local_params = local_params + params
256 of_port = bridge.add_port(port_name, local_params)
257 return (port_name, of_port)
259 def get_ports(self, switch_name):
260 """See IVswitch for general description
262 bridge = self._switches[switch_name]
263 ports = list(bridge.get_ports().items())
264 return [(name, of_port) for (name, (of_port, _)) in ports]
266 def del_port(self, switch_name, port_name):
267 """See IVswitch for general description
269 bridge = self._switches[switch_name]
270 bridge.del_port(port_name)
272 def add_flow(self, switch_name, flow, cache='off'):
273 """See IVswitch for general description
275 bridge = self._switches[switch_name]
276 bridge.add_flow(flow, cache=cache)
278 def del_flow(self, switch_name, flow=None):
279 """See IVswitch for general description
282 bridge = self._switches[switch_name]
283 bridge.del_flow(flow)
285 def dump_flows(self, switch_name):
286 """See IVswitch for general description
288 bridge = self._switches[switch_name]
291 def _prepare_flows(self, operation, switch_name, port1, port2, traffic=None):
292 """Prepare flows for add_connection, del_connection and validate methods
293 It returns a list of flows based on given parameters.
296 if operation == 'add':
297 bridge = self._switches[switch_name]
298 flow = self._flow_template.copy()
299 actions = [action.format(bridge.get_ports()[port2][0]) for action in self._flow_actions]
300 flow.update({'in_port': bridge.get_ports()[port1][0], 'actions': actions})
301 # check if stream specific connection(s) should be crated for multistream feature
302 if traffic and traffic['pre_installed_flows'].lower() == 'yes':
303 for stream in range(traffic['multistream']):
304 tmp_flow = flow.copy()
305 # update flow based on trafficgen settings
306 if traffic['stream_type'] == 'L2':
307 dst_mac_value = netaddr.EUI(traffic['l2']['dstmac']).value
308 tmp_mac = netaddr.EUI(dst_mac_value + stream)
309 tmp_mac.dialect = netaddr.mac_unix_expanded
310 tmp_flow.update({'dl_dst':tmp_mac})
311 elif traffic['stream_type'] == 'L3':
312 dst_ip_value = netaddr.IPAddress(traffic['l3']['dstip']).value
313 tmp_ip = netaddr.IPAddress(dst_ip_value + stream)
314 tmp_flow.update({'dl_type':'0x0800', 'nw_dst':tmp_ip})
315 elif traffic['stream_type'] == 'L4':
316 tmp_flow.update({'dl_type':'0x0800',
317 'nw_proto':socket.getprotobyname(traffic['l3']['proto'].lower()),
318 'tp_dst':(traffic['l4']['dstport'] + stream) % 65536})
319 flows.append(tmp_flow)
320 elif traffic and traffic['flow_type'].lower() == 'ip':
321 flow.update({'dl_type':'0x0800', 'nw_src':traffic['l3']['srcip'],
322 'nw_dst':traffic['l3']['dstip']})
326 elif operation == 'del' and port1:
327 bridge = self._switches[switch_name]
328 flows.append({'in_port': bridge.get_ports()[port1][0]})
334 def add_connection(self, switch_name, port1, port2, traffic=None):
335 """See IVswitch for general description
337 flows = self._prepare_flows('add', switch_name, port1, port2, traffic)
339 # enable flows caching for large number of flows
340 cache = 'on' if len(flows) > _CACHE_FLOWS_LIMIT else 'off'
343 self.add_flow(switch_name, flow, cache)
346 self.add_flow(switch_name, [], cache='flush')
348 def del_connection(self, switch_name, port1=None, port2=None):
349 """See IVswitch for general description
351 flows = self._prepare_flows('del', switch_name, port1, port2)
354 self.del_flow(switch_name, flow)
356 def dump_connections(self, switch_name):
357 """See IVswitch for general description
359 self.dump_flows(switch_name)
361 def add_route(self, switch_name, network, destination):
362 """See IVswitch for general description
364 bridge = self._switches[switch_name]
365 bridge.add_route(network, destination)
367 def set_tunnel_arp(self, ip_addr, mac_addr, switch_name):
368 """See IVswitch for general description
370 bridge = self._switches[switch_name]
371 bridge.set_tunnel_arp(ip_addr, mac_addr, switch_name)
373 def _get_port_count(self, param):
374 """Returns the number of ports having a certain parameter
377 for k in self._switches:
378 pparams = [c for (_, (_, c)) in list(self._switches[k].get_ports().items())]
379 phits = [i for i in pparams if param in i]
386 def disable_stp(self, switch_name):
388 Disable stp protocol on the bridge
389 :param switch_name: bridge to disable stp
392 bridge = self._switches[switch_name]
393 bridge.set_stp(False)
394 self._logger.info('Sleeping for 50 secs to allow stp to stop.')
395 time.sleep(50) # needs time to disable
397 def enable_stp(self, switch_name):
399 Enable stp protocol on the bridge
400 :param switch_name: bridge to enable stp
403 bridge = self._switches[switch_name]
405 self._logger.info('Sleeping for 50 secs to allow stp to start.')
406 time.sleep(50) # needs time to enable
408 def disable_rstp(self, switch_name):
410 Disable rstp on the bridge
411 :param switch_name: bridge to disable rstp
414 bridge = self._switches[switch_name]
415 bridge.set_rstp(False)
416 self._logger.info('Sleeping for 15 secs to allow rstp to stop.')
417 time.sleep(15) # needs time to disable
419 def enable_rstp(self, switch_name):
421 Enable rstp on the bridge
422 :param switch_name: bridge to enable rstp
425 bridge = self._switches[switch_name]
426 bridge.set_rstp(True)
427 self._logger.info('Sleeping for 15 secs to allow rstp to start.')
428 time.sleep(15) # needs time to enable
430 def kill(self, signal='-15', sleep=10):
431 """Kill ``ovs-vswitchd`` and ``ovs-ovsdb`` instances if they are alive.
435 if os.path.isfile(self._vswitchd_pidfile_path):
436 self._logger.info('Killing ovs-vswitchd...')
437 with open(self._vswitchd_pidfile_path, "r") as pidfile:
438 vswitchd_pid = pidfile.read().strip()
439 tasks.terminate_task(vswitchd_pid, logger=self._logger)
441 self._kill_ovsdb() # ovsdb must be killed after vswitchd
443 # just for case, that sudo envelope has not been terminated yet
444 tasks.Process.kill(self, signal, sleep)
448 def _reset_ovsdb(self):
449 """Reset system for 'ovsdb'.
453 self._logger.info('Resetting system after last run...')
455 # create a backup of ovs_var_tmp and ovs_etc_tmp; It is
456 # essential for OVS installed from binary packages.
457 self._stamp = '{:%Y%m%d_%H%M%S}_{}'.format(datetime.datetime.now(),
458 random.randrange(1000, 9999))
459 for tmp_dir in ['ovs_var_tmp', 'ovs_etc_tmp']:
460 if os.path.exists(settings.getValue('TOOLS')[tmp_dir]):
461 orig_dir = os.path.normpath(settings.getValue('TOOLS')[tmp_dir])
462 self._logger.info('Creating backup of %s directory...', tmp_dir)
463 tasks.run_task(['sudo', 'mv', orig_dir, '{}.{}'.format(orig_dir, self._stamp)],
466 # create fresh tmp dirs
467 tasks.run_task(['sudo', 'mkdir', '-p', settings.getValue('TOOLS')['ovs_var_tmp']], self._logger)
468 tasks.run_task(['sudo', 'mkdir', '-p', settings.getValue('TOOLS')['ovs_etc_tmp']], self._logger)
470 self._logger.info('System reset after last run.')
472 def _start_ovsdb(self):
473 """Start ``ovsdb-server`` instance.
477 ovsdb_tool_bin = settings.getValue('TOOLS')['ovsdb-tool']
478 tasks.run_task(['sudo', ovsdb_tool_bin, 'create',
479 os.path.join(settings.getValue('TOOLS')['ovs_etc_tmp'], 'conf.db'),
480 settings.getValue('TOOLS')['ovsschema']],
482 'Creating ovsdb configuration database...')
484 ovsdb_server_bin = settings.getValue('TOOLS')['ovsdb-server']
486 tasks.run_background_task(
487 ['sudo', ovsdb_server_bin,
488 '--remote=punix:%s' % os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'], 'db.sock'),
489 '--remote=db:Open_vSwitch,Open_vSwitch,manager_options',
490 '--pidfile=' + self._ovsdb_pidfile_path, '--overwrite-pidfile'],
492 'Starting ovsdb-server...')
494 def _kill_ovsdb(self):
495 """Kill ``ovsdb-server`` instance.
499 if os.path.isfile(self._ovsdb_pidfile_path):
500 with open(self._ovsdb_pidfile_path, "r") as pidfile:
501 ovsdb_pid = pidfile.read().strip()
503 self._logger.info("Killing ovsdb with pid: %s", ovsdb_pid)
506 tasks.terminate_task(ovsdb_pid, logger=self._logger)
508 # restore original content of ovs_var_tmp and ovs_etc_tmp; It is
509 # essential for OVS installed from binary packages.
511 for tmp_dir in ['ovs_var_tmp', 'ovs_etc_tmp']:
512 orig_dir = os.path.normpath(settings.getValue('TOOLS')[tmp_dir])
513 if os.path.exists('{}.{}'.format(orig_dir, self._stamp)):
514 self._logger.info('Restoring backup of %s directory...', tmp_dir)
515 tasks.run_task(['sudo', 'rm', '-rf', orig_dir], self._logger)
516 tasks.run_task(['sudo', 'mv', '{}.{}'.format(orig_dir, self._stamp), orig_dir],
520 def get_db_sock_path():
521 """Method returns location of db.sock file
523 :returns: path to db.sock file.
525 return os.path.join(settings.getValue('TOOLS')['ovs_var_tmp'], 'db.sock')
528 # validate methods required for integration testcases
530 def validate_add_switch(self, _dummy_result, switch_name, _dummy_params=None):
531 """Validate - Create a new logical switch with no ports
533 bridge = self._switches[switch_name]
534 output = bridge.run_vsctl(['show'], check_error=True)
535 assert not output[1] # there shouldn't be any stderr, but in case
536 assert re.search('Bridge ["\']?%s["\']?' % switch_name, output[0]) is not None
539 # Method could be a function
540 # pylint: disable=no-self-use
541 def validate_del_switch(self, _dummy_result, switch_name):
542 """Validate removal of switch
544 bridge = OFBridge('tmp')
545 output = bridge.run_vsctl(['show'], check_error=True)
546 assert not output[1] # there shouldn't be any stderr, but in case
547 assert re.search('Bridge ["\']?%s["\']?' % switch_name, output[0]) is None
550 def validate_add_phy_port(self, result, switch_name):
551 """ Validate that physical port was added to bridge.
553 bridge = self._switches[switch_name]
554 output = bridge.run_vsctl(['show'], check_error=True)
555 assert not output[1] # there shouldn't be any stderr, but in case
556 assert re.search('Port ["\']?%s["\']?' % result[0], output[0]) is not None
557 assert re.search('Interface ["\']?%s["\']?' % result[0], output[0]) is not None
560 def validate_add_vport(self, result, switch_name):
561 """ Validate that virtual port was added to bridge.
563 return self.validate_add_phy_port(result, switch_name)
565 def validate_del_port(self, _dummy_result, switch_name, port_name):
566 """ Validate that port_name was removed from bridge.
568 bridge = self._switches[switch_name]
569 output = bridge.run_vsctl(['show'], check_error=True)
570 assert not output[1] # there shouldn't be any stderr, but in case
571 assert 'Port "%s"' % port_name not in output[0]
574 def validate_add_connection(self, result, switch_name, port1, port2, traffic=None):
575 """ Validate that connection was added
577 for flow in self._prepare_flows('add', switch_name, port1, port2, traffic):
578 if not self.validate_add_flow(result, switch_name, flow):
583 def validate_del_connection(self, result, switch_name, port1, port2):
584 """ Validate that connection was deleted
586 for flow in self._prepare_flows('del', switch_name, port1, port2):
587 if not self.validate_del_flow(result, switch_name, flow):
592 def validate_dump_connections(self, _dummy_result, _dummy_switch_name):
593 """ Validate dump connections call
597 def validate_add_flow(self, _dummy_result, switch_name, flow, _dummy_cache='off'):
598 """ Validate insertion of the flow into the switch
601 if 'idle_timeout' in flow:
602 del flow['idle_timeout']
604 # Note: it should be possible to call `ovs-ofctl dump-flows switch flow`
605 # to verify flow insertion, but it doesn't accept the same flow syntax
606 # as add-flow, so we have to compare it the hard way
608 # get dump of flows and compare them one by one
609 flow_src = flow_key(flow)
610 bridge = self._switches[switch_name]
611 output = bridge.run_ofctl(['dump-flows', switch_name], check_error=True)
612 for flow_dump in output[0].split('\n'):
613 if flow_match(flow_dump, flow_src):
614 # flow was added correctly
618 def validate_del_flow(self, _dummy_result, switch_name, flow=None):
619 """ Validate removal of the flow
622 # what else we can do?
624 return not self.validate_add_flow(_dummy_result, switch_name, flow)
626 def validate_dump_flows(self, _dummy_result, _dummy_switch_name):
627 """ Validate call of flow dump
631 def validate_disable_rstp(self, _dummy_result, switch_name):
632 """ Validate rstp disable
634 bridge = self._switches[switch_name]
635 return 'rstp_enable : false' in ''.join(bridge.bridge_info())
637 def validate_enable_rstp(self, _dummy_result, switch_name):
638 """ Validate rstp enable
640 bridge = self._switches[switch_name]
641 return 'rstp_enable : true' in ''.join(bridge.bridge_info())
643 def validate_disable_stp(self, _dummy_result, switch_name):
644 """ Validate stp disable
646 bridge = self._switches[switch_name]
647 return 'stp_enable : false' in ''.join(bridge.bridge_info())
649 def validate_enable_stp(self, _dummy_result, switch_name):
650 """ Validate stp enable
652 bridge = self._switches[switch_name]
653 return 'stp_enable : true' in ''.join(bridge.bridge_info())
655 def validate_restart(self, _dummy_result):