|
|
@@ -183,6 +183,129 @@ async fn call(ha: &HaClient, entity: &str, service: &'static str) -> Result<()>
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/// Flow for `power switch --from <src> --to <tgt>`:
|
|
|
+/// 1. Shut down + HA turn_off nodes/entities unique to `from`,
|
|
|
+/// 2. HA turn_on all nodes/entities in `to`.
|
|
|
+pub async fn power_switch(
|
|
|
+ config: &Config,
|
|
|
+ from: &Cluster,
|
|
|
+ to: &Cluster,
|
|
|
+ dry_run: bool,
|
|
|
+) -> Result<()> {
|
|
|
+ let to_names: std::collections::HashSet<&str> =
|
|
|
+ to.nodes.iter().map(|n| n.name.as_str()).collect();
|
|
|
+ let disable_nodes: Vec<Node> = from
|
|
|
+ .nodes
|
|
|
+ .iter()
|
|
|
+ .filter(|n| !to_names.contains(n.name.as_str()))
|
|
|
+ .cloned()
|
|
|
+ .collect();
|
|
|
+ let to_extras: std::collections::HashSet<&str> =
|
|
|
+ to.extra_entities.iter().map(|e| e.as_str()).collect();
|
|
|
+ let disable_extras: Vec<&String> = from
|
|
|
+ .extra_entities
|
|
|
+ .iter()
|
|
|
+ .filter(|e| !to_extras.contains(e.as_str()))
|
|
|
+ .collect();
|
|
|
+
|
|
|
+ info!(
|
|
|
+ "🔌 switch: {} → {} ({} node(s) + {} entity(ies) to disable)",
|
|
|
+ from.name,
|
|
|
+ to.name,
|
|
|
+ disable_nodes.len(),
|
|
|
+ disable_extras.len()
|
|
|
+ );
|
|
|
+
|
|
|
+ if dry_run {
|
|
|
+ for node in &disable_nodes {
|
|
|
+ println!(
|
|
|
+ "would: ssh {}@{} sudo shutdown now",
|
|
|
+ config.ssh_user, node.ssh_ip
|
|
|
+ );
|
|
|
+ if let Some(entity) = &node.entity {
|
|
|
+ println!("would: HA turn_off {}", entity);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for extra in &disable_extras {
|
|
|
+ println!("would: HA turn_off {} (from extras)", extra);
|
|
|
+ }
|
|
|
+ for node in &to.nodes {
|
|
|
+ if let Some(entity) = &node.entity {
|
|
|
+ println!("would: HA turn_on {}", entity);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for extra in &to.extra_entities {
|
|
|
+ println!("would: HA turn_on {} (to extras)", extra);
|
|
|
+ }
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
+
|
|
|
+ // SSH shutdown nodes not in target
|
|
|
+ let mut handles = Vec::with_capacity(disable_nodes.len());
|
|
|
+ for node in disable_nodes.iter().cloned() {
|
|
|
+ let c = config.clone();
|
|
|
+ handles.push(tokio::spawn(async move {
|
|
|
+ (
|
|
|
+ node.name.clone(),
|
|
|
+ crate::ssh::run_ssh(&c, &node, "sudo shutdown now".into(), false).await,
|
|
|
+ )
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ for h in handles {
|
|
|
+ let (name, r) = h.await?;
|
|
|
+ if let Err(e) = r {
|
|
|
+ warn!("✗ {} shutdown: {}", name, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let delay = config
|
|
|
+ .home_assistant
|
|
|
+ .as_ref()
|
|
|
+ .map(|c| c.shutdown_delay_seconds)
|
|
|
+ .unwrap_or(30);
|
|
|
+ if !disable_nodes.is_empty() {
|
|
|
+ info!("⏳ waiting {}s for shutdown", delay);
|
|
|
+ tokio::time::sleep(Duration::from_secs(delay)).await;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 node in &disable_nodes {
|
|
|
+ if let Some(entity) = &node.entity {
|
|
|
+ if let Err(e) = split_entity(entity) {
|
|
|
+ warn!("{}: entity '{}': {}", node.name, entity, e);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if dry_run {
|
|
|
+ println!("would: HA turn_off {}", entity);
|
|
|
+ } else {
|
|
|
+ let _ = ha.turn_off(entity).await;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for extra in &disable_extras {
|
|
|
+ if dry_run {
|
|
|
+ println!("would: HA turn_off {} (from extras)", extra);
|
|
|
+ } else {
|
|
|
+ let _ = ha.turn_off(extra).await;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ info!(
|
|
|
+ "💡 switch: powering on {} node(s) + {} entity(ies) to {}",
|
|
|
+ to.nodes.len(),
|
|
|
+ to.extra_entities.len(),
|
|
|
+ to.name
|
|
|
+ );
|
|
|
+ toggle_entities(&ha, "turn_on", &to.nodes, &to.extra_entities, "to").await;
|
|
|
+ } else {
|
|
|
+ info!("no [home_assistant] config; OS shutdown was performed");
|
|
|
+ }
|
|
|
+ Ok(())
|
|
|
+}
|
|
|
+
|
|
|
fn select_targets(cluster: &Cluster, node_name: Option<&str>, op: &str) -> Result<Vec<Node>> {
|
|
|
let targets: Vec<Node> = match node_name {
|
|
|
Some(name) => cluster
|