cli.rs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. use clap::{Parser, Subcommand};
  2. #[derive(Parser)]
  3. #[command(author, version, about = "DGX Spark Launcher for vLLM and SGLang")]
  4. pub struct Cli {
  5. #[arg(long, short, default_value = "config.toml")]
  6. pub config: String,
  7. #[arg(long, short, help = "Show commands without executing")]
  8. pub dry_run: bool,
  9. #[command(subcommand)]
  10. pub command: Commands,
  11. }
  12. #[derive(Subcommand)]
  13. pub enum Commands {
  14. /// Launch a cluster
  15. Serve {
  16. #[arg(long, required = true)]
  17. recipe: String,
  18. #[arg(long, default_value = "auto")]
  19. cluster: String,
  20. /// Target a specific node within the cluster (default: all).
  21. #[arg(long)]
  22. node: Option<String>,
  23. /// Keep containers after stop (default: auto-remove on stop)
  24. #[arg(long)]
  25. keep: bool,
  26. /// Skip pre-serve hooks (cluster-level + recipe-level).
  27. #[arg(long)]
  28. skip_before_serve: bool,
  29. /// Follow logs after serving (use --no-follow to skip).
  30. #[arg(long, action = clap::ArgAction::SetFalse, default_value_t = true)]
  31. follow: bool,
  32. /// Power on cluster if switch is off (requires HA config).
  33. #[arg(short = 'O', long = "on")]
  34. auto_power_on: bool,
  35. /// Sync Docker image across nodes before starting.
  36. #[arg(long, default_value_t = true)]
  37. sync_image: bool,
  38. /// Explicit source node for image sync (default: auto-detect).
  39. #[arg(long)]
  40. sync_image_source: Option<String>,
  41. /// Build image on this node if not found (requires docker-images/<recipe>/).
  42. #[arg(long)]
  43. build_node: Option<String>,
  44. /// Force rebuild even if checksum matches.
  45. #[arg(long)]
  46. force_build: bool,
  47. /// Run even if nodes are occupied (another container running).
  48. /// Skips the occupancy check during cluster selection — use to run
  49. /// a lightweight test recipe alongside a live serve, or to force
  50. /// onto a busy node. Dangerous for GPU recipes (OOM, port
  51. /// conflicts); the operator is responsible for coexistence.
  52. #[arg(long)]
  53. force: bool,
  54. /// Serve single-node on the first free node found (overrides recipe
  55. /// `solo`). Use `--no-solo` to force multi-node even if the recipe
  56. /// sets `solo = true`.
  57. #[arg(long)]
  58. solo: Option<bool>,
  59. },
  60. /// Show container logs
  61. Logs {
  62. #[arg(long, required = true)]
  63. recipe: String,
  64. #[arg(long, action = clap::ArgAction::SetTrue)]
  65. follow: bool,
  66. #[arg(long, default_value = "auto")]
  67. cluster: String,
  68. /// Target a specific node within the cluster (default: head node if following, all nodes if not).
  69. #[arg(long)]
  70. node: Option<String>,
  71. },
  72. /// Stop a cluster
  73. Stop {
  74. #[arg(long, required = true)]
  75. recipe: String,
  76. /// Target specific nodes (repeatable, e.g. --node dgx1 --node dgx2).
  77. #[arg(long)]
  78. node: Vec<String>,
  79. #[arg(long, default_value = "auto")]
  80. cluster: String,
  81. /// Power off cluster after stopping containers (requires HA config).
  82. #[arg(long = "off")]
  83. power_off: bool,
  84. },
  85. /// Check cluster health
  86. Health {
  87. #[arg(
  88. long,
  89. help = "Cluster name (e.g. 4nodes-switch). If omitted — checks all."
  90. )]
  91. cluster: Option<String>,
  92. #[arg(long, help = "Show node host keys (for adding to config.toml)")]
  93. fingerprint: bool,
  94. },
  95. /// Check and repair a recipe (doctor mode)
  96. Doctor {
  97. #[arg(long, required = true, help = "Recipe name to check")]
  98. recipe: String,
  99. #[arg(
  100. long,
  101. default_value = "auto",
  102. help = "Cluster (if not auto — uses recipe.cluster)"
  103. )]
  104. cluster: String,
  105. #[arg(long, help = "Auto-fix issues (create Dockerfile + build image)")]
  106. repair: bool,
  107. },
  108. /// Manage tmux sessions (Docker alternative)
  109. #[command(subcommand)]
  110. Tmux(TmuxCommands),
  111. /// Auto-discover host keys and write them into config.toml
  112. InitHostKeys {
  113. #[arg(long, help = "Cluster name (e.g. 4nodes-switch). If omitted — all.")]
  114. cluster: Option<String>,
  115. },
  116. /// Run an arbitrary shell command on a cluster or single node
  117. Exec {
  118. /// Cluster name (mutually exclusive with --recipe).
  119. #[arg(long, conflicts_with = "recipe")]
  120. cluster: Option<String>,
  121. /// Recipe name; resolves to the recipe's pinned cluster.
  122. #[arg(long, conflicts_with = "cluster")]
  123. recipe: Option<String>,
  124. /// Target a specific node within the cluster (default: all).
  125. #[arg(long)]
  126. node: Option<String>,
  127. /// Name of an alias from `[exec_aliases]` in config.toml.
  128. /// Mutually exclusive with a trailing command.
  129. #[arg(long)]
  130. alias: Option<String>,
  131. /// Command to run (everything after `--`).
  132. #[arg(trailing_var_arg = true)]
  133. command: Vec<String>,
  134. },
  135. /// Build Docker image for a recipe on a remote node
  136. Build {
  137. #[arg(long, required = true, help = "Recipe name")]
  138. recipe: String,
  139. #[arg(long, required = true, help = "Node to build on (e.g. dgx1.lan)")]
  140. build_node: String,
  141. #[arg(long, default_value = "auto", help = "Cluster name")]
  142. cluster: String,
  143. /// Force rebuild even if checksum matches.
  144. #[arg(long)]
  145. force_build: bool,
  146. },
  147. /// Power on/off nodes via Home Assistant (smart plugs / Wake-on-LAN).
  148. Power {
  149. #[command(subcommand)]
  150. action: PowerAction,
  151. },
  152. }
  153. #[derive(Subcommand)]
  154. pub enum PowerAction {
  155. /// Power off: SSH `sudo shutdown now` + wait + HA `turn_off`.
  156. Off {
  157. #[arg(long)]
  158. cluster: String,
  159. /// Target a specific node within the cluster (default: all).
  160. #[arg(long)]
  161. node: Option<String>,
  162. },
  163. /// Power on: HA `turn_on` only (no SSH; the smart plug wakes the machine).
  164. On {
  165. #[arg(long)]
  166. cluster: String,
  167. /// Target a specific node within the cluster (default: all).
  168. #[arg(long)]
  169. node: Option<String>,
  170. },
  171. /// Switch clusters: disable nodes from source, enable nodes in target.
  172. Switch {
  173. /// Current cluster (nodes + entities to shut down).
  174. #[arg(long)]
  175. from: String,
  176. /// Target cluster (nodes + entities to wake up).
  177. #[arg(long)]
  178. to: String,
  179. },
  180. }
  181. #[derive(Subcommand)]
  182. pub enum TmuxCommands {
  183. /// Launch cluster via tmux
  184. Serve {
  185. #[arg(long, required = true)]
  186. recipe: String,
  187. #[arg(long, default_value = "auto")]
  188. cluster: String,
  189. /// Skip pre-serve hooks (cluster-level + recipe-level).
  190. #[arg(long)]
  191. skip_before_serve: bool,
  192. },
  193. /// Stop tmux sessions
  194. Stop {
  195. #[arg(long, required = true)]
  196. recipe: String,
  197. #[arg(long, default_value = "auto")]
  198. cluster: String,
  199. },
  200. /// Detailed tmux session status
  201. Status {
  202. #[arg(long, required = true)]
  203. recipe: String,
  204. #[arg(long, default_value = "auto")]
  205. cluster: String,
  206. },
  207. /// Show tmux session logs
  208. Logs {
  209. #[arg(long, required = true)]
  210. recipe: String,
  211. #[arg(long, action = clap::ArgAction::SetTrue)]
  212. follow: bool,
  213. #[arg(long, default_value = "auto")]
  214. cluster: String,
  215. },
  216. /// Attach to head node tmux session
  217. Attach {
  218. #[arg(long, required = true)]
  219. recipe: String,
  220. #[arg(long, default_value = "auto")]
  221. cluster: String,
  222. },
  223. }