clonebox 1.1.8__py3-none-any.whl → 1.1.10__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 +63 -6
- clonebox/cloner.py +57 -35
- clonebox/validator.py +17 -0
- {clonebox-1.1.8.dist-info → clonebox-1.1.10.dist-info}/METADATA +1 -1
- {clonebox-1.1.8.dist-info → clonebox-1.1.10.dist-info}/RECORD +9 -9
- {clonebox-1.1.8.dist-info → clonebox-1.1.10.dist-info}/WHEEL +0 -0
- {clonebox-1.1.8.dist-info → clonebox-1.1.10.dist-info}/entry_points.txt +0 -0
- {clonebox-1.1.8.dist-info → clonebox-1.1.10.dist-info}/licenses/LICENSE +0 -0
- {clonebox-1.1.8.dist-info → clonebox-1.1.10.dist-info}/top_level.txt +0 -0
clonebox/cli.py
CHANGED
|
@@ -2724,16 +2724,73 @@ def cmd_monitor(args) -> None:
|
|
|
2724
2724
|
def cmd_exec(args) -> None:
|
|
2725
2725
|
"""Execute command in VM via QEMU Guest Agent."""
|
|
2726
2726
|
vm_name, config_file = _resolve_vm_name_and_config_file(args.name)
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2727
|
+
user_session = getattr(args, "user", False)
|
|
2728
|
+
timeout = getattr(args, "timeout", 30)
|
|
2729
|
+
|
|
2730
|
+
# When using argparse.REMAINDER for `command`, any flags placed after the VM name
|
|
2731
|
+
# may end up inside args.command. Recover common exec flags from the remainder.
|
|
2732
|
+
command_tokens = args.command
|
|
2733
|
+
if not isinstance(command_tokens, list):
|
|
2734
|
+
command_tokens = [str(command_tokens)] if command_tokens is not None else []
|
|
2735
|
+
|
|
2736
|
+
if "--" in command_tokens:
|
|
2737
|
+
sep_idx = command_tokens.index("--")
|
|
2738
|
+
pre_tokens = command_tokens[:sep_idx]
|
|
2739
|
+
post_tokens = command_tokens[sep_idx + 1 :]
|
|
2740
|
+
else:
|
|
2741
|
+
pre_tokens = command_tokens
|
|
2742
|
+
post_tokens = []
|
|
2743
|
+
|
|
2744
|
+
i = 0
|
|
2745
|
+
while i < len(pre_tokens):
|
|
2746
|
+
tok = pre_tokens[i]
|
|
2747
|
+
if tok in ("-u", "--user"):
|
|
2748
|
+
user_session = True
|
|
2749
|
+
i += 1
|
|
2750
|
+
continue
|
|
2751
|
+
if tok in ("-t", "--timeout"):
|
|
2752
|
+
if i + 1 < len(pre_tokens):
|
|
2753
|
+
try:
|
|
2754
|
+
timeout = int(pre_tokens[i + 1])
|
|
2755
|
+
except ValueError:
|
|
2756
|
+
pass
|
|
2757
|
+
i += 2
|
|
2758
|
+
continue
|
|
2759
|
+
break
|
|
2760
|
+
|
|
2761
|
+
remaining_pre = pre_tokens[i:]
|
|
2762
|
+
if post_tokens:
|
|
2763
|
+
command_tokens = remaining_pre + post_tokens
|
|
2764
|
+
else:
|
|
2765
|
+
command_tokens = remaining_pre
|
|
2766
|
+
|
|
2767
|
+
command = " ".join(command_tokens).strip()
|
|
2731
2768
|
if not command:
|
|
2732
2769
|
console.print("[red]❌ No command specified[/]")
|
|
2733
2770
|
return
|
|
2734
|
-
timeout = getattr(args, "timeout", 30)
|
|
2735
2771
|
|
|
2736
|
-
if
|
|
2772
|
+
conn_uri = "qemu:///session" if user_session else "qemu:///system"
|
|
2773
|
+
other_conn_uri = "qemu:///system" if conn_uri == "qemu:///session" else "qemu:///session"
|
|
2774
|
+
|
|
2775
|
+
qga_ready = _qga_ping(vm_name, conn_uri)
|
|
2776
|
+
if not qga_ready:
|
|
2777
|
+
alt_ready = _qga_ping(vm_name, other_conn_uri)
|
|
2778
|
+
if alt_ready:
|
|
2779
|
+
conn_uri = other_conn_uri
|
|
2780
|
+
qga_ready = True
|
|
2781
|
+
if not qga_ready:
|
|
2782
|
+
for _ in range(12): # ~60s
|
|
2783
|
+
time.sleep(5)
|
|
2784
|
+
qga_ready = _qga_ping(vm_name, conn_uri)
|
|
2785
|
+
if not qga_ready:
|
|
2786
|
+
alt_ready = _qga_ping(vm_name, other_conn_uri)
|
|
2787
|
+
if alt_ready:
|
|
2788
|
+
conn_uri = other_conn_uri
|
|
2789
|
+
qga_ready = True
|
|
2790
|
+
if qga_ready:
|
|
2791
|
+
break
|
|
2792
|
+
|
|
2793
|
+
if not qga_ready:
|
|
2737
2794
|
console.print(f"[red]❌ Cannot connect to VM '{vm_name}' via QEMU Guest Agent[/]")
|
|
2738
2795
|
console.print("[dim]Make sure the VM is running and qemu-guest-agent is installed.[/]")
|
|
2739
2796
|
return
|
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
|
|
|
@@ -1250,7 +1256,8 @@ fi
|
|
|
1250
1256
|
cloudinit_dir.mkdir(exist_ok=True)
|
|
1251
1257
|
|
|
1252
1258
|
# Meta-data
|
|
1253
|
-
|
|
1259
|
+
instance_id = f"{config.name}-{uuid.uuid4().hex}"
|
|
1260
|
+
meta_data = f"instance-id: {instance_id}\nlocal-hostname: {config.name}\n"
|
|
1254
1261
|
(cloudinit_dir / "meta-data").write_text(meta_data)
|
|
1255
1262
|
|
|
1256
1263
|
# Generate mount commands and fstab entries for 9p filesystems
|
|
@@ -1260,25 +1267,33 @@ fi
|
|
|
1260
1267
|
pre_chown_dirs: set[str] = set()
|
|
1261
1268
|
for idx, (host_path, guest_path) in enumerate(all_paths.items()):
|
|
1262
1269
|
if Path(host_path).exists():
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
mount_commands.append(f" -
|
|
1270
|
+
# Ensure all parent directories in /home/ubuntu are owned by user
|
|
1271
|
+
# This prevents "Permission denied" when creating config dirs (e.g. .config) as root
|
|
1272
|
+
if str(guest_path).startswith("/home/ubuntu/"):
|
|
1273
|
+
try:
|
|
1274
|
+
rel_path = Path(guest_path).relative_to("/home/ubuntu")
|
|
1275
|
+
current = Path("/home/ubuntu")
|
|
1276
|
+
# Create and chown each component in the path
|
|
1277
|
+
for part in rel_path.parts:
|
|
1278
|
+
current = current / part
|
|
1279
|
+
d_str = str(current)
|
|
1280
|
+
if d_str not in pre_chown_dirs:
|
|
1281
|
+
pre_chown_dirs.add(d_str)
|
|
1282
|
+
mount_commands.append(f" - mkdir -p {d_str}")
|
|
1283
|
+
mount_commands.append(f" - chown 1000:1000 {d_str}")
|
|
1284
|
+
except ValueError:
|
|
1285
|
+
pass
|
|
1286
|
+
|
|
1276
1287
|
tag = f"mount{idx}"
|
|
1277
1288
|
# Use uid=1000,gid=1000 to give ubuntu user access to mounts
|
|
1278
1289
|
# mmap allows proper file mapping
|
|
1279
1290
|
mount_opts = "trans=virtio,version=9p2000.L,mmap,uid=1000,gid=1000,users"
|
|
1280
|
-
|
|
1281
|
-
|
|
1291
|
+
|
|
1292
|
+
# Ensure target exists and is owned by user (if not already handled)
|
|
1293
|
+
if str(guest_path) not in pre_chown_dirs:
|
|
1294
|
+
mount_commands.append(f" - mkdir -p {guest_path}")
|
|
1295
|
+
mount_commands.append(f" - chown 1000:1000 {guest_path}")
|
|
1296
|
+
|
|
1282
1297
|
mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {guest_path} || true")
|
|
1283
1298
|
# Add fstab entry for persistence after reboot
|
|
1284
1299
|
fstab_entries.append(f"{tag} {guest_path} 9p {mount_opts},nofail 0 0")
|
|
@@ -1323,16 +1338,37 @@ fi
|
|
|
1323
1338
|
for cmd in mount_commands:
|
|
1324
1339
|
runcmd_lines.append(cmd)
|
|
1325
1340
|
|
|
1341
|
+
# Create user directories with correct permissions EARLY to avoid race conditions with GDM
|
|
1342
|
+
if config.gui:
|
|
1343
|
+
# Create directories that GNOME services need
|
|
1344
|
+
runcmd_lines.extend(
|
|
1345
|
+
[
|
|
1346
|
+
" - mkdir -p /home/ubuntu/.config/pulse /home/ubuntu/.cache/ibus /home/ubuntu/.local/share",
|
|
1347
|
+
" - mkdir -p /home/ubuntu/.config/dconf /home/ubuntu/.cache/tracker3",
|
|
1348
|
+
" - mkdir -p /home/ubuntu/.config/autostart",
|
|
1349
|
+
" - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
|
|
1350
|
+
" - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
|
|
1351
|
+
" - systemctl set-default graphical.target",
|
|
1352
|
+
" - systemctl enable gdm3 || systemctl enable gdm || true",
|
|
1353
|
+
]
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1326
1356
|
runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu || true")
|
|
1327
1357
|
runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/snap || true")
|
|
1328
1358
|
|
|
1329
|
-
# Install snap packages
|
|
1359
|
+
# Install snap packages (with retry logic)
|
|
1330
1360
|
if config.snap_packages:
|
|
1331
1361
|
runcmd_lines.append(" - echo 'Installing snap packages...'")
|
|
1332
1362
|
for snap_pkg in config.snap_packages:
|
|
1333
|
-
|
|
1334
|
-
|
|
1363
|
+
# Try classic first, then strict, with retries
|
|
1364
|
+
cmd = (
|
|
1365
|
+
f"for i in 1 2 3; do "
|
|
1366
|
+
f"snap install {snap_pkg} --classic && break || "
|
|
1367
|
+
f"snap install {snap_pkg} && break || "
|
|
1368
|
+
f"sleep 10; "
|
|
1369
|
+
f"done"
|
|
1335
1370
|
)
|
|
1371
|
+
runcmd_lines.append(f" - {cmd}")
|
|
1336
1372
|
|
|
1337
1373
|
# Connect snap interfaces for GUI apps (not auto-connected via cloud-init)
|
|
1338
1374
|
runcmd_lines.append(" - echo 'Connecting snap interfaces...'")
|
|
@@ -1345,22 +1381,8 @@ fi
|
|
|
1345
1381
|
|
|
1346
1382
|
runcmd_lines.append(" - systemctl restart snapd || true")
|
|
1347
1383
|
|
|
1348
|
-
# Add GUI setup if enabled
|
|
1384
|
+
# Add remaining GUI setup if enabled
|
|
1349
1385
|
if config.gui:
|
|
1350
|
-
# Create directories that GNOME services need BEFORE GUI starts
|
|
1351
|
-
# These may conflict with mounted host directories, so ensure they exist with correct perms
|
|
1352
|
-
runcmd_lines.extend(
|
|
1353
|
-
[
|
|
1354
|
-
" - mkdir -p /home/ubuntu/.config/pulse /home/ubuntu/.cache/ibus /home/ubuntu/.local/share",
|
|
1355
|
-
" - mkdir -p /home/ubuntu/.config/dconf /home/ubuntu/.cache/tracker3",
|
|
1356
|
-
" - mkdir -p /home/ubuntu/.config/autostart",
|
|
1357
|
-
" - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
|
|
1358
|
-
" - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
|
|
1359
|
-
" - systemctl set-default graphical.target",
|
|
1360
|
-
" - systemctl enable gdm3 || systemctl enable gdm || true",
|
|
1361
|
-
]
|
|
1362
|
-
)
|
|
1363
|
-
|
|
1364
1386
|
# Create autostart entries for GUI apps
|
|
1365
1387
|
autostart_apps = {
|
|
1366
1388
|
"pycharm-community": (
|
clonebox/validator.py
CHANGED
|
@@ -897,6 +897,23 @@ class VMValidator:
|
|
|
897
897
|
self.results["overall"] = "qga_not_ready"
|
|
898
898
|
return self.results
|
|
899
899
|
|
|
900
|
+
ci_status = self._exec_in_vm("cloud-init status --long 2>/dev/null || cloud-init status 2>/dev/null || true", timeout=20)
|
|
901
|
+
if ci_status:
|
|
902
|
+
ci_lower = ci_status.lower()
|
|
903
|
+
if "running" in ci_lower:
|
|
904
|
+
self.console.print("[yellow]⏳ Cloud-init still running - skipping deep validation for now[/]")
|
|
905
|
+
self.results["overall"] = "cloud_init_running"
|
|
906
|
+
return self.results
|
|
907
|
+
|
|
908
|
+
ready_msg = self._exec_in_vm(
|
|
909
|
+
"cat /var/log/clonebox-ready 2>/dev/null || true",
|
|
910
|
+
timeout=10,
|
|
911
|
+
)
|
|
912
|
+
if not (ready_msg and "clonebox vm ready" in ready_msg.lower()):
|
|
913
|
+
self.console.print(
|
|
914
|
+
"[yellow]⚠️ CloneBox ready marker not found - provisioning may not have completed[/]"
|
|
915
|
+
)
|
|
916
|
+
|
|
900
917
|
# Run all validations
|
|
901
918
|
self.validate_mounts()
|
|
902
919
|
self.validate_packages()
|
|
@@ -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=6aaIMefXxoRQgzlQDOqKFHq7e8U0CIH5TuYKrsobhH4,137848
|
|
4
|
+
clonebox/cloner.py,sha256=gUpv0VIqpkmEZPhafyYZOttpd8ZWh9_kTTfk0cCULEg,91937
|
|
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
|
|
@@ -17,7 +17,7 @@ clonebox/resource_monitor.py,sha256=lDR9KyPbVtImeeOkOBPPVP-5yCgoL5hsVFPZ_UqsY0w,
|
|
|
17
17
|
clonebox/resources.py,sha256=IkuM4OdSDV4qhyc0eIynwbAHBTv0aVSxxW-gghsnCAs,6815
|
|
18
18
|
clonebox/rollback.py,sha256=hpwO-8Ehe1pW0wHuZvJkC_qxZ6yEo9otCJRhGIUArCo,5711
|
|
19
19
|
clonebox/secrets.py,sha256=QtGMIjhTKq14qQRGy-pX0TU3vKLiZt7CMloAlF2GmzQ,10515
|
|
20
|
-
clonebox/validator.py,sha256=
|
|
20
|
+
clonebox/validator.py,sha256=pxW1Np3s-T2TFvJ64BiO-N8lcKnJGzwInu5cPC9lBK0,39528
|
|
21
21
|
clonebox/backends/libvirt_backend.py,sha256=sIHFIvFO1hIOXEFR_foSkOGBgIzaJVQs-njOU8GdafA,7170
|
|
22
22
|
clonebox/backends/qemu_disk.py,sha256=YsGjYX5sbEf35Y4yjTpNkZat73a4RGBxY-KTVzJhqIs,1687
|
|
23
23
|
clonebox/backends/subprocess_runner.py,sha256=c-IyaMxM1cmUu64h654oAvulm83K5Mu-VQxXJ_0BOds,1506
|
|
@@ -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.10.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
38
|
+
clonebox-1.1.10.dist-info/METADATA,sha256=nRh_SZ7TDLcmP5gJPPBXwdyq_VRpaI_nSfbsDeYjL9E,48916
|
|
39
|
+
clonebox-1.1.10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
40
|
+
clonebox-1.1.10.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
|
|
41
|
+
clonebox-1.1.10.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
|
|
42
|
+
clonebox-1.1.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|