clonebox 1.1.12__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
@@ -263,13 +263,21 @@ class SelectiveVMCloner:
263
263
 
264
264
  return cached_path
265
265
 
266
- def _default_network_active(self) -> bool:
267
- """Check if libvirt default network is active."""
266
+ def _default_network_state(self) -> str:
268
267
  try:
269
- net = self.conn.networkLookupByName("default")
270
- return net.isActive() == 1
268
+ active = self.conn.listNetworks() or []
269
+ if "default" in active:
270
+ return "active"
271
+ defined = self.conn.listDefinedNetworks() or []
272
+ if "default" in defined:
273
+ return "inactive"
274
+ return "missing"
271
275
  except Exception:
272
- return False
276
+ return "unknown"
277
+
278
+ def _default_network_active(self) -> bool:
279
+ """Check if libvirt default network is active."""
280
+ return self._default_network_state() == "active"
273
281
 
274
282
  def resolve_network_mode(self, config: VMConfig) -> str:
275
283
  """Resolve network mode based on config and session type."""
@@ -310,10 +318,9 @@ class SelectiveVMCloner:
310
318
  )
311
319
 
312
320
  # Check default network
313
- try:
314
- net = self.conn.networkLookupByName("default")
315
- checks["default_network"] = net.isActive() == 1
316
- except libvirt.libvirtError:
321
+ default_net_state = self._default_network_state()
322
+ checks["default_network"] = default_net_state == "active"
323
+ if default_net_state in {"inactive", "missing", "unknown"}:
317
324
  checks["network_error"] = (
318
325
  "Default network not found or inactive.\n"
319
326
  " For user session, CloneBox can use user-mode networking (slirp) automatically.\n"
@@ -1088,6 +1095,10 @@ REPORT_FILE="/var/log/clonebox-health.log"
1088
1095
  PASSED=0
1089
1096
  FAILED=0
1090
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
1091
1102
 
1092
1103
  # Colors for output
1093
1104
  RED='\\033[0;31m'
@@ -1106,22 +1117,36 @@ check_apt_package() {{
1106
1117
  ((PASSED++))
1107
1118
  return 0
1108
1119
  else
1109
- log "[FAIL] APT package '$pkg' is NOT installed"
1110
- ((FAILED++))
1111
- 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
1112
1129
  fi
1113
1130
  }}
1114
1131
 
1115
1132
  check_snap_package() {{
1116
1133
  local pkg="$1"
1117
- if snap list "$pkg" &>/dev/null; then
1134
+ local out
1135
+ out=$(snap list "$pkg" 2>&1)
1136
+ if [ $? -eq 0 ]; then
1118
1137
  log "[PASS] Snap package '$pkg' is installed"
1119
1138
  ((PASSED++))
1120
1139
  return 0
1121
1140
  else
1122
- log "[FAIL] Snap package '$pkg' is NOT installed"
1123
- ((FAILED++))
1124
- 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
1125
1150
  fi
1126
1151
  }}
1127
1152
 
@@ -1214,13 +1239,23 @@ log "Warnings: $WARNINGS"
1214
1239
  if [ $FAILED -eq 0 ]; then
1215
1240
  log ""
1216
1241
  log "[SUCCESS] All critical checks passed!"
1217
- echo "HEALTH_STATUS=OK" > /var/log/clonebox-health-status
1218
- 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
1219
1249
  else
1220
1250
  log ""
1221
1251
  log "[ERROR] Some checks failed. Review log for details."
1222
- echo "HEALTH_STATUS=FAILED" > /var/log/clonebox-health-status
1223
- 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
1224
1259
  fi
1225
1260
  """
1226
1261
  # Encode script to base64 for safe embedding in cloud-init
@@ -1404,7 +1439,8 @@ fi
1404
1439
  " - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
1405
1440
  " - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
1406
1441
  " - systemctl set-default graphical.target",
1407
- " - systemctl enable gdm3 || systemctl enable gdm || true",
1442
+ " - systemctl enable --now gdm3 || systemctl enable --now gdm || true",
1443
+ " - systemctl start display-manager || true",
1408
1444
  ]
1409
1445
  )
1410
1446
 
@@ -1503,7 +1539,7 @@ Comment=CloneBox autostart
1503
1539
  )
1504
1540
  runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-health")
1505
1541
  runcmd_lines.append(
1506
- " - /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"
1507
1543
  )
1508
1544
  runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
1509
1545