Blame libnmstate/route.py

Packit Service 0535c1
#
Packit Service 0535c1
# Copyright (c) 2020 Red Hat, Inc.
Packit Service 0535c1
#
Packit Service 0535c1
# This file is part of nmstate
Packit Service 0535c1
#
Packit Service 0535c1
# This program is free software: you can redistribute it and/or modify
Packit Service 0535c1
# it under the terms of the GNU Lesser General Public License as published by
Packit Service 0535c1
# the Free Software Foundation, either version 2.1 of the License, or
Packit Service 0535c1
# (at your option) any later version.
Packit Service 0535c1
#
Packit Service 0535c1
# This program is distributed in the hope that it will be useful,
Packit Service 0535c1
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit Service 0535c1
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit Service 0535c1
# GNU Lesser General Public License for more details.
Packit Service 0535c1
#
Packit Service 0535c1
# You should have received a copy of the GNU Lesser General Public License
Packit Service 0535c1
# along with this program. If not, see <https://www.gnu.org/licenses/>.
Packit Service 0535c1
#
Packit Service 0535c1
Packit Service 0535c1
from collections import defaultdict
Packit Service 0535c1
Packit Service 0535c1
from libnmstate.error import NmstateValueError
Packit Service 0535c1
from libnmstate.error import NmstateVerificationError
Packit Service 0535c1
from libnmstate.iplib import is_ipv6_address
Packit Service 0535c1
from libnmstate.iplib import canonicalize_ip_network
Packit Service 0535c1
from libnmstate.iplib import canonicalize_ip_address
Packit Service 0535c1
from libnmstate.prettystate import format_desired_current_state_diff
Packit Service 0535c1
from libnmstate.schema import Interface
Packit Service 0535c1
from libnmstate.schema import Route
Packit Service 0535c1
Packit Service 0535c1
from .state import StateEntry
Packit Service 0535c1
from .state import state_match
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
class RouteEntry(StateEntry):
Packit Service 0535c1
    IPV4_DEFAULT_GATEWAY_DESTINATION = "0.0.0.0/0"
Packit Service 0535c1
    IPV6_DEFAULT_GATEWAY_DESTINATION = "::/0"
Packit Service 0535c1
Packit Service 0535c1
    def __init__(self, route):
Packit Service 0535c1
        self.table_id = route.get(Route.TABLE_ID)
Packit Service 0535c1
        self.state = route.get(Route.STATE)
Packit Service 0535c1
        self.metric = route.get(Route.METRIC)
Packit Service 0535c1
        self.destination = route.get(Route.DESTINATION)
Packit Service 0535c1
        self.next_hop_address = route.get(Route.NEXT_HOP_ADDRESS)
Packit Service 0535c1
        self.next_hop_interface = route.get(Route.NEXT_HOP_INTERFACE)
Packit Service 0535c1
        # TODO: Convert IPv6 full address to abbreviated address
Packit Service 0535c1
        self.complement_defaults()
Packit Service 0535c1
        self._invalid_reason = None
Packit Service 0535c1
        self._canonicalize_ip_address()
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def is_ipv6(self):
Packit Service 0535c1
        return is_ipv6_address(self.destination)
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def is_gateway(self):
Packit Service 0535c1
        if self.is_ipv6:
Packit Service 0535c1
            return (
Packit Service 0535c1
                self.destination == RouteEntry.IPV6_DEFAULT_GATEWAY_DESTINATION
Packit Service 0535c1
            )
Packit Service 0535c1
        else:
Packit Service 0535c1
            return (
Packit Service 0535c1
                self.destination == RouteEntry.IPV4_DEFAULT_GATEWAY_DESTINATION
Packit Service 0535c1
            )
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def invalid_reason(self):
Packit Service 0535c1
        return self._invalid_reason
Packit Service 0535c1
Packit Service 0535c1
    def complement_defaults(self):
Packit Service 0535c1
        if not self.absent:
Packit Service 0535c1
            if self.table_id is None:
Packit Service 0535c1
                self.table_id = Route.USE_DEFAULT_ROUTE_TABLE
Packit Service 0535c1
            if self.metric is None:
Packit Service 0535c1
                self.metric = Route.USE_DEFAULT_METRIC
Packit Service 0535c1
            if self.next_hop_address is None:
Packit Service 0535c1
                self.next_hop_address = ""
Packit Service 0535c1
Packit Service 0535c1
    def _keys(self):
Packit Service 0535c1
        return (
Packit Service 0535c1
            self.table_id,
Packit Service 0535c1
            self.metric,
Packit Service 0535c1
            self.destination,
Packit Service 0535c1
            self.next_hop_address,
Packit Service 0535c1
            self.next_hop_interface,
Packit Service 0535c1
        )
Packit Service 0535c1
Packit Service 0535c1
    def __lt__(self, other):
Packit Service 0535c1
        return (
Packit Service 0535c1
            self.table_id or Route.USE_DEFAULT_ROUTE_TABLE,
Packit Service 0535c1
            self.next_hop_interface or "",
Packit Service 0535c1
            self.destination or "",
Packit Service 0535c1
        ) < (
Packit Service 0535c1
            other.table_id or Route.USE_DEFAULT_ROUTE_TABLE,
Packit Service 0535c1
            other.next_hop_interface or "",
Packit Service 0535c1
            other.destination or "",
Packit Service 0535c1
        )
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def absent(self):
Packit Service 0535c1
        return self.state == Route.STATE_ABSENT
Packit Service 0535c1
Packit Service 0535c1
    def is_valid(self, ifaces):
Packit Service 0535c1
        """
Packit Service 0535c1
        Return False when next hop interface or destination not defined;
Packit Service 0535c1
        Return False when route is next hop to any of these interfaces:
Packit Service 0535c1
            * Interface not in InterfaceState.UP state.
Packit Service 0535c1
            * Interface does not exists.
Packit Service 0535c1
            * Interface has IPv4/IPv6 disabled.
Packit Service 0535c1
            * Interface configured as dynamic IPv4/IPv6.
Packit Service 0535c1
        """
Packit Service 0535c1
        if not self.next_hop_interface:
Packit Service 0535c1
            self._invalid_reason = (
Packit Service 0535c1
                "Route entry does not have next hop interface"
Packit Service 0535c1
            )
Packit Service 0535c1
            return False
Packit Service 0535c1
        if not self.destination:
Packit Service 0535c1
            self._invalid_reason = "Route entry does not have destination"
Packit Service 0535c1
            return False
Packit Service 0535c1
        iface = ifaces.get(self.next_hop_interface)
Packit Service 0535c1
        if not iface:
Packit Service 0535c1
            self._invalid_reason = (
Packit Service 0535c1
                f"Route {self.to_dict()} next hop to unknown interface"
Packit Service 0535c1
            )
Packit Service 0535c1
            return False
Packit Service 0535c1
        if not iface.is_up:
Packit Service 0535c1
            self._invalid_reason = (
Packit Service 0535c1
                f"Route {self.to_dict()} next hop to down/absent interface"
Packit Service 0535c1
            )
Packit Service 0535c1
            return False
Packit Service 0535c1
        if iface.is_dynamic(
Packit Service 0535c1
            Interface.IPV6 if self.is_ipv6 else Interface.IPV4
Packit Service 0535c1
        ):
Packit Service 0535c1
            self._invalid_reason = (
Packit Service 0535c1
                f"Route {self.to_dict()} next hop to interface with dynamic IP"
Packit Service 0535c1
            )
Packit Service 0535c1
            return False
Packit Service 0535c1
        if self.is_ipv6:
Packit Service 0535c1
            if not iface.is_ipv6_enabled():
Packit Service 0535c1
                self._invalid_reason = (
Packit Service 0535c1
                    f"Route {self.to_dict()} next hop to interface with IPv6 "
Packit Service 0535c1
                    "disabled"
Packit Service 0535c1
                )
Packit Service 0535c1
                return False
Packit Service 0535c1
        else:
Packit Service 0535c1
            if not iface.is_ipv4_enabled():
Packit Service 0535c1
                self._invalid_reason = (
Packit Service 0535c1
                    f"Route {self.to_dict()} next hop to interface with IPv4 "
Packit Service 0535c1
                    "disabled"
Packit Service 0535c1
                )
Packit Service 0535c1
                return False
Packit Service 0535c1
        return True
Packit Service 0535c1
Packit Service 0535c1
    def _canonicalize_ip_address(self):
Packit Service 0535c1
        if not self.absent:
Packit Service 0535c1
            if self.destination:
Packit Service 0535c1
                self.destination = canonicalize_ip_network(self.destination)
Packit Service 0535c1
            if self.next_hop_address:
Packit Service 0535c1
                self.next_hop_address = canonicalize_ip_address(
Packit Service 0535c1
                    self.next_hop_address
Packit Service 0535c1
                )
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
class RouteState:
Packit Service 0535c1
    def __init__(self, ifaces, des_route_state, cur_route_state):
Packit Service 0535c1
        self._cur_routes = defaultdict(set)
Packit Service 0535c1
        self._routes = defaultdict(set)
Packit Service 0535c1
        if cur_route_state:
Packit Service 0535c1
            for entry in cur_route_state.get(Route.CONFIG, []):
Packit Service 0535c1
                rt = RouteEntry(entry)
Packit Service 0535c1
                self._cur_routes[rt.next_hop_interface].add(rt)
Packit Service 0535c1
                if not ifaces or rt.is_valid(ifaces):
Packit Service 0535c1
                    self._routes[rt.next_hop_interface].add(rt)
Packit Service 0535c1
        if des_route_state:
Packit Service 0535c1
            self._merge_routes(des_route_state, ifaces)
Packit Service 0535c1
Packit Service 0535c1
    def _merge_routes(self, des_route_state, ifaces):
Packit Service 0535c1
        # Handle absent route before adding desired route entries to
Packit Service 0535c1
        # make sure absent route does not delete route defined in
Packit Service 0535c1
        # desire state
Packit Service 0535c1
        for entry in des_route_state.get(Route.CONFIG, []):
Packit Service 0535c1
            rt = RouteEntry(entry)
Packit Service 0535c1
            if rt.absent:
Packit Service 0535c1
                self._apply_absent_routes(rt, ifaces)
Packit Service 0535c1
        for entry in des_route_state.get(Route.CONFIG, []):
Packit Service 0535c1
            rt = RouteEntry(entry)
Packit Service 0535c1
            if not rt.absent:
Packit Service 0535c1
                if rt.is_valid(ifaces):
Packit Service 0535c1
                    ifaces[rt.next_hop_interface].mark_as_changed()
Packit Service 0535c1
                    self._routes[rt.next_hop_interface].add(rt)
Packit Service 0535c1
                else:
Packit Service 0535c1
                    raise NmstateValueError(rt.invalid_reason)
Packit Service 0535c1
Packit Service 0535c1
    def _apply_absent_routes(self, rt, ifaces):
Packit Service 0535c1
        """
Packit Service 0535c1
        Remove routes based on absent routes and treat missing property as
Packit Service 0535c1
        wildcard match.
Packit Service 0535c1
        """
Packit Service 0535c1
        absent_iface_name = rt.next_hop_interface
Packit Service 0535c1
        for iface_name, route_set in self._routes.items():
Packit Service 0535c1
            if absent_iface_name and absent_iface_name != iface_name:
Packit Service 0535c1
                continue
Packit Service 0535c1
            new_routes = set()
Packit Service 0535c1
            for route in route_set:
Packit Service 0535c1
                if not rt.match(route):
Packit Service 0535c1
                    new_routes.add(route)
Packit Service 0535c1
            if new_routes != route_set:
Packit Service 0535c1
                ifaces[iface_name].mark_as_changed()
Packit Service 0535c1
                self._routes[iface_name] = new_routes
Packit Service 0535c1
Packit Service 0535c1
    def gen_metadata(self, ifaces):
Packit Service 0535c1
        """
Packit Service 0535c1
        Generate metada which could used for storing into interface.
Packit Service 0535c1
        Data structure returned is:
Packit Service 0535c1
            {
Packit Service 0535c1
                iface_name: {
Packit Service 0535c1
                    Interface.IPV4: ipv4_routes,
Packit Service 0535c1
                    Interface.IPV6: ipv6_routes,
Packit Service 0535c1
                }
Packit Service 0535c1
            }
Packit Service 0535c1
        """
Packit Service 0535c1
        route_metadata = {}
Packit Service 0535c1
        for iface_name, route_set in self._routes.items():
Packit Service 0535c1
            route_metadata[iface_name] = {
Packit Service 0535c1
                Interface.IPV4: [],
Packit Service 0535c1
                Interface.IPV6: [],
Packit Service 0535c1
            }
Packit Service 0535c1
            for route in route_set:
Packit Service 0535c1
                family = Interface.IPV6 if route.is_ipv6 else Interface.IPV4
Packit Service 0535c1
                route_metadata[iface_name][family].append(route.to_dict())
Packit Service 0535c1
        return route_metadata
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def config_iface_routes(self):
Packit Service 0535c1
        """
Packit Service 0535c1
        Return configured routes indexed by next hop interface
Packit Service 0535c1
        """
Packit Service 0535c1
        if list(self._routes.values()) == [set()]:
Packit Service 0535c1
            return {}
Packit Service 0535c1
        return self._routes
Packit Service 0535c1
Packit Service 0535c1
    def verify(self, cur_route_state):
Packit Service 0535c1
        current = RouteState(
Packit Service 0535c1
            ifaces=None, des_route_state=None, cur_route_state=cur_route_state
Packit Service 0535c1
        )
Packit Service 0535c1
        for iface_name, route_set in self._routes.items():
Packit Service 0535c1
            routes_info = [r.to_dict() for r in sorted(route_set)]
Packit Service 0535c1
            cur_routes_info = [
Packit Service 0535c1
                r.to_dict()
Packit Service 0535c1
                for r in sorted(current._routes.get(iface_name, set()))
Packit Service 0535c1
            ]
Packit Service 0535c1
            if not state_match(routes_info, cur_routes_info):
Packit Service 0535c1
                raise NmstateVerificationError(
Packit Service 0535c1
                    format_desired_current_state_diff(
Packit Service 0535c1
                        {Route.KEY: {Route.CONFIG: routes_info}},
Packit Service 0535c1
                        {Route.KEY: {Route.CONFIG: cur_routes_info}},
Packit Service 0535c1
                    )
Packit Service 0535c1
                )