ovs: Configurable arguments of ovs-*ctl
[vswitchperf.git] / src / ovs / ofctl.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 """Wrapper for an OVS bridge for convenient use of ``ovs-vsctl`` and
16 ``ovs-ofctl`` on it.
17
18 Much of this code is based on ``ovs-lib.py`` from Open Stack:
19
20 https://github.com/openstack/neutron/blob/6eac1dc99124ca024d6a69b3abfa3bc69c735667/neutron/agent/linux/ovs_lib.py
21 """
22 import logging
23 import string
24 import re
25 import netaddr
26
27 from tools import tasks
28 from conf import settings as S
29
30 _OVS_BRIDGE_NAME = S.getValue('VSWITCH_BRIDGE_NAME')
31 _OVS_CMD_TIMEOUT = S.getValue('OVS_CMD_TIMEOUT')
32
33 _CACHE_FILE_NAME = '/tmp/vsperf_flows_cache'
34
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})?)"
37
38 class OFBase(object):
39     """Add/remove/show datapaths using ``ovs-ofctl``.
40     """
41     def __init__(self, timeout=_OVS_CMD_TIMEOUT):
42         """Initialise logger.
43
44         :param timeout: Timeout to be used for each command
45
46         :returns: None
47         """
48         self.logger = logging.getLogger(__name__)
49         self.timeout = timeout
50
51     # helpers
52
53     def run_vsctl(self, args, check_error=False):
54         """Run ``ovs-vsctl`` with supplied arguments.
55
56         In case that timeout is set to -1, then ovs-vsctl
57         will be called with --no-wait option.
58
59         :param args: Arguments to pass to ``ovs-vsctl``
60         :param check_error: Throw exception on error
61
62         :return: None
63         """
64         if self.timeout == -1:
65             cmd = ['sudo', S.getValue('TOOLS')['ovs-vsctl'], '--no-wait'] + \
66                   S.getValue('OVS_VSCTL_ARGS') + args
67         else:
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)
72
73
74     def run_appctl(self, args, check_error=False):
75         """Run ``ovs-appctl`` with supplied arguments.
76
77         :param args: Arguments to pass to ``ovs-appctl``
78         :param check_error: Throw exception on error
79
80         :return: None
81         """
82         cmd = ['sudo', S.getValue('TOOLS')['ovs-appctl'],
83                '--timeout',
84                str(self.timeout)] + S.getValue('OVS_APPCTL_ARGS') + args
85         return tasks.run_task(
86             cmd, self.logger, 'Running ovs-appctl...', check_error)
87
88
89     # datapath management
90
91     def add_br(self, br_name=_OVS_BRIDGE_NAME, params=None):
92         """Add datapath.
93
94         :param br_name: Name of bridge
95
96         :return: Instance of :class OFBridge:
97         """
98         if params is None:
99             params = []
100
101         self.logger.debug('add bridge')
102         self.run_vsctl(['add-br', br_name]+params)
103
104         return OFBridge(br_name, self.timeout)
105
106     def del_br(self, br_name=_OVS_BRIDGE_NAME):
107         """Delete datapath.
108
109         :param br_name: Name of bridge
110
111         :return: None
112         """
113         self.logger.debug('delete bridge')
114         self.run_vsctl(['del-br', br_name])
115
116     # Route and ARP functions
117
118     def add_route(self, network, destination):
119         """Add route to tunneling routing table.
120
121         :param network: Network
122         :param destination: Gateway
123
124         :return: None
125         """
126         self.logger.debug('add ovs/route')
127         self.run_appctl(['ovs/route/add', network, destination])
128
129
130     def set_tunnel_arp(self, ip_addr, mac_addr, br_name=_OVS_BRIDGE_NAME):
131         """Add OVS arp entry for tunneling
132
133         :param ip: IP of bridge
134         :param mac_addr: MAC address of the bridge
135         :param br_name: Name of the bridge
136
137         :return: None
138         """
139         self.logger.debug('tnl/arp/set')
140         self.run_appctl(['tnl/arp/set', br_name, ip_addr, mac_addr])
141
142
143 class OFBridge(OFBase):
144     """Control a bridge instance using ``ovs-vsctl`` and ``ovs-ofctl``.
145     """
146     def __init__(self, br_name=_OVS_BRIDGE_NAME, timeout=_OVS_CMD_TIMEOUT):
147         """Initialise bridge.
148
149         :param br_name: Bridge name
150         :param timeout: Timeout to be used for each command
151
152         :returns: None
153         """
154         super(OFBridge, self).__init__(timeout)
155         self.br_name = br_name
156         self._ports = {}
157         self._cache_file = None
158
159     # context manager
160
161     def __enter__(self):
162         """Create datapath
163
164         :returns: self
165         """
166         return self
167
168     def __exit__(self, type_, value, traceback):
169         """Remove datapath.
170         """
171         if not traceback:
172             self.destroy()
173
174     # helpers
175
176     def run_ofctl(self, args, check_error=False, timeout=None):
177         """Run ``ovs-ofctl`` with supplied arguments.
178
179         :param args: Arguments to pass to ``ovs-ofctl``
180         :param check_error: Throw exception on error
181
182         :return: None
183         """
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)
189
190     def create(self, params=None):
191         """Create bridge.
192         """
193         if params is None:
194             params = []
195
196         self.logger.debug('create bridge')
197         self.add_br(self.br_name, params=params)
198
199     def destroy(self):
200         """Destroy bridge.
201         """
202         self.logger.debug('destroy bridge')
203         self.del_br(self.br_name)
204
205     def reset(self):
206         """Reset bridge.
207         """
208         self.logger.debug('reset bridge')
209         self.destroy()
210         self.create()
211
212     # port management
213
214     def add_port(self, port_name, params):
215         """Add port to bridge.
216
217         :param port_name: Name of port
218         :param params: Additional list of parameters to add-port
219
220         :return: OpenFlow port number for the port
221         """
222         self.logger.debug('add port')
223         self.run_vsctl(['add-port', self.br_name, port_name]+params)
224
225         # This is how port number allocation works currently
226         # This possibly will not work correctly if there are port deletions
227         # in between
228         of_port = len(self._ports) + 1
229         self._ports[port_name] = (of_port, params)
230         return of_port
231
232     def del_port(self, port_name):
233         """Remove port from bridge.
234
235         :param port_name: Name of port
236
237         :return: None
238         """
239         self.logger.debug('delete port')
240         self.run_vsctl(['del-port', self.br_name, port_name])
241         self._ports.pop(port_name)
242
243     def set_db_attribute(self, table_name, record, column, value):
244         """Set database attribute.
245
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
250
251         :return: None
252         """
253         self.logger.debug('set attribute')
254         self.run_vsctl(['set', table_name, record, '%s=%s' % (column, value)])
255
256     def get_ports(self):
257         """Get the ports of this bridge
258
259         Structure of the returned ports dictionary is
260         'portname': (openflow_port_number, extra_parameters)
261
262         Example:
263         ports = {
264             'dpdkport0':
265                 (1, ['--', 'set', 'Interface', 'dpdkport0', 'type=dpdk']),
266             'dpdkvhostport0':
267                 (2, ['--', 'set', 'Interface', 'dpdkvhostport0',
268                      'type=dpdkvhost'])
269         }
270
271         :return: Dictionary of ports
272         """
273         return self._ports
274
275     def clear_db_attribute(self, table_name, record, column):
276         """Clear database attribute.
277
278         :param table_name: Name of table
279         :param record: Name of record
280         :param column: Name of column
281
282         :return: None
283         """
284         self.logger.debug('clear attribute')
285         self.run_vsctl(['clear', table_name, record, column])
286
287     # flow mangement
288
289     def add_flow(self, flow, cache='off'):
290         """Add flow to bridge.
291
292         :param flow: Flow description as a dictionary
293         For flow dictionary structure, see function flow_key
294
295         :return: None
296         """
297         # insert flows from cache into OVS if needed
298         if cache == 'flush':
299             if self._cache_file is None:
300                 self.logger.error('flow cache flush called, but nothing is cached')
301                 return
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)
306             return
307
308         if not flow.get('actions'):
309             self.logger.error('add flow requires actions')
310             return
311
312         _flow_key = flow_key(flow)
313         self.logger.debug('key : %s', _flow_key)
314
315         # insert flow to the cache or OVS
316         if cache == 'on':
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')
321         else:
322             self.run_ofctl(['add-flow', self.br_name, _flow_key])
323
324     def del_flow(self, flow):
325         """Delete flow from bridge.
326
327         :param flow: Flow description as a dictionary
328         For flow dictionary structure, see function flow_key
329         flow=None will delete all flows
330
331         :return: None
332         """
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])
337
338     def del_flows(self):
339         """Delete all flows from bridge.
340         """
341         self.logger.debug('delete flows')
342         self.run_ofctl(['del-flows', self.br_name])
343
344     def dump_flows(self):
345         """Dump all flows from bridge.
346         """
347         self.logger.debug('dump flows')
348         self.run_ofctl(['dump-flows', self.br_name], timeout=120)
349
350     def set_stp(self, enable=True):
351         """
352         Set stp status
353         :param enable: Boolean to enable or disable stp
354         :return: None
355         """
356         self.logger.debug(
357             'Setting stp on bridge to %s', 'on' if enable else 'off')
358         self.run_vsctl(
359             ['set', 'Bridge', self.br_name, 'stp_enable={}'.format(
360                 'true' if enable else 'false')])
361
362     def set_rstp(self, enable=True):
363         """
364         Set rstp status
365         :param enable: Boolean to enable or disable rstp
366         :return: None
367         """
368         self.logger.debug(
369             'Setting rstp on bridge to %s', 'on' if enable else 'off')
370         self.run_vsctl(
371             ['set', 'Bridge', self.br_name, 'rstp_enable={}'.format(
372                 'true' if enable else 'false')])
373
374     def bridge_info(self):
375         """
376         Get bridge info
377         :return: Returns bridge info from list bridge command
378         """
379         return self.run_vsctl(['list', 'bridge', self.br_name])
380
381 #
382 # helper functions
383 #
384
385 def flow_key(flow):
386     """Model a flow key string for ``ovs-ofctl``.
387
388     Syntax taken from ``ovs-ofctl`` manpages:
389         http://openvswitch.org/cgi-bin/ovsman.cgi?page=utilities%2Fovs-ofctl.8
390
391     Example flow dictionary:
392     flow = {
393         'in_port': '1',
394         'idle_timeout': '0',
395         'actions': ['output:3']
396     }
397
398     :param flow: Flow description as a dictionary
399
400     :return: String
401     :rtype: str
402     """
403     _flow_add_key = string.Template('${fields},action=${actions}')
404     _flow_del_key = string.Template('${fields}')
405
406     field_params = []
407
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})
412
413     field_params_str = ','.join(field_params)
414
415     _flow_key_param = {
416         'fields': field_params_str,
417     }
418
419     # no actions == delete key
420     if 'actions' in flow:
421         _flow_key_param['actions'] = ','.join(flow['actions'])
422
423         flow_str = _flow_add_key.substitute(_flow_key_param)
424     else:
425         flow_str = _flow_del_key.substitute(_flow_key_param)
426
427     return flow_str
428
429 def flow_match(flow_dump, flow_src):
430     """ Compares two flows
431
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()
434
435     :return: boolean
436     """
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')
456
457     # modify IPv4 CIDR to real network addresses
458     for ipv4_cidr in re.findall(_IPV4_REGEX, flow_src):
459         if ipv4_cidr[2]:
460             tmp_cidr = str(netaddr.IPNetwork(ipv4_cidr[0]).cidr)
461             flow_src = flow_src.replace(ipv4_cidr[0], tmp_cidr)
462
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)
466
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 len(flow_src_ctrl) else False