|
|
@@ -7,7 +7,10 @@ use crate::config::{Cluster, Config, NcclConfig, Recipe, resolve_cluster};
|
|
|
use crate::home_assistant::Client as HaClient;
|
|
|
use crate::ssh::run_ssh_with_retry;
|
|
|
|
|
|
-/// Power on cluster if switch is off.
|
|
|
+/// Power on cluster if the smart plug(s) are off.
|
|
|
+///
|
|
|
+/// Mirrors `power::power_on` but checks entity states first so it's a
|
|
|
+/// no-op when everything is already on.
|
|
|
pub async fn power_on_cluster_if_off(
|
|
|
config: &Config,
|
|
|
recipe: &Recipe,
|
|
|
@@ -42,82 +45,92 @@ pub async fn power_on_cluster_if_off(
|
|
|
(c, Some(n.clone()))
|
|
|
};
|
|
|
|
|
|
- let ha_switch_off = if let Some(ha_cfg) = &config.home_assistant {
|
|
|
- let ha = HaClient::new(ha_cfg).ok();
|
|
|
- let entities = cluster_to_check.extra_entities.clone();
|
|
|
- if !entities.is_empty() {
|
|
|
- if let Some(client) = ha {
|
|
|
- let mut switch_off = false;
|
|
|
- for e in &entities {
|
|
|
- match client.get_state(e).await {
|
|
|
- Some(ref s) if s == "on" => continue,
|
|
|
- _ => {
|
|
|
- switch_off = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
+ let Some(ha_cfg) = &config.home_assistant else {
|
|
|
+ // No HA → can't power on via smart plugs; assume node is already
|
|
|
+ // reachable or the user handles power separately.
|
|
|
+ return Ok(());
|
|
|
+ };
|
|
|
+ let ha = HaClient::new(ha_cfg)?;
|
|
|
+ crate::home_assistant::warn_if_token_looks_short(&ha_cfg.token);
|
|
|
+
|
|
|
+ // Collect all entity IDs for this cluster (per-node + cluster-level extras).
|
|
|
+ let mut all_entities: Vec<String> = Vec::new();
|
|
|
+ for node in &cluster_to_check.nodes {
|
|
|
+ if let Some(entity) = &node.entity {
|
|
|
+ all_entities.push(entity.clone());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ all_entities.extend(cluster_to_check.extra_entities.iter().cloned());
|
|
|
+
|
|
|
+ if all_entities.is_empty() {
|
|
|
+ // No entities configured → nothing to power on.
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if any entity is NOT "on".
|
|
|
+ let any_off = {
|
|
|
+ let mut off = false;
|
|
|
+ for e in &all_entities {
|
|
|
+ match ha.get_state(e).await {
|
|
|
+ Some(ref s) if s == "on" => {}
|
|
|
+ _ => {
|
|
|
+ off = true;
|
|
|
}
|
|
|
- switch_off
|
|
|
- } else {
|
|
|
- false
|
|
|
}
|
|
|
- } else {
|
|
|
- false
|
|
|
}
|
|
|
- } else {
|
|
|
- false
|
|
|
+ off
|
|
|
};
|
|
|
|
|
|
- if ha_switch_off {
|
|
|
- info!("{}", fluent_i18n::t!("cluster-switch-off-detected"));
|
|
|
- if !dry_run {
|
|
|
- if let Some(ha_cfg) = &config.home_assistant {
|
|
|
- let ha = HaClient::new(ha_cfg)?;
|
|
|
- crate::home_assistant::warn_if_token_looks_short(&ha_cfg.token);
|
|
|
- for e in &cluster_to_check.extra_entities {
|
|
|
- ha.turn_on(e).await?;
|
|
|
- }
|
|
|
- info!("{}", fluent_i18n::t!("cluster-switch-powered-on"));
|
|
|
+ if !any_off {
|
|
|
+ // All entities already on; nodes should be reachable.
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
|
|
|
- // Wait for nodes to become available
|
|
|
- info!("{}", fluent_i18n::t!("cluster-waiting-for-nodes"));
|
|
|
+ info!("{}", fluent_i18n::t!("cluster-switch-off-detected"));
|
|
|
+ if dry_run {
|
|
|
+ info!("{}", fluent_i18n::t!("power-on-dryrun"));
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
|
|
|
- // Initial boot delay — nodes need time for POST → bootloader → OS → SSH
|
|
|
- let boot_wait_secs = 60;
|
|
|
- info!("⏳ Waiting {}s for nodes to boot...", boot_wait_secs);
|
|
|
- tokio::time::sleep(std::time::Duration::from_secs(boot_wait_secs)).await;
|
|
|
+ // Power on each entity (per-node first, then cluster extras).
|
|
|
+ for e in &all_entities {
|
|
|
+ ha.turn_on(e).await?;
|
|
|
+ }
|
|
|
+ info!("{}", fluent_i18n::t!("cluster-switch-powered-on"));
|
|
|
|
|
|
- let max_poll_secs = 120;
|
|
|
- let poll_interval_secs = 10;
|
|
|
- let mut waited_secs = 0;
|
|
|
- while waited_secs < max_poll_secs {
|
|
|
- let mut all_online = true;
|
|
|
- for node in &cluster_to_check.nodes {
|
|
|
- let cmd = "echo ok".to_string();
|
|
|
- if run_ssh_with_retry(config, node, cmd, false, 2)
|
|
|
- .await
|
|
|
- .is_err()
|
|
|
- {
|
|
|
- all_online = false;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- if all_online {
|
|
|
- info!("{}", fluent_i18n::t!("cluster-nodes-available"));
|
|
|
- break;
|
|
|
- }
|
|
|
- tokio::time::sleep(std::time::Duration::from_secs(poll_interval_secs)).await;
|
|
|
- waited_secs += poll_interval_secs;
|
|
|
- }
|
|
|
- if waited_secs >= max_poll_secs {
|
|
|
- anyhow::bail!("{}", fluent_i18n::t!("cluster-nodes-not-available-timeout"));
|
|
|
- }
|
|
|
- } else {
|
|
|
- anyhow::bail!("{}", fluent_i18n::t!("ha-config-required-for-power-on"));
|
|
|
+ // Wait for nodes to become available via SSH.
|
|
|
+ info!("{}", fluent_i18n::t!("cluster-waiting-for-nodes"));
|
|
|
+
|
|
|
+ // Initial boot delay — nodes need time for POST → bootloader → OS → SSH.
|
|
|
+ let boot_wait_secs = 60;
|
|
|
+ info!("⏳ Waiting {}s for nodes to boot...", boot_wait_secs);
|
|
|
+ tokio::time::sleep(std::time::Duration::from_secs(boot_wait_secs)).await;
|
|
|
+
|
|
|
+ let max_poll_secs = 120;
|
|
|
+ let poll_interval_secs = 10;
|
|
|
+ let mut waited_secs = 0;
|
|
|
+ while waited_secs < max_poll_secs {
|
|
|
+ let mut all_online = true;
|
|
|
+ for node in &cluster_to_check.nodes {
|
|
|
+ let cmd = "echo ok".to_string();
|
|
|
+ if run_ssh_with_retry(config, node, cmd, false, 2)
|
|
|
+ .await
|
|
|
+ .is_err()
|
|
|
+ {
|
|
|
+ all_online = false;
|
|
|
+ break;
|
|
|
}
|
|
|
- } else {
|
|
|
- info!("{}", fluent_i18n::t!("power-on-dryrun"));
|
|
|
}
|
|
|
+ if all_online {
|
|
|
+ info!("{}", fluent_i18n::t!("cluster-nodes-available"));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ tokio::time::sleep(std::time::Duration::from_secs(poll_interval_secs)).await;
|
|
|
+ waited_secs += poll_interval_secs;
|
|
|
}
|
|
|
+ if waited_secs >= max_poll_secs {
|
|
|
+ anyhow::bail!("{}", fluent_i18n::t!("cluster-nodes-not-available-timeout"));
|
|
|
+ }
|
|
|
+
|
|
|
Ok(())
|
|
|
}
|