LCOV - code coverage report
Current view: top level - control_plane/storcon_cli/src - main.rs (source / functions) Coverage Total Hit
Test: 42f947419473a288706e86ecdf7c2863d760d5d7.info Lines: 0.0 % 630 0
Test Date: 2024-08-02 21:34:27 Functions: 0.0 % 92 0

            Line data    Source code
       1              : use futures::StreamExt;
       2              : use std::{str::FromStr, time::Duration};
       3              : 
       4              : use clap::{Parser, Subcommand};
       5              : use pageserver_api::{
       6              :     controller_api::{
       7              :         NodeAvailabilityWrapper, NodeDescribeResponse, ShardSchedulingPolicy, TenantCreateRequest,
       8              :         TenantDescribeResponse, TenantPolicyRequest,
       9              :     },
      10              :     models::{
      11              :         EvictionPolicy, EvictionPolicyLayerAccessThreshold, LocationConfigSecondary,
      12              :         ShardParameters, TenantConfig, TenantConfigRequest, TenantShardSplitRequest,
      13              :         TenantShardSplitResponse,
      14              :     },
      15              :     shard::{ShardStripeSize, TenantShardId},
      16              : };
      17              : use pageserver_client::mgmt_api::{self};
      18              : use reqwest::{Method, StatusCode, Url};
      19              : use utils::id::{NodeId, TenantId};
      20              : 
      21              : use pageserver_api::controller_api::{
      22              :     NodeConfigureRequest, NodeRegisterRequest, NodeSchedulingPolicy, PlacementPolicy,
      23              :     TenantShardMigrateRequest, TenantShardMigrateResponse,
      24              : };
      25              : use storage_controller_client::control_api::Client;
      26              : 
      27            0 : #[derive(Subcommand, Debug)]
      28              : enum Command {
      29              :     /// Register a pageserver with the storage controller.  This shouldn't usually be necessary,
      30              :     /// since pageservers auto-register when they start up
      31              :     NodeRegister {
      32              :         #[arg(long)]
      33            0 :         node_id: NodeId,
      34              : 
      35              :         #[arg(long)]
      36            0 :         listen_pg_addr: String,
      37              :         #[arg(long)]
      38            0 :         listen_pg_port: u16,
      39              : 
      40              :         #[arg(long)]
      41            0 :         listen_http_addr: String,
      42              :         #[arg(long)]
      43            0 :         listen_http_port: u16,
      44              :     },
      45              : 
      46              :     /// Modify a node's configuration in the storage controller
      47              :     NodeConfigure {
      48              :         #[arg(long)]
      49            0 :         node_id: NodeId,
      50              : 
      51              :         /// Availability is usually auto-detected based on heartbeats.  Set 'offline' here to
      52              :         /// manually mark a node offline
      53              :         #[arg(long)]
      54              :         availability: Option<NodeAvailabilityArg>,
      55              :         /// Scheduling policy controls whether tenant shards may be scheduled onto this node.
      56              :         #[arg(long)]
      57              :         scheduling: Option<NodeSchedulingPolicy>,
      58              :     },
      59              :     NodeDelete {
      60              :         #[arg(long)]
      61            0 :         node_id: NodeId,
      62              :     },
      63              :     /// Modify a tenant's policies in the storage controller
      64              :     TenantPolicy {
      65              :         #[arg(long)]
      66            0 :         tenant_id: TenantId,
      67              :         /// Placement policy controls whether a tenant is `detached`, has only a secondary location (`secondary`),
      68              :         /// or is in the normal attached state with N secondary locations (`attached:N`)
      69              :         #[arg(long)]
      70              :         placement: Option<PlacementPolicyArg>,
      71              :         /// Scheduling policy enables pausing the controller's scheduling activity involving this tenant.  `active` is normal,
      72              :         /// `essential` disables optimization scheduling changes, `pause` disables all scheduling changes, and `stop` prevents
      73              :         /// all reconciliation activity including for scheduling changes already made.  `pause` and `stop` can make a tenant
      74              :         /// unavailable, and are only for use in emergencies.
      75              :         #[arg(long)]
      76              :         scheduling: Option<ShardSchedulingPolicyArg>,
      77              :     },
      78              :     /// List nodes known to the storage controller
      79              :     Nodes {},
      80              :     /// List tenants known to the storage controller
      81              :     Tenants {},
      82              :     /// Create a new tenant in the storage controller, and by extension on pageservers.
      83              :     TenantCreate {
      84              :         #[arg(long)]
      85            0 :         tenant_id: TenantId,
      86              :     },
      87              :     /// Delete a tenant in the storage controller, and by extension on pageservers.
      88              :     TenantDelete {
      89              :         #[arg(long)]
      90            0 :         tenant_id: TenantId,
      91              :     },
      92              :     /// Split an existing tenant into a higher number of shards than its current shard count.
      93              :     TenantShardSplit {
      94              :         #[arg(long)]
      95            0 :         tenant_id: TenantId,
      96              :         #[arg(long)]
      97            0 :         shard_count: u8,
      98              :         /// Optional, in 8kiB pages.  e.g. set 2048 for 16MB stripes.
      99              :         #[arg(long)]
     100              :         stripe_size: Option<u32>,
     101              :     },
     102              :     /// Migrate the attached location for a tenant shard to a specific pageserver.
     103              :     TenantShardMigrate {
     104              :         #[arg(long)]
     105            0 :         tenant_shard_id: TenantShardId,
     106              :         #[arg(long)]
     107            0 :         node: NodeId,
     108              :     },
     109              :     /// Modify the pageserver tenant configuration of a tenant: this is the configuration structure
     110              :     /// that is passed through to pageservers, and does not affect storage controller behavior.
     111              :     TenantConfig {
     112              :         #[arg(long)]
     113            0 :         tenant_id: TenantId,
     114              :         #[arg(long)]
     115            0 :         config: String,
     116              :     },
     117              :     /// Print details about a particular tenant, including all its shards' states.
     118              :     TenantDescribe {
     119              :         #[arg(long)]
     120            0 :         tenant_id: TenantId,
     121              :     },
     122              :     /// For a tenant which hasn't been onboarded to the storage controller yet, add it in secondary
     123              :     /// mode so that it can warm up content on a pageserver.
     124              :     TenantWarmup {
     125              :         #[arg(long)]
     126            0 :         tenant_id: TenantId,
     127              :     },
     128              :     /// Uncleanly drop a tenant from the storage controller: this doesn't delete anything from pageservers. Appropriate
     129              :     /// if you e.g. used `tenant-warmup` by mistake on a tenant ID that doesn't really exist, or is in some other region.
     130              :     TenantDrop {
     131              :         #[arg(long)]
     132            0 :         tenant_id: TenantId,
     133              :         #[arg(long)]
     134            0 :         unclean: bool,
     135              :     },
     136              :     NodeDrop {
     137              :         #[arg(long)]
     138            0 :         node_id: NodeId,
     139              :         #[arg(long)]
     140            0 :         unclean: bool,
     141              :     },
     142              :     TenantSetTimeBasedEviction {
     143              :         #[arg(long)]
     144            0 :         tenant_id: TenantId,
     145              :         #[arg(long)]
     146            0 :         period: humantime::Duration,
     147              :         #[arg(long)]
     148            0 :         threshold: humantime::Duration,
     149              :     },
     150              :     // Drain a set of specified pageservers by moving the primary attachments to pageservers
     151              :     // outside of the specified set.
     152              :     Drain {
     153              :         // Set of pageserver node ids to drain.
     154              :         #[arg(long)]
     155            0 :         nodes: Vec<NodeId>,
     156              :         // Optional: migration concurrency (default is 8)
     157              :         #[arg(long)]
     158              :         concurrency: Option<usize>,
     159              :         // Optional: maximum number of shards to migrate
     160              :         #[arg(long)]
     161              :         max_shards: Option<usize>,
     162              :         // Optional: when set to true, nothing is migrated, but the plan is printed to stdout
     163              :         #[arg(long)]
     164              :         dry_run: Option<bool>,
     165              :     },
     166              : }
     167              : 
     168            0 : #[derive(Parser)]
     169              : #[command(
     170              :     author,
     171              :     version,
     172              :     about,
     173              :     long_about = "CLI for Storage Controller Support/Debug"
     174              : )]
     175              : #[command(arg_required_else_help(true))]
     176              : struct Cli {
     177              :     #[arg(long)]
     178              :     /// URL to storage controller.  e.g. http://127.0.0.1:1234 when using `neon_local`
     179            0 :     api: Url,
     180              : 
     181              :     #[arg(long)]
     182              :     /// JWT token for authenticating with storage controller.  Depending on the API used, this
     183              :     /// should have either `pageserverapi` or `admin` scopes: for convenience, you should mint
     184              :     /// a token with both scopes to use with this tool.
     185              :     jwt: Option<String>,
     186              : 
     187              :     #[command(subcommand)]
     188              :     command: Command,
     189              : }
     190              : 
     191              : #[derive(Debug, Clone)]
     192              : struct PlacementPolicyArg(PlacementPolicy);
     193              : 
     194              : impl FromStr for PlacementPolicyArg {
     195              :     type Err = anyhow::Error;
     196              : 
     197            0 :     fn from_str(s: &str) -> Result<Self, Self::Err> {
     198            0 :         match s {
     199            0 :             "detached" => Ok(Self(PlacementPolicy::Detached)),
     200            0 :             "secondary" => Ok(Self(PlacementPolicy::Secondary)),
     201            0 :             _ if s.starts_with("attached:") => {
     202            0 :                 let mut splitter = s.split(':');
     203            0 :                 let _prefix = splitter.next().unwrap();
     204            0 :                 match splitter.next().and_then(|s| s.parse::<usize>().ok()) {
     205            0 :                     Some(n) => Ok(Self(PlacementPolicy::Attached(n))),
     206            0 :                     None => Err(anyhow::anyhow!(
     207            0 :                         "Invalid format '{s}', a valid example is 'attached:1'"
     208            0 :                     )),
     209              :                 }
     210              :             }
     211            0 :             _ => Err(anyhow::anyhow!(
     212            0 :                 "Unknown placement policy '{s}', try detached,secondary,attached:<n>"
     213            0 :             )),
     214              :         }
     215            0 :     }
     216              : }
     217              : 
     218              : #[derive(Debug, Clone)]
     219              : struct ShardSchedulingPolicyArg(ShardSchedulingPolicy);
     220              : 
     221              : impl FromStr for ShardSchedulingPolicyArg {
     222              :     type Err = anyhow::Error;
     223              : 
     224            0 :     fn from_str(s: &str) -> Result<Self, Self::Err> {
     225            0 :         match s {
     226            0 :             "active" => Ok(Self(ShardSchedulingPolicy::Active)),
     227            0 :             "essential" => Ok(Self(ShardSchedulingPolicy::Essential)),
     228            0 :             "pause" => Ok(Self(ShardSchedulingPolicy::Pause)),
     229            0 :             "stop" => Ok(Self(ShardSchedulingPolicy::Stop)),
     230            0 :             _ => Err(anyhow::anyhow!(
     231            0 :                 "Unknown scheduling policy '{s}', try active,essential,pause,stop"
     232            0 :             )),
     233              :         }
     234            0 :     }
     235              : }
     236              : 
     237              : #[derive(Debug, Clone)]
     238              : struct NodeAvailabilityArg(NodeAvailabilityWrapper);
     239              : 
     240              : impl FromStr for NodeAvailabilityArg {
     241              :     type Err = anyhow::Error;
     242              : 
     243            0 :     fn from_str(s: &str) -> Result<Self, Self::Err> {
     244            0 :         match s {
     245            0 :             "active" => Ok(Self(NodeAvailabilityWrapper::Active)),
     246            0 :             "offline" => Ok(Self(NodeAvailabilityWrapper::Offline)),
     247            0 :             _ => Err(anyhow::anyhow!("Unknown availability state '{s}'")),
     248              :         }
     249            0 :     }
     250              : }
     251              : 
     252              : #[tokio::main]
     253            0 : async fn main() -> anyhow::Result<()> {
     254            0 :     let cli = Cli::parse();
     255            0 : 
     256            0 :     let storcon_client = Client::new(cli.api.clone(), cli.jwt.clone());
     257            0 : 
     258            0 :     let mut trimmed = cli.api.to_string();
     259            0 :     trimmed.pop();
     260            0 :     let vps_client = mgmt_api::Client::new(trimmed, cli.jwt.as_deref());
     261            0 : 
     262            0 :     match cli.command {
     263            0 :         Command::NodeRegister {
     264            0 :             node_id,
     265            0 :             listen_pg_addr,
     266            0 :             listen_pg_port,
     267            0 :             listen_http_addr,
     268            0 :             listen_http_port,
     269            0 :         } => {
     270            0 :             storcon_client
     271            0 :                 .dispatch::<_, ()>(
     272            0 :                     Method::POST,
     273            0 :                     "control/v1/node".to_string(),
     274            0 :                     Some(NodeRegisterRequest {
     275            0 :                         node_id,
     276            0 :                         listen_pg_addr,
     277            0 :                         listen_pg_port,
     278            0 :                         listen_http_addr,
     279            0 :                         listen_http_port,
     280            0 :                     }),
     281            0 :                 )
     282            0 :                 .await?;
     283            0 :         }
     284            0 :         Command::TenantCreate { tenant_id } => {
     285            0 :             storcon_client
     286            0 :                 .dispatch::<_, ()>(
     287            0 :                     Method::POST,
     288            0 :                     "v1/tenant".to_string(),
     289            0 :                     Some(TenantCreateRequest {
     290            0 :                         new_tenant_id: TenantShardId::unsharded(tenant_id),
     291            0 :                         generation: None,
     292            0 :                         shard_parameters: ShardParameters::default(),
     293            0 :                         placement_policy: Some(PlacementPolicy::Attached(1)),
     294            0 :                         config: TenantConfig::default(),
     295            0 :                     }),
     296            0 :                 )
     297            0 :                 .await?;
     298            0 :         }
     299            0 :         Command::TenantDelete { tenant_id } => {
     300            0 :             let status = vps_client
     301            0 :                 .tenant_delete(TenantShardId::unsharded(tenant_id))
     302            0 :                 .await?;
     303            0 :             tracing::info!("Delete status: {}", status);
     304            0 :         }
     305            0 :         Command::Nodes {} => {
     306            0 :             let mut resp = storcon_client
     307            0 :                 .dispatch::<(), Vec<NodeDescribeResponse>>(
     308            0 :                     Method::GET,
     309            0 :                     "control/v1/node".to_string(),
     310            0 :                     None,
     311            0 :                 )
     312            0 :                 .await?;
     313            0 : 
     314            0 :             resp.sort_by(|a, b| a.listen_http_addr.cmp(&b.listen_http_addr));
     315            0 : 
     316            0 :             let mut table = comfy_table::Table::new();
     317            0 :             table.set_header(["Id", "Hostname", "Scheduling", "Availability"]);
     318            0 :             for node in resp {
     319            0 :                 table.add_row([
     320            0 :                     format!("{}", node.id),
     321            0 :                     node.listen_http_addr,
     322            0 :                     format!("{:?}", node.scheduling),
     323            0 :                     format!("{:?}", node.availability),
     324            0 :                 ]);
     325            0 :             }
     326            0 :             println!("{table}");
     327            0 :         }
     328            0 :         Command::NodeConfigure {
     329            0 :             node_id,
     330            0 :             availability,
     331            0 :             scheduling,
     332            0 :         } => {
     333            0 :             let req = NodeConfigureRequest {
     334            0 :                 node_id,
     335            0 :                 availability: availability.map(|a| a.0),
     336            0 :                 scheduling,
     337            0 :             };
     338            0 :             storcon_client
     339            0 :                 .dispatch::<_, ()>(
     340            0 :                     Method::PUT,
     341            0 :                     format!("control/v1/node/{node_id}/config"),
     342            0 :                     Some(req),
     343            0 :                 )
     344            0 :                 .await?;
     345            0 :         }
     346            0 :         Command::Tenants {} => {
     347            0 :             let mut resp = storcon_client
     348            0 :                 .dispatch::<(), Vec<TenantDescribeResponse>>(
     349            0 :                     Method::GET,
     350            0 :                     "control/v1/tenant".to_string(),
     351            0 :                     None,
     352            0 :                 )
     353            0 :                 .await?;
     354            0 : 
     355            0 :             resp.sort_by(|a, b| a.tenant_id.cmp(&b.tenant_id));
     356            0 : 
     357            0 :             let mut table = comfy_table::Table::new();
     358            0 :             table.set_header([
     359            0 :                 "TenantId",
     360            0 :                 "ShardCount",
     361            0 :                 "StripeSize",
     362            0 :                 "Placement",
     363            0 :                 "Scheduling",
     364            0 :             ]);
     365            0 :             for tenant in resp {
     366            0 :                 let shard_zero = tenant.shards.into_iter().next().unwrap();
     367            0 :                 table.add_row([
     368            0 :                     format!("{}", tenant.tenant_id),
     369            0 :                     format!("{}", shard_zero.tenant_shard_id.shard_count.literal()),
     370            0 :                     format!("{:?}", tenant.stripe_size),
     371            0 :                     format!("{:?}", tenant.policy),
     372            0 :                     format!("{:?}", shard_zero.scheduling_policy),
     373            0 :                 ]);
     374            0 :             }
     375            0 : 
     376            0 :             println!("{table}");
     377            0 :         }
     378            0 :         Command::TenantPolicy {
     379            0 :             tenant_id,
     380            0 :             placement,
     381            0 :             scheduling,
     382            0 :         } => {
     383            0 :             let req = TenantPolicyRequest {
     384            0 :                 scheduling: scheduling.map(|s| s.0),
     385            0 :                 placement: placement.map(|p| p.0),
     386            0 :             };
     387            0 :             storcon_client
     388            0 :                 .dispatch::<_, ()>(
     389            0 :                     Method::PUT,
     390            0 :                     format!("control/v1/tenant/{tenant_id}/policy"),
     391            0 :                     Some(req),
     392            0 :                 )
     393            0 :                 .await?;
     394            0 :         }
     395            0 :         Command::TenantShardSplit {
     396            0 :             tenant_id,
     397            0 :             shard_count,
     398            0 :             stripe_size,
     399            0 :         } => {
     400            0 :             let req = TenantShardSplitRequest {
     401            0 :                 new_shard_count: shard_count,
     402            0 :                 new_stripe_size: stripe_size.map(ShardStripeSize),
     403            0 :             };
     404            0 : 
     405            0 :             let response = storcon_client
     406            0 :                 .dispatch::<TenantShardSplitRequest, TenantShardSplitResponse>(
     407            0 :                     Method::PUT,
     408            0 :                     format!("control/v1/tenant/{tenant_id}/shard_split"),
     409            0 :                     Some(req),
     410            0 :                 )
     411            0 :                 .await?;
     412            0 :             println!(
     413            0 :                 "Split tenant {} into {} shards: {}",
     414            0 :                 tenant_id,
     415            0 :                 shard_count,
     416            0 :                 response
     417            0 :                     .new_shards
     418            0 :                     .iter()
     419            0 :                     .map(|s| format!("{:?}", s))
     420            0 :                     .collect::<Vec<_>>()
     421            0 :                     .join(",")
     422            0 :             );
     423            0 :         }
     424            0 :         Command::TenantShardMigrate {
     425            0 :             tenant_shard_id,
     426            0 :             node,
     427            0 :         } => {
     428            0 :             let req = TenantShardMigrateRequest {
     429            0 :                 tenant_shard_id,
     430            0 :                 node_id: node,
     431            0 :             };
     432            0 : 
     433            0 :             storcon_client
     434            0 :                 .dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
     435            0 :                     Method::PUT,
     436            0 :                     format!("control/v1/tenant/{tenant_shard_id}/migrate"),
     437            0 :                     Some(req),
     438            0 :                 )
     439            0 :                 .await?;
     440            0 :         }
     441            0 :         Command::TenantConfig { tenant_id, config } => {
     442            0 :             let tenant_conf = serde_json::from_str(&config)?;
     443            0 : 
     444            0 :             vps_client
     445            0 :                 .tenant_config(&TenantConfigRequest {
     446            0 :                     tenant_id,
     447            0 :                     config: tenant_conf,
     448            0 :                 })
     449            0 :                 .await?;
     450            0 :         }
     451            0 :         Command::TenantDescribe { tenant_id } => {
     452            0 :             let describe_response = storcon_client
     453            0 :                 .dispatch::<(), TenantDescribeResponse>(
     454            0 :                     Method::GET,
     455            0 :                     format!("control/v1/tenant/{tenant_id}"),
     456            0 :                     None,
     457            0 :                 )
     458            0 :                 .await?;
     459            0 :             let shards = describe_response.shards;
     460            0 :             let mut table = comfy_table::Table::new();
     461            0 :             table.set_header(["Shard", "Attached", "Secondary", "Last error", "status"]);
     462            0 :             for shard in shards {
     463            0 :                 let secondary = shard
     464            0 :                     .node_secondary
     465            0 :                     .iter()
     466            0 :                     .map(|n| format!("{}", n))
     467            0 :                     .collect::<Vec<_>>()
     468            0 :                     .join(",");
     469            0 : 
     470            0 :                 let mut status_parts = Vec::new();
     471            0 :                 if shard.is_reconciling {
     472            0 :                     status_parts.push("reconciling");
     473            0 :                 }
     474            0 : 
     475            0 :                 if shard.is_pending_compute_notification {
     476            0 :                     status_parts.push("pending_compute");
     477            0 :                 }
     478            0 : 
     479            0 :                 if shard.is_splitting {
     480            0 :                     status_parts.push("splitting");
     481            0 :                 }
     482            0 :                 let status = status_parts.join(",");
     483            0 : 
     484            0 :                 table.add_row([
     485            0 :                     format!("{}", shard.tenant_shard_id),
     486            0 :                     shard
     487            0 :                         .node_attached
     488            0 :                         .map(|n| format!("{}", n))
     489            0 :                         .unwrap_or(String::new()),
     490            0 :                     secondary,
     491            0 :                     shard.last_error,
     492            0 :                     status,
     493            0 :                 ]);
     494            0 :             }
     495            0 :             println!("{table}");
     496            0 :         }
     497            0 :         Command::TenantWarmup { tenant_id } => {
     498            0 :             let describe_response = storcon_client
     499            0 :                 .dispatch::<(), TenantDescribeResponse>(
     500            0 :                     Method::GET,
     501            0 :                     format!("control/v1/tenant/{tenant_id}"),
     502            0 :                     None,
     503            0 :                 )
     504            0 :                 .await;
     505            0 :             match describe_response {
     506            0 :                 Ok(describe) => {
     507            0 :                     if matches!(describe.policy, PlacementPolicy::Secondary) {
     508            0 :                         // Fine: it's already known to controller in secondary mode: calling
     509            0 :                         // again to put it into secondary mode won't cause problems.
     510            0 :                     } else {
     511            0 :                         anyhow::bail!("Tenant already present with policy {:?}", describe.policy);
     512            0 :                     }
     513            0 :                 }
     514            0 :                 Err(mgmt_api::Error::ApiError(StatusCode::NOT_FOUND, _)) => {
     515            0 :                     // Fine: this tenant isn't know to the storage controller yet.
     516            0 :                 }
     517            0 :                 Err(e) => {
     518            0 :                     // Unexpected API error
     519            0 :                     return Err(e.into());
     520            0 :                 }
     521            0 :             }
     522            0 : 
     523            0 :             vps_client
     524            0 :                 .location_config(
     525            0 :                     TenantShardId::unsharded(tenant_id),
     526            0 :                     pageserver_api::models::LocationConfig {
     527            0 :                         mode: pageserver_api::models::LocationConfigMode::Secondary,
     528            0 :                         generation: None,
     529            0 :                         secondary_conf: Some(LocationConfigSecondary { warm: true }),
     530            0 :                         shard_number: 0,
     531            0 :                         shard_count: 0,
     532            0 :                         shard_stripe_size: ShardParameters::DEFAULT_STRIPE_SIZE.0,
     533            0 :                         tenant_conf: TenantConfig::default(),
     534            0 :                     },
     535            0 :                     None,
     536            0 :                     true,
     537            0 :                 )
     538            0 :                 .await?;
     539            0 : 
     540            0 :             let describe_response = storcon_client
     541            0 :                 .dispatch::<(), TenantDescribeResponse>(
     542            0 :                     Method::GET,
     543            0 :                     format!("control/v1/tenant/{tenant_id}"),
     544            0 :                     None,
     545            0 :                 )
     546            0 :                 .await?;
     547            0 : 
     548            0 :             let secondary_ps_id = describe_response
     549            0 :                 .shards
     550            0 :                 .first()
     551            0 :                 .unwrap()
     552            0 :                 .node_secondary
     553            0 :                 .first()
     554            0 :                 .unwrap();
     555            0 : 
     556            0 :             println!("Tenant {tenant_id} warming up on pageserver {secondary_ps_id}");
     557            0 :             loop {
     558            0 :                 let (status, progress) = vps_client
     559            0 :                     .tenant_secondary_download(
     560            0 :                         TenantShardId::unsharded(tenant_id),
     561            0 :                         Some(Duration::from_secs(10)),
     562            0 :                     )
     563            0 :                     .await?;
     564            0 :                 println!(
     565            0 :                     "Progress: {}/{} layers, {}/{} bytes",
     566            0 :                     progress.layers_downloaded,
     567            0 :                     progress.layers_total,
     568            0 :                     progress.bytes_downloaded,
     569            0 :                     progress.bytes_total
     570            0 :                 );
     571            0 :                 match status {
     572            0 :                     StatusCode::OK => {
     573            0 :                         println!("Download complete");
     574            0 :                         break;
     575            0 :                     }
     576            0 :                     StatusCode::ACCEPTED => {
     577            0 :                         // Loop
     578            0 :                     }
     579            0 :                     _ => {
     580            0 :                         anyhow::bail!("Unexpected download status: {status}");
     581            0 :                     }
     582            0 :                 }
     583            0 :             }
     584            0 :         }
     585            0 :         Command::TenantDrop { tenant_id, unclean } => {
     586            0 :             if !unclean {
     587            0 :                 anyhow::bail!("This command is not a tenant deletion, and uncleanly drops all controller state for the tenant.  If you know what you're doing, add `--unclean` to proceed.")
     588            0 :             }
     589            0 :             storcon_client
     590            0 :                 .dispatch::<(), ()>(
     591            0 :                     Method::POST,
     592            0 :                     format!("debug/v1/tenant/{tenant_id}/drop"),
     593            0 :                     None,
     594            0 :                 )
     595            0 :                 .await?;
     596            0 :         }
     597            0 :         Command::NodeDrop { node_id, unclean } => {
     598            0 :             if !unclean {
     599            0 :                 anyhow::bail!("This command is not a clean node decommission, and uncleanly drops all controller state for the node, without checking if any tenants still refer to it.  If you know what you're doing, add `--unclean` to proceed.")
     600            0 :             }
     601            0 :             storcon_client
     602            0 :                 .dispatch::<(), ()>(Method::POST, format!("debug/v1/node/{node_id}/drop"), None)
     603            0 :                 .await?;
     604            0 :         }
     605            0 :         Command::NodeDelete { node_id } => {
     606            0 :             storcon_client
     607            0 :                 .dispatch::<(), ()>(Method::DELETE, format!("control/v1/node/{node_id}"), None)
     608            0 :                 .await?;
     609            0 :         }
     610            0 :         Command::TenantSetTimeBasedEviction {
     611            0 :             tenant_id,
     612            0 :             period,
     613            0 :             threshold,
     614            0 :         } => {
     615            0 :             vps_client
     616            0 :                 .tenant_config(&TenantConfigRequest {
     617            0 :                     tenant_id,
     618            0 :                     config: TenantConfig {
     619            0 :                         eviction_policy: Some(EvictionPolicy::LayerAccessThreshold(
     620            0 :                             EvictionPolicyLayerAccessThreshold {
     621            0 :                                 period: period.into(),
     622            0 :                                 threshold: threshold.into(),
     623            0 :                             },
     624            0 :                         )),
     625            0 :                         ..Default::default()
     626            0 :                     },
     627            0 :                 })
     628            0 :                 .await?;
     629            0 :         }
     630            0 :         Command::Drain {
     631            0 :             nodes,
     632            0 :             concurrency,
     633            0 :             max_shards,
     634            0 :             dry_run,
     635            0 :         } => {
     636            0 :             // Load the list of nodes, split them up into the drained and filled sets,
     637            0 :             // and validate that draining is possible.
     638            0 :             let node_descs = storcon_client
     639            0 :                 .dispatch::<(), Vec<NodeDescribeResponse>>(
     640            0 :                     Method::GET,
     641            0 :                     "control/v1/node".to_string(),
     642            0 :                     None,
     643            0 :                 )
     644            0 :                 .await?;
     645            0 : 
     646            0 :             let mut node_to_drain_descs = Vec::new();
     647            0 :             let mut node_to_fill_descs = Vec::new();
     648            0 : 
     649            0 :             for desc in node_descs {
     650            0 :                 let to_drain = nodes.iter().any(|id| *id == desc.id);
     651            0 :                 if to_drain {
     652            0 :                     node_to_drain_descs.push(desc);
     653            0 :                 } else {
     654            0 :                     node_to_fill_descs.push(desc);
     655            0 :                 }
     656            0 :             }
     657            0 : 
     658            0 :             if nodes.len() != node_to_drain_descs.len() {
     659            0 :                 anyhow::bail!("Drain requested for node which doesn't exist.")
     660            0 :             }
     661            0 : 
     662            0 :             node_to_fill_descs.retain(|desc| {
     663            0 :                 matches!(desc.availability, NodeAvailabilityWrapper::Active)
     664            0 :                     && matches!(
     665            0 :                         desc.scheduling,
     666            0 :                         NodeSchedulingPolicy::Active | NodeSchedulingPolicy::Filling
     667            0 :                     )
     668            0 :             });
     669            0 : 
     670            0 :             if node_to_fill_descs.is_empty() {
     671            0 :                 anyhow::bail!("There are no nodes to drain to")
     672            0 :             }
     673            0 : 
     674            0 :             // Set the node scheduling policy to draining for the nodes which
     675            0 :             // we plan to drain.
     676            0 :             for node_desc in node_to_drain_descs.iter() {
     677            0 :                 let req = NodeConfigureRequest {
     678            0 :                     node_id: node_desc.id,
     679            0 :                     availability: None,
     680            0 :                     scheduling: Some(NodeSchedulingPolicy::Draining),
     681            0 :                 };
     682            0 : 
     683            0 :                 storcon_client
     684            0 :                     .dispatch::<_, ()>(
     685            0 :                         Method::PUT,
     686            0 :                         format!("control/v1/node/{}/config", node_desc.id),
     687            0 :                         Some(req),
     688            0 :                     )
     689            0 :                     .await?;
     690            0 :             }
     691            0 : 
     692            0 :             // Perform the drain: move each tenant shard scheduled on a node to
     693            0 :             // be drained to a node which is being filled. A simple round robin
     694            0 :             // strategy is used to pick the new node.
     695            0 :             let tenants = storcon_client
     696            0 :                 .dispatch::<(), Vec<TenantDescribeResponse>>(
     697            0 :                     Method::GET,
     698            0 :                     "control/v1/tenant".to_string(),
     699            0 :                     None,
     700            0 :                 )
     701            0 :                 .await?;
     702            0 : 
     703            0 :             let mut selected_node_idx = 0;
     704            0 : 
     705            0 :             struct DrainMove {
     706            0 :                 tenant_shard_id: TenantShardId,
     707            0 :                 from: NodeId,
     708            0 :                 to: NodeId,
     709            0 :             }
     710            0 : 
     711            0 :             let mut moves: Vec<DrainMove> = Vec::new();
     712            0 : 
     713            0 :             let shards = tenants
     714            0 :                 .into_iter()
     715            0 :                 .flat_map(|tenant| tenant.shards.into_iter());
     716            0 :             for shard in shards {
     717            0 :                 if let Some(max_shards) = max_shards {
     718            0 :                     if moves.len() >= max_shards {
     719            0 :                         println!(
     720            0 :                             "Stop planning shard moves since the requested maximum was reached"
     721            0 :                         );
     722            0 :                         break;
     723            0 :                     }
     724            0 :                 }
     725            0 : 
     726            0 :                 let should_migrate = {
     727            0 :                     if let Some(attached_to) = shard.node_attached {
     728            0 :                         node_to_drain_descs
     729            0 :                             .iter()
     730            0 :                             .map(|desc| desc.id)
     731            0 :                             .any(|id| id == attached_to)
     732            0 :                     } else {
     733            0 :                         false
     734            0 :                     }
     735            0 :                 };
     736            0 : 
     737            0 :                 if !should_migrate {
     738            0 :                     continue;
     739            0 :                 }
     740            0 : 
     741            0 :                 moves.push(DrainMove {
     742            0 :                     tenant_shard_id: shard.tenant_shard_id,
     743            0 :                     from: shard
     744            0 :                         .node_attached
     745            0 :                         .expect("We only migrate attached tenant shards"),
     746            0 :                     to: node_to_fill_descs[selected_node_idx].id,
     747            0 :                 });
     748            0 :                 selected_node_idx = (selected_node_idx + 1) % node_to_fill_descs.len();
     749            0 :             }
     750            0 : 
     751            0 :             let total_moves = moves.len();
     752            0 : 
     753            0 :             if dry_run == Some(true) {
     754            0 :                 println!("Dryrun requested. Planned {total_moves} moves:");
     755            0 :                 for mv in &moves {
     756            0 :                     println!("{}: {} -> {}", mv.tenant_shard_id, mv.from, mv.to)
     757            0 :                 }
     758            0 : 
     759            0 :                 return Ok(());
     760            0 :             }
     761            0 : 
     762            0 :             const DEFAULT_MIGRATE_CONCURRENCY: usize = 8;
     763            0 :             let mut stream = futures::stream::iter(moves)
     764            0 :                 .map(|mv| {
     765            0 :                     let client = Client::new(cli.api.clone(), cli.jwt.clone());
     766            0 :                     async move {
     767            0 :                         client
     768            0 :                             .dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
     769            0 :                                 Method::PUT,
     770            0 :                                 format!("control/v1/tenant/{}/migrate", mv.tenant_shard_id),
     771            0 :                                 Some(TenantShardMigrateRequest {
     772            0 :                                     tenant_shard_id: mv.tenant_shard_id,
     773            0 :                                     node_id: mv.to,
     774            0 :                                 }),
     775            0 :                             )
     776            0 :                             .await
     777            0 :                             .map_err(|e| (mv.tenant_shard_id, mv.from, mv.to, e))
     778            0 :                     }
     779            0 :                 })
     780            0 :                 .buffered(concurrency.unwrap_or(DEFAULT_MIGRATE_CONCURRENCY));
     781            0 : 
     782            0 :             let mut success = 0;
     783            0 :             let mut failure = 0;
     784            0 : 
     785            0 :             while let Some(res) = stream.next().await {
     786            0 :                 match res {
     787            0 :                     Ok(_) => {
     788            0 :                         success += 1;
     789            0 :                     }
     790            0 :                     Err((tenant_shard_id, from, to, error)) => {
     791            0 :                         failure += 1;
     792            0 :                         println!(
     793            0 :                             "Failed to migrate {} from node {} to node {}: {}",
     794            0 :                             tenant_shard_id, from, to, error
     795            0 :                         );
     796            0 :                     }
     797            0 :                 }
     798            0 : 
     799            0 :                 if (success + failure) % 20 == 0 {
     800            0 :                     println!(
     801            0 :                         "Processed {}/{} shards: {} succeeded, {} failed",
     802            0 :                         success + failure,
     803            0 :                         total_moves,
     804            0 :                         success,
     805            0 :                         failure
     806            0 :                     );
     807            0 :                 }
     808            0 :             }
     809            0 : 
     810            0 :             println!(
     811            0 :                 "Processed {}/{} shards: {} succeeded, {} failed",
     812            0 :                 success + failure,
     813            0 :                 total_moves,
     814            0 :                 success,
     815            0 :                 failure
     816            0 :             );
     817            0 :         }
     818            0 :     }
     819            0 : 
     820            0 :     Ok(())
     821            0 : }
        

Generated by: LCOV version 2.1-beta