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