clonebox 1.1.15__tar.gz → 1.1.16__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.
- {clonebox-1.1.15/src/clonebox.egg-info → clonebox-1.1.16}/PKG-INFO +1 -1
- {clonebox-1.1.15 → clonebox-1.1.16}/pyproject.toml +1 -1
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/cli.py +105 -6
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/cloner.py +178 -57
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/validator.py +27 -12
- {clonebox-1.1.15 → clonebox-1.1.16/src/clonebox.egg-info}/PKG-INFO +1 -1
- {clonebox-1.1.15 → clonebox-1.1.16}/LICENSE +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/README.md +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/setup.cfg +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/__init__.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/__main__.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/audit.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/backends/libvirt_backend.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/backends/qemu_disk.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/backends/subprocess_runner.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/container.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/dashboard.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/detector.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/di.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/exporter.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/health/__init__.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/health/manager.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/health/models.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/health/probes.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/importer.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/interfaces/disk.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/interfaces/hypervisor.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/interfaces/network.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/interfaces/process.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/logging.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/models.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/monitor.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/orchestrator.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/p2p.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/plugins/__init__.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/plugins/base.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/plugins/manager.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/profiles.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/remote.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/resource_monitor.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/resources.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/rollback.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/secrets.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/snapshots/__init__.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/snapshots/manager.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/snapshots/models.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox/templates/profiles/web-stack.yaml +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox.egg-info/SOURCES.txt +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox.egg-info/dependency_links.txt +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox.egg-info/entry_points.txt +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox.egg-info/requires.txt +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/src/clonebox.egg-info/top_level.txt +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_audit.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_cli.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_cli_new_commands.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_cloner.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_cloner_simple.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_container.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_coverage_additional.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_coverage_boost_final.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_dashboard_coverage.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_detector.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_models.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_network.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_orchestrator.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_plugins.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_profiles.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_remote.py +0 -0
- {clonebox-1.1.15 → clonebox-1.1.16}/tests/test_validator.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clonebox"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.16"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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]✓
|
|
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]
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
@@ -1224,6 +1234,30 @@ check_mount() {{
|
|
|
1224
1234
|
log "[INFO] Mount point '$path' does not exist yet"
|
|
1225
1235
|
return 0
|
|
1226
1236
|
fi
|
|
1237
|
+
}
|
|
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
|
|
1227
1261
|
}}
|
|
1228
1262
|
|
|
1229
1263
|
check_gui() {{
|
|
@@ -1408,37 +1442,38 @@ fi
|
|
|
1408
1442
|
|
|
1409
1443
|
# Handle copy_paths (import then copy)
|
|
1410
1444
|
all_copy_paths = dict(config.copy_paths) if config.copy_paths else {}
|
|
1411
|
-
for
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1445
|
+
existing_copy_paths = {h: g for h, g in all_copy_paths.items() if Path(h).exists()}
|
|
1446
|
+
|
|
1447
|
+
for idx, (host_path, guest_path) in enumerate(existing_copy_paths.items()):
|
|
1448
|
+
tag = f"import{idx}"
|
|
1449
|
+
temp_mount_point = f"/mnt/import{idx}"
|
|
1450
|
+
# Use regular mount options
|
|
1451
|
+
mount_opts = "trans=virtio,version=9p2000.L,mmap,uid=1000,gid=1000"
|
|
1452
|
+
|
|
1453
|
+
# 1. Create temp mount point
|
|
1454
|
+
mount_commands.append(f" - mkdir -p {temp_mount_point}")
|
|
1455
|
+
|
|
1456
|
+
# 2. Mount the 9p share
|
|
1457
|
+
mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {temp_mount_point} || true")
|
|
1458
|
+
|
|
1459
|
+
# 3. Ensure target directory exists and permissions are prepared
|
|
1460
|
+
if str(guest_path).startswith("/home/ubuntu/"):
|
|
1461
|
+
mount_commands.append(f" - mkdir -p {guest_path}")
|
|
1462
|
+
mount_commands.append(f" - chown 1000:1000 {guest_path}")
|
|
1463
|
+
else:
|
|
1464
|
+
mount_commands.append(f" - mkdir -p {guest_path}")
|
|
1430
1465
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1466
|
+
# 4. Copy contents (cp -rT to copy contents of source to target)
|
|
1467
|
+
# We use || true to ensure boot continues even if copy fails
|
|
1468
|
+
mount_commands.append(f" - echo 'Importing {host_path} to {guest_path}...'")
|
|
1469
|
+
mount_commands.append(f" - cp -rT {temp_mount_point} {guest_path} || true")
|
|
1470
|
+
|
|
1471
|
+
# 5. Fix ownership recursively
|
|
1472
|
+
mount_commands.append(f" - chown -R 1000:1000 {guest_path}")
|
|
1438
1473
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1474
|
+
# 6. Unmount and cleanup
|
|
1475
|
+
mount_commands.append(f" - umount {temp_mount_point} || true")
|
|
1476
|
+
mount_commands.append(f" - rmdir {temp_mount_point} || true")
|
|
1442
1477
|
|
|
1443
1478
|
# User-data
|
|
1444
1479
|
# Add desktop environment if GUI is enabled
|
|
@@ -1457,31 +1492,82 @@ fi
|
|
|
1457
1492
|
# Build runcmd - services, mounts, snaps, post_commands
|
|
1458
1493
|
runcmd_lines = []
|
|
1459
1494
|
|
|
1495
|
+
# Add detailed logging header
|
|
1496
|
+
runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
|
|
1497
|
+
runcmd_lines.append(" - echo ' CloneBox VM Installation Progress'")
|
|
1498
|
+
runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
|
|
1499
|
+
runcmd_lines.append(" - echo ''")
|
|
1500
|
+
|
|
1501
|
+
# Phase 1: APT Packages
|
|
1502
|
+
if all_packages:
|
|
1503
|
+
runcmd_lines.append(f" - echo '[1/9] 📦 Installing APT packages ({len(all_packages)} total)...'")
|
|
1504
|
+
runcmd_lines.append(" - export DEBIAN_FRONTEND=noninteractive")
|
|
1505
|
+
runcmd_lines.append(" - apt-get update")
|
|
1506
|
+
for i, pkg in enumerate(all_packages, 1):
|
|
1507
|
+
runcmd_lines.append(f" - echo ' → [{i}/{len(all_packages)}] Installing {pkg}...'")
|
|
1508
|
+
runcmd_lines.append(f" - apt-get install -y {pkg} || echo ' ⚠️ Failed to install {pkg}'")
|
|
1509
|
+
runcmd_lines.append(" - echo ' ✓ APT packages installed'")
|
|
1510
|
+
runcmd_lines.append(" - echo ''")
|
|
1511
|
+
else:
|
|
1512
|
+
runcmd_lines.append(" - echo '[1/9] 📦 No APT packages to install'")
|
|
1513
|
+
runcmd_lines.append(" - echo ''")
|
|
1514
|
+
|
|
1515
|
+
# Phase 2: Core services
|
|
1516
|
+
runcmd_lines.append(" - echo '[2/9] 🔧 Enabling core services...'")
|
|
1517
|
+
runcmd_lines.append(" - echo ' → qemu-guest-agent'")
|
|
1460
1518
|
runcmd_lines.append(" - systemctl enable --now qemu-guest-agent || true")
|
|
1519
|
+
runcmd_lines.append(" - echo ' → snapd'")
|
|
1461
1520
|
runcmd_lines.append(" - systemctl enable --now snapd || true")
|
|
1521
|
+
runcmd_lines.append(" - echo ' → Waiting for snap system seed...'")
|
|
1462
1522
|
runcmd_lines.append(" - timeout 300 snap wait system seed.loaded || true")
|
|
1523
|
+
runcmd_lines.append(" - echo ' ✓ Core services enabled'")
|
|
1524
|
+
runcmd_lines.append(" - echo ''")
|
|
1463
1525
|
|
|
1464
|
-
#
|
|
1465
|
-
|
|
1526
|
+
# Phase 3: User services
|
|
1527
|
+
runcmd_lines.append(f" - echo '[3/9] 🔧 Enabling user services ({len(config.services)} total)...'")
|
|
1528
|
+
for i, svc in enumerate(config.services, 1):
|
|
1529
|
+
runcmd_lines.append(f" - echo ' → [{i}/{len(config.services)}] {svc}'")
|
|
1466
1530
|
runcmd_lines.append(f" - systemctl enable --now {svc} || true")
|
|
1531
|
+
runcmd_lines.append(" - echo ' ✓ User services enabled'")
|
|
1532
|
+
runcmd_lines.append(" - echo ''")
|
|
1467
1533
|
|
|
1468
|
-
#
|
|
1534
|
+
# Phase 4: Filesystem mounts
|
|
1535
|
+
runcmd_lines.append(f" - echo '[4/9] 📁 Mounting shared directories ({len(fstab_entries)} mounts)...'")
|
|
1469
1536
|
if fstab_entries:
|
|
1470
1537
|
runcmd_lines.append(
|
|
1471
1538
|
" - grep -q '^# CloneBox 9p mounts' /etc/fstab || echo '# CloneBox 9p mounts' >> /etc/fstab"
|
|
1472
1539
|
)
|
|
1473
|
-
for entry in fstab_entries:
|
|
1540
|
+
for i, entry in enumerate(fstab_entries, 1):
|
|
1541
|
+
mount_point = entry.split()[1] if len(entry.split()) > 1 else entry
|
|
1542
|
+
runcmd_lines.append(f" - echo ' → [{i}/{len(fstab_entries)}] {mount_point}'")
|
|
1474
1543
|
runcmd_lines.append(
|
|
1475
1544
|
f" - grep -qF \"{entry}\" /etc/fstab || echo '{entry}' >> /etc/fstab"
|
|
1476
1545
|
)
|
|
1477
1546
|
runcmd_lines.append(" - mount -a || true")
|
|
1547
|
+
runcmd_lines.append(" - echo ' ✓ Mounts configured'")
|
|
1548
|
+
runcmd_lines.append(" - echo ''")
|
|
1549
|
+
|
|
1550
|
+
# Phase 5: Data Import (copied paths)
|
|
1551
|
+
if existing_copy_paths:
|
|
1552
|
+
runcmd_lines.append(f" - echo '[5/9] 📥 Importing data ({len(existing_copy_paths)} paths)...'")
|
|
1553
|
+
# Add mounts (immediate, before reboot)
|
|
1554
|
+
import_count = 0
|
|
1555
|
+
for cmd in mount_commands:
|
|
1556
|
+
if "Importing" in cmd:
|
|
1557
|
+
import_count += 1
|
|
1558
|
+
runcmd_lines.append(cmd.replace("Importing", f" → [{import_count}/{len(existing_copy_paths)}] Importing"))
|
|
1559
|
+
else:
|
|
1560
|
+
runcmd_lines.append(cmd)
|
|
1561
|
+
runcmd_lines.append(" - echo ' ✓ Data import completed'")
|
|
1562
|
+
runcmd_lines.append(" - echo ''")
|
|
1563
|
+
else:
|
|
1564
|
+
runcmd_lines.append(" - echo '[5/9] 📥 No data to import'")
|
|
1565
|
+
runcmd_lines.append(" - echo ''")
|
|
1478
1566
|
|
|
1479
|
-
#
|
|
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
|
|
1567
|
+
# Phase 6: GUI Environment Setup
|
|
1484
1568
|
if config.gui:
|
|
1569
|
+
runcmd_lines.append(" - echo '[6/9] 🖥️ Setting up GUI environment...'")
|
|
1570
|
+
runcmd_lines.append(" - echo ' → Creating user directories'")
|
|
1485
1571
|
# Create directories that GNOME services need
|
|
1486
1572
|
runcmd_lines.extend(
|
|
1487
1573
|
[
|
|
@@ -1491,41 +1577,56 @@ fi
|
|
|
1491
1577
|
" - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
|
|
1492
1578
|
" - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
|
|
1493
1579
|
" - systemctl set-default graphical.target",
|
|
1494
|
-
" -
|
|
1495
|
-
" - systemctl start display-manager || true",
|
|
1580
|
+
" - echo ' → Starting display manager'",
|
|
1496
1581
|
]
|
|
1497
1582
|
)
|
|
1583
|
+
runcmd_lines.append(" - systemctl enable --now gdm3 || systemctl enable --now gdm || true")
|
|
1584
|
+
runcmd_lines.append(" - systemctl start display-manager || true")
|
|
1585
|
+
runcmd_lines.append(" - echo ' ✓ GUI environment ready'")
|
|
1586
|
+
runcmd_lines.append(" - echo ''")
|
|
1587
|
+
else:
|
|
1588
|
+
runcmd_lines.append(" - echo '[6/9] 🖥️ No GUI requested'")
|
|
1589
|
+
runcmd_lines.append(" - echo ''")
|
|
1498
1590
|
|
|
1499
1591
|
runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu || true")
|
|
1500
1592
|
runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/snap || true")
|
|
1501
1593
|
|
|
1502
|
-
#
|
|
1594
|
+
# Phase 7: Snap packages
|
|
1503
1595
|
if config.snap_packages:
|
|
1504
|
-
runcmd_lines.append(" - echo 'Installing snap packages...'")
|
|
1505
|
-
for snap_pkg in config.snap_packages:
|
|
1596
|
+
runcmd_lines.append(f" - echo '[7/9] 📦 Installing snap packages ({len(config.snap_packages)} packages)...'")
|
|
1597
|
+
for i, snap_pkg in enumerate(config.snap_packages, 1):
|
|
1598
|
+
runcmd_lines.append(f" - echo ' → [{i}/{len(config.snap_packages)}] {snap_pkg}'")
|
|
1506
1599
|
# Try classic first, then strict, with retries
|
|
1507
1600
|
cmd = (
|
|
1508
1601
|
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; "
|
|
1602
|
+
f"snap install {snap_pkg} --classic && echo ' ✓ {snap_pkg} installed (classic)' && break || "
|
|
1603
|
+
f"snap install {snap_pkg} && echo ' ✓ {snap_pkg} installed' && break || "
|
|
1604
|
+
f"echo ' ⟳ Retry $i/3...' && sleep 10; "
|
|
1512
1605
|
f"done"
|
|
1513
1606
|
)
|
|
1514
1607
|
runcmd_lines.append(f" - {cmd}")
|
|
1608
|
+
runcmd_lines.append(" - echo ' ✓ Snap packages installed'")
|
|
1609
|
+
runcmd_lines.append(" - echo ''")
|
|
1515
1610
|
|
|
1516
1611
|
# Connect snap interfaces for GUI apps (not auto-connected via cloud-init)
|
|
1517
|
-
runcmd_lines.append(" - echo 'Connecting snap interfaces...'")
|
|
1612
|
+
runcmd_lines.append(f" - echo ' 🔌 Connecting snap interfaces...'")
|
|
1518
1613
|
for snap_pkg in config.snap_packages:
|
|
1614
|
+
runcmd_lines.append(f" - echo ' → {snap_pkg}'")
|
|
1519
1615
|
interfaces = SNAP_INTERFACES.get(snap_pkg, DEFAULT_SNAP_INTERFACES)
|
|
1520
1616
|
for iface in interfaces:
|
|
1521
1617
|
runcmd_lines.append(
|
|
1522
1618
|
f" - snap connect {snap_pkg}:{iface} :{iface} 2>/dev/null || true"
|
|
1523
1619
|
)
|
|
1524
|
-
|
|
1620
|
+
runcmd_lines.append(" - echo ' ✓ Snap interfaces connected'")
|
|
1525
1621
|
runcmd_lines.append(" - systemctl restart snapd || true")
|
|
1622
|
+
runcmd_lines.append(" - echo ''")
|
|
1623
|
+
else:
|
|
1624
|
+
runcmd_lines.append(" - echo '[7/9] 📦 No snap packages to install'")
|
|
1625
|
+
runcmd_lines.append(" - echo ''")
|
|
1526
1626
|
|
|
1527
1627
|
# Add remaining GUI setup if enabled
|
|
1528
1628
|
if config.gui:
|
|
1629
|
+
runcmd_lines.append(" - echo ' ⚙️ Creating autostart entries...'")
|
|
1529
1630
|
# Create autostart entries for GUI apps
|
|
1530
1631
|
autostart_apps = {
|
|
1531
1632
|
"pycharm-community": (
|
|
@@ -1577,15 +1678,28 @@ Comment=CloneBox autostart
|
|
|
1577
1678
|
|
|
1578
1679
|
# Fix ownership of autostart directory
|
|
1579
1680
|
runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/.config/autostart")
|
|
1681
|
+
runcmd_lines.append(" - echo ' ✓ Autostart entries created'")
|
|
1682
|
+
runcmd_lines.append(" - echo ''")
|
|
1580
1683
|
|
|
1581
|
-
#
|
|
1684
|
+
# Phase 8: Post commands
|
|
1582
1685
|
if config.post_commands:
|
|
1583
|
-
runcmd_lines.append(" - echo 'Running post-setup commands...'")
|
|
1584
|
-
for cmd in config.post_commands:
|
|
1686
|
+
runcmd_lines.append(f" - echo '[8/9] ⚙️ Running post-setup commands ({len(config.post_commands)} commands)...'")
|
|
1687
|
+
for i, cmd in enumerate(config.post_commands, 1):
|
|
1688
|
+
# Truncate long commands for display
|
|
1689
|
+
display_cmd = cmd[:60] + '...' if len(cmd) > 60 else cmd
|
|
1690
|
+
runcmd_lines.append(f" - echo ' → [{i}/{len(config.post_commands)}] {display_cmd}'")
|
|
1585
1691
|
runcmd_lines.append(f" - {cmd}")
|
|
1692
|
+
runcmd_lines.append(f" - echo ' ✓ Command {i} completed'")
|
|
1693
|
+
runcmd_lines.append(" - echo ' ✓ Post-setup commands completed'")
|
|
1694
|
+
runcmd_lines.append(" - echo ''")
|
|
1695
|
+
else:
|
|
1696
|
+
runcmd_lines.append(" - echo '[8/9] ⚙️ No post-setup commands'")
|
|
1697
|
+
runcmd_lines.append(" - echo ''")
|
|
1586
1698
|
|
|
1587
1699
|
# Generate health check script
|
|
1588
1700
|
health_script = self._generate_health_check_script(config)
|
|
1701
|
+
# Phase 9: Health checks and finalization
|
|
1702
|
+
runcmd_lines.append(" - echo '[9/9] 🏥 Running health checks...'")
|
|
1589
1703
|
runcmd_lines.append(
|
|
1590
1704
|
f" - echo '{health_script}' | base64 -d > /usr/local/bin/clonebox-health"
|
|
1591
1705
|
)
|
|
@@ -1593,8 +1707,15 @@ Comment=CloneBox autostart
|
|
|
1593
1707
|
runcmd_lines.append(
|
|
1594
1708
|
" - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1 || true"
|
|
1595
1709
|
)
|
|
1710
|
+
runcmd_lines.append(" - echo ' ✓ Health checks completed'")
|
|
1596
1711
|
runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
|
|
1597
|
-
|
|
1712
|
+
|
|
1713
|
+
# Final status
|
|
1714
|
+
runcmd_lines.append(" - echo ''")
|
|
1715
|
+
runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
|
|
1716
|
+
runcmd_lines.append(" - echo ' ✅ CloneBox VM Installation Complete!'")
|
|
1717
|
+
runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
|
|
1718
|
+
runcmd_lines.append(" - echo ''")
|
|
1598
1719
|
# Generate boot diagnostic script (self-healing)
|
|
1599
1720
|
boot_diag_script = self._generate_boot_diagnostic_script(config)
|
|
1600
1721
|
runcmd_lines.append(
|
|
@@ -2349,7 +2470,8 @@ if __name__ == "__main__":
|
|
|
2349
2470
|
|
|
2350
2471
|
# Add reboot command at the end if GUI is enabled
|
|
2351
2472
|
if config.gui:
|
|
2352
|
-
runcmd_lines.append(" - echo 'Rebooting in 10 seconds to start GUI...'")
|
|
2473
|
+
runcmd_lines.append(" - echo '🔄 Rebooting in 10 seconds to start GUI...'")
|
|
2474
|
+
runcmd_lines.append(" - echo ' (After reboot, GUI will auto-start)'")
|
|
2353
2475
|
runcmd_lines.append(" - sleep 10 && reboot")
|
|
2354
2476
|
|
|
2355
2477
|
runcmd_yaml = "\n".join(runcmd_lines) if runcmd_lines else ""
|
|
@@ -2397,9 +2519,8 @@ package_update: true
|
|
|
2397
2519
|
package_upgrade: false
|
|
2398
2520
|
{bootcmd_block}
|
|
2399
2521
|
|
|
2400
|
-
# Install packages
|
|
2401
|
-
packages:
|
|
2402
|
-
{packages_yaml}
|
|
2522
|
+
# Install packages moved to runcmd for better logging
|
|
2523
|
+
packages: []
|
|
2403
2524
|
|
|
2404
2525
|
# Run after packages are installed
|
|
2405
2526
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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 -
|
|
1001
|
-
|
|
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
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|