route: Add support for the route-type
When users have BGP routing setups, it is common practice to blackhole some less-specific routes in order to avoid routing loops, and the BGP router might insert a more specific route dynamically afterwards. Examples: ``` interfaces: - name: eth1 type: ethernet state: up ipv4: address: - ip: 192.0.2.251 prefix-length: 24 dhcp: false enabled: true routes: config: - destination: 198.51.100.0/24 metric: 150 next-hop-address: 192.0.2.1 next-hop-interface: eth1 table-id: 254 - destination: 198.51.200.0/24 route-type: blackhole ``` Signed-off-by: Wen Liang <liangwen12year@gmail.com>
This commit is contained in:
parent
af88bb2429
commit
f8c57a88fe
@ -17,3 +17,5 @@ routes:
|
||||
next-hop-address: 192.0.2.1
|
||||
next-hop-interface: eth1
|
||||
table-id: 254
|
||||
- destination: 198.51.200.0/24
|
||||
route-type: blackhole
|
||||
|
@ -177,7 +177,7 @@ pub use crate::policy::{
|
||||
NetworkCaptureRules, NetworkPolicy, NetworkStateTemplate,
|
||||
};
|
||||
pub(crate) use crate::route::MergedRoutes;
|
||||
pub use crate::route::{RouteEntry, RouteState, Routes};
|
||||
pub use crate::route::{RouteEntry, RouteState, RouteType, Routes};
|
||||
pub(crate) use crate::route_rule::MergedRouteRules;
|
||||
pub use crate::route_rule::{
|
||||
RouteRuleAction, RouteRuleEntry, RouteRuleState, RouteRules,
|
||||
|
@ -1,6 +1,6 @@
|
||||
use log::warn;
|
||||
|
||||
use crate::{RouteEntry, Routes};
|
||||
use crate::{RouteEntry, RouteType, Routes};
|
||||
|
||||
const SUPPORTED_ROUTE_SCOPE: [nispor::RouteScope; 2] =
|
||||
[nispor::RouteScope::Universe, nispor::RouteScope::Link];
|
||||
@ -26,7 +26,11 @@ const IPV6_EMPTY_NEXT_HOP_ADDRESS: &str = "::";
|
||||
pub(crate) fn get_routes(running_config_only: bool) -> Routes {
|
||||
let mut ret = Routes::new();
|
||||
let mut np_routes: Vec<nispor::Route> = Vec::new();
|
||||
|
||||
let route_type = [
|
||||
nispor::RouteType::BlackHole,
|
||||
nispor::RouteType::Unreachable,
|
||||
nispor::RouteType::Prohibit,
|
||||
];
|
||||
let protocols = if running_config_only {
|
||||
SUPPORTED_STATIC_ROUTE_PROTOCOL.as_slice()
|
||||
} else {
|
||||
@ -64,6 +68,8 @@ pub(crate) fn get_routes(running_config_only: bool) -> Routes {
|
||||
for route in flat_multipath_route(np_route) {
|
||||
running_routes.push(route);
|
||||
}
|
||||
} else if route_type.contains(&np_route.route_type) {
|
||||
running_routes.push(np_routetype_to_nmstate(np_route));
|
||||
} else if np_route.oif.is_some() {
|
||||
running_routes.push(np_route_to_nmstate(np_route));
|
||||
}
|
||||
@ -80,6 +86,8 @@ pub(crate) fn get_routes(running_config_only: bool) -> Routes {
|
||||
for route in flat_multipath_route(np_route) {
|
||||
config_routes.push(route);
|
||||
}
|
||||
} else if route_type.contains(&np_route.route_type) {
|
||||
config_routes.push(np_routetype_to_nmstate(np_route));
|
||||
} else if np_route.oif.is_some() {
|
||||
config_routes.push(np_route_to_nmstate(np_route));
|
||||
}
|
||||
@ -88,6 +96,50 @@ pub(crate) fn get_routes(running_config_only: bool) -> Routes {
|
||||
ret
|
||||
}
|
||||
|
||||
fn np_routetype_to_nmstate(np_route: &nispor::Route) -> RouteEntry {
|
||||
let destination = match &np_route.dst {
|
||||
Some(dst) => Some(dst.to_string()),
|
||||
None => match np_route.address_family {
|
||||
nispor::AddressFamily::IPv4 => {
|
||||
Some(IPV4_DEFAULT_GATEWAY.to_string())
|
||||
}
|
||||
nispor::AddressFamily::IPv6 => {
|
||||
Some(IPV6_DEFAULT_GATEWAY.to_string())
|
||||
}
|
||||
_ => {
|
||||
warn!(
|
||||
"Route {:?} is holding unknown IP family {:?}",
|
||||
np_route, np_route.address_family
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut route_entry = RouteEntry::new();
|
||||
route_entry.destination = destination;
|
||||
if np_route.address_family == nispor::AddressFamily::IPv6 {
|
||||
route_entry.next_hop_iface = np_route.oif.as_ref().cloned();
|
||||
}
|
||||
route_entry.metric = np_route.metric.map(i64::from);
|
||||
route_entry.table_id = Some(np_route.table);
|
||||
match np_route.route_type {
|
||||
nispor::RouteType::BlackHole => {
|
||||
route_entry.route_type = Some(RouteType::Blackhole)
|
||||
}
|
||||
nispor::RouteType::Unreachable => {
|
||||
route_entry.route_type = Some(RouteType::Unreachable)
|
||||
}
|
||||
nispor::RouteType::Prohibit => {
|
||||
route_entry.route_type = Some(RouteType::Prohibit)
|
||||
}
|
||||
_ => {
|
||||
log::debug!("Got unsupported route {:?}", np_route);
|
||||
}
|
||||
}
|
||||
route_entry
|
||||
}
|
||||
|
||||
fn np_route_to_nmstate(np_route: &nispor::Route) -> RouteEntry {
|
||||
let destination = match &np_route.dst {
|
||||
Some(dst) => Some(dst.to_string()),
|
||||
|
@ -16,6 +16,7 @@ pub struct NmIpRoute {
|
||||
pub table: Option<u32>,
|
||||
pub metric: Option<u32>,
|
||||
pub weight: Option<u32>,
|
||||
pub route_type: Option<String>,
|
||||
_other: DbusDictionary,
|
||||
}
|
||||
|
||||
@ -35,6 +36,7 @@ impl TryFrom<DbusDictionary> for NmIpRoute {
|
||||
table: _from_map!(v, "table", u32::try_from)?,
|
||||
metric: _from_map!(v, "metric", u32::try_from)?,
|
||||
weight,
|
||||
route_type: _from_map!(v, "type", String::try_from)?,
|
||||
_other: v,
|
||||
})
|
||||
}
|
||||
@ -82,7 +84,12 @@ impl NmIpRoute {
|
||||
zvariant::Value::new(zvariant::Value::new(v)),
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(v) = &self.route_type {
|
||||
ret.append(
|
||||
zvariant::Value::new("type"),
|
||||
zvariant::Value::new(zvariant::Value::new(v)),
|
||||
)?;
|
||||
}
|
||||
for (key, value) in self._other.iter() {
|
||||
ret.append(
|
||||
zvariant::Value::new(key.as_str()),
|
||||
|
@ -26,6 +26,9 @@ impl NmIpRoute {
|
||||
if let Some(weight) = self.weight {
|
||||
write!(opt_string, ",weight={}", weight).ok();
|
||||
}
|
||||
if let Some(route_type) = self.route_type.as_ref() {
|
||||
write!(opt_string, ",type={}", route_type).ok();
|
||||
}
|
||||
ret.insert("options".to_string(), opt_string);
|
||||
}
|
||||
ret
|
||||
|
@ -4,7 +4,9 @@ use std::convert::TryFrom;
|
||||
|
||||
use super::super::nm_dbus::NmIpRoute;
|
||||
|
||||
use crate::{ip::is_ipv6_addr, InterfaceIpAddr, NmstateError, RouteEntry};
|
||||
use crate::{
|
||||
ip::is_ipv6_addr, InterfaceIpAddr, NmstateError, RouteEntry, RouteType,
|
||||
};
|
||||
|
||||
pub(crate) fn gen_nm_ip_routes(
|
||||
routes: &[RouteEntry],
|
||||
@ -35,6 +37,12 @@ pub(crate) fn gen_nm_ip_routes(
|
||||
if let Some(weight) = route.weight {
|
||||
nm_route.weight = Some(weight as u32);
|
||||
}
|
||||
nm_route.route_type = match route.route_type {
|
||||
Some(RouteType::Blackhole) => Some("blackhole".to_string()),
|
||||
Some(RouteType::Prohibit) => Some("prohibit".to_string()),
|
||||
Some(RouteType::Unreachable) => Some("unreachable".to_string()),
|
||||
None => None,
|
||||
};
|
||||
ret.push(nm_route);
|
||||
}
|
||||
Ok(ret)
|
||||
|
@ -44,7 +44,9 @@ impl MergedRoutes {
|
||||
if let Some(cur_rts) = current.config.as_ref() {
|
||||
for cur_rt in cur_rts {
|
||||
if let Some(via) = cur_rt.next_hop_iface.as_ref() {
|
||||
if ignored_ifaces.contains(&via.as_str()) {
|
||||
if ignored_ifaces.contains(&via.as_str())
|
||||
&& cur_rt.route_type.is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -78,6 +80,24 @@ impl MergedRoutes {
|
||||
),
|
||||
));
|
||||
}
|
||||
} else if rt.route_type.is_some() {
|
||||
// In nispor, the IPv4 route with route type `Blackhole`,
|
||||
// `Unreachable`, `Prohibit` does not have the route oif
|
||||
// setting.
|
||||
let mut route_type_rt = rt.clone();
|
||||
if !route_type_rt.is_ipv6() {
|
||||
route_type_rt.next_hop_iface = None;
|
||||
}
|
||||
if !cur_routes
|
||||
.as_slice()
|
||||
.iter()
|
||||
.any(|cur_rt| route_type_rt.is_match(cur_rt))
|
||||
{
|
||||
return Err(NmstateError::new(
|
||||
ErrorKind::VerificationError,
|
||||
format!("Desired route {rt} not found after apply"),
|
||||
));
|
||||
}
|
||||
} else if !cur_routes
|
||||
.as_slice()
|
||||
.iter()
|
||||
|
@ -11,6 +11,9 @@ use crate::{
|
||||
ErrorKind, InterfaceType, MergedInterfaces, NmstateError,
|
||||
};
|
||||
|
||||
const DEFAULT_TABLE_ID: u32 = 254; // main route table ID
|
||||
const LOOPBACK_IFACE_NAME: &str = "lo";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@ -64,17 +67,45 @@ impl Routes {
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), NmstateError> {
|
||||
// All desire non-absent route should have next hop interface
|
||||
// All desire non-absent route should have next hop interface except
|
||||
// for route with route type `Blackhole`, `Unreachable`, `Prohibit`.
|
||||
if let Some(config_routes) = self.config.as_ref() {
|
||||
for route in config_routes.iter() {
|
||||
if !route.is_absent() && route.next_hop_iface.is_none() {
|
||||
return Err(NmstateError::new(
|
||||
ErrorKind::NotImplementedError,
|
||||
format!(
|
||||
"Route with empty next hop interface \
|
||||
let no_nexthop_route_type = [
|
||||
RouteType::Blackhole,
|
||||
RouteType::Unreachable,
|
||||
RouteType::Prohibit,
|
||||
];
|
||||
if !route.is_absent() {
|
||||
if route.route_type.is_some()
|
||||
&& no_nexthop_route_type
|
||||
.contains(&route.route_type.unwrap())
|
||||
&& (route.next_hop_iface.is_some()
|
||||
&& route.next_hop_iface
|
||||
!= Some(LOOPBACK_IFACE_NAME.to_string())
|
||||
|| route.next_hop_addr.is_some())
|
||||
{
|
||||
return Err(NmstateError::new(
|
||||
ErrorKind::InvalidArgument,
|
||||
format!(
|
||||
"A {:?} Route cannot have a next \
|
||||
hop : {route:?}",
|
||||
route.route_type.unwrap()
|
||||
),
|
||||
));
|
||||
} else if route.next_hop_iface.is_none()
|
||||
&& (route.route_type.is_none()
|
||||
|| !no_nexthop_route_type
|
||||
.contains(&route.route_type.unwrap()))
|
||||
{
|
||||
return Err(NmstateError::new(
|
||||
ErrorKind::NotImplementedError,
|
||||
format!(
|
||||
"Route with empty next hop interface \
|
||||
is not supported: {route:?}"
|
||||
),
|
||||
));
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(dst) = route.destination.as_deref() {
|
||||
validate_route_dst(dst)?;
|
||||
@ -118,7 +149,8 @@ pub struct RouteEntry {
|
||||
)]
|
||||
/// Route next hop interface name.
|
||||
/// Serialize and deserialize to/from `next-hop-interface`.
|
||||
/// Mandatory for every non-absent routes.
|
||||
/// Mandatory for every non-absent routes except for route with
|
||||
/// route type `Blackhole`, `Unreachable`, `Prohibit`.
|
||||
pub next_hop_iface: Option<String>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
@ -154,6 +186,48 @@ pub struct RouteEntry {
|
||||
deserialize_with = "crate::deserializer::option_u16_or_string"
|
||||
)]
|
||||
pub weight: Option<u16>,
|
||||
/// Route type
|
||||
/// Serialize and deserialize to/from `route-type`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub route_type: Option<RouteType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[non_exhaustive]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum RouteType {
|
||||
Blackhole,
|
||||
Unreachable,
|
||||
Prohibit,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RouteType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Blackhole => "blackhole",
|
||||
Self::Unreachable => "unreachable",
|
||||
Self::Prohibit => "prohibit",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const RTN_BLACKHOLE: u8 = 6;
|
||||
const RTN_UNREACHABLE: u8 = 7;
|
||||
const RTN_PROHIBIT: u8 = 8;
|
||||
|
||||
impl From<RouteType> for u8 {
|
||||
fn from(v: RouteType) -> u8 {
|
||||
match v {
|
||||
RouteType::Blackhole => RTN_BLACKHOLE,
|
||||
RouteType::Unreachable => RTN_UNREACHABLE,
|
||||
RouteType::Prohibit => RTN_PROHIBIT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RouteEntry {
|
||||
@ -195,6 +269,9 @@ impl RouteEntry {
|
||||
if self.weight.is_some() && self.weight != other.weight {
|
||||
return false;
|
||||
}
|
||||
if self.route_type.is_some() && self.route_type != other.route_type {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@ -208,8 +285,10 @@ impl RouteEntry {
|
||||
.as_ref()
|
||||
.map(|d| is_ipv6_addr(d.as_str()))
|
||||
.unwrap_or_default(),
|
||||
self.table_id.unwrap_or(RouteEntry::USE_DEFAULT_ROUTE_TABLE),
|
||||
self.next_hop_iface.as_deref().unwrap_or(""),
|
||||
self.table_id.unwrap_or(DEFAULT_TABLE_ID),
|
||||
self.next_hop_iface
|
||||
.as_deref()
|
||||
.unwrap_or(LOOPBACK_IFACE_NAME),
|
||||
self.destination.as_deref().unwrap_or(""),
|
||||
self.next_hop_addr.as_deref().unwrap_or(""),
|
||||
self.weight.unwrap_or_default(),
|
||||
@ -411,6 +490,8 @@ impl MergedRoutes {
|
||||
));
|
||||
}
|
||||
changed_ifaces.insert(via.as_str());
|
||||
} else if rt.route_type.is_some() {
|
||||
changed_ifaces.insert(LOOPBACK_IFACE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@ -423,6 +504,8 @@ impl MergedRoutes {
|
||||
if absent_rt.is_match(rt) {
|
||||
if let Some(via) = rt.next_hop_iface.as_ref() {
|
||||
changed_ifaces.insert(via.as_str());
|
||||
} else {
|
||||
changed_ifaces.insert(LOOPBACK_IFACE_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -479,6 +562,13 @@ impl MergedRoutes {
|
||||
Entry::Vacant(v) => v.insert(Vec::new()),
|
||||
};
|
||||
rts.push(rt);
|
||||
} else if rt.route_type.is_some() {
|
||||
let rts: &mut Vec<RouteEntry> =
|
||||
match indexed.entry(LOOPBACK_IFACE_NAME.to_string()) {
|
||||
Entry::Occupied(o) => o.into_mut(),
|
||||
Entry::Vacant(v) => v.insert(Vec::new()),
|
||||
};
|
||||
rts.push(rt);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,10 @@ class Route:
|
||||
NEXT_HOP_ADDRESS = "next-hop-address"
|
||||
METRIC = "metric"
|
||||
WEIGHT = "weight"
|
||||
ROUTETYPE = "route-type"
|
||||
ROUTETYPE_BLACKHOLE = "blackhole"
|
||||
ROUTETYPE_UNREACHABLE = "unreachable"
|
||||
ROUTETYPE_PROHIBIT = "prohibit"
|
||||
USE_DEFAULT_METRIC = -1
|
||||
USE_DEFAULT_ROUTE_TABLE = 0
|
||||
|
||||
|
@ -115,6 +115,11 @@ def test_dns_edit(eth1_up):
|
||||
|
||||
|
||||
@pytest.mark.tier1
|
||||
@pytest.mark.skipif(
|
||||
nm_minor_version() < 42,
|
||||
reason="Loopback is only support on NM 1.42+, and blackhole type route "
|
||||
"is stored in loopback",
|
||||
)
|
||||
def test_add_remove_routes(eth1_up):
|
||||
"""
|
||||
Test adding a strict route and removing all routes next hop to eth1.
|
||||
|
@ -140,6 +140,175 @@ def test_add_static_route_without_next_hop_address(eth1_up):
|
||||
assert_routes(routes, cur_state)
|
||||
|
||||
|
||||
@pytest.mark.tier1
|
||||
@pytest.mark.skipif(
|
||||
nm_minor_version() < 42,
|
||||
reason="Loopback is only support on NM 1.42+, and blackhole type route "
|
||||
"is stored in loopback",
|
||||
)
|
||||
def test_add_static_route_with_route_type(eth1_up):
|
||||
route = [
|
||||
{
|
||||
Route.DESTINATION: IPV4_TEST_NET1,
|
||||
Route.NEXT_HOP_INTERFACE: "lo",
|
||||
Route.ROUTETYPE: Route.ROUTETYPE_BLACKHOLE,
|
||||
},
|
||||
{
|
||||
Route.DESTINATION: IPV6_TEST_NET1,
|
||||
Route.ROUTETYPE: Route.ROUTETYPE_UNREACHABLE,
|
||||
},
|
||||
{
|
||||
Route.DESTINATION: "198.51.100.0/24",
|
||||
Route.ROUTETYPE: Route.ROUTETYPE_PROHIBIT,
|
||||
},
|
||||
]
|
||||
libnmstate.apply(
|
||||
{
|
||||
Interface.KEY: [ETH1_INTERFACE_STATE],
|
||||
Route.KEY: {Route.CONFIG: route},
|
||||
}
|
||||
)
|
||||
routes_output4 = _get_routes_from_iproute(4, "main")
|
||||
routes_output6 = _get_routes_from_iproute(6, "main")
|
||||
assert IPV4_TEST_NET1 in routes_output4
|
||||
assert Route.ROUTETYPE_BLACKHOLE in routes_output4
|
||||
assert "198.51.100.0/24" in routes_output4
|
||||
assert Route.ROUTETYPE_PROHIBIT in routes_output4
|
||||
assert IPV6_TEST_NET1 in routes_output6
|
||||
assert Route.ROUTETYPE_UNREACHABLE in routes_output6
|
||||
|
||||
|
||||
@pytest.mark.tier1
|
||||
@pytest.mark.skipif(
|
||||
nm_minor_version() < 42,
|
||||
reason="Loopback is only support on NM 1.42+, and blackhole type route "
|
||||
"is stored in loopback",
|
||||
)
|
||||
def test_add_static_route_and_apply_route_absent(eth1_up):
|
||||
routes = [
|
||||
{
|
||||
Route.DESTINATION: "198.51.100.0/24",
|
||||
Route.ROUTETYPE: Route.ROUTETYPE_BLACKHOLE,
|
||||
},
|
||||
{
|
||||
Route.DESTINATION: IPV4_TEST_NET1,
|
||||
Route.NEXT_HOP_INTERFACE: "eth1",
|
||||
Route.NEXT_HOP_ADDRESS: "192.0.2.1",
|
||||
},
|
||||
{
|
||||
Route.DESTINATION: IPV6_TEST_NET1,
|
||||
Route.NEXT_HOP_INTERFACE: "eth1",
|
||||
Route.NEXT_HOP_ADDRESS: "2001:db8:1::b",
|
||||
},
|
||||
]
|
||||
libnmstate.apply(
|
||||
{
|
||||
Interface.KEY: [ETH1_INTERFACE_STATE],
|
||||
Route.KEY: {Route.CONFIG: routes},
|
||||
}
|
||||
)
|
||||
absent_route = routes[0]
|
||||
absent_route[Route.STATE] = Route.STATE_ABSENT
|
||||
libnmstate.apply(
|
||||
{
|
||||
Interface.KEY: [ETH1_INTERFACE_STATE],
|
||||
Route.KEY: {Route.CONFIG: [absent_route]},
|
||||
}
|
||||
)
|
||||
remaining_routes = routes[1:]
|
||||
cur_state = libnmstate.show()
|
||||
assert_routes(remaining_routes, cur_state)
|
||||
|
||||
|
||||
@pytest.mark.tier1
|
||||
@pytest.mark.skipif(
|
||||
nm_minor_version() < 42,
|
||||
reason="Loopback is only support on NM 1.42+, and blackhole type route "
|
||||
"is stored in loopback",
|
||||
)
|
||||
def test_add_static_Ipv4_route_with_route_type(eth1_up):
|
||||
routes = [
|
||||
{
|
||||
Route.DESTINATION: "198.51.100.0/24",
|
||||
Route.NEXT_HOP_INTERFACE: "lo",
|
||||
Route.ROUTETYPE: Route.ROUTETYPE_BLACKHOLE,
|
||||
},
|
||||
]
|
||||
libnmstate.apply(
|
||||
{
|
||||
Interface.KEY: [ETH1_INTERFACE_STATE],
|
||||
Route.KEY: {Route.CONFIG: routes},
|
||||
}
|
||||
)
|
||||
cur_state = libnmstate.show()
|
||||
current_routes = cur_state[Route.KEY][Route.CONFIG]
|
||||
for route in current_routes:
|
||||
if route.get(Route.ROUTETYPE, None) == Route.ROUTETYPE_BLACKHOLE:
|
||||
assert route.get(Route.NEXT_HOP_INTERFACE, None) is None
|
||||
|
||||
|
||||
@pytest.mark.tier1
|
||||
@pytest.mark.skipif(
|
||||
nm_minor_version() < 42,
|
||||
reason="Loopback is only support on NM 1.42+, and blackhole type route "
|
||||
"is stored in loopback",
|
||||
)
|
||||
def test_route_type_with_next_hop_interface(eth1_up):
|
||||
route = [
|
||||
{
|
||||
Route.DESTINATION: IPV4_TEST_NET1,
|
||||
Route.NEXT_HOP_INTERFACE: "eth1",
|
||||
Route.ROUTETYPE: Route.ROUTETYPE_BLACKHOLE,
|
||||
},
|
||||
]
|
||||
state = {
|
||||
Interface.KEY: [ETH1_INTERFACE_STATE],
|
||||
Route.KEY: {Route.CONFIG: route},
|
||||
}
|
||||
|
||||
with pytest.raises(NmstateValueError):
|
||||
libnmstate.apply(state)
|
||||
|
||||
|
||||
@pytest.mark.tier1
|
||||
@pytest.mark.skipif(
|
||||
nm_minor_version() < 42,
|
||||
reason="Loopback is only support on NM 1.42+, and blackhole type route "
|
||||
"is stored in loopback",
|
||||
)
|
||||
def test_apply_route_with_route_type_multiple_times(eth1_up):
|
||||
routes = [
|
||||
{
|
||||
Route.DESTINATION: "198.51.100.0/24",
|
||||
Route.ROUTETYPE: Route.ROUTETYPE_BLACKHOLE,
|
||||
},
|
||||
{
|
||||
Route.DESTINATION: IPV6_TEST_NET1,
|
||||
Route.ROUTETYPE: Route.ROUTETYPE_UNREACHABLE,
|
||||
},
|
||||
]
|
||||
libnmstate.apply(
|
||||
{
|
||||
Interface.KEY: [ETH1_INTERFACE_STATE],
|
||||
Route.KEY: {Route.CONFIG: routes},
|
||||
}
|
||||
)
|
||||
libnmstate.apply(
|
||||
{
|
||||
Interface.KEY: [ETH1_INTERFACE_STATE],
|
||||
Route.KEY: {Route.CONFIG: routes},
|
||||
}
|
||||
)
|
||||
_, routes_out_v4, _ = cmdlib.exec_cmd(
|
||||
"nmcli -g ipv4.routes con show lo".split(), check=True
|
||||
)
|
||||
_, routes_out_v6, _ = cmdlib.exec_cmd(
|
||||
"nmcli -g ipv6.routes con show lo".split(), check=True
|
||||
)
|
||||
assert routes_out_v4.count("blackhole") == 1
|
||||
assert routes_out_v6.count("unreachable") == 1
|
||||
|
||||
|
||||
@pytest.mark.tier1
|
||||
def test_add_gateway(eth1_up):
|
||||
routes = [_get_ipv4_gateways()[0], _get_ipv6_test_routes()[0]]
|
||||
|
@ -141,7 +141,7 @@ def assert_no_config_route_to_iface(iface_name):
|
||||
assert not any(
|
||||
route
|
||||
for route in current_state[Route.KEY][Route.CONFIG]
|
||||
if route[Route.NEXT_HOP_INTERFACE] == iface_name
|
||||
if route.get(Route.NEXT_HOP_INTERFACE, None) == iface_name
|
||||
)
|
||||
|
||||
|
||||
|
@ -30,7 +30,7 @@ def assert_routes(routes, state, nic="eth1"):
|
||||
routes.sort(key=_route_sort_key)
|
||||
config_routes = []
|
||||
for config_route in state[Route.KEY][Route.CONFIG]:
|
||||
if config_route[Route.NEXT_HOP_INTERFACE] == nic:
|
||||
if config_route.get(Route.NEXT_HOP_INTERFACE, None) == nic:
|
||||
config_routes.append(config_route)
|
||||
|
||||
# The kernel routes contains more route entries than desired config
|
||||
|
Loading…
x
Reference in New Issue
Block a user