clonebox 1.1.19__tar.gz → 1.1.21__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 (74) hide show
  1. {clonebox-1.1.19/src/clonebox.egg-info → clonebox-1.1.21}/PKG-INFO +1 -1
  2. {clonebox-1.1.19 → clonebox-1.1.21}/pyproject.toml +1 -1
  3. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/cli.py +49 -7
  4. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/cloner.py +35 -6
  5. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/validator.py +100 -15
  6. {clonebox-1.1.19 → clonebox-1.1.21/src/clonebox.egg-info}/PKG-INFO +1 -1
  7. {clonebox-1.1.19 → clonebox-1.1.21}/LICENSE +0 -0
  8. {clonebox-1.1.19 → clonebox-1.1.21}/README.md +0 -0
  9. {clonebox-1.1.19 → clonebox-1.1.21}/setup.cfg +0 -0
  10. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/__init__.py +0 -0
  11. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/__main__.py +0 -0
  12. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/audit.py +0 -0
  13. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/backends/libvirt_backend.py +0 -0
  14. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/backends/qemu_disk.py +0 -0
  15. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/backends/subprocess_runner.py +0 -0
  16. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/container.py +0 -0
  17. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/dashboard.py +0 -0
  18. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/detector.py +0 -0
  19. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/di.py +0 -0
  20. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/exporter.py +0 -0
  21. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/health/__init__.py +0 -0
  22. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/health/manager.py +0 -0
  23. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/health/models.py +0 -0
  24. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/health/probes.py +0 -0
  25. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/importer.py +0 -0
  26. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/interfaces/disk.py +0 -0
  27. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/interfaces/hypervisor.py +0 -0
  28. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/interfaces/network.py +0 -0
  29. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/interfaces/process.py +0 -0
  30. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/logging.py +0 -0
  31. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/models.py +0 -0
  32. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/monitor.py +0 -0
  33. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/orchestrator.py +0 -0
  34. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/p2p.py +0 -0
  35. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/plugins/__init__.py +0 -0
  36. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/plugins/base.py +0 -0
  37. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/plugins/manager.py +0 -0
  38. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/policies/__init__.py +0 -0
  39. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/policies/engine.py +0 -0
  40. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/policies/models.py +0 -0
  41. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/policies/validators.py +0 -0
  42. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/profiles.py +0 -0
  43. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/remote.py +0 -0
  44. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/resource_monitor.py +0 -0
  45. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/resources.py +0 -0
  46. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/rollback.py +0 -0
  47. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/secrets.py +0 -0
  48. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/snapshots/__init__.py +0 -0
  49. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/snapshots/manager.py +0 -0
  50. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/snapshots/models.py +0 -0
  51. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
  52. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox/templates/profiles/web-stack.yaml +0 -0
  53. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox.egg-info/SOURCES.txt +0 -0
  54. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox.egg-info/dependency_links.txt +0 -0
  55. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox.egg-info/entry_points.txt +0 -0
  56. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox.egg-info/requires.txt +0 -0
  57. {clonebox-1.1.19 → clonebox-1.1.21}/src/clonebox.egg-info/top_level.txt +0 -0
  58. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_audit.py +0 -0
  59. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_cli.py +0 -0
  60. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_cli_new_commands.py +0 -0
  61. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_cloner.py +0 -0
  62. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_cloner_simple.py +0 -0
  63. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_container.py +0 -0
  64. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_coverage_additional.py +0 -0
  65. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_coverage_boost_final.py +0 -0
  66. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_dashboard_coverage.py +0 -0
  67. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_detector.py +0 -0
  68. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_models.py +0 -0
  69. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_network.py +0 -0
  70. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_orchestrator.py +0 -0
  71. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_plugins.py +0 -0
  72. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_profiles.py +0 -0
  73. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_remote.py +0 -0
  74. {clonebox-1.1.19 → clonebox-1.1.21}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.19
3
+ Version: 1.1.21
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.19"
7
+ version = "1.1.21"
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"}
@@ -945,10 +945,27 @@ def interactive_mode():
945
945
  cloner = SelectiveVMCloner(user_session=getattr(args, "user", False))
946
946
 
947
947
  # Check prerequisites
948
- checks = cloner.check_prerequisites()
949
- if not all(checks.values()):
948
+ checks = cloner.check_prerequisites(config)
949
+ required_keys = [
950
+ "libvirt_connected",
951
+ "kvm_available",
952
+ "default_network",
953
+ "images_dir_writable",
954
+ "genisoimage_installed",
955
+ "qemu_img_installed",
956
+ ]
957
+ if getattr(config, "gui", False):
958
+ required_keys.append("virt_viewer_installed")
959
+
960
+ required_checks = {
961
+ k: checks.get(k)
962
+ for k in required_keys
963
+ if isinstance(checks.get(k), bool)
964
+ }
965
+
966
+ if required_checks and not all(required_checks.values()):
950
967
  console.print("[yellow]⚠️ Prerequisites check:[/]")
951
- for check, passed in checks.items():
968
+ for check, passed in required_checks.items():
952
969
  icon = "✅" if passed else "❌"
953
970
  console.print(f" {icon} {check}")
954
971
 
@@ -1884,14 +1901,24 @@ def cmd_test(args):
1884
1901
  # Give QEMU Guest Agent some time to come up (common during early boot)
1885
1902
  qga_ready = _qga_ping(vm_name, conn_uri)
1886
1903
  if not qga_ready:
1887
- for _ in range(12): # ~60s
1904
+ console.print("[yellow]⏳ Waiting for QEMU Guest Agent (up to 60s)...[/]")
1905
+ qga_wait_start = time.time()
1906
+ for attempt in range(12): # ~60s
1888
1907
  time.sleep(5)
1889
1908
  qga_ready = _qga_ping(vm_name, conn_uri)
1909
+ elapsed = int(time.time() - qga_wait_start)
1890
1910
  if qga_ready:
1911
+ console.print(f"[green]✅ QEMU Guest Agent connected after {elapsed}s[/]")
1891
1912
  break
1913
+ if attempt % 2 == 1:
1914
+ console.print(f"[dim] ...still waiting ({elapsed}s elapsed)[/]")
1915
+
1916
+ if not qga_ready:
1917
+ console.print("[yellow]⚠️ QEMU Guest Agent still not connected[/]")
1892
1918
 
1893
1919
  # Check cloud-init status immediately if QGA is ready
1894
1920
  if qga_ready:
1921
+ console.print("[dim] Checking cloud-init status via QGA...[/]")
1895
1922
  status = _qga_exec(
1896
1923
  vm_name, conn_uri, "cloud-init status 2>/dev/null || true", timeout=15
1897
1924
  )
@@ -2068,6 +2095,8 @@ def cmd_test(args):
2068
2095
 
2069
2096
  # Run full validation if requested
2070
2097
  if validate_all and state == "running":
2098
+ console.print("[bold cyan]🔎 Starting deep validation (--validate)[/]")
2099
+ console.print("[dim]This can take a few minutes on first boot (waiting for QGA/cloud-init, checking packages/services).[/]")
2071
2100
  validator = VMValidator(
2072
2101
  config,
2073
2102
  vm_name,
@@ -2612,10 +2641,23 @@ def create_vm_from_config(config, start=False, user_session=False, replace=False
2612
2641
  cloner = SelectiveVMCloner(user_session=user_session)
2613
2642
 
2614
2643
  # Check prerequisites
2615
- checks = cloner.check_prerequisites()
2616
- if not all(checks.values()):
2644
+ checks = cloner.check_prerequisites(vm_config)
2645
+ required_keys = [
2646
+ "libvirt_connected",
2647
+ "kvm_available",
2648
+ "default_network",
2649
+ "images_dir_writable",
2650
+ "genisoimage_installed",
2651
+ "qemu_img_installed",
2652
+ ]
2653
+ if getattr(vm_config, "gui", False):
2654
+ required_keys.append("virt_viewer_installed")
2655
+
2656
+ required_checks = {k: checks.get(k) for k in required_keys if isinstance(checks.get(k), bool)}
2657
+
2658
+ if required_checks and not all(required_checks.values()):
2617
2659
  console.print("[yellow]⚠️ Prerequisites check:[/]")
2618
- for check, passed in checks.items():
2660
+ for check, passed in required_checks.items():
2619
2661
  icon = "✅" if passed else "❌"
2620
2662
  console.print(f" {icon} {check}")
2621
2663
 
@@ -15,6 +15,7 @@ import tempfile
15
15
  import time
16
16
  import urllib.request
17
17
  import uuid
18
+ import zlib
18
19
  import xml.etree.ElementTree as ET
19
20
  from dataclasses import dataclass, field
20
21
  from datetime import datetime
@@ -299,20 +300,29 @@ class SelectiveVMCloner:
299
300
  return mode
300
301
  return "default"
301
302
 
302
- def check_prerequisites(self) -> dict:
303
+ def check_prerequisites(self, config: Optional[VMConfig] = None) -> dict:
303
304
  """Check system prerequisites for VM creation."""
304
305
  images_dir = self.get_images_dir()
305
306
 
307
+ resolved_network_mode: Optional[str] = None
308
+ if config is not None:
309
+ try:
310
+ resolved_network_mode = self.resolve_network_mode(config)
311
+ except Exception:
312
+ resolved_network_mode = None
313
+
306
314
  checks = {
307
315
  "libvirt_connected": False,
308
316
  "kvm_available": False,
309
317
  "default_network": False,
318
+ "default_network_required": True,
310
319
  "images_dir_writable": False,
311
320
  "images_dir": str(images_dir),
312
321
  "session_type": "user" if self.user_session else "system",
313
322
  "genisoimage_installed": False,
314
323
  "virt_viewer_installed": False,
315
324
  "qemu_img_installed": False,
325
+ "passt_installed": shutil.which("passt") is not None,
316
326
  }
317
327
 
318
328
  # Check for genisoimage
@@ -340,8 +350,14 @@ class SelectiveVMCloner:
340
350
 
341
351
  # Check default network
342
352
  default_net_state = self._default_network_state()
343
- checks["default_network"] = default_net_state == "active"
344
- if default_net_state in {"inactive", "missing", "unknown"}:
353
+ checks["default_network_required"] = (resolved_network_mode or "default") != "user"
354
+ if checks["default_network_required"]:
355
+ checks["default_network"] = default_net_state == "active"
356
+ else:
357
+ # For user-mode networking (slirp/passt), libvirt's default network is not required.
358
+ checks["default_network"] = True
359
+
360
+ if checks["default_network_required"] and default_net_state in {"inactive", "missing", "unknown"}:
345
361
  checks["network_error"] = (
346
362
  "Default network not found or inactive.\n"
347
363
  " For user session, CloneBox can use user-mode networking (slirp) automatically.\n"
@@ -351,6 +367,9 @@ class SelectiveVMCloner:
351
367
  " Or use system session: clonebox clone . (without --user)\n"
352
368
  )
353
369
 
370
+ if resolved_network_mode is not None:
371
+ checks["network_mode"] = resolved_network_mode
372
+
354
373
  # Check images directory
355
374
  if images_dir.exists():
356
375
  checks["images_dir_writable"] = os.access(images_dir, os.W_OK)
@@ -1358,6 +1377,13 @@ fi
1358
1377
  if network_mode == "user":
1359
1378
  iface = ET.SubElement(devices, "interface", type="user")
1360
1379
  ET.SubElement(iface, "model", type="virtio")
1380
+
1381
+ if shutil.which("passt"):
1382
+ ET.SubElement(iface, "backend", type="passt")
1383
+
1384
+ ssh_port = 22000 + (zlib.crc32(config.name.encode("utf-8")) % 1000)
1385
+ pf = ET.SubElement(iface, "portForward", proto="tcp", address="127.0.0.1")
1386
+ ET.SubElement(pf, "range", start=str(ssh_port), to="22")
1361
1387
  else:
1362
1388
  iface = ET.SubElement(devices, "interface", type="network")
1363
1389
  ET.SubElement(iface, "source", network="default")
@@ -1398,6 +1424,7 @@ fi
1398
1424
 
1399
1425
  # Channel for guest agent
1400
1426
  channel = ET.SubElement(devices, "channel", type="unix")
1427
+ # For both user and system sessions, let libvirt handle the path
1401
1428
  ET.SubElement(channel, "source", mode="bind")
1402
1429
  ET.SubElement(channel, "target", type="virtio", name="org.qemu.guest_agent.0")
1403
1430
 
@@ -1563,7 +1590,7 @@ fi
1563
1590
 
1564
1591
  # User-data
1565
1592
  # Add desktop environment if GUI is enabled
1566
- base_packages = ["qemu-guest-agent", "cloud-guest-utils"]
1593
+ base_packages = ["qemu-guest-agent", "cloud-guest-utils", "openssh-server"]
1567
1594
  if config.gui:
1568
1595
  base_packages.extend(
1569
1596
  [
@@ -1619,9 +1646,11 @@ fi
1619
1646
 
1620
1647
  # Phase 3: Core services
1621
1648
  runcmd_lines.append(" - echo '[3/10] 🔧 Enabling core services...'")
1622
- runcmd_lines.append(" - echo ' → [1/2] Enabling qemu-guest-agent'")
1649
+ runcmd_lines.append(" - echo ' → [1/3] Enabling qemu-guest-agent'")
1623
1650
  runcmd_lines.append(" - systemctl enable --now qemu-guest-agent || echo ' → ❌ Failed to enable qemu-guest-agent'")
1624
- runcmd_lines.append(" - echo ' → [2/2] Enabling snapd'")
1651
+ runcmd_lines.append(" - echo ' → [2/3] Enabling SSH server'")
1652
+ runcmd_lines.append(" - systemctl enable --now ssh || systemctl enable --now sshd || echo ' → ❌ Failed to enable ssh'")
1653
+ runcmd_lines.append(" - echo ' → [3/3] Enabling snapd'")
1625
1654
  runcmd_lines.append(" - systemctl enable --now snapd || echo ' → ❌ Failed to enable snapd'")
1626
1655
  runcmd_lines.append(" - echo ' → Waiting for snap system seed...'")
1627
1656
  runcmd_lines.append(" - timeout 300 snap wait system seed.loaded || true")
@@ -6,6 +6,7 @@ import subprocess
6
6
  import json
7
7
  import base64
8
8
  import time
9
+ import zlib
9
10
  from typing import Dict, List, Tuple, Optional
10
11
  from pathlib import Path
11
12
  from rich.console import Console
@@ -49,9 +50,66 @@ class VMValidator:
49
50
  }
50
51
 
51
52
  self._setup_in_progress_cache: Optional[bool] = None
53
+ self._exec_transport: str = "qga" # qga|ssh
54
+
55
+ def _get_ssh_key_path(self) -> Optional[Path]:
56
+ """Return path to the SSH key generated for this VM (if present)."""
57
+ try:
58
+ if self.conn_uri.endswith("/session"):
59
+ images_dir = Path.home() / ".local/share/libvirt/images"
60
+ else:
61
+ images_dir = Path("/var/lib/libvirt/images")
62
+ key_path = images_dir / self.vm_name / "ssh_key"
63
+ return key_path if key_path.exists() else None
64
+ except Exception:
65
+ return None
66
+
67
+ def _get_ssh_port(self) -> int:
68
+ """Deterministic host-side SSH port for passt port forwarding."""
69
+ return 22000 + (zlib.crc32(self.vm_name.encode("utf-8")) % 1000)
70
+
71
+ def _ssh_exec(self, command: str, timeout: int = 10) -> Optional[str]:
72
+ key_path = self._get_ssh_key_path()
73
+ if key_path is None:
74
+ return None
75
+
76
+ ssh_port = self._get_ssh_port()
77
+ user = (self.config.get("vm") or {}).get("username") or "ubuntu"
78
+
79
+ try:
80
+ result = subprocess.run(
81
+ [
82
+ "ssh",
83
+ "-i",
84
+ str(key_path),
85
+ "-p",
86
+ str(ssh_port),
87
+ "-o",
88
+ "StrictHostKeyChecking=no",
89
+ "-o",
90
+ "UserKnownHostsFile=/dev/null",
91
+ "-o",
92
+ "ConnectTimeout=5",
93
+ "-o",
94
+ "BatchMode=yes",
95
+ f"{user}@127.0.0.1",
96
+ command,
97
+ ],
98
+ capture_output=True,
99
+ text=True,
100
+ timeout=timeout,
101
+ )
102
+ if result.returncode != 0:
103
+ return None
104
+ return (result.stdout or "").strip()
105
+ except Exception:
106
+ return None
52
107
 
53
108
  def _exec_in_vm(self, command: str, timeout: int = 10) -> Optional[str]:
54
- """Execute command in VM using QEMU guest agent."""
109
+ """Execute command in VM using QEMU guest agent, with SSH fallback."""
110
+ if self._exec_transport == "ssh":
111
+ return self._ssh_exec(command, timeout=timeout)
112
+
55
113
  try:
56
114
  # Execute command
57
115
  result = subprocess.run(
@@ -266,12 +324,17 @@ class VMValidator:
266
324
  self.console.print("[dim]No APT packages configured[/]")
267
325
  return self.results["packages"]
268
326
 
327
+ total_pkgs = len(packages)
328
+ self.console.print(f"[dim]Checking {total_pkgs} packages via QGA...[/]")
329
+
269
330
  pkg_table = Table(title="Package Validation", border_style="cyan")
270
331
  pkg_table.add_column("Package", style="bold")
271
332
  pkg_table.add_column("Status", justify="center")
272
333
  pkg_table.add_column("Version", style="dim")
273
334
 
274
- for package in packages:
335
+ for idx, package in enumerate(packages, 1):
336
+ if idx == 1 or idx % 25 == 0 or idx == total_pkgs:
337
+ self.console.print(f"[dim] ...packages progress: {idx}/{total_pkgs}[/]")
275
338
  self.results["packages"]["total"] += 1
276
339
 
277
340
  # Check if installed
@@ -315,12 +378,17 @@ class VMValidator:
315
378
  self.console.print("[dim]No snap packages configured[/]")
316
379
  return self.results["snap_packages"]
317
380
 
381
+ total_snaps = len(snap_packages)
382
+ self.console.print(f"[dim]Checking {total_snaps} snap packages via QGA...[/]")
383
+
318
384
  snap_table = Table(title="Snap Package Validation", border_style="cyan")
319
385
  snap_table.add_column("Package", style="bold")
320
386
  snap_table.add_column("Status", justify="center")
321
387
  snap_table.add_column("Version", style="dim")
322
388
 
323
- for package in snap_packages:
389
+ for idx, package in enumerate(snap_packages, 1):
390
+ if idx == 1 or idx % 25 == 0 or idx == total_snaps:
391
+ self.console.print(f"[dim] ...snap progress: {idx}/{total_snaps}[/]")
324
392
  self.results["snap_packages"]["total"] += 1
325
393
 
326
394
  # Check if installed
@@ -395,6 +463,9 @@ class VMValidator:
395
463
  self.console.print("[dim]No services configured[/]")
396
464
  return self.results["services"]
397
465
 
466
+ total_svcs = len(services)
467
+ self.console.print(f"[dim]Checking {total_svcs} services via QGA...[/]")
468
+
398
469
  if "skipped" not in self.results["services"]:
399
470
  self.results["services"]["skipped"] = 0
400
471
 
@@ -405,7 +476,9 @@ class VMValidator:
405
476
  svc_table.add_column("PID", justify="right", style="dim")
406
477
  svc_table.add_column("Note", style="dim")
407
478
 
408
- for service in services:
479
+ for idx, service in enumerate(services, 1):
480
+ if idx == 1 or idx % 25 == 0 or idx == total_svcs:
481
+ self.console.print(f"[dim] ...services progress: {idx}/{total_svcs}[/]")
409
482
  if service in self.VM_EXCLUDED_SERVICES:
410
483
  svc_table.add_row(service, "[dim]—[/]", "[dim]—[/]", "[dim]—[/]", "host-only")
411
484
  self.results["services"]["skipped"] += 1
@@ -1144,23 +1217,35 @@ class VMValidator:
1144
1217
  if not self._check_qga_ready():
1145
1218
  wait_deadline = time.time() + 180
1146
1219
  self.console.print("[yellow]⏳ Waiting for QEMU Guest Agent (up to 180s)...[/]")
1220
+ last_log = 0
1147
1221
  while time.time() < wait_deadline:
1148
1222
  time.sleep(5)
1149
1223
  if self._check_qga_ready():
1150
1224
  break
1225
+ elapsed = int(180 - (wait_deadline - time.time()))
1226
+ if elapsed - last_log >= 15:
1227
+ self.console.print(f"[dim] ...still waiting for QGA ({elapsed}s elapsed)[/]")
1228
+ last_log = elapsed
1151
1229
 
1152
1230
  if not self._check_qga_ready():
1153
- self.console.print("[red]❌ QEMU Guest Agent not responding[/]")
1154
- self.console.print("\n[bold]🔧 Troubleshooting QGA:[/]")
1155
- self.console.print(" 1. The VM might still be booting. Wait 30-60 seconds.")
1156
- self.console.print(" 2. Ensure the agent is installed and running inside the VM:")
1157
- self.console.print(" [dim]virsh console " + self.vm_name + "[/]")
1158
- self.console.print(" [dim]sudo systemctl status qemu-guest-agent[/]")
1159
- self.console.print(" 3. If newly created, cloud-init might still be running.")
1160
- self.console.print(" 4. Check VM logs: [dim]clonebox logs " + self.vm_name + "[/]")
1161
- self.console.print(f"\n[yellow]⚠️ Skipping deep validation as it requires a working Guest Agent.[/]")
1162
- self.results["overall"] = "qga_not_ready"
1163
- return self.results
1231
+ # SSH fallback (primarily for --user networking, where passt can forward ports)
1232
+ self.console.print("[yellow]⚠️ QEMU Guest Agent not responding - trying SSH fallback...[/]")
1233
+ self._exec_transport = "ssh"
1234
+ smoke = self._ssh_exec("echo ok", timeout=10)
1235
+ if smoke != "ok":
1236
+ self.console.print("[red] SSH fallback failed[/]")
1237
+ self.console.print("\n[bold]🔧 Troubleshooting QGA:[/]")
1238
+ self.console.print(" 1. The VM might still be booting. Wait 30-60 seconds.")
1239
+ self.console.print(" 2. Ensure the agent is installed and running inside the VM:")
1240
+ self.console.print(" [dim]virsh console " + self.vm_name + "[/]")
1241
+ self.console.print(" [dim]sudo systemctl status qemu-guest-agent[/]")
1242
+ self.console.print(" 3. If newly created, cloud-init might still be running.")
1243
+ self.console.print(" 4. Check VM logs: [dim]clonebox logs " + self.vm_name + "[/]")
1244
+ self.console.print(f"\n[yellow]⚠️ Skipping deep validation as it requires a working Guest Agent or SSH access.[/]")
1245
+ self.results["overall"] = "qga_not_ready"
1246
+ return self.results
1247
+
1248
+ self.console.print("[green]✅ SSH fallback connected (executing validations over SSH)[/]")
1164
1249
 
1165
1250
  ci_status = self._exec_in_vm("cloud-init status --long 2>/dev/null || cloud-init status 2>/dev/null || true", timeout=20)
1166
1251
  if ci_status:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.19
3
+ Version: 1.1.21
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