Skip to content

Commit 0abc01f

Browse files
authored
add CLI for executing blueprints by hand (#7801)
1 parent 5c90e2b commit 0abc01f

File tree

8 files changed

+325
-12
lines changed

8 files changed

+325
-12
lines changed

Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ members = [
4343
"dev-tools/oxlog",
4444
"dev-tools/pins",
4545
"dev-tools/reconfigurator-cli",
46+
"dev-tools/reconfigurator-exec-unsafe",
4647
"dev-tools/releng",
4748
"dev-tools/xtask",
4849
"dns-server",
@@ -178,6 +179,7 @@ default-members = [
178179
"dev-tools/oxlog",
179180
"dev-tools/pins",
180181
"dev-tools/reconfigurator-cli",
182+
"dev-tools/reconfigurator-exec-unsafe",
181183
"dev-tools/releng",
182184
# Do not include xtask in the list of default members, because this causes
183185
# hakari to not work as well and build times to be longer.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[package]
2+
name = "omicron-reconfigurator-exec-unsafe"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MPL-2.0"
6+
7+
[lints]
8+
workspace = true
9+
10+
[build-dependencies]
11+
omicron-rpaths.workspace = true
12+
13+
[dependencies]
14+
anyhow.workspace = true
15+
camino.workspace = true
16+
clap.workspace = true
17+
dropshot.workspace = true
18+
internal-dns-resolver.workspace = true
19+
nexus-db-model.workspace = true
20+
nexus-db-queries.workspace = true
21+
nexus-reconfigurator-execution.workspace = true
22+
nexus-types.workspace = true
23+
omicron-uuid-kinds.workspace = true
24+
# See omicron-rpaths for more about the "pq-sys" dependency.
25+
pq-sys = "*"
26+
serde_json.workspace = true
27+
slog.workspace = true
28+
supports-color.workspace = true
29+
tokio = { workspace = true, features = [ "full" ] }
30+
update-engine.workspace = true
31+
uuid.workspace = true
32+
omicron-workspace-hack.workspace = true
33+
34+
[[bin]]
35+
name = "reconfigurator-exec-unsafe"
36+
path = "src/main.rs"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
// See omicron-rpaths for documentation.
6+
// NOTE: This file MUST be kept in sync with the other build.rs files in this
7+
// repository.
8+
fn main() {
9+
omicron_rpaths::configure_default_omicron_rpaths();
10+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! Execute blueprints from the command line
6+
7+
use anyhow::Context;
8+
use anyhow::bail;
9+
use camino::Utf8PathBuf;
10+
use clap::ColorChoice;
11+
use clap::Parser;
12+
use nexus_db_queries::context::OpContext;
13+
use nexus_db_queries::db;
14+
use nexus_db_queries::db::DataStore;
15+
use nexus_reconfigurator_execution::{RequiredRealizeArgs, realize_blueprint};
16+
use nexus_types::deployment::Blueprint;
17+
use omicron_uuid_kinds::GenericUuid;
18+
use omicron_uuid_kinds::OmicronZoneUuid;
19+
use slog::info;
20+
use std::net::SocketAddr;
21+
use std::sync::Arc;
22+
use update_engine::EventBuffer;
23+
use update_engine::NestedError;
24+
use update_engine::display::LineDisplay;
25+
use update_engine::display::LineDisplayStyles;
26+
27+
#[tokio::main]
28+
async fn main() -> Result<(), anyhow::Error> {
29+
let args = ReconfiguratorExec::parse();
30+
31+
if let Err(error) = args.exec().await {
32+
eprintln!("error: {:#}", error);
33+
std::process::exit(1);
34+
}
35+
36+
Ok(())
37+
}
38+
39+
/// Execute blueprints from the command line
40+
#[derive(Debug, Parser)]
41+
struct ReconfiguratorExec {
42+
/// log level filter
43+
#[arg(
44+
env,
45+
long,
46+
value_parser = parse_dropshot_log_level,
47+
default_value = "info",
48+
)]
49+
log_level: dropshot::ConfigLoggingLevel,
50+
51+
/// an internal DNS server in this deployment
52+
// This default value is currently appropriate for all deployed systems.
53+
// That relies on two assumptions:
54+
//
55+
// 1. The internal DNS servers' underlay addresses are at a fixed location
56+
// from the base of the AZ subnet. This is unlikely to change, since the
57+
// DNS servers must be discoverable with virtually no other information.
58+
// 2. The AZ subnet used for all deployments today is fixed.
59+
//
60+
// For simulated systems (e.g., `cargo xtask omicron-dev run-all`), or if
61+
// these assumptions change in the future, we may need to adjust this.
62+
#[arg(long, default_value = "[fd00:1122:3344:3::1]:53")]
63+
dns_server: SocketAddr,
64+
65+
/// Color output
66+
#[arg(long, value_enum, default_value_t)]
67+
color: ColorChoice,
68+
69+
/// path to a serialized (JSON) blueprint file
70+
blueprint_file: Utf8PathBuf,
71+
}
72+
73+
fn parse_dropshot_log_level(
74+
s: &str,
75+
) -> Result<dropshot::ConfigLoggingLevel, anyhow::Error> {
76+
serde_json::from_str(&format!("{:?}", s)).context("parsing log level")
77+
}
78+
79+
impl ReconfiguratorExec {
80+
async fn exec(self) -> Result<(), anyhow::Error> {
81+
let log = dropshot::ConfigLogging::StderrTerminal {
82+
level: self.log_level.clone(),
83+
}
84+
.to_logger("reconfigurator-exec")
85+
.context("failed to create logger")?;
86+
87+
info!(&log, "setting up resolver");
88+
let qorb_resolver =
89+
internal_dns_resolver::QorbResolver::new(vec![self.dns_server]);
90+
91+
info!(&log, "setting up database pool");
92+
let pool = Arc::new(db::Pool::new(&log, &qorb_resolver));
93+
let datastore = Arc::new(
94+
DataStore::new_failfast(&log, pool)
95+
.await
96+
.context("creating datastore")?,
97+
);
98+
99+
let result = self.do_exec(log, &datastore).await;
100+
datastore.terminate().await;
101+
result
102+
}
103+
104+
async fn do_exec(
105+
&self,
106+
log: slog::Logger,
107+
datastore: &Arc<DataStore>,
108+
) -> Result<(), anyhow::Error> {
109+
info!(&log, "setting up arguments for execution");
110+
let opctx = OpContext::for_tests(log.clone(), datastore.clone());
111+
let resolver = internal_dns_resolver::Resolver::new_from_addrs(
112+
log.clone(),
113+
&[self.dns_server],
114+
)
115+
.with_context(|| {
116+
format!(
117+
"creating DNS resolver for DNS server {:?}",
118+
self.dns_server
119+
)
120+
})?;
121+
122+
let input_path = &self.blueprint_file;
123+
info!(
124+
&log,
125+
"loading blueprint file";
126+
"input_path" => %input_path
127+
);
128+
let file = std::fs::File::open(&self.blueprint_file)
129+
.with_context(|| format!("open {:?}", input_path))?;
130+
let bufread = std::io::BufReader::new(file);
131+
let blueprint: Blueprint = serde_json::from_reader(bufread)
132+
.with_context(|| format!("read and parse {:?}", &input_path))?;
133+
134+
// Check that the blueprint is the current system target.
135+
// (This is currently redundant with a check done early during blueprint
136+
// realization, but we want to avoid assuming that here in this tool.)
137+
let target = datastore
138+
.blueprint_target_get_current(&opctx)
139+
.await
140+
.context("loading current target blueprint")?;
141+
if target.target_id != blueprint.id {
142+
bail!(
143+
"requested blueprint {} does not match current target ({})",
144+
blueprint.id,
145+
target.target_id
146+
);
147+
}
148+
149+
let (sender, mut receiver) = update_engine::channel();
150+
151+
let receiver_task = tokio::spawn(async move {
152+
let mut event_buffer = EventBuffer::default();
153+
while let Some(event) = receiver.recv().await {
154+
event_buffer.add_event(event);
155+
}
156+
event_buffer
157+
});
158+
159+
// This uuid uses similar conventions as the DB fixed data. It's
160+
// intended to be recognizable by a human (maybe just as "a little
161+
// strange looking") and ideally evoke this tool.
162+
let creator =
163+
uuid::Uuid::from_u128(0x001de000_4cf4_4000_8000_4ec04f140000);
164+
// "oxide" "rcfg" "reconfig[urator]"
165+
info!(&log, "beginning execution");
166+
let rv = realize_blueprint(
167+
RequiredRealizeArgs {
168+
opctx: &opctx,
169+
datastore: &datastore,
170+
resolver: &resolver,
171+
blueprint: &blueprint,
172+
creator: OmicronZoneUuid::from_untyped_uuid(creator),
173+
sender,
174+
}
175+
.into(),
176+
)
177+
.await
178+
.context("blueprint execution failed");
179+
180+
// Get and dump the report from the receiver task.
181+
let event_buffer =
182+
receiver_task.await.map_err(|error| NestedError::new(&error))?;
183+
let mut line_display = LineDisplay::new(std::io::stdout());
184+
let should_colorize = match self.color {
185+
ColorChoice::Always => true,
186+
ColorChoice::Auto => {
187+
supports_color::on(supports_color::Stream::Stdout).is_some()
188+
}
189+
ColorChoice::Never => false,
190+
};
191+
if should_colorize {
192+
line_display.set_styles(LineDisplayStyles::colorized());
193+
}
194+
line_display.write_event_buffer(&event_buffer)?;
195+
196+
rv.map(|_| ())
197+
}
198+
}

0 commit comments

Comments
 (0)