@@ -23,6 +23,7 @@ use omicron_common::api::external::PaginationOrder;
2323use omicron_git_version:: GitVersion ;
2424use omicron_uuid_kinds:: GenericUuid ;
2525use omicron_uuid_kinds:: SitrepUuid ;
26+ use std:: fmt;
2627use tabled:: Tabled ;
2728use 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 ) ]
7582struct 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
0 commit comments