clonebox 1.1.20__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.20/src/clonebox.egg-info → clonebox-1.1.21}/PKG-INFO +1 -1
  2. {clonebox-1.1.20 → clonebox-1.1.21}/pyproject.toml +1 -1
  3. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/cli.py +36 -6
  4. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/cloner.py +35 -6
  5. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/validator.py +77 -12
  6. {clonebox-1.1.20 → clonebox-1.1.21/src/clonebox.egg-info}/PKG-INFO +1 -1
  7. {clonebox-1.1.20 → clonebox-1.1.21}/LICENSE +0 -0
  8. {clonebox-1.1.20 → clonebox-1.1.21}/README.md +0 -0
  9. {clonebox-1.1.20 → clonebox-1.1.21}/setup.cfg +0 -0
  10. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/__init__.py +0 -0
  11. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/__main__.py +0 -0
  12. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/audit.py +0 -0
  13. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/backends/libvirt_backend.py +0 -0
  14. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/backends/qemu_disk.py +0 -0
  15. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/backends/subprocess_runner.py +0 -0
  16. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/container.py +0 -0
  17. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/dashboard.py +0 -0
  18. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/detector.py +0 -0
  19. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/di.py +0 -0
  20. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/exporter.py +0 -0
  21. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/health/__init__.py +0 -0
  22. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/health/manager.py +0 -0
  23. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/health/models.py +0 -0
  24. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/health/probes.py +0 -0
  25. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/importer.py +0 -0
  26. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/interfaces/disk.py +0 -0
  27. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/interfaces/hypervisor.py +0 -0
  28. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/interfaces/network.py +0 -0
  29. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/interfaces/process.py +0 -0
  30. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/logging.py +0 -0
  31. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/models.py +0 -0
  32. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/monitor.py +0 -0
  33. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/orchestrator.py +0 -0
  34. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/p2p.py +0 -0
  35. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/plugins/__init__.py +0 -0
  36. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/plugins/base.py +0 -0
  37. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/plugins/manager.py +0 -0
  38. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/policies/__init__.py +0 -0
  39. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/policies/engine.py +0 -0
  40. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/policies/models.py +0 -0
  41. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/policies/validators.py +0 -0
  42. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/profiles.py +0 -0
  43. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/remote.py +0 -0
  44. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/resource_monitor.py +0 -0
  45. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/resources.py +0 -0
  46. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/rollback.py +0 -0
  47. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/secrets.py +0 -0
  48. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/snapshots/__init__.py +0 -0
  49. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/snapshots/manager.py +0 -0
  50. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/snapshots/models.py +0 -0
  51. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
  52. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/templates/profiles/web-stack.yaml +0 -0
  53. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/SOURCES.txt +0 -0
  54. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/dependency_links.txt +0 -0
  55. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/entry_points.txt +0 -0
  56. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/requires.txt +0 -0
  57. {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/top_level.txt +0 -0
  58. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_audit.py +0 -0
  59. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_cli.py +0 -0
  60. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_cli_new_commands.py +0 -0
  61. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_cloner.py +0 -0
  62. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_cloner_simple.py +0 -0
  63. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_container.py +0 -0
  64. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_coverage_additional.py +0 -0
  65. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_coverage_boost_final.py +0 -0
  66. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_dashboard_coverage.py +0 -0
  67. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_detector.py +0 -0
  68. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_models.py +0 -0
  69. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_network.py +0 -0
  70. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_orchestrator.py +0 -0
  71. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_plugins.py +0 -0
  72. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_profiles.py +0 -0
  73. {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_remote.py +0 -0
  74. {clonebox-1.1.20 → 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.20
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.20"
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
 
@@ -2624,10 +2641,23 @@ def create_vm_from_config(config, start=False, user_session=False, replace=False
2624
2641
  cloner = SelectiveVMCloner(user_session=user_session)
2625
2642
 
2626
2643
  # Check prerequisites
2627
- checks = cloner.check_prerequisites()
2628
- 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()):
2629
2659
  console.print("[yellow]⚠️ Prerequisites check:[/]")
2630
- for check, passed in checks.items():
2660
+ for check, passed in required_checks.items():
2631
2661
  icon = "✅" if passed else "❌"
2632
2662
  console.print(f" {icon} {check}")
2633
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(
@@ -1170,17 +1228,24 @@ class VMValidator:
1170
1228
  last_log = elapsed
1171
1229
 
1172
1230
  if not self._check_qga_ready():
1173
- self.console.print("[red]❌ QEMU Guest Agent not responding[/]")
1174
- self.console.print("\n[bold]🔧 Troubleshooting QGA:[/]")
1175
- self.console.print(" 1. The VM might still be booting. Wait 30-60 seconds.")
1176
- self.console.print(" 2. Ensure the agent is installed and running inside the VM:")
1177
- self.console.print(" [dim]virsh console " + self.vm_name + "[/]")
1178
- self.console.print(" [dim]sudo systemctl status qemu-guest-agent[/]")
1179
- self.console.print(" 3. If newly created, cloud-init might still be running.")
1180
- self.console.print(" 4. Check VM logs: [dim]clonebox logs " + self.vm_name + "[/]")
1181
- self.console.print(f"\n[yellow]⚠️ Skipping deep validation as it requires a working Guest Agent.[/]")
1182
- self.results["overall"] = "qga_not_ready"
1183
- 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)[/]")
1184
1249
 
1185
1250
  ci_status = self._exec_in_vm("cloud-init status --long 2>/dev/null || cloud-init status 2>/dev/null || true", timeout=20)
1186
1251
  if ci_status:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.20
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