From cf3aa300dc515b3cc75f43daa842d29dfe4c1dc2 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 9 Jan 2024 14:45:48 -0800 Subject: [PATCH] omdb support for these APIs --- dev-tools/omdb/src/bin/omdb/nexus.rs | 193 +++++++++++++ nexus/db-queries/src/db/datastore/zpool.rs | 27 +- nexus/deployment/src/planner.rs | 5 +- nexus/src/app/deployment.rs | 18 +- nexus/src/internal_api/http_entrypoints.rs | 6 +- nexus/types/src/deployment.rs | 16 +- openapi/nexus-internal.json | 314 +++++++++++++++++++++ 7 files changed, 550 insertions(+), 29 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/nexus.rs b/dev-tools/omdb/src/bin/omdb/nexus.rs index df5248b52d..6f4f832ccb 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus.rs @@ -11,6 +11,7 @@ use chrono::SecondsFormat; use chrono::Utc; use clap::Args; use clap::Subcommand; +use futures::TryStreamExt; use nexus_client::types::ActivationReason; use nexus_client::types::BackgroundTask; use nexus_client::types::CurrentStatus; @@ -36,6 +37,8 @@ pub struct NexusArgs { enum NexusCommands { /// print information about background tasks BackgroundTasks(BackgroundTasksArgs), + /// print information about blueprints + Blueprints(BlueprintsArgs), } #[derive(Debug, Args)] @@ -54,6 +57,34 @@ enum BackgroundTasksCommands { Show, } +#[derive(Debug, Args)] +struct BlueprintsArgs { + #[command(subcommand)] + command: BlueprintsCommands, +} + +#[derive(Debug, Subcommand)] +enum BlueprintsCommands { + /// List all blueprints + List, + /// Show a blueprint + Show(BlueprintIdArgs), + /// Delete a blueprint + Delete(BlueprintIdArgs), + /// Set the current target blueprint + SetTarget(BlueprintIdArgs), + /// Generate an initial blueprint from the current state + GenerateCurrent, + /// Generate a new blueprint + Regenerate, +} + +#[derive(Debug, Args)] +struct BlueprintIdArgs { + /// id of a blueprint + blueprint_id: Uuid, +} + impl NexusArgs { /// Run a `omdb nexus` subcommand. pub(crate) async fn run_cmd( @@ -93,6 +124,25 @@ impl NexusArgs { NexusCommands::BackgroundTasks(BackgroundTasksArgs { command: BackgroundTasksCommands::Show, }) => cmd_nexus_background_tasks_show(&client).await, + + NexusCommands::Blueprints(BlueprintsArgs { + command: BlueprintsCommands::List, + }) => cmd_nexus_blueprints_list(&client).await, + NexusCommands::Blueprints(BlueprintsArgs { + command: BlueprintsCommands::Show(args), + }) => cmd_nexus_blueprints_show(&client, args).await, + NexusCommands::Blueprints(BlueprintsArgs { + command: BlueprintsCommands::Delete(args), + }) => cmd_nexus_blueprints_delete(&client, args).await, + NexusCommands::Blueprints(BlueprintsArgs { + command: BlueprintsCommands::SetTarget(args), + }) => cmd_nexus_blueprints_set_target(&client, args).await, + NexusCommands::Blueprints(BlueprintsArgs { + command: BlueprintsCommands::Regenerate, + }) => cmd_nexus_blueprints_regenerate(&client).await, + NexusCommands::Blueprints(BlueprintsArgs { + command: BlueprintsCommands::GenerateCurrent, + }) => cmd_nexus_blueprints_generate_current(&client).await, } } } @@ -629,3 +679,146 @@ fn reason_code(reason: ActivationReason) -> char { ActivationReason::Timeout => 'T', } } + +async fn cmd_nexus_blueprints_list( + client: &nexus_client::Client, +) -> Result<(), anyhow::Error> { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct BlueprintRow { + #[tabled(rename = "T")] + is_target: &'static str, + id: String, + parent: String, + reason: String, + time_created: String, + } + + let target_id = client + .blueprint_target_view() + .await + .context("fetching current target blueprint")? + .into_inner() + .target_id; + let rows: Vec = client + .blueprint_list_stream(None, None) + .try_collect::>() + .await + .context("listing blueprints")? + .into_iter() + .map(|blueprint| { + let is_target = match target_id { + Some(target_id) if target_id == blueprint.id => "*", + _ => "", + }; + + BlueprintRow { + is_target, + id: blueprint.id.to_string(), + parent: blueprint + .parent_blueprint_id + .map(|s| s.to_string()) + .unwrap_or_else(|| String::from("")), + reason: blueprint.reason, + time_created: humantime::format_rfc3339_millis( + blueprint.time_created.into(), + ) + .to_string(), + } + }) + .collect(); + + 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_nexus_blueprints_show( + client: &nexus_client::Client, + args: &BlueprintIdArgs, +) -> Result<(), anyhow::Error> { + let blueprint = client + .blueprint_view(&args.blueprint_id) + .await + .with_context(|| format!("fetching blueprint {}", args.blueprint_id))?; + println!("blueprint {}", blueprint.id); + println!( + "parent: {}", + blueprint + .parent_blueprint_id + .map(|u| u.to_string()) + .unwrap_or_else(|| String::from("")) + ); + println!( + "created by {}{}", + blueprint.creator, + if blueprint.creator.parse::().is_ok() { + " (likely a Nexus instance)" + } else { + "" + } + ); + println!( + "created at {}", + humantime::format_rfc3339_millis(blueprint.time_created.into(),) + ); + println!("created for: {}", blueprint.reason,); + + Ok(()) +} + +async fn cmd_nexus_blueprints_delete( + client: &nexus_client::Client, + args: &BlueprintIdArgs, +) -> Result<(), anyhow::Error> { + let _ = client + .blueprint_delete(&args.blueprint_id) + .await + .with_context(|| format!("deleting blueprint {}", args.blueprint_id))?; + println!("blueprint {} deleted", args.blueprint_id); + Ok(()) +} + +// XXX-dap add "diff" command? + +async fn cmd_nexus_blueprints_set_target( + client: &nexus_client::Client, + args: &BlueprintIdArgs, +) -> Result<(), anyhow::Error> { + client + .blueprint_target_set(&nexus_client::types::BlueprintTargetSet { + target_id: args.blueprint_id, + // XXX-dap ideally keep existing value + enabled: true, + }) + .await + .with_context(|| { + format!("setting target to blueprint {}", args.blueprint_id) + })?; + eprintln!("set target blueprint to {}", args.blueprint_id); + Ok(()) +} + +async fn cmd_nexus_blueprints_generate_current( + client: &nexus_client::Client, +) -> Result<(), anyhow::Error> { + let blueprint = client + .blueprint_create_current() + .await + .context("creating blueprint from current state")?; + eprintln!("created blueprint {} from current state", blueprint.id); + Ok(()) +} + +async fn cmd_nexus_blueprints_regenerate( + client: &nexus_client::Client, +) -> Result<(), anyhow::Error> { + let blueprint = + client.blueprint_regenerate().await.context("generating blueprint")?; + eprintln!("generated new blueprint {}", blueprint.id); + Ok(()) +} diff --git a/nexus/db-queries/src/db/datastore/zpool.rs b/nexus/db-queries/src/db/datastore/zpool.rs index 6a4f2d441d..dd1014f02f 100644 --- a/nexus/db-queries/src/db/datastore/zpool.rs +++ b/nexus/db-queries/src/db/datastore/zpool.rs @@ -16,10 +16,14 @@ use crate::db::identity::Asset; use crate::db::model::Sled; use crate::db::model::Zpool; use crate::db::pagination::paginated; +use crate::db::queries::ALLOW_FULL_TABLE_SCAN_SQL; +use async_bb8_diesel::AsyncConnection; use async_bb8_diesel::AsyncRunQueryDsl; +use async_bb8_diesel::AsyncSimpleConnection; use chrono::Utc; use diesel::prelude::*; use diesel::upsert::excluded; +use nexus_db_model::PhysicalDiskKind; use omicron_common::api::external::CreateResult; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Error; @@ -27,7 +31,6 @@ use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupType; use omicron_common::api::external::ResourceType; use uuid::Uuid; -use nexus_db_model::PhysicalDiskKind; impl DataStore { /// Stores a new zpool in the database. @@ -113,17 +116,21 @@ impl DataStore { use db::schema::physical_disk::dsl as dsl_physical_disk; use db::schema::zpool::dsl as dsl_zpool; - paginated(dsl_zpool::zpool, dsl_zpool::id, pagparams) - .inner_join( - db::schema::physical_disk::table.on( + // XXX-dap shouldn't need to disable full scan? + let conn = self.pool_connection_authorized(opctx).await?; + conn.transaction_async(|conn| async move { + conn.batch_execute_async(ALLOW_FULL_TABLE_SCAN_SQL).await?; + paginated(dsl_zpool::zpool, dsl_zpool::id, pagparams) + .inner_join(db::schema::physical_disk::table.on( dsl_zpool::physical_disk_id.eq(dsl_physical_disk::id).and( dsl_physical_disk::variant.eq(PhysicalDiskKind::U2), ), - ), - ) - .select(Zpool::as_select()) - .load_async(&*self.pool_connection_authorized(opctx).await?) - .await - .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + )) + .select(Zpool::as_select()) + .load_async(&conn) + .await + }) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } } diff --git a/nexus/deployment/src/planner.rs b/nexus/deployment/src/planner.rs index 6f697d2a50..b3080d0db1 100644 --- a/nexus/deployment/src/planner.rs +++ b/nexus/deployment/src/planner.rs @@ -11,7 +11,6 @@ use crate::blueprint_builder::Error; use crate::blueprint_builder::SledInfo; use nexus_types::deployment::Blueprint; use nexus_types::deployment::OmicronZoneType; -use nexus_types::inventory::Collection; use slog::{info, Logger}; use std::collections::BTreeMap; use uuid::Uuid; @@ -19,7 +18,6 @@ use uuid::Uuid; pub struct Planner<'a> { log: Logger, parent_blueprint: &'a Blueprint, - collection: &'a Collection, sleds: &'a BTreeMap, blueprint: BlueprintBuilder<'a>, } @@ -28,7 +26,6 @@ impl<'a> Planner<'a> { pub fn new_based_on( log: Logger, parent_blueprint: &'a Blueprint, - collection: &'a Collection, sleds: &'a BTreeMap, creator: &str, reason: &str, @@ -39,7 +36,7 @@ impl<'a> Planner<'a> { creator, reason, ); - Planner { log, parent_blueprint, collection, sleds, blueprint } + Planner { log, parent_blueprint, sleds, blueprint } } pub fn plan(mut self) -> Result { diff --git a/nexus/src/app/deployment.rs b/nexus/src/app/deployment.rs index da237fe24e..53a952e063 100644 --- a/nexus/src/app/deployment.rs +++ b/nexus/src/app/deployment.rs @@ -55,18 +55,20 @@ impl super::Nexus { pub async fn blueprint_list( &self, _opctx: &OpContext, - _pagparams: &DataPageParams<'_, Uuid>, + pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { // XXX-dap authz check - // Since this is just temporary until we have a database impl, ignore - // pagination. Ok(self .blueprints .lock() .unwrap() .all_blueprints .values() - .cloned() + .filter_map(|f| match pagparams.marker { + None => Some(f.clone()), + Some(marker) if f.id > *marker => Some(f.clone()), + _ => None, + }) .collect()) } @@ -126,7 +128,7 @@ impl super::Nexus { pub async fn blueprint_target_set( &self, _opctx: &OpContext, - params: params::BlueprintTarget, + params: params::BlueprintTargetSet, ) -> Result { // XXX-dap authz check let new_target_id = params.target_id; @@ -175,7 +177,6 @@ impl super::Nexus { Error::unavail("no recent inventory collection available") })?; - // XXX-dap working here let sled_rows = { let mut all_sleds = Vec::new(); let mut paginator = Paginator::new(limit); @@ -235,7 +236,7 @@ impl super::Nexus { async fn blueprint_add( &self, - opctx: &OpContext, + _opctx: &OpContext, blueprint: Blueprint, ) -> Result<(), Error> { // XXX-dap authz check @@ -277,7 +278,7 @@ impl super::Nexus { let blueprints = self.blueprints.lock().unwrap(); let Some(target_id) = blueprints.target.target_id else { return Err(Error::conflict(&format!( - "cannot add sled before initial blueprint is created" + "cannot regenerate blueprint without existing target" ))); }; blueprints @@ -290,7 +291,6 @@ impl super::Nexus { let planner = Planner::new_based_on( opctx.log.clone(), &parent_blueprint, - &planning_context.collection, &planning_context.sleds, &planning_context.creator, // XXX-dap this "reason" was intended for the case where we know why diff --git a/nexus/src/internal_api/http_entrypoints.rs b/nexus/src/internal_api/http_entrypoints.rs index 13cbaec62f..332cf0f275 100644 --- a/nexus/src/internal_api/http_entrypoints.rs +++ b/nexus/src/internal_api/http_entrypoints.rs @@ -82,7 +82,7 @@ pub(crate) fn internal_api() -> NexusApiDescription { api.register(blueprint_target_view)?; api.register(blueprint_target_set)?; api.register(blueprint_create_current)?; - api.register(blueprint_create_add_sled)?; + api.register(blueprint_regenerate)?; Ok(()) } @@ -713,7 +713,7 @@ async fn blueprint_target_view( }] async fn blueprint_target_set( rqctx: RequestContext>, - target: TypedBody, + target: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); let handler = async { @@ -752,7 +752,7 @@ async fn blueprint_create_current( method = POST, path = "/deployment/blueprints/regenerate", }] -async fn blueprint_create_add_sled( +async fn blueprint_regenerate( rqctx: RequestContext>, ) -> Result, HttpError> { let apictx = rqctx.context(); diff --git a/nexus/types/src/deployment.rs b/nexus/types/src/deployment.rs index 6b8328f141..6a74c7830e 100644 --- a/nexus/types/src/deployment.rs +++ b/nexus/types/src/deployment.rs @@ -173,10 +173,20 @@ pub mod views { #[derive(Serialize, JsonSchema)] pub struct Blueprint { pub id: Uuid, + pub parent_blueprint_id: Option, + pub time_created: chrono::DateTime, + pub creator: String, + pub reason: String, } impl From for Blueprint { - fn from(value: super::Blueprint) -> Self { - todo!() // XXX-dap + fn from(generic: super::Blueprint) -> Self { + Blueprint { + id: generic.id, + parent_blueprint_id: generic.parent_blueprint_id, + time_created: generic.time_created, + creator: generic.creator, + reason: generic.reason, + } } } @@ -208,7 +218,7 @@ pub mod params { /// Specifies what blueprint, if any, the system should be working toward #[derive(Deserialize, JsonSchema)] - pub struct BlueprintTarget { + pub struct BlueprintTargetSet { pub target_id: Uuid, pub enabled: bool, } diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index a1d70d838b..1606c5f643 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -125,6 +125,230 @@ } } }, + "/deployment/blueprints/all": { + "get": { + "summary": "Lists blueprints", + "operationId": "blueprint_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlueprintResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/deployment/blueprints/all/{blueprint_id}": { + "get": { + "summary": "Fetches one blueprint", + "operationId": "blueprint_view", + "parameters": [ + { + "in": "path", + "name": "blueprint_id", + "description": "ID of the blueprint", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Blueprint" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Deletes one blueprint", + "operationId": "blueprint_delete", + "parameters": [ + { + "in": "path", + "name": "blueprint_id", + "description": "ID of the blueprint", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/deployment/blueprints/current": { + "post": { + "summary": "Generates a new blueprint matching the latest inventory collection", + "operationId": "blueprint_create_current", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Blueprint" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/deployment/blueprints/regenerate": { + "post": { + "summary": "Generates a new blueprint for the current system, re-evaluating anything", + "description": "that's changed since the last one was generated", + "operationId": "blueprint_regenerate", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Blueprint" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/deployment/blueprints/target": { + "get": { + "summary": "Fetches the current target blueprint, if any", + "operationId": "blueprint_target_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlueprintTarget" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Make the specified blueprint the new target", + "operationId": "blueprint_target_set", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlueprintTargetSet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlueprintTarget" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/disk/{disk_id}/remove-read-only-parent": { "post": { "summary": "Request removal of a read_only_parent from a disk", @@ -1844,6 +2068,96 @@ "range" ] }, + "Blueprint": { + "type": "object", + "properties": { + "creator": { + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "parent_blueprint_id": { + "nullable": true, + "type": "string", + "format": "uuid" + }, + "reason": { + "type": "string" + }, + "time_created": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "creator", + "id", + "reason", + "time_created" + ] + }, + "BlueprintResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Blueprint" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "BlueprintTarget": { + "description": "Describes what blueprint, if any, the system is currently working toward", + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "set_at": { + "type": "string", + "format": "date-time" + }, + "target_id": { + "nullable": true, + "type": "string", + "format": "uuid" + } + }, + "required": [ + "enabled", + "set_at" + ] + }, + "BlueprintTargetSet": { + "description": "Specifies what blueprint, if any, the system should be working toward", + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "target_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "enabled", + "target_id" + ] + }, "ByteCount": { "description": "Byte count to express memory or storage capacity.", "type": "integer",