mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 13:27:43 +00:00
351 lines
12 KiB
Rust
351 lines
12 KiB
Rust
// 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<rustfs_ecstore::endpoints::EndpointServerPools> {
|
|
resolve_endpoints_handle()
|
|
}
|
|
|
|
pub fn register_pool_route(r: &mut S3Router<AdminOperation>) -> 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 <endpoint>/<admin-API>/pools/list
|
|
#[tracing::instrument(skip_all)]
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
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::<Option<RemoteAddr>>().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 <endpoint>/<admin-API>/pools/status?pool=http://server{1...4}/disk{1...4}
|
|
#[tracing::instrument(skip_all)]
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
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::<Option<RemoteAddr>>().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 <endpoint>/<admin-API>/pools/decommission?pool=http://server{1...4}/disk{1...4}
|
|
#[tracing::instrument(skip_all)]
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
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::<Option<RemoteAddr>>().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::<usize>()
|
|
.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 <endpoint>/<admin-API>/pools/cancel?pool=http://server{1...4}/disk{1...4}
|
|
#[tracing::instrument(skip_all)]
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
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::<Option<RemoteAddr>>().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::<usize>().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())))
|
|
}
|
|
}
|