Merge "conf/integration: Fix typo on Integration testcase example."
[vswitchperf.git] / src / ovs / ofctl.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 """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
23 import os
24 import logging
25 import string
26 import re
27
28 from tools import tasks
29 from conf import settings
30
31 _OVS_VSCTL_BIN = os.path.join(settings.getValue('OVS_DIR'), 'utilities',
32                               'ovs-vsctl')
33 _OVS_OFCTL_BIN = os.path.join(settings.getValue('OVS_DIR'), 'utilities',
34                               'ovs-ofctl')
35 _OVS_APPCTL_BIN = os.path.join(settings.getValue('OVS_DIR'), 'utilities',
36                                'ovs-appctl')
37
38 _OVS_BRIDGE_NAME = settings.getValue('VSWITCH_BRIDGE_NAME')
39
40 _CACHE_FILE_NAME = '/tmp/vsperf_flows_cache'
41
42 class OFBase(object):
43     """Add/remove/show datapaths using ``ovs-ofctl``.
44     """
45     def __init__(self, timeout=10):
46         """Initialise logger.
47
48         :param timeout: Timeout to be used for each command
49
50         :returns: None
51         """
52         self.logger = logging.getLogger(__name__)
53         self.timeout = timeout
54
55     # helpers
56
57     def run_vsctl(self, args, check_error=False):
58         """Run ``ovs-vsctl`` with supplied arguments.
59
60         In case that timeout is set to -1, then ovs-vsctl
61         will be called with --no-wait option.
62
63         :param args: Arguments to pass to ``ovs-vsctl``
64         :param check_error: Throw exception on error
65
66         :return: None
67         """
68         if self.timeout == -1:
69             cmd = ['sudo', _OVS_VSCTL_BIN, '--no-wait'] + args
70         else:
71             cmd = ['sudo', _OVS_VSCTL_BIN, '--timeout', str(self.timeout)] + args
72         return tasks.run_task(
73             cmd, self.logger, 'Running ovs-vsctl...', check_error)
74
75
76     def run_appctl(self, args, check_error=False):
77         """Run ``ovs-appctl`` with supplied arguments.
78
79         :param args: Arguments to pass to ``ovs-appctl``
80         :param check_error: Throw exception on error
81
82         :return: None
83         """
84         cmd = ['sudo', _OVS_APPCTL_BIN,
85                '--timeout',
86                str(self.timeout)] + args
87         return tasks.run_task(
88             cmd, self.logger, 'Running ovs-appctl...', check_error)
89
90
91     # datapath management
92
93     def add_br(self, br_name=_OVS_BRIDGE_NAME, params=None):
94         """Add datapath.
95
96         :param br_name: Name of bridge
97
98         :return: Instance of :class OFBridge:
99         """
100         if params is None:
101             params = []
102
103         self.logger.debug('add bridge')
104         self.run_vsctl(['add-br', br_name]+params)
105
106         return OFBridge(br_name, self.timeout)
107
108     def del_br(self, br_name=_OVS_BRIDGE_NAME):
109         """Delete datapath.
110
111         :param br_name: Name of bridge
112
113         :return: None
114         """
115         self.logger.debug('delete bridge')
116         self.run_vsctl(['del-br', br_name])
117
118     # Route and ARP functions
119
120     def add_route(self, network, destination):
121         """Add route to tunneling routing table.
122
123         :param network: Network
124         :param destination: Gateway
125
126         :return: None
127         """
128         self.logger.debug('add ovs/route')
129         self.run_appctl(['ovs/route/add', network, destination])
130
131
132     def set_tunnel_arp(self, ip_addr, mac_addr, br_name=_OVS_BRIDGE_NAME):
133         """Add OVS arp entry for tunneling
134
135         :param ip: IP of bridge
136         :param mac_addr: MAC address of the bridge
137         :param br_name: Name of the bridge
138
139         :return: None
140         """
141         self.logger.debug('tnl/arp/set')
142         self.run_appctl(['tnl/arp/set', br_name, ip_addr, mac_addr])
143
144
145 class OFBridge(OFBase):
146     """Control a bridge instance using ``ovs-vsctl`` and ``ovs-ofctl``.
147     """
148     def __init__(self, br_name=_OVS_BRIDGE_NAME, timeout=10):
149         """Initialise bridge.
150
151         :param br_name: Bridge name
152         :param timeout: Timeout to be used for each command
153
154         :returns: None
155         """
156         super(OFBridge, self).__init__(timeout)
157         self.br_name = br_name
158         self._ports = {}
159         self._cache_file = None
160
161     # context manager
162
163     def __enter__(self):
164         """Create datapath
165
166         :returns: self
167         """
168         return self
169
170     def __exit__(self, type_, value, traceback):
171         """Remove datapath.
172         """
173         if not traceback:
174             self.destroy()
175
176     # helpers
177
178     def run_ofctl(self, args, check_error=False, timeout=None):
179         """Run ``ovs-ofctl`` with supplied arguments.
180
181         :param args: Arguments to pass to ``ovs-ofctl``
182         :param check_error: Throw exception on error
183
184         :return: None
185         """
186         tmp_timeout = self.timeout if timeout == None else timeout
187         cmd = ['sudo', _OVS_OFCTL_BIN, '-O', 'OpenFlow13', '--timeout',
188                str(tmp_timeout)] + args
189         return tasks.run_task(
190             cmd, self.logger, 'Running ovs-ofctl...', check_error)
191
192     def create(self, params=None):
193         """Create bridge.
194         """
195         if params is None:
196             params = []
197
198         self.logger.debug('create bridge')
199         self.add_br(self.br_name, params=params)
200
201     def destroy(self):
202         """Destroy bridge.
203         """
204         self.logger.debug('destroy bridge')
205         self.del_br(self.br_name)
206
207     def reset(self):
208         """Reset bridge.
209         """
210         self.logger.debug('reset bridge')
211         self.destroy()
212         self.create()
213
214     # port management
215
216     def add_port(self, port_name, params):
217         """Add port to bridge.
218
219         :param port_name: Name of port
220         :param params: Additional list of parameters to add-port
221
222         :return: OpenFlow port number for the port
223         """
224         self.logger.debug('add port')
225         self.run_vsctl(['add-port', self.br_name, port_name]+params)
226
227         # This is how port number allocation works currently
228         # This possibly will not work correctly if there are port deletions
229         # in between
230         of_port = len(self._ports) + 1
231         self._ports[port_name] = (of_port, params)
232         return of_port
233
234     def del_port(self, port_name):
235         """Remove port from bridge.
236
237         :param port_name: Name of port
238
239         :return: None
240         """
241         self.logger.debug('delete port')
242         self.run_vsctl(['del-port', self.br_name, port_name])
243         self._ports.pop(port_name)
244
245     def set_db_attribute(self, table_name, record, column, value):
246         """Set database attribute.
247
248         :param table_name: Name of table
249         :param record: Name of record
250         :param column: Name of column
251         :param value: Value to set
252
253         :return: None
254         """
255         self.logger.debug('set attribute')
256         self.run_vsctl(['set', table_name, record, '%s=%s' % (column, value)])
257
258     def get_ports(self):
259         """Get the ports of this bridge
260
261         Structure of the returned ports dictionary is
262         'portname': (openflow_port_number, extra_parameters)
263
264         Example:
265         ports = {
266             'dpdkport0':
267                 (1, ['--', 'set', 'Interface', 'dpdkport0', 'type=dpdk']),
268             'dpdkvhostport0':
269                 (2, ['--', 'set', 'Interface', 'dpdkvhostport0',
270                      'type=dpdkvhost'])
271         }
272
273         :return: Dictionary of ports
274         """
275         return self._ports
276
277     def clear_db_attribute(self, table_name, record, column):
278         """Clear database attribute.
279
280         :param table_name: Name of table
281         :param record: Name of record
282         :param column: Name of column
283
284         :return: None
285         """
286         self.logger.debug('clear attribute')
287         self.run_vsctl(['clear', table_name, record, column])
288
289     # flow mangement
290
291     def add_flow(self, flow, cache='off'):
292         """Add flow to bridge.
293
294         :param flow: Flow description as a dictionary
295         For flow dictionary structure, see function flow_key
296
297         :return: None
298         """
299         # insert flows from cache into OVS if needed
300         if cache == 'flush':
301             if self._cache_file == None:
302                 self.logger.error('flow cache flush called, but nothing is cached')
303                 return
304             self.logger.debug('flows cached in %s will be added to the bridge', _CACHE_FILE_NAME)
305             self._cache_file.close()
306             self._cache_file = None
307             self.run_ofctl(['add-flows', self.br_name, _CACHE_FILE_NAME], timeout=600)
308             return
309
310         if not flow.get('actions'):
311             self.logger.error('add flow requires actions')
312             return
313
314         _flow_key = flow_key(flow)
315         self.logger.debug('key : %s', _flow_key)
316
317         # insert flow to the cache or OVS
318         if cache == 'on':
319             # create and open cache file if needed
320             if self._cache_file == None:
321                 self._cache_file = open(_CACHE_FILE_NAME, 'w')
322             self._cache_file.write(_flow_key + '\n')
323         else:
324             self.run_ofctl(['add-flow', self.br_name, _flow_key])
325
326     def del_flow(self, flow):
327         """Delete flow from bridge.
328
329         :param flow: Flow description as a dictionary
330         For flow dictionary structure, see function flow_key
331         flow=None will delete all flows
332
333         :return: None
334         """
335         self.logger.debug('delete flow')
336         _flow_key = flow_key(flow)
337         self.logger.debug('key : %s', _flow_key)
338         self.run_ofctl(['del-flows', self.br_name, _flow_key])
339
340     def del_flows(self):
341         """Delete all flows from bridge.
342         """
343         self.logger.debug('delete flows')
344         self.run_ofctl(['del-flows', self.br_name])
345
346     def dump_flows(self):
347         """Dump all flows from bridge.
348         """
349         self.logger.debug('dump flows')
350         self.run_ofctl(['dump-flows', self.br_name], timeout=120)
351
352     def set_stp(self, enable=True):
353         """
354         Set stp status
355         :param enable: Boolean to enable or disable stp
356         :return: None
357         """
358         self.logger.debug(
359             'Setting stp on bridge to %s', 'on' if enable else 'off')
360         self.run_vsctl(
361             ['set', 'Bridge', self.br_name, 'stp_enable={}'.format(
362                 'true' if enable else 'false')])
363
364     def set_rstp(self, enable=True):
365         """
366         Set rstp status
367         :param enable: Boolean to enable or disable rstp
368         :return: None
369         """
370         self.logger.debug(
371             'Setting rstp on bridge to %s', 'on' if enable else 'off')
372         self.run_vsctl(
373             ['set', 'Bridge', self.br_name, 'rstp_enable={}'.format(
374                 'true' if enable else 'false')])
375
376     def bridge_info(self):
377         """
378         Get bridge info
379         :return: Returns bridge info from list bridge command
380         """
381         return self.run_vsctl(['list', 'bridge', self.br_name])
382
383 #
384 # helper functions
385 #
386
387 def flow_key(flow):
388     """Model a flow key string for ``ovs-ofctl``.
389
390     Syntax taken from ``ovs-ofctl`` manpages:
391         http://openvswitch.org/cgi-bin/ovsman.cgi?page=utilities%2Fovs-ofctl.8
392
393     Example flow dictionary:
394     flow = {
395         'in_port': '1',
396         'idle_timeout': '0',
397         'actions': ['output:3']
398     }
399
400     :param flow: Flow description as a dictionary
401
402     :return: String
403     :rtype: str
404     """
405     _flow_add_key = string.Template('${fields},action=${actions}')
406     _flow_del_key = string.Template('${fields}')
407
408     field_params = []
409
410     user_params = (x for x in list(flow.items()) if x[0] != 'actions')
411     for (key, default) in user_params:
412         field_params.append('%(field)s=%(value)s' %
413                             {'field': key, 'value': default})
414
415     field_params_str = ','.join(field_params)
416
417     _flow_key_param = {
418         'fields': field_params_str,
419     }
420
421     # no actions == delete key
422     if 'actions' in flow:
423         _flow_key_param['actions'] = ','.join(flow['actions'])
424
425         flow_str = _flow_add_key.substitute(_flow_key_param)
426     else:
427         flow_str = _flow_del_key.substitute(_flow_key_param)
428
429     return flow_str
430
431 def flow_match(flow_dump, flow_src):
432     """ Compares two flows
433
434     :param flow_dump: string - a string with flow obtained by ovs-ofctl dump-flows
435     :param flow_src: string - a string with flow obtained by call of flow_key()
436
437     :return: boolean
438     """
439     # perform unifications on both source and destination flows
440     flow_dump = flow_dump.replace('actions=', 'action=')
441     flow_src = flow_src.replace('actions=', 'action=')
442     # For complex flows the output of "ovs-ofctl dump-flows" can use the
443     # shorthand notation.
444     # eg if we set a flow with constraints on UDP ports like in the following
445     # {'dl_type': '0x0800', 'nw_proto': '17', 'in_port': '1', 'udp_dst': '0', 'actions': ['output:2']}
446     # dump-flows output can combine the first 2 constraints into 'udp' and translate
447     # 'udp_dst' into 'tp_dst' like
448     # "udp,in_port=1,tp_dst=0 actions=output:2".
449     # So the next replacements are needed.
450     flow_dump = flow_dump.replace('ip', 'dl_type=0x0800')
451     flow_dump = flow_dump.replace('tcp', 'nw_proto=6,dl_type=0x0800')
452     flow_dump = flow_dump.replace('udp', 'nw_proto=17,dl_type=0x0800')
453     flow_src = flow_src.replace('udp_src', 'tp_src')
454     flow_src = flow_src.replace('udp_dst', 'tp_dst')
455     flow_src = flow_src.replace('tcp_src', 'tp_src')
456     flow_src = flow_src.replace('tcp_dst', 'tp_dst')
457
458     # split flow strings into lists of comparable elements
459     flow_dump_list = re.findall(r"[\w.:=()]+", flow_dump)
460     flow_src_list = re.findall(r"[\w.:=()]+", flow_src)
461
462     # check if all items from source flow are present in dump flow
463     flow_src_ctrl = list(flow_src_list)
464     for rule in flow_src_list:
465         if rule in flow_dump_list:
466             flow_src_ctrl.remove(rule)
467     return True if not len(flow_src_ctrl) else False