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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/agent/src/dhcp_server_grpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ impl From<ModelInterfaceInfoV6> for proto::InterfaceInfoV6 {
impl From<ModelInterfaceInfo> for proto::InterfaceInfo {
fn from(i: ModelInterfaceInfo) -> Self {
proto::InterfaceInfo {
address: i.address.to_string(),
gateway: i.gateway.to_string(),
address: i.address.map(|address| address.to_string()),
gateway: i.gateway.map(|gateway| gateway.to_string()),
prefix: i.prefix,
fqdn: i.fqdn,
booturl: i.booturl,
Expand Down
63 changes: 29 additions & 34 deletions crates/agent/src/ethernet_virtualization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,8 @@ pub(super) async fn update_nvue(
}
};

// For dual-stack FNN, the DPU-side IPv6 address is the network address
// of the /127 linknet (the ::0 end). The ::1 end is the host.
// For FNN interfaces with IPv6, the DPU-side address is the network
// address of the /127 linknet (the ::0 end). The ::1 end is the host.
ifs.push(nvue::PortConfig {
interface_name: name,
is_phy: net.function_type == rpc::InterfaceFunctionType::Physical as i32,
Expand Down Expand Up @@ -1137,7 +1137,7 @@ pub(super) async fn interfaces(
mac_address: Some(factory_mac_address.to_string()),
addresses,
prefixes,
gateways: vec![iface.gateway.clone()],
gateways: build_dual_stack_list(iface.gateway.clone(), None),
network_security_group: None,
internal_uuid: iface.internal_uuid.clone(),
});
Expand Down Expand Up @@ -1224,7 +1224,7 @@ pub(super) async fn interfaces(
mac_address: mac,
addresses,
prefixes,
gateways: vec![iface.gateway.clone()],
gateways: build_dual_stack_list(iface.gateway.clone(), None),
network_security_group,
internal_uuid: iface.internal_uuid.clone(),
});
Expand All @@ -1239,7 +1239,7 @@ pub(super) fn tenant_peers(network_config: &rpc::ManagedHostNetworkConfigRespons
network_config
.tenant_interfaces
.iter()
.map(|iface| iface.ip.as_str())
.filter_map(|iface| (!iface.ip.is_empty()).then_some(iface.ip.as_str()))
.collect()
}

Expand Down Expand Up @@ -1838,7 +1838,6 @@ mod tests {
use ::rpc::{common as rpc_common, forge as rpc};
use carbide_network::virtualization::{VpcVirtualizationType, get_svi_ip};
use carbide_rpc_utils::dhcp::{DhcpConfig, HostConfig};
use carbide_utils::none_if_empty::NoneIfEmpty;
use eyre::WrapErr;
use ipnetwork::IpNetwork;

Expand Down Expand Up @@ -3716,36 +3715,32 @@ mod tests {
}
}

#[test]
fn test_dual_stack_addresses_building() {
// Verify the iterator-based pattern used to build dual-stack address/prefix vectors.
let ip = "10.0.0.1".to_string();
let ip6 = Some("2001:db8::1".to_string());
let interface_prefix = "10.0.0.0/31".to_string();
let interface_prefix_v6 = Some("2001:db8::/127".to_string());

let addresses: Vec<String> = std::iter::once(ip.clone())
.chain(ip6.none_if_empty())
.collect();
assert_eq!(addresses, vec!["10.0.0.1", "2001:db8::1"]);
#[tokio::test]
#[allow(deprecated)]
async fn ipv6_only_status_omits_empty_ipv4_compatibility_values() {
let network_config = rpc::ManagedHostNetworkConfigResponse {
tenant_interfaces: vec![rpc::FlatInterfaceConfig {
function_type: rpc::InterfaceFunctionType::Physical.into(),
vlan_id: 100,
ipv6_interface_config: Some(rpc::FlatInterfaceIpv6Config {
ip: "2001:db8::1".to_string(),
interface_prefix: "2001:db8::/127".to_string(),
svi_ip: None,
}),
..Default::default()
}],
..Default::default()
};

let prefixes: Vec<String> = std::iter::once(interface_prefix)
.chain(interface_prefix_v6.none_if_empty())
.collect();
assert_eq!(prefixes, vec!["10.0.0.0/31", "2001:db8::/127"]);
assert!(tenant_peers(&network_config).is_empty());

// Verify empty ip6 is not included.
let empty_ip6: Option<String> = Some("".to_string());
let addresses2: Vec<String> = std::iter::once(ip)
.chain(empty_ip6.none_if_empty())
.collect();
assert_eq!(addresses2, vec!["10.0.0.1"]);
let observations = interfaces(&network_config, "02:00:00:00:00:01".parse().unwrap(), None)
.await
.unwrap();

// Verify None ip6 is not included.
let none_ip6: Option<String> = None;
let addresses3: Vec<String> = std::iter::once("10.0.0.1".to_string())
.chain(none_ip6.none_if_empty())
.collect();
assert_eq!(addresses3, vec!["10.0.0.1"]);
assert_eq!(observations.len(), 1);
assert_eq!(observations[0].addresses, vec!["2001:db8::1"]);
assert_eq!(observations[0].prefixes, vec!["2001:db8::/127"]);
assert!(observations[0].gateways.is_empty());
}
}
42 changes: 39 additions & 3 deletions crates/agent/src/nvue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,9 @@ pub fn build(conf: NvueConfig) -> eyre::Result<String> {
RoutingProfile: interface_routing_profile.unwrap_or_default(),
IsPhy: network.is_phy,
L2VNI: network.vni.map(|x| x.to_string()).unwrap_or("".to_string()),
IPs: vec![network.gateway_cidr.clone()],
IPs: std::iter::once(network.gateway_cidr.clone())
.filter(|address| !address.is_empty())
.collect(),
IPsIpv6: network
.ipv6_port_config
.as_ref()
Expand Down Expand Up @@ -1262,7 +1264,7 @@ pub struct PortConfig {
pub vni: Option<u32>, // In FNN, admin network has both an l2vni and an l3vni
pub l3_vni: Option<u32>,
pub gateway_cidr: String,
/// Optional IPv6 configuration for dual-stack interfaces.
/// Optional IPv6 configuration for interfaces that include IPv6.
pub ipv6_port_config: Option<Ipv6PortConfig>,
pub vpc_prefixes: Vec<String>,
pub vpc_peer_prefixes: Vec<String>,
Expand Down Expand Up @@ -1554,7 +1556,7 @@ struct TmplVpc {
struct TmplHostInterfaces {
ID: u32,
HostIP: String,
/// IPv6 host address (if dual-stack).
/// IPv6 host address, when configured.
HostIPv6: Option<String>,

// HostRoute in the context of FNN-L3 is the /31 prefix allocation.
Expand Down Expand Up @@ -2316,6 +2318,40 @@ mod tests {
);
}

#[test]
fn test_build_fnn_ipv6_only_interface() {
let mut conf = dual_stack_fnn_config();
let port = conf
.ct_port_configs
.first_mut()
.expect("fixture should have a port");
port.host_ip.clear();
port.host_route.clear();
port.gateway_cidr.clear();
port.svi_ip = None;
port.vpc_prefixes
.retain(|prefix| matches!(prefix.parse::<IpNet>(), Ok(IpNet::V6(_))));

let vlan = conf
.ct_access_vlans
.first_mut()
.expect("fixture should have an access VLAN");
vlan.ip.clear();
vlan.network.clear();

let output = build(conf).expect("build should succeed");
let docs: serde_yaml::Value =
serde_yaml::from_str(&output).expect("output should be valid YAML");
let set = &docs.as_sequence().unwrap()[1]["set"];

assert_eq!(
yaml_mapping_keys(&set["interface"]["pf0vf0_if"]["ip"]["address"]),
address_set(&["2001:db8::0/127"]),
);
let neighbors = &set["vrf"]["vpc_100"]["router"]["bgp"]["neighbor"];
assert_eq!(yaml_mapping_keys(neighbors), address_set(&["2001:db8::1"]),);
}

#[test]
fn test_build_fnn_dual_stack_l2_interface() {
let mut conf = dual_stack_fnn_config();
Expand Down
51 changes: 35 additions & 16 deletions crates/agent/src/periodic_config_fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,19 +333,21 @@ fn normalize_interface_addresses(
}
}

let ipv4 = ipv4.ok_or_else(|| {
eyre::eyre!(
"IPv4 address configuration is required; IPv6-only agent support is tracked by https://github.com/NVIDIA/infra-controller/issues/2402"
)
})?;

interface.gateway.clone_from(&ipv4.gateway);
interface.ip.clone_from(&ipv4.ip);
interface
.interface_prefix
.clone_from(&ipv4.interface_prefix);
interface.prefix.clone_from(&ipv4.prefix);
interface.svi_ip.clone_from(&ipv4.svi_ip);
if let Some(ipv4) = ipv4 {
interface.gateway.clone_from(&ipv4.gateway);
interface.ip.clone_from(&ipv4.ip);
interface
.interface_prefix
.clone_from(&ipv4.interface_prefix);
interface.prefix.clone_from(&ipv4.prefix);
interface.svi_ip.clone_from(&ipv4.svi_ip);
} else {
interface.gateway.clear();
interface.ip.clear();
interface.interface_prefix.clear();
interface.prefix.clear();
interface.svi_ip = None;
}
let prefixless_legacy_ipv6 = interface
.ipv6_interface_config
.clone()
Expand Down Expand Up @@ -594,6 +596,21 @@ mod tests {
}
}

// Describes an authoritative IPv6-only projection with cleared IPv4 compatibility fields.
#[allow(deprecated)]
fn expected_ipv6_interface() -> rpc::FlatInterfaceConfig {
rpc::FlatInterfaceConfig {
vlan_id: 100,
ipv6_interface_config: Some(rpc::FlatInterfaceIpv6Config {
ip: "2001:db8::1".to_string(),
interface_prefix: "2001:db8::/127".to_string(),
svi_ip: Some("2001:db8::2/64".to_string()),
}),
addresses: vec![ipv6_address()],
..Default::default()
}
}

// Exercises authoritative clearing of the deprecated optional SVI field.
#[test]
#[allow(deprecated)]
Expand Down Expand Up @@ -643,6 +660,9 @@ mod tests {
"reversed dual-stack list is selected by family without reordering" {
legacy_interface(vec![ipv6_address(), ipv4_address()]) => Yields(dual_stack),
}
"IPv6-only list clears stale IPv4 compatibility fields" {
legacy_interface(vec![ipv6_address()]) => Yields(expected_ipv6_interface()),
}
"absent V4 SVI clears the legacy value" {
legacy_interface(vec![ipv4_without_svi]) => Yields(expected_ipv4_without_svi),
}
Expand All @@ -663,9 +683,6 @@ mod tests {
unknown.address_family = 99;

scenarios!(run = normalized_interface;
"V4 is required during the compatibility phase" {
legacy_interface(vec![ipv6_address()]) => Fails,
}
"explicit family is required" {
legacy_interface(vec![unspecified]) => Fails,
}
Expand All @@ -688,6 +705,7 @@ mod tests {
tenant_interfaces: vec![
legacy_interface(vec![ipv4_address()]),
legacy_interface(vec![ipv6_address(), ipv4_address()]),
legacy_interface(vec![ipv6_address()]),
],
..Default::default()
};
Expand All @@ -708,6 +726,7 @@ mod tests {
svi_ip: Some("2001:db8::2/64".to_string()),
})
);
assert_eq!(response.tenant_interfaces[2], expected_ipv6_interface());
}

#[test]
Expand Down
Loading
Loading