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