move admin router to rustfs, add pool stats/list admin api

This commit is contained in:
weisd
2024-11-26 17:34:18 +08:00
parent b1510d1107
commit b79b967036
16 changed files with 295 additions and 103 deletions
+2 -1
View File
@@ -53,11 +53,12 @@ transform-stream.workspace = true
uuid = "1.11.0"
admin = { path = "../api/admin" }
axum.workspace = true
router = { version = "0.0.1", path = "../router" }
matchit = "0.8.4"
shadow-rs = "0.35.2"
const-str = { version = "0.5.7", features = ["std", "proc"] }
atoi = "2.0.0"
serde.workspace = true
serde_urlencoded = "0.7.1"
[build-dependencies]
prost-build.workspace = true
+464
View File
@@ -0,0 +1,464 @@
use super::router::Operation;
use crate::storage::error::to_s3_error;
use ecstore::bucket::policy::action::{Action, ActionSet};
use ecstore::bucket::policy::bucket_policy::{BPStatement, BucketPolicy};
use ecstore::bucket::policy::effect::Effect;
use ecstore::bucket::policy::resource::{Resource, ResourceSet};
use ecstore::store_api::StorageAPI;
use ecstore::utils::xml;
use ecstore::GLOBAL_Endpoints;
use ecstore::{new_object_layer_fn, store_api::BackendInfo};
use hyper::StatusCode;
use matchit::Params;
use s3s::S3ErrorCode;
use s3s::{
dto::{AssumeRoleOutput, Credentials, Timestamp},
s3_error, Body, S3Error, S3Request, S3Response, S3Result,
};
use serde::{Deserialize, Serialize};
use serde_urlencoded::from_bytes;
use std::collections::HashSet;
use time::{Duration, OffsetDateTime};
use tracing::warn;
#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "PascalCase", default)]
pub struct AssumeRoleRequest {
pub action: String,
pub duration_seconds: usize,
pub version: String,
pub role_arn: String,
pub role_session_name: String,
pub policy: String,
pub external_id: String,
}
// #[derive(Debug, Serialize, Default)]
// #[serde(rename_all = "PascalCase", default)]
// pub struct AssumeRoleResponse {
// #[serde(rename = "AssumeRoleResult")]
// pub result: AssumeRoleResult,
// }
// #[derive(Debug, Serialize, Default)]
// #[serde(rename_all = "PascalCase", default)]
// pub struct AssumeRoleResult {
// pub credentials: Credentials,
// }
// #[derive(Debug, Serialize, Default)]
// #[serde(rename_all = "PascalCase", default)]
// pub struct Credentials {
// #[serde(rename = "AccessKeyId")]
// pub access_key: String,
// #[serde(rename = "SecretAccessKey")]
// pub secret_key: String,
// pub status: String,
// pub expiration: usize,
// pub session_token: String,
// pub parent_user: String,
// }
pub struct AssumeRoleHandle {}
#[async_trait::async_trait]
impl Operation for AssumeRoleHandle {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle AssumeRoleHandle");
let Some(cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) };
let mut input = req.input;
let Some(bytes) = input.take_bytes() else {
return Err(s3_error!(InvalidRequest, "get body failed"));
};
let body: AssumeRoleRequest = from_bytes(&bytes).map_err(|_e| s3_error!(InvalidRequest, "get body failed"))?;
warn!("AssumeRole get body {:?}", body);
let exp = OffsetDateTime::now_utc().saturating_add(Duration::days(1));
// TODO: create tmp access_key
let resp = AssumeRoleOutput {
credentials: Some(Credentials {
access_key_id: cred.access_key,
expiration: Timestamp::from(exp),
secret_access_key: cred.secret_key.expose().to_string(),
session_token: "sdfsdf".to_owned(),
}),
..Default::default()
};
// getAssumeRoleCredentials
let output = xml::serialize::<AssumeRoleOutput>(&resp).unwrap();
Ok(S3Response::new((StatusCode::OK, Body::from(output))))
// return Err(s3_error!(NotImplemented));
}
}
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "PascalCase", default)]
pub struct AccountInfo {
pub account_name: String,
pub server: BackendInfo,
pub policy: BucketPolicy,
}
pub struct AccountInfoHandler {}
#[async_trait::async_trait]
impl Operation for AccountInfoHandler {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle AccountInfoHandler");
let Some(cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) };
warn!("AccountInfoHandler cread {:?}", &cred);
let layer = new_object_layer_fn();
let lock = layer.read().await;
let store = match lock.as_ref() {
Some(s) => s,
None => return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())),
};
// test policy
let mut s3_all_act = HashSet::with_capacity(1);
s3_all_act.insert(Action::AllActions);
let mut all_res = HashSet::with_capacity(1);
all_res.insert(Resource::new("*"));
let bucket_policy = BucketPolicy {
id: "".to_owned(),
version: "2012-10-17".to_owned(),
statements: vec![BPStatement {
sid: "".to_owned(),
effect: Effect::Allow,
actions: ActionSet(s3_all_act.clone()),
resources: ResourceSet(all_res),
..Default::default()
}],
};
// let policy = bucket_policy
// .marshal_msg()
// .map_err(|_e| S3Error::with_message(S3ErrorCode::InternalError, "parse policy failed"))?;
let backend_info = store.backend_info().await;
let info = AccountInfo {
account_name: cred.access_key,
server: backend_info,
policy: bucket_policy,
};
let output = serde_json::to_string(&info)
.map_err(|_e| S3Error::with_message(S3ErrorCode::InternalError, "parse accountInfo failed"))?;
Ok(S3Response::new((StatusCode::OK, Body::from(output))))
}
}
pub struct ServiceHandle {}
#[async_trait::async_trait]
impl Operation for ServiceHandle {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle ServiceHandle");
return Err(s3_error!(NotImplemented));
}
}
pub struct ServerInfoHandler {}
#[async_trait::async_trait]
impl Operation for ServerInfoHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle ServerInfoHandler");
return Err(s3_error!(NotImplemented));
}
}
pub struct InspectDataHandler {}
#[async_trait::async_trait]
impl Operation for InspectDataHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle InspectDataHandler");
return Err(s3_error!(NotImplemented));
}
}
pub struct StorageInfoHandler {}
#[async_trait::async_trait]
impl Operation for StorageInfoHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle StorageInfoHandler");
return Err(s3_error!(NotImplemented));
}
}
pub struct DataUsageInfoHandler {}
#[async_trait::async_trait]
impl Operation for DataUsageInfoHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle DataUsageInfoHandler");
return Err(s3_error!(NotImplemented));
}
}
pub struct MetricsHandler {}
#[async_trait::async_trait]
impl Operation for MetricsHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle MetricsHandler");
return Err(s3_error!(NotImplemented));
}
}
pub struct HealHandler {}
#[async_trait::async_trait]
impl Operation for HealHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle HealHandler");
return Err(s3_error!(NotImplemented));
}
}
pub struct BackgroundHealStatusHandler {}
#[async_trait::async_trait]
impl Operation for BackgroundHealStatusHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle BackgroundHealStatusHandler");
return Err(s3_error!(NotImplemented));
}
}
pub struct ListPools {}
#[async_trait::async_trait]
impl Operation for ListPools {
// GET <endpoint>/<admin-API>/pools/list
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle ListPools");
let layer = new_object_layer_fn();
let lock = layer.read().await;
let store = match lock.as_ref() {
Some(s) => s,
None => return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())),
};
let Some(endpoints) = GLOBAL_Endpoints.get() else {
return Err(s3_error!(NotImplemented));
};
if endpoints.legacy() {
return Err(s3_error!(NotImplemented));
}
let mut pools_status = Vec::new();
for (idx, _) in endpoints.as_ref().iter().enumerate() {
let state = store.status(idx).await.map_err(to_s3_error)?;
pools_status.push(state);
}
let output = serde_json::to_string(&pools_status)
.map_err(|_e| S3Error::with_message(S3ErrorCode::InternalError, "parse accountInfo failed"))?;
Ok(S3Response::new((StatusCode::OK, Body::from(output))))
}
}
#[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}
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle StatusPool");
let Some(endpoints) = GLOBAL_Endpoints.get() 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!(InvalidRequest, "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 layer = new_object_layer_fn();
let lock = layer.read().await;
let store = match lock.as_ref() {
Some(s) => s,
None => return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())),
};
let pools_status = store.status(idx).await.map_err(to_s3_error)?;
let output = serde_json::to_string(&pools_status)
.map_err(|_e| S3Error::with_message(S3ErrorCode::InternalError, "parse accountInfo failed"))?;
Ok(S3Response::new((StatusCode::OK, Body::from(output))))
}
}
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}
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle StartDecommission");
return Err(s3_error!(NotImplemented));
}
}
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}
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle CancelDecommission");
let Some(endpoints) = GLOBAL_Endpoints.get() 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!(InvalidRequest, "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 layer = new_object_layer_fn();
let lock = layer.write().await;
let _store = match lock.as_ref() {
Some(s) => s,
None => return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())),
};
// FIXME:
// store.decommission_cancel(idx).await;
return Err(s3_error!(NotImplemented));
}
}
pub struct RebalanceStart {}
#[async_trait::async_trait]
impl Operation for RebalanceStart {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle RebalanceStart");
return Err(s3_error!(NotImplemented));
}
}
// RebalanceStatus
pub struct RebalanceStatus {}
#[async_trait::async_trait]
impl Operation for RebalanceStatus {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle RebalanceStatus");
return Err(s3_error!(NotImplemented));
}
}
// RebalanceStop
pub struct RebalanceStop {}
#[async_trait::async_trait]
impl Operation for RebalanceStop {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle RebalanceStop");
return Err(s3_error!(NotImplemented));
}
}
+105
View File
@@ -0,0 +1,105 @@
pub mod handlers;
pub mod router;
use common::error::Result;
use hyper::Method;
use router::{AdminOperation, S3Router};
use s3s::route::S3Route;
const ADMIN_PREFIX: &str = "/rustfs/admin";
pub fn make_admin_route() -> Result<impl S3Route> {
let mut r = S3Router::new();
r.insert(Method::POST, "/", AdminOperation(&handlers::AssumeRoleHandle {}))?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/accountinfo").as_str(),
AdminOperation(&handlers::AccountInfoHandler {}),
)?;
r.insert(
Method::POST,
format!("{}{}", ADMIN_PREFIX, "/v3/service").as_str(),
AdminOperation(&handlers::ServiceHandle {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/info").as_str(),
AdminOperation(&handlers::ServerInfoHandler {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/inspect-data").as_str(),
AdminOperation(&handlers::InspectDataHandler {}),
)?;
r.insert(
Method::POST,
format!("{}{}", ADMIN_PREFIX, "/v3/inspect-data").as_str(),
AdminOperation(&handlers::InspectDataHandler {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/storageinfo").as_str(),
AdminOperation(&handlers::StorageInfoHandler {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/datausageinfo").as_str(),
AdminOperation(&handlers::DataUsageInfoHandler {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/metrics").as_str(),
AdminOperation(&handlers::MetricsHandler {}),
)?;
r.insert(
Method::POST,
format!("{}{}", ADMIN_PREFIX, "/v3/heal/{bucket}/{prefix}").as_str(),
AdminOperation(&handlers::HealHandler {}),
)?;
r.insert(
Method::POST,
format!("{}{}", ADMIN_PREFIX, "/v3/background-heal/status").as_str(),
AdminOperation(&handlers::BackgroundHealStatusHandler {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/pools/list").as_str(),
AdminOperation(&handlers::ListPools {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/pools/status").as_str(),
AdminOperation(&handlers::StatusPool {}),
)?;
r.insert(
Method::POST,
format!("{}{}", ADMIN_PREFIX, "/v3/pools/decommission").as_str(),
AdminOperation(&handlers::StartDecommission {}),
)?;
r.insert(
Method::POST,
format!("{}{}", ADMIN_PREFIX, "/v3/pools/cancel").as_str(),
AdminOperation(&handlers::CancelDecommission {}),
)?;
r.insert(
Method::POST,
format!("{}{}", ADMIN_PREFIX, "/v3/rebalance/start").as_str(),
AdminOperation(&handlers::RebalanceStart {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/rebalance/status").as_str(),
AdminOperation(&handlers::RebalanceStatus {}),
)?;
r.insert(
Method::POST,
format!("{}{}", ADMIN_PREFIX, "/v3/rebalance/stop").as_str(),
AdminOperation(&handlers::RebalanceStop {}),
)?;
Ok(r)
}
+97
View File
@@ -0,0 +1,97 @@
use common::error::Result;
use hyper::http::Extensions;
use hyper::HeaderMap;
use hyper::Method;
use hyper::StatusCode;
use hyper::Uri;
use matchit::Params;
use matchit::Router;
use s3s::header;
use s3s::route::S3Route;
use s3s::s3_error;
use s3s::Body;
use s3s::S3Request;
use s3s::S3Response;
use s3s::S3Result;
use super::ADMIN_PREFIX;
pub struct S3Router<T> {
router: Router<T>,
}
impl<T: Operation> S3Router<T> {
pub fn new() -> Self {
let router = Router::new();
Self { router }
}
pub fn insert(&mut self, method: Method, path: &str, operation: T) -> Result<()> {
let path = Self::make_route_str(method, path);
// warn!("set uri {}", &path);
self.router.insert(path, operation)?;
Ok(())
}
fn make_route_str(method: Method, path: &str) -> String {
format!("{}|{}", method.as_str(), path)
}
}
impl<T: Operation> Default for S3Router<T> {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl<T> S3Route for S3Router<T>
where
T: Operation,
{
fn is_match(&self, method: &Method, uri: &Uri, headers: &HeaderMap, _: &mut Extensions) -> bool {
// AssumeRole
if method == Method::POST && uri.path() == "/" {
if let Some(val) = headers.get(header::CONTENT_TYPE) {
if val.as_bytes() == b"application/x-www-form-urlencoded" {
return true;
}
}
}
uri.path().starts_with(ADMIN_PREFIX)
}
async fn call(&self, req: S3Request<Body>) -> S3Result<S3Response<(StatusCode, Body)>> {
let uri = format!("{}|{}", &req.method, req.uri.path());
// warn!("get uri {}", &uri);
if let Ok(mat) = self.router.at(&uri) {
let op: &T = mat.value;
return op.call(req, mat.params).await;
}
return Err(s3_error!(NotImplemented));
}
}
#[async_trait::async_trait]
pub trait Operation: Send + Sync + 'static {
// fn method() -> Method;
// fn uri() -> &'static str;
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>>;
}
pub struct AdminOperation(pub &'static dyn Operation);
#[async_trait::async_trait]
impl Operation for AdminOperation {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
self.0.call(req, params).await
}
}
+3 -2
View File
@@ -1,3 +1,4 @@
mod admin;
mod config;
mod grpc;
mod service;
@@ -84,7 +85,7 @@ async fn run(opt: config::Opt) -> Result<()> {
);
}
set_global_endpoints(endpoint_pools.as_ref().clone()).await;
set_global_endpoints(endpoint_pools.as_ref().clone()).map_err(|err| Error::from_string(err.to_string()))?;
update_erasure_type(setup_type).await;
// 初始化本地磁盘
@@ -114,7 +115,7 @@ async fn run(opt: config::Opt) -> Result<()> {
b.set_access(store.clone());
b.set_route(router::make_admin_route()?);
b.set_route(admin::make_admin_route()?);
// // Enable parsing virtual-hosted-style requests
// if let Some(dm) = opt.domain_name {
+1
View File
@@ -61,6 +61,7 @@ pub fn to_s3_error(err: Error) -> S3Error {
StorageError::InsufficientWriteQuorum => {
s3_error!(SlowDown, "Storage resources are insufficient for the write operation")
}
StorageError::DecommissionNotStarted => s3_error!(InvalidArgument, "Decommission Not Started"),
};
}