Skip to content

Commit 939a865

Browse files
authored
feat: Set NTP servers from site config (#2610)
## Description - Add site-level `ntp_servers` config. - Call redfish to `set_ntp_servers` on the BMC during preingestion, see [libredfish PR87](NVIDIA/libredfish#87). - Plumb site-level `ntp_servers` config into the `DhcpRecord` response of `DiscoverDhcp` RPC call. - Update Kea DHCP config to prefer API-provided `DhcpRecord.ntp_servers` for Option 42, while keeping the old workflow (read ntp_servers from kea dhcp config) for fallback. - Add site level `ntp_servers` into `ManagedHostNetworkConfigResponse` and have DPU agent use it for DHCP server. ## Type of Change <!-- Check one that best describes this PR --> - [x] **Add** - New feature or capability - [ ] **Change** - Changes in existing functionality - [ ] **Fix** - Bug fixes - [ ] **Remove** - Removed features or deprecated functionality - [ ] **Internal** - Internal changes (refactoring, tests, docs, etc.) ## Related Issues (Optional) #548 (comment) ## Breaking Changes - [ ] This PR contains breaking changes <!-- If checked above, describe the breaking changes and migration steps --> ## Testing <!-- How was this tested? Check all that apply --> - [x] Unit tests added/updated - [ ] Integration tests added/updated - [x] Manual testing performed - [ ] No testing required (docs, internal refactor, etc.) ## Additional Notes <!-- Any additional context, deployment notes, or reviewer guidance --> --------- Signed-off-by: Felicity Xu <hanyux@nvidia.com>
1 parent f310e89 commit 939a865

25 files changed

Lines changed: 692 additions & 33 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ authors = ["NVIDIA Carbide Engineering <carbide-dev@exchange.nvidia.com>"]
2626

2727
[workspace.dependencies]
2828
clap = { version = "4", features = ["derive", "env"] }
29-
libredfish = { git = "https://github.com/NVIDIA/libredfish.git", tag = "v0.44.10" }
29+
libredfish = { git = "https://github.com/NVIDIA/libredfish.git", tag = "v0.44.11" }
3030
librms = { git = "https://github.com/NVIDIA/nv-rms-client.git", tag = "v0.9.0-rc1" }
3131
ansi-to-html = "0.2.2"
3232

crates/agent/src/ethernet_virtualization.rs

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,52 @@ pub struct ServiceAddresses {
138138
pub nameservers: Vec<IpAddr>,
139139
}
140140

141+
fn build_dhcp_ntp_servers(
142+
nc: &rpc::ManagedHostNetworkConfigResponse,
143+
service_addrs: &ServiceAddresses,
144+
) -> Vec<Ipv4Addr> {
145+
// Start with the NTP servers from the service addresses, which is read from carbide-ntp.forge.
146+
let mut ntp_servers = service_addrs
147+
.ntpservers
148+
.iter()
149+
.filter_map(|x| match x {
150+
IpAddr::V4(x) => Some(*x),
151+
_ => None,
152+
})
153+
.collect::<Vec<Ipv4Addr>>();
154+
155+
// If the site has configured NTP servers, use them instead.
156+
if !nc.ntp_servers.is_empty() {
157+
let site_ntp_servers: Vec<Ipv4Addr> = nc.ntp_servers
158+
.iter()
159+
.filter_map(|s| match IpAddr::from_str(s) {
160+
Ok(IpAddr::V4(ip)) => Some(ip),
161+
Ok(IpAddr::V6(_)) => {
162+
tracing::debug!(
163+
ntp_server = %s,
164+
"IPv6 NTP server from ManagedHostNetworkConfigResponse is ignored for DHCPv4 config"
165+
);
166+
None
167+
}
168+
Err(e) => {
169+
tracing::debug!(
170+
ntp_server = %s,
171+
error = %e,
172+
"Invalid NTP server IP from ManagedHostNetworkConfigResponse, ignoring"
173+
);
174+
None
175+
}
176+
})
177+
.collect();
178+
179+
if !site_ntp_servers.is_empty() {
180+
ntp_servers = site_ntp_servers;
181+
}
182+
}
183+
184+
ntp_servers
185+
}
186+
141187
/// How we tell HBN to notice the new file we wrote
142188
#[derive(Debug)]
143189
struct PostAction {
@@ -1029,14 +1075,7 @@ async fn update_dhcp_via_grpc(
10291075
})
10301076
.collect::<Vec<Ipv4Addr>>();
10311077

1032-
let ntpservers_v4 = service_addrs
1033-
.ntpservers
1034-
.iter()
1035-
.filter_map(|x| match x {
1036-
IpAddr::V4(x) => Some(*x),
1037-
_ => None,
1038-
})
1039-
.collect::<Vec<Ipv4Addr>>();
1078+
let ntpservers_v4 = build_dhcp_ntp_servers(network_config, service_addrs);
10401079

10411080
let pxe_ip_v4 = service_addrs
10421081
.pxe_ips
@@ -1445,14 +1484,7 @@ fn write_dhcp_v4_server_config(
14451484
})
14461485
.collect::<Vec<Ipv4Addr>>();
14471486

1448-
let ntpservers_v4 = service_addrs
1449-
.ntpservers
1450-
.iter()
1451-
.filter_map(|x| match x {
1452-
IpAddr::V4(x) => Some(*x),
1453-
_ => None,
1454-
})
1455-
.collect::<Vec<Ipv4Addr>>();
1487+
let ntpservers_v4 = build_dhcp_ntp_servers(nc, service_addrs);
14561488

14571489
let pxe_ip_v4 = service_addrs
14581490
.pxe_ips
@@ -1911,6 +1943,54 @@ mod tests {
19111943
carbide_host_support::init_logging("nico-dpu-agent").unwrap();
19121944
}
19131945

1946+
#[test]
1947+
fn test_build_dhcp_ntp_servers() {
1948+
let service_addrs = ServiceAddresses {
1949+
pxe_ips: vec![],
1950+
ntpservers: vec![IpAddr::from([192, 0, 2, 20])],
1951+
nameservers: vec![],
1952+
};
1953+
let nc = rpc::ManagedHostNetworkConfigResponse {
1954+
ntp_servers: vec!["198.51.100.1".to_string(), "198.51.100.2".to_string()],
1955+
..Default::default()
1956+
};
1957+
1958+
let out = build_dhcp_ntp_servers(&nc, &service_addrs);
1959+
assert_eq!(
1960+
out,
1961+
vec![
1962+
Ipv4Addr::from([198, 51, 100, 1]),
1963+
Ipv4Addr::from([198, 51, 100, 2])
1964+
]
1965+
);
1966+
}
1967+
1968+
#[test]
1969+
fn test_build_dhcp_ntp_servers_fallback() {
1970+
let service_addrs = ServiceAddresses {
1971+
pxe_ips: vec![],
1972+
ntpservers: vec![IpAddr::from([192, 0, 2, 20])],
1973+
nameservers: vec![],
1974+
};
1975+
1976+
let empty_nc = rpc::ManagedHostNetworkConfigResponse::default();
1977+
1978+
assert_eq!(
1979+
build_dhcp_ntp_servers(&empty_nc, &service_addrs),
1980+
vec![Ipv4Addr::from([192, 0, 2, 20])]
1981+
);
1982+
1983+
let invalid_nc = rpc::ManagedHostNetworkConfigResponse {
1984+
ntp_servers: vec!["not-an-ip".to_string(), "2001:db8::1".to_string()],
1985+
..Default::default()
1986+
};
1987+
1988+
assert_eq!(
1989+
build_dhcp_ntp_servers(&invalid_nc, &service_addrs),
1990+
vec![Ipv4Addr::from([192, 0, 2, 20])]
1991+
);
1992+
}
1993+
19141994
#[test]
19151995
fn test_hostname() -> Result<(), Box<dyn std::error::Error>> {
19161996
let syscall_h = super::hostname()?;
@@ -2935,6 +3015,7 @@ mod tests {
29353015

29363016
// yes it's in there twice I dunno either
29373017
dhcp_servers: vec!["10.217.5.197".to_string(), "10.217.5.197".to_string()],
3018+
ntp_servers: vec![],
29383019
vni_device: "vxlan48".to_string(),
29393020

29403021
managed_host_config: Some(netconf),
@@ -3421,6 +3502,7 @@ mod tests {
34213502

34223503
// yes it's in there twice I dunno either
34233504
dhcp_servers: vec!["10.217.5.197".to_string(), "10.217.5.197".to_string()],
3505+
ntp_servers: vec![],
34243506
vni_device: "vxlan48".to_string(),
34253507

34263508
managed_host_config: Some(netconf),
@@ -3616,6 +3698,7 @@ mod tests {
36163698
routing_profile: None,
36173699
traffic_intercept_config: None,
36183700
dhcp_servers: vec![],
3701+
ntp_servers: vec![],
36193702
vni_device: "vxlan48".to_string(),
36203703
managed_host_config: Some(netconf),
36213704
managed_host_config_version: "V1-T1".to_string(),

crates/agent/src/tests/full.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,7 @@ async fn handle_netconf(AxumState(state): AxumState<Arc<Mutex<State>>>) -> impl
885885
}),
886886

887887
dhcp_servers: vec!["127.0.0.1".to_string()],
888+
ntp_servers: vec![],
888889
vni_device: "".to_string(),
889890

890891
managed_host_config: Some(rpc::forge::ManagedHostNetworkConfig {

crates/api-core/src/cfg/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ applicable.
2020
| `ib_config` | `Option<IBFabricConfig>` || InfiniBand fabric configuration (see [IBFabricConfig](#ibfabricconfig)). |
2121
| `asn` | `u32` | **required** | Autonomous System Number, fixed per environment. Used by nico-dpu-agent for `frr.conf` BGP routing. |
2222
| `dhcp_servers` | `Vec<Ipv4Addr>` | `[]` | DHCP server addresses announced to DPUs during network provisioning. |
23+
| `ntp_servers` | `Vec<Ipv4Addr>` | `[]` | Site-level NTP server IPs used for BMC time configuration and DHCP NTP Server configuration. |
2324
| `route_servers` | `Vec<String>` | `[]` | Route server IPs for L2VPN Ethernet Virtual network support. |
2425
| `enable_route_servers` | `bool` | `false` | Enables route server injection into DPU FRR configs for L2VPN. |
2526
| `deny_prefixes` | `Vec<Ipv4Network>` | `[]` | IPv4 CIDR prefixes that tenant instances are blocked from reaching. Generates iptables DROP rules and nvue ACL policies. |

crates/api-core/src/cfg/file.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ pub struct CarbideConfig {
128128
#[serde(default)]
129129
pub dhcp_servers: Vec<Ipv4Addr>,
130130

131+
/// NTP server IP addresses for the site.
132+
#[serde(default)]
133+
pub ntp_servers: Vec<Ipv4Addr>,
134+
131135
/// Route server IP addresses for L2VPN (Ethernet
132136
/// Virtual) network support on DPUs.
133137
#[serde(default)]
@@ -2863,6 +2867,10 @@ mod tests {
28632867
config.dhcp_servers,
28642868
vec![Ipv4Addr::new(1, 2, 3, 4), Ipv4Addr::new(5, 6, 7, 8)]
28652869
);
2870+
assert_eq!(
2871+
config.ntp_servers,
2872+
vec![Ipv4Addr::new(10, 20, 30, 40), Ipv4Addr::new(50, 60, 70, 80)]
2873+
);
28662874
assert_eq!(config.vpc_peering_policy, Some(VpcPeeringPolicy::Exclusive));
28672875
assert_eq!(
28682876
config.vpc_peering_policy_on_existing,

crates/api-core/src/cfg/test_data/full_config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ database_url = "postgres://a:b@postgresql"
44
max_database_connections = 1222
55
asn = 123
66
dhcp_servers = ["1.2.3.4", "5.6.7.8"]
7+
ntp_servers = ["10.20.30.40", "50.60.70.80"]
78
route_servers = ["9.10.11.12"]
89
rapid_iterations = false
910
initial_domain_name = "forge.local"

crates/api-core/src/dhcp/discover.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ async fn handle_overlay_from_dpa(
5252
dpa_if: &mut DpaInterface,
5353
macaddr: MacAddress,
5454
desired_addr: IpAddr,
55+
ntp_servers: &[Ipv4Addr],
5556
) -> Result<Option<Response<rpc::DhcpRecord>>, CarbideError> {
5657
let IpAddr::V4(ip_v4_addr) = desired_addr else {
5758
return Err(CarbideError::internal(
@@ -80,6 +81,7 @@ async fn handle_overlay_from_dpa(
8081
mtu: SPX_MTU,
8182
fqdn: String::new(),
8283
prefix,
84+
ntp_servers: ntp_servers.iter().map(ToString::to_string).collect(),
8385
})))
8486
}
8587

@@ -90,6 +92,7 @@ async fn handle_underlay_from_dpa(
9092
dpa_if: &mut DpaInterface,
9193
macaddr: MacAddress,
9294
relay_address: String,
95+
ntp_servers: &[Ipv4Addr],
9396
) -> Result<Option<Response<rpc::DhcpRecord>>, CarbideError> {
9497
// The relay address and the mac address should differ only in bit 0
9598
let relay_addr = Ipv4Addr::from_str(&relay_address)?;
@@ -119,6 +122,7 @@ async fn handle_underlay_from_dpa(
119122
mtu: SPX_MTU,
120123
fqdn: String::new(),
121124
prefix,
125+
ntp_servers: ntp_servers.iter().map(ToString::to_string).collect(),
122126
})))
123127
}
124128

@@ -156,10 +160,24 @@ async fn handle_dhcp_from_dpa(
156160
let mut dpa_if = dpa_ifs.remove(0);
157161

158162
if let Some(addr) = desired_address {
159-
return handle_overlay_from_dpa(txn, &mut dpa_if, macaddr, addr).await;
163+
return handle_overlay_from_dpa(
164+
txn,
165+
&mut dpa_if,
166+
macaddr,
167+
addr,
168+
&api.runtime_config.ntp_servers,
169+
)
170+
.await;
160171
}
161172

162-
handle_underlay_from_dpa(txn, &mut dpa_if, macaddr, relay_address).await
173+
handle_underlay_from_dpa(
174+
txn,
175+
&mut dpa_if,
176+
macaddr,
177+
relay_address,
178+
&api.runtime_config.ntp_servers,
179+
)
180+
.await
163181
}
164182

165183
pub async fn discover_dhcp(
@@ -437,7 +455,7 @@ pub async fn discover_dhcp(
437455

438456
let mut txn = api.txn_begin().await?;
439457

440-
let record: rpc::DhcpRecord = db::dhcp_record::find_by_mac_address(
458+
let mut record: rpc::DhcpRecord = db::dhcp_record::find_by_mac_address(
441459
&mut txn,
442460
&parsed_mac,
443461
&machine_interface.segment_id,
@@ -447,5 +465,13 @@ pub async fn discover_dhcp(
447465
.into();
448466

449467
txn.commit().await?;
468+
469+
record.ntp_servers = api
470+
.runtime_config
471+
.ntp_servers
472+
.iter()
473+
.map(ToString::to_string)
474+
.collect();
475+
450476
Ok(Response::new(record))
451477
}

crates/api-core/src/handlers/dpu.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,12 @@ pub(crate) async fn get_managed_host_network_config_inner(
595595
.map(|addr| addr.to_string())
596596
.collect(),
597597
route_servers,
598+
ntp_servers: api
599+
.runtime_config
600+
.ntp_servers
601+
.iter()
602+
.map(|addr| addr.to_string())
603+
.collect(),
598604
// TODO: Automatically add the prefix(es?) from the IPv4 loopback
599605
// pool to deny_prefixes. The database stores the pool in an
600606
// exploded representation, so we either need to reconstruct the

crates/api-core/src/setup.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,6 +1463,7 @@ async fn initialize_and_start_controllers<'a>(
14631463
Some(upload_limiter),
14641464
Some(api_service.credential_manager.clone()),
14651465
work_lock_manager_handle.clone(),
1466+
carbide_config.ntp_servers.clone(),
14661467
)
14671468
.start(join_set, cancel_token.clone())?;
14681469

0 commit comments

Comments
 (0)