소스 검색

power off --node: do NOT turn off cluster-level extras (fabric switch/fan)

A single-node power off (power off --cluster C --node N) also HA turn_off'd
the cluster's extra_entities — the 200G fabric switch and cooling fan. On a
live multi-node serve that takes the whole cluster's NCCL fabric down: the
in-flight requests wedge at 0 throughput and the engine dies (observed today
wedging a Ling-3.1-flash-fp4 TP4 bench when powering off one unrelated node).

Extras are now toggled off only for a whole-cluster power off (no --node);
a subset keeps them on (with an info log naming what was skipped). Extracted
the decision into a pure off_extras() helper + test. i18n key
power-off-extras-skipped added to all 6 locales.
Alexander Korolev 6 일 전
부모
커밋
62bf7be418
9개의 변경된 파일과 55개의 추가작업 그리고 4개의 파일을 삭제
  1. 1 0
      locales/de/main.ftl
  2. 1 0
      locales/en/main.ftl
  3. 1 0
      locales/es/main.ftl
  4. 1 0
      locales/fr/main.ftl
  5. 1 0
      locales/it/main.ftl
  6. 1 0
      locales/ru/main.ftl
  7. 20 4
      src/power.rs
  8. 16 0
      src/power/tests.rs
  9. 13 0
      src/power/toggle.rs

+ 1 - 0
locales/de/main.ftl

@@ -62,6 +62,7 @@ power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
 power-off-waiting = ⏳ warte { $secs}s bis Knoten sich beruhigen
 power-off-no-ha = keine [home_assistant]-Konfig; OS-Herunterfahren oben ausgeführt
 power-off-ssh-failures = einige Knoten meldeten SSH-Fehler beim Herunterfahren
+power-off-extras-skipped = teilweises Ausschalten ({ $nodes }): Cluster-Entitäten bleiben an ({ $extras }) — sie werden von den aktiven Knoten mitgenutzt
 power-on-dryrun = dry-run: tatsächlicher HA-Aufruf übersprungen
 power-on-would-on = würde: HA turn_on { $entity }
 power-on-would-skip = würde überspringen: { $name } (keine Entity)

+ 1 - 0
locales/en/main.ftl

@@ -55,6 +55,7 @@ power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
 power-off-waiting = ⏳ waiting { $secs }s for nodes to settle
 power-off-no-ha = no [home_assistant] config; OS shutdown was performed above
 power-off-ssh-failures = some nodes reported SSH failures during shutdown
+power-off-extras-skipped = subset power off ({ $nodes }): keeping cluster-level extras on ({ $extras }) — they are shared by the nodes that stay up
 power-on-dryrun = dry-run: skipping actual HA call
 power-on-would-on = would: HA turn_on { $entity }
 power-on-would-skip = would skip: { $name } (no entity)

+ 1 - 0
locales/es/main.ftl

@@ -62,6 +62,7 @@ power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
 power-off-waiting = ⏳ esperando { $secs}s para que los nodos se asienten
 power-off-no-ha = sin config [home_assistant]; el apagado de SO se realizó arriba
 power-off-ssh-failures = algunos nodos reportaron fallos SSH durante el apagado
+power-off-extras-skipped = apagado parcial ({ $nodes }): se mantienen las entidades del clúster ({ $extras }) — son compartidas por los nodos activos
 power-on-dryrun = dry-run: omitiendo llamada HA real
 power-on-would-on = sería: HA turn_on { $entity }
 power-on-would-skip = se omitiría: { $name } (sin entity)

+ 1 - 0
locales/fr/main.ftl

@@ -62,6 +62,7 @@ power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
 power-off-waiting = ⏳ attente { $secs}s pour stabilisation des nœuds
 power-off-no-ha = pas de config [home_assistant] ; l'extinction OS a été effectuée ci-dessus
 power-off-ssh-failures = certains nœuds ont signalé des erreurs SSH pendant l'extinction
+power-off-extras-skipped = extinction partielle ({ $nodes }) : entités du cluster laissées activées ({ $extras }) — partagées par les nœuds restants
 power-on-dryrun = dry-run : appel HA réel ignoré
 power-on-would-on = serait : HA turn_on { $entity }
 power-on-would-skip = serait ignoré : { $name } (pas d'entity)

+ 1 - 0
locales/it/main.ftl

@@ -62,6 +62,7 @@ power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
 power-off-waiting = ⏳ attesa { $secs}s per assestamento dei nodi
 power-off-no-ha = nessuna config [home_assistant]; spegnimento OS eseguito sopra
 power-off-ssh-failures = alcuni nodi hanno riportato errori SSH durante lo spegnimento
+power-off-extras-skipped = spegnimento parziale ({ $nodes }): entità del cluster lasciate accese ({ $extras }) — condivise dai nodi attivi
 power-on-dryrun = dry-run: saltando chiamata HA reale
 power-on-would-on = sarebbe: HA turn_on { $entity }
 power-on-would-skip = saltato: { $name } (nessuna entity)

+ 1 - 0
locales/ru/main.ftl

@@ -63,6 +63,7 @@ power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
 power-off-waiting = ⏳ ждем { $secs }с для завершения остановки
 power-off-no-ha = нет [home_assistant] конфига; OS shutdown выполнен
 power-off-ssh-failures = некоторые узлы сообщили об ошибках SSH при остановке
+power-off-extras-skipped = точечное выключение ({ $nodes }): cluster-сущности оставлены включёнными ({ $extras }) — они общие для оставшихся узлов
 power-on-dryrun = dry-run: пропускаем реальный HA вызов
 power-on-would-on = будет: HA turn_on { $entity }
 power-on-would-skip = будет пропущено: { $name } (нет entity)

+ 20 - 4
src/power.rs

@@ -19,7 +19,7 @@ use crate::home_assistant::Client as HaClient;
 pub mod plug;
 pub mod toggle;
 pub use plug::{OnAction, decide_on, stale_nodes};
-use toggle::{call, extra_ids, toggle_entities, toggle_one};
+use toggle::{call, extra_ids, off_extras, toggle_entities, toggle_one};
 mod mikro;
 
 /// Flow for `power off`: SSH shutdown + wait + HA `turn_off`.
@@ -31,9 +31,25 @@ pub async fn power_off(
 ) -> Result<()> {
     let targets = select_targets(cluster, node_names, "power off")?;
 
+    // Cluster-level extras (the fabric switch, a cooling fan) are SHARED by
+    // every node in the cluster.  Powering off a SUBSET (`--node`) must not
+    // take the fabric down for the nodes that stay up — that wedges live
+    // multi-node serves (NCCL over the switch dies).  So extras are toggled
+    // off only for a whole-cluster power off (no `--node`).
+    let extras = off_extras(cluster, node_names);
+    if !node_names.is_empty() && !cluster.extra_entities.is_empty() {
+        info!(
+            "{}",
+            fluent_i18n::t!("power-off-extras-skipped", {
+                "nodes" => node_names.join(", "),
+                "extras" => extra_ids(cluster).join(", ")
+            })
+        );
+    }
+
     if dry_run {
         warn!("{}", fluent_i18n::t!("power-off-dryrun"));
-        mikro::dry_off(config, &targets, &extra_ids(cluster));
+        mikro::dry_off(config, &targets, &extras);
         return Ok(());
     }
 
@@ -92,14 +108,14 @@ pub async fn power_off(
             "turn_off",
             config.mikrotik.as_ref(),
             &targets,
-            &extra_ids(cluster),
+            &extras,
             "cluster",
         )
         .await
     } else {
         info!("{}", fluent_i18n::t!("power-off-no-ha"));
         // No HA at all — still shut down any mikrotik device referenced.
-        mikro::collect_off(config, &targets, &extra_ids(cluster))
+        mikro::collect_off(config, &targets, &extras)
     };
 
     // 4) RouterOS graceful shutdown for each referenced device.

+ 16 - 0
src/power/tests.rs

@@ -1,4 +1,5 @@
 use super::*;
+use crate::power::toggle::off_extras;
 
 fn cfg_with_ha() -> Config {
     let toml = r#"
@@ -110,6 +111,21 @@ fn cluster_extra_entities_parsed() {
     );
 }
 
+#[test]
+fn off_extras_whole_cluster_keeps_extras_subset_skips_them() {
+    // A whole-cluster power off (no --node) turns the shared fabric off too.
+    let cfg = cfg_with_ha();
+    let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
+    assert_eq!(off_extras(&c, &[]), vec!["switch.network_x".to_string()]);
+    // A subset power off (--node) must NOT take the shared switch down —
+    // the nodes that stay up depend on it (NCCL over the fabric).
+    assert_eq!(off_extras(&c, &["n1".to_string()]), Vec::<String>::new());
+    assert_eq!(
+        off_extras(&c, &["n1".to_string(), "n2".to_string()]),
+        Vec::<String>::new()
+    );
+}
+
 #[test]
 fn extra_entity_forms_blocking_and_soft() {
     use crate::config::ExtraEntity;

+ 13 - 0
src/power/toggle.rs

@@ -17,6 +17,19 @@ pub(crate) fn extra_ids(cluster: &Cluster) -> Vec<String> {
         .collect()
 }
 
+/// Cluster-level extras to toggle OFF for a power off: the full list only for
+/// a whole-cluster off (no `--node`); empty for a subset.  The fabric switch
+/// and fan are shared by every node, so powering off one node must not take
+/// the cluster's fabric down for the nodes that stay up (it wedges live
+/// multi-node serves — NCCL over the switch dies).
+pub(crate) fn off_extras(cluster: &Cluster, node_names: &[String]) -> Vec<String> {
+    if node_names.is_empty() {
+        extra_ids(cluster)
+    } else {
+        Vec::new()
+    }
+}
+
 /// Handle one entity id for a power toggle: dispatch `[mikrotik]`
 /// devices to the RouterOS API path (or their HA relay), everything else
 /// to HA with `domain.name` validation.