Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewjstone committed Dec 27, 2023
1 parent e709afb commit 279e9ac
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 4 deletions.
169 changes: 167 additions & 2 deletions nexus/src/app/background/plan_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use nexus_types::blueprint::{Blueprint, OmicronZonesConfig};
use serde_json::json;
use slog::Logger;
use std::collections::BTreeMap;
use std::net::SocketAddrV6;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::watch;
use uuid::Uuid;
Expand All @@ -27,6 +27,8 @@ pub struct PlanExecutor {
}

impl PlanExecutor {
// Temporary until we wire up the background task
#[allow(unused)]
pub fn new(
rx_blueprint: watch::Receiver<Option<Arc<Blueprint>>>,
) -> PlanExecutor {
Expand Down Expand Up @@ -87,7 +89,7 @@ async fn realize_blueprint(

async fn deploy_zones(
log: &Logger,
zones: &BTreeMap<Uuid, (SocketAddrV6, OmicronZonesConfig)>,
zones: &BTreeMap<Uuid, (SocketAddr, OmicronZonesConfig)>,
) -> Result<(), Vec<anyhow::Error>> {
let errors: Vec<_> = stream::iter(zones.clone())
.filter_map(|(sled_id, (addr, config))| async move {
Expand Down Expand Up @@ -132,7 +134,15 @@ async fn deploy_zones(
mod test {
use super::*;
use crate::app::background::common::BackgroundTask;
use httptest::matchers::{all_of, json_decoded, request};
use httptest::responders::status_code;
use httptest::Expectation;
use nexus_test_utils_macros::nexus_test;
use nexus_types::inventory::{
OmicronZoneConfig, OmicronZoneDataset, OmicronZoneType,
};
use serde::Deserialize;
use sled_agent_client::types::Generation;

type ControlPlaneTestContext =
nexus_test_utils::ControlPlaneTestContext<crate::Server>;
Expand All @@ -155,8 +165,163 @@ mod test {

// Get a success (empty) result back when the blueprint has an empty set of zones
let blueprint = Arc::new(Blueprint::OmicronZones(BTreeMap::new()));
blueprint_tx.send(Some(blueprint)).unwrap();
let value = task.activate(&opctx).await;
assert_eq!(value, json!({}));

// Create some fake sled-agent servers to respond to zone puts
let mut s1 = httptest::Server::run();
let mut s2 = httptest::Server::run();

// The particular dataset doesn't matter for this test.
// We re-use the same one to not obfuscate things
// TODO: Why do we even need to allocate that zone dataset from nexus in
// the first place?
let dataset = OmicronZoneDataset {
pool_name: format!("oxp_{}", Uuid::new_v4()).parse().unwrap(),
};

// Zones are updated in a particular order, but each request contains
// the full set of zones that must be running.
// See https://github.com/oxidecomputer/omicron/blob/main/sled-agent/src/rack_setup/service.rs#L976-L998
// for more details.
let mut zones = OmicronZonesConfig {
generation: Generation(1),
zones: vec![OmicronZoneConfig {
id: Uuid::new_v4(),
underlay_address: "::1".parse().unwrap(),
zone_type: OmicronZoneType::InternalDns {
dataset,
dns_address: "oh-hello-internal-dns".into(),
gz_address: "::1".parse().unwrap(),
gz_address_index: 0,
http_address: "some-ipv6-address".into(),
},
}],
};

// Create a blueprint with only the `InternalDns` zone for both servers
// We reuse the same `OmicronZonesConfig` because the details don't
// matter for this test.
let sled_id1 = Uuid::new_v4();
let sled_id2 = Uuid::new_v4();

let blueprint = Arc::new(Blueprint::OmicronZones(BTreeMap::from([
(sled_id1, (s1.addr(), zones.clone())),
(sled_id2, (s2.addr(), zones.clone())),
])));

// Send the blueprint with the first set of zones to the task
blueprint_tx.send(Some(blueprint)).unwrap();

// Check that the initial requests were sent to the fake sled-agents
for s in [&mut s1, &mut s2] {
s.expect(
Expectation::matching(all_of![
request::method_path("PUT", "/omicron-zones",),
// Our generation number should be 1 and there should
// be only a single zone.
request::body(json_decoded(|c: &OmicronZonesConfig| {
c.generation == Generation(1) && c.zones.len() == 1
}))
])
.respond_with(status_code(204)),
);
}

// Activate the task to trigger zone configuration on the sled-agents
let value = task.activate(&opctx).await;
assert_eq!(value, json!({}));
s1.verify_and_clear();
s2.verify_and_clear();

// Do it again. This should trigger the same request.
for s in [&mut s1, &mut s2] {
s.expect(
Expectation::matching(request::method_path(
"PUT",
"/omicron-zones",
))
.respond_with(status_code(204)),
);
}
let value = task.activate(&opctx).await;
assert_eq!(value, json!({}));
s1.verify_and_clear();
s2.verify_and_clear();

// Take another lap, but this time, have one server fail the request and
// try again.
s1.expect(
Expectation::matching(request::method_path(
"PUT",
"/omicron-zones",
))
.respond_with(status_code(204)),
);
s2.expect(
Expectation::matching(request::method_path(
"PUT",
"/omicron-zones",
))
.respond_with(status_code(500)),
);

// Define a type we can use to pick stuff out of error objects.
#[derive(Deserialize)]
struct ErrorResult {
errors: Vec<String>,
}

let value = task.activate(&opctx).await;
println!("{:?}", value);
let result: ErrorResult = serde_json::from_value(value).unwrap();
assert_eq!(result.errors.len(), 1);
assert!(
result.errors[0].starts_with("Failed to put OmicronZonesConfig")
);
s1.verify_and_clear();
s2.verify_and_clear();

// Add an `InternalNtp` zone for our next update
zones.generation = Generation(2);
zones.zones.push(OmicronZoneConfig {
id: Uuid::new_v4(),
underlay_address: "::1".parse().unwrap(),
zone_type: OmicronZoneType::InternalNtp {
address: "::1".into(),
dns_servers: vec!["::1".parse().unwrap()],
domain: None,
ntp_servers: vec!["some-ntp-server-addr".into()],
},
});

// Update our watch channel
let blueprint = Arc::new(Blueprint::OmicronZones(BTreeMap::from([
(sled_id1, (s1.addr(), zones.clone())),
(sled_id2, (s2.addr(), zones.clone())),
])));
blueprint_tx.send(Some(blueprint)).unwrap();

// Set our new expectations
for s in [&mut s1, &mut s2] {
s.expect(
Expectation::matching(all_of![
request::method_path("PUT", "/omicron-zones",),
// Our generation number should be bumped and there should
// be two zones.
request::body(json_decoded(|c: &OmicronZonesConfig| {
c.generation == Generation(2) && c.zones.len() == 2
}))
])
.respond_with(status_code(204)),
);
}

// Activate the task
let value = task.activate(&opctx).await;
assert_eq!(value, json!({}));
s1.verify_and_clear();
s2.verify_and_clear();
}
}
4 changes: 2 additions & 2 deletions nexus/types/src/blueprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! Deployment details crated by the update planner
pub use sled_agent_client::types::OmicronZonesConfig;
use std::{collections::BTreeMap, net::SocketAddrV6};
use std::{collections::BTreeMap, net::SocketAddr};
use uuid::Uuid;

/// An individual decision made by the update planner in order to drive the
Expand Down Expand Up @@ -46,7 +46,7 @@ use uuid::Uuid;
#[derive(Debug, PartialEq, Eq)]
pub enum Blueprint {
// Mapping from Sled UUID to (sled-agent address, zone config) pair
OmicronZones(BTreeMap<Uuid, (SocketAddrV6, OmicronZonesConfig)>),
OmicronZones(BTreeMap<Uuid, (SocketAddr, OmicronZonesConfig)>),
//Dns(SomeDnsMapping)
}

Expand Down

0 comments on commit 279e9ac

Please sign in to comment.