clonebox 1.1.7__py3-none-any.whl → 1.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
clonebox/cli.py CHANGED
@@ -93,22 +93,30 @@ def _resolve_vm_name_and_config_file(name: Optional[str]) -> Tuple[str, Optional
93
93
  def _qga_ping(vm_name: str, conn_uri: str) -> bool:
94
94
  import subprocess
95
95
  import json
96
+ import time
96
97
 
97
98
  try:
98
- result = subprocess.run(
99
- [
100
- "virsh",
101
- "--connect",
102
- conn_uri,
103
- "qemu-agent-command",
104
- vm_name,
105
- json.dumps({"execute": "guest-ping"}),
106
- ],
107
- capture_output=True,
108
- text=True,
109
- timeout=5,
110
- )
111
- return result.returncode == 0
99
+ for _ in range(5):
100
+ try:
101
+ result = subprocess.run(
102
+ [
103
+ "virsh",
104
+ "--connect",
105
+ conn_uri,
106
+ "qemu-agent-command",
107
+ vm_name,
108
+ json.dumps({"execute": "guest-ping"}),
109
+ ],
110
+ capture_output=True,
111
+ text=True,
112
+ timeout=5,
113
+ )
114
+ if result.returncode == 0:
115
+ return True
116
+ except subprocess.TimeoutExpired:
117
+ pass
118
+ time.sleep(1)
119
+ return False
112
120
  except Exception:
113
121
  return False
114
122
 
@@ -1656,6 +1664,7 @@ def cmd_test(args):
1656
1664
  """Test VM configuration and health."""
1657
1665
  import subprocess
1658
1666
  import json
1667
+ import time
1659
1668
  from clonebox.validator import VMValidator
1660
1669
 
1661
1670
  name = args.name
@@ -1735,6 +1744,15 @@ def cmd_test(args):
1735
1744
  if state == "running":
1736
1745
  console.print("[green]✅ VM is running[/]")
1737
1746
 
1747
+ # Give QEMU Guest Agent some time to come up (common during early boot)
1748
+ qga_ready = _qga_ping(vm_name, conn_uri)
1749
+ if not qga_ready:
1750
+ for _ in range(12): # ~60s
1751
+ time.sleep(5)
1752
+ qga_ready = _qga_ping(vm_name, conn_uri)
1753
+ if qga_ready:
1754
+ break
1755
+
1738
1756
  # Test network if running
1739
1757
  console.print("\n Checking network...")
1740
1758
  try:
@@ -1753,11 +1771,7 @@ def cmd_test(args):
1753
1771
  else:
1754
1772
  console.print("[yellow]⚠️ No IP address detected via virsh domifaddr[/]")
1755
1773
  # Fallback: try to get IP via QEMU Guest Agent (useful for slirp/user networking)
1756
- try:
1757
- from .cli import _qga_ping, _qga_exec
1758
- except ImportError:
1759
- from clonebox.cli import _qga_ping, _qga_exec
1760
- if _qga_ping(vm_name, conn_uri):
1774
+ if qga_ready:
1761
1775
  try:
1762
1776
  ip_out = _qga_exec(
1763
1777
  vm_name,
@@ -1789,61 +1803,22 @@ def cmd_test(args):
1789
1803
  if not quick and state == "running":
1790
1804
  console.print("[bold]3. Cloud-init Status[/]")
1791
1805
  try:
1792
- # Try to get cloud-init status via QEMU guest agent
1793
- result = subprocess.run(
1794
- [
1795
- "virsh",
1796
- "--connect",
1797
- conn_uri,
1798
- "qemu-agent-command",
1799
- vm_name,
1800
- '{"execute":"guest-exec","arguments":{"path":"cloud-init","arg":["status"],"capture-output":true}}',
1801
- ],
1802
- capture_output=True,
1803
- text=True,
1804
- timeout=15,
1805
- )
1806
- if result.returncode == 0:
1807
- try:
1808
- response = json.loads(result.stdout)
1809
- if "return" in response:
1810
- pid = response["return"]["pid"]
1811
- # Get output
1812
- result2 = subprocess.run(
1813
- [
1814
- "virsh",
1815
- "--connect",
1816
- conn_uri,
1817
- "qemu-agent-command",
1818
- vm_name,
1819
- f'{{"execute":"guest-exec-status","arguments":{"pid":{pid}}}}',
1820
- ],
1821
- capture_output=True,
1822
- text=True,
1823
- timeout=15,
1824
- )
1825
- if result2.returncode == 0:
1826
- resp2 = json.loads(result2.stdout)
1827
- if "return" in resp2 and resp2["return"]["exited"]:
1828
- output = resp2["return"]["out-data"]
1829
- if output:
1830
- import base64
1831
-
1832
- status = base64.b64decode(output).decode()
1833
- if "done" in status.lower():
1834
- console.print("[green]✅ Cloud-init completed[/]")
1835
- elif "running" in status.lower():
1836
- console.print("[yellow]⚠️ Cloud-init still running[/]")
1837
- else:
1838
- console.print(
1839
- f"[yellow]⚠️ Cloud-init status: {status.strip()}[/]"
1840
- )
1841
- except:
1842
- pass
1843
- except:
1844
- console.print(
1845
- "[yellow]⚠️ Could not check cloud-init (QEMU agent may not be running)[/]"
1846
- )
1806
+ if not qga_ready:
1807
+ console.print("[yellow]⚠️ Cloud-init status unknown (QEMU Guest Agent not connected)[/]")
1808
+ else:
1809
+ status = _qga_exec(vm_name, conn_uri, "cloud-init status 2>/dev/null || true", timeout=15)
1810
+ if status is None:
1811
+ console.print("[yellow]⚠️ Could not check cloud-init (QGA command failed)[/]")
1812
+ elif "done" in status.lower():
1813
+ console.print("[green]✅ Cloud-init completed[/]")
1814
+ elif "running" in status.lower():
1815
+ console.print("[yellow]⚠️ Cloud-init still running[/]")
1816
+ elif status.strip():
1817
+ console.print(f"[yellow]⚠️ Cloud-init status: {status.strip()}[/]")
1818
+ else:
1819
+ console.print("[yellow]⚠️ Cloud-init status: unknown[/]")
1820
+ except Exception:
1821
+ console.print("[yellow]⚠️ Could not check cloud-init (QEMU agent may not be running)[/]")
1847
1822
 
1848
1823
  console.print()
1849
1824
 
@@ -1878,25 +1853,31 @@ def cmd_test(args):
1878
1853
  if not quick and state == "running":
1879
1854
  console.print("[bold]5. Health Check[/]")
1880
1855
  try:
1881
- result = subprocess.run(
1882
- [
1883
- "virsh",
1884
- "--connect",
1885
- conn_uri,
1886
- "qemu-agent-command",
1887
- vm_name,
1888
- '{"execute":"guest-exec","arguments":{"path":"/usr/local/bin/clonebox-health","capture-output":true}}',
1889
- ],
1890
- capture_output=True,
1891
- text=True,
1892
- timeout=60,
1893
- )
1894
- if result.returncode == 0:
1895
- console.print("[green]✅ Health check triggered[/]")
1896
- console.print(" View results in VM: cat /var/log/clonebox-health.log")
1856
+ if not qga_ready:
1857
+ console.print("[yellow]⚠️ QEMU Guest Agent not connected - cannot run health check[/]")
1897
1858
  else:
1898
- console.print("[yellow]⚠️ Health check script not found[/]")
1899
- console.print(" VM may not have been created with health checks")
1859
+ exists = _qga_exec(
1860
+ vm_name,
1861
+ conn_uri,
1862
+ "test -x /usr/local/bin/clonebox-health && echo yes || echo no",
1863
+ timeout=10,
1864
+ )
1865
+ if exists and exists.strip() == "yes":
1866
+ out = _qga_exec(
1867
+ vm_name,
1868
+ conn_uri,
1869
+ "/usr/local/bin/clonebox-health >/dev/null 2>&1 && echo yes || echo no",
1870
+ timeout=60,
1871
+ )
1872
+ if out and out.strip() == "yes":
1873
+ console.print("[green]✅ Health check ran successfully[/]")
1874
+ console.print(" View results in VM: cat /var/log/clonebox-health.log")
1875
+ else:
1876
+ console.print("[yellow]⚠️ Health check did not report success[/]")
1877
+ console.print(" View logs in VM: cat /var/log/clonebox-health.log")
1878
+ else:
1879
+ console.print("[yellow]⚠️ Health check script not found[/]")
1880
+ console.print(" This is expected until cloud-init completes")
1900
1881
  except Exception as e:
1901
1882
  console.print(f"[yellow]⚠️ Could not run health check: {e}[/]")
1902
1883
 
@@ -2745,9 +2726,22 @@ def cmd_exec(args) -> None:
2745
2726
  vm_name, config_file = _resolve_vm_name_and_config_file(args.name)
2746
2727
  conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2747
2728
  command = args.command
2729
+ if isinstance(command, list):
2730
+ command = " ".join(command).strip()
2731
+ if not command:
2732
+ console.print("[red]❌ No command specified[/]")
2733
+ return
2748
2734
  timeout = getattr(args, "timeout", 30)
2749
2735
 
2750
- if not _qga_ping(vm_name, conn_uri):
2736
+ qga_ready = _qga_ping(vm_name, conn_uri)
2737
+ if not qga_ready:
2738
+ for _ in range(12): # ~60s
2739
+ time.sleep(5)
2740
+ qga_ready = _qga_ping(vm_name, conn_uri)
2741
+ if qga_ready:
2742
+ break
2743
+
2744
+ if not qga_ready:
2751
2745
  console.print(f"[red]❌ Cannot connect to VM '{vm_name}' via QEMU Guest Agent[/]")
2752
2746
  console.print("[dim]Make sure the VM is running and qemu-guest-agent is installed.[/]")
2753
2747
  return
@@ -3488,7 +3482,9 @@ def main():
3488
3482
  exec_parser.add_argument(
3489
3483
  "name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
3490
3484
  )
3491
- exec_parser.add_argument("command", help="Command to execute in VM")
3485
+ exec_parser.add_argument(
3486
+ "command", nargs=argparse.REMAINDER, help="Command to execute in VM"
3487
+ )
3492
3488
  exec_parser.add_argument(
3493
3489
  "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3494
3490
  )
clonebox/cloner.py CHANGED
@@ -433,7 +433,13 @@ class SelectiveVMCloner:
433
433
 
434
434
  # Create cloud-init ISO if packages/services specified
435
435
  cloudinit_iso = None
436
- if config.packages or config.services:
436
+ if (
437
+ config.packages
438
+ or config.services
439
+ or config.snap_packages
440
+ or config.post_commands
441
+ or config.gui
442
+ ):
437
443
  cloudinit_iso = ctx.add_file(self._create_cloudinit_iso(vm_dir, config))
438
444
  log.info(f"Created cloud-init ISO with {len(config.packages)} packages")
439
445
 
@@ -623,8 +629,11 @@ class SelectiveVMCloner:
623
629
  ET.SubElement(devices, "input", type="tablet", bus="usb")
624
630
  ET.SubElement(devices, "input", type="keyboard", bus="usb")
625
631
 
632
+ ET.SubElement(devices, "controller", type="virtio-serial", index="0")
633
+
626
634
  # Channel for guest agent
627
635
  channel = ET.SubElement(devices, "channel", type="unix")
636
+ ET.SubElement(channel, "source", mode="bind")
628
637
  ET.SubElement(channel, "target", type="virtio", name="org.qemu.guest_agent.0")
629
638
 
630
639
  # Memory balloon
@@ -1247,15 +1256,30 @@ fi
1247
1256
  cloudinit_dir.mkdir(exist_ok=True)
1248
1257
 
1249
1258
  # Meta-data
1250
- meta_data = f"instance-id: {config.name}\nlocal-hostname: {config.name}\n"
1259
+ instance_id = f"{config.name}-{uuid.uuid4().hex}"
1260
+ meta_data = f"instance-id: {instance_id}\nlocal-hostname: {config.name}\n"
1251
1261
  (cloudinit_dir / "meta-data").write_text(meta_data)
1252
1262
 
1253
1263
  # Generate mount commands and fstab entries for 9p filesystems
1254
1264
  mount_commands = []
1255
1265
  fstab_entries = []
1256
1266
  all_paths = dict(config.paths) if config.paths else {}
1267
+ pre_chown_dirs: set[str] = set()
1257
1268
  for idx, (host_path, guest_path) in enumerate(all_paths.items()):
1258
1269
  if Path(host_path).exists():
1270
+ if str(guest_path).startswith("/home/ubuntu/snap/"):
1271
+ guest_parts = Path(guest_path).parts
1272
+ if len(guest_parts) > 4:
1273
+ snap_name = guest_parts[4]
1274
+ for d in (
1275
+ "/home/ubuntu",
1276
+ "/home/ubuntu/snap",
1277
+ f"/home/ubuntu/snap/{snap_name}",
1278
+ ):
1279
+ if d not in pre_chown_dirs:
1280
+ pre_chown_dirs.add(d)
1281
+ mount_commands.append(f" - mkdir -p {d}")
1282
+ mount_commands.append(f" - chown 1000:1000 {d}")
1259
1283
  tag = f"mount{idx}"
1260
1284
  # Use uid=1000,gid=1000 to give ubuntu user access to mounts
1261
1285
  # mmap allows proper file mapping
@@ -1306,6 +1330,9 @@ fi
1306
1330
  for cmd in mount_commands:
1307
1331
  runcmd_lines.append(cmd)
1308
1332
 
1333
+ runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu || true")
1334
+ runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/snap || true")
1335
+
1309
1336
  # Install snap packages
1310
1337
  if config.snap_packages:
1311
1338
  runcmd_lines.append(" - echo 'Installing snap packages...'")
@@ -2146,7 +2173,7 @@ if __name__ == "__main__":
2146
2173
  " - cp -r /var/log/clonebox*.log /mnt/logs/var/log/ 2>/dev/null || true",
2147
2174
  " - cp -r /tmp/*-error.log /mnt/logs/tmp/ 2>/dev/null || true",
2148
2175
  " - echo 'Logs disk mounted at /mnt/logs - accessible from host as /var/lib/libvirt/images/clonebox-logs.qcow2'",
2149
- " - echo 'To view logs on host: sudo mount -o loop /var/lib/libvirt/images/clonebox-logs.qcow2 /mnt/clonebox-logs'",
2176
+ " - \"echo 'To view logs on host: sudo mount -o loop /var/lib/libvirt/images/clonebox-logs.qcow2 /mnt/clonebox-logs'\"",
2150
2177
  ]
2151
2178
  )
2152
2179
 
@@ -2158,9 +2185,7 @@ if __name__ == "__main__":
2158
2185
  runcmd_yaml = "\n".join(runcmd_lines) if runcmd_lines else ""
2159
2186
 
2160
2187
  # Build bootcmd combining mount commands and extra security bootcmds
2161
- bootcmd_lines = list(mount_commands) if mount_commands else []
2162
- if bootcmd_extra:
2163
- bootcmd_lines.extend(bootcmd_extra)
2188
+ bootcmd_lines = list(bootcmd_extra) if bootcmd_extra else []
2164
2189
 
2165
2190
  bootcmd_block = ""
2166
2191
  if bootcmd_lines:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.7
3
+ Version: 1.1.9
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
@@ -1,7 +1,7 @@
1
1
  clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
2
2
  clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
3
- clonebox/cli.py,sha256=KR3naqYB1ZRFDGGoQAfqRk4Ci4NVF3WiY3nOzRKmcyg,136362
4
- clonebox/cloner.py,sha256=aH7BwmLgPITHzyHn_n_AKqtEADDm_aUXiRG-nB2rxMQ,90063
3
+ clonebox/cli.py,sha256=VZ1K0z0jLbqrEakXR4pC9d4FVTvzLiyzuOff3S0s2gg,136214
4
+ clonebox/cloner.py,sha256=7JzouMKoqNQ2AYKA2HL4hk0jIDeaWh2b0mV1UW6nwD0,91267
5
5
  clonebox/container.py,sha256=tiYK1ZB-DhdD6A2FuMA0h_sRNkUI7KfYcJ0tFOcdyeM,6105
6
6
  clonebox/dashboard.py,sha256=dMY6odvPq3j6FronhRRsX7aY3qdCwznB-aCWKEmHDNw,5768
7
7
  clonebox/detector.py,sha256=vS65cvFNPmUBCX1Y_TMTnSRljw6r1Ae9dlVtACs5XFc,23075
@@ -34,9 +34,9 @@ clonebox/snapshots/manager.py,sha256=hGzM8V6ZJPXjTqj47c4Kr8idlE-c1Q3gPUvuw1HvS1A
34
34
  clonebox/snapshots/models.py,sha256=sRnn3OZE8JG9FZJlRuA3ihO-JXoPCQ3nD3SQytflAao,6206
35
35
  clonebox/templates/profiles/ml-dev.yaml,sha256=w07MToGh31xtxpjbeXTBk9BkpAN8A3gv8HeA3ESKG9M,461
36
36
  clonebox/templates/profiles/web-stack.yaml,sha256=EBnnGMzML5vAjXmIUbCpbTCwmRaNJiuWd3EcL43DOK8,485
37
- clonebox-1.1.7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
38
- clonebox-1.1.7.dist-info/METADATA,sha256=ojWCleuifB5DP3QDwjgT-X45ge_SslR30e7CyHl8iwY,48915
39
- clonebox-1.1.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
40
- clonebox-1.1.7.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
41
- clonebox-1.1.7.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
42
- clonebox-1.1.7.dist-info/RECORD,,
37
+ clonebox-1.1.9.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
38
+ clonebox-1.1.9.dist-info/METADATA,sha256=UjgHC8vzL5OL-yr1WP4XB0KAAacblP6YNiKsD9zWpa8,48915
39
+ clonebox-1.1.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
40
+ clonebox-1.1.9.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
41
+ clonebox-1.1.9.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
42
+ clonebox-1.1.9.dist-info/RECORD,,