Skip to content

Commit 53b7cb0

Browse files
committed
WIP: Blueprint planner test
1 parent dfd9bba commit 53b7cb0

File tree

4 files changed

+231
-74
lines changed

4 files changed

+231
-74
lines changed

nexus/src/app/background/tasks/blueprint_planner.rs

Lines changed: 154 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl BlueprintPlanner {
6969
return status;
7070
};
7171
let (target, parent) = &*loaded;
72-
status.blueprint_id = parent.id;
72+
status.parent_id = parent.id;
7373

7474
// Get the inventory most recently seen by the collection
7575
// background task. The value is `Copy`, so with the deref
@@ -105,7 +105,7 @@ impl BlueprintPlanner {
105105
}
106106
};
107107

108-
// Assemble the planning context.
108+
// Assemble and adjust the planning context.
109109
let input =
110110
match PlanningInputFromDb::assemble(opctx, &self.datastore).await {
111111
Ok(input) => input,
@@ -155,72 +155,71 @@ impl BlueprintPlanner {
155155

156156
// Compare the new blueprint to its parent.
157157
let summary = blueprint.diff_since_blueprint(&parent);
158-
if summary.has_changes() {
158+
if !summary.has_changes() {
159+
// Blueprint is unchanged, do nothing.
159160
info!(
160161
&opctx.log,
161-
"planning produced new blueprint";
162+
"blueprint unchanged from current target";
162163
"parent_blueprint_id" => %parent.id,
163-
"blueprint_id" => %blueprint.id,
164164
);
165-
status.blueprint_id = blueprint.id;
165+
return status;
166+
}
166167

167-
// Save it.
168-
match self.datastore.blueprint_insert(opctx, &blueprint).await {
169-
Ok(()) => (),
170-
Err(error) => {
171-
error!(
172-
&opctx.log,
173-
"can't save blueprint";
174-
"error" => %error,
175-
"blueprint_id" => %blueprint.id,
176-
);
177-
status.error = Some(format!(
178-
"can't save blueprint {}: {}",
179-
blueprint.id, error
180-
));
181-
return status;
182-
}
183-
}
168+
// We have a fresh blueprint.
169+
info!(
170+
&opctx.log,
171+
"planning produced new blueprint";
172+
"parent_blueprint_id" => %parent.id,
173+
"blueprint_id" => %blueprint.id,
174+
);
175+
status.blueprint_id = Some(blueprint.id);
184176

185-
// Make it the current target.
186-
let target = BlueprintTarget {
187-
target_id: blueprint.id,
188-
enabled: target.enabled,
189-
time_made_target: Utc::now(),
190-
};
191-
match self
192-
.datastore
193-
.blueprint_target_set_current(opctx, target)
194-
.await
195-
{
196-
Ok(()) => (),
197-
Err(error) => {
198-
warn!(
199-
&opctx.log,
200-
"can't make blueprint the current target";
201-
"error" => %error,
202-
"blueprint_id" => %blueprint.id
203-
);
204-
status.error = Some(format!(
205-
"can't make blueprint {} the current target: {}",
206-
blueprint.id, error,
207-
));
208-
return status;
209-
}
177+
// Save it.
178+
match self.datastore.blueprint_insert(opctx, &blueprint).await {
179+
Ok(()) => (),
180+
Err(error) => {
181+
error!(
182+
&opctx.log,
183+
"can't save blueprint";
184+
"error" => %error,
185+
"blueprint_id" => %blueprint.id,
186+
);
187+
status.error = Some(format!(
188+
"can't save blueprint {}: {}",
189+
blueprint.id, error
190+
));
191+
return status;
210192
}
193+
}
211194

212-
// Notify watchers that we have a new target.
213-
self.tx_blueprint.send_replace(Some(Arc::new((target, blueprint))));
214-
} else {
215-
// Blueprint is unchanged, do nothing.
216-
info!(
217-
&opctx.log,
218-
"blueprint unchanged from current target";
219-
"parent_blueprint_id" => %parent.id,
220-
);
221-
status.unchanged = true;
195+
// Try to make it the current target.
196+
let target = BlueprintTarget {
197+
target_id: blueprint.id,
198+
enabled: target.enabled,
199+
time_made_target: Utc::now(),
200+
};
201+
match self.datastore.blueprint_target_set_current(opctx, target).await {
202+
Ok(()) => {
203+
status.new_target = true;
204+
}
205+
Err(error) => {
206+
warn!(
207+
&opctx.log,
208+
"can't make blueprint the current target";
209+
"error" => %error,
210+
"blueprint_id" => %blueprint.id
211+
);
212+
status.error = Some(format!(
213+
"can't make blueprint {} the current target: {}",
214+
blueprint.id, error,
215+
));
216+
return status;
217+
}
222218
}
223219

220+
// Notify watchers that we have a new target.
221+
self.tx_blueprint.send_replace(Some(Arc::new((target, blueprint))));
222+
224223
status
225224
}
226225
}
@@ -233,3 +232,100 @@ impl BackgroundTask for BlueprintPlanner {
233232
Box::pin(async move { json!(self.plan(opctx).await) })
234233
}
235234
}
235+
236+
#[cfg(test)]
237+
mod test {
238+
use super::*;
239+
use crate::app::background::tasks::blueprint_load::TargetBlueprintLoader;
240+
use crate::app::background::tasks::inventory_collection::InventoryCollector;
241+
use nexus_test_utils_macros::nexus_test;
242+
243+
type ControlPlaneTestContext =
244+
nexus_test_utils::ControlPlaneTestContext<crate::Server>;
245+
246+
#[nexus_test(server = crate::Server)]
247+
async fn test_blueprint_planner(cptestctx: &ControlPlaneTestContext) {
248+
// Set up the test context.
249+
let nexus = &cptestctx.server.server_context().nexus;
250+
let datastore = nexus.datastore();
251+
let opctx = OpContext::for_tests(
252+
cptestctx.logctx.log.clone(),
253+
datastore.clone(),
254+
);
255+
256+
// Spin up the blueprint loader background task.
257+
let mut loader = TargetBlueprintLoader::new(datastore.clone());
258+
let mut rx_loader = loader.watcher();
259+
loader.activate(&opctx).await;
260+
let (_initial_target, initial_blueprint) = &*rx_loader
261+
.borrow_and_update()
262+
.clone()
263+
.expect("no initial blueprint");
264+
265+
// Spin up the inventory collector background task.
266+
let resolver = internal_dns_resolver::Resolver::new_from_addrs(
267+
cptestctx.logctx.log.clone(),
268+
&[cptestctx.internal_dns.dns_server.local_address()],
269+
)
270+
.unwrap();
271+
let mut collector = InventoryCollector::new(
272+
datastore.clone(),
273+
resolver.clone(),
274+
"test_planner",
275+
1,
276+
false,
277+
);
278+
let rx_collector = collector.watcher();
279+
collector.activate(&opctx).await;
280+
281+
// Finally, spin up the planner background task.
282+
let mut planner = BlueprintPlanner::new(
283+
datastore.clone(),
284+
false,
285+
rx_collector,
286+
rx_loader.clone(),
287+
);
288+
let _rx_planner = planner.watcher();
289+
290+
// On activation, the planner should run successfully and generate
291+
// a new target blueprint.
292+
let status = serde_json::from_value::<BlueprintPlannerStatus>(
293+
planner.activate(&opctx).await,
294+
)
295+
.unwrap();
296+
assert!(!status.disabled);
297+
assert!(status.error.is_none());
298+
assert_eq!(status.parent_id, initial_blueprint.id);
299+
let blueprint_id = status.blueprint_id.unwrap();
300+
assert_ne!(blueprint_id, initial_blueprint.id);
301+
assert!(status.new_target);
302+
303+
// Load and check the new target blueprint.
304+
loader.activate(&opctx).await;
305+
let (target, blueprint) = &*rx_loader
306+
.borrow_and_update()
307+
.clone()
308+
.expect("failed to load blueprint");
309+
assert_eq!(target.target_id, blueprint.id);
310+
assert_eq!(target.target_id, blueprint_id);
311+
assert!(
312+
blueprint.diff_since_blueprint(initial_blueprint).has_changes()
313+
);
314+
315+
// Planning again should not change the plan.
316+
let status = serde_json::from_value::<BlueprintPlannerStatus>(
317+
planner.activate(&opctx).await,
318+
)
319+
.unwrap();
320+
assert_eq!(
321+
status,
322+
BlueprintPlannerStatus {
323+
disabled: false,
324+
error: None,
325+
parent_id: blueprint_id,
326+
blueprint_id: None,
327+
new_target: false,
328+
}
329+
);
330+
}
331+
}

nexus/src/lib.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -269,22 +269,18 @@ impl nexus_test_interface::NexusServer for Server {
269269
let opctx =
270270
internal_server.apictx.context.nexus.opctx_for_internal_api();
271271

272-
// Allocation of the initial Nexus's external IP is a little funny. In
272+
// Allocation of initial external IP addresses is a little funny. In
273273
// a real system, it'd be allocated by RSS and provided with the rack
274274
// initialization request (which we're about to simulate). RSS also
275275
// provides information about the external IP pool ranges available for
276-
// system services. The Nexus external IP that it picks comes from this
277-
// range. During rack initialization, Nexus "allocates" the IP (which
278-
// was really already allocated) -- recording that allocation like any
279-
// other one.
280-
//
281-
// In this context, the IP was "allocated" by the user. Most likely,
282-
// it's 127.0.0.1, having come straight from the stock testing config
283-
// file. Whatever it is, we fake up an IP pool range for use by system
284-
// services that includes solely this IP.
276+
// system services. But here, we fake up IP pool ranges based on the
277+
// external addresses of services that we start or mock.
285278
let internal_services_ip_pool_ranges = blueprint
286279
.all_omicron_zones(BlueprintZoneDisposition::is_in_service)
287280
.filter_map(|(_, zc)| match &zc.zone_type {
281+
BlueprintZoneType::BoundaryNtp(
282+
blueprint_zone_type::BoundaryNtp { external_ip, .. },
283+
) => Some(IpRange::from(external_ip.snat_cfg.ip)),
288284
BlueprintZoneType::ExternalDns(
289285
blueprint_zone_type::ExternalDns { dns_address, .. },
290286
) => Some(IpRange::from(dns_address.addr.ip())),

nexus/test-utils/src/lib.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ use nexus_types::deployment::BlueprintZoneType;
5050
use nexus_types::deployment::CockroachDbPreserveDowngrade;
5151
use nexus_types::deployment::OmicronZoneExternalFloatingAddr;
5252
use nexus_types::deployment::OmicronZoneExternalFloatingIp;
53+
use nexus_types::deployment::OmicronZoneExternalSnatIp;
5354
use nexus_types::deployment::OximeterReadMode;
5455
use nexus_types::deployment::blueprint_zone_type;
5556
use nexus_types::external_api::views::SledState;
5657
use nexus_types::internal_api::params::DnsConfigParams;
5758
use omicron_common::address::DNS_OPTE_IPV4_SUBNET;
5859
use omicron_common::address::NEXUS_OPTE_IPV4_SUBNET;
60+
use omicron_common::address::NTP_OPTE_IPV4_SUBNET;
5961
use omicron_common::api::external::Generation;
6062
use omicron_common::api::external::MacAddr;
6163
use omicron_common::api::external::UserId;
@@ -67,6 +69,7 @@ use omicron_common::api::internal::nexus::ProducerKind;
6769
use omicron_common::api::internal::shared::DatasetKind;
6870
use omicron_common::api::internal::shared::NetworkInterface;
6971
use omicron_common::api::internal::shared::NetworkInterfaceKind;
72+
use omicron_common::api::internal::shared::SourceNatConfig;
7073
use omicron_common::api::internal::shared::SwitchLocation;
7174
use omicron_common::disk::CompressionAlgorithm;
7275
use omicron_common::zpool_name::ZpoolName;
@@ -91,7 +94,7 @@ use slog::{Logger, debug, error, o};
9194
use std::collections::BTreeMap;
9295
use std::collections::HashMap;
9396
use std::fmt::Debug;
94-
use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6};
97+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
9598
use std::sync::Arc;
9699
use std::time::Duration;
97100
use uuid::Uuid;
@@ -1199,6 +1202,57 @@ impl<'a, N: NexusServer> ControlPlaneTestContextBuilder<'a, N> {
11991202
})
12001203
}
12011204

1205+
/// Configure a mock boundary-NTP server on the first sled agent
1206+
pub async fn configure_boundary_ntp(&mut self) {
1207+
let mac = self
1208+
.rack_init_builder
1209+
.mac_addrs
1210+
.next()
1211+
.expect("ran out of MAC addresses");
1212+
let internal_ip = NTP_OPTE_IPV4_SUBNET
1213+
.nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 1)
1214+
.unwrap();
1215+
let external_ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
1216+
let zone_id = OmicronZoneUuid::new_v4();
1217+
let zpool_id = ZpoolUuid::new_v4();
1218+
1219+
self.blueprint_zones.push(BlueprintZoneConfig {
1220+
disposition: BlueprintZoneDisposition::InService,
1221+
id: zone_id,
1222+
filesystem_pool: ZpoolName::new_external(zpool_id),
1223+
zone_type: BlueprintZoneType::BoundaryNtp(
1224+
blueprint_zone_type::BoundaryNtp {
1225+
address: "[::1]:80".parse().unwrap(),
1226+
ntp_servers: vec![],
1227+
dns_servers: vec![],
1228+
domain: None,
1229+
nic: NetworkInterface {
1230+
id: Uuid::new_v4(),
1231+
kind: NetworkInterfaceKind::Service {
1232+
id: zone_id.into_untyped_uuid(),
1233+
},
1234+
ip: internal_ip.into(),
1235+
mac,
1236+
name: format!("boundary-ntp-{zone_id}")
1237+
.parse()
1238+
.unwrap(),
1239+
primary: true,
1240+
slot: 0,
1241+
subnet: (*NTP_OPTE_IPV4_SUBNET).into(),
1242+
vni: Vni::SERVICES_VNI,
1243+
transit_ips: vec![],
1244+
},
1245+
external_ip: OmicronZoneExternalSnatIp {
1246+
id: ExternalIpUuid::new_v4(),
1247+
snat_cfg: SourceNatConfig::new(external_ip, 0, 16383)
1248+
.unwrap(),
1249+
},
1250+
},
1251+
),
1252+
image_source: BlueprintZoneImageSource::InstallDataset,
1253+
});
1254+
}
1255+
12021256
/// Set up the Crucible Pantry on the first sled agent
12031257
pub async fn start_crucible_pantry(&mut self) {
12041258
let pantry = self.sled_agents[0].start_pantry().await;
@@ -1685,6 +1739,12 @@ async fn setup_with_config_impl<N: NexusServer>(
16851739
builder
16861740
.init_with_steps(
16871741
vec![
1742+
(
1743+
"configure_boundary_ntp",
1744+
Box::new(|builder| {
1745+
builder.configure_boundary_ntp().boxed()
1746+
}),
1747+
),
16881748
(
16891749
"start_crucible_pantry",
16901750
Box::new(|builder| builder.start_crucible_pantry().boxed()),

0 commit comments

Comments
 (0)