clonebox 1.1.8__tar.gz → 1.1.10__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.
Files changed (59) hide show
  1. {clonebox-1.1.8/src/clonebox.egg-info → clonebox-1.1.10}/PKG-INFO +1 -1
  2. {clonebox-1.1.8 → clonebox-1.1.10}/pyproject.toml +1 -1
  3. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/cli.py +63 -6
  4. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/cloner.py +57 -35
  5. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/validator.py +17 -0
  6. {clonebox-1.1.8 → clonebox-1.1.10/src/clonebox.egg-info}/PKG-INFO +1 -1
  7. {clonebox-1.1.8 → clonebox-1.1.10}/LICENSE +0 -0
  8. {clonebox-1.1.8 → clonebox-1.1.10}/README.md +0 -0
  9. {clonebox-1.1.8 → clonebox-1.1.10}/setup.cfg +0 -0
  10. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/__init__.py +0 -0
  11. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/__main__.py +0 -0
  12. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/backends/libvirt_backend.py +0 -0
  13. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/backends/qemu_disk.py +0 -0
  14. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/backends/subprocess_runner.py +0 -0
  15. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/container.py +0 -0
  16. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/dashboard.py +0 -0
  17. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/detector.py +0 -0
  18. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/di.py +0 -0
  19. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/exporter.py +0 -0
  20. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/health/__init__.py +0 -0
  21. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/health/manager.py +0 -0
  22. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/health/models.py +0 -0
  23. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/health/probes.py +0 -0
  24. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/importer.py +0 -0
  25. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/interfaces/disk.py +0 -0
  26. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/interfaces/hypervisor.py +0 -0
  27. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/interfaces/network.py +0 -0
  28. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/interfaces/process.py +0 -0
  29. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/logging.py +0 -0
  30. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/models.py +0 -0
  31. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/monitor.py +0 -0
  32. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/p2p.py +0 -0
  33. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/profiles.py +0 -0
  34. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/resource_monitor.py +0 -0
  35. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/resources.py +0 -0
  36. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/rollback.py +0 -0
  37. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/secrets.py +0 -0
  38. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/snapshots/__init__.py +0 -0
  39. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/snapshots/manager.py +0 -0
  40. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/snapshots/models.py +0 -0
  41. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
  42. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox/templates/profiles/web-stack.yaml +0 -0
  43. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox.egg-info/SOURCES.txt +0 -0
  44. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox.egg-info/dependency_links.txt +0 -0
  45. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox.egg-info/entry_points.txt +0 -0
  46. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox.egg-info/requires.txt +0 -0
  47. {clonebox-1.1.8 → clonebox-1.1.10}/src/clonebox.egg-info/top_level.txt +0 -0
  48. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_cli.py +0 -0
  49. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_cloner.py +0 -0
  50. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_cloner_simple.py +0 -0
  51. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_container.py +0 -0
  52. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_coverage_additional.py +0 -0
  53. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_coverage_boost_final.py +0 -0
  54. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_dashboard_coverage.py +0 -0
  55. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_detector.py +0 -0
  56. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_models.py +0 -0
  57. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_network.py +0 -0
  58. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_profiles.py +0 -0
  59. {clonebox-1.1.8 → clonebox-1.1.10}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.8
3
+ Version: 1.1.10
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.8"
7
+ version = "1.1.10"
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"}
@@ -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
- conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2728
- command = args.command
2729
- if isinstance(command, list):
2730
- command = " ".join(command).strip()
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 not _qga_ping(vm_name, conn_uri):
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
@@ -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
 
@@ -1250,7 +1256,8 @@ fi
1250
1256
  cloudinit_dir.mkdir(exist_ok=True)
1251
1257
 
1252
1258
  # Meta-data
1253
- 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"
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
- if str(guest_path).startswith("/home/ubuntu/snap/"):
1264
- guest_parts = Path(guest_path).parts
1265
- if len(guest_parts) > 4:
1266
- snap_name = guest_parts[4]
1267
- for d in (
1268
- "/home/ubuntu",
1269
- "/home/ubuntu/snap",
1270
- f"/home/ubuntu/snap/{snap_name}",
1271
- ):
1272
- if d not in pre_chown_dirs:
1273
- pre_chown_dirs.add(d)
1274
- mount_commands.append(f" - mkdir -p {d}")
1275
- mount_commands.append(f" - chown 1000:1000 {d}")
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
- mount_commands.append(f" - mkdir -p {guest_path}")
1281
- mount_commands.append(f" - chown 1000:1000 {guest_path}")
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
- runcmd_lines.append(
1334
- f" - snap install {snap_pkg} --classic || snap install {snap_pkg} || true"
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 - runs AFTER package installation completes
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": (
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.8
3
+ Version: 1.1.10
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
File without changes