Signed-off-by: IWASE Yusuke <iwase.yusu...@gmail.com>
---
 ryu/flags.py                                 |   5 +
 ryu/services/protocols/zebra/db/__init__.py  |  42 +++++
 ryu/services/protocols/zebra/db/base.py      |  70 +++++++
 ryu/services/protocols/zebra/db/interface.py | 271 +++++++++++++++++++++++++++
 ryu/services/protocols/zebra/db/route.py     | 201 ++++++++++++++++++++
 5 files changed, 589 insertions(+)
 create mode 100644 ryu/services/protocols/zebra/db/__init__.py
 create mode 100644 ryu/services/protocols/zebra/db/base.py
 create mode 100644 ryu/services/protocols/zebra/db/interface.py
 create mode 100644 ryu/services/protocols/zebra/db/route.py

diff --git a/ryu/flags.py b/ryu/flags.py
index 11ce83d..ff53f76 100644
--- a/ryu/flags.py
+++ b/ryu/flags.py
@@ -78,6 +78,7 @@ DEFAULT_ZSERV_PORT = 2600
 DEFAULT_ZSERV_VERSION = 2  # Version of Ubuntu 16.04 LTS packaged Quagga
 DEFAULT_ZSERV_CLIENT_ROUTE_TYPE = 'BGP'
 DEFAULT_ZSERV_INTERVAL = 10
+DEFAULT_ZSERV_DATABASE = 'sqlite:///zebra.db'
 
 CONF.register_cli_opts([
     cfg.StrOpt(
@@ -101,4 +102,8 @@ CONF.register_cli_opts([
         'retry-interval', default=DEFAULT_ZSERV_INTERVAL,
         help='Retry interval connecting to Zebra server '
              '(default: %s)' % DEFAULT_ZSERV_INTERVAL),
+    cfg.StrOpt(
+        'db-url', default=DEFAULT_ZSERV_DATABASE,
+        help='URL to database used by Zebra protocol service '
+             '(default: %s)' % DEFAULT_ZSERV_DATABASE),
 ], group='zapi')
diff --git a/ryu/services/protocols/zebra/db/__init__.py 
b/ryu/services/protocols/zebra/db/__init__.py
new file mode 100644
index 0000000..2b1cf3a
--- /dev/null
+++ b/ryu/services/protocols/zebra/db/__init__.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2017 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Database implementation for Zebra protocol service.
+"""
+
+from __future__ import absolute_import
+
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+from ryu import cfg
+
+# Configuration parameters for Zebra service
+CONF = cfg.CONF['zapi']
+
+# Connect to database
+ENGINE = create_engine(CONF.db_url)
+
+Session = sessionmaker(bind=ENGINE)
+"""
+Session class connecting to database
+"""
+
+# Create all tables
+from . import base
+from . import interface
+from . import route
+base.Base.metadata.create_all(ENGINE)
diff --git a/ryu/services/protocols/zebra/db/base.py 
b/ryu/services/protocols/zebra/db/base.py
new file mode 100644
index 0000000..0e20b70
--- /dev/null
+++ b/ryu/services/protocols/zebra/db/base.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2017 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+
+import functools
+import logging
+
+from sqlalchemy.ext.declarative import declarative_base
+
+
+LOG = logging.getLogger(__name__)
+
+Base = declarative_base()
+"""
+Base class for Zebra protocol database tables.
+"""
+
+
+def _repr(self):
+    m = ', '.join(
+        ['%s=%r' % (k, v)
+         for k, v in self.__dict__.items() if not k.startswith('_')])
+    return "%s(%s)" % (self.__class__.__name__, m)
+
+Base.__repr__ = _repr
+
+
+def sql_function(func):
+    """
+    Decorator for wrapping the given function in order to manipulate (CRUD)
+    the records safely.
+
+    For the adding/updating/deleting records function, this decorator
+    invokes "Session.commit()" after the given function.
+    If any exception while modifying records raised, this decorator invokes
+    "Session.rollbacks()".
+    """
+    @functools.wraps(func)
+    def _wrapper(session, *args, **kwargs):
+        ret = None
+        try:
+            ret = func(session, *args, **kwargs)
+            if session.dirty:
+                # If the given function has any update to records,
+                # commits them.
+                session.commit()
+        except Exception as e:
+            # If any exception raised, rollbacks the transaction.
+            LOG.error('Error in %s: %s', func.__name__, e)
+            if session.dirty:
+                LOG.error('Do rolling back %s table',
+                          session.dirty[0].__tablename__)
+                session.rollback()
+
+        return ret
+
+    return _wrapper
diff --git a/ryu/services/protocols/zebra/db/interface.py 
b/ryu/services/protocols/zebra/db/interface.py
new file mode 100644
index 0000000..218c590
--- /dev/null
+++ b/ryu/services/protocols/zebra/db/interface.py
@@ -0,0 +1,271 @@
+# Copyright (C) 2017 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+
+import logging
+
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import String
+
+from ryu.lib import netdevice
+from ryu.lib import ip
+from ryu.lib.packet import zebra
+
+from . import base
+
+
+LOG = logging.getLogger(__name__)
+
+# Default value for ethernet interface
+DEFAULT_ETH_FLAGS = (
+    netdevice.IFF_UP
+    | netdevice.IFF_BROADCAST
+    | netdevice.IFF_RUNNING
+    | netdevice.IFF_MULTICAST)
+DEFAULT_ETH_MTU = 1500
+
+
+class Interface(base.Base):
+    """
+    Interface table for Zebra protocol service.
+
+    The default value for each fields suppose "Loopback" interface.
+
+    ``ifindex``: Number of index.
+
+    ``ifname``: Name of this interface.
+
+    ``status``: A combination of flags
+    "ryu.lib.packet.zebra.ZEBRA_INTERFACE_*".
+    The default value shows "active" and "link-detect".
+
+    ``flags``: A combination of flags "ryu.lib.netdevice.IFF_*".
+    The default value show "up", "loopback" and "running".
+
+    ``metric``: Metric of this interface.
+
+    ``ifmtu``: IPv4 MTU of this interface.
+
+    ``ifmtu6``: IPv6 MTU of this interface.
+
+    ``bandwidth``: Bandwidth of this interface.
+
+    ``ll_type``: Link Layer Type.
+    One of "ryu.lib.packet.zebra.ZEBRA_LLT_*" types.
+
+    ``hw_addr``: Hardware address of this interface (mostly, MAC address).
+
+    ``inet``: List of IPv4 addresses separated by a comma.
+    (e.g., "192.168.1.100/24,192.168.2.100/24)".
+
+    ``inet6``: List of IPv6 addresses separated by a comma.
+    """
+    __tablename__ = 'interface'
+
+    ifindex = Column(Integer, primary_key=True)
+    ifname = Column(String, default="lo")
+    status = Column(
+        Integer,
+        default=(
+            zebra.ZEBRA_INTERFACE_ACTIVE
+            | zebra.ZEBRA_INTERFACE_LINKDETECTION))
+    flags = Column(
+        Integer,
+        default=(
+            netdevice.IFF_UP
+            | netdevice.IFF_LOOPBACK
+            | netdevice.IFF_RUNNING))
+    metric = Column(Integer, default=1)
+    ifmtu = Column(Integer, default=0x10000)
+    ifmtu6 = Column(Integer, default=0x10000)
+    bandwidth = Column(Integer, default=0)
+    ll_type = Column(Integer, default=zebra.ZEBRA_LLT_ETHER)
+    hw_addr = Column(String, default='00:00:00:00:00:00')
+    # Note: Only the PostgreSQL backend has support sqlalchemy.ARRAY,
+    # we use the comma separated string as array instead.
+    inet = Column(String, default='')
+    inet6 = Column(String, default='')
+
+
+@base.sql_function
+def ip_link_show(session, **kwargs):
+    """
+    Returns a first interface record matching the given filtering rules.
+
+    The arguments for "kwargs" is the same with Interface class.
+
+    :param session: Session instance connecting to database.
+    :param kwargs: Filtering rules to query.
+    :return: An instance of Interface record.
+    """
+    return session.query(Interface).filter_by(**kwargs).first()
+
+
+@base.sql_function
+def ip_link_show_all(session, **kwargs):
+    """
+    Returns all interface records matching the given filtering rules.
+
+    The arguments for "kwargs" is the same with Interface class.
+
+    :param session: Session instance connecting to database.
+    :param kwargs: Filtering rules to query.
+    :return: A list of Interface records.
+    """
+    return session.query(Interface).filter_by(**kwargs).all()
+
+
+@base.sql_function
+def ip_link_add(session, name, type_='loopback', lladdr='00:00:00:00:00:00'):
+    """
+    Adds an interface record into Zebra protocol service database.
+
+    The arguments are similar to "ip link add" command of iproute2.
+
+    :param session: Session instance connecting to database.
+    :param name: Name of interface.
+    :param type_: Type of interface. 'loopback' or 'ethernet'.
+    :param lladdr: Link layer address. Mostly MAC address.
+    :return: Instance of added record or already existing record.
+    """
+    intf = ip_link_show(session, ifname=name)
+    if intf:
+        LOG.debug('Interface "%s" already exists: %s', intf.ifname, intf)
+        return intf
+
+    if type_ == 'ethernet':
+        intf = Interface(
+            ifname=name,
+            flags=DEFAULT_ETH_FLAGS,
+            ifmtu=DEFAULT_ETH_MTU,
+            ifmtu6=DEFAULT_ETH_MTU,
+            hw_addr=lladdr)
+    else:  # type_ == 'loopback':
+        intf = Interface(
+            ifname=name,
+            inet='127.0.0.1/8',
+            inet6='::1/128')
+
+    session.add(intf)
+
+    return intf
+
+
+@base.sql_function
+def ip_link_delete(session, name):
+    """
+    Deletes an interface record from Zebra protocol service database.
+
+    The arguments are similar to "ip link delete" command of iproute2.
+
+    :param session: Session instance connecting to database.
+    :param name: Name of interface.
+    :return: Name of interface which was deleted. None if failed.
+    """
+    intf = ip_link_show(session, ifname=name)
+    if not intf:
+        LOG.debug('Interface "%s" does not exist', name)
+        return None
+
+    session.delete(intf)
+
+    return name
+
+
+# Currently, functions corresponding to "ip link show" and "ip address show"
+# have the same implementation.
+ip_address_show = ip_link_show
+ip_address_show_all = ip_link_show_all
+
+
+@base.sql_function
+def ip_address_add(session, ifname, ifaddr):
+    """
+    Adds an IP address to interface record identified with the given "ifname".
+
+    The arguments are similar to "ip address add" command of iproute2.
+
+    :param session: Session instance connecting to database.
+    :param ifname: Name of interface.
+    :param ifaddr: IPv4 or IPv6 address.
+    :return: Instance of record or "None" if failed.
+    """
+    def _append_inet_addr(intf_inet, addr):
+        addr_list = intf_inet.split(',')
+        if addr in addr_list:
+            LOG.debug(
+                'Interface "%s" has already "ifaddr": %s',
+                intf.ifname, addr)
+            return intf_inet
+        else:
+            addr_list.append(addr)
+            return ','.join(addr_list)
+
+    intf = ip_link_show(session, ifname=ifname)
+    if not intf:
+        LOG.debug('Interface "%s" does not exist', ifname)
+        return None
+
+    if ip.valid_ipv4(ifaddr):
+        intf.inet = _append_inet_addr(intf.inet, ifaddr)
+    elif ip.valid_ipv6(ifaddr):
+        intf.inet6 = _append_inet_addr(intf.inet6, ifaddr)
+    else:
+        LOG.debug('Invalid IP address for "ifaddr": %s', ifaddr)
+        return None
+
+    return intf
+
+
+@base.sql_function
+def ip_address_delete(session, ifname, ifaddr):
+    """
+    Deletes an IP address from interface record identified with the given
+    "ifname".
+
+    The arguments are similar to "ip address delete" command of iproute2.
+
+    :param session: Session instance connecting to database.
+    :param ifname: Name of interface.
+    :param ifaddr: IPv4 or IPv6 address.
+    :return: Instance of record or "None" if failed.
+    """
+    def _remove_inet_addr(intf_inet, addr):
+        addr_list = intf_inet.split(',')
+        if addr not in addr_list:
+            LOG.debug(
+                'Interface "%s" does not have "ifaddr": %s',
+                intf.ifname, addr)
+            return intf_inet
+        else:
+            addr_list.remove(addr)
+            return ','.join(addr_list)
+
+    intf = ip_link_show(session, ifname=ifname)
+    if not intf:
+        LOG.debug('Interface "%s" does not exist', ifname)
+        return None
+
+    if ip.valid_ipv4(ifaddr):
+        intf.inet = _remove_inet_addr(intf.inet, ifaddr)
+    elif ip.valid_ipv6(ifaddr):
+        intf.inet6 = _remove_inet_addr(intf.inet6, ifaddr)
+    else:
+        LOG.debug('Invalid IP address for "ifaddr": %s', ifaddr)
+        return None
+
+    return intf
diff --git a/ryu/services/protocols/zebra/db/route.py 
b/ryu/services/protocols/zebra/db/route.py
new file mode 100644
index 0000000..ef3feb9
--- /dev/null
+++ b/ryu/services/protocols/zebra/db/route.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2017 Nippon Telegraph and Telephone Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+
+import logging
+import socket
+
+import netaddr
+from sqlalchemy import Column
+from sqlalchemy import Boolean
+from sqlalchemy import Integer
+from sqlalchemy import String
+
+from ryu.lib.packet import safi as packet_safi
+from ryu.lib.packet import zebra
+
+from . import base
+from . import interface
+
+
+LOG = logging.getLogger(__name__)
+
+
+class Route(base.Base):
+    """
+    Route table (like routing table) for Zebra protocol service.
+
+    ``id``: (Primary Key) ID of this route.
+
+    ``family``: Address Family, not AFI (Address Family Identifiers).
+    Mostly, "socket.AF_INET" or "socket.AF_INET6".
+
+    ``safi``: Subsequent Address Family Identifiers.
+
+    ``destination``: Destination prefix of this route.
+
+    ``gateway``: Next hop address of this route.
+    The default is "" (empty string).
+
+    ``ifindex``: Index of interface to forward packets.
+
+    ``source``: Source IP address of this route, which should be an
+     address assigned to the local interface.
+
+    ``route_type``: Route Type of this route.
+    This type shows which daemon (or kernel) generated this route.
+
+    ``is_selected``: Whether this route is selected for "destination".
+    """
+    __tablename__ = 'route'
+
+    id = Column(Integer, primary_key=True)
+    family = Column(Integer, default=socket.AF_INET)
+    safi = Column(Integer, default=packet_safi.UNICAST)
+    destination = Column(String, default='0.0.0.0/0')
+    gateway = Column(String, default='')
+    ifindex = Column(Integer, default=0)
+    source = Column(String, default='')
+    route_type = Column(Integer, default=zebra.ZEBRA_ROUTE_KERNEL)
+    is_selected = Column(Boolean, default=False)
+
+
+@base.sql_function
+def ip_route_show(session, destination, device, **kwargs):
+    """
+    Returns a selected route record matching the given filtering rules.
+
+    The arguments are similar to "ip route showdump" command of iproute2.
+
+    :param session: Session instance connecting to database.
+    :param destination: Destination prefix.
+    :param device: Source device.
+    :param kwargs: Filtering rules to query.
+    :return: Instance of route record or "None" if failed.
+    """
+    intf = interface.ip_link_show(session, ifname=device)
+    if not intf:
+        LOG.debug('Interface "%s" does not exist', device)
+        return None
+
+    return session.query(Route).filter_by(
+        destination=destination, ifindex=intf.ifindex, **kwargs).first()
+
+
+@base.sql_function
+def ip_route_show_all(session, **kwargs):
+    """
+    Returns a selected route record matching the given filtering rules.
+
+    The arguments are similar to "ip route showdump" command of iproute2.
+
+    If "is_selected=True", disables the existing selected route for the
+    given destination.
+
+    :param session: Session instance connecting to database.
+    :param kwargs: Filtering rules to query.
+    :return: A list of route records.
+    """
+    return session.query(Route).filter_by(**kwargs).all()
+
+
+@base.sql_function
+def ip_route_add(session, destination, device=None, gateway='', source='',
+                 ifindex=0, route_type=zebra.ZEBRA_ROUTE_KERNEL,
+                 is_selected=True):
+    """
+    Adds a route record into Zebra protocol service database.
+
+    The arguments are similar to "ip route add" command of iproute2.
+
+    If "is_selected=True", disables the existing selected route for the
+    given destination.
+
+    :param session: Session instance connecting to database.
+    :param destination: Destination prefix.
+    :param device: Source device.
+    :param gateway: Gateway IP address.
+    :param source: Source IP address.
+    :param ifindex: Index of source device.
+    :param route_type: Route type of daemon (or kernel).
+    :param is_selected: If select the given route as "in use" or not.
+    :return: Instance of record or "None" if failed.
+    """
+    if device:
+        intf = interface.ip_link_show(session, ifname=device)
+        if not intf:
+            LOG.debug('Interface "%s" does not exist', device)
+            return None
+        ifindex = ifindex or intf.ifindex
+
+        route = ip_route_show(session, destination=destination, device=device)
+        if route:
+            LOG.debug(
+                'Route to "%s" already exists on "%s" device',
+                destination, device)
+            return route
+
+    dest_addr, dest_prefix_num = destination.split('/')
+    dest_prefix_num = int(dest_prefix_num)
+    if netaddr.valid_ipv4(dest_addr) and 0 <= dest_prefix_num <= 32:
+        family = socket.AF_INET
+    elif netaddr.valid_ipv6(dest_addr) and 0 <= dest_prefix_num <= 128:
+        family = socket.AF_INET6
+    else:
+        LOG.debug('Invalid IP address for "prefix": %s', destination)
+        return None
+    safi = packet_safi.UNICAST
+
+    if is_selected:
+        old_routes = ip_route_show_all(
+            session, destination=destination, is_selected=True)
+        for old_route in old_routes:
+            if old_route:
+                LOG.debug('Set existing route to unselected: %s', old_route)
+                old_route.is_selected = False
+
+    new_route = Route(
+        family=family,
+        safi=safi,
+        destination=destination,
+        gateway=gateway,
+        ifindex=ifindex,
+        source=source,
+        route_type=route_type,
+        is_selected=is_selected)
+
+    session.add(new_route)
+
+    return new_route
+
+
+@base.sql_function
+def ip_route_delete(session, destination, **kwargs):
+    """
+    Deletes route record(s) from Zebra protocol service database.
+
+    The arguments are similar to "ip route delete" command of iproute2.
+
+    :param session: Session instance connecting to database.
+    :param destination: Destination prefix.
+    :param kwargs: Filtering rules to query.
+    :return: Records which are deleted.
+    """
+    routes = ip_route_show_all(session, destination=destination, **kwargs)
+    for route in routes:
+        session.delete(route)
+
+    return routes
-- 
2.7.4


------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, SlashDot.org! http://sdm.link/slashdot
_______________________________________________
Ryu-devel mailing list
Ryu-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/ryu-devel

Reply via email to