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 +90 -94
- clonebox/cloner.py +31 -6
- {clonebox-1.1.7.dist-info → clonebox-1.1.9.dist-info}/METADATA +1 -1
- {clonebox-1.1.7.dist-info → clonebox-1.1.9.dist-info}/RECORD +8 -8
- {clonebox-1.1.7.dist-info → clonebox-1.1.9.dist-info}/WHEEL +0 -0
- {clonebox-1.1.7.dist-info → clonebox-1.1.9.dist-info}/entry_points.txt +0 -0
- {clonebox-1.1.7.dist-info → clonebox-1.1.9.dist-info}/licenses/LICENSE +0 -0
- {clonebox-1.1.7.dist-info → clonebox-1.1.9.dist-info}/top_level.txt +0 -0
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1899
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,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=
|
|
4
|
-
clonebox/cloner.py,sha256=
|
|
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.
|
|
38
|
-
clonebox-1.1.
|
|
39
|
-
clonebox-1.1.
|
|
40
|
-
clonebox-1.1.
|
|
41
|
-
clonebox-1.1.
|
|
42
|
-
clonebox-1.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|