555 lines
16 KiB
Rust
555 lines
16 KiB
Rust
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(())
|
||
}
|