// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use http::{HeaderMap, StatusCode}; use matchit::Params; use rustfs_policy::policy::action::{Action, AdminAction}; use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error}; use serde::Deserialize; use serde_urlencoded::from_bytes; use tokio_util::sync::CancellationToken; use tracing::warn; use crate::{ admin::{ auth::validate_admin_request, router::{AdminOperation, Operation, S3Router}, }, app::admin_usecase::{DefaultAdminUsecase, QueryPoolStatusRequest}, app::context::resolve_endpoints_handle, auth::{check_key_valid, get_session_token}, error::ApiError, server::{ADMIN_PREFIX, RemoteAddr}, }; use hyper::Method; use rustfs_ecstore::new_object_layer_fn; fn endpoints_from_context() -> Option { resolve_endpoints_handle() } pub fn register_pool_route(r: &mut S3Router) -> std::io::Result<()> { r.insert( Method::GET, format!("{}{}", ADMIN_PREFIX, "/v3/pools/list").as_str(), AdminOperation(&ListPools {}), )?; r.insert( Method::GET, format!("{}{}", ADMIN_PREFIX, "/v3/pools/status").as_str(), AdminOperation(&StatusPool {}), )?; r.insert( Method::POST, format!("{}{}", ADMIN_PREFIX, "/v3/pools/decommission").as_str(), AdminOperation(&StartDecommission {}), )?; r.insert( Method::POST, format!("{}{}", ADMIN_PREFIX, "/v3/pools/cancel").as_str(), AdminOperation(&CancelDecommission {}), )?; Ok(()) } pub struct ListPools {} #[async_trait::async_trait] impl Operation for ListPools { // GET //pools/list #[tracing::instrument(skip_all)] async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { warn!("handle ListPools"); let Some(input_cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")); }; let (cred, owner) = check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; validate_admin_request( &req.headers, &cred, owner, false, vec![ Action::AdminAction(AdminAction::ServerInfoAdminAction), Action::AdminAction(AdminAction::DecommissionAdminAction), ], req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), ) .await?; let usecase = DefaultAdminUsecase::from_global(); let pools_status = usecase.execute_list_pool_statuses().await.map_err(S3Error::from)?; let data = serde_json::to_vec(&pools_status) .map_err(|_e| S3Error::with_message(S3ErrorCode::InternalError, "parse accountInfo failed"))?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().unwrap()); Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) } } #[derive(Debug, Deserialize, Default)] #[serde(default)] pub struct StatusPoolQuery { pub pool: String, #[serde(rename = "by-id")] pub by_id: String, } pub struct StatusPool {} #[async_trait::async_trait] impl Operation for StatusPool { // GET //pools/status?pool=http://server{1...4}/disk{1...4} #[tracing::instrument(skip_all)] async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { warn!("handle StatusPool"); let Some(input_cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")); }; let (cred, owner) = check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; validate_admin_request( &req.headers, &cred, owner, false, vec![ Action::AdminAction(AdminAction::ServerInfoAdminAction), Action::AdminAction(AdminAction::DecommissionAdminAction), ], req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), ) .await?; let query = { if let Some(query) = req.uri.query() { let input: StatusPoolQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed"))?; input } else { StatusPoolQuery::default() } }; let usecase = DefaultAdminUsecase::from_global(); let pools_status = usecase .execute_query_pool_status(QueryPoolStatusRequest { pool: query.pool, by_id: query.by_id.as_str() == "true", }) .await .map_err(S3Error::from)?; let data = serde_json::to_vec(&pools_status) .map_err(|_e| S3Error::with_message(S3ErrorCode::InternalError, "parse accountInfo failed"))?; let mut header = HeaderMap::new(); header.insert(CONTENT_TYPE, "application/json".parse().unwrap()); Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header)) } } pub struct StartDecommission {} #[async_trait::async_trait] impl Operation for StartDecommission { // POST //pools/decommission?pool=http://server{1...4}/disk{1...4} #[tracing::instrument(skip_all)] async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { warn!("handle StartDecommission"); let Some(input_cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")); }; let (cred, owner) = check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; validate_admin_request( &req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::DecommissionAdminAction)], req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), ) .await?; let Some(endpoints) = endpoints_from_context() else { return Err(s3_error!(NotImplemented)); }; if endpoints.legacy() { return Err(s3_error!(NotImplemented)); } let Some(store) = new_object_layer_fn() else { return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); }; if store.is_decommission_running().await { return Err(S3Error::with_message( S3ErrorCode::InvalidRequest, "DecommissionAlreadyRunning".to_string(), )); } if store.is_rebalance_started().await { return Err(S3Error::with_message( S3ErrorCode::OperationAborted, "Decommission cannot be started, rebalance is already in progress".to_string(), )); } let query = { if let Some(query) = req.uri.query() { let input: StatusPoolQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed"))?; input } else { StatusPoolQuery::default() } }; let is_byid = query.by_id.as_str() == "true"; let pools: Vec<&str> = query.pool.split(",").collect(); let mut pools_indices = Vec::with_capacity(pools.len()); let ctx = CancellationToken::new(); for pool in pools.iter() { let idx = { if is_byid { pool.parse::() .map_err(|_e| s3_error!(InvalidArgument, "pool parse failed"))? } else { let Some(idx) = endpoints.get_pool_idx(pool) else { return Err(s3_error!(InvalidArgument, "pool parse failed")); }; idx } }; let mut has_found = None; for (i, pool) in store.pools.iter().enumerate() { if i == idx { has_found = Some(pool.clone()); break; } } let Some(_p) = has_found else { return Err(s3_error!(InvalidArgument)); }; pools_indices.push(idx); } if !pools_indices.is_empty() { store.decommission(ctx.clone(), pools_indices).await.map_err(ApiError::from)?; } Ok(S3Response::new((StatusCode::OK, Body::default()))) } } pub struct CancelDecommission {} #[async_trait::async_trait] impl Operation for CancelDecommission { // POST //pools/cancel?pool=http://server{1...4}/disk{1...4} #[tracing::instrument(skip_all)] async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { warn!("handle CancelDecommission"); let Some(input_cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")); }; let (cred, owner) = check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; validate_admin_request( &req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::DecommissionAdminAction)], req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)), ) .await?; let Some(endpoints) = endpoints_from_context() else { return Err(s3_error!(NotImplemented)); }; if endpoints.legacy() { return Err(s3_error!(NotImplemented)); } let query = { if let Some(query) = req.uri.query() { let input: StatusPoolQuery = from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed"))?; input } else { StatusPoolQuery::default() } }; let is_byid = query.by_id.as_str() == "true"; let has_idx = { if is_byid { let a = query.pool.parse::().unwrap_or_default(); if a < endpoints.as_ref().len() { Some(a) } else { None } } else { endpoints.get_pool_idx(&query.pool) } }; let Some(idx) = has_idx else { warn!("specified pool {} not found, please specify a valid pool", &query.pool); return Err(s3_error!(InvalidArgument)); }; let Some(store) = new_object_layer_fn() else { return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); }; store.decommission_cancel(idx).await.map_err(ApiError::from)?; Ok(S3Response::new((StatusCode::OK, Body::default()))) } }