photoncloud-monorepo/deployer/crates/deployer-ctl/src/main.rs

555 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
use tracing_subscriber::EnvFilter;
mod chainfire;
mod power;
mod remote;
/// Deployer control CLI for PhotonCloud.
///
/// - 初回ブートストラップ時に Chainfire 上の Cluster/Node/Service 定義を作成
/// - 既存の Deployer クラスタに対して宣言的な設定を apply する
/// - Deployer が壊れた場合でも、Chainfire 上の状態を直接修復できることを目標とする
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Cli {
/// Chainfire API エンドポイント (例: http://127.0.0.1:7000)
#[arg(long, global = true, default_value = "http://127.0.0.1:7000")]
chainfire_endpoint: String,
/// PhotonCloud Cluster ID (論理名)
#[arg(long, global = true)]
cluster_id: Option<String>,
/// PhotonCloud cluster namespace (default: photoncloud)
#[arg(long, global = true, default_value = "photoncloud")]
cluster_namespace: String,
/// Deployer namespace used for machine_id -> NodeConfig bootstrap mappings
#[arg(long, global = true, default_value = "deployer")]
deployer_namespace: String,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
/// 初回ブートストラップ用の基本オブジェクトを作成する
///
/// - Cluster メタデータ
/// - Node 情報 (ローカルード1台分)
/// - 必要であれば Service/ServiceInstance のシード
Bootstrap {
/// ブートストラップ用のJSON/YAML設定ファイル
#[arg(long)]
config: PathBuf,
},
/// 宣言的な PhotonCloud クラスタ設定を Chainfire に apply する (GitOps 的に利用可能)
Apply {
/// Cluster/Node/Service/Instance/MTLSPolicy を含むJSON/YAML
#[arg(long)]
config: PathBuf,
/// 既存エントリを pruning するかどうか
#[arg(long, default_value_t = false)]
prune: bool,
},
/// Chainfire 上の PhotonCloud 関連キーをダンプする (デバッグ用途)
Dump {
/// ダンプ対象の prefix (未指定の場合は cluster-namespace を使用)
#[arg(long, default_value = "")]
prefix: String,
/// 出力形式
#[arg(long, value_enum, default_value_t = DumpFormat::Text)]
format: DumpFormat,
},
/// Deployer HTTP API を経由して、クラスタ状態を同期・確認する
///
/// 現時点ではプレースホルダであり、将来的なGitOps連携を見据えた形だけ用意する。
Deployer {
/// Deployer HTTP エンドポイント (例: http://deployer.local:8080)
#[arg(long)]
endpoint: String,
/// 一旦は `status` のみをサポート
#[arg(long, default_value = "status")]
action: String,
},
/// ノード単位の inventory / lifecycle 状態を確認・更新する
Node {
#[command(subcommand)]
command: NodeCommand,
},
/// HostDeployment rollout object を確認・操作する
Deployment {
#[command(subcommand)]
command: DeploymentCommand,
},
/// Service spec/status/publication/instances を表示する
Service {
#[command(subcommand)]
command: ServiceCommand,
},
}
#[derive(Subcommand, Debug)]
enum NodeCommand {
/// 指定ノードの記録と関連 state を表示する
Inspect {
#[arg(long)]
node_id: String,
#[arg(long, default_value_t = false)]
include_desired_system: bool,
#[arg(long, default_value_t = false)]
include_observed_system: bool,
#[arg(long, value_enum, default_value_t = DumpFormat::Json)]
format: DumpFormat,
},
/// 指定ノードの lifecycle / commissioning 状態を更新する
SetState {
#[arg(long)]
node_id: String,
#[arg(long, value_enum)]
state: Option<NodeLifecycleStateArg>,
#[arg(long, value_enum)]
commission_state: Option<CommissionStateArg>,
#[arg(long, value_enum)]
install_state: Option<InstallStateArg>,
#[arg(long, value_enum)]
power_state: Option<PowerStateArg>,
#[arg(long)]
bmc_ref: Option<String>,
},
/// 指定ノードの observed-system を更新する
SetObserved {
#[arg(long)]
node_id: String,
#[arg(long)]
status: Option<String>,
#[arg(long)]
nixos_configuration: Option<String>,
#[arg(long)]
target_system: Option<String>,
#[arg(long)]
current_system: Option<String>,
#[arg(long)]
configured_system: Option<String>,
#[arg(long)]
booted_system: Option<String>,
#[arg(long)]
rollback_system: Option<String>,
},
/// 指定ノードの電源操作を行う
Power {
#[arg(long)]
node_id: String,
#[arg(long, value_enum)]
action: PowerActionArg,
},
/// 指定ノードに再インストールを要求する
Reinstall {
#[arg(long)]
node_id: String,
#[arg(long, default_value_t = false)]
power_cycle: bool,
},
}
#[derive(Subcommand, Debug)]
enum DeploymentCommand {
/// HostDeployment の spec/status を表示する
Inspect {
#[arg(long)]
name: String,
#[arg(long, value_enum, default_value_t = DumpFormat::Json)]
format: DumpFormat,
},
/// HostDeployment を一時停止する
Pause {
#[arg(long)]
name: String,
},
/// HostDeployment を再開する
Resume {
#[arg(long)]
name: String,
},
/// HostDeployment を中止し、配布済み desired-system を取り消す
Abort {
#[arg(long)]
name: String,
},
}
#[derive(Subcommand, Debug)]
enum ServiceCommand {
/// Service の spec/status/publication を表示する
Inspect {
#[arg(long)]
name: String,
#[arg(long, default_value_t = false)]
include_instances: bool,
#[arg(long, value_enum, default_value_t = DumpFormat::Json)]
format: DumpFormat,
},
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum DumpFormat {
Text,
Json,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum NodeLifecycleStateArg {
Pending,
Provisioning,
Active,
Failed,
Draining,
}
impl NodeLifecycleStateArg {
fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Provisioning => "provisioning",
Self::Active => "active",
Self::Failed => "failed",
Self::Draining => "draining",
}
}
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum CommissionStateArg {
Discovered,
Commissioning,
Commissioned,
}
impl CommissionStateArg {
fn as_str(self) -> &'static str {
match self {
Self::Discovered => "discovered",
Self::Commissioning => "commissioning",
Self::Commissioned => "commissioned",
}
}
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum InstallStateArg {
Pending,
Installing,
Installed,
Failed,
ReinstallRequested,
}
impl InstallStateArg {
fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Installing => "installing",
Self::Installed => "installed",
Self::Failed => "failed",
Self::ReinstallRequested => "reinstall_requested",
}
}
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum PowerStateArg {
On,
Off,
Cycling,
Unknown,
}
impl PowerStateArg {
fn as_str(self) -> &'static str {
match self {
Self::On => "on",
Self::Off => "off",
Self::Cycling => "cycling",
Self::Unknown => "unknown",
}
}
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum PowerActionArg {
On,
Off,
Cycle,
Refresh,
}
impl PowerActionArg {
fn as_str(self) -> &'static str {
match self {
Self::On => "on",
Self::Off => "off",
Self::Cycle => "cycle",
Self::Refresh => "refresh",
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(env_filter)
.init();
let cli = Cli::parse();
match cli.command {
Command::Bootstrap { config } => {
chainfire::bootstrap_cluster(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
&cli.deployer_namespace,
cli.cluster_id.as_deref(),
&config,
)
.await?;
}
Command::Apply { config, prune } => {
chainfire::apply_cluster_state(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
&cli.deployer_namespace,
cli.cluster_id.as_deref(),
&config,
prune,
)
.await?;
}
Command::Dump { prefix, format } => {
let resolved_prefix = if prefix.trim().is_empty() {
format!("{}/", cli.cluster_namespace)
} else {
prefix
};
chainfire::dump_prefix(
&cli.chainfire_endpoint,
&resolved_prefix,
matches!(format, DumpFormat::Json),
)
.await?;
}
Command::Deployer { endpoint, action } => {
remote::run_deployer_command(&endpoint, &action).await?;
}
Command::Node { command } => {
let cluster_id = cli
.cluster_id
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--cluster-id is required for node commands"))?;
match command {
NodeCommand::Inspect {
node_id,
include_desired_system,
include_observed_system,
format,
} => {
chainfire::inspect_node(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&node_id,
include_desired_system,
include_observed_system,
matches!(format, DumpFormat::Json),
)
.await?;
}
NodeCommand::SetState {
node_id,
state,
commission_state,
install_state,
power_state,
bmc_ref,
} => {
chainfire::set_node_states(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&node_id,
state.map(|value| value.as_str().to_string()),
commission_state.map(|value| value.as_str().to_string()),
install_state.map(|value| value.as_str().to_string()),
power_state.map(|value| value.as_str().to_string()),
bmc_ref,
)
.await?;
}
NodeCommand::SetObserved {
node_id,
status,
nixos_configuration,
target_system,
current_system,
configured_system,
booted_system,
rollback_system,
} => {
chainfire::set_observed_system(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&node_id,
status,
nixos_configuration,
target_system,
current_system,
configured_system,
booted_system,
rollback_system,
)
.await?;
}
NodeCommand::Power { node_id, action } => {
power::power_node(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&node_id,
action.as_str(),
)
.await?;
}
NodeCommand::Reinstall {
node_id,
power_cycle,
} => {
power::request_reinstall(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&node_id,
power_cycle,
)
.await?;
}
}
}
Command::Deployment { command } => {
let cluster_id = cli.cluster_id.as_deref().ok_or_else(|| {
anyhow::anyhow!("--cluster-id is required for deployment commands")
})?;
match command {
DeploymentCommand::Inspect { name, format } => {
chainfire::inspect_host_deployment(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&name,
matches!(format, DumpFormat::Json),
)
.await?;
}
DeploymentCommand::Pause { name } => {
chainfire::set_host_deployment_paused(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&name,
true,
)
.await?;
}
DeploymentCommand::Resume { name } => {
chainfire::set_host_deployment_paused(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&name,
false,
)
.await?;
}
DeploymentCommand::Abort { name } => {
chainfire::abort_host_deployment(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&name,
)
.await?;
}
}
}
Command::Service { command } => {
let cluster_id = cli
.cluster_id
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--cluster-id is required for service commands"))?;
match command {
ServiceCommand::Inspect {
name,
include_instances,
format,
} => {
chainfire::inspect_service(
&cli.chainfire_endpoint,
&cli.cluster_namespace,
cluster_id,
&name,
include_instances,
matches!(format, DumpFormat::Json),
)
.await?;
}
}
}
}
Ok(())
}