admin api: management of layout parameters through admin api

This commit is contained in:
Alex Auvolat 2025-03-06 17:12:52 +01:00
parent 6b19d7628e
commit e4881e62f1
6 changed files with 204 additions and 73 deletions

View file

@ -1875,15 +1875,29 @@
"required": [
"version",
"roles",
"parameters",
"stagedRoleChanges"
],
"properties": {
"parameters": {
"$ref": "#/components/schemas/LayoutParameters"
},
"roles": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NodeRoleResp"
}
},
"stagedParameters": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/LayoutParameters"
}
]
},
"stagedRoleChanges": {
"type": "array",
"items": {
@ -2021,6 +2035,17 @@
}
}
},
"LayoutParameters": {
"type": "object",
"required": [
"zoneRedundancy"
],
"properties": {
"zoneRedundancy": {
"$ref": "#/components/schemas/ZoneRedundancy"
}
}
},
"ListBucketsResponse": {
"type": "array",
"items": {
@ -3109,9 +3134,24 @@
}
},
"UpdateClusterLayoutRequest": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NodeRoleChange"
"type": "object",
"properties": {
"parameters": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/LayoutParameters"
}
]
},
"roles": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NodeRoleChange"
}
}
}
},
"UpdateClusterLayoutResponse": {
@ -3289,6 +3329,28 @@
]
}
]
},
"ZoneRedundancy": {
"oneOf": [
{
"type": "object",
"required": [
"atLeast"
],
"properties": {
"atLeast": {
"type": "integer",
"minimum": 0
}
}
},
{
"type": "string",
"enum": [
"maximum"
]
}
]
}
},
"securitySchemes": {

View file

@ -180,9 +180,9 @@ pub struct NodeResp {
pub is_up: bool,
pub last_seen_secs_ago: Option<u64>,
pub draining: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data_partition: Option<FreeSpaceResp>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata_partition: Option<FreeSpaceResp>,
}
@ -272,7 +272,9 @@ pub struct GetClusterLayoutRequest;
pub struct GetClusterLayoutResponse {
pub version: u64,
pub roles: Vec<NodeRoleResp>,
pub parameters: LayoutParameters,
pub staged_role_changes: Vec<NodeRoleChange>,
pub staged_parameters: Option<LayoutParameters>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@ -303,10 +305,28 @@ pub enum NodeRoleChangeEnum {
},
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LayoutParameters {
pub zone_redundancy: ZoneRedundancy,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub enum ZoneRedundancy {
AtLeast(usize),
Maximum,
}
// ---- UpdateClusterLayout ----
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateClusterLayoutRequest(pub Vec<NodeRoleChange>);
pub struct UpdateClusterLayoutRequest {
#[serde(default)]
pub roles: Vec<NodeRoleChange>,
#[serde(default)]
pub parameters: Option<LayoutParameters>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateClusterLayoutResponse(pub GetClusterLayoutResponse);
@ -367,7 +387,7 @@ pub struct GetKeyInfoRequest {
pub struct GetKeyInfoResponse {
pub name: String,
pub access_key_id: String,
#[serde(skip_serializing_if = "is_default")]
#[serde(default, skip_serializing_if = "is_default")]
pub secret_access_key: Option<String>,
pub permissions: KeyPerm,
pub buckets: Vec<KeyInfoBucketResponse>,

View file

@ -218,10 +218,19 @@ fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResp
})
.collect::<Vec<_>>();
let staged_parameters = if *layout.staging.get().parameters.get() != layout.current().parameters
{
Some((*layout.staging.get().parameters.get()).into())
} else {
None
};
GetClusterLayoutResponse {
version: layout.current().version,
roles,
parameters: layout.current().parameters.into(),
staged_role_changes,
staged_parameters,
}
}
@ -242,7 +251,7 @@ impl RequestHandler for UpdateClusterLayoutRequest {
let mut roles = layout.current().roles.clone();
roles.merge(&layout.staging.get().roles);
for change in self.0 {
for change in self.roles {
let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?;
let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?;
@ -252,11 +261,16 @@ impl RequestHandler for UpdateClusterLayoutRequest {
zone,
capacity,
tags,
} => Some(layout::NodeRole {
zone,
capacity,
tags,
}),
} => {
if matches!(capacity, Some(cap) if cap < 1024) {
return Err(Error::bad_request("Capacity should be at least 1K (1024)"));
}
Some(layout::NodeRole {
zone,
capacity,
tags,
})
}
_ => return Err(Error::bad_request("Invalid layout change")),
};
@ -267,6 +281,22 @@ impl RequestHandler for UpdateClusterLayoutRequest {
.merge(&roles.update_mutator(node, layout::NodeRoleV(new_role)));
}
if let Some(param) = self.parameters {
if let ZoneRedundancy::AtLeast(r_int) = param.zone_redundancy {
if r_int > layout.current().replication_factor {
return Err(Error::bad_request(format!(
"The zone redundancy must be smaller or equal to the replication factor ({}).",
layout.current().replication_factor
)));
} else if r_int < 1 {
return Err(Error::bad_request(
"The zone redundancy must be at least 1.",
));
}
}
layout.staging.get_mut().parameters.update(param.into());
}
garage
.system
.layout_manager
@ -322,3 +352,39 @@ impl RequestHandler for RevertClusterLayoutRequest {
Ok(RevertClusterLayoutResponse(res))
}
}
// ----
impl From<layout::ZoneRedundancy> for ZoneRedundancy {
fn from(x: layout::ZoneRedundancy) -> Self {
match x {
layout::ZoneRedundancy::Maximum => ZoneRedundancy::Maximum,
layout::ZoneRedundancy::AtLeast(x) => ZoneRedundancy::AtLeast(x),
}
}
}
impl Into<layout::ZoneRedundancy> for ZoneRedundancy {
fn into(self) -> layout::ZoneRedundancy {
match self {
ZoneRedundancy::Maximum => layout::ZoneRedundancy::Maximum,
ZoneRedundancy::AtLeast(x) => layout::ZoneRedundancy::AtLeast(x),
}
}
}
impl From<layout::LayoutParameters> for LayoutParameters {
fn from(x: layout::LayoutParameters) -> Self {
LayoutParameters {
zone_redundancy: x.zone_redundancy.into(),
}
}
}
impl Into<layout::LayoutParameters> for LayoutParameters {
fn into(self) -> layout::LayoutParameters {
layout::LayoutParameters {
zone_redundancy: self.zone_redundancy.into(),
}
}
}

View file

@ -108,10 +108,7 @@ impl AdminApiRequest {
Endpoint::GetClusterLayout => {
Ok(AdminApiRequest::GetClusterLayout(GetClusterLayoutRequest))
}
Endpoint::UpdateClusterLayout => {
let updates = parse_json_body::<UpdateClusterLayoutRequest, _, Error>(req).await?;
Ok(AdminApiRequest::UpdateClusterLayout(updates))
}
// UpdateClusterLayout semantics changed
Endpoint::ApplyClusterLayout => {
let param = parse_json_body::<ApplyClusterLayoutRequest, _, Error>(req).await?;
Ok(AdminApiRequest::ApplyClusterLayout(param))

View file

@ -57,54 +57,6 @@ pub async fn cmd_show_layout(
Ok(())
}
pub async fn cmd_config_layout(
rpc_cli: &Endpoint<SystemRpc, ()>,
rpc_host: NodeID,
config_opt: ConfigLayoutOpt,
) -> Result<(), Error> {
let mut layout = fetch_layout(rpc_cli, rpc_host).await?;
let mut did_something = false;
match config_opt.redundancy {
None => (),
Some(r_str) => {
let r = r_str
.parse::<ZoneRedundancy>()
.ok_or_message("invalid zone redundancy value")?;
if let ZoneRedundancy::AtLeast(r_int) = r {
if r_int > layout.current().replication_factor {
return Err(Error::Message(format!(
"The zone redundancy must be smaller or equal to the \
replication factor ({}).",
layout.current().replication_factor
)));
} else if r_int < 1 {
return Err(Error::Message(
"The zone redundancy must be at least 1.".into(),
));
}
}
layout
.staging
.get_mut()
.parameters
.update(LayoutParameters { zone_redundancy: r });
println!("The zone redundancy parameter has been set to '{}'.", r);
did_something = true;
}
}
if !did_something {
return Err(Error::Message(
"Please specify an action for `garage layout config`".into(),
));
}
send_layout(rpc_cli, rpc_host, layout).await?;
Ok(())
}
pub async fn cmd_layout_history(
rpc_cli: &Endpoint<SystemRpc, ()>,
rpc_host: NodeID,

View file

@ -4,6 +4,7 @@ use format_table::format_table;
use garage_util::error::*;
use garage_api_admin::api::*;
use garage_rpc::layout;
use crate::cli::layout as cli_v1;
use crate::cli::structs::*;
@ -14,6 +15,7 @@ impl Cli {
match cmd {
LayoutOperation::Assign(assign_opt) => self.cmd_assign_role(assign_opt).await,
LayoutOperation::Remove(remove_opt) => self.cmd_remove_role(remove_opt).await,
LayoutOperation::Config(config_opt) => self.cmd_config_layout(config_opt).await,
LayoutOperation::Apply(apply_opt) => self.cmd_apply_layout(apply_opt).await,
LayoutOperation::Revert(revert_opt) => self.cmd_revert_layout(revert_opt).await,
@ -21,10 +23,6 @@ impl Cli {
LayoutOperation::Show => {
cli_v1::cmd_show_layout(&self.system_rpc_endpoint, self.rpc_host).await
}
LayoutOperation::Config(config_opt) => {
cli_v1::cmd_config_layout(&self.system_rpc_endpoint, self.rpc_host, config_opt)
.await
}
LayoutOperation::History => {
cli_v1::cmd_layout_history(&self.system_rpc_endpoint, self.rpc_host).await
}
@ -100,8 +98,11 @@ impl Cli {
});
}
self.api_request(UpdateClusterLayoutRequest(actions))
.await?;
self.api_request(UpdateClusterLayoutRequest {
roles: actions,
parameters: None,
})
.await?;
println!("Role changes are staged but not yet committed.");
println!("Use `garage layout show` to view staged role changes,");
@ -126,8 +127,11 @@ impl Cli {
action: NodeRoleChangeEnum::Remove { remove: true },
}];
self.api_request(UpdateClusterLayoutRequest(actions))
.await?;
self.api_request(UpdateClusterLayoutRequest {
roles: actions,
parameters: None,
})
.await?;
println!("Role removal is staged but not yet committed.");
println!("Use `garage layout show` to view staged role changes,");
@ -135,6 +139,36 @@ impl Cli {
Ok(())
}
pub async fn cmd_config_layout(&self, config_opt: ConfigLayoutOpt) -> Result<(), Error> {
let mut did_something = false;
match config_opt.redundancy {
None => (),
Some(r_str) => {
let r = r_str
.parse::<layout::ZoneRedundancy>()
.ok_or_message("invalid zone redundancy value")?;
self.api_request(UpdateClusterLayoutRequest {
roles: vec![],
parameters: Some(LayoutParameters {
zone_redundancy: r.into(),
}),
})
.await?;
println!("The zone redundancy parameter has been set to '{}'.", r);
did_something = true;
}
}
if !did_something {
return Err(Error::Message(
"Please specify an action for `garage layout config`".into(),
));
}
Ok(())
}
pub async fn cmd_apply_layout(&self, apply_opt: ApplyLayoutOpt) -> Result<(), Error> {
let missing_version_error = r#"
Please pass the new layout version number to ensure that you are writing the correct version of the cluster layout.