|
@@ -22,7 +22,7 @@ mod pre_serve;
|
|
|
mod ssh;
|
|
mod ssh;
|
|
|
mod tmux;
|
|
mod tmux;
|
|
|
|
|
|
|
|
-use crate::cli::{Cli, Commands};
|
|
|
|
|
|
|
+use crate::cli::{Cli, Commands, PowerAction};
|
|
|
use crate::config::{
|
|
use crate::config::{
|
|
|
Cluster, Config, default_config_dir, load_recipe, resolve_cluster, select_cluster_by_name,
|
|
Cluster, Config, default_config_dir, load_recipe, resolve_cluster, select_cluster_by_name,
|
|
|
};
|
|
};
|
|
@@ -93,11 +93,11 @@ async fn main() -> Result<()> {
|
|
|
build_node,
|
|
build_node,
|
|
|
force_build,
|
|
force_build,
|
|
|
} => {
|
|
} => {
|
|
|
- let cluster = docker::serve(
|
|
|
|
|
|
|
+ handle_serve(
|
|
|
&config,
|
|
&config,
|
|
|
&recipe,
|
|
&recipe,
|
|
|
&cluster,
|
|
&cluster,
|
|
|
- node.as_deref(),
|
|
|
|
|
|
|
+ node,
|
|
|
docker::ServeOpts {
|
|
docker::ServeOpts {
|
|
|
keep,
|
|
keep,
|
|
|
skip_before_serve,
|
|
skip_before_serve,
|
|
@@ -108,13 +108,10 @@ async fn main() -> Result<()> {
|
|
|
build_node,
|
|
build_node,
|
|
|
force_build,
|
|
force_build,
|
|
|
},
|
|
},
|
|
|
|
|
+ follow,
|
|
|
|
|
+ dry_run,
|
|
|
)
|
|
)
|
|
|
.await?;
|
|
.await?;
|
|
|
- if follow && !dry_run {
|
|
|
|
|
- tracing::info!("{}", fluent_i18n::t!("container-log-header"));
|
|
|
|
|
- docker::show_logs(&config, &cluster, &recipe, true, node.as_deref(), dry_run)
|
|
|
|
|
- .await?;
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
Commands::Build {
|
|
Commands::Build {
|
|
|
recipe,
|
|
recipe,
|
|
@@ -122,25 +119,13 @@ async fn main() -> Result<()> {
|
|
|
cluster,
|
|
cluster,
|
|
|
force_build,
|
|
force_build,
|
|
|
} => {
|
|
} => {
|
|
|
- let recipe_obj = crate::config::load_recipe(&recipe).await?;
|
|
|
|
|
- let cluster = resolve_cluster_for_op(&config, &recipe, &cluster).await?;
|
|
|
|
|
- let opts = docker::ServeOpts {
|
|
|
|
|
- keep: false,
|
|
|
|
|
- skip_before_serve: true,
|
|
|
|
|
- dry_run,
|
|
|
|
|
- auto_power_on: false,
|
|
|
|
|
- sync_image: false,
|
|
|
|
|
- sync_image_source: None,
|
|
|
|
|
- build_node: Some(build_node.clone()),
|
|
|
|
|
- force_build,
|
|
|
|
|
- };
|
|
|
|
|
- docker::build::handle_build(
|
|
|
|
|
|
|
+ handle_build_cmd(
|
|
|
&config,
|
|
&config,
|
|
|
- &cluster,
|
|
|
|
|
&recipe,
|
|
&recipe,
|
|
|
- &recipe_obj,
|
|
|
|
|
- &opts,
|
|
|
|
|
&build_node,
|
|
&build_node,
|
|
|
|
|
+ &cluster,
|
|
|
|
|
+ force_build,
|
|
|
|
|
+ dry_run,
|
|
|
)
|
|
)
|
|
|
.await?;
|
|
.await?;
|
|
|
}
|
|
}
|
|
@@ -150,16 +135,7 @@ async fn main() -> Result<()> {
|
|
|
follow,
|
|
follow,
|
|
|
node,
|
|
node,
|
|
|
} => {
|
|
} => {
|
|
|
- let cluster = resolve_cluster_for_op(&config, &recipe_name, &cluster).await?;
|
|
|
|
|
- docker::show_logs(
|
|
|
|
|
- &config,
|
|
|
|
|
- &cluster,
|
|
|
|
|
- &recipe_name,
|
|
|
|
|
- follow,
|
|
|
|
|
- node.as_deref(),
|
|
|
|
|
- dry_run,
|
|
|
|
|
- )
|
|
|
|
|
- .await?;
|
|
|
|
|
|
|
+ handle_logs(&config, &recipe_name, &cluster, follow, node, dry_run).await?;
|
|
|
}
|
|
}
|
|
|
Commands::Stop {
|
|
Commands::Stop {
|
|
|
recipe: recipe_name,
|
|
recipe: recipe_name,
|
|
@@ -167,89 +143,13 @@ async fn main() -> Result<()> {
|
|
|
node,
|
|
node,
|
|
|
power_off,
|
|
power_off,
|
|
|
} => {
|
|
} => {
|
|
|
- if !node.is_empty() {
|
|
|
|
|
- // User specified specific nodes — stop only those.
|
|
|
|
|
- let cluster = resolve_cluster_for_op(&config, &recipe_name, &cluster).await?;
|
|
|
|
|
- docker::stop_cluster(&config, &cluster, &recipe_name, &node, dry_run).await?;
|
|
|
|
|
- } else {
|
|
|
|
|
- // Discover running deployments and resolve ambiguity.
|
|
|
|
|
- let variants =
|
|
|
|
|
- docker::deploy::discover_recipe_deployments(&config, &recipe_name).await?;
|
|
|
|
|
- if variants.is_empty() {
|
|
|
|
|
- anyhow::bail!("No running containers found for recipe '{}'", recipe_name);
|
|
|
|
|
- }
|
|
|
|
|
- if variants.len() == 1 {
|
|
|
|
|
- let v = &variants[0];
|
|
|
|
|
- let cluster = select_cluster_by_name(&config, &v.cluster)?;
|
|
|
|
|
- docker::stop_cluster(&config, &cluster, &recipe_name, &v.nodes, dry_run)
|
|
|
|
|
- .await?;
|
|
|
|
|
- } else {
|
|
|
|
|
- let mut msg = format!(
|
|
|
|
|
- "Multiple deployments of '{}' found — use --node to select:\n",
|
|
|
|
|
- recipe_name
|
|
|
|
|
- );
|
|
|
|
|
- for (i, v) in variants.iter().enumerate() {
|
|
|
|
|
- let nodes = v
|
|
|
|
|
- .nodes
|
|
|
|
|
- .iter()
|
|
|
|
|
- .map(|n| format!("--node {}", n))
|
|
|
|
|
- .collect::<Vec<_>>()
|
|
|
|
|
- .join(" ");
|
|
|
|
|
- msg.push_str(&format!(
|
|
|
|
|
- " {}: cluster '{}' with {}\n",
|
|
|
|
|
- i + 1,
|
|
|
|
|
- v.cluster,
|
|
|
|
|
- nodes
|
|
|
|
|
- ));
|
|
|
|
|
- }
|
|
|
|
|
- anyhow::bail!(msg);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // Handle power_off flag
|
|
|
|
|
- if power_off {
|
|
|
|
|
- let cluster = resolve_cluster_for_op(&config, &recipe_name, &cluster).await?;
|
|
|
|
|
- let node_str = if node.is_empty() {
|
|
|
|
|
- None
|
|
|
|
|
- } else {
|
|
|
|
|
- Some(node[0].as_str())
|
|
|
|
|
- };
|
|
|
|
|
- crate::power::power_off(&config, &cluster, node_str, dry_run).await?;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ handle_stop(&config, &recipe_name, &cluster, &node, power_off, dry_run).await?;
|
|
|
}
|
|
}
|
|
|
Commands::Health {
|
|
Commands::Health {
|
|
|
cluster: cluster_name,
|
|
cluster: cluster_name,
|
|
|
fingerprint,
|
|
fingerprint,
|
|
|
} => {
|
|
} => {
|
|
|
- if fingerprint {
|
|
|
|
|
- let clusters_to_check: Vec<Cluster> = match &cluster_name {
|
|
|
|
|
- Some(name) => vec![select_cluster_by_name(&config, name)?],
|
|
|
|
|
- None => config.clusters.clone(),
|
|
|
|
|
- };
|
|
|
|
|
- for cluster in &clusters_to_check {
|
|
|
|
|
- println!("\n=== Кластер: {} ===", cluster.name);
|
|
|
|
|
- for node in &cluster.nodes {
|
|
|
|
|
- match fetch_host_key(&config, node).await {
|
|
|
|
|
- Ok(fp) => {
|
|
|
|
|
- println!("{} ({}) -> host_key = \"{}\"", node.name, node.ssh_ip, fp)
|
|
|
|
|
- }
|
|
|
|
|
- Err(e) => println!("{} ({}) -> error: {}", node.name, node.ssh_ip, e),
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- match cluster_name {
|
|
|
|
|
- Some(ref name) => {
|
|
|
|
|
- let cluster = select_cluster_by_name(&config, name)?;
|
|
|
|
|
- docker::health::health_check(&config, &cluster, dry_run).await?;
|
|
|
|
|
- }
|
|
|
|
|
- None => {
|
|
|
|
|
- for cluster in &config.clusters {
|
|
|
|
|
- docker::health::health_check(&config, cluster, dry_run).await?;
|
|
|
|
|
- }
|
|
|
|
|
- docker::health::health_summary(&config).await?;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ handle_health(&config, cluster_name, fingerprint).await?;
|
|
|
}
|
|
}
|
|
|
Commands::Doctor {
|
|
Commands::Doctor {
|
|
|
recipe,
|
|
recipe,
|
|
@@ -276,30 +176,213 @@ async fn main() -> Result<()> {
|
|
|
alias,
|
|
alias,
|
|
|
command,
|
|
command,
|
|
|
} => {
|
|
} => {
|
|
|
- let (cluster, _recipe) =
|
|
|
|
|
- exec::resolve_target(&config, cluster.as_deref(), recipe.as_deref()).await?;
|
|
|
|
|
- // Resolve command: alias takes precedence, otherwise join
|
|
|
|
|
- // the trailing positional args into a single command line.
|
|
|
|
|
- let cmd_str = exec::resolve_command(&config, alias.as_deref(), &command)?;
|
|
|
|
|
- exec::run(&config, &cluster, node.as_deref(), &cmd_str, dry_run).await?;
|
|
|
|
|
|
|
+ handle_exec(&config, cluster, recipe, node, alias, command, dry_run).await?;
|
|
|
}
|
|
}
|
|
|
- Commands::Power { action } => match action {
|
|
|
|
|
- cli::PowerAction::Off { cluster, node } => {
|
|
|
|
|
- let cluster = select_cluster_by_name(&config, &cluster)?;
|
|
|
|
|
- power::power_off(&config, &cluster, node.as_deref(), dry_run).await?;
|
|
|
|
|
|
|
+ Commands::Power { action } => {
|
|
|
|
|
+ handle_power(&config, action, dry_run).await?;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
|
|
+// Per-command handler functions
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+/// Handle `serve` command: launch inference containers on a cluster.
|
|
|
|
|
+async fn handle_serve(
|
|
|
|
|
+ config: &Config,
|
|
|
|
|
+ recipe: &str,
|
|
|
|
|
+ cluster: &str,
|
|
|
|
|
+ node: Option<String>,
|
|
|
|
|
+ opts: docker::ServeOpts,
|
|
|
|
|
+ follow: bool,
|
|
|
|
|
+ dry_run: bool,
|
|
|
|
|
+) -> Result<()> {
|
|
|
|
|
+ let cluster = docker::serve(config, recipe, cluster, node.as_deref(), opts).await?;
|
|
|
|
|
+ if follow && !dry_run {
|
|
|
|
|
+ tracing::info!("{}", fluent_i18n::t!("container-log-header"));
|
|
|
|
|
+ docker::show_logs(config, &cluster, recipe, true, node.as_deref(), dry_run).await?;
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Handle `build` command: build a Docker image on a remote node.
|
|
|
|
|
+async fn handle_build_cmd(
|
|
|
|
|
+ config: &Config,
|
|
|
|
|
+ recipe: &str,
|
|
|
|
|
+ build_node: &str,
|
|
|
|
|
+ cluster: &str,
|
|
|
|
|
+ force_build: bool,
|
|
|
|
|
+ dry_run: bool,
|
|
|
|
|
+) -> Result<()> {
|
|
|
|
|
+ let recipe_obj = crate::config::load_recipe(recipe).await?;
|
|
|
|
|
+ let cluster = resolve_cluster_for_op(config, recipe, cluster).await?;
|
|
|
|
|
+ let opts = docker::ServeOpts {
|
|
|
|
|
+ keep: false,
|
|
|
|
|
+ skip_before_serve: true,
|
|
|
|
|
+ dry_run,
|
|
|
|
|
+ auto_power_on: false,
|
|
|
|
|
+ sync_image: false,
|
|
|
|
|
+ sync_image_source: None,
|
|
|
|
|
+ build_node: Some(build_node.to_string()),
|
|
|
|
|
+ force_build,
|
|
|
|
|
+ };
|
|
|
|
|
+ docker::build::handle_build(config, &cluster, recipe, &recipe_obj, &opts, build_node).await?;
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Handle `logs` command: show container logs.
|
|
|
|
|
+async fn handle_logs(
|
|
|
|
|
+ config: &Config,
|
|
|
|
|
+ recipe_name: &str,
|
|
|
|
|
+ cluster: &str,
|
|
|
|
|
+ follow: bool,
|
|
|
|
|
+ node: Option<String>,
|
|
|
|
|
+ dry_run: bool,
|
|
|
|
|
+) -> Result<()> {
|
|
|
|
|
+ let cluster = resolve_cluster_for_op(config, recipe_name, cluster).await?;
|
|
|
|
|
+ docker::show_logs(
|
|
|
|
|
+ config,
|
|
|
|
|
+ &cluster,
|
|
|
|
|
+ recipe_name,
|
|
|
|
|
+ follow,
|
|
|
|
|
+ node.as_deref(),
|
|
|
|
|
+ dry_run,
|
|
|
|
|
+ )
|
|
|
|
|
+ .await?;
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Handle `stop` command: stop containers and optionally power off.
|
|
|
|
|
+async fn handle_stop(
|
|
|
|
|
+ config: &Config,
|
|
|
|
|
+ recipe_name: &str,
|
|
|
|
|
+ cluster: &str,
|
|
|
|
|
+ node: &[String],
|
|
|
|
|
+ power_off: bool,
|
|
|
|
|
+ dry_run: bool,
|
|
|
|
|
+) -> Result<()> {
|
|
|
|
|
+ if !node.is_empty() {
|
|
|
|
|
+ let cluster = resolve_cluster_for_op(config, recipe_name, cluster).await?;
|
|
|
|
|
+ docker::stop_cluster(config, &cluster, recipe_name, node, dry_run).await?;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let variants = docker::deploy::discover_recipe_deployments(config, recipe_name).await?;
|
|
|
|
|
+ if variants.is_empty() {
|
|
|
|
|
+ anyhow::bail!("No running containers found for recipe '{}'", recipe_name);
|
|
|
|
|
+ }
|
|
|
|
|
+ if variants.len() == 1 {
|
|
|
|
|
+ let v = &variants[0];
|
|
|
|
|
+ let cluster = select_cluster_by_name(config, &v.cluster)?;
|
|
|
|
|
+ docker::stop_cluster(config, &cluster, recipe_name, &v.nodes, dry_run).await?;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let mut msg = format!(
|
|
|
|
|
+ "Multiple deployments of '{}' found — use --node to select:\n",
|
|
|
|
|
+ recipe_name
|
|
|
|
|
+ );
|
|
|
|
|
+ for (i, v) in variants.iter().enumerate() {
|
|
|
|
|
+ let nodes = v
|
|
|
|
|
+ .nodes
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|n| format!("--node {}", n))
|
|
|
|
|
+ .collect::<Vec<_>>()
|
|
|
|
|
+ .join(" ");
|
|
|
|
|
+ msg.push_str(&format!(
|
|
|
|
|
+ " {}: cluster '{}' with {}\n",
|
|
|
|
|
+ i + 1,
|
|
|
|
|
+ v.cluster,
|
|
|
|
|
+ nodes
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+ anyhow::bail!(msg);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if power_off {
|
|
|
|
|
+ let cluster = resolve_cluster_for_op(config, recipe_name, cluster).await?;
|
|
|
|
|
+ let node_str = if node.is_empty() {
|
|
|
|
|
+ None
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Some(node[0].as_str())
|
|
|
|
|
+ };
|
|
|
|
|
+ crate::power::power_off(config, &cluster, node_str, dry_run).await?;
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Handle `health` command: check cluster health or show host key fingerprints.
|
|
|
|
|
+async fn handle_health(
|
|
|
|
|
+ config: &Config,
|
|
|
|
|
+ cluster_name: Option<String>,
|
|
|
|
|
+ fingerprint: bool,
|
|
|
|
|
+) -> Result<()> {
|
|
|
|
|
+ if fingerprint {
|
|
|
|
|
+ let clusters_to_check: Vec<Cluster> = match &cluster_name {
|
|
|
|
|
+ Some(name) => vec![select_cluster_by_name(config, name)?],
|
|
|
|
|
+ None => config.clusters.clone(),
|
|
|
|
|
+ };
|
|
|
|
|
+ for cluster in &clusters_to_check {
|
|
|
|
|
+ println!("\n=== Кластер: {} ===", cluster.name);
|
|
|
|
|
+ for node in &cluster.nodes {
|
|
|
|
|
+ match fetch_host_key(config, node).await {
|
|
|
|
|
+ Ok(fp) => {
|
|
|
|
|
+ println!("{} ({}) -> host_key = \"{}\"", node.name, node.ssh_ip, fp)
|
|
|
|
|
+ }
|
|
|
|
|
+ Err(e) => println!("{} ({}) -> error: {}", node.name, node.ssh_ip, e),
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- cli::PowerAction::On { cluster, node } => {
|
|
|
|
|
- let cluster = select_cluster_by_name(&config, &cluster)?;
|
|
|
|
|
- power::power_on(&config, &cluster, node.as_deref(), dry_run).await?;
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ match cluster_name {
|
|
|
|
|
+ Some(ref name) => {
|
|
|
|
|
+ let cluster = select_cluster_by_name(config, name)?;
|
|
|
|
|
+ docker::health::health_check(config, &cluster, false).await?;
|
|
|
}
|
|
}
|
|
|
- cli::PowerAction::Switch { from, to } => {
|
|
|
|
|
- let from = select_cluster_by_name(&config, &from)?;
|
|
|
|
|
- let to = select_cluster_by_name(&config, &to)?;
|
|
|
|
|
- power::power_switch(&config, &from, &to, dry_run).await?;
|
|
|
|
|
|
|
+ None => {
|
|
|
|
|
+ for cluster in &config.clusters {
|
|
|
|
|
+ docker::health::health_check(config, cluster, false).await?;
|
|
|
|
|
+ }
|
|
|
|
|
+ docker::health::health_summary(config).await?;
|
|
|
}
|
|
}
|
|
|
- },
|
|
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Handle `exec` command: run a command on cluster nodes via SSH.
|
|
|
|
|
+async fn handle_exec(
|
|
|
|
|
+ config: &Config,
|
|
|
|
|
+ cluster: Option<String>,
|
|
|
|
|
+ recipe: Option<String>,
|
|
|
|
|
+ node: Option<String>,
|
|
|
|
|
+ alias: Option<String>,
|
|
|
|
|
+ command: Vec<String>,
|
|
|
|
|
+ dry_run: bool,
|
|
|
|
|
+) -> Result<()> {
|
|
|
|
|
+ let (cluster, _recipe) =
|
|
|
|
|
+ exec::resolve_target(config, cluster.as_deref(), recipe.as_deref()).await?;
|
|
|
|
|
+ let cmd_str = exec::resolve_command(config, alias.as_deref(), &command)?;
|
|
|
|
|
+ exec::run(config, &cluster, node.as_deref(), &cmd_str, dry_run).await?;
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+/// Handle `power` command: power on/off/switch nodes.
|
|
|
|
|
+async fn handle_power(config: &Config, action: PowerAction, dry_run: bool) -> Result<()> {
|
|
|
|
|
+ match action {
|
|
|
|
|
+ PowerAction::Off { cluster, node } => {
|
|
|
|
|
+ let cluster = select_cluster_by_name(config, &cluster)?;
|
|
|
|
|
+ power::power_off(config, &cluster, node.as_deref(), dry_run).await?;
|
|
|
|
|
+ }
|
|
|
|
|
+ PowerAction::On { cluster, node } => {
|
|
|
|
|
+ let cluster = select_cluster_by_name(config, &cluster)?;
|
|
|
|
|
+ power::power_on(config, &cluster, node.as_deref(), dry_run).await?;
|
|
|
|
|
+ }
|
|
|
|
|
+ PowerAction::Switch { from, to } => {
|
|
|
|
|
+ let from = select_cluster_by_name(config, &from)?;
|
|
|
|
|
+ let to = select_cluster_by_name(config, &to)?;
|
|
|
|
|
+ power::power_switch(config, &from, &to, dry_run).await?;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
Ok(())
|
|
Ok(())
|
|
|
}
|
|
}
|
|
|
|
|
|