From 1b486a0634baa667d99ae2a404a35b882e5fbf84 Mon Sep 17 00:00:00 2001 From: IWASE Yusuke Date: Fri, 17 Feb 2017 13:06:02 +0900 Subject: [PATCH] zebra: Implement database for Zebra protocol service Signed-off-by: IWASE Yusuke Signed-off-by: FUJITA Tomonori --- 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 11ce83d0..ff53f76a 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 00000000..2b1cf3ac --- /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 00000000..0e20b70d --- /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 00000000..218c5900 --- /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 00000000..ef3feb9d --- /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