1 # Copyright 2015-2017 Intel Corporation.
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 """Wrapper for an OVS bridge for convenient use of ``ovs-vsctl`` and
18 Much of this code is based on ``ovs-lib.py`` from Open Stack:
20 https://github.com/openstack/neutron/blob/6eac1dc99124ca024d6a69b3abfa3bc69c735667/neutron/agent/linux/ovs_lib.py
27 from tools import tasks
28 from conf import settings as S
30 _OVS_BRIDGE_NAME = S.getValue('VSWITCH_BRIDGE_NAME')
31 _OVS_CMD_TIMEOUT = S.getValue('OVS_CMD_TIMEOUT')
33 _CACHE_FILE_NAME = '/tmp/vsperf_flows_cache'
35 # only simple regex is used; validity of IPv4 is not checked by regex
36 _IPV4_REGEX = r"([0-9]{1,3}(\.[0-9]{1,3}){3}(\/[0-9]{1,2})?)"
39 """Add/remove/show datapaths using ``ovs-ofctl``.
41 def __init__(self, timeout=_OVS_CMD_TIMEOUT):
44 :param timeout: Timeout to be used for each command
48 self.logger = logging.getLogger(__name__)
49 self.timeout = timeout
53 def run_vsctl(self, args, check_error=False):
54 """Run ``ovs-vsctl`` with supplied arguments.
56 In case that timeout is set to -1, then ovs-vsctl
57 will be called with --no-wait option.
59 :param args: Arguments to pass to ``ovs-vsctl``
60 :param check_error: Throw exception on error
64 if self.timeout == -1:
65 cmd = ['sudo', S.getValue('TOOLS')['ovs-vsctl'], '--no-wait'] + \
66 S.getValue('OVS_VSCTL_ARGS') + args
68 cmd = ['sudo', S.getValue('TOOLS')['ovs-vsctl'], '--timeout',
69 str(self.timeout)] + S.getValue('OVS_VSCTL_ARGS') + args
70 return tasks.run_task(
71 cmd, self.logger, 'Running ovs-vsctl...', check_error)
74 def run_appctl(self, args, check_error=False):
75 """Run ``ovs-appctl`` with supplied arguments.
77 :param args: Arguments to pass to ``ovs-appctl``
78 :param check_error: Throw exception on error
82 cmd = ['sudo', S.getValue('TOOLS')['ovs-appctl'],
84 str(self.timeout)] + S.getValue('OVS_APPCTL_ARGS') + args
85 return tasks.run_task(
86 cmd, self.logger, 'Running ovs-appctl...', check_error)
91 def add_br(self, br_name=_OVS_BRIDGE_NAME, params=None):
94 :param br_name: Name of bridge
96 :return: Instance of :class OFBridge:
101 self.logger.debug('add bridge')
102 self.run_vsctl(['add-br', br_name]+params)
104 return OFBridge(br_name, self.timeout)
106 def del_br(self, br_name=_OVS_BRIDGE_NAME):
109 :param br_name: Name of bridge
113 self.logger.debug('delete bridge')
114 self.run_vsctl(['del-br', br_name])
116 # Route and ARP functions
118 def add_route(self, network, destination):
119 """Add route to tunneling routing table.
121 :param network: Network
122 :param destination: Gateway
126 self.logger.debug('add ovs/route')
127 self.run_appctl(['ovs/route/add', network, destination])
130 def set_tunnel_arp(self, ip_addr, mac_addr, br_name=_OVS_BRIDGE_NAME):
131 """Add OVS arp entry for tunneling
133 :param ip: IP of bridge
134 :param mac_addr: MAC address of the bridge
135 :param br_name: Name of the bridge
139 self.logger.debug('tnl/arp/set')
140 self.run_appctl(['tnl/arp/set', br_name, ip_addr, mac_addr])
143 class OFBridge(OFBase):
144 """Control a bridge instance using ``ovs-vsctl`` and ``ovs-ofctl``.
146 def __init__(self, br_name=_OVS_BRIDGE_NAME, timeout=_OVS_CMD_TIMEOUT):
147 """Initialise bridge.
149 :param br_name: Bridge name
150 :param timeout: Timeout to be used for each command
154 super(OFBridge, self).__init__(timeout)
155 self.br_name = br_name
157 self._cache_file = None
168 def __exit__(self, type_, value, traceback):
176 def run_ofctl(self, args, check_error=False, timeout=None):
177 """Run ``ovs-ofctl`` with supplied arguments.
179 :param args: Arguments to pass to ``ovs-ofctl``
180 :param check_error: Throw exception on error
184 tmp_timeout = self.timeout if timeout is None else timeout
185 cmd = ['sudo', S.getValue('TOOLS')['ovs-ofctl'], '--timeout',
186 str(tmp_timeout)] + S.getValue('OVS_OFCTL_ARGS') + args
187 return tasks.run_task(
188 cmd, self.logger, 'Running ovs-ofctl...', check_error)
190 def create(self, params=None):
196 self.logger.debug('create bridge')
197 self.add_br(self.br_name, params=params)
202 self.logger.debug('destroy bridge')
203 self.del_br(self.br_name)
208 self.logger.debug('reset bridge')
214 def add_port(self, port_name, params):
215 """Add port to bridge.
217 :param port_name: Name of port
218 :param params: Additional list of parameters to add-port
220 :return: OpenFlow port number for the port
222 self.logger.debug('add port')
223 self.run_vsctl(['add-port', self.br_name, port_name]+params)
225 # This is how port number allocation works currently
226 # This possibly will not work correctly if there are port deletions
228 of_port = len(self._ports) + 1
229 self._ports[port_name] = (of_port, params)
232 def del_port(self, port_name):
233 """Remove port from bridge.
235 :param port_name: Name of port
239 self.logger.debug('delete port')
240 self.run_vsctl(['del-port', self.br_name, port_name])
241 self._ports.pop(port_name)
243 def set_db_attribute(self, table_name, record, column, value):
244 """Set database attribute.
246 :param table_name: Name of table
247 :param record: Name of record
248 :param column: Name of column
249 :param value: Value to set
253 self.logger.debug('set attribute')
254 self.run_vsctl(['set', table_name, record, '%s=%s' % (column, value)])
257 """Get the ports of this bridge
259 Structure of the returned ports dictionary is
260 'portname': (openflow_port_number, extra_parameters)
265 (1, ['--', 'set', 'Interface', 'dpdkport0', 'type=dpdk']),
267 (2, ['--', 'set', 'Interface', 'dpdkvhostport0',
271 :return: Dictionary of ports
275 def clear_db_attribute(self, table_name, record, column):
276 """Clear database attribute.
278 :param table_name: Name of table
279 :param record: Name of record
280 :param column: Name of column
284 self.logger.debug('clear attribute')
285 self.run_vsctl(['clear', table_name, record, column])
289 def add_flow(self, flow, cache='off'):
290 """Add flow to bridge.
292 :param flow: Flow description as a dictionary
293 For flow dictionary structure, see function flow_key
297 # insert flows from cache into OVS if needed
299 if self._cache_file is None:
300 self.logger.error('flow cache flush called, but nothing is cached')
302 self.logger.debug('flows cached in %s will be added to the bridge', _CACHE_FILE_NAME)
303 self._cache_file.close()
304 self._cache_file = None
305 self.run_ofctl(['add-flows', self.br_name, _CACHE_FILE_NAME], timeout=600)
308 if not flow.get('actions'):
309 self.logger.error('add flow requires actions')
312 _flow_key = flow_key(flow)
313 self.logger.debug('key : %s', _flow_key)
315 # insert flow to the cache or OVS
317 # create and open cache file if needed
318 if self._cache_file is None:
319 self._cache_file = open(_CACHE_FILE_NAME, 'w')
320 self._cache_file.write(_flow_key + '\n')
322 self.run_ofctl(['add-flow', self.br_name, _flow_key])
324 def del_flow(self, flow):
325 """Delete flow from bridge.
327 :param flow: Flow description as a dictionary
328 For flow dictionary structure, see function flow_key
329 flow=None will delete all flows
333 self.logger.debug('delete flow')
334 _flow_key = flow_key(flow)
335 self.logger.debug('key : %s', _flow_key)
336 self.run_ofctl(['del-flows', self.br_name, _flow_key])
339 """Delete all flows from bridge.
341 self.logger.debug('delete flows')
342 self.run_ofctl(['del-flows', self.br_name])
344 def dump_flows(self):
345 """Dump all flows from bridge.
347 self.logger.debug('dump flows')
348 self.run_ofctl(['dump-flows', self.br_name], timeout=120)
350 def set_stp(self, enable=True):
353 :param enable: Boolean to enable or disable stp
357 'Setting stp on bridge to %s', 'on' if enable else 'off')
359 ['set', 'Bridge', self.br_name, 'stp_enable={}'.format(
360 'true' if enable else 'false')])
362 def set_rstp(self, enable=True):
365 :param enable: Boolean to enable or disable rstp
369 'Setting rstp on bridge to %s', 'on' if enable else 'off')
371 ['set', 'Bridge', self.br_name, 'rstp_enable={}'.format(
372 'true' if enable else 'false')])
374 def bridge_info(self):
377 :return: Returns bridge info from list bridge command
379 return self.run_vsctl(['list', 'bridge', self.br_name])
386 """Model a flow key string for ``ovs-ofctl``.
388 Syntax taken from ``ovs-ofctl`` manpages:
389 http://openvswitch.org/cgi-bin/ovsman.cgi?page=utilities%2Fovs-ofctl.8
391 Example flow dictionary:
395 'actions': ['output:3']
398 :param flow: Flow description as a dictionary
403 _flow_add_key = string.Template('${fields},action=${actions}')
404 _flow_del_key = string.Template('${fields}')
408 user_params = (x for x in list(flow.items()) if x[0] != 'actions')
409 for (key, default) in user_params:
410 field_params.append('%(field)s=%(value)s' %
411 {'field': key, 'value': default})
413 field_params_str = ','.join(field_params)
416 'fields': field_params_str,
419 # no actions == delete key
420 if 'actions' in flow:
421 _flow_key_param['actions'] = ','.join(flow['actions'])
423 flow_str = _flow_add_key.substitute(_flow_key_param)
425 flow_str = _flow_del_key.substitute(_flow_key_param)
429 def flow_match(flow_dump, flow_src):
430 """ Compares two flows
432 :param flow_dump: string - a string with flow obtained by ovs-ofctl dump-flows
433 :param flow_src: string - a string with flow obtained by call of flow_key()
437 # perform unifications on both source and destination flows
438 flow_dump = flow_dump.replace('actions=', 'action=')
439 flow_src = flow_src.replace('actions=', 'action=')
440 # For complex flows the output of "ovs-ofctl dump-flows" can use the
441 # shorthand notation.
442 # eg if we set a flow with constraints on UDP ports like in the following
443 # {'dl_type': '0x0800', 'nw_proto': '17', 'in_port': '1', 'udp_dst': '0', 'actions': ['output:2']}
444 # dump-flows output can combine the first 2 constraints into 'udp' and translate
445 # 'udp_dst' into 'tp_dst' like
446 # "udp,in_port=1,tp_dst=0 actions=output:2".
447 # So the next replacements are needed.
448 flow_dump = flow_dump.replace('ip', 'dl_type=0x0800')
449 flow_dump = flow_dump.replace('tcp', 'nw_proto=6,dl_type=0x0800')
450 flow_dump = flow_dump.replace('udp', 'nw_proto=17,dl_type=0x0800')
451 flow_src = flow_src.replace('udp_src', 'tp_src')
452 flow_src = flow_src.replace('udp_dst', 'tp_dst')
453 flow_src = flow_src.replace('tcp_src', 'tp_src')
454 flow_src = flow_src.replace('tcp_dst', 'tp_dst')
455 flow_src = flow_src.replace('0x800', '0x0800')
457 # modify IPv4 CIDR to real network addresses
458 for ipv4_cidr in re.findall(_IPV4_REGEX, flow_src):
460 tmp_cidr = str(netaddr.IPNetwork(ipv4_cidr[0]).cidr)
461 flow_src = flow_src.replace(ipv4_cidr[0], tmp_cidr)
463 # split flow strings into lists of comparable elements
464 flow_dump_list = re.findall(r"[\w.:=()/]+", flow_dump)
465 flow_src_list = re.findall(r"[\w.:=()/]+", flow_src)
467 # check if all items from source flow are present in dump flow
468 flow_src_ctrl = list(flow_src_list)
469 for rule in flow_src_list:
470 if rule in flow_dump_list:
471 flow_src_ctrl.remove(rule)
472 return True if not flow_src_ctrl else False