routes: Restructure routes processing

The routes processing has been splitted into several stages:
- Merging: Focus on merging the routes entries, with no consideration
  about the interfaces states.
  The following logic is applied when merging the desired and the current
  routes:
  - Desired routes are kept.
  - Current routes are kept, under specific conditions.
    At least one route exist under a desired interface that:
    - That is not DOWN or ABSENT.
    - That its has IPv4/6 stack is no disabled (in correspondence to the
      route IP version).
  - Desired absent routes overwrite other routes state.
    - Wildcard support for matching the absent routes.
- Metadata: Responsible to attach route metadata on desired interfaces.
  In cases where the interface is missing, it is complemented from the
  current state (by just mentioning its name and type) if available or
  just ignored if it is not found.
- Validation:
  A route has several requirements it must comply with:
    - Next-hop interface must be provided.
    - The next-hop interface must:
        - Exist and be up (no down/absent).
        - Have the relevant IPv4/6 stack enabled.

Signed-off-by: Edward Haas <edwardh@redhat.com>
This commit is contained in:
Edward Haas 2019-06-12 02:06:42 +03:00 committed by Gris Ge
parent 66e76e66f9
commit 9e47122b77
5 changed files with 446 additions and 397 deletions

View File

@ -69,7 +69,7 @@ def generate_ifaces_metadata(desired_state, current_state):
set_metadata_func=linux_bridge.set_bridge_ports_metadata
)
_generate_dns_metadata(desired_state, current_state)
_generate_route_metadata(desired_state)
_generate_route_metadata(desired_state, current_state)
def remove_ifaces_metadata(ifaces_state):
@ -169,19 +169,30 @@ def _generate_link_master_metadata(ifaces_desired_state,
master_state, ifaces_desired_state[slave])
def _generate_route_metadata(desired_state):
def _generate_route_metadata(desired_state, current_state):
"""
Save routes under interface IP protocol so that nm/ipv4.py or nm/ipv6.py
could include route configuration in `create_setting()`.
Save routes metadata under interface IP protocol for future processing by
the providers.
Currently route['next-hop-interface'] is mandatory.
Routes which do not match any current or desired interface are ignored.
"""
for iface_name, routes in six.viewitems(desired_state.config_iface_routes):
iface_state = desired_state.interfaces.get(iface_name, {})
for family in (Interface.IPV4, Interface.IPV6):
if family in iface_state:
iface_state[family][ROUTES] = []
else:
iface_state[family] = {ROUTES: []}
desired_iface_state = desired_state.interfaces.get(iface_name)
current_iface_state = current_state.interfaces.get(iface_name)
if desired_iface_state:
_attach_route_metadata(desired_iface_state, routes)
elif current_iface_state:
desired_iface_state = desired_state.interfaces[iface_name] = {
Interface.NAME: iface_name,
Interface.TYPE: current_iface_state[Interface.TYPE]
}
_attach_route_metadata(desired_iface_state, routes)
def _attach_route_metadata(iface_state, routes):
_init_iface_route_metadata(iface_state, Interface.IPV4)
_init_iface_route_metadata(iface_state, Interface.IPV6)
for route in routes:
if iplib.is_ipv6_address(route.destination):
iface_state[Interface.IPV6][ROUTES].append(route.to_dict())
@ -189,6 +200,13 @@ def _generate_route_metadata(desired_state):
iface_state[Interface.IPV4][ROUTES].append(route.to_dict())
def _init_iface_route_metadata(iface_state, ip_key):
ip_state = iface_state.get(ip_key)
if not ip_state:
ip_state = iface_state[ip_key] = {}
ip_state[ROUTES] = []
def _generate_dns_metadata(desired_state, current_state):
"""
Save DNS configuration on chosen interfaces as metadata.

View File

@ -111,11 +111,12 @@ def _apply_ifaces_state(desired_state, verify_change, commit,
desired_state.sanitize_ethernet(current_state)
desired_state.sanitize_dynamic_ip()
desired_state.merge_route_config(current_state)
desired_state.merge_routes(current_state)
desired_state.merge_dns(current_state)
metadata.generate_ifaces_metadata(desired_state, current_state)
validator.validate_interfaces_state(desired_state, current_state)
validator.validate_routes(desired_state, current_state)
new_interfaces = _list_new_interfaces(desired_state, current_state)

View File

@ -31,6 +31,7 @@ from libnmstate import iplib
from libnmstate import metadata
from libnmstate.error import NmstateValueError
from libnmstate.error import NmstateVerificationError
from libnmstate.iplib import is_ipv6_address
from libnmstate.prettystate import format_desired_current_state_diff
from libnmstate.schema import DNS
from libnmstate.schema import Ethernet
@ -40,6 +41,9 @@ from libnmstate.schema import InterfaceType
from libnmstate.schema import Route
NON_UP_STATES = (InterfaceState.DOWN, InterfaceState.ABSENT)
@total_ordering
class RouteEntry(object):
def __init__(self, route):
@ -278,66 +282,72 @@ class State(object):
dict_update(other_state.interfaces[name], self.interfaces[name])
self._ifaces_state[name] = other_state.interfaces[name]
def merge_route_config(self, other_state):
def merge_routes(self, other_state):
"""
Converting route partial editing to route full editing by
merging config routes from other state to self.
Assuming the other state is from `netinfo.show()` which don't have
absent routes in it.
Given the self and other states, complete the self state by merging
the routes form the current state.
The resulting routes are a combination of the current routes and the
desired routes:
- Self routes are kept.
- Other routes are kept if:
- There are self routes under iface OR
- Self iface explicitly specified (regardless of routes)
- Self absent routes overwrite other routes state.
- Support wildcard for matching the absent routes.
"""
iface_enable_states = _get_iface_enable_states(self, other_state)
ipv4_enable_states = _get_ip_enable_states(
Interface.IPV4, self, other_state)
ipv6_enable_states = _get_ip_enable_states(
Interface.IPV6, self, other_state)
self_iface_route_sets = defaultdict(set)
absent_route_sets = set()
other_iface_route_sets = defaultdict(set)
other_routes = set()
for ifname, routes in six.viewitems(other_state.config_iface_routes):
if (ifname in self.config_iface_routes or
self._is_interface_routable(ifname, routes)):
other_routes |= set(routes)
for route in self._config_routes:
if route.get(Route.STATE) == Route.STATE_ABSENT:
absent_route_sets.add(RouteEntry(route))
self_routes = {
route
for routes in six.viewvalues(self.config_iface_routes)
for route in routes if not route.absent
}
for route in other_state._config_routes:
# The other_state(running state) does not have any absent
# route entry and all have NEXT_HOP_INTERFACE
iface_name = route[Route.NEXT_HOP_INTERFACE]
other_iface_route_sets[iface_name].add(RouteEntry(route))
absent_routes = set()
for routes in six.viewvalues(self.config_iface_routes):
for absent_route in (r for r in routes if r.absent):
absent_routes |= {
r for r in other_routes if absent_route.match(r)
}
for route in self._config_routes:
if route.get(Route.STATE) != Route.STATE_ABSENT:
# The absent route is discarded, so the metadata.py don't need
# handle it.
if Route.NEXT_HOP_INTERFACE in route:
self_iface_route_sets[route[Route.NEXT_HOP_INTERFACE]].add(
RouteEntry(route))
_validate_routes(self_iface_route_sets,
iface_enable_states,
ipv4_enable_states,
ipv6_enable_states)
# Absent route can only remove routes in current state.
_apply_absent_routes(absent_route_sets, other_iface_route_sets)
for iface_name, route_set in six.viewitems(other_iface_route_sets):
# The other_state(running state) does not have any absent
# route entry and all have NEXT_HOP_INTERFACE
for route_obj in route_set:
if _route_is_valid(route_obj,
iface_enable_states,
ipv4_enable_states,
ipv6_enable_states):
self_iface_route_sets[iface_name].add(route_obj)
merged_routes = []
for iface_name, route_set in six.viewitems(self_iface_route_sets):
merged_routes.extend(
list(route_obj.to_dict() for route_obj in route_set))
self._config_routes = merged_routes
merged_routes = (other_routes | self_routes) - absent_routes
self._config_routes = [r.to_dict() for r in sorted(merged_routes)]
# FIXME: Index based on route objects directly.
self._config_iface_routes = self._index_routes_by_iface()
def _is_interface_routable(self, ifname, routes):
"""
And interface is able to support routes if:
- It exists.
- It is not DOWN or ABSENT.
- It is not IPv4/6 disabled (corresponding to the routes).
"""
ifstate = self.interfaces.get(ifname)
if not ifstate:
return False
iface_up = ifstate.get(Interface.STATE) not in NON_UP_STATES
if iface_up:
ipv4_state = ifstate.get(Interface.IPV4, {})
ipv4_disabled = ipv4_state.get('enabled') is False
if (ipv4_disabled and
any(not is_ipv6_address(r.destination) for r in routes)):
return False
ipv6_state = ifstate.get(Interface.IPV6, {})
ipv6_disabled = ipv6_state.get('enabled') is False
if (ipv6_disabled and
any(is_ipv6_address(r.destination) for r in routes)):
return False
return True
return False
def merge_dns(self, other_state):
"""
If DNS is not mentioned in the self state, overwrite it with the other
@ -374,9 +384,8 @@ class State(object):
def _index_routes_by_iface(self):
iface_routes = defaultdict(list)
for route in self._config_routes:
iface_name = route.get(Route.NEXT_HOP_INTERFACE)
if iface_name:
iface_routes[iface_name].append(RouteEntry(route))
iface_name = route.get(Route.NEXT_HOP_INTERFACE, '')
iface_routes[iface_name].append(RouteEntry(route))
for routes in six.viewvalues(iface_routes):
routes.sort()
return iface_routes

View File

@ -590,71 +590,135 @@ class TestDesiredStateOvsMetadata(object):
assert current_state == expected_cstate
def test_route_metadata():
routes = _get_mixed_test_routes()
desired_state = state.State({
Route.KEY: {
Route.CONFIG: routes,
},
Interface.KEY: [
{
Interface.NAME: 'eth1',
Interface.TYPE: InterfaceType.ETHERNET,
Interface.IPV4: {},
Interface.IPV6: {},
},
{
Interface.NAME: 'eth2',
Interface.TYPE: InterfaceType.ETHERNET,
Interface.IPV4: {},
Interface.IPV6: {},
},
]
})
current_state = state.State({})
metadata.generate_ifaces_metadata(desired_state, current_state)
expected_iface_state = {
TEST_IFACE1: {
Interface.NAME: TEST_IFACE1,
Interface.TYPE: InterfaceType.ETHERNET,
Interface.IPV4: {
metadata.ROUTES: [routes[0]]
},
Interface.IPV6: {
metadata.ROUTES: []
},
},
'eth2': {
Interface.NAME: 'eth2',
Interface.TYPE: InterfaceType.ETHERNET,
Interface.IPV4: {
metadata.ROUTES: []
},
Interface.IPV6: {
metadata.ROUTES: [routes[1]]
},
}
class TestRouteMetadata(object):
def test_with_empty_states(self):
desired_state = state.State({})
current_state = state.State({})
metadata.generate_ifaces_metadata(desired_state, current_state)
assert {} == desired_state.interfaces
def test_no_routes_with_no_interfaces(self):
desired_state = state.State({
Interface.KEY: [],
Route.KEY: {Route.CONFIG: []}}
)
current_state = state.State({
Interface.KEY: [],
Route.KEY: {Route.CONFIG: []}}
)
metadata.generate_ifaces_metadata(desired_state, current_state)
assert {} == desired_state.interfaces
def test_route_with_no_desired_or_current_interfaces(self):
route0 = self._create_route0()
desired_state = state.State({
Interface.KEY: [],
Route.KEY: {Route.CONFIG: [route0.to_dict()]}}
)
current_state = state.State({})
metadata.generate_ifaces_metadata(desired_state, current_state)
assert {} == desired_state.interfaces
def test_route_with_no_desired_or_current_matching_interface(self):
route0 = self._create_route0()
desired_state = state.State({
Interface.KEY: [_create_interface_state('foo')],
Route.KEY: {Route.CONFIG: [route0.to_dict()]}}
)
current_state = state.State({
Interface.KEY: [_create_interface_state('boo')],
Route.KEY: {Route.CONFIG: []}}
)
metadata.generate_ifaces_metadata(desired_state, current_state)
assert 'foo' in desired_state.interfaces
assert metadata.ROUTES not in desired_state.interfaces['foo']
def test_route_with_matching_desired_interface(self):
route0 = self._create_route0()
desired_state = state.State({
Interface.KEY: [_create_interface_state('eth1')],
Route.KEY: {Route.CONFIG: [route0.to_dict()]}}
)
current_state = state.State({})
metadata.generate_ifaces_metadata(desired_state, current_state)
iface_state = desired_state.interfaces['eth1']
route_metadata, = iface_state[Interface.IPV4][metadata.ROUTES]
assert route0.to_dict() == route_metadata
def test_route_with_matching_current_interface(self):
route0 = self._create_route0()
desired_state = state.State({
Interface.KEY: [],
Route.KEY: {Route.CONFIG: [route0.to_dict()]}
})
current_state = state.State({
Interface.KEY: [_create_interface_state('eth1')],
Route.KEY: {Route.CONFIG: []}
})
metadata.generate_ifaces_metadata(desired_state, current_state)
iface_state = desired_state.interfaces['eth1']
route_metadata, = iface_state[Interface.IPV4][metadata.ROUTES]
assert route0.to_dict() == route_metadata
def test_two_routes_with_matching_interfaces(self):
route0 = self._create_route0()
route1 = self._create_route1()
desired_state = state.State({
Interface.KEY: [_create_interface_state('eth1')],
Route.KEY: {Route.CONFIG: [route0.to_dict(), route1.to_dict()]}
})
current_state = state.State({
Interface.KEY: [_create_interface_state('eth2')],
Route.KEY: {Route.CONFIG: []}
})
metadata.generate_ifaces_metadata(desired_state, current_state)
iface0_state = desired_state.interfaces['eth1']
iface1_state = desired_state.interfaces['eth2']
route0_metadata, = iface0_state[Interface.IPV4][metadata.ROUTES]
route1_metadata, = iface1_state[Interface.IPV6][metadata.ROUTES]
assert route0.to_dict() == route0_metadata
assert route1.to_dict() == route1_metadata
def _create_route0(self):
return _create_route('198.51.100.0/24', '192.0.2.1', 'eth1', 50, 103)
def _create_route1(self):
return _create_route(
'2001:db8:a::/64', '2001:db8:1::a', 'eth2', 51, 104)
def _create_interface_state(iface_name):
return {
Interface.NAME: iface_name,
Interface.TYPE: InterfaceType.ETHERNET,
Interface.IPV4: {},
Interface.IPV6: {},
}
assert desired_state.interfaces == expected_iface_state
def _get_mixed_test_routes():
return [
{
Route.DESTINATION: '198.51.100.0/24',
Route.METRIC: 103,
Route.NEXT_HOP_INTERFACE: TEST_IFACE1,
Route.NEXT_HOP_ADDRESS: '192.0.2.1',
Route.TABLE_ID: 50
},
{
Route.DESTINATION: '2001:db8:a::/64',
Route.METRIC: 104,
Route.NEXT_HOP_INTERFACE: 'eth2',
Route.NEXT_HOP_ADDRESS: '2001:db8:1::a',
Route.TABLE_ID: 51
}
]
def _create_route(dest, via_addr, via_iface, table, metric):
return state.RouteEntry({
Route.DESTINATION: dest,
Route.METRIC: metric,
Route.NEXT_HOP_ADDRESS: via_addr,
Route.NEXT_HOP_INTERFACE: via_iface,
Route.TABLE_ID: table
})
def test_dns_metadata_empty():

View File

@ -19,8 +19,6 @@ from collections import defaultdict
import pytest
from libnmstate import state
from libnmstate.iplib import is_ipv6_address
from libnmstate.error import NmstateValueError
from libnmstate.error import NmstateVerificationError
from libnmstate.schema import DNS
from libnmstate.schema import Interface
@ -299,6 +297,231 @@ class TestRouteEntry(object):
assert expected_routes == sorted(routes)
class TestRouteStateMerge(object):
def test_merge_empty_states(self):
s0 = state.State({})
s1 = state.State({})
s0.merge_routes(s1)
assert {'interfaces': [], 'routes': {'config': []}} == s0.state
assert {} == s0.config_iface_routes
def test_merge_identical_states(self):
route0_obj = self._create_route0()
route0 = route0_obj.to_dict()
s0 = state.State({'routes': {'config': [route0]}})
s1 = state.State({'routes': {'config': [route0]}})
s0.merge_routes(s1)
assert {'interfaces': [], 'routes': {'config': [route0]}} == s0.state
assert {'eth1': [route0_obj]} == s0.config_iface_routes
def test_merge_unique_states(self):
route0_obj = self._create_route0()
route0 = route0_obj.to_dict()
route1_obj = self._create_route1()
route1 = route1_obj.to_dict()
s0 = state.State({'routes': {'config': [route0]}})
s1 = state.State({'routes': {'config': [route1]}})
s0.merge_routes(s1)
expected_state = {'interfaces': [], 'routes': {'config': [route0]}}
assert expected_state == s0.state
expected_indexed_routes = {'eth1': [route0_obj]}
assert expected_indexed_routes == s0.config_iface_routes
def test_merge_empty_with_non_empty_state(self):
route0_obj = self._create_route0()
route0 = route0_obj.to_dict()
empty_state = state.State({})
state_with_route0 = state.State({'routes': {'config': [route0]}})
empty_state.merge_routes(state_with_route0)
assert ({'interfaces': [], 'routes': {'config': []}} ==
empty_state.state)
assert {} == empty_state.config_iface_routes
def test_merge_iface_only_with_same_iface_routes_state(self):
route0_obj = self._create_route0()
route0 = route0_obj.to_dict()
iface_only_state = state.State({
Interface.KEY: [{Interface.NAME: route0_obj.next_hop_interface}]
})
state_with_route0 = state.State({Route.KEY: {Route.CONFIG: [route0]}})
iface_only_state.merge_routes(state_with_route0)
expected = {
Interface.KEY: [{
Interface.NAME: route0_obj.next_hop_interface,
Interface.IPV4: {},
Interface.IPV6: {},
}],
Route.KEY: {Route.CONFIG: [route0]},
}
assert expected == iface_only_state.state
assert ({route0_obj.next_hop_interface: [route0_obj]} ==
iface_only_state.config_iface_routes)
def test_merge_iface_down_with_same_iface_routes_state(self):
route0_obj = self._create_route0()
route0 = route0_obj.to_dict()
iface_down_state = state.State({
Interface.KEY: [{
Interface.NAME: route0_obj.next_hop_interface,
Interface.STATE: InterfaceState.DOWN,
}]
})
state_with_route0 = state.State({Route.KEY: {Route.CONFIG: [route0]}})
iface_down_state.merge_routes(state_with_route0)
expected = {
Interface.KEY: [{
Interface.NAME: route0_obj.next_hop_interface,
Interface.STATE: InterfaceState.DOWN,
Interface.IPV4: {},
Interface.IPV6: {},
}],
Route.KEY: {Route.CONFIG: []},
}
assert expected == iface_down_state.state
assert {} == iface_down_state.config_iface_routes
def test_merge_iface_ipv4_disabled_with_same_iface_routes_state(self):
route0_obj = self._create_route0()
route0 = route0_obj.to_dict()
iface_down_state = state.State({
Interface.KEY: [{
Interface.NAME: route0_obj.next_hop_interface,
Interface.STATE: InterfaceState.UP,
Interface.IPV4: {'enabled': False},
}]
})
state_with_route0 = state.State({Route.KEY: {Route.CONFIG: [route0]}})
iface_down_state.merge_routes(state_with_route0)
expected = {
Interface.KEY: [{
Interface.NAME: route0_obj.next_hop_interface,
Interface.STATE: InterfaceState.UP,
Interface.IPV4: {'enabled': False},
Interface.IPV6: {},
}],
Route.KEY: {Route.CONFIG: []},
}
assert expected == iface_down_state.state
assert {} == iface_down_state.config_iface_routes
def test_merge_iface_ipv6_disabled_with_same_iface_routes_state(self):
route1_obj = self._create_route1()
route1 = route1_obj.to_dict()
iface_down_state = state.State({
Interface.KEY: [{
Interface.NAME: route1_obj.next_hop_interface,
Interface.STATE: InterfaceState.UP,
Interface.IPV6: {'enabled': False},
}]
})
state_with_route1 = state.State({Route.KEY: {Route.CONFIG: [route1]}})
iface_down_state.merge_routes(state_with_route1)
expected = {
Interface.KEY: [{
Interface.NAME: route1_obj.next_hop_interface,
Interface.STATE: InterfaceState.UP,
Interface.IPV4: {},
Interface.IPV6: {'enabled': False},
}],
Route.KEY: {Route.CONFIG: []},
}
assert expected == iface_down_state.state
assert {} == iface_down_state.config_iface_routes
def test_merge_iface_ipv6_disabled_with_same_iface_ipv4_routes_state(self):
route0_obj = self._create_route0()
route0 = route0_obj.to_dict()
iface_down_state = state.State({
Interface.KEY: [{
Interface.NAME: route0_obj.next_hop_interface,
Interface.STATE: InterfaceState.UP,
Interface.IPV6: {'enabled': False},
}]
})
state_with_route0 = state.State({Route.KEY: {Route.CONFIG: [route0]}})
iface_down_state.merge_routes(state_with_route0)
expected = {
Interface.KEY: [{
Interface.NAME: route0_obj.next_hop_interface,
Interface.STATE: InterfaceState.UP,
Interface.IPV4: {},
Interface.IPV6: {'enabled': False},
}],
Route.KEY: {Route.CONFIG: [route0]},
}
assert expected == iface_down_state.state
assert ({route0_obj.next_hop_interface: [route0_obj]} ==
iface_down_state.config_iface_routes)
def test_merge_non_empty_with_empty_state(self):
route0_obj = self._create_route0()
route0 = route0_obj.to_dict()
empty_state = state.State({})
state_with_route0 = state.State({'routes': {'config': [route0]}})
state_with_route0.merge_routes(empty_state)
assert ({'interfaces': [], 'routes': {'config': [route0]}} ==
state_with_route0.state)
assert {'eth1': [route0_obj]} == state_with_route0.config_iface_routes
def test_merge_absent_routes_with_no_matching(self):
absent_route_obj = self._create_route0()
absent_route_obj.state = Route.STATE_ABSENT
absent_route = absent_route_obj.to_dict()
other_route_obj = self._create_route1()
other_route = other_route_obj.to_dict()
s0 = state.State({'routes': {'config': [absent_route]}})
s1 = state.State({'routes': {'config': [other_route]}})
s0.merge_routes(s1)
expected_state = {
'interfaces': [], 'routes': {'config': []}}
assert expected_state == s0.state
assert {} == s0.config_iface_routes
def test_merge_absent_routes_with_matching(self):
absent_route_obj = self._create_route0()
absent_route_obj.state = Route.STATE_ABSENT
absent_route = absent_route_obj.to_dict()
other_route_obj = self._create_route0()
other_route = other_route_obj.to_dict()
s0 = state.State({'routes': {'config': [absent_route]}})
s1 = state.State({'routes': {'config': [other_route]}})
s0.merge_routes(s1)
assert {'interfaces': [], 'routes': {'config': []}} == s0.state
assert {} == s0.config_iface_routes
def _create_route0(self):
return _create_route('198.51.100.0/24', '192.0.2.1', 'eth1', 50, 103)
def _create_route1(self):
return _create_route(
'2001:db8:a::/64', '2001:db8:1::a', 'eth2', 51, 104)
def _create_route(dest, via_addr, via_iface, table, metric):
return state.RouteEntry(
_create_route_dict(dest, via_addr, via_iface, table, metric))
@ -388,182 +611,6 @@ def test_state_iface_routes_order():
reverse_route_state.config_iface_routes)
def test_state_merge_config_add():
routes = _get_mixed_test_routes()
iface_states = _gen_iface_states_for_routes(routes)
other_state = state.State(
{
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: [routes[0]],
}
}
)
route_state = state.State(
{
Route.KEY: {
Route.CONFIG: [routes[1]],
}
}
)
route_state.merge_route_config(other_state)
expected_indexed_route_state = defaultdict(list)
for route in routes[:2]:
iface_name = route[Route.NEXT_HOP_INTERFACE]
expected_indexed_route_state[iface_name].append(
state.RouteEntry(route))
# No need to sort the routes as there is only 1 route per interface.
assert expected_indexed_route_state == route_state.config_iface_routes
def test_state_merge_config_add_duplicate():
routes = _get_mixed_test_routes()
iface_states = _gen_iface_states_for_routes(routes)
other_state = state.State(
{
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: routes,
}
}
)
route_state = state.State(
{
Route.KEY: {
Route.CONFIG: [routes[0]],
}
}
)
route_state.merge_route_config(other_state)
expected_indexed_route_state = defaultdict(list)
for route in routes:
iface_name = route[Route.NEXT_HOP_INTERFACE]
expected_indexed_route_state[iface_name].append(
state.RouteEntry(route))
# No need to sort the routes as there is only 1 route per interface.
assert expected_indexed_route_state == route_state.config_iface_routes
def test_state_merge_config_add_empty():
routes = _get_mixed_test_routes()
iface_states = _gen_iface_states_for_routes(routes)
other_state = state.State(
{
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: routes,
}
}
)
route_state = state.State(
{
Route.KEY: {
Route.CONFIG: []
}
}
)
route_state.merge_route_config(other_state)
expected_indexed_route_state = defaultdict(list)
for route in routes:
iface_name = route[Route.NEXT_HOP_INTERFACE]
expected_indexed_route_state[iface_name].append(
state.RouteEntry(route))
# No need to sort the routes as there is only 1 route per interface.
assert expected_indexed_route_state == route_state.config_iface_routes
def test_state_merge_config_discard_absent():
routes = _get_mixed_test_routes()
iface_states = _gen_iface_states_for_routes(routes)
for route in routes:
route[Route.STATE] = Route.STATE_ABSENT
other_state = state.State(
{
Route.KEY: {
Route.CONFIG: [],
}
}
)
route_state = state.State(
{
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: routes
}
}
)
route_state.merge_route_config(other_state)
expected_indexed_route_state = {}
assert expected_indexed_route_state == route_state.config_iface_routes
def test_merge_desired_route_iface_down():
routes = _get_mixed_test_routes()
iface_states = _gen_iface_states_for_routes(routes)
iface_states[0][Interface.STATE] = InterfaceState.DOWN
desired_state = state.State(
{
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: routes,
}
}
)
with pytest.raises(NmstateValueError):
desired_state.merge_route_config(state.State({}))
def test_merge_desired_route_iface_missing():
routes = _get_mixed_test_routes()
iface_states = _gen_iface_states_for_routes(routes)
routes[0][Route.NEXT_HOP_INTERFACE] = 'not_exists'
desired_state = state.State(
{
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: routes,
}
}
)
with pytest.raises(NmstateValueError):
desired_state.merge_route_config(state.State({}))
def test_merge_desired_route_ip_disabled():
routes = _get_mixed_test_routes()
ipv4_routes = []
ipv6_routes = []
for route in routes:
if is_ipv6_address(route[Route.DESTINATION]):
ipv6_routes.append(route)
else:
ipv4_routes.append(route)
for routes in (ipv4_routes, ipv6_routes):
iface_states = _gen_iface_states_for_routes(routes)
iface_states[0][Interface.IPV4]['enabled'] = False
iface_states[0][Interface.IPV6]['enabled'] = False
desired_state = state.State(
{
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: routes,
}
}
)
with pytest.raises(NmstateValueError):
desired_state.merge_route_config(state.State({}))
def test_state_verify_route_same():
routes = _get_mixed_test_routes()
route_state = state.State({
@ -620,96 +667,6 @@ def test_state_verify_route_empty():
route_state.verify_routes(route_state_2)
def test_state_merge_config_specific_remove():
routes = _get_mixed_test_routes()
iface_states = _gen_iface_states_for_routes(routes)
current_state = state.State({
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: routes
}
})
absent_route = routes[0]
absent_route[Route.STATE] = Route.STATE_ABSENT
desired_state = state.State({
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: [absent_route]
}
})
desired_state.merge_route_config(current_state)
expected_indexed_route_state = defaultdict(list)
for route in routes[1:]:
iface_name = route[Route.NEXT_HOP_INTERFACE]
expected_indexed_route_state[iface_name].append(
state.RouteEntry(route))
# No need to sort the routes as there is only 1 route per interface.
assert expected_indexed_route_state == desired_state.config_iface_routes
def test_state_remote_route_wildcard_with_iface():
routes = _get_mixed_test_routes()
iface_states = _gen_iface_states_for_routes(routes)
current_state = state.State({
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: routes
}
})
absent_route_iface_name = routes[0][Route.NEXT_HOP_INTERFACE]
absent_route = {
Route.STATE: Route.STATE_ABSENT,
Route.NEXT_HOP_INTERFACE: absent_route_iface_name
}
desired_state = state.State({
Interface.KEY: iface_states,
Route.KEY: {
Route.CONFIG: [absent_route]
}
})
desired_state.merge_route_config(current_state)
expected_indexed_route_state = defaultdict(list)
for route in routes:
iface_name = route[Route.NEXT_HOP_INTERFACE]
if iface_name != absent_route_iface_name:
expected_indexed_route_state[iface_name].append(
state.RouteEntry(route))
# No need to sort the routes as there is only 1 route per
# interface.
assert expected_indexed_route_state == desired_state.config_iface_routes
def test_state_remote_route_wildcard_destination_without_iface():
routes = _get_mixed_test_routes()
current_state = state.State({
Route.KEY: {
Route.CONFIG: routes
}
})
absent_routes = []
for route in routes:
absent_routes.append({
Route.STATE: Route.STATE_ABSENT,
Route.DESTINATION: route[Route.DESTINATION]
})
desired_state = state.State({
Route.KEY: {
Route.CONFIG: absent_routes
}
})
desired_state.merge_route_config(current_state)
expected_indexed_route_state = {}
assert expected_indexed_route_state == desired_state.config_iface_routes
def _get_mixed_test_routes():
r0 = _create_route('198.51.100.0/24', '192.0.2.1', 'eth1', 50, 103)
r1 = _create_route('2001:db8:a::/64', '2001:db8:1::a', 'eth2', 51, 104)