Преглед на файлове

power.rs back under the 500-line cap: HA entity toggling (toggle_one/toggle_entities/extra_ids/call) moved to power/toggle.rs, plug decisions (OnAction/decide_on/stale_nodes) to power/plug.rs; stale doc comment cleaned

Alexander Korolev преди 2 седмици
родител
ревизия
162b9417a5
променени са 3 файла, в които са добавени 179 реда и са изтрити 160 реда
  1. 8 160
      src/power.rs
  2. 49 0
      src/power/plug.rs
  3. 122 0
      src/power/toggle.rs

+ 8 - 160
src/power.rs

@@ -13,11 +13,13 @@ use anyhow::Result;
 use std::time::Duration;
 use tracing::{info, warn};
 
-use crate::home_assistant::split_entity;
-
-use crate::config::{Cluster, Config, MikrotikConfig, Node};
+use crate::config::{Cluster, Config, Node};
 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};
 mod mikro;
 
 /// Flow for `power off`: SSH shutdown + wait + HA `turn_off`.
@@ -112,53 +114,12 @@ pub async fn power_off(
 /// Flow for `power on`: HA `turn_on` for each node with an `entity`
 /// and each cluster-level `extra_entities`.  No SSH needed — the smart
 /// plug handles waking the machine.
-/// What `power on` should do with one node's plug.
-#[derive(Debug, PartialEq, Eq)]
-pub(crate) enum OnAction {
-    /// Plug off (or state unknown) — plain `turn_on`.
-    TurnOn,
-    /// Plug reads on but the node is believed down — cut and re-apply
-    /// (a graceful shutdown leaves the plug on while the machine sits in
-    /// soft-off, so a plain `turn_on` would be a silent no-op).
-    Cycle,
-    /// Plug on and the node is believed up — leave it alone.
-    AlreadyOn,
-}
-
-/// SGR-free decision: pure so the semantics are testable.
-pub(crate) fn decide_on(state: Option<&str>, node_is_down: bool) -> OnAction {
-    match state {
-        Some("on") if node_is_down => OnAction::Cycle,
-        Some("on") => OnAction::AlreadyOn,
-        _ => OnAction::TurnOn,
-    }
-}
-
-/// TCP connect to the node's SSH port (1.5 s) — a soft-off machine refuses
-/// or times out, a live one accepts. Not an SSH handshake: no auth, cheap.
-pub(crate) async fn stale_nodes(targets: &[Node]) -> Vec<String> {
-    let mut out = Vec::new();
-    for node in targets {
-        let (host, port) = match node.ssh_ip.rsplit_once(':') {
-            Some((h, p)) if p.parse::<u16>().is_ok() => (h.to_string(), p.parse().unwrap()),
-            _ => (node.ssh_ip.clone(), 22),
-        };
-        let attempt = tokio::time::timeout(
-            Duration::from_millis(1500),
-            tokio::net::TcpStream::connect((host.as_str(), port)),
-        )
-        .await;
-        if !matches!(attempt, Ok(Ok(_))) {
-            out.push(node.name.clone());
-        }
-    }
-    out
-}
-
+/// Flow for `power on`: HA `turn_on` for each node with an `entity`
 /// Flow for `power on`: per-node plug decisions (`decide_on` + the caller's
 /// `stale` list), then cluster-level `extra_entities`.  Returns one report
 /// line per node — the job surface (web job message, CLI log) tells what
-/// actually happened instead of a blanket "ok".
+/// actually happened instead of a blanket "ok".  No SSH needed — the smart
+/// plug (plus a cycle for stale-on plugs) wakes the machine.
 pub async fn power_on(
     config: &Config,
     cluster: &Cluster,
@@ -289,119 +250,6 @@ pub async fn power_on(
     Ok(report)
 }
 
-/// 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()
-}
-
-/// 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.
-async fn toggle_one(
-    ha: &HaClient,
-    service: &'static str,
-    mikrotik: Option<&MikrotikConfig>,
-    entity: &str,
-    name_for_log: &str,
-    mikrotik_off: &mut Vec<String>,
-) {
-    // Mikrotik devices are addressed by exact name — dispatch before any
-    // HA validation (a router name could otherwise parse as a fictional
-    // `mikrotik.<x>` HA entity).
-    if let Some(dev) = mikro::device(mikrotik, entity) {
-        if service == "turn_off" {
-            if !mikrotik_off.iter().any(|n| n == &dev.name) {
-                mikrotik_off.push(dev.name.clone());
-            }
-            info!(
-                "{}",
-                fluent_i18n::t!("power-off-mikrotik-skip", { "entity" => entity.to_string(), "address" => dev.address.clone() })
-            );
-            return;
-        }
-        if let Some(relay) = &dev.on_entity {
-            if let Err(e) = call(ha, relay, service).await {
-                warn!(
-                    "{}",
-                    fluent_i18n::t!("ha-extra-failed", { "service" => service, "extra" => relay.clone(), "label" => name_for_log.to_string(), "error" => e.to_string() })
-                );
-            }
-        } else {
-            warn!(
-                "{}",
-                fluent_i18n::t!("power-on-mikrotik-manual", { "name" => dev.name.clone() })
-            );
-        }
-        return;
-    }
-    if let Err(e) = split_entity(entity) {
-        warn!(
-            "{}",
-            fluent_i18n::t!("ha-invalid-entity", { "name" => name_for_log.to_string(), "entity" => entity.to_string(), "error" => e.to_string() })
-        );
-        return;
-    }
-    if let Err(e) = call(ha, entity, service).await {
-        warn!(
-            "{}",
-            fluent_i18n::t!("ha-call-failed", { "service" => service, "entity" => entity.to_string(), "error" => e.to_string() })
-        );
-    }
-}
-
-/// Call HA `service` for each per-node entity, then each cluster-level
-/// `extra_entities`.  Entities that name a `[mikrotik]` device are NOT
-/// sent to HA: for `turn_off` their device names are returned (the caller
-/// does the RouterOS API shutdown afterwards); for `turn_on` an optional
-/// `on_entity` relay is toggled in HA instead, or a manual-power warning
-/// is logged.
-async fn toggle_entities(
-    ha: &HaClient,
-    service: &'static str,
-    mikrotik: Option<&MikrotikConfig>,
-    targets: &[Node],
-    extras: &[String],
-    extras_label: &'static str,
-) -> Vec<String> {
-    let mut mikrotik_off = Vec::new();
-    for node in targets {
-        if let Some(entity) = &node.entity {
-            toggle_one(ha, service, mikrotik, entity, &node.name, &mut mikrotik_off).await;
-        } else {
-            warn!(
-                "{}",
-                fluent_i18n::t!("ha-skip-no-entity", { "name" => &node.name })
-            );
-        }
-    }
-    for extra in extras {
-        toggle_one(
-            ha,
-            service,
-            mikrotik,
-            extra,
-            extras_label,
-            &mut mikrotik_off,
-        )
-        .await;
-    }
-    mikrotik_off
-}
-
-/// Dispatch to the right HA service.  Inlined because each call only
-/// differs by `service`, and inlining keeps the trait surface small.
-async fn call(ha: &HaClient, entity: &str, service: &'static str) -> Result<()> {
-    match service {
-        "turn_off" => ha.turn_off(entity).await,
-        "turn_on" => ha.turn_on(entity).await,
-        other => anyhow::bail!("unsupported HA service '{}'", other),
-    }
-}
-
 /// 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`.

+ 49 - 0
src/power/plug.rs

@@ -0,0 +1,49 @@
+//! Per-node plug decisions for `power on`: what to do when a plug reads
+//! on but the node is believed down (graceful shutdowns leave the plug on
+//! while the machine sits in soft-off).
+
+use super::Node;
+use std::time::Duration;
+
+/// What `power on` should do with one node's plug.
+#[derive(Debug, PartialEq, Eq)]
+pub enum OnAction {
+    /// Plug off (or state unknown) — plain `turn_on`.
+    TurnOn,
+    /// Plug reads on but the node is believed down — cut and re-apply
+    /// (a graceful shutdown leaves the plug on while the machine sits in
+    /// soft-off, so a plain `turn_on` would be a silent no-op).
+    Cycle,
+    /// Plug on and the node is believed up — leave it alone.
+    AlreadyOn,
+}
+
+/// SGR-free decision: pure so the semantics are testable.
+pub fn decide_on(state: Option<&str>, node_is_down: bool) -> OnAction {
+    match state {
+        Some("on") if node_is_down => OnAction::Cycle,
+        Some("on") => OnAction::AlreadyOn,
+        _ => OnAction::TurnOn,
+    }
+}
+
+/// TCP connect to the node's SSH port (1.5 s) — a soft-off machine refuses
+/// or times out, a live one accepts. Not an SSH handshake: no auth, cheap.
+pub async fn stale_nodes(targets: &[Node]) -> Vec<String> {
+    let mut out = Vec::new();
+    for node in targets {
+        let (host, port) = match node.ssh_ip.rsplit_once(':') {
+            Some((h, p)) if p.parse::<u16>().is_ok() => (h.to_string(), p.parse().unwrap()),
+            _ => (node.ssh_ip.clone(), 22),
+        };
+        let attempt = tokio::time::timeout(
+            Duration::from_millis(1500),
+            tokio::net::TcpStream::connect((host.as_str(), port)),
+        )
+        .await;
+        if !matches!(attempt, Ok(Ok(_))) {
+            out.push(node.name.clone());
+        }
+    }
+    out
+}

+ 122 - 0
src/power/toggle.rs

@@ -0,0 +1,122 @@
+//! HA entity toggling shared by the power flows: per-node plug calls,
+//! RouterOS dispatch, cluster extras, and the raw service call.
+
+use super::HaClient;
+use super::mikro;
+use crate::config::{Cluster, MikrotikConfig, Node};
+use crate::home_assistant::split_entity;
+use anyhow::Result;
+use tracing::{info, warn};
+
+/// Entity ids of a cluster's `extra_entities` (both forms).
+pub(crate) fn extra_ids(cluster: &Cluster) -> Vec<String> {
+    cluster
+        .extra_entities
+        .iter()
+        .map(|e| e.entity().to_string())
+        .collect()
+}
+
+/// 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.
+pub(crate) async fn toggle_one(
+    ha: &HaClient,
+    service: &'static str,
+    mikrotik: Option<&MikrotikConfig>,
+    entity: &str,
+    name_for_log: &str,
+    mikrotik_off: &mut Vec<String>,
+) {
+    // Mikrotik devices are addressed by exact name — dispatch before any
+    // HA validation (a router name could otherwise parse as a fictional
+    // `mikrotik.<x>` HA entity).
+    if let Some(dev) = mikro::device(mikrotik, entity) {
+        if service == "turn_off" {
+            if !mikrotik_off.iter().any(|n| n == &dev.name) {
+                mikrotik_off.push(dev.name.clone());
+            }
+            info!(
+                "{}",
+                fluent_i18n::t!("power-off-mikrotik-skip", { "entity" => entity.to_string(), "address" => dev.address.clone() })
+            );
+            return;
+        }
+        if let Some(relay) = &dev.on_entity {
+            if let Err(e) = call(ha, relay, service).await {
+                warn!(
+                    "{}",
+                    fluent_i18n::t!("ha-extra-failed", { "service" => service, "extra" => relay.clone(), "label" => name_for_log.to_string(), "error" => e.to_string() })
+                );
+            }
+        } else {
+            warn!(
+                "{}",
+                fluent_i18n::t!("power-on-mikrotik-manual", { "name" => dev.name.clone() })
+            );
+        }
+        return;
+    }
+    if let Err(e) = split_entity(entity) {
+        warn!(
+            "{}",
+            fluent_i18n::t!("ha-invalid-entity", { "name" => name_for_log.to_string(), "entity" => entity.to_string(), "error" => e.to_string() })
+        );
+        return;
+    }
+    if let Err(e) = call(ha, entity, service).await {
+        warn!(
+            "{}",
+            fluent_i18n::t!("ha-call-failed", { "service" => service, "entity" => entity.to_string(), "error" => e.to_string() })
+        );
+    }
+}
+
+/// Call HA `service` for each per-node entity, then each cluster-level
+/// `extra_entities`.  Entities that name a `[mikrotik]` device are NOT
+/// sent to HA: for `turn_off` their device names are returned (the caller
+/// does the RouterOS API shutdown afterwards); for `turn_on` an optional
+/// `on_entity` relay is toggled in HA instead, or a manual-power warning
+/// is logged.
+pub(crate) async fn toggle_entities(
+    ha: &HaClient,
+    service: &'static str,
+    mikrotik: Option<&MikrotikConfig>,
+    targets: &[Node],
+    extras: &[String],
+    extras_label: &'static str,
+) -> Vec<String> {
+    let mut mikrotik_off = Vec::new();
+    for node in targets {
+        if let Some(entity) = &node.entity {
+            toggle_one(ha, service, mikrotik, entity, &node.name, &mut mikrotik_off).await;
+        } else {
+            warn!(
+                "{}",
+                fluent_i18n::t!("ha-skip-no-entity", { "name" => &node.name })
+            );
+        }
+    }
+    for extra in extras {
+        toggle_one(
+            ha,
+            service,
+            mikrotik,
+            extra,
+            extras_label,
+            &mut mikrotik_off,
+        )
+        .await;
+    }
+    mikrotik_off
+}
+
+/// Dispatch to the right HA service.  Inlined because each call only
+/// differs by `service`, and inlining keeps the trait surface small.
+pub(crate) async fn call(ha: &HaClient, entity: &str, service: &'static str) -> Result<()> {
+    match service {
+        "turn_off" => ha.turn_off(entity).await,
+        "turn_on" => ha.turn_on(entity).await,
+        other => anyhow::bail!("unsupported HA service '{}'", other),
+    }
+}