@ornncompute/cli 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +13 -7
  2. package/package.json +1 -1
  3. package/src/cli.mjs +40 -11
package/README.md CHANGED
@@ -56,9 +56,9 @@ ornn nodes keys list <node-id> [--json]
56
56
  ornn node health <node-id> [--json]
57
57
  ornn node diagnose <node-id> [--json]
58
58
  ornn node deenroll <node-id> [--reason <text>] [--keep-record] [--json]
59
- ornn fleet clean <ip>... --operator <id-or-slug> --ib-island <name> --identity-file <path> --dry-run [--ssh-user ubuntu|admin|ornn] [--json]
60
- ornn fleet clean <failed-fleet-id> --identity-file <path> --dry-run [--json]
61
- ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash> [--json]
59
+ ornn fleet clean <ip>... --operator <id-or-slug> --ib-island <name> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... [--ssh-user ubuntu|admin|ornn] [--json]
60
+ ornn fleet clean <failed-fleet-id> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... [--json]
61
+ ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash> [--source-user <user>]... [--preserve-user <user>]... [--json]
62
62
  ornn fleet enroll <fleet-id> --identity-file <path> [--confirm-takeover <ip[,ip...]>] [--json]
63
63
  ornn fleet deploy <fleet-id> --tenant <contact-email> --user <email-or-id> [--commerce-reservation <id>] [--network public|private] [--json]
64
64
  ornn ssh <node-or-reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]
@@ -212,11 +212,17 @@ Fleet handoff is an ordered, Compute-backed workflow:
212
212
  not trust a key first presented during cleanup.
213
213
  - `fleet clean ... --dry-run` tries `ubuntu`, `admin`, then `ornn`, records each
214
214
  host's hardware identity and exact deletion plan, and prints one fleet plan
215
- hash. Re-running with `--confirm-clean <plan-hash>` removes the approved tenant
215
+ hash. Repeat `--source-user` for Linux accounts owned by the departing tenant
216
+ (including an explicitly reviewed orphaned `/home/<user>` directory) and
217
+ `--preserve-user` for reviewed host accounts. Package/service-owned
218
+ accounts are protected automatically; any other account stops planning for
219
+ explicit review. Re-running with `--confirm-clean <plan-hash>` and the same
220
+ account flags removes the approved tenant
216
221
  users, keys, containers/images/volumes/networks/build cache, workload paths,
217
- mounts, services, and logs; locks preserved-account passwords, enforces
218
- key-only SSH, trims supported filesystems, pins the preserved management
219
- account to the supplied key, and stores per-node proof in Compute. A failed
222
+ mounts, services, and tenant-scoped logs. Apply refuses plans with running
223
+ containers, locks the management/root passwords, enforces key-only SSH, trims
224
+ supported filesystems, preserves existing root and management keys and host
225
+ logs, and stores per-node proof in Compute. A failed
220
226
  cleanup can be dry-run again with the same fleet ID; only failed members are
221
227
  replanned. Residual GPU
222
228
  consumers or tenant/platform listeners fail verification instead of being
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -130,9 +130,9 @@ Usage:
130
130
  ornn tokens list [--json]
131
131
  ornn tokens create --operator <id> [--facility <id>] [--expires-in <seconds>] [--mode bare-metal|vm] [--ip <addr>] [--force] [--json]
132
132
  ornn tokens revoke <token-id> [--json]
133
- ornn fleet clean <ip>... --operator <id-or-slug> --ib-island <name> --identity-file <path> --dry-run [--ssh-user ubuntu|admin|ornn] [--json]
134
- ornn fleet clean <failed-fleet-id> --identity-file <path> --dry-run [--json]
135
- ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash> [--json]
133
+ ornn fleet clean <ip>... --operator <id-or-slug> --ib-island <name> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... [--ssh-user ubuntu|admin|ornn] [--json]
134
+ ornn fleet clean <failed-fleet-id> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... [--json]
135
+ ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash> [--source-user <user>]... [--preserve-user <user>]... [--json]
136
136
  ornn fleet enroll <fleet-id> --identity-file <path> [--confirm-takeover <ip[,ip...]>] [--json]
137
137
  ornn fleet deploy <fleet-id> --tenant <email> --user <email-or-id> [--commerce-reservation <id>] [--network public|private] [--json]
138
138
  ornn reservations withdraw <reservation-id> [--json]
@@ -2193,6 +2193,18 @@ const FLEET_DEFAULT_TIMEOUT_SECONDS = 600;
2193
2193
  const FLEET_RESULT_ERROR_MAX_LENGTH = 4000;
2194
2194
  const FLEET_MANAGEMENT_USERS = ["ubuntu", "admin", "ornn"];
2195
2195
 
2196
+ function fleetAccountOptions(value, optionName) {
2197
+ if (!optionProvided(value)) return [];
2198
+ const values = Array.isArray(value) ? value : [value];
2199
+ const normalized = values.map((entry) => String(entry || "").trim());
2200
+ for (const user of normalized) {
2201
+ if (!/^[a-z_][a-z0-9_-]{0,30}\$?$/i.test(user)) {
2202
+ throw new Error(`${optionName} must be a valid Linux account name.`);
2203
+ }
2204
+ }
2205
+ return [...new Set(normalized)].sort();
2206
+ }
2207
+
2196
2208
  async function fleet(args, context) {
2197
2209
  const [subcommand, ...rest] = args;
2198
2210
  if (subcommand === "clean") {
@@ -2209,7 +2221,7 @@ async function fleet(args, context) {
2209
2221
 
2210
2222
  async function fleetClean(args, context) {
2211
2223
  const usage =
2212
- "Usage: ornn fleet clean <ip>... --operator <id-or-slug> --ib-island <name> --identity-file <path> --dry-run [--ssh-user ubuntu|admin|ornn] OR ornn fleet clean <failed-fleet-id> --identity-file <path> --dry-run OR ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash>";
2224
+ "Usage: ornn fleet clean <ip>... --operator <id-or-slug> --ib-island <name> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... [--ssh-user ubuntu|admin|ornn] OR ornn fleet clean <failed-fleet-id> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... OR ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash> [--source-user <user>]... [--preserve-user <user>]...";
2213
2225
  const { options, positionals } = parseOptions(args, {
2214
2226
  boolean: ["dry-run", "json"],
2215
2227
  value: [
@@ -2217,6 +2229,8 @@ async function fleetClean(args, context) {
2217
2229
  "ib-island",
2218
2230
  "identity-file",
2219
2231
  "confirm-clean",
2232
+ "source-user",
2233
+ "preserve-user",
2220
2234
  "ssh-user",
2221
2235
  "ssh-port",
2222
2236
  "parallel",
@@ -2240,6 +2254,12 @@ async function fleetClean(args, context) {
2240
2254
  const timeoutSeconds = optionProvided(options.timeout)
2241
2255
  ? positiveIntegerOption(options.timeout, "--timeout")
2242
2256
  : FLEET_DEFAULT_TIMEOUT_SECONDS;
2257
+ const sourceUsers = fleetAccountOptions(options.sourceUser, "--source-user");
2258
+ const preserveUsers = fleetAccountOptions(options.preserveUser, "--preserve-user");
2259
+ const accountOverlap = sourceUsers.filter((user) => preserveUsers.includes(user));
2260
+ if (accountOverlap.length) {
2261
+ throw new Error(`Users cannot be both source and preserved: ${accountOverlap.join(", ")}.`);
2262
+ }
2243
2263
  const confirmHash = optionalStringOption(options.confirmClean);
2244
2264
  if (confirmHash) {
2245
2265
  if (positionals.length !== 1 || options.dryRun || options.operator || options.ibIsland) {
@@ -2250,7 +2270,11 @@ async function fleetClean(args, context) {
2250
2270
  }
2251
2271
  const fleetId = positionals[0];
2252
2272
  const approved = await operatorRequest({
2253
- body: { plan_hash: confirmHash },
2273
+ body: {
2274
+ plan_hash: confirmHash,
2275
+ source_users: sourceUsers,
2276
+ preserve_users: preserveUsers,
2277
+ },
2254
2278
  endpoint: `/internal/fleets/${encodeURIComponent(fleetId)}/cleanup/approve`,
2255
2279
  env: context.env,
2256
2280
  fetchImpl: context.fetchImpl,
@@ -2415,21 +2439,22 @@ async function fleetClean(args, context) {
2415
2439
  requestedUser: requestedUser || node.ssh_user || null,
2416
2440
  timeoutSeconds: Math.min(timeoutSeconds, 30),
2417
2441
  });
2442
+ const accountArguments = [
2443
+ ...sourceUsers.map((user) => `--source-user ${shellQuote(user)}`),
2444
+ ...preserveUsers.map((user) => `--preserve-user ${shellQuote(user)}`),
2445
+ ].join(" ");
2418
2446
  const result = await runFleetSshCapture({
2419
2447
  context,
2420
2448
  identityFile,
2421
2449
  ip: node.ip_address,
2422
2450
  port: sshPort,
2423
- remoteCommand: `sudo -n python3 - plan --management-ip ${node.ip_address} --management-user ${sshUser} --management-public-key-b64 ${managementPublicKeyB64}`,
2451
+ remoteCommand: `sudo -n python3 - plan --management-ip ${node.ip_address} --management-user ${sshUser} --management-public-key-b64 ${managementPublicKeyB64}${accountArguments ? ` ${accountArguments}` : ""}`,
2424
2452
  stdin: runner.script,
2425
2453
  timeoutSeconds,
2426
2454
  user: sshUser,
2427
2455
  });
2428
- if (result.exitCode !== 0) {
2429
- throw new Error(result.stderr || "Cleanup plan failed.");
2430
- }
2431
2456
  const payload = fleetCleanupJson(result.stdout);
2432
- if (!payload.plan || !payload.plan_hash) {
2457
+ if (result.exitCode !== 0 || !payload.plan || !payload.plan_hash) {
2433
2458
  throw new Error(payload.error || result.stderr || "Cleanup plan failed.");
2434
2459
  }
2435
2460
  return {
@@ -2484,6 +2509,10 @@ async function fleetClean(args, context) {
2484
2509
  `${planningError} Fleet ${fleetRecord.id} was marked failed, but its local manifest could not be saved: ${fleetResultError(manifestError)}`,
2485
2510
  );
2486
2511
  }
2512
+ if (options.json) {
2513
+ writeJson(context.stdout, { error: planningError, fleet: failedFleet });
2514
+ return 1;
2515
+ }
2487
2516
  throw new Error(`${planningError} Fleet ${fleetRecord.id} was marked failed.`);
2488
2517
  }
2489
2518
  await saveFleetManifest(recorded.fleet, context.env);
@@ -2499,7 +2528,7 @@ async function fleetClean(args, context) {
2499
2528
  }
2500
2529
  context.stdout.write(`Cleanup plan: ${recorded.plan_hash}\n`);
2501
2530
  context.stdout.write(
2502
- `Approve exactly this plan with: ornn fleet clean ${fleetRecord.id} --identity-file ${shellQuote(identityFile)} --confirm-clean ${recorded.plan_hash}\n`
2531
+ `Approve exactly this plan with: ornn fleet clean ${fleetRecord.id} --identity-file ${shellQuote(identityFile)} --confirm-clean ${recorded.plan_hash}${sourceUsers.map((user) => ` --source-user ${shellQuote(user)}`).join("")}${preserveUsers.map((user) => ` --preserve-user ${shellQuote(user)}`).join("")}\n`
2503
2532
  );
2504
2533
  }
2505
2534
  return 0;