Skip to content

Commit

Permalink
[nexus] move UuidRng code out to a common crate, make CollectionBuild…
Browse files Browse the repository at this point in the history
…er use it (#5341)

In #5270, we need determinism not just from blueprints but also
collections. So move the UuidRng into a common place.

As part of that, I also decided to make it its own crate and write some
documentation about it, making it more generic along the way. I think
this should be a pretty clean representation of what this is trying to do.
  • Loading branch information
sunshowers authored Mar 28, 2024
1 parent 4ca89ca commit 7484017
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 54 deletions.
14 changes: 13 additions & 1 deletion Cargo.lock

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ members = [
"test-utils",
"tufaceous-lib",
"tufaceous",
"typed-rng",
"update-common",
"update-engine",
"uuid-kinds",
Expand Down Expand Up @@ -149,6 +150,7 @@ default-members = [
"test-utils",
"tufaceous-lib",
"tufaceous",
"typed-rng",
"update-common",
"update-engine",
"uuid-kinds",
Expand Down Expand Up @@ -343,6 +345,7 @@ propolis-mock-server = { git = "https://github.com/oxidecomputer/propolis", rev
proptest = "1.4.0"
quote = "1.0"
rand = "0.8.5"
rand_core = "0.6.4"
rand_seeder = "0.2.3"
ratatui = "0.26.1"
rayon = "1.9"
Expand Down Expand Up @@ -434,6 +437,7 @@ trybuild = "1.0.89"
tufaceous = { path = "tufaceous" }
tufaceous-lib = { path = "tufaceous-lib" }
tui-tree-widget = "0.17.0"
typed-rng = { path = "typed-rng" }
unicode-width = "0.1.11"
update-common = { path = "update-common" }
update-engine = { path = "update-engine" }
Expand Down
1 change: 1 addition & 0 deletions nexus/inventory/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ sled-agent-client.workspace = true
slog.workspace = true
strum.workspace = true
thiserror.workspace = true
typed-rng.workspace = true
uuid.workspace = true
omicron-workspace-hack.workspace = true

Expand Down
19 changes: 18 additions & 1 deletion nexus/inventory/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ use nexus_types::inventory::SledAgent;
use nexus_types::inventory::Zpool;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::hash::Hash;
use std::sync::Arc;
use thiserror::Error;
use typed_rng::UuidRng;
use uuid::Uuid;

/// Describes an operational error encountered during the collection process
Expand Down Expand Up @@ -86,6 +88,8 @@ pub struct CollectionBuilder {
BTreeMap<RotPageWhich, BTreeMap<Arc<BaseboardId>, RotPageFound>>,
sleds: BTreeMap<Uuid, SledAgent>,
omicron_zones: BTreeMap<Uuid, OmicronZonesFound>,
// We just generate one UUID for each collection.
id_rng: UuidRng,
}

impl CollectionBuilder {
Expand All @@ -111,6 +115,7 @@ impl CollectionBuilder {
rot_pages_found: BTreeMap::new(),
sleds: BTreeMap::new(),
omicron_zones: BTreeMap::new(),
id_rng: UuidRng::from_entropy(),
}
}

Expand All @@ -123,7 +128,7 @@ impl CollectionBuilder {
}

Collection {
id: Uuid::new_v4(),
id: self.id_rng.next(),
errors: self.errors.into_iter().map(|e| e.to_string()).collect(),
time_started: self.time_started,
time_done: now_db_precision(),
Expand All @@ -140,6 +145,18 @@ impl CollectionBuilder {
}
}

/// Within tests, set a seeded RNG for deterministic results.
///
/// This will ensure that tests that use this builder will produce the same
/// results each time they are run.
pub fn set_rng_seed<H: Hash>(&mut self, seed: H) -> &mut Self {
// Important to add some more bytes here, so that builders with the
// same seed but different purposes don't end up with the same UUIDs.
const SEED_EXTRA: &str = "collection-builder";
self.id_rng.set_seed(seed, SEED_EXTRA);
self
}

/// Record service processor state `sp_state` reported by MGS
///
/// `sp_type` and `slot` identify which SP this was.
Expand Down
2 changes: 1 addition & 1 deletion nexus/reconfigurator/planning/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ nexus-inventory.workspace = true
nexus-types.workspace = true
omicron-common.workspace = true
rand.workspace = true
rand_seeder.workspace = true
sled-agent-client.workspace = true
slog.workspace = true
thiserror.workspace = true
typed-rng.workspace = true
uuid.workspace = true

omicron-workspace-hack.workspace = true
Expand Down
61 changes: 14 additions & 47 deletions nexus/reconfigurator/planning/src/blueprint_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ use omicron_common::api::external::Vni;
use omicron_common::api::internal::shared::NetworkInterface;
use omicron_common::api::internal::shared::NetworkInterfaceKind;
use rand::rngs::StdRng;
use rand::RngCore;
use rand::SeedableRng;
use slog::o;
use slog::Logger;
Expand All @@ -50,6 +49,7 @@ use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
use std::net::SocketAddrV6;
use thiserror::Error;
use typed_rng::UuidRng;
use uuid::Uuid;

/// Errors encountered while assembling blueprints
Expand Down Expand Up @@ -223,7 +223,7 @@ impl<'a> BlueprintBuilder<'a> {
})
.collect::<Result<_, Error>>()?;
Ok(Blueprint {
id: rng.blueprint_rng.next_uuid(),
id: rng.blueprint_rng.next(),
blueprint_zones,
parent_blueprint_id: None,
internal_dns_version,
Expand Down Expand Up @@ -375,7 +375,7 @@ impl<'a> BlueprintBuilder<'a> {
let blueprint_zones =
self.zones.into_zones_map(self.policy.sleds.keys().copied());
Blueprint {
id: self.rng.blueprint_rng.next_uuid(),
id: self.rng.blueprint_rng.next(),
blueprint_zones,
parent_blueprint_id: Some(self.parent_blueprint.id),
internal_dns_version: self.internal_dns_version,
Expand Down Expand Up @@ -452,7 +452,7 @@ impl<'a> BlueprintBuilder<'a> {
.collect();

let zone = OmicronZoneConfig {
id: self.rng.zone_rng.next_uuid(),
id: self.rng.zone_rng.next(),
underlay_address: ip,
zone_type: OmicronZoneType::InternalNtp {
address: ntp_address.to_string(),
Expand Down Expand Up @@ -502,7 +502,7 @@ impl<'a> BlueprintBuilder<'a> {
let port = omicron_common::address::CRUCIBLE_PORT;
let address = SocketAddrV6::new(ip, port, 0, 0).to_string();
let zone = OmicronZoneConfig {
id: self.rng.zone_rng.next_uuid(),
id: self.rng.zone_rng.next(),
underlay_address: ip,
zone_type: OmicronZoneType::Crucible {
address,
Expand Down Expand Up @@ -589,7 +589,7 @@ impl<'a> BlueprintBuilder<'a> {
};

for _ in 0..num_nexus_to_add {
let nexus_id = self.rng.zone_rng.next_uuid();
let nexus_id = self.rng.zone_rng.next();
let external_ip = self
.available_external_ips
.next()
Expand Down Expand Up @@ -617,7 +617,7 @@ impl<'a> BlueprintBuilder<'a> {
.next()
.ok_or(Error::NoSystemMacAddressAvailable)?;
NetworkInterface {
id: self.rng.network_interface_rng.next_uuid(),
id: self.rng.network_interface_rng.next(),
kind: NetworkInterfaceKind::Service { id: nexus_id },
name: format!("nexus-{nexus_id}").parse().unwrap(),
ip,
Expand Down Expand Up @@ -739,14 +739,14 @@ struct BlueprintBuilderRng {

impl BlueprintBuilderRng {
fn new() -> Self {
Self::new_from_rng(StdRng::from_entropy())
Self::new_from_parent(StdRng::from_entropy())
}

fn new_from_rng(mut root_rng: StdRng) -> Self {
let blueprint_rng = UuidRng::from_root_rng(&mut root_rng, "blueprint");
let zone_rng = UuidRng::from_root_rng(&mut root_rng, "zone");
fn new_from_parent(mut parent: StdRng) -> Self {
let blueprint_rng = UuidRng::from_parent_rng(&mut parent, "blueprint");
let zone_rng = UuidRng::from_parent_rng(&mut parent, "zone");
let network_interface_rng =
UuidRng::from_root_rng(&mut root_rng, "network_interface");
UuidRng::from_parent_rng(&mut parent, "network_interface");

BlueprintBuilderRng { blueprint_rng, zone_rng, network_interface_rng }
}
Expand All @@ -755,40 +755,7 @@ impl BlueprintBuilderRng {
// Important to add some more bytes here, so that builders with the
// same seed but different purposes don't end up with the same UUIDs.
const SEED_EXTRA: &str = "blueprint-builder";
let mut seeder = rand_seeder::Seeder::from((seed, SEED_EXTRA));
*self = Self::new_from_rng(seeder.make_rng::<StdRng>());
}
}

#[derive(Debug)]
pub(crate) struct UuidRng {
rng: StdRng,
}

impl UuidRng {
/// Returns a new `UuidRng` generated from the root RNG.
///
/// `extra` is a string that should be unique to the purpose of the UUIDs.
fn from_root_rng(root_rng: &mut StdRng, extra: &'static str) -> Self {
let seed = root_rng.next_u64();
let mut seeder = rand_seeder::Seeder::from((seed, extra));
Self { rng: seeder.make_rng::<StdRng>() }
}

/// `extra` is a string that should be unique to the purpose of the UUIDs.
pub(crate) fn from_seed<H: Hash>(seed: H, extra: &'static str) -> Self {
let mut seeder = rand_seeder::Seeder::from((seed, extra));
Self { rng: seeder.make_rng::<StdRng>() }
}

/// Returns a new UUIDv4 generated from the RNG.
pub(crate) fn next_uuid(&mut self) -> Uuid {
let mut bytes = [0; 16];
self.rng.fill_bytes(&mut bytes);
// Builder::from_random_bytes will turn the random bytes into a valid
// UUIDv4. (Parts of the system depend on the UUID actually being valid
// v4, so it's important that we don't just use `uuid::from_bytes`.)
uuid::Builder::from_random_bytes(bytes).into_uuid()
*self = Self::new_from_parent(typed_rng::from_seed(seed, SEED_EXTRA));
}
}

Expand Down Expand Up @@ -1013,7 +980,7 @@ pub mod test {
assert_eq!(diff.sleds_changed().count(), 0);

// The next step is adding these zones to a new sled.
let new_sled_id = example.sled_rng.next_uuid();
let new_sled_id = example.sled_rng.next();
let _ =
example.system.sled(SledBuilder::new().id(new_sled_id)).unwrap();
let policy = example.system.to_policy().unwrap();
Expand Down
6 changes: 3 additions & 3 deletions nexus/reconfigurator/planning/src/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
//! Example blueprints
use crate::blueprint_builder::BlueprintBuilder;
use crate::blueprint_builder::UuidRng;
use crate::system::SledBuilder;
use crate::system::SystemDescription;
use nexus_types::deployment::Blueprint;
Expand All @@ -14,6 +13,7 @@ use nexus_types::deployment::Policy;
use nexus_types::inventory::Collection;
use omicron_common::api::external::Generation;
use sled_agent_client::types::OmicronZonesConfig;
use typed_rng::UuidRng;

pub struct ExampleSystem {
pub system: SystemDescription,
Expand All @@ -38,8 +38,7 @@ impl ExampleSystem {
) -> ExampleSystem {
let mut system = SystemDescription::new();
let mut sled_rng = UuidRng::from_seed(test_name, "ExampleSystem");
let sled_ids: Vec<_> =
(0..nsleds).map(|_| sled_rng.next_uuid()).collect();
let sled_ids: Vec<_> = (0..nsleds).map(|_| sled_rng.next()).collect();
for sled_id in &sled_ids {
let _ = system.sled(SledBuilder::new().id(*sled_id)).unwrap();
}
Expand Down Expand Up @@ -107,6 +106,7 @@ impl ExampleSystem {
let blueprint = builder.build();
let mut builder =
system.to_collection_builder().expect("failed to build collection");
builder.set_rng_seed((test_name, "ExampleSystem collection"));

for sled_id in blueprint.sleds() {
let Some(zones) = blueprint.blueprint_zones.get(&sled_id) else {
Expand Down
2 changes: 1 addition & 1 deletion nexus/reconfigurator/planning/src/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ mod test {
verify_blueprint(&blueprint2);

// Now add a new sled.
let new_sled_id = example.sled_rng.next_uuid();
let new_sled_id = example.sled_rng.next();
let _ =
example.system.sled(SledBuilder::new().id(new_sled_id)).unwrap();
let policy = example.system.to_policy().unwrap();
Expand Down
11 changes: 11 additions & 0 deletions typed-rng/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "typed-rng"
version = "0.1.0"
edition = "2021"

[dependencies]
omicron-workspace-hack.workspace = true
rand.workspace = true
rand_core.workspace = true
rand_seeder.workspace = true
uuid.workspace = true
Loading

0 comments on commit 7484017

Please sign in to comment.