diff --git a/docs/release-notes.md b/docs/release-notes.md index 4e00cf43b..4d6535e0a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ Minor changes: - Reuse the existing HTTP client when building PATCH, PUT, and POST requests - Azure: Read the admin password from OVF `UserPassword` when rendering the platform-user Ignition fragment +- KubeVirt: Support `default` route destinations in NoCloud network config Packaging changes: diff --git a/src/providers/kubevirt/nocloud.rs b/src/providers/kubevirt/nocloud.rs index f63c6d550..0d27cca50 100644 --- a/src/providers/kubevirt/nocloud.rs +++ b/src/providers/kubevirt/nocloud.rs @@ -24,6 +24,21 @@ pub fn read_config_file(path: &Path, file: &str) -> Result Result { + if destination == "default" { + let unspecified = match gateway { + IpAddr::V4(_) => "0.0.0.0/0", + IpAddr::V6(_) => "::/0", + }; + return Ok(IpNetwork::from_str(unspecified)?); + } + Ok(IpNetwork::from_str(destination)?) +} + /// Cloud-init Network Config format wrapper /// /// This can be either v1 or v2 format @@ -71,7 +86,7 @@ pub struct NetworkConfigV1Entry { /// OpenStack-compatible `network`/`netmask` alias. #[derive(Debug, Deserialize)] pub struct RouteConfigV1 { - /// Destination in CIDR notation (preferred) + /// Destination in CIDR notation (preferred), or `default` pub destination: Option, /// Destination network (OpenStack alias for `destination`) pub network: Option, @@ -195,7 +210,7 @@ pub struct NameserversConfig { /// Route configuration in v2 format #[derive(Debug, Deserialize)] pub struct RouteConfigV2 { - /// Destination network in CIDR notation + /// Destination network in CIDR notation, or `default` pub to: String, /// Gateway address pub via: String, @@ -351,7 +366,7 @@ impl NetworkConfigV1Entry { // Parse destination: prefer `destination` (CIDR), fall back to `network`/`netmask` let destination = if let Some(dest) = &route.destination { - IpNetwork::from_str(dest)? + parse_route_destination(dest, gateway)? } else if let Some(network) = &route.network { let network_addr = IpAddr::from_str(network)?; if let Some(netmask) = &route.netmask { @@ -455,9 +470,10 @@ impl NetworkConfigV2 { // Process explicit routes first for route in &config.routes { + let gateway = IpAddr::from_str(&route.via)?; iface.routes.push(NetworkRoute { - destination: IpNetwork::from_str(&route.to)?, - gateway: IpAddr::from_str(&route.via)?, + destination: parse_route_destination(&route.to, gateway)?, + gateway, }); } diff --git a/src/providers/kubevirt/tests.rs b/src/providers/kubevirt/tests.rs index d25724b8d..d4495904f 100644 --- a/src/providers/kubevirt/tests.rs +++ b/src/providers/kubevirt/tests.rs @@ -398,3 +398,51 @@ fn test_no_duplicate_routes_nocloud_v2() { .expect("cannot parse config"); assert_no_duplicate_routes(&config); } + +/// `default` route destinations resolve to the gateway's address family. +fn assert_route_to_default(config: &KubeVirtCloudConfig) { + let interfaces = config.networks().expect("cannot get interfaces"); + assert_eq!(interfaces.len(), 1); + + let eth0 = &interfaces[0]; + assert_eq!(eth0.routes.len(), 2, "Unexpected routes: {:?}", eth0.routes); + assert!(eth0 + .routes + .iter() + .any(|r| r.gateway == IpAddr::from_str("192.168.1.1").unwrap() + && r.destination == IpNetwork::from_str("0.0.0.0/0").unwrap())); + assert!(eth0 + .routes + .iter() + .any(|r| r.gateway == IpAddr::from_str("2001:db8::1").unwrap() + && r.destination == IpNetwork::from_str("::/0").unwrap())); + + let kargs = config.rd_network_kargs().unwrap().unwrap(); + let expected = [ + "rd.route=0.0.0.0/0:192.168.1.1", + "rd.route=[::/0]:[2001:db8::1]", + ]; + for exp in expected { + assert!(kargs.contains(exp), "Missing '{exp}' in: {kargs}"); + } +} + +#[test] +fn test_route_to_default_nocloud_v1() { + let config = KubeVirtCloudConfig::try_new( + Path::new("./tests/fixtures/kubevirt/route_to_default_nocloud_v1"), + NetworkConfigurationFormat::NoCloud, + ) + .expect("cannot parse config"); + assert_route_to_default(&config); +} + +#[test] +fn test_route_to_default_nocloud_v2() { + let config = KubeVirtCloudConfig::try_new( + Path::new("./tests/fixtures/kubevirt/route_to_default_nocloud_v2"), + NetworkConfigurationFormat::NoCloud, + ) + .expect("cannot parse config"); + assert_route_to_default(&config); +} diff --git a/tests/fixtures/kubevirt/route_to_default_nocloud_v1/meta-data b/tests/fixtures/kubevirt/route_to_default_nocloud_v1/meta-data new file mode 100644 index 000000000..4ecd9feb1 --- /dev/null +++ b/tests/fixtures/kubevirt/route_to_default_nocloud_v1/meta-data @@ -0,0 +1,2 @@ +instance-id: test-route-to-default-v1 +local-hostname: test-route-to-default-v1 diff --git a/tests/fixtures/kubevirt/route_to_default_nocloud_v1/network-config b/tests/fixtures/kubevirt/route_to_default_nocloud_v1/network-config new file mode 100644 index 000000000..f975fb8f3 --- /dev/null +++ b/tests/fixtures/kubevirt/route_to_default_nocloud_v1/network-config @@ -0,0 +1,15 @@ +version: 1 +config: + - type: physical + name: eth0 + subnets: + - type: static + address: 192.168.1.10/24 + routes: + - destination: default + gateway: 192.168.1.1 + - type: static6 + address: "2001:db8::10/64" + routes: + - destination: default + gateway: "2001:db8::1" diff --git a/tests/fixtures/kubevirt/route_to_default_nocloud_v2/meta-data b/tests/fixtures/kubevirt/route_to_default_nocloud_v2/meta-data new file mode 100644 index 000000000..240a81745 --- /dev/null +++ b/tests/fixtures/kubevirt/route_to_default_nocloud_v2/meta-data @@ -0,0 +1,2 @@ +instance-id: test-route-to-default-v2 +local-hostname: test-route-to-default-v2 diff --git a/tests/fixtures/kubevirt/route_to_default_nocloud_v2/network-config b/tests/fixtures/kubevirt/route_to_default_nocloud_v2/network-config new file mode 100644 index 000000000..1b7852d5c --- /dev/null +++ b/tests/fixtures/kubevirt/route_to_default_nocloud_v2/network-config @@ -0,0 +1,11 @@ +version: 2 +ethernets: + eth0: + addresses: + - 192.168.1.10/24 + - "2001:db8::10/64" + routes: + - to: default + via: 192.168.1.1 + - to: default + via: "2001:db8::1"