clonebox 1.1.15__tar.gz → 1.1.17__tar.gz

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.

Potentially problematic release.


This version of clonebox might be problematic. Click here for more details.

Files changed (70) hide show
  1. {clonebox-1.1.15/src/clonebox.egg-info → clonebox-1.1.17}/PKG-INFO +1 -1
  2. {clonebox-1.1.15 → clonebox-1.1.17}/pyproject.toml +1 -1
  3. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/cli.py +105 -6
  4. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/cloner.py +193 -64
  5. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/validator.py +27 -12
  6. {clonebox-1.1.15 → clonebox-1.1.17/src/clonebox.egg-info}/PKG-INFO +1 -1
  7. {clonebox-1.1.15 → clonebox-1.1.17}/LICENSE +0 -0
  8. {clonebox-1.1.15 → clonebox-1.1.17}/README.md +0 -0
  9. {clonebox-1.1.15 → clonebox-1.1.17}/setup.cfg +0 -0
  10. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/__init__.py +0 -0
  11. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/__main__.py +0 -0
  12. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/audit.py +0 -0
  13. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/backends/libvirt_backend.py +0 -0
  14. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/backends/qemu_disk.py +0 -0
  15. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/backends/subprocess_runner.py +0 -0
  16. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/container.py +0 -0
  17. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/dashboard.py +0 -0
  18. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/detector.py +0 -0
  19. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/di.py +0 -0
  20. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/exporter.py +0 -0
  21. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/health/__init__.py +0 -0
  22. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/health/manager.py +0 -0
  23. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/health/models.py +0 -0
  24. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/health/probes.py +0 -0
  25. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/importer.py +0 -0
  26. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/interfaces/disk.py +0 -0
  27. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/interfaces/hypervisor.py +0 -0
  28. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/interfaces/network.py +0 -0
  29. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/interfaces/process.py +0 -0
  30. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/logging.py +0 -0
  31. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/models.py +0 -0
  32. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/monitor.py +0 -0
  33. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/orchestrator.py +0 -0
  34. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/p2p.py +0 -0
  35. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/plugins/__init__.py +0 -0
  36. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/plugins/base.py +0 -0
  37. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/plugins/manager.py +0 -0
  38. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/profiles.py +0 -0
  39. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/remote.py +0 -0
  40. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/resource_monitor.py +0 -0
  41. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/resources.py +0 -0
  42. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/rollback.py +0 -0
  43. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/secrets.py +0 -0
  44. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/snapshots/__init__.py +0 -0
  45. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/snapshots/manager.py +0 -0
  46. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/snapshots/models.py +0 -0
  47. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
  48. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox/templates/profiles/web-stack.yaml +0 -0
  49. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox.egg-info/SOURCES.txt +0 -0
  50. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox.egg-info/dependency_links.txt +0 -0
  51. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox.egg-info/entry_points.txt +0 -0
  52. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox.egg-info/requires.txt +0 -0
  53. {clonebox-1.1.15 → clonebox-1.1.17}/src/clonebox.egg-info/top_level.txt +0 -0
  54. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_audit.py +0 -0
  55. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_cli.py +0 -0
  56. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_cli_new_commands.py +0 -0
  57. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_cloner.py +0 -0
  58. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_cloner_simple.py +0 -0
  59. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_container.py +0 -0
  60. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_coverage_additional.py +0 -0
  61. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_coverage_boost_final.py +0 -0
  62. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_dashboard_coverage.py +0 -0
  63. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_detector.py +0 -0
  64. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_models.py +0 -0
  65. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_network.py +0 -0
  66. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_orchestrator.py +0 -0
  67. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_plugins.py +0 -0
  68. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_profiles.py +0 -0
  69. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_remote.py +0 -0
  70. {clonebox-1.1.15 → clonebox-1.1.17}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.15
3
+ Version: 1.1.17
4
4
  Summary: Clone your workstation environment to an isolated VM with selective apps, paths and services
5
5
  Author: CloneBox Team
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clonebox"
7
- version = "1.1.15"
7
+ version = "1.1.17"
8
8
  description = "Clone your workstation environment to an isolated VM with selective apps, paths and services"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -1930,7 +1930,10 @@ def cmd_test(args):
1930
1930
  if is_accessible == "yes":
1931
1931
  console.print(f"[green]✅ {guest_path} (mount)[/]")
1932
1932
  else:
1933
- console.print(f"[red]❌ {guest_path} (mount inaccessible)[/]")
1933
+ if cloud_init_running:
1934
+ console.print(f"[yellow]⏳ {guest_path} (mount pending)[/]")
1935
+ else:
1936
+ console.print(f"[red]❌ {guest_path} (mount inaccessible)[/]")
1934
1937
  except Exception:
1935
1938
  console.print(f"[yellow]⚠️ {guest_path} (could not check)[/]")
1936
1939
 
@@ -1943,7 +1946,10 @@ def cmd_test(args):
1943
1946
  if is_accessible == "yes":
1944
1947
  console.print(f"[green]✅ {guest_path} (copied)[/]")
1945
1948
  else:
1946
- console.print(f"[red]❌ {guest_path} (copy missing)[/]")
1949
+ if cloud_init_running:
1950
+ console.print(f"[yellow]⏳ {guest_path} (copy pending)[/]")
1951
+ else:
1952
+ console.print(f"[red]❌ {guest_path} (copy missing)[/]")
1947
1953
  except Exception:
1948
1954
  console.print(f"[yellow]⚠️ {guest_path} (could not check)[/]")
1949
1955
  else:
@@ -2349,6 +2355,68 @@ def load_clonebox_config(path: Path) -> dict:
2349
2355
  return config
2350
2356
 
2351
2357
 
2358
+ def _exec_in_vm_qga(vm_name: str, conn_uri: str, command: str) -> Optional[str]:
2359
+ """Internal helper to execute command via QGA and get output."""
2360
+ import subprocess
2361
+ import json
2362
+ import base64
2363
+ import time
2364
+
2365
+ try:
2366
+ # 1. Start execution
2367
+ cmd_json = {
2368
+ "execute": "guest-exec",
2369
+ "arguments": {
2370
+ "path": "/bin/sh",
2371
+ "arg": ["-c", command],
2372
+ "capture-output": True
2373
+ }
2374
+ }
2375
+
2376
+ result = subprocess.run(
2377
+ ["virsh", "--connect", conn_uri, "qemu-agent-command", vm_name, json.dumps(cmd_json)],
2378
+ capture_output=True,
2379
+ text=True,
2380
+ timeout=5
2381
+ )
2382
+
2383
+ if result.returncode != 0:
2384
+ return None
2385
+
2386
+ resp = json.loads(result.stdout)
2387
+ pid = resp.get("return", {}).get("pid")
2388
+ if not pid:
2389
+ return None
2390
+
2391
+ # 2. Wait and get status (quick check)
2392
+ status_json = {"execute": "guest-exec-status", "arguments": {"pid": pid}}
2393
+
2394
+ for _ in range(3): # Try a few times
2395
+ status_result = subprocess.run(
2396
+ ["virsh", "--connect", conn_uri, "qemu-agent-command", vm_name, json.dumps(status_json)],
2397
+ capture_output=True,
2398
+ text=True,
2399
+ timeout=5
2400
+ )
2401
+
2402
+ if status_result.returncode != 0:
2403
+ continue
2404
+
2405
+ status_resp = json.loads(status_result.stdout)
2406
+ ret = status_resp.get("return", {})
2407
+
2408
+ if ret.get("exited"):
2409
+ if "out-data" in ret:
2410
+ return base64.b64decode(ret["out-data"]).decode().strip()
2411
+ return ""
2412
+
2413
+ time.sleep(0.5)
2414
+
2415
+ return None
2416
+ except Exception:
2417
+ return None
2418
+
2419
+
2352
2420
  def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout: int = 900):
2353
2421
  """Monitor cloud-init status in VM and show progress."""
2354
2422
  import subprocess
@@ -2358,6 +2426,8 @@ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout:
2358
2426
  start_time = time.time()
2359
2427
  shutdown_count = 0 # Count consecutive shutdown detections
2360
2428
  restart_detected = False
2429
+ last_phases = []
2430
+ seen_lines = set()
2361
2431
 
2362
2432
  with Progress(
2363
2433
  SpinnerColumn(),
@@ -2390,7 +2460,7 @@ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout:
2390
2460
  restart_detected = True
2391
2461
  progress.update(
2392
2462
  task,
2393
- description="[yellow]⟳ VM restarting after package installation...",
2463
+ description="[yellow]⟳ VM restarting after installation...",
2394
2464
  )
2395
2465
  time.sleep(3)
2396
2466
  continue
@@ -2403,7 +2473,7 @@ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout:
2403
2473
  if restart_detected and "running" in vm_state and shutdown_count >= 3:
2404
2474
  # VM restarted successfully - GUI should be ready
2405
2475
  progress.update(
2406
- task, description=f"[green]✓ GUI ready! Total time: {minutes}m {seconds}s"
2476
+ task, description=f"[green]✓ VM ready! Total time: {minutes}m {seconds}s"
2407
2477
  )
2408
2478
  time.sleep(2)
2409
2479
  break
@@ -2420,15 +2490,44 @@ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout:
2420
2490
  else:
2421
2491
  remaining = "almost done"
2422
2492
 
2493
+ # Try to get real-time progress via QGA
2494
+ # Get last 3 unique progress lines
2495
+ raw_info = _exec_in_vm_qga(
2496
+ vm_name,
2497
+ conn_uri,
2498
+ "grep -E '\\[[0-9]/[0-9]\\]|→' /var/log/cloud-init-output.log 2>/dev/null | tail -n 5"
2499
+ )
2500
+
2501
+ if raw_info:
2502
+ lines = [l.strip() for l in raw_info.strip().split('\n') if l.strip()]
2503
+ for line in lines:
2504
+ if line not in seen_lines:
2505
+ # If it's a new phase line, we can log it to console above the progress bar
2506
+ if "[" in line and "/9]" in line:
2507
+ console.print(f"[dim] {line}[/]")
2508
+ seen_lines.add(line)
2509
+ last_phases.append(line)
2510
+
2511
+ # Keep only last 2 for the progress bar description
2512
+ if len(last_phases) > 2:
2513
+ last_phases = last_phases[-2:]
2514
+
2423
2515
  if restart_detected:
2424
2516
  progress.update(
2425
2517
  task,
2426
- description=f"[cyan]Starting GUI... ({minutes}m {seconds}s, {remaining})",
2518
+ description=f"[cyan]Finalizing setup... ({minutes}m {seconds}s, {remaining})",
2519
+ )
2520
+ elif last_phases:
2521
+ # Show the actual phase from logs
2522
+ current_status = last_phases[-1]
2523
+ progress.update(
2524
+ task,
2525
+ description=f"[cyan]{current_status} ({minutes}m {seconds}s, {remaining})",
2427
2526
  )
2428
2527
  else:
2429
2528
  progress.update(
2430
2529
  task,
2431
- description=f"[cyan]Installing desktop packages... ({minutes}m {seconds}s, {remaining})",
2530
+ description=f"[cyan]Installing packages... ({minutes}m {seconds}s, {remaining})",
2432
2531
  )
2433
2532
 
2434
2533
  except (subprocess.TimeoutExpired, Exception) as e:
@@ -1053,7 +1053,11 @@ if [ -n "$(echo "$COPIED_PATHS" | tr -d '[:space:]')" ]; then
1053
1053
  while IFS= read -r p; do
1054
1054
  [ -z "$p" ] && continue
1055
1055
  if [ -d "$p" ]; then
1056
- ok "$p copied"
1056
+ if [ "$(ls -A "$p" 2>/dev/null | wc -l)" -gt 0 ]; then
1057
+ ok "$p copied"
1058
+ else
1059
+ ok "$p copied (empty)"
1060
+ fi
1057
1061
  else
1058
1062
  fail "$p missing (copy)"
1059
1063
  fi
@@ -1118,6 +1122,12 @@ fi
1118
1122
  mount_checks = []
1119
1123
  for idx, (host_path, guest_path) in enumerate(config.paths.items()):
1120
1124
  mount_checks.append(f'check_mount "{guest_path}" "mount{idx}"')
1125
+
1126
+ # Add copied paths checks
1127
+ copy_paths = config.copy_paths or config.app_data_paths
1128
+ if copy_paths:
1129
+ for idx, (host_path, guest_path) in enumerate(copy_paths.items()):
1130
+ mount_checks.append(f'check_copy_path "{guest_path}"')
1121
1131
 
1122
1132
  apt_checks_str = "\n".join(apt_checks) if apt_checks else "echo 'No apt packages to check'"
1123
1133
  snap_checks_str = (
@@ -1226,6 +1236,30 @@ check_mount() {{
1226
1236
  fi
1227
1237
  }}
1228
1238
 
1239
+ check_copy_path() {{
1240
+ local path="$1"
1241
+ if [ -d "$path" ]; then
1242
+ if [ "$(ls -A "$path" 2>/dev/null | wc -l)" -gt 0 ]; then
1243
+ log "[PASS] Path '$path' exists and contains data"
1244
+ ((PASSED++))
1245
+ return 0
1246
+ else
1247
+ log "[WARN] Path '$path' exists but is EMPTY"
1248
+ ((WARNINGS++))
1249
+ return 1
1250
+ fi
1251
+ else
1252
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1253
+ log "[INFO] Path '$path' not imported yet"
1254
+ return 0
1255
+ else
1256
+ log "[FAIL] Path '$path' MISSING"
1257
+ ((FAILED++))
1258
+ return 1
1259
+ fi
1260
+ fi
1261
+ }}
1262
+
1229
1263
  check_gui() {{
1230
1264
  if systemctl get-default | grep -q graphical; then
1231
1265
  log "[PASS] System configured for graphical target"
@@ -1369,7 +1403,7 @@ fi
1369
1403
  (cloudinit_dir / "meta-data").write_text(meta_data)
1370
1404
 
1371
1405
  # Generate mount commands and fstab entries for 9p filesystems
1372
- mount_commands = []
1406
+ bind_mount_commands = []
1373
1407
  fstab_entries = []
1374
1408
  all_paths = dict(config.paths) if config.paths else {}
1375
1409
  pre_chown_dirs: set[str] = set()
@@ -1387,8 +1421,8 @@ fi
1387
1421
  d_str = str(current)
1388
1422
  if d_str not in pre_chown_dirs:
1389
1423
  pre_chown_dirs.add(d_str)
1390
- mount_commands.append(f" - mkdir -p {d_str}")
1391
- mount_commands.append(f" - chown 1000:1000 {d_str}")
1424
+ bind_mount_commands.append(f" - mkdir -p {d_str}")
1425
+ bind_mount_commands.append(f" - chown 1000:1000 {d_str}")
1392
1426
  except ValueError:
1393
1427
  pass
1394
1428
 
@@ -1399,46 +1433,48 @@ fi
1399
1433
 
1400
1434
  # Ensure target exists and is owned by user (if not already handled)
1401
1435
  if str(guest_path) not in pre_chown_dirs:
1402
- mount_commands.append(f" - mkdir -p {guest_path}")
1403
- mount_commands.append(f" - chown 1000:1000 {guest_path}")
1436
+ bind_mount_commands.append(f" - mkdir -p {guest_path}")
1437
+ bind_mount_commands.append(f" - chown 1000:1000 {guest_path}")
1404
1438
 
1405
- mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {guest_path} || true")
1439
+ bind_mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {guest_path} || true")
1406
1440
  # Add fstab entry for persistence after reboot
1407
1441
  fstab_entries.append(f"{tag} {guest_path} 9p {mount_opts},nofail 0 0")
1408
1442
 
1409
1443
  # Handle copy_paths (import then copy)
1444
+ import_mount_commands = []
1410
1445
  all_copy_paths = dict(config.copy_paths) if config.copy_paths else {}
1411
- for idx, (host_path, guest_path) in enumerate(all_copy_paths.items()):
1412
- if Path(host_path).exists():
1413
- tag = f"import{idx}"
1414
- temp_mount_point = f"/mnt/import{idx}"
1415
- # Use regular mount options
1416
- mount_opts = "trans=virtio,version=9p2000.L,mmap,uid=1000,gid=1000"
1417
-
1418
- # 1. Create temp mount point
1419
- mount_commands.append(f" - mkdir -p {temp_mount_point}")
1420
-
1421
- # 2. Mount the 9p share
1422
- mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {temp_mount_point} || true")
1423
-
1424
- # 3. Ensure target directory exists and permissions are prepared
1425
- if str(guest_path).startswith("/home/ubuntu/"):
1426
- mount_commands.append(f" - mkdir -p {guest_path}")
1427
- mount_commands.append(f" - chown 1000:1000 {guest_path}")
1428
- else:
1429
- mount_commands.append(f" - mkdir -p {guest_path}")
1446
+ existing_copy_paths = {h: g for h, g in all_copy_paths.items() if Path(h).exists()}
1447
+
1448
+ for idx, (host_path, guest_path) in enumerate(existing_copy_paths.items()):
1449
+ tag = f"import{idx}"
1450
+ temp_mount_point = f"/mnt/import{idx}"
1451
+ # Use regular mount options
1452
+ mount_opts = "trans=virtio,version=9p2000.L,mmap,uid=1000,gid=1000"
1453
+
1454
+ # 1. Create temp mount point
1455
+ import_mount_commands.append(f" - mkdir -p {temp_mount_point}")
1456
+
1457
+ # 2. Mount the 9p share
1458
+ import_mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {temp_mount_point} || true")
1459
+
1460
+ # 3. Ensure target directory exists and permissions are prepared
1461
+ if str(guest_path).startswith("/home/ubuntu/"):
1462
+ import_mount_commands.append(f" - mkdir -p {guest_path}")
1463
+ import_mount_commands.append(f" - chown 1000:1000 {guest_path}")
1464
+ else:
1465
+ import_mount_commands.append(f" - mkdir -p {guest_path}")
1430
1466
 
1431
- # 4. Copy contents (cp -rT to copy contents of source to target)
1432
- # We use || true to ensure boot continues even if copy fails
1433
- mount_commands.append(f" - echo 'Importing {host_path} to {guest_path}...'")
1434
- mount_commands.append(f" - cp -rT {temp_mount_point} {guest_path} || true")
1435
-
1436
- # 5. Fix ownership recursively
1437
- mount_commands.append(f" - chown -R 1000:1000 {guest_path}")
1467
+ # 4. Copy contents (cp -rT to copy contents of source to target)
1468
+ # We use || true to ensure boot continues even if copy fails
1469
+ import_mount_commands.append(f" - echo 'Importing {host_path} to {guest_path}...'")
1470
+ import_mount_commands.append(f" - cp -rT {temp_mount_point} {guest_path} || true")
1471
+
1472
+ # 5. Fix ownership recursively
1473
+ import_mount_commands.append(f" - chown -R 1000:1000 {guest_path}")
1438
1474
 
1439
- # 6. Unmount and cleanup
1440
- mount_commands.append(f" - umount {temp_mount_point} || true")
1441
- mount_commands.append(f" - rmdir {temp_mount_point} || true")
1475
+ # 6. Unmount and cleanup
1476
+ import_mount_commands.append(f" - umount {temp_mount_point} || true")
1477
+ import_mount_commands.append(f" - rmdir {temp_mount_point} || true")
1442
1478
 
1443
1479
  # User-data
1444
1480
  # Add desktop environment if GUI is enabled
@@ -1457,31 +1493,89 @@ fi
1457
1493
  # Build runcmd - services, mounts, snaps, post_commands
1458
1494
  runcmd_lines = []
1459
1495
 
1496
+ # Add detailed logging header
1497
+ runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
1498
+ runcmd_lines.append(" - echo ' CloneBox VM Installation Progress'")
1499
+ runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
1500
+ runcmd_lines.append(" - echo ''")
1501
+
1502
+ # Phase 1: APT Packages
1503
+ if all_packages:
1504
+ runcmd_lines.append(f" - echo '[1/9] 📦 Installing APT packages ({len(all_packages)} total)...'")
1505
+ runcmd_lines.append(" - export DEBIAN_FRONTEND=noninteractive")
1506
+ runcmd_lines.append(" - apt-get update")
1507
+ for i, pkg in enumerate(all_packages, 1):
1508
+ runcmd_lines.append(f" - echo ' → [{i}/{len(all_packages)}] Installing {pkg}...'")
1509
+ runcmd_lines.append(f" - apt-get install -y {pkg} || echo ' ⚠️ Failed to install {pkg}'")
1510
+ runcmd_lines.append(" - echo ' ✓ APT packages installed'")
1511
+ runcmd_lines.append(" - echo ''")
1512
+ else:
1513
+ runcmd_lines.append(" - echo '[1/9] 📦 No APT packages to install'")
1514
+ runcmd_lines.append(" - echo ''")
1515
+
1516
+ # Phase 2: Core services
1517
+ runcmd_lines.append(" - echo '[2/9] 🔧 Enabling core services...'")
1518
+ runcmd_lines.append(" - echo ' → qemu-guest-agent'")
1460
1519
  runcmd_lines.append(" - systemctl enable --now qemu-guest-agent || true")
1520
+ runcmd_lines.append(" - echo ' → snapd'")
1461
1521
  runcmd_lines.append(" - systemctl enable --now snapd || true")
1522
+ runcmd_lines.append(" - echo ' → Waiting for snap system seed...'")
1462
1523
  runcmd_lines.append(" - timeout 300 snap wait system seed.loaded || true")
1524
+ runcmd_lines.append(" - echo ' ✓ Core services enabled'")
1525
+ runcmd_lines.append(" - echo ''")
1463
1526
 
1464
- # Add service enablement
1465
- for svc in config.services:
1527
+ # Phase 3: User services
1528
+ runcmd_lines.append(f" - echo '[3/9] 🔧 Enabling user services ({len(config.services)} total)...'")
1529
+ for i, svc in enumerate(config.services, 1):
1530
+ runcmd_lines.append(f" - echo ' → [{i}/{len(config.services)}] {svc}'")
1466
1531
  runcmd_lines.append(f" - systemctl enable --now {svc} || true")
1467
-
1468
- # Add fstab entries for persistent mounts after reboot
1532
+ runcmd_lines.append(" - echo ' ✓ User services enabled'")
1533
+ runcmd_lines.append(" - echo ''")
1534
+
1535
+ # Phase 4: Filesystem mounts
1536
+ runcmd_lines.append(f" - echo '[4/9] 📁 Mounting shared directories ({len(config.paths)} mounts)...'")
1537
+ if bind_mount_commands:
1538
+ for cmd in bind_mount_commands:
1539
+ if "mount -t 9p" in cmd:
1540
+ # Extract mount point for logging
1541
+ parts = cmd.split()
1542
+ mp = parts[-2] if len(parts) > 2 else "path"
1543
+ runcmd_lines.append(f" - echo ' → Mounting {mp}...'")
1544
+ runcmd_lines.append(cmd)
1545
+
1469
1546
  if fstab_entries:
1470
1547
  runcmd_lines.append(
1471
1548
  " - grep -q '^# CloneBox 9p mounts' /etc/fstab || echo '# CloneBox 9p mounts' >> /etc/fstab"
1472
1549
  )
1473
- for entry in fstab_entries:
1550
+ for i, entry in enumerate(fstab_entries, 1):
1474
1551
  runcmd_lines.append(
1475
1552
  f" - grep -qF \"{entry}\" /etc/fstab || echo '{entry}' >> /etc/fstab"
1476
1553
  )
1477
1554
  runcmd_lines.append(" - mount -a || true")
1555
+ runcmd_lines.append(" - echo ' ✓ Mounts configured'")
1556
+ runcmd_lines.append(" - echo ''")
1557
+
1558
+ # Phase 5: Data Import (copied paths)
1559
+ if existing_copy_paths:
1560
+ runcmd_lines.append(f" - echo '[5/9] 📥 Importing data ({len(existing_copy_paths)} paths)...'")
1561
+ # Add import commands with progress
1562
+ import_count = 0
1563
+ for cmd in import_mount_commands:
1564
+ if "Importing" in cmd:
1565
+ import_count += 1
1566
+ runcmd_lines.append(cmd.replace("Importing", f" → [{import_count}/{len(existing_copy_paths)}] Importing"))
1567
+ else:
1568
+ runcmd_lines.append(cmd)
1569
+ runcmd_lines.append(" - echo ' ✓ Data import completed'")
1570
+ runcmd_lines.append(" - echo ''")
1571
+ else:
1572
+ runcmd_lines.append(" - echo '[5/9] 📥 No data to import'")
1573
+ runcmd_lines.append(" - echo ''")
1478
1574
 
1479
- # Add mounts (immediate, before reboot)
1480
- for cmd in mount_commands:
1481
- runcmd_lines.append(cmd)
1482
-
1483
- # Create user directories with correct permissions EARLY to avoid race conditions with GDM
1575
+ # Phase 6: GUI Environment Setup
1484
1576
  if config.gui:
1577
+ runcmd_lines.append(" - echo '[6/9] 🖥️ Setting up GUI environment...'")
1578
+ runcmd_lines.append(" - echo ' → Creating user directories'")
1485
1579
  # Create directories that GNOME services need
1486
1580
  runcmd_lines.extend(
1487
1581
  [
@@ -1491,41 +1585,56 @@ fi
1491
1585
  " - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
1492
1586
  " - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
1493
1587
  " - systemctl set-default graphical.target",
1494
- " - systemctl enable --now gdm3 || systemctl enable --now gdm || true",
1495
- " - systemctl start display-manager || true",
1588
+ " - echo ' → Starting display manager'",
1496
1589
  ]
1497
1590
  )
1591
+ runcmd_lines.append(" - systemctl enable --now gdm3 || systemctl enable --now gdm || true")
1592
+ runcmd_lines.append(" - systemctl start display-manager || true")
1593
+ runcmd_lines.append(" - echo ' ✓ GUI environment ready'")
1594
+ runcmd_lines.append(" - echo ''")
1595
+ else:
1596
+ runcmd_lines.append(" - echo '[6/9] 🖥️ No GUI requested'")
1597
+ runcmd_lines.append(" - echo ''")
1498
1598
 
1499
1599
  runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu || true")
1500
1600
  runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/snap || true")
1501
1601
 
1502
- # Install snap packages (with retry logic)
1602
+ # Phase 7: Snap packages
1503
1603
  if config.snap_packages:
1504
- runcmd_lines.append(" - echo 'Installing snap packages...'")
1505
- for snap_pkg in config.snap_packages:
1604
+ runcmd_lines.append(f" - echo '[7/9] 📦 Installing snap packages ({len(config.snap_packages)} packages)...'")
1605
+ for i, snap_pkg in enumerate(config.snap_packages, 1):
1606
+ runcmd_lines.append(f" - echo ' → [{i}/{len(config.snap_packages)}] {snap_pkg}'")
1506
1607
  # Try classic first, then strict, with retries
1507
1608
  cmd = (
1508
1609
  f"for i in 1 2 3; do "
1509
- f"snap install {snap_pkg} --classic && break || "
1510
- f"snap install {snap_pkg} && break || "
1511
- f"sleep 10; "
1610
+ f"snap install {snap_pkg} --classic && echo ' ✓ {snap_pkg} installed (classic)' && break || "
1611
+ f"snap install {snap_pkg} && echo ' ✓ {snap_pkg} installed' && break || "
1612
+ f"echo ' ⟳ Retry $i/3...' && sleep 10; "
1512
1613
  f"done"
1513
1614
  )
1514
1615
  runcmd_lines.append(f" - {cmd}")
1616
+ runcmd_lines.append(" - echo ' ✓ Snap packages installed'")
1617
+ runcmd_lines.append(" - echo ''")
1515
1618
 
1516
1619
  # Connect snap interfaces for GUI apps (not auto-connected via cloud-init)
1517
- runcmd_lines.append(" - echo 'Connecting snap interfaces...'")
1620
+ runcmd_lines.append(f" - echo ' 🔌 Connecting snap interfaces...'")
1518
1621
  for snap_pkg in config.snap_packages:
1622
+ runcmd_lines.append(f" - echo ' → {snap_pkg}'")
1519
1623
  interfaces = SNAP_INTERFACES.get(snap_pkg, DEFAULT_SNAP_INTERFACES)
1520
1624
  for iface in interfaces:
1521
1625
  runcmd_lines.append(
1522
1626
  f" - snap connect {snap_pkg}:{iface} :{iface} 2>/dev/null || true"
1523
1627
  )
1524
-
1628
+ runcmd_lines.append(" - echo ' ✓ Snap interfaces connected'")
1525
1629
  runcmd_lines.append(" - systemctl restart snapd || true")
1630
+ runcmd_lines.append(" - echo ''")
1631
+ else:
1632
+ runcmd_lines.append(" - echo '[7/9] 📦 No snap packages to install'")
1633
+ runcmd_lines.append(" - echo ''")
1526
1634
 
1527
1635
  # Add remaining GUI setup if enabled
1528
1636
  if config.gui:
1637
+ runcmd_lines.append(" - echo ' ⚙️ Creating autostart entries...'")
1529
1638
  # Create autostart entries for GUI apps
1530
1639
  autostart_apps = {
1531
1640
  "pycharm-community": (
@@ -1577,15 +1686,28 @@ Comment=CloneBox autostart
1577
1686
 
1578
1687
  # Fix ownership of autostart directory
1579
1688
  runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/.config/autostart")
1689
+ runcmd_lines.append(" - echo ' ✓ Autostart entries created'")
1690
+ runcmd_lines.append(" - echo ''")
1580
1691
 
1581
- # Run user-defined post commands
1692
+ # Phase 8: Post commands
1582
1693
  if config.post_commands:
1583
- runcmd_lines.append(" - echo 'Running post-setup commands...'")
1584
- for cmd in config.post_commands:
1694
+ runcmd_lines.append(f" - echo '[8/9] ⚙️ Running post-setup commands ({len(config.post_commands)} commands)...'")
1695
+ for i, cmd in enumerate(config.post_commands, 1):
1696
+ # Truncate long commands for display
1697
+ display_cmd = cmd[:60] + '...' if len(cmd) > 60 else cmd
1698
+ runcmd_lines.append(f" - echo ' → [{i}/{len(config.post_commands)}] {display_cmd}'")
1585
1699
  runcmd_lines.append(f" - {cmd}")
1700
+ runcmd_lines.append(f" - echo ' ✓ Command {i} completed'")
1701
+ runcmd_lines.append(" - echo ' ✓ Post-setup commands completed'")
1702
+ runcmd_lines.append(" - echo ''")
1703
+ else:
1704
+ runcmd_lines.append(" - echo '[8/9] ⚙️ No post-setup commands'")
1705
+ runcmd_lines.append(" - echo ''")
1586
1706
 
1587
1707
  # Generate health check script
1588
1708
  health_script = self._generate_health_check_script(config)
1709
+ # Phase 9: Health checks and finalization
1710
+ runcmd_lines.append(" - echo '[9/9] 🏥 Running health checks...'")
1589
1711
  runcmd_lines.append(
1590
1712
  f" - echo '{health_script}' | base64 -d > /usr/local/bin/clonebox-health"
1591
1713
  )
@@ -1593,8 +1715,15 @@ Comment=CloneBox autostart
1593
1715
  runcmd_lines.append(
1594
1716
  " - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1 || true"
1595
1717
  )
1718
+ runcmd_lines.append(" - echo ' ✓ Health checks completed'")
1596
1719
  runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
1597
-
1720
+
1721
+ # Final status
1722
+ runcmd_lines.append(" - echo ''")
1723
+ runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
1724
+ runcmd_lines.append(" - echo ' ✅ CloneBox VM Installation Complete!'")
1725
+ runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
1726
+ runcmd_lines.append(" - echo ''")
1598
1727
  # Generate boot diagnostic script (self-healing)
1599
1728
  boot_diag_script = self._generate_boot_diagnostic_script(config)
1600
1729
  runcmd_lines.append(
@@ -2349,7 +2478,8 @@ if __name__ == "__main__":
2349
2478
 
2350
2479
  # Add reboot command at the end if GUI is enabled
2351
2480
  if config.gui:
2352
- runcmd_lines.append(" - echo 'Rebooting in 10 seconds to start GUI...'")
2481
+ runcmd_lines.append(" - echo '🔄 Rebooting in 10 seconds to start GUI...'")
2482
+ runcmd_lines.append(" - echo ' (After reboot, GUI will auto-start)'")
2353
2483
  runcmd_lines.append(" - sleep 10 && reboot")
2354
2484
 
2355
2485
  runcmd_yaml = "\n".join(runcmd_lines) if runcmd_lines else ""
@@ -2397,9 +2527,8 @@ package_update: true
2397
2527
  package_upgrade: false
2398
2528
  {bootcmd_block}
2399
2529
 
2400
- # Install packages (cloud-init waits for completion before runcmd)
2401
- packages:
2402
- {packages_yaml}
2530
+ # Install packages moved to runcmd for better logging
2531
+ packages: []
2403
2532
 
2404
2533
  # Run after packages are installed
2405
2534
  runcmd:
@@ -155,6 +155,7 @@ class VMValidator:
155
155
  mount_table.add_column("Files", justify="right")
156
156
 
157
157
  # Validate bind mounts (paths)
158
+ setup_in_progress = self._setup_in_progress() is True
158
159
  for host_path, guest_path in paths.items():
159
160
  self.results["mounts"]["total"] += 1
160
161
 
@@ -182,6 +183,9 @@ class VMValidator:
182
183
  status_icon = "[red]❌ Inaccessible[/]"
183
184
  self.results["mounts"]["failed"] += 1
184
185
  status = "mounted_but_inaccessible"
186
+ elif setup_in_progress:
187
+ status_icon = "[yellow]⏳ Pending[/]"
188
+ status = "pending"
185
189
  else:
186
190
  status_icon = "[red]❌ Not Mounted[/]"
187
191
  self.results["mounts"]["failed"] += 1
@@ -219,6 +223,9 @@ class VMValidator:
219
223
  status_icon = "[green]✅ Copied[/]"
220
224
  self.results["mounts"]["passed"] += 1
221
225
  status = "pass"
226
+ elif setup_in_progress:
227
+ status_icon = "[yellow]⏳ Pending[/]"
228
+ status = "pending"
222
229
  else:
223
230
  status_icon = "[red]❌ Missing[/]"
224
231
  self.results["mounts"]["failed"] += 1
@@ -255,6 +262,8 @@ class VMValidator:
255
262
  pkg_table.add_column("Status", justify="center")
256
263
  pkg_table.add_column("Version", style="dim")
257
264
 
265
+ setup_in_progress = self._setup_in_progress() is True
266
+
258
267
  for package in packages:
259
268
  self.results["packages"]["total"] += 1
260
269
 
@@ -269,11 +278,17 @@ class VMValidator:
269
278
  {"package": package, "installed": True, "version": version}
270
279
  )
271
280
  else:
272
- pkg_table.add_row(package, "[red]❌ Missing[/]", "")
273
- self.results["packages"]["failed"] += 1
274
- self.results["packages"]["details"].append(
275
- {"package": package, "installed": False, "version": None}
276
- )
281
+ if setup_in_progress:
282
+ pkg_table.add_row(package, "[yellow]⏳ Pending[/]", "")
283
+ self.results["packages"]["details"].append(
284
+ {"package": package, "installed": False, "version": None, "pending": True}
285
+ )
286
+ else:
287
+ pkg_table.add_row(package, "[red]❌ Missing[/]", "")
288
+ self.results["packages"]["failed"] += 1
289
+ self.results["packages"]["details"].append(
290
+ {"package": package, "installed": False, "version": None}
291
+ )
277
292
 
278
293
  self.console.print(pkg_table)
279
294
  self.console.print(
@@ -419,14 +434,14 @@ class VMValidator:
419
434
  else:
420
435
  pid_value = "—"
421
436
 
422
- enabled_icon = "[green]✅[/]" if is_enabled else "[yellow]⚠️[/]"
423
- running_icon = "[green]✅[/]" if is_running else "[red]❌[/]"
437
+ enabled_icon = "[green]✅[/]" if is_enabled else ("[yellow]⏳[/]" if setup_in_progress else "[yellow]⚠️[/]")
438
+ running_icon = "[green]✅[/]" if is_running else ("[yellow]⏳[/]" if setup_in_progress else "[red]❌[/]")
424
439
 
425
440
  svc_table.add_row(service, enabled_icon, running_icon, pid_value, "")
426
441
 
427
442
  if is_enabled and is_running:
428
443
  self.results["services"]["passed"] += 1
429
- else:
444
+ elif not setup_in_progress:
430
445
  self.results["services"]["failed"] += 1
431
446
 
432
447
  self.results["services"]["details"].append(
@@ -994,18 +1009,18 @@ class VMValidator:
994
1009
  return self.results
995
1010
 
996
1011
  ci_status = self._exec_in_vm("cloud-init status --long 2>/dev/null || cloud-init status 2>/dev/null || true", timeout=20)
1012
+ setup_in_progress = False
997
1013
  if ci_status:
998
1014
  ci_lower = ci_status.lower()
999
1015
  if "running" in ci_lower:
1000
- self.console.print("[yellow]⏳ Cloud-init still running - skipping deep validation for now[/]")
1001
- self.results["overall"] = "cloud_init_running"
1002
- return self.results
1016
+ self.console.print("[yellow]⏳ Cloud-init still running - deep validation will show pending states[/]")
1017
+ setup_in_progress = True
1003
1018
 
1004
1019
  ready_msg = self._exec_in_vm(
1005
1020
  "cat /var/log/clonebox-ready 2>/dev/null || true",
1006
1021
  timeout=10,
1007
1022
  )
1008
- if not (ready_msg and "clonebox vm ready" in ready_msg.lower()):
1023
+ if not setup_in_progress and not (ready_msg and "clonebox vm ready" in ready_msg.lower()):
1009
1024
  self.console.print(
1010
1025
  "[yellow]⚠️ CloneBox ready marker not found - provisioning may not have completed[/]"
1011
1026
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.15
3
+ Version: 1.1.17
4
4
  Summary: Clone your workstation environment to an isolated VM with selective apps, paths and services
5
5
  Author: CloneBox Team
6
6
  License: Apache-2.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes