clonebox 1.1.13__py3-none-any.whl → 1.1.14__py3-none-any.whl

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.
clonebox/cli.py CHANGED
@@ -34,6 +34,9 @@ from clonebox.monitor import ResourceMonitor, format_bytes
34
34
  from clonebox.p2p import P2PManager
35
35
  from clonebox.snapshots import SnapshotManager, SnapshotType
36
36
  from clonebox.health import HealthCheckManager, ProbeConfig, ProbeType
37
+ from clonebox.audit import get_audit_logger, AuditQuery, AuditEventType, AuditOutcome
38
+ from clonebox.orchestrator import Orchestrator, OrchestrationResult
39
+ from clonebox.plugins import get_plugin_manager, PluginHook, PluginContext
37
40
 
38
41
  # Custom questionary style
39
42
  custom_style = Style(
@@ -458,6 +461,9 @@ def run_vm_diagnostics(
458
461
  if health_status and "HEALTH_STATUS=OK" in health_status:
459
462
  result["health"]["status"] = "ok"
460
463
  console.print("[green]✅ Health: All checks passed[/]")
464
+ elif health_status and "HEALTH_STATUS=PENDING" in health_status:
465
+ result["health"]["status"] = "pending"
466
+ console.print("[yellow]⏳ Health: Setup in progress[/]")
461
467
  elif health_status and "HEALTH_STATUS=FAILED" in health_status:
462
468
  result["health"]["status"] = "failed"
463
469
  console.print("[red]❌ Health: Some checks failed[/]")
@@ -1800,6 +1806,8 @@ def cmd_test(args):
1800
1806
  console.print()
1801
1807
 
1802
1808
  # Test 3: Check cloud-init status (if running)
1809
+ cloud_init_complete: Optional[bool] = None
1810
+ cloud_init_running: bool = False
1803
1811
  if not quick and state == "running":
1804
1812
  console.print("[bold]3. Cloud-init Status[/]")
1805
1813
  try:
@@ -1809,16 +1817,23 @@ def cmd_test(args):
1809
1817
  status = _qga_exec(vm_name, conn_uri, "cloud-init status 2>/dev/null || true", timeout=15)
1810
1818
  if status is None:
1811
1819
  console.print("[yellow]⚠️ Could not check cloud-init (QGA command failed)[/]")
1820
+ cloud_init_complete = None
1812
1821
  elif "done" in status.lower():
1813
1822
  console.print("[green]✅ Cloud-init completed[/]")
1823
+ cloud_init_complete = True
1814
1824
  elif "running" in status.lower():
1815
1825
  console.print("[yellow]⚠️ Cloud-init still running[/]")
1826
+ cloud_init_complete = False
1827
+ cloud_init_running = True
1816
1828
  elif status.strip():
1817
1829
  console.print(f"[yellow]⚠️ Cloud-init status: {status.strip()}[/]")
1830
+ cloud_init_complete = None
1818
1831
  else:
1819
1832
  console.print("[yellow]⚠️ Cloud-init status: unknown[/]")
1833
+ cloud_init_complete = None
1820
1834
  except Exception:
1821
1835
  console.print("[yellow]⚠️ Could not check cloud-init (QEMU agent may not be running)[/]")
1836
+ cloud_init_complete = None
1822
1837
 
1823
1838
  console.print()
1824
1839
 
@@ -1877,17 +1892,33 @@ def cmd_test(args):
1877
1892
  timeout=10,
1878
1893
  )
1879
1894
  if exists and exists.strip() == "yes":
1880
- out = _qga_exec(
1895
+ _qga_exec(
1881
1896
  vm_name,
1882
1897
  conn_uri,
1883
- "/usr/local/bin/clonebox-health >/dev/null 2>&1 && echo yes || echo no",
1898
+ "/usr/local/bin/clonebox-health >/dev/null 2>&1 || true",
1884
1899
  timeout=60,
1885
1900
  )
1886
- if out and out.strip() == "yes":
1887
- console.print("[green]✅ Health check ran successfully[/]")
1901
+ health_status = _qga_exec(
1902
+ vm_name,
1903
+ conn_uri,
1904
+ "cat /var/log/clonebox-health-status 2>/dev/null || true",
1905
+ timeout=10,
1906
+ )
1907
+ if health_status and "HEALTH_STATUS=OK" in health_status:
1908
+ console.print("[green]✅ Health check passed[/]")
1888
1909
  console.print(" View results in VM: cat /var/log/clonebox-health.log")
1910
+ elif health_status and "HEALTH_STATUS=PENDING" in health_status:
1911
+ console.print("[yellow]⚠️ Health check pending (setup in progress)[/]")
1912
+ if cloud_init_running:
1913
+ console.print(" Cloud-init is still running; re-check after it completes")
1914
+ console.print(" View logs in VM: cat /var/log/clonebox-health.log")
1915
+ elif health_status and "HEALTH_STATUS=FAILED" in health_status:
1916
+ console.print("[yellow]⚠️ Health check reports failures[/]")
1917
+ if cloud_init_running:
1918
+ console.print(" Cloud-init is still running; some failures may be transient")
1919
+ console.print(" View logs in VM: cat /var/log/clonebox-health.log")
1889
1920
  else:
1890
- console.print("[yellow]⚠️ Health check did not report success[/]")
1921
+ console.print("[yellow]⚠️ Health check status not available yet[/]")
1891
1922
  console.print(" View logs in VM: cat /var/log/clonebox-health.log")
1892
1923
  else:
1893
1924
  console.print("[yellow]⚠️ Health check script not found[/]")
@@ -3098,6 +3129,307 @@ def cmd_list_remote(args) -> None:
3098
3129
  console.print("[yellow]No VMs found on remote host.[/]")
3099
3130
 
3100
3131
 
3132
+ # === Audit Commands ===
3133
+
3134
+
3135
+ def cmd_audit_list(args) -> None:
3136
+ """List audit events."""
3137
+ query = AuditQuery()
3138
+
3139
+ # Build filters
3140
+ event_type = None
3141
+ if hasattr(args, "type") and args.type:
3142
+ try:
3143
+ event_type = AuditEventType(args.type)
3144
+ except ValueError:
3145
+ console.print(f"[red]Unknown event type: {args.type}[/]")
3146
+ return
3147
+
3148
+ outcome = None
3149
+ if hasattr(args, "outcome") and args.outcome:
3150
+ try:
3151
+ outcome = AuditOutcome(args.outcome)
3152
+ except ValueError:
3153
+ console.print(f"[red]Unknown outcome: {args.outcome}[/]")
3154
+ return
3155
+
3156
+ limit = getattr(args, "limit", 50)
3157
+ target = getattr(args, "target", None)
3158
+
3159
+ events = query.query(
3160
+ event_type=event_type,
3161
+ target_name=target,
3162
+ outcome=outcome,
3163
+ limit=limit,
3164
+ )
3165
+
3166
+ if not events:
3167
+ console.print("[yellow]No audit events found.[/]")
3168
+ return
3169
+
3170
+ if getattr(args, "json", False):
3171
+ console.print_json(json.dumps([e.to_dict() for e in events], default=str))
3172
+ return
3173
+
3174
+ table = Table(title="Audit Events", border_style="cyan")
3175
+ table.add_column("Time", style="dim")
3176
+ table.add_column("Event")
3177
+ table.add_column("Target")
3178
+ table.add_column("Outcome")
3179
+ table.add_column("User")
3180
+
3181
+ for event in reversed(events[-limit:]):
3182
+ outcome_style = {
3183
+ "success": "green",
3184
+ "failure": "red",
3185
+ "partial": "yellow",
3186
+ "denied": "red bold",
3187
+ "skipped": "dim",
3188
+ }.get(event.outcome.value, "white")
3189
+
3190
+ target_str = event.target_name or "-"
3191
+ table.add_row(
3192
+ event.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
3193
+ event.event_type.value,
3194
+ target_str,
3195
+ f"[{outcome_style}]{event.outcome.value}[/]",
3196
+ event.user,
3197
+ )
3198
+
3199
+ console.print(table)
3200
+
3201
+
3202
+ def cmd_audit_show(args) -> None:
3203
+ """Show audit event details."""
3204
+ query = AuditQuery()
3205
+ events = query.query(limit=1000)
3206
+
3207
+ for event in events:
3208
+ if event.event_id == args.event_id:
3209
+ console.print_json(json.dumps(event.to_dict(), indent=2, default=str))
3210
+ return
3211
+
3212
+ console.print(f"[red]Event not found: {args.event_id}[/]")
3213
+
3214
+
3215
+ def cmd_audit_failures(args) -> None:
3216
+ """Show recent failures."""
3217
+ query = AuditQuery()
3218
+ events = query.get_failures(limit=getattr(args, "limit", 20))
3219
+
3220
+ if not events:
3221
+ console.print("[green]No failures recorded.[/]")
3222
+ return
3223
+
3224
+ table = Table(title="Recent Failures", border_style="red")
3225
+ table.add_column("Time", style="dim")
3226
+ table.add_column("Event")
3227
+ table.add_column("Target")
3228
+ table.add_column("Error")
3229
+
3230
+ for event in reversed(events):
3231
+ table.add_row(
3232
+ event.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
3233
+ event.event_type.value,
3234
+ event.target_name or "-",
3235
+ (event.error_message or "-")[:50],
3236
+ )
3237
+
3238
+ console.print(table)
3239
+
3240
+
3241
+ # === Orchestration Commands ===
3242
+
3243
+
3244
+ def cmd_compose_up(args) -> None:
3245
+ """Start VMs from compose file."""
3246
+ compose_file = Path(args.file) if hasattr(args, "file") and args.file else Path("clonebox-compose.yaml")
3247
+
3248
+ if not compose_file.exists():
3249
+ console.print(f"[red]Compose file not found: {compose_file}[/]")
3250
+ return
3251
+
3252
+ user_session = getattr(args, "user", False)
3253
+ services = args.services if hasattr(args, "services") and args.services else None
3254
+
3255
+ console.print(f"[cyan]🚀 Starting VMs from: {compose_file}[/]")
3256
+
3257
+ try:
3258
+ orch = Orchestrator.from_file(compose_file, user_session=user_session)
3259
+ result = orch.up(services=services, console=console)
3260
+
3261
+ if result.success:
3262
+ console.print("[green]✅ All VMs started successfully[/]")
3263
+ else:
3264
+ console.print("[yellow]⚠️ Some VMs failed to start:[/]")
3265
+ for name, error in result.errors.items():
3266
+ console.print(f" [red]{name}:[/] {error}")
3267
+
3268
+ console.print(f"[dim]Duration: {result.duration_seconds:.1f}s[/]")
3269
+
3270
+ except Exception as e:
3271
+ console.print(f"[red]❌ Orchestration failed: {e}[/]")
3272
+
3273
+
3274
+ def cmd_compose_down(args) -> None:
3275
+ """Stop VMs from compose file."""
3276
+ compose_file = Path(args.file) if hasattr(args, "file") and args.file else Path("clonebox-compose.yaml")
3277
+
3278
+ if not compose_file.exists():
3279
+ console.print(f"[red]Compose file not found: {compose_file}[/]")
3280
+ return
3281
+
3282
+ user_session = getattr(args, "user", False)
3283
+ services = args.services if hasattr(args, "services") and args.services else None
3284
+ force = getattr(args, "force", False)
3285
+
3286
+ console.print(f"[cyan]🛑 Stopping VMs from: {compose_file}[/]")
3287
+
3288
+ try:
3289
+ orch = Orchestrator.from_file(compose_file, user_session=user_session)
3290
+ result = orch.down(services=services, force=force, console=console)
3291
+
3292
+ if result.success:
3293
+ console.print("[green]✅ All VMs stopped successfully[/]")
3294
+ else:
3295
+ console.print("[yellow]⚠️ Some VMs failed to stop:[/]")
3296
+ for name, error in result.errors.items():
3297
+ console.print(f" [red]{name}:[/] {error}")
3298
+
3299
+ except Exception as e:
3300
+ console.print(f"[red]❌ Stop failed: {e}[/]")
3301
+
3302
+
3303
+ def cmd_compose_status(args) -> None:
3304
+ """Show status of VMs from compose file."""
3305
+ compose_file = Path(args.file) if hasattr(args, "file") and args.file else Path("clonebox-compose.yaml")
3306
+
3307
+ if not compose_file.exists():
3308
+ console.print(f"[red]Compose file not found: {compose_file}[/]")
3309
+ return
3310
+
3311
+ user_session = getattr(args, "user", False)
3312
+
3313
+ try:
3314
+ orch = Orchestrator.from_file(compose_file, user_session=user_session)
3315
+ status = orch.status()
3316
+
3317
+ if getattr(args, "json", False):
3318
+ console.print_json(json.dumps(status, default=str))
3319
+ return
3320
+
3321
+ table = Table(title=f"Compose Status: {compose_file.name}", border_style="cyan")
3322
+ table.add_column("VM")
3323
+ table.add_column("State")
3324
+ table.add_column("Actual")
3325
+ table.add_column("Health")
3326
+ table.add_column("Depends On")
3327
+
3328
+ for name, info in status.items():
3329
+ state = info["orchestration_state"]
3330
+ actual = info["actual_state"]
3331
+ health = "✅" if info["health_check_passed"] else "⏳"
3332
+ deps = ", ".join(info["depends_on"]) or "-"
3333
+
3334
+ state_style = {
3335
+ "running": "green",
3336
+ "healthy": "green bold",
3337
+ "stopped": "dim",
3338
+ "failed": "red",
3339
+ "pending": "yellow",
3340
+ }.get(state, "white")
3341
+
3342
+ table.add_row(
3343
+ name,
3344
+ f"[{state_style}]{state}[/]",
3345
+ actual,
3346
+ health,
3347
+ deps,
3348
+ )
3349
+
3350
+ console.print(table)
3351
+
3352
+ except Exception as e:
3353
+ console.print(f"[red]❌ Failed to get status: {e}[/]")
3354
+
3355
+
3356
+ # === Plugin Commands ===
3357
+
3358
+
3359
+ def cmd_plugin_list(args) -> None:
3360
+ """List installed plugins."""
3361
+ manager = get_plugin_manager()
3362
+
3363
+ # Load plugins if not already loaded
3364
+ if not manager.list_plugins():
3365
+ manager.load_all()
3366
+
3367
+ plugins = manager.list_plugins()
3368
+
3369
+ if not plugins:
3370
+ console.print("[yellow]No plugins installed.[/]")
3371
+ console.print("[dim]Plugin directories:[/]")
3372
+ for d in manager.plugin_dirs:
3373
+ console.print(f" {d}")
3374
+ return
3375
+
3376
+ table = Table(title="Installed Plugins", border_style="cyan")
3377
+ table.add_column("Name")
3378
+ table.add_column("Version")
3379
+ table.add_column("Enabled")
3380
+ table.add_column("Description")
3381
+
3382
+ for plugin in plugins:
3383
+ enabled = "[green]✅[/]" if plugin["enabled"] else "[red]❌[/]"
3384
+ table.add_row(
3385
+ plugin["name"],
3386
+ plugin["version"],
3387
+ enabled,
3388
+ (plugin.get("description", "") or "")[:40],
3389
+ )
3390
+
3391
+ console.print(table)
3392
+
3393
+
3394
+ def cmd_plugin_enable(args) -> None:
3395
+ """Enable a plugin."""
3396
+ manager = get_plugin_manager()
3397
+ manager.load_all()
3398
+
3399
+ if manager.enable(args.name):
3400
+ console.print(f"[green]✅ Plugin '{args.name}' enabled[/]")
3401
+ else:
3402
+ console.print(f"[red]Plugin '{args.name}' not found[/]")
3403
+
3404
+
3405
+ def cmd_plugin_disable(args) -> None:
3406
+ """Disable a plugin."""
3407
+ manager = get_plugin_manager()
3408
+ manager.load_all()
3409
+
3410
+ if manager.disable(args.name):
3411
+ console.print(f"[yellow]⚠️ Plugin '{args.name}' disabled[/]")
3412
+ else:
3413
+ console.print(f"[red]Plugin '{args.name}' not found[/]")
3414
+
3415
+
3416
+ def cmd_plugin_discover(args) -> None:
3417
+ """Discover available plugins."""
3418
+ manager = get_plugin_manager()
3419
+ discovered = manager.discover()
3420
+
3421
+ if not discovered:
3422
+ console.print("[yellow]No plugins discovered.[/]")
3423
+ console.print("[dim]Plugin directories:[/]")
3424
+ for d in manager.plugin_dirs:
3425
+ console.print(f" {d}")
3426
+ return
3427
+
3428
+ console.print("[bold]Discovered plugins:[/]")
3429
+ for name in discovered:
3430
+ console.print(f" • {name}")
3431
+
3432
+
3101
3433
  def main():
3102
3434
  """Main entry point."""
3103
3435
  parser = argparse.ArgumentParser(
@@ -3684,6 +4016,67 @@ def main():
3684
4016
  list_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3685
4017
  list_remote_parser.set_defaults(func=cmd_list_remote)
3686
4018
 
4019
+ # === Audit Commands ===
4020
+ audit_parser = subparsers.add_parser("audit", help="View audit logs")
4021
+ audit_sub = audit_parser.add_subparsers(dest="audit_command", help="Audit commands")
4022
+
4023
+ audit_list = audit_sub.add_parser("list", aliases=["ls"], help="List audit events")
4024
+ audit_list.add_argument("--type", "-t", help="Filter by event type (e.g., vm.create)")
4025
+ audit_list.add_argument("--target", help="Filter by target name")
4026
+ audit_list.add_argument("--outcome", "-o", choices=["success", "failure", "partial"], help="Filter by outcome")
4027
+ audit_list.add_argument("--limit", "-n", type=int, default=50, help="Max events to show")
4028
+ audit_list.add_argument("--json", action="store_true", help="Output as JSON")
4029
+ audit_list.set_defaults(func=cmd_audit_list)
4030
+
4031
+ audit_show = audit_sub.add_parser("show", help="Show audit event details")
4032
+ audit_show.add_argument("event_id", help="Event ID to show")
4033
+ audit_show.set_defaults(func=cmd_audit_show)
4034
+
4035
+ audit_failures = audit_sub.add_parser("failures", help="Show recent failures")
4036
+ audit_failures.add_argument("--limit", "-n", type=int, default=20, help="Max events to show")
4037
+ audit_failures.set_defaults(func=cmd_audit_failures)
4038
+
4039
+ # === Compose/Orchestration Commands ===
4040
+ compose_parser = subparsers.add_parser("compose", help="Multi-VM orchestration")
4041
+ compose_sub = compose_parser.add_subparsers(dest="compose_command", help="Compose commands")
4042
+
4043
+ compose_up = compose_sub.add_parser("up", help="Start VMs from compose file")
4044
+ compose_up.add_argument("-f", "--file", default="clonebox-compose.yaml", help="Compose file")
4045
+ compose_up.add_argument("-u", "--user", action="store_true", help="Use user session")
4046
+ compose_up.add_argument("services", nargs="*", help="Specific services to start")
4047
+ compose_up.set_defaults(func=cmd_compose_up)
4048
+
4049
+ compose_down = compose_sub.add_parser("down", help="Stop VMs from compose file")
4050
+ compose_down.add_argument("-f", "--file", default="clonebox-compose.yaml", help="Compose file")
4051
+ compose_down.add_argument("-u", "--user", action="store_true", help="Use user session")
4052
+ compose_down.add_argument("--force", action="store_true", help="Force stop")
4053
+ compose_down.add_argument("services", nargs="*", help="Specific services to stop")
4054
+ compose_down.set_defaults(func=cmd_compose_down)
4055
+
4056
+ compose_status = compose_sub.add_parser("status", aliases=["ps"], help="Show compose status")
4057
+ compose_status.add_argument("-f", "--file", default="clonebox-compose.yaml", help="Compose file")
4058
+ compose_status.add_argument("-u", "--user", action="store_true", help="Use user session")
4059
+ compose_status.add_argument("--json", action="store_true", help="Output as JSON")
4060
+ compose_status.set_defaults(func=cmd_compose_status)
4061
+
4062
+ # === Plugin Commands ===
4063
+ plugin_parser = subparsers.add_parser("plugin", help="Manage plugins")
4064
+ plugin_sub = plugin_parser.add_subparsers(dest="plugin_command", help="Plugin commands")
4065
+
4066
+ plugin_list = plugin_sub.add_parser("list", aliases=["ls"], help="List plugins")
4067
+ plugin_list.set_defaults(func=cmd_plugin_list)
4068
+
4069
+ plugin_enable = plugin_sub.add_parser("enable", help="Enable a plugin")
4070
+ plugin_enable.add_argument("name", help="Plugin name")
4071
+ plugin_enable.set_defaults(func=cmd_plugin_enable)
4072
+
4073
+ plugin_disable = plugin_sub.add_parser("disable", help="Disable a plugin")
4074
+ plugin_disable.add_argument("name", help="Plugin name")
4075
+ plugin_disable.set_defaults(func=cmd_plugin_disable)
4076
+
4077
+ plugin_discover = plugin_sub.add_parser("discover", help="Discover available plugins")
4078
+ plugin_discover.set_defaults(func=cmd_plugin_discover)
4079
+
3687
4080
  args = parser.parse_args()
3688
4081
 
3689
4082
  if hasattr(args, "func"):
clonebox/cloner.py CHANGED
@@ -1095,6 +1095,10 @@ REPORT_FILE="/var/log/clonebox-health.log"
1095
1095
  PASSED=0
1096
1096
  FAILED=0
1097
1097
  WARNINGS=0
1098
+ SETUP_IN_PROGRESS=0
1099
+ if [ ! -f /var/lib/cloud/instance/boot-finished ]; then
1100
+ SETUP_IN_PROGRESS=1
1101
+ fi
1098
1102
 
1099
1103
  # Colors for output
1100
1104
  RED='\\033[0;31m'
@@ -1113,22 +1117,36 @@ check_apt_package() {{
1113
1117
  ((PASSED++))
1114
1118
  return 0
1115
1119
  else
1116
- log "[FAIL] APT package '$pkg' is NOT installed"
1117
- ((FAILED++))
1118
- return 1
1120
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1121
+ log "[WARN] APT package '$pkg' is not installed yet"
1122
+ ((WARNINGS++))
1123
+ return 1
1124
+ else
1125
+ log "[FAIL] APT package '$pkg' is NOT installed"
1126
+ ((FAILED++))
1127
+ return 1
1128
+ fi
1119
1129
  fi
1120
1130
  }}
1121
1131
 
1122
1132
  check_snap_package() {{
1123
1133
  local pkg="$1"
1124
- if snap list "$pkg" &>/dev/null; then
1134
+ local out
1135
+ out=$(snap list "$pkg" 2>&1)
1136
+ if [ $? -eq 0 ]; then
1125
1137
  log "[PASS] Snap package '$pkg' is installed"
1126
1138
  ((PASSED++))
1127
1139
  return 0
1128
1140
  else
1129
- log "[FAIL] Snap package '$pkg' is NOT installed"
1130
- ((FAILED++))
1131
- return 1
1141
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1142
+ log "[WARN] Snap package '$pkg' is not installed yet"
1143
+ ((WARNINGS++))
1144
+ return 1
1145
+ else
1146
+ log "[FAIL] Snap package '$pkg' is NOT installed"
1147
+ ((FAILED++))
1148
+ return 1
1149
+ fi
1132
1150
  fi
1133
1151
  }}
1134
1152
 
@@ -1221,13 +1239,23 @@ log "Warnings: $WARNINGS"
1221
1239
  if [ $FAILED -eq 0 ]; then
1222
1240
  log ""
1223
1241
  log "[SUCCESS] All critical checks passed!"
1224
- echo "HEALTH_STATUS=OK" > /var/log/clonebox-health-status
1225
- exit 0
1242
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1243
+ echo "HEALTH_STATUS=PENDING" > /var/log/clonebox-health-status
1244
+ exit 0
1245
+ else
1246
+ echo "HEALTH_STATUS=OK" > /var/log/clonebox-health-status
1247
+ exit 0
1248
+ fi
1226
1249
  else
1227
1250
  log ""
1228
1251
  log "[ERROR] Some checks failed. Review log for details."
1229
- echo "HEALTH_STATUS=FAILED" > /var/log/clonebox-health-status
1230
- exit 1
1252
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1253
+ echo "HEALTH_STATUS=PENDING" > /var/log/clonebox-health-status
1254
+ exit 0
1255
+ else
1256
+ echo "HEALTH_STATUS=FAILED" > /var/log/clonebox-health-status
1257
+ exit 1
1258
+ fi
1231
1259
  fi
1232
1260
  """
1233
1261
  # Encode script to base64 for safe embedding in cloud-init
@@ -1511,7 +1539,7 @@ Comment=CloneBox autostart
1511
1539
  )
1512
1540
  runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-health")
1513
1541
  runcmd_lines.append(
1514
- " - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1"
1542
+ " - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1 || true"
1515
1543
  )
1516
1544
  runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
1517
1545