1# Copyright (C) 2012 Nippon Telegraph and Telephone Corporation.
2# Copyright (C) 2012 Isaku Yamahata <yamahata at private email ne jp>
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Wrapper utility library of :py:mod:`ryu.lib.ovs.vsctl`
19"""
20
21import functools
22import logging
23
24from ryu import cfg
25import ryu.exception as ryu_exc
26import ryu.lib.dpid as dpid_lib
27import ryu.lib.ovs.vsctl as ovs_vsctl
28from ryu.lib.ovs.vsctl import valid_ovsdb_addr
29
30
31LOG = logging.getLogger(__name__)
32
33CONF = cfg.CONF
34CONF.register_opts([
35    cfg.IntOpt('ovsdb-timeout', default=2, help='ovsdb timeout')
36])
37
38
39class OVSBridgeNotFound(ryu_exc.RyuException):
40    message = 'no bridge for datapath_id %(datapath_id)s'
41
42
43class VifPort(object):
44
45    def __init__(self, port_name, ofport, vif_id, vif_mac, switch):
46        super(VifPort, self).__init__()
47        self.port_name = port_name
48        self.ofport = ofport
49        self.vif_id = vif_id
50        self.vif_mac = vif_mac
51        self.switch = switch
52
53    def __str__(self):
54        return ('iface-id=%s, '
55                'vif_mac=%s, '
56                'port_name=%s, '
57                'ofport=%d, '
58                'bridge_name=%s' % (self.vif_id,
59                                    self.vif_mac,
60                                    self.port_name,
61                                    self.ofport,
62                                    self.switch.br_name))
63
64
65class TunnelPort(object):
66
67    def __init__(self, port_name, ofport, tunnel_type, local_ip, remote_ip):
68        super(TunnelPort, self).__init__()
69        self.port_name = port_name
70        self.ofport = ofport
71        self.tunnel_type = tunnel_type
72        self.local_ip = local_ip
73        self.remote_ip = remote_ip
74
75    def __eq__(self, other):
76        return (self.port_name == other.port_name and
77                self.ofport == other.ofport and
78                self.tunnel_type == other.tunnel_type and
79                self.local_ip == other.local_ip and
80                self.remote_ip == other.remote_ip)
81
82    def __str__(self):
83        return ('port_name=%s, '
84                'ofport=%s, '
85                'type=%s, '
86                'local_ip=%s, '
87                'remote_ip=%s' % (self.port_name,
88                                  self.ofport,
89                                  self.tunnel_type,
90                                  self.local_ip,
91                                  self.remote_ip))
92
93
94class OVSBridge(object):
95    """
96    Class to provide wrapper utilities of :py:mod:`ryu.lib.ovs.vsctl.VSCtl`
97
98    ``CONF`` is a instance of ``oslo_config.cfg.ConfigOpts``.
99    Mostly ``self.CONF`` is sufficient to instantiate this class from your Ryu
100    application.
101
102    ``datapath_id`` specifies Datapath ID of the target OVS instance.
103
104    ``ovsdb_addr`` specifies the address of the OVS instance.
105    Automatically validated when you call ``init()`` method.
106    Refer to :py:mod:`ryu.lib.ovs.vsctl.valid_ovsdb_addr` for the format of
107    this address.
108
109    if ``timeout`` is omitted, ``CONF.ovsdb_timeout`` will be used as the
110    default value.
111
112    Usage of ``timeout`` and ``exception`` is the same with ``timeout_sec``
113    and ``exception`` of :py:mod:`ryu.lib.ovs.vsctl.VSCtl.run_command`.
114    """
115
116    def __init__(self, CONF, datapath_id, ovsdb_addr, timeout=None,
117                 exception=None):
118        super(OVSBridge, self).__init__()
119        self.datapath_id = datapath_id
120        self.ovsdb_addr = ovsdb_addr
121        self.vsctl = ovs_vsctl.VSCtl(ovsdb_addr)
122        self.timeout = timeout or CONF.ovsdb_timeout
123        self.exception = exception
124
125        self.br_name = None
126
127    def run_command(self, commands):
128        """
129        Executes the given commands and sends OVSDB messages.
130
131        ``commands`` must be a list of
132        :py:mod:`ryu.lib.ovs.vsctl.VSCtlCommand`.
133
134        The given ``timeout`` and ``exception`` when instantiation will be used
135        to call :py:mod:`ryu.lib.ovs.vsctl.VSCtl.run_command`.
136        """
137        self.vsctl.run_command(commands, self.timeout, self.exception)
138
139    def init(self):
140        """
141        Validates the given ``ovsdb_addr`` and connects to OVS instance.
142
143        If failed to connect to OVS instance or the given ``datapath_id`` does
144        not match with the Datapath ID of the connected OVS instance, raises
145        :py:mod:`ryu.lib.ovs.bridge.OVSBridgeNotFound` exception.
146        """
147        if not valid_ovsdb_addr(self.ovsdb_addr):
148            raise ValueError('Invalid OVSDB address: %s' % self.ovsdb_addr)
149        if self.br_name is None:
150            self.br_name = self._get_bridge_name()
151
152    def _get_bridge_name(self):
153        """ get Bridge name of a given 'datapath_id' """
154        command = ovs_vsctl.VSCtlCommand(
155            'find',
156            ('Bridge',
157             'datapath_id=%s' % dpid_lib.dpid_to_str(self.datapath_id)))
158        self.run_command([command])
159        if not isinstance(command.result, list) or len(command.result) != 1:
160            raise OVSBridgeNotFound(
161                datapath_id=dpid_lib.dpid_to_str(self.datapath_id))
162        return command.result[0].name
163
164    def get_controller(self):
165        """
166        Gets the configured OpenFlow controller address.
167
168        This method is corresponding to the following ovs-vsctl command::
169
170            $ ovs-vsctl get-controller <bridge>
171        """
172        command = ovs_vsctl.VSCtlCommand('get-controller', [self.br_name])
173        self.run_command([command])
174        return command.result[0]
175
176    def set_controller(self, controllers):
177        """
178        Sets the OpenFlow controller address.
179
180        This method is corresponding to the following ovs-vsctl command::
181
182            $ ovs-vsctl set-controller <bridge> <target>...
183        """
184        command = ovs_vsctl.VSCtlCommand('set-controller', [self.br_name])
185        command.args.extend(controllers)
186        self.run_command([command])
187
188    def del_controller(self):
189        """
190        Deletes the configured OpenFlow controller address.
191
192        This method is corresponding to the following ovs-vsctl command::
193
194            $ ovs-vsctl del-controller <bridge>
195        """
196        command = ovs_vsctl.VSCtlCommand('del-controller', [self.br_name])
197        self.run_command([command])
198
199    def list_db_attributes(self, table, record=None):
200        """
201        Lists 'record' (or all records) in 'table'.
202
203        This method is corresponding to the following ovs-vsctl command::
204
205            $ ovs-vsctl list TBL [REC]
206        """
207        command = ovs_vsctl.VSCtlCommand('list', (table, record))
208        self.run_command([command])
209        if command.result:
210            return command.result
211        return []
212
213    def find_db_attributes(self, table, *conditions):
214        """
215        Lists records satisfying 'conditions' in 'table'.
216
217        This method is corresponding to the following ovs-vsctl command::
218
219            $ ovs-vsctl find TBL CONDITION...
220
221        .. Note::
222
223            Currently, only '=' condition is supported.
224            To support other condition is TODO.
225        """
226        args = [table]
227        args.extend(conditions)
228        command = ovs_vsctl.VSCtlCommand('find', args)
229        self.run_command([command])
230        if command.result:
231            return command.result
232        return []
233
234    def get_db_attribute(self, table, record, column, key=None):
235        """
236        Gets values of 'column' in 'record' in 'table'.
237
238        This method is corresponding to the following ovs-vsctl command::
239
240            $ ovs-vsctl get TBL REC COL[:KEY]
241        """
242        if key is not None:
243            column = '%s:%s' % (column, key)
244        command = ovs_vsctl.VSCtlCommand(
245            'get', (table, record, column))
246        self.run_command([command])
247        if command.result:
248            return command.result[0]
249        return None
250
251    def set_db_attribute(self, table, record, column, value, key=None):
252        """
253        Sets 'value' into 'column' in 'record' in 'table'.
254
255        This method is corresponding to the following ovs-vsctl command::
256
257            $ ovs-vsctl set TBL REC COL[:KEY]=VALUE
258        """
259        if key is not None:
260            column = '%s:%s' % (column, key)
261        command = ovs_vsctl.VSCtlCommand(
262            'set', (table, record, '%s=%s' % (column, value)))
263        self.run_command([command])
264
265    def add_db_attribute(self, table, record, column, value, key=None):
266        """
267        Adds ('key'=)'value' into 'column' in 'record' in 'table'.
268
269        This method is corresponding to the following ovs-vsctl command::
270
271            $ ovs-vsctl add TBL REC COL [KEY=]VALUE
272        """
273        if key is not None:
274            value = '%s=%s' % (key, value)
275        command = ovs_vsctl.VSCtlCommand(
276            'add', (table, record, column, value))
277        self.run_command([command])
278
279    def remove_db_attribute(self, table, record, column, value, key=None):
280        """
281        Removes ('key'=)'value' into 'column' in 'record' in 'table'.
282
283        This method is corresponding to the following ovs-vsctl command::
284
285            $ ovs-vsctl remove TBL REC COL [KEY=]VALUE
286        """
287        if key is not None:
288            value = '%s=%s' % (key, value)
289        command = ovs_vsctl.VSCtlCommand(
290            'remove', (table, record, column, value))
291        self.run_command([command])
292
293    def clear_db_attribute(self, table, record, column):
294        """
295        Clears values from 'column' in 'record' in 'table'.
296
297        This method is corresponding to the following ovs-vsctl command::
298
299            $ ovs-vsctl clear TBL REC COL
300        """
301        command = ovs_vsctl.VSCtlCommand('clear', (table, record, column))
302        self.run_command([command])
303
304    def db_get_val(self, table, record, column):
305        """
306        Gets values of 'column' in 'record' in 'table'.
307
308        This method is corresponding to the following ovs-vsctl command::
309
310            $ ovs-vsctl get TBL REC COL
311        """
312        command = ovs_vsctl.VSCtlCommand('get', (table, record, column))
313        self.run_command([command])
314        assert len(command.result) == 1
315        return command.result[0]
316
317    def db_get_map(self, table, record, column):
318        """
319        Gets dict type value of 'column' in 'record' in 'table'.
320
321        This method is corresponding to the following ovs-vsctl command::
322
323            $ ovs-vsctl get TBL REC COL
324        """
325        val = self.db_get_val(table, record, column)
326        assert isinstance(val, dict)
327        return val
328
329    def get_datapath_id(self):
330        """
331        Gets Datapath ID of OVS instance.
332
333        This method is corresponding to the following ovs-vsctl command::
334
335            $ ovs-vsctl get Bridge <bridge> datapath_id
336        """
337        return self.db_get_val('Bridge', self.br_name, 'datapath_id')
338
339    def delete_port(self, port_name):
340        """
341        Deletes a port on the OVS instance.
342
343        This method is corresponding to the following ovs-vsctl command::
344
345            $ ovs-vsctl --if-exists del-port <bridge> <port>
346        """
347        command = ovs_vsctl.VSCtlCommand(
348            'del-port', (self.br_name, port_name), '--if-exists')
349        self.run_command([command])
350
351    def get_ofport(self, port_name):
352        """
353        Gets the OpenFlow port number.
354
355        This method is corresponding to the following ovs-vsctl command::
356
357            $ ovs-vsctl get Interface <port> ofport
358        """
359        ofport_list = self.db_get_val('Interface', port_name, 'ofport')
360        assert len(ofport_list) == 1
361        return int(ofport_list[0])
362
363    def get_port_name_list(self):
364        """
365        Gets a list of all ports on OVS instance.
366
367        This method is corresponding to the following ovs-vsctl command::
368
369            $ ovs-vsctl list-ports <bridge>
370        """
371        command = ovs_vsctl.VSCtlCommand('list-ports', (self.br_name, ))
372        self.run_command([command])
373        return command.result
374
375    def add_bond(self, name, ifaces, bond_mode=None, lacp=None):
376        """
377        Creates a bonded port.
378
379        :param name: Port name to be created
380        :param ifaces: List of interfaces containing at least 2 interfaces
381        :param bond_mode: Bonding mode (active-backup, balance-tcp
382                          or balance-slb)
383        :param lacp: LACP mode (active, passive or off)
384        """
385        assert len(ifaces) >= 2
386
387        options = ''
388        if bond_mode:
389            options += 'bond_mode=%(bond_mode)s' % locals()
390        if lacp:
391            options += 'lacp=%(lacp)s' % locals()
392
393        command_add = ovs_vsctl.VSCtlCommand(
394            'add-bond', (self.br_name, name, ifaces), options)
395        self.run_command([command_add])
396
397    def add_tunnel_port(self, name, tunnel_type, remote_ip,
398                        local_ip=None, key=None, ofport=None):
399        """
400        Creates a tunnel port.
401
402        :param name: Port name to be created
403        :param tunnel_type: Type of tunnel (gre or vxlan)
404        :param remote_ip: Remote IP address of tunnel
405        :param local_ip: Local IP address of tunnel
406        :param key: Key of GRE or VNI of VxLAN
407        :param ofport: Requested OpenFlow port number
408        """
409        options = 'remote_ip=%(remote_ip)s' % locals()
410        if key:
411            options += ',key=%(key)s' % locals()
412        if local_ip:
413            options += ',local_ip=%(local_ip)s' % locals()
414
415        args = ['Interface', name, 'type=%s' % tunnel_type,
416                'options:%s' % options]
417        if ofport:
418            args.append('ofport_request=%(ofport)s' % locals())
419
420        command_add = ovs_vsctl.VSCtlCommand('add-port', (self.br_name, name))
421        command_set = ovs_vsctl.VSCtlCommand('set', args)
422        self.run_command([command_add, command_set])
423
424    def add_gre_port(self, name, remote_ip,
425                     local_ip=None, key=None, ofport=None):
426        """
427        Creates a GRE tunnel port.
428
429        See the description of ``add_tunnel_port()``.
430        """
431        self.add_tunnel_port(name, 'gre', remote_ip,
432                             local_ip=local_ip, key=key, ofport=ofport)
433
434    def add_vxlan_port(self, name, remote_ip,
435                       local_ip=None, key=None, ofport=None):
436        """
437        Creates a VxLAN tunnel port.
438
439        See the description of ``add_tunnel_port()``.
440        """
441        self.add_tunnel_port(name, 'vxlan', remote_ip,
442                             local_ip=local_ip, key=key, ofport=ofport)
443
444    def del_port(self, port_name):
445        """
446        Deletes a port on OVS instance.
447
448        This method is corresponding to the following ovs-vsctl command::
449
450            $ ovs-vsctl del-port <bridge> <port>
451        """
452        command = ovs_vsctl.VSCtlCommand('del-port', (self.br_name, port_name))
453        self.run_command([command])
454
455    def _get_ports(self, get_port):
456        ports = []
457        port_names = self.get_port_name_list()
458        for name in port_names:
459            if self.get_ofport(name) < 0:
460                continue
461            port = get_port(name)
462            if port:
463                ports.append(port)
464
465        return ports
466
467    def _vifport(self, name, external_ids):
468        ofport = self.get_ofport(name)
469        return VifPort(name, ofport, external_ids['iface-id'],
470                       external_ids['attached-mac'], self)
471
472    def _get_vif_port(self, name):
473        external_ids = self.db_get_map('Interface', name, 'external_ids')
474        if 'iface-id' in external_ids and 'attached-mac' in external_ids:
475            return self._vifport(name, external_ids)
476
477    def get_vif_ports(self):
478        """ Returns a VIF object for each VIF port """
479        return self._get_ports(self._get_vif_port)
480
481    def _get_external_port(self, name):
482        # exclude vif ports
483        external_ids = self.db_get_map('Interface', name, 'external_ids')
484        if external_ids:
485            return
486
487        # exclude tunnel ports
488        options = self.db_get_map('Interface', name, 'options')
489        if 'remote_ip' in options:
490            return
491
492        ofport = self.get_ofport(name)
493        return VifPort(name, ofport, None, None, self)
494
495    def get_external_ports(self):
496        return self._get_ports(self._get_external_port)
497
498    def get_tunnel_port(self, name, tunnel_type='gre'):
499        type_ = self.db_get_val('Interface', name, 'type')
500        if type_ != tunnel_type:
501            return
502
503        options = self.db_get_map('Interface', name, 'options')
504        if 'local_ip' in options and 'remote_ip' in options:
505            ofport = self.get_ofport(name)
506            return TunnelPort(name, ofport, tunnel_type,
507                              options['local_ip'], options['remote_ip'])
508
509    def get_tunnel_ports(self, tunnel_type='gre'):
510        get_tunnel_port = functools.partial(self.get_tunnel_port,
511                                            tunnel_type=tunnel_type)
512        return self._get_ports(get_tunnel_port)
513
514    def get_quantum_ports(self, port_name):
515        LOG.debug('port_name %s', port_name)
516        command = ovs_vsctl.VSCtlCommand(
517            'list-ifaces-verbose',
518            [dpid_lib.dpid_to_str(self.datapath_id), port_name])
519        self.run_command([command])
520        if command.result:
521            return command.result[0]
522        return None
523
524    def set_qos(self, port_name, type='linux-htb', max_rate=None, queues=None):
525        """
526        Sets a Qos rule and creates Queues on the given port.
527        """
528        queues = queues if queues else []
529        command_qos = ovs_vsctl.VSCtlCommand(
530            'set-qos',
531            [port_name, type, max_rate])
532        command_queue = ovs_vsctl.VSCtlCommand(
533            'set-queue',
534            [port_name, queues])
535        self.run_command([command_qos, command_queue])
536        if command_qos.result and command_queue.result:
537            return command_qos.result + command_queue.result
538        return None
539
540    def del_qos(self, port_name):
541        """
542        Deletes the Qos rule on the given port.
543        """
544        command = ovs_vsctl.VSCtlCommand(
545            'del-qos',
546            [port_name])
547        self.run_command([command])
548