Skip to content

Commit 8cd11db

Browse files
committed
[omdb] allow looking up sitreps by version
Currently, `omdb db sitrep info` and `omdb db sitrep analysis-report` let you look up sitreps by their ID, or "current" to select the current sitrep. When looking at a sitrep, it is sometimes desirable to be able to select an arbitrary prior version without first having to use the sitrep history command to determine the ID of that version. This commit changes the sitrep commands to allow selecting a sitrep by ID, "current", or its version number, so you can just say `omdb db sitrep show 3` if you want to see the 3rd sitrep in the history. Fixes #10802
1 parent 3298185 commit 8cd11db

3 files changed

Lines changed: 309 additions & 81 deletions

File tree

dev-tools/omdb/src/bin/omdb/db/sitrep.rs

Lines changed: 162 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use omicron_common::api::external::PaginationOrder;
2323
use omicron_git_version::GitVersion;
2424
use omicron_uuid_kinds::GenericUuid;
2525
use omicron_uuid_kinds::SitrepUuid;
26+
use std::fmt;
2627
use tabled::Tabled;
2728
use uuid::Uuid;
2829

@@ -48,9 +49,15 @@ enum Commands {
4849
/// Show details on a situation report.
4950
#[clap(alias = "show")]
5051
Info {
51-
/// The UUID of the sitrep to show, or "current" to show the current
52-
/// sitrep.
53-
sitrep: SitrepIdOrCurrent,
52+
/// The sitrep to show, identified by UUID, version number, or
53+
/// "current".
54+
///
55+
/// A value of "current" selects the current sitrep. An integer
56+
/// (optionally prefixed with "v", e.g. "3" or "v3") selects the
57+
/// sitrep with that version number in the sitrep history. Any other
58+
/// value is parsed as a sitrep UUID.
59+
#[clap(value_name = "UUID|VERSION|current")]
60+
sitrep: SitrepSelector,
5461

5562
#[clap(flatten)]
5663
opts: ShowOptions,
@@ -73,9 +80,15 @@ pub(super) struct SitrepHistoryArgs {
7380

7481
#[derive(Debug, Args, Clone)]
7582
struct AnalysisReportArgs {
76-
/// The UUID of the sitrep to show, or "current" to show the current
77-
/// sitrep.
78-
sitrep: SitrepIdOrCurrent,
83+
/// The sitrep whose analysis report to show, identified by UUID, version
84+
/// number, or "current".
85+
///
86+
/// A value of "current" selects the current sitrep. An integer
87+
/// (optionally prefixed with "v", e.g. "3" or "v3") selects the sitrep
88+
/// with that version number in the sitrep history. Any other value is
89+
/// parsed as a sitrep UUID.
90+
#[clap(value_name = "UUID|VERSION|current")]
91+
sitrep: SitrepSelector,
7992

8093
#[clap(flatten)]
8194
opts: ShowOptions,
@@ -87,22 +100,140 @@ struct ShowOptions {
87100
json: bool,
88101
}
89102

103+
/// Selects a sitrep by UUID, by version number, or the current one.
90104
#[derive(Debug, Clone, Copy)]
91-
enum SitrepIdOrCurrent {
105+
enum SitrepSelector {
92106
Current,
93107
Id(SitrepUuid),
108+
Version(u32),
94109
}
95110

96-
impl std::str::FromStr for SitrepIdOrCurrent {
97-
type Err = omicron_uuid_kinds::ParseError;
111+
impl std::str::FromStr for SitrepSelector {
112+
type Err = anyhow::Error;
98113

99114
fn from_str(s: &str) -> Result<Self, Self::Err> {
100115
let s = s.trim();
101116
if s.eq_ignore_ascii_case("current") {
102-
Ok(Self::Current)
103-
} else {
104-
let id = s.parse()?;
105-
Ok(Self::Id(id))
117+
return Ok(Self::Current);
118+
}
119+
// Accept an optional leading 'v', since `omdb db sitrep history`
120+
// displays versions as "v1", "v2", etc.
121+
let (s, is_definitely_version) = match s.strip_prefix(['v', 'V']) {
122+
Some(rest) => (rest, true),
123+
None => (s, false),
124+
};
125+
// A value which parses as a `u32` will not also parse as a valid UUID,
126+
// since a `u32` is at most 10 decimal digits, while a UUID requires 32
127+
// hex digits. Even if a valid UUID value omits dashes, `u32::from_str`
128+
// will not accept it, as there are insufficient digits.
129+
match s.parse::<u32>() {
130+
Ok(version) => return Ok(Self::Version(version)),
131+
// If the value doesn't parse as a u32 but had the version prefix,
132+
// bail out now.
133+
Err(e) if is_definitely_version => {
134+
return Err(anyhow::Error::from(e).context(
135+
"a sitrep version (prefixed with 'v' or 'V') must be a \
136+
valid 32-bit unsigned integer",
137+
));
138+
}
139+
// Otherwise, fall back to parsing as a UUID.
140+
Err(_) => {}
141+
}
142+
match s.parse() {
143+
Ok(id) => Ok(Self::Id(id)),
144+
Err(_) => Err(anyhow::anyhow!(
145+
"expected a sitrep UUID, a version number, or \"current\""
146+
)),
147+
}
148+
}
149+
}
150+
151+
impl SitrepSelector {
152+
fn display_for_error<'a>(
153+
&'a self,
154+
id: &'a SitrepUuid,
155+
) -> impl fmt::Display + 'a {
156+
struct Displayer<'a> {
157+
sel: &'a SitrepSelector,
158+
id: &'a SitrepUuid,
159+
}
160+
impl fmt::Display for Displayer<'_> {
161+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162+
let Self { sel, id } = self;
163+
match sel {
164+
SitrepSelector::Current => {
165+
write!(f, "the current fault-management sitrep ({id})")
166+
}
167+
SitrepSelector::Version(v) => {
168+
write!(f, "fault-management sitrep v{v} ({id})")
169+
}
170+
SitrepSelector::Id(id) => {
171+
write!(f, "fault-management sitrep {id}")
172+
}
173+
}
174+
}
175+
}
176+
Displayer { sel: &self, id }
177+
}
178+
179+
/// Determine the sitrep UUID, and optional version number, for this
180+
/// selector.
181+
///
182+
/// The version number is optional because selecting a sitrep by its UUID
183+
/// may refer to a sitrep which has not been committed to the history table.
184+
/// When selecting by UUID, we attempt to resolve the version record from
185+
/// the `fm_sitrep_history` table if one exists, but return `None` if the
186+
/// sitrep is not part of the history.
187+
async fn resolve(
188+
&self,
189+
datastore: &DataStore,
190+
opctx: &OpContext,
191+
) -> anyhow::Result<(Option<fm::SitrepVersion>, SitrepUuid)> {
192+
match self {
193+
SitrepSelector::Current => {
194+
let version = datastore
195+
.fm_current_sitrep_version(&opctx)
196+
.await
197+
.context("failed to look up the current sitrep version")?
198+
.ok_or_else(|| {
199+
anyhow::anyhow!(
200+
"no current sitrep version exists at this time!"
201+
)
202+
})?;
203+
204+
let id = version.id;
205+
Ok((Some(version), id))
206+
}
207+
SitrepSelector::Version(v) => {
208+
let version = history_dsl::fm_sitrep_history
209+
.filter(history_dsl::version.eq(model::SqlU32::new(*v)))
210+
.select(model::SitrepVersion::as_select())
211+
.first_async(&*datastore.pool_connection_for_tests().await?)
212+
.await
213+
.optional()
214+
.with_context(|| {
215+
format!("failed to look up sitrep version v{v}")
216+
})?
217+
.map(fm::SitrepVersion::from)
218+
.ok_or_else(|| anyhow::anyhow!("no sitrep v{v} exists"))?;
219+
let id = version.id;
220+
Ok((Some(version), id))
221+
}
222+
SitrepSelector::Id(id) => {
223+
// If we are looking up a sitrep by UUID, it may or may not
224+
// exist in the history. Let's see if it does!
225+
let maybe_version = history_dsl::fm_sitrep_history
226+
.filter(history_dsl::sitrep_id.eq(id.into_untyped_uuid()))
227+
.select(model::SitrepVersion::as_select())
228+
.first_async(&*datastore.pool_connection_for_tests().await?)
229+
.await
230+
.optional()
231+
.with_context(|| {
232+
format!("failed to look up sitrep version for ID {id}")
233+
})?
234+
.map(Into::into);
235+
Ok((maybe_version, *id))
236+
}
106237
}
107238
}
108239
}
@@ -126,7 +257,7 @@ pub(super) async fn cmd_db_sitrep(
126257
datastore,
127258
fetch_opts,
128259
args,
129-
SitrepIdOrCurrent::Current,
260+
SitrepSelector::Current,
130261
)
131262
.await
132263
}
@@ -215,53 +346,25 @@ async fn cmd_db_sitrep_show(
215346
datastore: &DataStore,
216347
_fetch_opts: &DbFetchOptions,
217348
opts: &ShowOptions,
218-
sitrep: SitrepIdOrCurrent,
349+
sitrep_selector: SitrepSelector,
219350
) -> anyhow::Result<()> {
220-
let ctx = || match sitrep {
221-
SitrepIdOrCurrent::Current => {
222-
"looking up the current fault management sitrep".to_string()
223-
}
224-
SitrepIdOrCurrent::Id(id) => {
225-
format!("looking up fault management sitrep {id:?}")
226-
}
227-
};
228-
229351
let current_version = datastore
230352
.fm_current_sitrep_version(&opctx)
231353
.await
232354
.context("failed to look up the current sitrep version")?;
233355

234356
let conn = datastore.pool_connection_for_tests().await?;
235-
let (maybe_version, sitrep) = match sitrep {
236-
SitrepIdOrCurrent::Id(id) => {
237-
let sitrep =
238-
datastore.fm_sitrep_read(opctx, id).await.with_context(ctx)?;
239-
let version = history_dsl::fm_sitrep_history
240-
.filter(history_dsl::sitrep_id.eq(id.into_untyped_uuid()))
241-
.select(model::SitrepVersion::as_select())
242-
.first_async(&*conn)
243-
.await
244-
.optional()
245-
.with_context(ctx)?
246-
.map(Into::into);
247-
(version, sitrep)
248-
}
249-
SitrepIdOrCurrent::Current => {
250-
let Some(version) = current_version.clone() else {
251-
anyhow::bail!("no current sitrep exists at this time");
252-
};
253-
254-
let sitrep = datastore
255-
.fm_sitrep_read(opctx, version.id)
256-
.await
257-
.with_context(ctx)?;
258-
(Some(version), sitrep)
259-
}
260-
};
357+
let (maybe_version, id) =
358+
sitrep_selector.resolve(&datastore, &opctx).await?;
359+
let err_ctx = sitrep_selector.display_for_error(&id);
360+
let sitrep = datastore
361+
.fm_sitrep_read(&opctx, id)
362+
.await
363+
.with_context(|| format!("failed to look up {err_ctx}"))?;
261364

262365
if opts.json {
263366
serde_json::to_writer_pretty(std::io::stdout(), &sitrep)
264-
.context("failed to serialize sitrep")?;
367+
.with_context(|| format!("failed to serialize {err_ctx}"))?;
265368
return Ok(());
266369
}
267370

@@ -410,34 +513,12 @@ async fn cmd_db_sitrep_analysis_report(
410513
args: &AnalysisReportArgs,
411514
) -> anyhow::Result<()> {
412515
let &AnalysisReportArgs { sitrep, ref opts } = args;
413-
let id = match sitrep {
414-
SitrepIdOrCurrent::Current => {
415-
datastore
416-
.fm_current_sitrep_version(&opctx)
417-
.await
418-
.context("failed to look up the current sitrep version")?
419-
.ok_or_else(|| {
420-
anyhow::anyhow!("no current sitrep exists at this time")
421-
})?
422-
.id
423-
}
424-
SitrepIdOrCurrent::Id(id) => id,
425-
};
426-
427-
let ctx = || match sitrep {
428-
SitrepIdOrCurrent::Current => {
429-
"looking up analysis report for the current fault management \
430-
sitrep"
431-
.to_string()
432-
}
433-
SitrepIdOrCurrent::Id(id) => {
434-
format!(
435-
"looking up analysis report for fault management sitrep {id}"
436-
)
437-
}
438-
};
439-
440-
let report = load_analysis_report(datastore, id).await.with_context(ctx)?;
516+
let (_, id) = sitrep.resolve(&datastore, &opctx).await?;
517+
let err_ctx = sitrep.display_for_error(&id);
518+
let report =
519+
load_analysis_report(datastore, id).await.with_context(|| {
520+
format!("failed to load analysis report for {err_ctx}",)
521+
})?;
441522
print_analysis_report(&report, opts.json)?;
442523

443524
Ok(())
@@ -493,7 +574,7 @@ fn print_analysis_report(
493574
);
494575
let displayer =
495576
nexus_types::fm::json_display::Displayer::new(&input_report);
496-
println!("{displayer}",);
577+
println!("{displayer}");
497578
}
498579
}
499580

@@ -507,7 +588,7 @@ fn print_analysis_report(
507588
);
508589
let displayer =
509590
nexus_types::fm::json_display::Displayer::new(&analysis_report);
510-
println!("{displayer}",);
591+
println!("{displayer}");
511592
}
512593
}
513594

dev-tools/omdb/tests/test_all_output.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ async fn test_omdb_usage_errors() {
103103
&["db", "ereport", "info", "--help"],
104104
&["db", "sleds", "--help"],
105105
&["db", "sitrep", "--help"],
106+
&["db", "sitrep", "show", "--help"],
107+
&["db", "sitrep", "analysis-report", "--help"],
108+
// Invalid sitrep selectors: not a UUID, a version number, or "current"
109+
&["db", "sitrep", "show", "not-a-sitrep"],
110+
// Invalid sitrep selector: begins with 'v' but is not an integer.
111+
&["db", "sitrep", "show", "v1.2.3"],
106112
&["db", "saga"],
107113
&["db", "snapshots"],
108114
&["db", "network"],

0 commit comments

Comments
 (0)