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:
parent
66e76e66f9
commit
9e47122b77
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user