|
|
@@ -42,9 +42,10 @@ pub async fn power_off(
|
|
|
}
|
|
|
}
|
|
|
for extra in &cluster.extra_entities {
|
|
|
+ let id = extra.entity().to_string();
|
|
|
println!(
|
|
|
"{}",
|
|
|
- fluent_i18n::t!("power-switch-would-off-extra", { "extra" => extra })
|
|
|
+ fluent_i18n::t!("power-switch-would-off-extra", { "extra" => &id })
|
|
|
);
|
|
|
}
|
|
|
return Ok(());
|
|
|
@@ -98,14 +99,7 @@ pub async fn power_off(
|
|
|
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);
|
|
|
- toggle_entities(
|
|
|
- &ha,
|
|
|
- "turn_off",
|
|
|
- &targets,
|
|
|
- &cluster.extra_entities,
|
|
|
- "cluster",
|
|
|
- )
|
|
|
- .await;
|
|
|
+ toggle_entities(&ha, "turn_off", &targets, &extra_ids(cluster), "cluster").await;
|
|
|
} else {
|
|
|
info!("{}", fluent_i18n::t!("power-off-no-ha"));
|
|
|
}
|
|
|
@@ -152,9 +146,10 @@ pub async fn power_on(
|
|
|
}
|
|
|
}
|
|
|
for extra in &cluster.extra_entities {
|
|
|
+ let id = extra.entity().to_string();
|
|
|
println!(
|
|
|
"{}",
|
|
|
- fluent_i18n::t!("power-on-would-on", { "entity" => extra })
|
|
|
+ fluent_i18n::t!("power-on-would-on", { "entity" => &id })
|
|
|
);
|
|
|
}
|
|
|
return Ok(());
|
|
|
@@ -164,10 +159,19 @@ pub async fn power_on(
|
|
|
"{}",
|
|
|
fluent_i18n::t!("power-on-head", { "nodes" => targets.len() })
|
|
|
);
|
|
|
- toggle_entities(&ha, "turn_on", &targets, &cluster.extra_entities, "cluster").await;
|
|
|
+ toggle_entities(&ha, "turn_on", &targets, &extra_ids(cluster), "cluster").await;
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
+/// Entity ids of a cluster's `extra_entities` (both forms).
|
|
|
+fn extra_ids(cluster: &Cluster) -> Vec<String> {
|
|
|
+ cluster
|
|
|
+ .extra_entities
|
|
|
+ .iter()
|
|
|
+ .map(|e| e.entity().to_string())
|
|
|
+ .collect()
|
|
|
+}
|
|
|
+
|
|
|
/// Call HA `service` for each per-node entity, then each cluster-level
|
|
|
/// `extra_entities`. Validation is the same in both cases (entity IDs
|
|
|
/// must be `domain.name`).
|
|
|
@@ -245,12 +249,12 @@ pub async fn power_switch(
|
|
|
.cloned()
|
|
|
.collect();
|
|
|
let to_extras: std::collections::HashSet<&str> =
|
|
|
- to.extra_entities.iter().map(|e| e.as_str()).collect();
|
|
|
+ to.extra_entities.iter().map(|e| e.entity()).collect();
|
|
|
let disable_extras: Vec<String> = from
|
|
|
.extra_entities
|
|
|
.iter()
|
|
|
- .filter(|e| !to_extras.contains(e.as_str()))
|
|
|
- .cloned()
|
|
|
+ .filter(|e| !to_extras.contains(e.entity()))
|
|
|
+ .map(|e| e.entity().to_string())
|
|
|
.collect();
|
|
|
|
|
|
info!(
|
|
|
@@ -286,9 +290,10 @@ pub async fn power_switch(
|
|
|
}
|
|
|
}
|
|
|
for extra in &to.extra_entities {
|
|
|
+ let id = extra.entity().to_string();
|
|
|
println!(
|
|
|
"{}",
|
|
|
- fluent_i18n::t!("power-switch-would-on-extra", { "extra" => extra })
|
|
|
+ fluent_i18n::t!("power-switch-would-on-extra", { "extra" => &id })
|
|
|
);
|
|
|
}
|
|
|
return Ok(());
|
|
|
@@ -345,7 +350,7 @@ pub async fn power_switch(
|
|
|
"{}",
|
|
|
fluent_i18n::t!("power-switch-powering-on", { "nodes" => to.nodes.len(), "entities" => to.extra_entities.len(), "to" => &to.name })
|
|
|
);
|
|
|
- toggle_entities(&ha, "turn_on", &to.nodes, &to.extra_entities, "to").await;
|
|
|
+ toggle_entities(&ha, "turn_on", &to.nodes, &extra_ids(to), "to").await;
|
|
|
} else {
|
|
|
info!("{}", fluent_i18n::t!("power-switch-no-ha"));
|
|
|
}
|
|
|
@@ -382,119 +387,4 @@ fn select_targets(cluster: &Cluster, node_names: &[String], op: &str) -> Result<
|
|
|
}
|
|
|
|
|
|
#[cfg(test)]
|
|
|
-mod tests {
|
|
|
- use super::*;
|
|
|
-
|
|
|
- fn cfg_with_ha() -> Config {
|
|
|
- let toml = r#"
|
|
|
-ssh_user = "u"
|
|
|
-ssh_key_path = "/k"
|
|
|
-[[clusters]]
|
|
|
-name = "x"
|
|
|
-master_ib_ip = "1.1.1.1"
|
|
|
-nodes = [
|
|
|
- { name = "n1", ssh_ip = "10.0.0.1", ib_ip = "10.0.0.1", rank = 0, entity = "switch.n1" },
|
|
|
- { name = "n2", ssh_ip = "10.0.0.2", ib_ip = "10.0.0.2", rank = 1 },
|
|
|
-]
|
|
|
-extra_entities = ["switch.network_x"]
|
|
|
-[nccl."x"]
|
|
|
-ib_hca = "h"
|
|
|
-socket_ifname = "s"
|
|
|
-ib_gid_index = 1
|
|
|
-ib_disable = 0
|
|
|
-
|
|
|
-[home_assistant]
|
|
|
-url = "http://ha.local:8123"
|
|
|
-token = "abcdef0123456789abcdef0123456789"
|
|
|
-shutdown_delay_seconds = 5
|
|
|
-"#;
|
|
|
- toml::from_str(toml).unwrap()
|
|
|
- }
|
|
|
-
|
|
|
- fn cfg_no_ha() -> Config {
|
|
|
- let toml = r#"
|
|
|
-ssh_user = "u"
|
|
|
-ssh_key_path = "/k"
|
|
|
-[[clusters]]
|
|
|
-name = "x"
|
|
|
-master_ib_ip = "1.1.1.1"
|
|
|
-nodes = [
|
|
|
- { name = "n1", ssh_ip = "10.0.0.1", ib_ip = "10.0.0.1", rank = 0, entity = "switch.n1" },
|
|
|
-]
|
|
|
-[nccl."x"]
|
|
|
-ib_hca = "h"
|
|
|
-socket_ifname = "s"
|
|
|
-ib_gid_index = 1
|
|
|
-ib_disable = 0
|
|
|
-"#;
|
|
|
- toml::from_str(toml).unwrap()
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn select_targets_filters_by_node() {
|
|
|
- let cfg = cfg_with_ha();
|
|
|
- let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
|
|
|
- let t = select_targets(&c, &["n1".to_string()], "test").unwrap();
|
|
|
- assert_eq!(t.len(), 1);
|
|
|
- assert_eq!(t[0].name, "n1");
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn select_targets_all() {
|
|
|
- let cfg = cfg_with_ha();
|
|
|
- let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
|
|
|
- let t = select_targets(&c, &[], "test").unwrap();
|
|
|
- assert_eq!(t.len(), 2);
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn select_targets_multiple_nodes() {
|
|
|
- // `stop --node a --node b --off` must power off *both* nodes, not just
|
|
|
- // the first. (Regression for I-5.)
|
|
|
- let cfg = cfg_with_ha();
|
|
|
- let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
|
|
|
- let t = select_targets(&c, &["n1".to_string(), "n2".to_string()], "test").unwrap();
|
|
|
- assert_eq!(t.len(), 2);
|
|
|
- let names: Vec<_> = t.iter().map(|n| n.name.as_str()).collect();
|
|
|
- assert!(names.contains(&"n1"));
|
|
|
- assert!(names.contains(&"n2"));
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn select_targets_partial_missing_errors() {
|
|
|
- let cfg = cfg_with_ha();
|
|
|
- let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
|
|
|
- // One valid + one missing → error listing the missing one.
|
|
|
- let r = select_targets(&c, &["n1".to_string(), "nope".to_string()], "test");
|
|
|
- assert!(r.is_err());
|
|
|
- assert!(r.unwrap_err().to_string().contains("nope"));
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn select_targets_unknown_node_errors() {
|
|
|
- let cfg = cfg_with_ha();
|
|
|
- let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
|
|
|
- let r = select_targets(&c, &["nope".to_string()], "test");
|
|
|
- assert!(r.is_err());
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn power_on_requires_ha_config() {
|
|
|
- let cfg = cfg_no_ha();
|
|
|
- assert!(cfg.home_assistant.is_none());
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn cluster_extra_entities_parsed() {
|
|
|
- let cfg = cfg_with_ha();
|
|
|
- let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
|
|
|
- assert_eq!(c.extra_entities, vec!["switch.network_x"]);
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn cluster_without_extra_entities_is_empty() {
|
|
|
- let cfg = cfg_no_ha();
|
|
|
- let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
|
|
|
- assert!(c.extra_entities.is_empty());
|
|
|
- }
|
|
|
-}
|
|
|
+mod tests;
|