Преглед изворни кода

fix(serve): dry-run skips SSH probing so it works with cluster off

prepare_cluster always SSH-probes every node, filters to reachable+
unoccupied ones, then bails with "requires N nodes, but cluster has
only 0 nodes" when the cluster is off — so `-d serve` was useless for
previewing the generated docker run command without hardware up.

Add prepare_cluster_dry_run in orchestrate: when opts.dry_run, serve
calls it instead of prepare_cluster. It reuses resolve_cluster_simple
(treats every configured node as reachable+free) + select_cluster, so:
- no SSH probing, power-on, or pinging
- works with the cluster fully off or partially unreachable
- keeps all configured nodes (no free-node filtering — none inspected)
- auto-selection by required_nodes still works (smallest fitting cluster)
- solo-node (--node) path also skips probing

2 new tests cover the all-unreachable case and the solo-node case.
Alexander Korolev пре 3 месеци
родитељ
комит
ac19fa45b8
3 измењених фајлова са 65 додато и 11 уклоњено
  1. 17 11
      src/docker/mod.rs
  2. 21 0
      src/orchestrate/mod.rs
  3. 27 0
      src/orchestrate/tests.rs

+ 17 - 11
src/docker/mod.rs

@@ -63,17 +63,23 @@ pub async fn serve(
     } else {
         None
     };
-    let (cluster, nccl) = prepare_cluster(
-        config,
-        &recipe,
-        cluster_name,
-        node_name,
-        opts.auto_power_on,
-        &SshProber,
-        power_ctrl.as_ref(),
-        &SshNodePinger,
-    )
-    .await?;
+    let (cluster, nccl) = if opts.dry_run {
+        // Dry-run: skip all SSH probing/power/pinging so it works even when the
+        // cluster is off or unreachable (treat every configured node as free).
+        crate::orchestrate::prepare_cluster_dry_run(config, &recipe, cluster_name, node_name)?
+    } else {
+        prepare_cluster(
+            config,
+            &recipe,
+            cluster_name,
+            node_name,
+            opts.auto_power_on,
+            &SshProber,
+            power_ctrl.as_ref(),
+            &SshNodePinger,
+        )
+        .await?
+    };
     crate::before_serve::run(
         config,
         &cluster,

+ 21 - 0
src/orchestrate/mod.rs

@@ -143,6 +143,27 @@ fn resolve_cluster_simple<'c>(
     select_cluster(config, cluster_name, recipe.required_nodes, &state)
 }
 
+/// Dry-run cluster resolution: treat every configured node as reachable and
+/// free, then select. Skips all SSH probing/power/pinging so a dry-run works
+/// even when the cluster is off or unreachable (the whole point of a dry-run
+/// is to preview the generated command, not to verify hardware).
+///
+/// Like [`prepare_cluster`], returns the cluster with **all** its nodes (dry-run
+/// does not filter for free nodes, since none are actually inspected).
+pub fn prepare_cluster_dry_run(
+    config: &Config,
+    recipe: &Recipe,
+    cluster_name: &str,
+    node_name: Option<&str>,
+) -> Result<(Cluster, NcclConfig)> {
+    if let Some(nn) = node_name {
+        let (c, n) = resolve_solo_node(config, nn)?;
+        return Ok((c, n.clone()));
+    }
+    let (c, n) = resolve_cluster_simple(config, recipe, cluster_name)?;
+    Ok((c.clone(), n.clone()))
+}
+
 /// Does the state contain any unreachable nodes?
 fn state_has_unreachable(state: &ClusterState, config: &Config) -> bool {
     for cluster in &config.clusters {

+ 27 - 0
src/orchestrate/tests.rs

@@ -334,3 +334,30 @@ async fn occupied_nodes_excluded() {
     assert_eq!(cluster.nodes.len(), 1);
     assert_eq!(cluster.nodes[0].name, "n2");
 }
+
+#[test]
+fn dry_run_resolves_without_probing_when_all_nodes_unreachable() {
+    // Dry-run must resolve a cluster even when no node is reachable — the
+    // whole point is to preview the command without touching hardware.
+    let c = test_cluster("c", &["n1", "n2"]);
+    let config = test_config(vec![c.clone()]);
+    let recipe = test_recipe(Some(2));
+
+    let (cluster, _nccl) =
+        crate::orchestrate::prepare_cluster_dry_run(&config, &recipe, "auto", None).unwrap();
+    assert_eq!(cluster.name, "c");
+    // Dry-run keeps every configured node (no free-node filtering).
+    assert_eq!(cluster.nodes.len(), 2);
+}
+
+#[test]
+fn dry_run_solo_node_skips_probing() {
+    let c = test_cluster("c", &["n1", "n2"]);
+    let config = test_config(vec![c.clone()]);
+    let recipe = test_recipe(Some(1));
+
+    let (cluster, _nccl) =
+        crate::orchestrate::prepare_cluster_dry_run(&config, &recipe, "auto", Some("n2")).unwrap();
+    assert_eq!(cluster.nodes.len(), 1);
+    assert_eq!(cluster.nodes[0].name, "n2");
+}