cluster_power.rs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. //! Cluster power operations - power on cluster if switch is off.
  2. use anyhow::{Context, Result};
  3. use tracing::info;
  4. use crate::config::{Cluster, Config, NcclConfig, Recipe, resolve_cluster};
  5. use crate::home_assistant::Client as HaClient;
  6. use crate::ssh::run_ssh_with_retry;
  7. /// Power on cluster if switch is off.
  8. pub async fn power_on_cluster_if_off(
  9. config: &Config,
  10. recipe: &Recipe,
  11. cluster_name: &str,
  12. node_name: Option<&str>,
  13. dry_run: bool,
  14. ) -> Result<()> {
  15. let (cluster_to_check, _nccl_opt) = if let Some(nn) = node_name {
  16. let (solo_c, _solo_n) = config
  17. .clusters
  18. .iter()
  19. .find_map(|c| {
  20. c.nodes.iter().find(|n| n.name == nn).map(|node| {
  21. let solo = Cluster {
  22. name: format!("solo/{}", node.name),
  23. master_ib_ip: node.ib_ip.clone(),
  24. nodes: vec![node.clone()],
  25. extra_entities: vec![],
  26. };
  27. (solo, None::<NcclConfig>)
  28. })
  29. })
  30. .context(format!("Node '{}' not found in any cluster", nn))?;
  31. (solo_c, None)
  32. } else {
  33. let (c, n) = resolve_cluster(
  34. config,
  35. recipe,
  36. cluster_name,
  37. &std::collections::HashSet::new(),
  38. )?;
  39. (c, Some(n.clone()))
  40. };
  41. let ha_switch_off = if let Some(ha_cfg) = &config.home_assistant {
  42. let ha = HaClient::new(ha_cfg).ok();
  43. let entities = cluster_to_check.extra_entities.clone();
  44. if !entities.is_empty() {
  45. if let Some(client) = ha {
  46. let mut switch_off = false;
  47. for e in &entities {
  48. match client.get_state(e).await {
  49. Some(ref s) if s == "on" => continue,
  50. _ => {
  51. switch_off = true;
  52. break;
  53. }
  54. }
  55. }
  56. switch_off
  57. } else {
  58. false
  59. }
  60. } else {
  61. false
  62. }
  63. } else {
  64. false
  65. };
  66. if ha_switch_off {
  67. info!("{}", fluent_i18n::t!("cluster-switch-off-detected"));
  68. if !dry_run {
  69. if let Some(ha_cfg) = &config.home_assistant {
  70. let ha = HaClient::new(ha_cfg)?;
  71. crate::home_assistant::warn_if_token_looks_short(&ha_cfg.token);
  72. for e in &cluster_to_check.extra_entities {
  73. ha.turn_on(e).await?;
  74. }
  75. info!("{}", fluent_i18n::t!("cluster-switch-powered-on"));
  76. // Wait for nodes to become available
  77. info!("{}", fluent_i18n::t!("cluster-waiting-for-nodes"));
  78. // Initial boot delay — nodes need time for POST → bootloader → OS → SSH
  79. let boot_wait_secs = 60;
  80. info!("⏳ Waiting {}s for nodes to boot...", boot_wait_secs);
  81. tokio::time::sleep(std::time::Duration::from_secs(boot_wait_secs)).await;
  82. let max_poll_secs = 120;
  83. let poll_interval_secs = 10;
  84. let mut waited_secs = 0;
  85. while waited_secs < max_poll_secs {
  86. let mut all_online = true;
  87. for node in &cluster_to_check.nodes {
  88. let cmd = "echo ok".to_string();
  89. if run_ssh_with_retry(config, node, cmd, false, 2)
  90. .await
  91. .is_err()
  92. {
  93. all_online = false;
  94. break;
  95. }
  96. }
  97. if all_online {
  98. info!("{}", fluent_i18n::t!("cluster-nodes-available"));
  99. break;
  100. }
  101. tokio::time::sleep(std::time::Duration::from_secs(poll_interval_secs)).await;
  102. waited_secs += poll_interval_secs;
  103. }
  104. if waited_secs >= max_poll_secs {
  105. anyhow::bail!("{}", fluent_i18n::t!("cluster-nodes-not-available-timeout"));
  106. }
  107. } else {
  108. anyhow::bail!("{}", fluent_i18n::t!("ha-config-required-for-power-on"));
  109. }
  110. } else {
  111. info!("{}", fluent_i18n::t!("power-on-dryrun"));
  112. }
  113. }
  114. Ok(())
  115. }