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
6 changes: 6 additions & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions end-to-end-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ bytes.workspace = true
chrono.workspace = true
http.workspace = true
futures.workspace = true
internal-dns-resolver.workspace = true
internal-dns-types.workspace = true
nexus-client.workspace = true
omicron-sled-agent.workspace = true
omicron-test-utils.workspace = true
oxide-client.workspace = true
Expand All @@ -25,6 +28,9 @@ russh-keys = "0.45.0"
serde.workspace = true
serde_json.workspace = true
sled-agent-types.workspace = true
slog.workspace = true
slog-error-chain.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
toml.workspace = true
hickory-resolver.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion end-to-end-tests/src/helpers/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl Context {
}
}

fn rss_config() -> Result<RackInitializeRequest> {
pub fn rss_config() -> Result<RackInitializeRequest> {
let path = "/opt/oxide/sled-agent/pkg/config-rss.toml";
let content =
std::fs::read_to_string(&path).unwrap_or(RSS_CONFIG_STR.to_string());
Expand Down
1 change: 1 addition & 0 deletions end-to-end-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod helpers;

mod instance_launch;
mod no_spoof;
mod noop_blueprint;
124 changes: 124 additions & 0 deletions end-to-end-tests/src/noop_blueprint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! Test that generating a new blueprint on a freshly-installed system will not
//! change anything.

#![cfg(test)]

use internal_dns_resolver::Resolver;
use internal_dns_types::names::ServiceName;
use nexus_client::Client as NexusClient;
use omicron_test_utils::dev::poll::{wait_for_condition, CondCheckError};
use omicron_test_utils::dev::test_setup_log;
use slog::{debug, info};
use slog_error_chain::InlineErrorChain;
use std::time::Duration;
use thiserror::Error;

/// Test that generating a new blueprint on a freshly-installed system will not
/// change anything.
///
/// If this test fails, there's probably a bug somewhere. Maybe the initial
/// system blueprint is incorrect or incomplete or maybe the planner is doing
/// the wrong thing after initial setup.
#[tokio::test]
async fn new_blueprint_noop() {
// In order to check anything with blueprints, we need to reach the Nexus
// internal API. This in turn requires finding the internal DNS servers.
let rss_config =
crate::helpers::ctx::rss_config().expect("loading RSS config");
let rack_subnet = &rss_config.rack_network_config.rack_subnet;
println!("rack subnet: {}", rack_subnet);
let logctx = test_setup_log("new_blueprint_noop");
let resolver =
Resolver::new_from_ip(logctx.log.clone(), rack_subnet.addr())
.expect("creating internal DNS resolver");

// Wait up to 5 minutes to get a working Nexus client.
let nexus_client = wait_for_condition(
|| async {
match make_nexus_client(&resolver, &logctx.log).await {
Ok(nexus_client) => Ok(nexus_client),
Err(e) => {
debug!(
&logctx.log,
"obtaining a working Nexus client failed";
InlineErrorChain::new(&e),
);
Err(CondCheckError::<()>::NotYet)
}
}
},
&Duration::from_millis(500),
&Duration::from_secs(300),
)
.await
.expect("timed out waiting to obtain a working Nexus client");
println!("Nexus is running and has a target blueprint");

// Now generate a new blueprint.
let new_blueprint = nexus_client
.blueprint_regenerate()
.await
.expect("failed to generate new blueprint")
.into_inner();
println!("new blueprint generated: {}", new_blueprint.id);
let parent_blueprint_id = new_blueprint
.parent_blueprint_id
.expect("generated blueprint always has a parent");
println!("parent blueprint id: {}", parent_blueprint_id);

// Fetch its parent.
let parent_blueprint = nexus_client
.blueprint_view(&parent_blueprint_id)
.await
.expect("failed to fetch parent blueprint")
.into_inner();

let diff = new_blueprint.diff_since_blueprint(&parent_blueprint);
println!("new blueprint: {}", new_blueprint.id);
println!("differences:");
println!("{}", diff.display());

if diff.has_changes() {
panic!(
"unexpected changes between initial blueprint and \
newly-generated one (see above)"
);
}

logctx.cleanup_successful();
}

/// Error returned by [`make_nexus_client()`].
#[derive(Debug, Error)]
enum MakeNexusError {
#[error("looking up Nexus IP in internal DNS")]
Resolve(#[from] internal_dns_resolver::ResolveError),
#[error("making request to Nexus")]
Request(#[from] nexus_client::Error<nexus_client::types::Error>),
}

/// Make one attempt to look up the IP of Nexus in internal DNS and make an HTTP
/// request to its internal API to fetch its current target blueprint.
///
/// If this succeeds, Nexus is ready for the rest of this test to proceed.
///
/// Returns a client for this Nexus.
async fn make_nexus_client(
resolver: &Resolver,
log: &slog::Logger,
) -> Result<NexusClient, MakeNexusError> {
debug!(log, "doing DNS lookup for Nexus");
let nexus_ip = resolver.lookup_socket_v6(ServiceName::Nexus).await?;
let url = format!("http://{}", nexus_ip);
debug!(log, "found Nexus IP"; "nexus_ip" => %nexus_ip, "url" => &url);

let client = NexusClient::new(&url, log.clone());

// Once this call succeeds, Nexus is ready for us to proceed.
let blueprint_response = client.blueprint_target_view().await?.into_inner();
info!(log, "found target blueprint (Nexus is ready)";
"target_blueprint" => ?blueprint_response
);

Ok(client)
}
36 changes: 36 additions & 0 deletions nexus/types/src/deployment/blueprint_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use super::{
zone_sort_key, Blueprint, ClickhouseClusterConfig,
CockroachDbPreserveDowngrade, DiffBeforeClickhouseClusterConfig,
};
use diffus::Diffable;
use nexus_sled_agent_shared::inventory::ZoneKind;
use omicron_common::api::external::Generation;
use omicron_common::disk::DiskIdentity;
Expand Down Expand Up @@ -942,6 +943,41 @@ impl BlueprintDiff {
pub fn display(&self) -> BlueprintDiffDisplay<'_> {
BlueprintDiffDisplay::new(self)
}

/// Returns whether the diff reflects any changes or if the blueprints are
/// equivalent.
pub fn has_changes(&self) -> bool {
// Any changes to physical disks, datasets, or zones would be reflected
// in `self.sleds_modified`, `self.sleds_added`, or
// `self.sleds_removed`.
if !self.sleds_modified.is_empty()
|| !self.sleds_added.is_empty()
|| !self.sleds_removed.is_empty()
{
return true;
}

// The clickhouse cluster config has changed if:
// - there was one before and now there isn't
// - there wasn't one before and now there is
// - there's one both before and after and their generation has changed
match (
&self.before_clickhouse_cluster_config,
&self.after_clickhouse_cluster_config,
) {
(DiffBeforeClickhouseClusterConfig::Blueprint(None), None) => false,
(DiffBeforeClickhouseClusterConfig::Blueprint(None), Some(_)) => {
true
}
(DiffBeforeClickhouseClusterConfig::Blueprint(Some(_)), None) => {
true
}
(
DiffBeforeClickhouseClusterConfig::Blueprint(Some(before)),
Some(after),
) => before.diff(&after).is_change(),
}
}
}

/// A printable representation of `ClickhouseClusterConfig` diff tables where
Expand Down
Loading