@@ -22,6 +22,7 @@ use diesel::ExpressionMethods;
2222use diesel:: OptionalExtension ;
2323use diesel:: expression:: SelectableHelper ;
2424use diesel:: query_dsl:: QueryDsl ;
25+ use nexus_db_lookup:: LookupPath ;
2526use nexus_db_model:: Alert ;
2627use nexus_db_model:: AlertClass ;
2728use nexus_db_model:: AlertReceiver ;
@@ -30,8 +31,6 @@ use nexus_db_model::fm::RendezvousAlertCreated;
3031use nexus_db_queries:: context:: OpContext ;
3132use nexus_db_queries:: db;
3233use nexus_db_queries:: db:: DataStore ;
33- use nexus_db_schema:: schema:: alert:: dsl as alert_dsl;
34- use nexus_db_schema:: schema:: rendezvous_alert_created:: dsl as rendezvous_created_dsl;
3534use nexus_db_schema:: schema:: webhook_delivery:: dsl as delivery_dsl;
3635use nexus_db_schema:: schema:: webhook_delivery_attempt:: dsl as attempt_dsl;
3736use nexus_types:: identity:: Resource ;
@@ -40,6 +39,7 @@ use omicron_common::api::external::Generation;
4039use omicron_common:: api:: external:: NameOrId ;
4140use omicron_common:: api:: external:: http_pagination:: PaginatedBy ;
4241use omicron_uuid_kinds:: AlertUuid ;
42+ use omicron_uuid_kinds:: CaseUuid ;
4343use omicron_uuid_kinds:: GenericUuid ;
4444use tabled:: Tabled ;
4545use uuid:: Uuid ;
@@ -95,9 +95,20 @@ struct AlertListArgs {
9595 /// cases will be included in the output.
9696 ///
9797 /// Note that not all alerts are requested by fault management cases.
98- #[ clap( long = "case" ) ]
98+ #[ clap( long, num_args ( 1 .. ) ) ]
9999 cases : Vec < Uuid > ,
100100
101+ /// Include only alerts with the specified alert classes.
102+ ///
103+ /// If multiple values are provided, alerts with any of those classes will
104+ /// be included in the output.
105+ #[ clap(
106+ long,
107+ num_args( 1 ..) ,
108+ value_enum,
109+ ) ]
110+ classes : Vec < ClapAlertClass > ,
111+
101112 /// If `true`, include only alerts that have been fully dispatched.
102113 /// If `false`, include only alerts that have not been fully dispatched.
103114 ///
@@ -107,6 +118,39 @@ struct AlertListArgs {
107118 dispatched : Option < bool > ,
108119}
109120
121+ // A workaround for not being able to derive `clap::ValueEnum` on
122+ // `nexus_types::alert::AlertClass`, due to it living in the `nexus_types`
123+ // crate(which doesn't really want a `clap` dependency).
124+ #[ derive( Debug , Copy , Clone ) ]
125+ struct ClapAlertClass ( nexus_types:: alert:: AlertClass ) ;
126+
127+ impl clap:: ValueEnum for ClapAlertClass {
128+ fn value_variants < ' a > ( ) -> & ' a [ Self ] {
129+ static VALUE_VARIANTS : std:: sync:: LazyLock < Vec < ClapAlertClass > > =
130+ std:: sync:: LazyLock :: new ( || {
131+ nexus_types:: alert:: AlertClass :: ALL_CLASSES
132+ . iter ( )
133+ . copied ( )
134+ . map ( ClapAlertClass )
135+ . collect ( )
136+ } ) ;
137+ & VALUE_VARIANTS
138+ }
139+
140+ fn to_possible_value ( & self ) -> Option < clap:: builder:: PossibleValue > {
141+ Some (
142+ clap:: builder:: PossibleValue :: new ( self . 0 . as_str ( ) )
143+ . help ( self . 0 . description ( ) ) ,
144+ )
145+ }
146+ }
147+
148+ impl From < & ' _ ClapAlertClass > for AlertClass {
149+ fn from ( & ClapAlertClass ( class) : & ClapAlertClass ) -> AlertClass {
150+ class. into ( )
151+ }
152+ }
153+
110154#[ derive( Debug , Args , Clone ) ]
111155struct AlertInfoArgs {
112156 /// The ID of the alert to show
@@ -215,10 +259,10 @@ pub(super) async fn cmd_db_alert(
215259) -> anyhow:: Result < ( ) > {
216260 match & args. command {
217261 Commands :: Info ( args) => {
218- cmd_db_alert_info ( datastore, fetch_opts, args) . await
262+ cmd_db_alert_info ( opctx , datastore, fetch_opts, args) . await
219263 }
220264 Commands :: List ( args) => {
221- cmd_db_alert_list ( datastore, fetch_opts, args) . await
265+ cmd_db_alert_list ( opctx , datastore, fetch_opts, args) . await
222266 }
223267 Commands :: Webhook ( args) => {
224268 cmd_db_webhook ( opctx, datastore, fetch_opts, args) . await
@@ -876,6 +920,7 @@ async fn cmd_db_webhook_delivery_info(
876920}
877921
878922async fn cmd_db_alert_list (
923+ opctx : & OpContext ,
879924 datastore : & DataStore ,
880925 fetch_opts : & DbFetchOptions ,
881926 args : & AlertListArgs ,
@@ -888,80 +933,57 @@ async fn cmd_db_alert_list(
888933 dispatched_before,
889934 dispatched_after,
890935 dispatched,
936+ classes,
891937 } = args;
892938
893- if let ( Some ( before) , Some ( after) ) = ( before, after) {
894- anyhow:: ensure!(
895- after < before,
896- "if both `--after` and `--before` are included, after must be
897- earlier than before"
898- ) ;
899- }
900-
901- if let ( Some ( before) , Some ( after) ) = ( dispatched_before, dispatched_after) {
902- anyhow:: ensure!(
903- after < before,
904- "if both `--dispatched-after` and `--dispatched-before` are
905- included, after must be earlier than before"
906- ) ;
907- }
908-
909- let conn = datastore. pool_connection_for_tests ( ) . await ?;
910-
911- // Fetch all alerts up to our limit, including any corresponding
912- // `rendezvous_alert_created` markers so we can display the generation at
913- // which the alert was created. Note, all FM-created alerts can be
914- // distinguished by their fm_case_id, but won't necessarily have a creation
915- // marker: GC will clean up markers that are no longer needed.
916- let mut query = alert_dsl:: alert
917- . left_join ( rendezvous_created_dsl:: rendezvous_alert_created)
918- . limit ( fetch_opts. fetch_limit . get ( ) . into ( ) )
919- . order_by ( alert_dsl:: time_created. asc ( ) )
920- . select ( (
921- Alert :: as_select ( ) ,
922- Option :: < RendezvousAlertCreated > :: as_select ( ) ,
923- ) )
924- . into_boxed ( ) ;
925-
926- if let Some ( before) = before {
927- query = query. filter ( alert_dsl:: time_created. lt ( * before) ) ;
928- }
929-
930- if let Some ( after) = after {
931- query = query. filter ( alert_dsl:: time_created. gt ( * after) ) ;
932- }
933-
934- if let Some ( before) = dispatched_before {
935- query = query. filter ( alert_dsl:: time_dispatched. lt ( * before) ) ;
936- }
937-
938- if let Some ( after) = dispatched_after {
939- query = query. filter ( alert_dsl:: time_dispatched. gt ( * after) ) ;
940- }
941-
942- if let Some ( dispatched) = dispatched {
943- if * dispatched {
944- query = query. filter ( alert_dsl:: time_dispatched. is_not_null ( ) ) ;
945- } else {
946- query = query. filter ( alert_dsl:: time_dispatched. is_null ( ) ) ;
939+ let filters = {
940+ let mut filters = nexus_db_queries:: db:: datastore:: AlertFilters :: new ( ) ;
941+ if let & Some ( before) = before {
942+ filters = filters. before ( before) ?;
947943 }
948- }
944+ if let & Some ( after) = after {
945+ filters = filters. after ( after) ?;
946+ }
947+ if let & Some ( dispatched_before) = dispatched_before {
948+ filters = filters. dispatched_before ( dispatched_before) ?;
949+ }
950+ if let & Some ( dispatched_after) = dispatched_after {
951+ filters = filters. dispatched_after ( dispatched_after) ?;
952+ }
953+ if let & Some ( dispatched) = dispatched {
954+ filters = filters. dispatched ( dispatched) ?;
955+ }
956+ if !cases. is_empty ( ) {
957+ filters = filters. for_fm_cases (
958+ cases. iter ( ) . copied ( ) . map ( CaseUuid :: from_untyped_uuid) ,
959+ ) ;
960+ }
961+ if !classes. is_empty ( ) {
962+ filters = filters. with_classes ( classes) ;
963+ }
964+ filters
965+ } ;
949966
950- if !cases. is_empty ( ) {
951- query = query. filter ( alert_dsl:: case_id. eq_any ( cases. clone ( ) ) ) ;
952- }
967+ let pagparams = DataPageParams {
968+ // Descending order shows the newest alerts first
969+ direction : dropshot:: PaginationOrder :: Descending ,
970+ ..first_page ( fetch_opts. fetch_limit )
971+ } ;
953972
954973 let ctx = || "loading alerts" ;
955- let alerts: Vec < ( Alert , Option < RendezvousAlertCreated > ) > =
956- query. load_async ( & * conn) . await . with_context ( ctx) ?;
957-
974+ let alerts: Vec < ( Alert , Option < RendezvousAlertCreated > ) > = datastore
975+ . alert_list_matching ( opctx, & filters, & pagparams)
976+ . await
977+ . with_context ( ctx) ?;
958978 check_limit ( & alerts, fetch_opts. fetch_limit , ctx) ;
959979
960980 #[ derive( Tabled ) ]
961981 #[ tabled( rename_all = "SCREAMING_SNAKE_CASE" ) ]
962982 struct AlertRow {
963983 id : Uuid ,
964984 class : AlertClass ,
985+ #[ tabled( rename = "V" ) ]
986+ version : u32 ,
965987 #[ tabled( display_with = "datetime_rfc3339_concise" ) ]
966988 time_created : DateTime < Utc > ,
967989 #[ tabled( display_with = "datetime_opt_rfc3339_concise" ) ]
@@ -977,6 +999,7 @@ async fn cmd_db_alert_list(
977999 |alert : & Alert , marker : & Option < RendezvousAlertCreated > | AlertRow {
9781000 id : alert. identity . id . into_untyped_uuid ( ) ,
9791001 class : alert. class ,
1002+ version : u32:: from ( alert. version ) ,
9801003 time_created : alert. identity . time_created ,
9811004 time_dispatched : alert. time_dispatched ,
9821005 dispatched : alert. num_dispatched ,
@@ -1022,35 +1045,31 @@ async fn cmd_db_alert_list(
10221045}
10231046
10241047async fn cmd_db_alert_info (
1048+ opctx : & OpContext ,
10251049 datastore : & DataStore ,
10261050 fetch_opts : & DbFetchOptions ,
10271051 args : & AlertInfoArgs ,
10281052) -> anyhow:: Result < ( ) > {
10291053 let AlertInfoArgs { id } = args;
1030- let conn = datastore. pool_connection_for_tests ( ) . await ?;
1031-
1032- // Fetch the requested alert, including any corresponding
1033- // `rendezvous_alert_created` marker so we can display the generation at
1034- // which the alert was created. Note, all FM-created alerts can be
1035- // distinguished by their fm_case_id, but won't necessarily have a creation
1036- // marker: GC will clean up markers that are no longer needed.
1037- let ( alert, rendezvous_created) : ( Alert , Option < RendezvousAlertCreated > ) =
1038- alert_dsl:: alert
1039- . left_join ( rendezvous_created_dsl:: rendezvous_alert_created)
1040- . filter ( alert_dsl:: id. eq ( id. into_untyped_uuid ( ) ) )
1041- . select ( (
1042- Alert :: as_select ( ) ,
1043- Option :: < RendezvousAlertCreated > :: as_select ( ) ,
1044- ) )
1045- . limit ( 1 )
1046- . get_result_async ( & * conn)
1047- . await
1048- . optional ( )
1049- . with_context ( || format ! ( "loading alert {id}" ) ) ?
1050- . ok_or_else ( || anyhow:: anyhow!( "no alert {id} exists" ) ) ?;
1054+ let ( authz_alert, alert) = LookupPath :: new ( opctx, datastore)
1055+ . alert_id ( * id)
1056+ . fetch ( )
1057+ . await
1058+ . with_context ( || format ! ( "failed to look up alert {id}" ) ) ?;
1059+
1060+ // Fetch any corresponding `rendezvous_alert_created` marker so we can
1061+ // display the generation at which the alert was created. Note, all
1062+ // FM-created alerts can be distinguished by their fm_case_id, but won't
1063+ // necessarily have a creation marker: GC will clean up markers that are no
1064+ // longer needed.
1065+ let rendezvous_created =
1066+ datastore. alert_fetch_fm_rendezvous_gen ( opctx, & authz_alert) . await ;
1067+ if let Err ( ref e) = rendezvous_created {
1068+ eprintln ! ( "error: failed to fetch FM rendezvous marker: {e}" ) ;
1069+ }
10511070
10521071 let Alert {
1053- identity : db:: model:: AlertIdentity { id , time_created, time_modified } ,
1072+ identity : db:: model:: AlertIdentity { time_created, time_modified, .. } ,
10541073 time_dispatched,
10551074 class,
10561075 payload,
@@ -1100,19 +1119,23 @@ async fn cmd_db_alert_info(
11001119 ( Some ( case_id) , marker) => {
11011120 println ! ( " {PROVENANCE:>WIDTH$}: created by fault management" ) ;
11021121 println ! ( " {CASE_ID:>WIDTH$}: {case_id:?}" ) ;
1103- if let Some ( marker) = marker {
1104- println ! (
1105- " {FM_GENERATION:>WIDTH$}: {}" ,
1106- marker. created_at_generation. 0
1107- ) ;
1122+ match marker {
1123+ Ok ( Some ( marker) ) => {
1124+ println ! (
1125+ " {FM_GENERATION:>WIDTH$}: {}" ,
1126+ marker. created_at_generation. 0
1127+ ) ;
1128+ }
1129+ Ok ( None ) => { } // may have been GCed...
1130+ Err ( _) => {
1131+ println ! (
1132+ " {FM_GENERATION:>WIDTH$}: /!\\ UNKNOWN (failed to \
1133+ fetch marker record)"
1134+ ) ;
1135+ }
11081136 }
11091137 }
1110- ( None , None ) => {
1111- println ! (
1112- " {PROVENANCE:>WIDTH$}: not created by fault management"
1113- ) ;
1114- }
1115- ( None , Some ( marker) ) => {
1138+ ( None , Ok ( Some ( marker) ) ) => {
11161139 println ! (
11171140 " {PROVENANCE:>WIDTH$}: /!\\ WEIRD: creation marker present \
11181141 but no FM case (possible bug)"
@@ -1122,6 +1145,16 @@ async fn cmd_db_alert_info(
11221145 marker. created_at_generation. 0
11231146 ) ;
11241147 }
1148+ // Note that this includes both cases where we successfully fetched an
1149+ // `Ok(None)` marker record *and* cases where there was a database
1150+ // error. We already printed the db error earlier, so we need not print
1151+ // it again here, and we know the alert is not supposed to have one
1152+ // regardless.
1153+ ( None , _) => {
1154+ println ! (
1155+ " {PROVENANCE:>WIDTH$}: not created by fault management"
1156+ ) ;
1157+ }
11251158 }
11261159
11271160 println ! ( "\n {:=<80}" , "== ALERT PAYLOAD " ) ;
@@ -1130,11 +1163,14 @@ async fn cmd_db_alert_info(
11301163 ) ?;
11311164
11321165 let ctx = || format ! ( "listing deliveries for alert {id:?}" ) ;
1133- let deliveries = delivery_dsl:: webhook_delivery
1134- . limit ( fetch_opts. fetch_limit . get ( ) . into ( ) )
1135- . order_by ( delivery_dsl:: time_created. desc ( ) )
1136- . select ( WebhookDelivery :: as_select ( ) )
1137- . load_async ( & * conn)
1166+
1167+ let pagparams = DataPageParams {
1168+ // Descending order shows the most recent delivery first
1169+ direction : dropshot:: PaginationOrder :: Descending ,
1170+ ..first_page ( fetch_opts. fetch_limit )
1171+ } ;
1172+ let deliveries = datastore
1173+ . alert_list_webhook_deliveries ( opctx, & authz_alert, & pagparams)
11381174 . await
11391175 . with_context ( ctx) ?;
11401176
0 commit comments