2021-06-01 15:00:30 +00:00
|
|
|
use std::cmp::max;
|
2021-03-12 17:12:31 +00:00
|
|
|
use std::collections::HashSet;
|
|
|
|
use std::net::SocketAddr;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use structopt::StructOpt;
|
|
|
|
|
2021-05-02 21:13:08 +00:00
|
|
|
use garage_util::data::Uuid;
|
2021-04-05 17:55:53 +00:00
|
|
|
use garage_util::error::Error;
|
2021-03-15 15:21:41 +00:00
|
|
|
use garage_util::time::*;
|
2021-03-12 17:12:31 +00:00
|
|
|
|
|
|
|
use garage_rpc::membership::*;
|
|
|
|
use garage_rpc::ring::*;
|
|
|
|
use garage_rpc::rpc_client::*;
|
|
|
|
|
|
|
|
use garage_model::bucket_table::*;
|
|
|
|
use garage_model::key_table::*;
|
|
|
|
|
|
|
|
use crate::admin_rpc::*;
|
|
|
|
|
|
|
|
#[derive(StructOpt, Debug)]
|
|
|
|
pub enum Command {
|
|
|
|
/// Run Garage server
|
|
|
|
#[structopt(name = "server")]
|
|
|
|
Server(ServerOpt),
|
|
|
|
|
|
|
|
/// Get network status
|
|
|
|
#[structopt(name = "status")]
|
|
|
|
Status,
|
|
|
|
|
|
|
|
/// Garage node operations
|
|
|
|
#[structopt(name = "node")]
|
|
|
|
Node(NodeOperation),
|
|
|
|
|
|
|
|
/// Bucket operations
|
|
|
|
#[structopt(name = "bucket")]
|
|
|
|
Bucket(BucketOperation),
|
|
|
|
|
|
|
|
/// Key operations
|
|
|
|
#[structopt(name = "key")]
|
|
|
|
Key(KeyOperation),
|
|
|
|
|
|
|
|
/// Start repair of node data
|
|
|
|
#[structopt(name = "repair")]
|
|
|
|
Repair(RepairOpt),
|
|
|
|
|
|
|
|
/// Gather node statistics
|
|
|
|
#[structopt(name = "stats")]
|
|
|
|
Stats(StatsOpt),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(StructOpt, Debug)]
|
|
|
|
pub struct ServerOpt {
|
|
|
|
/// Configuration file
|
|
|
|
#[structopt(short = "c", long = "config", default_value = "./config.toml")]
|
|
|
|
pub config_file: PathBuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(StructOpt, Debug)]
|
|
|
|
pub enum NodeOperation {
|
|
|
|
/// Configure Garage node
|
|
|
|
#[structopt(name = "configure")]
|
|
|
|
Configure(ConfigureNodeOpt),
|
|
|
|
|
|
|
|
/// Remove Garage node from cluster
|
|
|
|
#[structopt(name = "remove")]
|
|
|
|
Remove(RemoveNodeOpt),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(StructOpt, Debug)]
|
|
|
|
pub struct ConfigureNodeOpt {
|
|
|
|
/// Node to configure (prefix of hexadecimal node id)
|
|
|
|
node_id: String,
|
|
|
|
|
2021-05-28 11:18:31 +00:00
|
|
|
/// Location (zone or datacenter) of the node
|
|
|
|
#[structopt(short = "z", long = "zone")]
|
|
|
|
zone: Option<String>,
|
2021-03-12 17:12:31 +00:00
|
|
|
|
|
|
|
/// Capacity (in relative terms, use 1 to represent your smallest server)
|
|
|
|
#[structopt(short = "c", long = "capacity")]
|
|
|
|
capacity: Option<u32>,
|
|
|
|
|
2021-05-28 10:36:22 +00:00
|
|
|
/// Gateway-only node
|
|
|
|
#[structopt(short = "g", long = "gateway")]
|
|
|
|
gateway: bool,
|
|
|
|
|
2021-03-21 23:00:09 +00:00
|
|
|
/// Optional node tag
|
2021-03-12 17:12:31 +00:00
|
|
|
#[structopt(short = "t", long = "tag")]
|
|
|
|
tag: Option<String>,
|
2021-03-18 20:49:12 +00:00
|
|
|
|
|
|
|
/// Replaced node(s): list of node IDs that will be removed from the current cluster
|
|
|
|
#[structopt(long = "replace")]
|
|
|
|
replace: Vec<String>,
|
2021-03-12 17:12:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(StructOpt, Debug)]
|
|
|
|
pub struct RemoveNodeOpt {
|
|
|
|
/// Node to configure (prefix of hexadecimal node id)
|
|
|
|
node_id: String,
|
|
|
|
|
|
|
|
/// If this flag is not given, the node won't be removed
|
|
|
|
#[structopt(long = "yes")]
|
|
|
|
yes: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub enum BucketOperation {
|
|
|
|
/// List buckets
|
|
|
|
#[structopt(name = "list")]
|
|
|
|
List,
|
|
|
|
|
|
|
|
/// Get bucket info
|
|
|
|
#[structopt(name = "info")]
|
|
|
|
Info(BucketOpt),
|
|
|
|
|
|
|
|
/// Create bucket
|
|
|
|
#[structopt(name = "create")]
|
|
|
|
Create(BucketOpt),
|
|
|
|
|
|
|
|
/// Delete bucket
|
|
|
|
#[structopt(name = "delete")]
|
|
|
|
Delete(DeleteBucketOpt),
|
|
|
|
|
|
|
|
/// Allow key to read or write to bucket
|
|
|
|
#[structopt(name = "allow")]
|
|
|
|
Allow(PermBucketOpt),
|
|
|
|
|
2021-05-29 19:22:15 +00:00
|
|
|
/// Deny key from reading or writing to bucket
|
2021-03-12 17:12:31 +00:00
|
|
|
#[structopt(name = "deny")]
|
|
|
|
Deny(PermBucketOpt),
|
|
|
|
|
|
|
|
/// Expose as website or not
|
|
|
|
#[structopt(name = "website")]
|
|
|
|
Website(WebsiteOpt),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub struct WebsiteOpt {
|
|
|
|
/// Create
|
|
|
|
#[structopt(long = "allow")]
|
|
|
|
pub allow: bool,
|
|
|
|
|
|
|
|
/// Delete
|
|
|
|
#[structopt(long = "deny")]
|
|
|
|
pub deny: bool,
|
|
|
|
|
|
|
|
/// Bucket name
|
|
|
|
pub bucket: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub struct BucketOpt {
|
|
|
|
/// Bucket name
|
|
|
|
pub name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub struct DeleteBucketOpt {
|
|
|
|
/// Bucket name
|
|
|
|
pub name: String,
|
|
|
|
|
|
|
|
/// If this flag is not given, the bucket won't be deleted
|
|
|
|
#[structopt(long = "yes")]
|
|
|
|
pub yes: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub struct PermBucketOpt {
|
2021-03-15 18:14:26 +00:00
|
|
|
/// Access key name or ID
|
2021-03-12 17:12:31 +00:00
|
|
|
#[structopt(long = "key")]
|
2021-03-15 18:14:26 +00:00
|
|
|
pub key_pattern: String,
|
2021-03-12 17:12:31 +00:00
|
|
|
|
|
|
|
/// Allow/deny read operations
|
|
|
|
#[structopt(long = "read")]
|
|
|
|
pub read: bool,
|
|
|
|
|
|
|
|
/// Allow/deny write operations
|
|
|
|
#[structopt(long = "write")]
|
|
|
|
pub write: bool,
|
|
|
|
|
|
|
|
/// Bucket name
|
|
|
|
pub bucket: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub enum KeyOperation {
|
|
|
|
/// List keys
|
|
|
|
#[structopt(name = "list")]
|
|
|
|
List,
|
|
|
|
|
|
|
|
/// Get key info
|
|
|
|
#[structopt(name = "info")]
|
|
|
|
Info(KeyOpt),
|
|
|
|
|
|
|
|
/// Create new key
|
|
|
|
#[structopt(name = "new")]
|
|
|
|
New(KeyNewOpt),
|
|
|
|
|
|
|
|
/// Rename key
|
|
|
|
#[structopt(name = "rename")]
|
|
|
|
Rename(KeyRenameOpt),
|
|
|
|
|
|
|
|
/// Delete key
|
|
|
|
#[structopt(name = "delete")]
|
|
|
|
Delete(KeyDeleteOpt),
|
2021-03-18 18:24:59 +00:00
|
|
|
|
|
|
|
/// Import key
|
|
|
|
#[structopt(name = "import")]
|
|
|
|
Import(KeyImportOpt),
|
2021-03-12 17:12:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub struct KeyOpt {
|
2021-03-15 18:14:26 +00:00
|
|
|
/// ID or name of the key
|
|
|
|
pub key_pattern: String,
|
2021-03-12 17:12:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub struct KeyNewOpt {
|
|
|
|
/// Name of the key
|
|
|
|
#[structopt(long = "name", default_value = "Unnamed key")]
|
|
|
|
pub name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub struct KeyRenameOpt {
|
2021-03-15 18:14:26 +00:00
|
|
|
/// ID or name of the key
|
|
|
|
pub key_pattern: String,
|
2021-03-12 17:12:31 +00:00
|
|
|
|
|
|
|
/// New name of the key
|
|
|
|
pub new_name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub struct KeyDeleteOpt {
|
2021-03-15 18:14:26 +00:00
|
|
|
/// ID or name of the key
|
|
|
|
pub key_pattern: String,
|
2021-03-12 17:12:31 +00:00
|
|
|
|
|
|
|
/// Confirm deletion
|
|
|
|
#[structopt(long = "yes")]
|
|
|
|
pub yes: bool,
|
|
|
|
}
|
|
|
|
|
2021-03-18 18:24:59 +00:00
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug)]
|
|
|
|
pub struct KeyImportOpt {
|
|
|
|
/// Access key ID
|
|
|
|
pub key_id: String,
|
|
|
|
|
|
|
|
/// Secret access key
|
|
|
|
pub secret_key: String,
|
|
|
|
|
|
|
|
/// Key name
|
|
|
|
#[structopt(short = "n", default_value = "Imported key")]
|
|
|
|
pub name: String,
|
|
|
|
}
|
|
|
|
|
2021-03-12 17:12:31 +00:00
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug, Clone)]
|
|
|
|
pub struct RepairOpt {
|
|
|
|
/// Launch repair operation on all nodes
|
|
|
|
#[structopt(short = "a", long = "all-nodes")]
|
|
|
|
pub all_nodes: bool,
|
|
|
|
|
|
|
|
/// Confirm the launch of the repair operation
|
|
|
|
#[structopt(long = "yes")]
|
|
|
|
pub yes: bool,
|
|
|
|
|
|
|
|
#[structopt(subcommand)]
|
|
|
|
pub what: Option<RepairWhat>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
|
|
|
pub enum RepairWhat {
|
|
|
|
/// Only do a full sync of metadata tables
|
|
|
|
#[structopt(name = "tables")]
|
|
|
|
Tables,
|
|
|
|
/// Only repair (resync/rebalance) the set of stored blocks
|
|
|
|
#[structopt(name = "blocks")]
|
|
|
|
Blocks,
|
|
|
|
/// Only redo the propagation of object deletions to the version table (slow)
|
|
|
|
#[structopt(name = "versions")]
|
|
|
|
Versions,
|
|
|
|
/// Only redo the propagation of version deletions to the block ref table (extremely slow)
|
|
|
|
#[structopt(name = "block_refs")]
|
|
|
|
BlockRefs,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, StructOpt, Debug, Clone)]
|
|
|
|
pub struct StatsOpt {
|
|
|
|
/// Gather statistics from all nodes
|
|
|
|
#[structopt(short = "a", long = "all-nodes")]
|
|
|
|
pub all_nodes: bool,
|
|
|
|
|
|
|
|
/// Gather detailed statistics (this can be long)
|
|
|
|
#[structopt(short = "d", long = "detailed")]
|
|
|
|
pub detailed: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn cli_cmd(
|
|
|
|
cmd: Command,
|
|
|
|
membership_rpc_cli: RpcAddrClient<Message>,
|
2021-04-23 20:41:24 +00:00
|
|
|
admin_rpc_cli: RpcAddrClient<AdminRpc>,
|
2021-03-12 17:16:03 +00:00
|
|
|
rpc_host: SocketAddr,
|
|
|
|
) -> Result<(), Error> {
|
2021-03-12 17:12:31 +00:00
|
|
|
match cmd {
|
|
|
|
Command::Status => cmd_status(membership_rpc_cli, rpc_host).await,
|
|
|
|
Command::Node(NodeOperation::Configure(configure_opt)) => {
|
|
|
|
cmd_configure(membership_rpc_cli, rpc_host, configure_opt).await
|
|
|
|
}
|
|
|
|
Command::Node(NodeOperation::Remove(remove_opt)) => {
|
|
|
|
cmd_remove(membership_rpc_cli, rpc_host, remove_opt).await
|
|
|
|
}
|
|
|
|
Command::Bucket(bo) => {
|
2021-04-23 20:41:24 +00:00
|
|
|
cmd_admin(admin_rpc_cli, rpc_host, AdminRpc::BucketOperation(bo)).await
|
2021-03-12 17:12:31 +00:00
|
|
|
}
|
2021-04-23 20:41:24 +00:00
|
|
|
Command::Key(ko) => cmd_admin(admin_rpc_cli, rpc_host, AdminRpc::KeyOperation(ko)).await,
|
|
|
|
Command::Repair(ro) => cmd_admin(admin_rpc_cli, rpc_host, AdminRpc::LaunchRepair(ro)).await,
|
|
|
|
Command::Stats(so) => cmd_admin(admin_rpc_cli, rpc_host, AdminRpc::Stats(so)).await,
|
2021-03-12 17:12:31 +00:00
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-12 17:16:03 +00:00
|
|
|
pub async fn cmd_status(
|
|
|
|
rpc_cli: RpcAddrClient<Message>,
|
|
|
|
rpc_host: SocketAddr,
|
|
|
|
) -> Result<(), Error> {
|
2021-03-12 17:12:31 +00:00
|
|
|
let status = match rpc_cli
|
|
|
|
.call(&rpc_host, &Message::PullStatus, ADMIN_RPC_TIMEOUT)
|
|
|
|
.await??
|
|
|
|
{
|
|
|
|
Message::AdvertiseNodesUp(nodes) => nodes,
|
|
|
|
resp => return Err(Error::Message(format!("Invalid RPC response: {:?}", resp))),
|
|
|
|
};
|
|
|
|
let config = match rpc_cli
|
|
|
|
.call(&rpc_host, &Message::PullConfig, ADMIN_RPC_TIMEOUT)
|
|
|
|
.await??
|
|
|
|
{
|
|
|
|
Message::AdvertiseConfig(cfg) => cfg,
|
|
|
|
resp => return Err(Error::Message(format!("Invalid RPC response: {:?}", resp))),
|
|
|
|
};
|
|
|
|
|
2021-06-01 15:00:30 +00:00
|
|
|
let (hostname_len, addr_len, tag_len, zone_len) = status
|
|
|
|
.iter()
|
|
|
|
.map(|adv| (adv, config.members.get(&adv.id)))
|
|
|
|
.map(|(adv, cfg)| {
|
|
|
|
(
|
|
|
|
adv.state_info.hostname.len(),
|
|
|
|
adv.addr.to_string().len(),
|
|
|
|
cfg.map(|c| c.tag.len()).unwrap_or(0),
|
|
|
|
cfg.map(|c| c.zone.len()).unwrap_or(0),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.fold((0, 0, 0, 0), |(h, a, t, z), (mh, ma, mt, mz)| {
|
|
|
|
(max(h, mh), max(a, ma), max(t, mt), max(z, mz))
|
|
|
|
});
|
|
|
|
|
2021-03-12 17:12:31 +00:00
|
|
|
println!("Healthy nodes:");
|
|
|
|
for adv in status.iter().filter(|x| x.is_up) {
|
|
|
|
if let Some(cfg) = config.members.get(&adv.id) {
|
|
|
|
println!(
|
2021-06-01 15:00:30 +00:00
|
|
|
"{id:?}\t{host}{h_pad}\t{addr}{a_pad}\t[{tag}]{t_pad}\t{zone}{z_pad}\t{capacity}",
|
|
|
|
id = adv.id,
|
|
|
|
host = adv.state_info.hostname,
|
|
|
|
addr = adv.addr,
|
|
|
|
tag = cfg.tag,
|
|
|
|
zone = cfg.zone,
|
|
|
|
capacity = cfg.capacity_string(),
|
|
|
|
h_pad = " ".repeat(hostname_len - adv.state_info.hostname.len()),
|
|
|
|
a_pad = " ".repeat(addr_len - adv.addr.to_string().len()),
|
|
|
|
t_pad = " ".repeat(tag_len - cfg.tag.len()),
|
|
|
|
z_pad = " ".repeat(zone_len - cfg.zone.len()),
|
2021-03-12 17:12:31 +00:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
println!(
|
2021-06-01 15:00:30 +00:00
|
|
|
"{id:?}\t{h}{h_pad}\t{addr}{a_pad}\tUNCONFIGURED/REMOVED",
|
|
|
|
id = adv.id,
|
|
|
|
h = adv.state_info.hostname,
|
|
|
|
addr = adv.addr,
|
|
|
|
h_pad = " ".repeat(hostname_len - adv.state_info.hostname.len()),
|
|
|
|
a_pad = " ".repeat(addr_len - adv.addr.to_string().len()),
|
2021-03-12 17:12:31 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let status_keys = status.iter().map(|x| x.id).collect::<HashSet<_>>();
|
|
|
|
let failure_case_1 = status.iter().any(|x| !x.is_up);
|
|
|
|
let failure_case_2 = config
|
|
|
|
.members
|
|
|
|
.iter()
|
|
|
|
.any(|(id, _)| !status_keys.contains(id));
|
|
|
|
if failure_case_1 || failure_case_2 {
|
|
|
|
println!("\nFailed nodes:");
|
|
|
|
for adv in status.iter().filter(|x| !x.is_up) {
|
|
|
|
if let Some(cfg) = config.members.get(&adv.id) {
|
|
|
|
println!(
|
2021-06-01 15:00:30 +00:00
|
|
|
"{id:?}\t{host}{h_pad}\t{addr}{a_pad}\t[{tag}]{t_pad}\t{zone}{z_pad}\t{capacity}\tlast seen: {last_seen}s ago",
|
|
|
|
id=adv.id,
|
|
|
|
host=adv.state_info.hostname,
|
|
|
|
addr=adv.addr,
|
|
|
|
tag=cfg.tag,
|
|
|
|
zone=cfg.zone,
|
|
|
|
capacity=cfg.capacity_string(),
|
|
|
|
last_seen=(now_msec() - adv.last_seen) / 1000,
|
|
|
|
h_pad=" ".repeat(hostname_len - adv.state_info.hostname.len()),
|
|
|
|
a_pad=" ".repeat(addr_len - adv.addr.to_string().len()),
|
|
|
|
t_pad=" ".repeat(tag_len - cfg.tag.len()),
|
|
|
|
z_pad=" ".repeat(zone_len - cfg.zone.len()),
|
2021-03-12 17:12:31 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2021-06-01 15:00:30 +00:00
|
|
|
let (tag_len, zone_len) = config
|
|
|
|
.members
|
|
|
|
.iter()
|
|
|
|
.filter(|(&id, _)| !status.iter().any(|x| x.id == id))
|
|
|
|
.map(|(_, cfg)| (cfg.tag.len(), cfg.zone.len()))
|
|
|
|
.fold((0, 0), |(t, z), (mt, mz)| (max(t, mt), max(z, mz)));
|
|
|
|
|
2021-03-12 17:12:31 +00:00
|
|
|
for (id, cfg) in config.members.iter() {
|
|
|
|
if !status.iter().any(|x| x.id == *id) {
|
|
|
|
println!(
|
2021-06-01 15:00:30 +00:00
|
|
|
"{id:?}\t{tag}{t_pad}\t{zone}{z_pad}\t{capacity}\tnever seen",
|
|
|
|
id = id,
|
|
|
|
tag = cfg.tag,
|
|
|
|
zone = cfg.zone,
|
|
|
|
capacity = cfg.capacity_string(),
|
|
|
|
t_pad = " ".repeat(tag_len - cfg.tag.len()),
|
|
|
|
z_pad = " ".repeat(zone_len - cfg.zone.len()),
|
2021-03-12 17:12:31 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-04-05 17:55:53 +00:00
|
|
|
pub fn find_matching_node(
|
2021-05-02 21:13:08 +00:00
|
|
|
cand: impl std::iter::Iterator<Item = Uuid>,
|
2021-04-05 17:55:53 +00:00
|
|
|
pattern: &str,
|
2021-05-02 21:13:08 +00:00
|
|
|
) -> Result<Uuid, Error> {
|
2021-03-18 20:49:12 +00:00
|
|
|
let mut candidates = vec![];
|
|
|
|
for c in cand {
|
|
|
|
if hex::encode(&c).starts_with(&pattern) {
|
|
|
|
candidates.push(c);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if candidates.len() != 1 {
|
|
|
|
Err(Error::Message(format!(
|
|
|
|
"{} nodes match '{}'",
|
|
|
|
candidates.len(),
|
|
|
|
pattern,
|
|
|
|
)))
|
|
|
|
} else {
|
|
|
|
Ok(candidates[0])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-12 17:12:31 +00:00
|
|
|
pub async fn cmd_configure(
|
|
|
|
rpc_cli: RpcAddrClient<Message>,
|
|
|
|
rpc_host: SocketAddr,
|
|
|
|
args: ConfigureNodeOpt,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
let status = match rpc_cli
|
|
|
|
.call(&rpc_host, &Message::PullStatus, ADMIN_RPC_TIMEOUT)
|
|
|
|
.await??
|
|
|
|
{
|
|
|
|
Message::AdvertiseNodesUp(nodes) => nodes,
|
|
|
|
resp => return Err(Error::Message(format!("Invalid RPC response: {:?}", resp))),
|
|
|
|
};
|
|
|
|
|
2021-03-18 20:49:12 +00:00
|
|
|
let added_node = find_matching_node(status.iter().map(|x| x.id), &args.node_id)?;
|
2021-03-12 17:12:31 +00:00
|
|
|
|
|
|
|
let mut config = match rpc_cli
|
|
|
|
.call(&rpc_host, &Message::PullConfig, ADMIN_RPC_TIMEOUT)
|
|
|
|
.await??
|
|
|
|
{
|
|
|
|
Message::AdvertiseConfig(cfg) => cfg,
|
|
|
|
resp => return Err(Error::Message(format!("Invalid RPC response: {:?}", resp))),
|
|
|
|
};
|
|
|
|
|
2021-03-18 20:49:12 +00:00
|
|
|
for replaced in args.replace.iter() {
|
|
|
|
let replaced_node = find_matching_node(config.members.keys().cloned(), replaced)?;
|
|
|
|
if config.members.remove(&replaced_node).is_none() {
|
2021-04-05 17:55:53 +00:00
|
|
|
return Err(Error::Message(format!(
|
|
|
|
"Cannot replace node {:?} as it is not in current configuration",
|
|
|
|
replaced_node
|
|
|
|
)));
|
2021-03-18 20:49:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-28 10:36:22 +00:00
|
|
|
if args.capacity.is_some() && args.gateway {
|
|
|
|
return Err(Error::Message(
|
|
|
|
"-c and -g are mutually exclusive, please configure node either with c>0 to act as a storage node or with -g to act as a gateway node".into()));
|
|
|
|
}
|
|
|
|
if args.capacity == Some(0) {
|
|
|
|
return Err(Error::Message("Invalid capacity value: 0".into()));
|
|
|
|
}
|
|
|
|
|
2021-03-18 20:49:12 +00:00
|
|
|
let new_entry = match config.members.get(&added_node) {
|
2021-05-28 10:36:22 +00:00
|
|
|
None => {
|
|
|
|
let capacity = match args.capacity {
|
|
|
|
Some(c) => Some(c),
|
|
|
|
None if args.gateway => None,
|
|
|
|
_ => return Err(Error::Message(
|
|
|
|
"Please specify a capacity with the -c flag, or set node explicitly as gateway with -g".into())),
|
|
|
|
};
|
|
|
|
NetworkConfigEntry {
|
2021-05-28 11:58:47 +00:00
|
|
|
zone: args.zone.expect("Please specifiy a zone with the -z flag"),
|
2021-05-28 10:36:22 +00:00
|
|
|
capacity,
|
|
|
|
tag: args.tag.unwrap_or_default(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(old) => {
|
|
|
|
let capacity = match args.capacity {
|
|
|
|
Some(c) => Some(c),
|
|
|
|
None if args.gateway => None,
|
|
|
|
_ => old.capacity,
|
|
|
|
};
|
|
|
|
NetworkConfigEntry {
|
2021-05-28 11:58:47 +00:00
|
|
|
zone: args.zone.unwrap_or_else(|| old.zone.to_string()),
|
2021-05-28 10:36:22 +00:00
|
|
|
capacity,
|
|
|
|
tag: args.tag.unwrap_or_else(|| old.tag.to_string()),
|
|
|
|
}
|
|
|
|
}
|
2021-03-12 17:12:31 +00:00
|
|
|
};
|
|
|
|
|
2021-03-18 20:49:12 +00:00
|
|
|
config.members.insert(added_node, new_entry);
|
2021-03-12 17:12:31 +00:00
|
|
|
config.version += 1;
|
|
|
|
|
|
|
|
rpc_cli
|
|
|
|
.call(
|
|
|
|
&rpc_host,
|
|
|
|
&Message::AdvertiseConfig(config),
|
|
|
|
ADMIN_RPC_TIMEOUT,
|
|
|
|
)
|
|
|
|
.await??;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn cmd_remove(
|
|
|
|
rpc_cli: RpcAddrClient<Message>,
|
|
|
|
rpc_host: SocketAddr,
|
|
|
|
args: RemoveNodeOpt,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
let mut config = match rpc_cli
|
|
|
|
.call(&rpc_host, &Message::PullConfig, ADMIN_RPC_TIMEOUT)
|
|
|
|
.await??
|
|
|
|
{
|
|
|
|
Message::AdvertiseConfig(cfg) => cfg,
|
|
|
|
resp => return Err(Error::Message(format!("Invalid RPC response: {:?}", resp))),
|
|
|
|
};
|
|
|
|
|
2021-03-18 20:49:12 +00:00
|
|
|
let deleted_node = find_matching_node(config.members.keys().cloned(), &args.node_id)?;
|
2021-03-12 17:12:31 +00:00
|
|
|
|
|
|
|
if !args.yes {
|
|
|
|
return Err(Error::Message(format!(
|
|
|
|
"Add the flag --yes to really remove {:?} from the cluster",
|
2021-03-18 20:49:12 +00:00
|
|
|
deleted_node
|
2021-03-12 17:12:31 +00:00
|
|
|
)));
|
|
|
|
}
|
|
|
|
|
2021-03-18 20:49:12 +00:00
|
|
|
config.members.remove(&deleted_node);
|
2021-03-12 17:12:31 +00:00
|
|
|
config.version += 1;
|
|
|
|
|
|
|
|
rpc_cli
|
|
|
|
.call(
|
|
|
|
&rpc_host,
|
|
|
|
&Message::AdvertiseConfig(config),
|
|
|
|
ADMIN_RPC_TIMEOUT,
|
|
|
|
)
|
|
|
|
.await??;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn cmd_admin(
|
2021-04-23 20:41:24 +00:00
|
|
|
rpc_cli: RpcAddrClient<AdminRpc>,
|
2021-03-12 17:12:31 +00:00
|
|
|
rpc_host: SocketAddr,
|
2021-04-23 20:41:24 +00:00
|
|
|
args: AdminRpc,
|
2021-03-12 17:12:31 +00:00
|
|
|
) -> Result<(), Error> {
|
|
|
|
match rpc_cli.call(&rpc_host, args, ADMIN_RPC_TIMEOUT).await?? {
|
2021-04-23 20:41:24 +00:00
|
|
|
AdminRpc::Ok(msg) => {
|
2021-03-12 17:12:31 +00:00
|
|
|
println!("{}", msg);
|
|
|
|
}
|
2021-04-23 20:41:24 +00:00
|
|
|
AdminRpc::BucketList(bl) => {
|
2021-03-12 17:12:31 +00:00
|
|
|
println!("List of buckets:");
|
|
|
|
for bucket in bl {
|
|
|
|
println!("{}", bucket);
|
|
|
|
}
|
|
|
|
}
|
2021-04-23 20:41:24 +00:00
|
|
|
AdminRpc::BucketInfo(bucket) => {
|
2021-03-12 17:12:31 +00:00
|
|
|
print_bucket_info(&bucket);
|
|
|
|
}
|
2021-04-23 20:41:24 +00:00
|
|
|
AdminRpc::KeyList(kl) => {
|
2021-03-12 17:12:31 +00:00
|
|
|
println!("List of keys:");
|
|
|
|
for key in kl {
|
|
|
|
println!("{}\t{}", key.0, key.1);
|
|
|
|
}
|
|
|
|
}
|
2021-04-23 20:41:24 +00:00
|
|
|
AdminRpc::KeyInfo(key) => {
|
2021-03-12 17:12:31 +00:00
|
|
|
print_key_info(&key);
|
|
|
|
}
|
|
|
|
r => {
|
|
|
|
error!("Unexpected response: {:?}", r);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn print_key_info(key: &Key) {
|
|
|
|
println!("Key name: {}", key.name.get());
|
|
|
|
println!("Key ID: {}", key.key_id);
|
|
|
|
println!("Secret key: {}", key.secret_key);
|
|
|
|
if key.deleted.get() {
|
|
|
|
println!("Key is deleted.");
|
|
|
|
} else {
|
|
|
|
println!("Authorized buckets:");
|
|
|
|
for (b, _, perm) in key.authorized_buckets.items().iter() {
|
|
|
|
println!("- {} R:{} W:{}", b, perm.allow_read, perm.allow_write);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn print_bucket_info(bucket: &Bucket) {
|
|
|
|
println!("Bucket name: {}", bucket.name);
|
|
|
|
match bucket.state.get() {
|
|
|
|
BucketState::Deleted => println!("Bucket is deleted."),
|
|
|
|
BucketState::Present(p) => {
|
|
|
|
println!("Authorized keys:");
|
|
|
|
for (k, _, perm) in p.authorized_keys.items().iter() {
|
|
|
|
println!("- {} R:{} W:{}", k, perm.allow_read, perm.allow_write);
|
|
|
|
}
|
|
|
|
println!("Website access: {}", p.website.get());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|