Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
26 changes: 21 additions & 5 deletions src/providers/kubevirt/nocloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ pub fn read_config_file(path: &Path, file: &str) -> Result<Option<BufReader<File
Ok(Some(BufReader::new(file)))
}

/// Parse a route destination in CIDR notation
///
/// Like cloud-init and Netplan, `default` is accepted as an alias for the
/// default route of the gateway's address family.
fn parse_route_destination(destination: &str, gateway: IpAddr) -> Result<IpNetwork> {
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
Expand Down Expand Up @@ -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<String>,
/// Destination network (OpenStack alias for `destination`)
pub network: Option<String>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
});
}

Expand Down
48 changes: 48 additions & 0 deletions src/providers/kubevirt/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 2 additions & 0 deletions tests/fixtures/kubevirt/route_to_default_nocloud_v1/meta-data
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
instance-id: test-route-to-default-v1
local-hostname: test-route-to-default-v1
15 changes: 15 additions & 0 deletions tests/fixtures/kubevirt/route_to_default_nocloud_v1/network-config
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions tests/fixtures/kubevirt/route_to_default_nocloud_v2/meta-data
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
instance-id: test-route-to-default-v2
local-hostname: test-route-to-default-v2
11 changes: 11 additions & 0 deletions tests/fixtures/kubevirt/route_to_default_nocloud_v2/network-config
Original file line number Diff line number Diff line change
@@ -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"
Loading