-
Notifications
You must be signed in to change notification settings - Fork 61
[nexus] fault management situation reports #9320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hawkw
wants to merge
24
commits into
main
Choose a base branch
from
eliza/fm-sitrep
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
65f6ffe
[nexus] initial schema for sitrep tables
hawkw be4b7ff
[nexus] sitrep types
hawkw c3b36f0
[nexus] finish loader task, other stuff
hawkw 72a7c50
[nexus] s/fm_sitrep_version/fm_sitrep_history
hawkw 5d0cfa9
start on horrific CTE
hawkw 9cf6b8d
finish sitrep insert CTE
hawkw 6565dcb
wip omdb stuff
hawkw 05c8504
finish omdb sitrep cmds
hawkw bef2996
add bg task details
hawkw 585b39d
add sitrep_loader to config tomls
hawkw c4acdac
fix clap
hawkw 18142e3
finally update OMDB tests
hawkw b9c5c07
migrations
hawkw 6640ab6
commentary + a few API tweaks
hawkw 218056e
clippy tidiness
hawkw e9d87b2
fixup docs
hawkw 4108899
whoops make module public so the error is visible
hawkw 570dda5
you gotta remember to update the config tests
hawkw 350508b
Merge branch 'main' into eliza/fm-sitrep
hawkw aae8d15
reorder fields
hawkw 8522c68
add sitrep loader test
hawkw dfffd69
check that the current version hasn't gone down
hawkw 649a327
make OMDB's view of the current sitrep more consistent
hawkw cb1813f
rename model module
hawkw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,355 @@ | ||
| // This Source Code Form is subject to the terms of the Mozilla Public | ||
| // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
|
||
| //! `omdb db sitrep` subcommands | ||
|
|
||
| use crate::db::DbFetchOptions; | ||
| use crate::db::check_limit; | ||
| use crate::helpers::const_max_len; | ||
| use crate::helpers::datetime_rfc3339_concise; | ||
| use anyhow::Context; | ||
| use async_bb8_diesel::AsyncRunQueryDsl; | ||
| use chrono::{DateTime, Utc}; | ||
| use clap::Args; | ||
| use clap::Subcommand; | ||
| use diesel::prelude::*; | ||
| use nexus_db_queries::context::OpContext; | ||
| use nexus_db_queries::db::DataStore; | ||
| use nexus_db_queries::db::model; | ||
| use nexus_db_queries::db::pagination::paginated; | ||
| use nexus_types::fm; | ||
| use omicron_common::api::external::DataPageParams; | ||
| use omicron_common::api::external::PaginationOrder; | ||
| use omicron_uuid_kinds::GenericUuid; | ||
| use omicron_uuid_kinds::SitrepUuid; | ||
| use tabled::Tabled; | ||
| use uuid::Uuid; | ||
|
|
||
| use nexus_db_schema::schema::fm_sitrep::dsl as sitrep_dsl; | ||
| use nexus_db_schema::schema::fm_sitrep_history::dsl as history_dsl; | ||
| use nexus_db_schema::schema::inv_collection::dsl as inv_collection_dsl; | ||
|
|
||
| #[derive(Debug, Args, Clone)] | ||
| pub(super) struct SitrepArgs { | ||
| #[command(subcommand)] | ||
| command: Commands, | ||
| } | ||
|
|
||
| #[derive(Debug, Subcommand, Clone)] | ||
| enum Commands { | ||
| /// List the current situation report history. | ||
| History(SitrepHistoryArgs), | ||
|
|
||
| /// Show the current situation report. | ||
| /// | ||
| /// This is an alias for `omdb db sitrep info current`. | ||
| Current(ShowArgs), | ||
|
|
||
| /// Show details on a situation report. | ||
| #[clap(alias = "show")] | ||
| Info { | ||
| /// The UUID of the sitrep to show, or "current" to show the current | ||
| /// sitrep. | ||
| sitrep: SitrepIdOrCurrent, | ||
|
|
||
| #[clap(flatten)] | ||
| args: ShowArgs, | ||
| }, | ||
| } | ||
|
|
||
| #[derive(Debug, Args, Clone)] | ||
| pub(super) struct SitrepHistoryArgs { | ||
| /// If present, start at this sitrep version. | ||
| /// | ||
| /// If this is not set, the list will start with the current sitrep. This | ||
| /// option is useful when the number of sitreps exceeds the database fetch | ||
| /// limit. | ||
| #[arg(long, short, alias = "starting_at")] | ||
| from: Option<u32>, | ||
| } | ||
|
|
||
| #[derive(Debug, Args, Clone)] | ||
| struct ShowArgs {} | ||
|
|
||
| #[derive(Debug, Clone, Copy)] | ||
| enum SitrepIdOrCurrent { | ||
| Current, | ||
| Id(SitrepUuid), | ||
| } | ||
|
|
||
| impl std::str::FromStr for SitrepIdOrCurrent { | ||
| type Err = omicron_uuid_kinds::ParseError; | ||
|
|
||
| fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| let s = s.trim(); | ||
| if s.eq_ignore_ascii_case("current") { | ||
| Ok(Self::Current) | ||
| } else { | ||
| let id = s.parse()?; | ||
| Ok(Self::Id(id)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub(super) async fn cmd_db_sitrep( | ||
| opctx: &OpContext, | ||
| datastore: &DataStore, | ||
| fetch_opts: &DbFetchOptions, | ||
| args: &SitrepArgs, | ||
| ) -> anyhow::Result<()> { | ||
| match args.command { | ||
| Commands::History(ref args) => { | ||
| cmd_db_sitrep_history(datastore, fetch_opts, args).await | ||
| } | ||
| Commands::Info { sitrep, ref args } => { | ||
| cmd_db_sitrep_show(opctx, datastore, fetch_opts, args, sitrep).await | ||
| } | ||
| Commands::Current(ref args) => { | ||
| cmd_db_sitrep_show( | ||
| opctx, | ||
| datastore, | ||
| fetch_opts, | ||
| args, | ||
| SitrepIdOrCurrent::Current, | ||
| ) | ||
| .await | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub(super) async fn cmd_db_sitrep_history( | ||
| datastore: &DataStore, | ||
| fetch_opts: &DbFetchOptions, | ||
| args: &SitrepHistoryArgs, | ||
| ) -> anyhow::Result<()> { | ||
| let ctx = || { | ||
| if let Some(from) = args.from { | ||
| format!( | ||
| "listing fault management sitrep history (starting at {from})" | ||
| ) | ||
| } else { | ||
| "listing fault management sitrep history".to_string() | ||
| } | ||
| }; | ||
|
|
||
| #[derive(Tabled)] | ||
| #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] | ||
| struct SitrepRow { | ||
| v: u32, | ||
| id: Uuid, | ||
| #[tabled(display_with = "datetime_rfc3339_concise")] | ||
| created_at: DateTime<Utc>, | ||
| comment: String, | ||
| } | ||
|
|
||
| let conn = datastore.pool_connection_for_tests().await?; | ||
| let marker = args.from.map(model::SqlU32::new); | ||
| let pagparams = DataPageParams { | ||
| marker: marker.as_ref(), | ||
| direction: PaginationOrder::Descending, | ||
| limit: fetch_opts.fetch_limit, | ||
| }; | ||
| let sitreps: Vec<(model::SitrepVersion, model::SitrepMetadata)> = | ||
| paginated( | ||
| history_dsl::fm_sitrep_history, | ||
| history_dsl::version, | ||
| &pagparams, | ||
| ) | ||
| .inner_join( | ||
| sitrep_dsl::fm_sitrep.on(history_dsl::sitrep_id.eq(sitrep_dsl::id)), | ||
| ) | ||
| .select(( | ||
| model::SitrepVersion::as_select(), | ||
| model::SitrepMetadata::as_select(), | ||
| )) | ||
| .load_async(&*conn) | ||
| .await | ||
| .with_context(ctx)?; | ||
|
|
||
| check_limit(&sitreps, fetch_opts.fetch_limit, ctx); | ||
|
|
||
| let rows = sitreps.into_iter().map(|(version, metadata)| { | ||
| let model::SitrepMetadata { | ||
| id, | ||
| time_created, | ||
| comment, | ||
| creator_id: _, | ||
| parent_sitrep_id: _, | ||
| inv_collection_id: _, | ||
| } = metadata; | ||
| SitrepRow { | ||
| v: version.version.into(), | ||
| id: id.into_untyped_uuid(), | ||
| created_at: time_created, | ||
| comment, | ||
| } | ||
| }); | ||
|
|
||
| let table = tabled::Table::new(rows) | ||
| .with(tabled::settings::Style::empty()) | ||
| .with(tabled::settings::Padding::new(0, 1, 0, 0)) | ||
| .to_string(); | ||
| println!("{table}"); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| async fn cmd_db_sitrep_show( | ||
| opctx: &OpContext, | ||
| datastore: &DataStore, | ||
| _fetch_opts: &DbFetchOptions, | ||
| _args: &ShowArgs, | ||
| sitrep: SitrepIdOrCurrent, | ||
| ) -> anyhow::Result<()> { | ||
| let ctx = || match sitrep { | ||
| SitrepIdOrCurrent::Current => { | ||
| "looking up the current fault management sitrep".to_string() | ||
| } | ||
| SitrepIdOrCurrent::Id(id) => { | ||
| format!("looking up fault management sitrep {id:?}") | ||
| } | ||
| }; | ||
| let conn = datastore.pool_connection_for_tests().await?; | ||
|
|
||
| let (maybe_version, sitrep) = match sitrep { | ||
| SitrepIdOrCurrent::Id(id) => { | ||
| let sitrep = | ||
| datastore.fm_sitrep_read(opctx, id).await.with_context(ctx)?; | ||
| let version = history_dsl::fm_sitrep_history | ||
| .filter(history_dsl::sitrep_id.eq(id.into_untyped_uuid())) | ||
| .select(model::SitrepVersion::as_select()) | ||
| .first_async(&*conn) | ||
| .await | ||
| .optional() | ||
| .with_context(ctx)? | ||
| .map(Into::into); | ||
| (version, sitrep) | ||
| } | ||
| SitrepIdOrCurrent::Current => { | ||
| let Some((version, sitrep)) = | ||
| datastore.fm_sitrep_read_current(opctx).await? | ||
| else { | ||
| anyhow::bail!("no current sitrep exists at this time"); | ||
| }; | ||
| (Some(version), sitrep) | ||
| } | ||
| }; | ||
|
|
||
| let fm::Sitrep { metadata } = sitrep; | ||
| let fm::SitrepMetadata { | ||
| id, | ||
| creator_id, | ||
| time_created, | ||
| parent_sitrep_id, | ||
| inv_collection_id, | ||
| comment, | ||
| } = metadata; | ||
|
|
||
| const ID: &'static str = "ID"; | ||
| const PARENT_SITREP_ID: &'static str = "parent sitrep ID"; | ||
| const CREATED_BY: &'static str = "created by"; | ||
| const CREATED_AT: &'static str = "created at"; | ||
| const COMMENT: &'static str = "comment"; | ||
| const STATUS: &'static str = "status"; | ||
| const VERSION: &'static str = " version"; | ||
| const MADE_CURRENT_AT: &'static str = " made current at"; | ||
| const INV_COLLECTION_ID: &'static str = "inventory collection ID"; | ||
| const INV_STARTED_AT: &'static str = " started at"; | ||
| const INV_FINISHED_AT: &'static str = " finished at"; | ||
|
|
||
| const WIDTH: usize = const_max_len(&[ | ||
| ID, | ||
| PARENT_SITREP_ID, | ||
| CREATED_AT, | ||
| CREATED_BY, | ||
| COMMENT, | ||
| STATUS, | ||
| VERSION, | ||
| MADE_CURRENT_AT, | ||
| INV_COLLECTION_ID, | ||
| INV_STARTED_AT, | ||
| INV_FINISHED_AT, | ||
| ]); | ||
|
|
||
| println!("\n{:=<80}", "== FAULT MANAGEMENT SITUATION REPORT "); | ||
| println!(" {ID:>WIDTH$}: {id:?}"); | ||
| println!(" {PARENT_SITREP_ID:>WIDTH$}: {parent_sitrep_id:?}"); | ||
| println!(" {CREATED_BY:>WIDTH$}: {creator_id}"); | ||
| println!(" {CREATED_AT:>WIDTH$}: {time_created}"); | ||
| if comment.is_empty() { | ||
| println!(" {COMMENT:>WIDTH$}: N/A\n"); | ||
| } else { | ||
| println!(" {COMMENT:>WIDTH$}:"); | ||
| println!("{}\n", textwrap::indent(&comment, " ")); | ||
| } | ||
|
|
||
| match maybe_version { | ||
| None => println!( | ||
| " {STATUS:>WIDTH$}: not committed to the sitrep history" | ||
| ), | ||
| Some(fm::SitrepVersion { version, time_made_current, .. }) => { | ||
| let current_version = | ||
| datastore.fm_current_sitrep_version(&opctx).await; | ||
| if matches!(current_version, Ok(Some(ref v)) if v.id == id) { | ||
| println!(" {STATUS:>WIDTH$}: this is the current sitrep!",); | ||
| } else { | ||
| println!(" {STATUS:>WIDTH$}: in the sitrep history"); | ||
| } | ||
| println!(" {VERSION:>WIDTH$}: v{version}"); | ||
| println!(" {MADE_CURRENT_AT:>WIDTH$}: {time_made_current}"); | ||
| match current_version { | ||
| Ok(Some(v)) if v.id == id => {} | ||
| Ok(Some(fm::SitrepVersion { version, id, .. })) => { | ||
| println!( | ||
| "(i) note: the current sitrep is {id:?} \ | ||
| (at v{version})", | ||
| ); | ||
| } | ||
| Ok(None) => { | ||
| eprintln!( | ||
| "/!\\ WEIRD: this sitrep is in the sitrep history, \ | ||
| but there is no current sitrep. this should not \ | ||
| happen!" | ||
| ); | ||
| } | ||
| Err(err) => { | ||
| eprintln!( | ||
| "/!\\ failed to determine the current sitrep \ | ||
| version: {err}" | ||
| ); | ||
| } | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| println!("\n{:-<80}", "== DIAGNOSIS INPUTS "); | ||
| println!(" {INV_COLLECTION_ID:>WIDTH$}: {inv_collection_id:?}"); | ||
| let inv_collection = inv_collection_dsl::inv_collection | ||
| .filter( | ||
| inv_collection_dsl::id.eq(inv_collection_id.into_untyped_uuid()), | ||
| ) | ||
| .select(model::InvCollection::as_select()) | ||
| .first_async(&*conn) | ||
| .await | ||
| .optional(); | ||
| match inv_collection { | ||
| Err(err) => { | ||
| eprintln!( | ||
| "/!\\ failed to fetch inventory collection details: {err}" | ||
| ); | ||
| } | ||
| Ok(Some(model::InvCollection { time_started, time_done, .. })) => { | ||
| println!(" {INV_STARTED_AT:>WIDTH$}: {time_started}"); | ||
| println!(" {INV_FINISHED_AT:>WIDTH$}: {time_done}"); | ||
| } | ||
| Ok(None) => { | ||
| println!( | ||
| " note: this collection no longer exists (perhaps it has \ | ||
| been pruned?)" | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.