Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ jobs:
run: cargo fmt -- --check -l
- name: cargo clippy (warnings)
run: cargo clippy --all-targets -- -D warnings
- name: validate fixtures
run: make validate-fixtures
tests-other-channels:
name: Tests, unstable toolchain
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ lint:
.PHONY: test
test:
cargo test --all-targets --release

.PHONY: validate-fixtures
validate-fixtures:
python3 tests/fixtures/validate.py
2 changes: 1 addition & 1 deletion docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ nav_order: 8

Major changes:

- KubeVirt: Add support for static IP configuration from cloud-init
- KubeVirt: Add support for static and dynamic IP configuration from cloud-init

Minor changes:

Expand Down
18 changes: 18 additions & 0 deletions src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ pub fn try_parse_cidr(address: IpAddr, netmask: IpAddr) -> Result<IpNetwork> {
IpNetwork::new(address, prefix).context("failed to parse network")
}

/// Format an IP address for dracut kernel arguments.
/// IPv6 addresses are wrapped in brackets so dracut's colon-delimited
/// parser can distinguish them from field separators.
pub fn dracut_addr(addr: &IpAddr) -> String {
match addr {
IpAddr::V6(_) => format!("[{addr}]"),
_ => addr.to_string(),
}
}

/// Format an IP network (address/prefix) for dracut kernel arguments.
pub fn dracut_network(net: &IpNetwork) -> String {
match net {
IpNetwork::V6(_) => format!("[{net}]"),
_ => net.to_string(),
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NetworkRoute {
pub destination: IpNetwork,
Expand Down
33 changes: 17 additions & 16 deletions src/providers/kubevirt/cloudconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use super::provider::NetworkConfigurationFormat;
use crate::{
network::{DhcpSetting, Interface, VirtualNetDev},
network::{dracut_addr, dracut_network, DhcpSetting, Interface, VirtualNetDev},
providers::{kubevirt::configdrive::NetworkData, MetadataProvider},
};
use anyhow::{bail, Context, Result};
Expand Down Expand Up @@ -201,21 +201,9 @@ impl MetadataProvider for KubeVirtCloudConfig {
for addr in iface.ip_addresses {
let (ip, netmask_or_prefix) = match addr {
IpNetwork::V4(n) => (n.ip().to_string(), n.mask().to_string()),
IpNetwork::V6(n) => (n.ip().to_string(), n.prefix().to_string()),
IpNetwork::V6(n) => (dracut_addr(&n.ip().into()), n.prefix().to_string()),
};

let gateway = iface.routes.iter().find(|r| {
r.destination.prefix() == 0 && r.destination.is_ipv4() == addr.is_ipv4()
});

if let Some(gateway) = gateway {
kargs.push(format!(
"ip={}::{}:{}::{}:static",
ip, gateway.gateway, netmask_or_prefix, id,
));
} else {
kargs.push(format!("ip={}:::{}::{}:static", ip, netmask_or_prefix, id));
}
kargs.push(format!("ip={}:::{}::{}:static", ip, netmask_or_prefix, id));
}

// Add DHCP configuration
Expand All @@ -227,6 +215,19 @@ impl MetadataProvider for KubeVirtCloudConfig {
}
}

// Add static routes for the interface (including DHCP interfaces)
// This allows DHCP interfaces to have static gateway configuration
for route in &iface.routes {
// Only add routes with prefix 0 (default routes)
if route.destination.prefix() == 0 {
kargs.push(format!(
"rd.route={}:{}",
dracut_network(&route.destination),
dracut_addr(&route.gateway)
));
}
}

// Collect nameservers from all interfaces
for nameserver in &iface.nameservers {
if !all_nameservers.contains(nameserver) {
Expand All @@ -237,7 +238,7 @@ impl MetadataProvider for KubeVirtCloudConfig {

// Add nameservers as separate arguments
for nameserver in &all_nameservers {
kargs.push(format!("nameserver={}", nameserver));
kargs.push(format!("nameserver={}", dracut_addr(nameserver)));
}

if kargs.is_empty() {
Expand Down
66 changes: 35 additions & 31 deletions src/providers/kubevirt/configdrive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,47 +265,51 @@ impl NetworkData {
}
}

// Collect nameservers
// Collect nameservers from network-specific DNS configuration
for ns in &network.dns_nameservers {
let nameserver = IpAddr::from_str(ns)?;
if !all_nameservers.contains(&nameserver) {
all_nameservers.push(nameserver);
}
}

// Process routes
for route in &network.routes {
// Handle network and netmask according to OpenStack schema
let destination = if route.network == "0.0.0.0" && route.netmask == "0.0.0.0" {
// Default IPv4 route
IpNetwork::from_str("0.0.0.0/0")?
} else if route.network == "::" && route.netmask == "::" {
// Default IPv6 route
IpNetwork::from_str("::/0")?
} else {
// Calculate prefix length from netmask for proper CIDR notation
let network_addr = IpAddr::from_str(&route.network)?;
if let Ok(netmask_addr) = IpAddr::from_str(&route.netmask) {
IpNetwork::with_netmask(network_addr, netmask_addr)?
} else if let Ok(prefix_len) = route.netmask.parse::<u8>() {
IpNetwork::new(network_addr, prefix_len)?
// Process routes — always add routes when present, regardless of network type.
// Per the OpenStack schema, routes are valid on any network type including
// ipv4_dhcp/ipv6_dhcp, allowing static gateway configuration with DHCP addresses.
{
for route in &network.routes {
// Handle network and netmask according to OpenStack schema
let destination = if route.network == "0.0.0.0" && route.netmask == "0.0.0.0" {
// Default IPv4 route
IpNetwork::from_str("0.0.0.0/0")?
} else if route.network == "::" && route.netmask == "::" {
// Default IPv6 route
IpNetwork::from_str("::/0")?
} else {
// For IPv6, netmask might be in full format like "ffff:ffff:ffff:ffff::"
if network_addr.is_ipv6() && route.netmask == "ffff:ffff:ffff:ffff::" {
IpNetwork::new(network_addr, 64)?
// Calculate prefix length from netmask for proper CIDR notation
let network_addr = IpAddr::from_str(&route.network)?;
if let Ok(netmask_addr) = IpAddr::from_str(&route.netmask) {
IpNetwork::with_netmask(network_addr, netmask_addr)?
} else if let Ok(prefix_len) = route.netmask.parse::<u8>() {
IpNetwork::new(network_addr, prefix_len)?
} else {
return Err(anyhow::anyhow!(
"Invalid netmask format: {}. Expected IP address or prefix length.",
route.netmask
));
// For IPv6, netmask might be in full format like "ffff:ffff:ffff:ffff::"
if network_addr.is_ipv6() && route.netmask == "ffff:ffff:ffff:ffff::" {
IpNetwork::new(network_addr, 64)?
} else {
return Err(anyhow::anyhow!(
"Invalid netmask format: {}. Expected IP address or prefix length.",
route.netmask
));
}
}
}
};
let gateway = IpAddr::from_str(&route.gateway)?;
iface.routes.push(NetworkRoute {
destination,
gateway,
});
};
let gateway = IpAddr::from_str(&route.gateway)?;
iface.routes.push(NetworkRoute {
destination,
gateway,
});
}
}
}

Expand Down
Loading
Loading