clonebox 1.1.9__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.9/src/clonebox.egg-info → clonebox-1.1.10}/PKG-INFO +1 -1
  2. {clonebox-1.1.9 → clonebox-1.1.10}/pyproject.toml +1 -1
  3. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/cli.py +54 -5
  4. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/cloner.py +48 -33
  5. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/validator.py +17 -0
  6. {clonebox-1.1.9 → clonebox-1.1.10/src/clonebox.egg-info}/PKG-INFO +1 -1
  7. {clonebox-1.1.9 → clonebox-1.1.10}/LICENSE +0 -0
  8. {clonebox-1.1.9 → clonebox-1.1.10}/README.md +0 -0
  9. {clonebox-1.1.9 → clonebox-1.1.10}/setup.cfg +0 -0
  10. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/__init__.py +0 -0
  11. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/__main__.py +0 -0
  12. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/backends/libvirt_backend.py +0 -0
  13. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/backends/qemu_disk.py +0 -0
  14. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/backends/subprocess_runner.py +0 -0
  15. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/container.py +0 -0
  16. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/dashboard.py +0 -0
  17. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/detector.py +0 -0
  18. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/di.py +0 -0
  19. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/exporter.py +0 -0
  20. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/health/__init__.py +0 -0
  21. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/health/manager.py +0 -0
  22. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/health/models.py +0 -0
  23. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/health/probes.py +0 -0
  24. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/importer.py +0 -0
  25. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/interfaces/disk.py +0 -0
  26. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/interfaces/hypervisor.py +0 -0
  27. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/interfaces/network.py +0 -0
  28. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/interfaces/process.py +0 -0
  29. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/logging.py +0 -0
  30. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/models.py +0 -0
  31. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/monitor.py +0 -0
  32. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/p2p.py +0 -0
  33. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/profiles.py +0 -0
  34. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/resource_monitor.py +0 -0
  35. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/resources.py +0 -0
  36. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/rollback.py +0 -0
  37. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/secrets.py +0 -0
  38. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/snapshots/__init__.py +0 -0
  39. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/snapshots/manager.py +0 -0
  40. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/snapshots/models.py +0 -0
  41. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
  42. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox/templates/profiles/web-stack.yaml +0 -0
  43. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox.egg-info/SOURCES.txt +0 -0
  44. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox.egg-info/dependency_links.txt +0 -0
  45. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox.egg-info/entry_points.txt +0 -0
  46. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox.egg-info/requires.txt +0 -0
  47. {clonebox-1.1.9 → clonebox-1.1.10}/src/clonebox.egg-info/top_level.txt +0 -0
  48. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_cli.py +0 -0
  49. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_cloner.py +0 -0
  50. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_cloner_simple.py +0 -0
  51. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_container.py +0 -0
  52. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_coverage_additional.py +0 -0
  53. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_coverage_boost_final.py +0 -0
  54. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_dashboard_coverage.py +0 -0
  55. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_detector.py +0 -0
  56. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_models.py +0 -0
  57. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_network.py +0 -0
  58. {clonebox-1.1.9 → clonebox-1.1.10}/tests/test_profiles.py +0 -0
  59. {clonebox-1.1.9 → 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.9
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.9"
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,20 +2724,69 @@ 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)
2771
+
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"
2735
2774
 
2736
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
2737
2781
  if not qga_ready:
2738
2782
  for _ in range(12): # ~60s
2739
2783
  time.sleep(5)
2740
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
2741
2790
  if qga_ready:
2742
2791
  break
2743
2792
 
@@ -1267,25 +1267,33 @@ fi
1267
1267
  pre_chown_dirs: set[str] = set()
1268
1268
  for idx, (host_path, guest_path) in enumerate(all_paths.items()):
1269
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}")
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
+
1283
1287
  tag = f"mount{idx}"
1284
1288
  # Use uid=1000,gid=1000 to give ubuntu user access to mounts
1285
1289
  # mmap allows proper file mapping
1286
1290
  mount_opts = "trans=virtio,version=9p2000.L,mmap,uid=1000,gid=1000,users"
1287
- mount_commands.append(f" - mkdir -p {guest_path}")
1288
- 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
+
1289
1297
  mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {guest_path} || true")
1290
1298
  # Add fstab entry for persistence after reboot
1291
1299
  fstab_entries.append(f"{tag} {guest_path} 9p {mount_opts},nofail 0 0")
@@ -1330,16 +1338,37 @@ fi
1330
1338
  for cmd in mount_commands:
1331
1339
  runcmd_lines.append(cmd)
1332
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
+
1333
1356
  runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu || true")
1334
1357
  runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/snap || true")
1335
1358
 
1336
- # Install snap packages
1359
+ # Install snap packages (with retry logic)
1337
1360
  if config.snap_packages:
1338
1361
  runcmd_lines.append(" - echo 'Installing snap packages...'")
1339
1362
  for snap_pkg in config.snap_packages:
1340
- runcmd_lines.append(
1341
- 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"
1342
1370
  )
1371
+ runcmd_lines.append(f" - {cmd}")
1343
1372
 
1344
1373
  # Connect snap interfaces for GUI apps (not auto-connected via cloud-init)
1345
1374
  runcmd_lines.append(" - echo 'Connecting snap interfaces...'")
@@ -1352,22 +1381,8 @@ fi
1352
1381
 
1353
1382
  runcmd_lines.append(" - systemctl restart snapd || true")
1354
1383
 
1355
- # Add GUI setup if enabled - runs AFTER package installation completes
1384
+ # Add remaining GUI setup if enabled
1356
1385
  if config.gui:
1357
- # Create directories that GNOME services need BEFORE GUI starts
1358
- # These may conflict with mounted host directories, so ensure they exist with correct perms
1359
- runcmd_lines.extend(
1360
- [
1361
- " - mkdir -p /home/ubuntu/.config/pulse /home/ubuntu/.cache/ibus /home/ubuntu/.local/share",
1362
- " - mkdir -p /home/ubuntu/.config/dconf /home/ubuntu/.cache/tracker3",
1363
- " - mkdir -p /home/ubuntu/.config/autostart",
1364
- " - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
1365
- " - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
1366
- " - systemctl set-default graphical.target",
1367
- " - systemctl enable gdm3 || systemctl enable gdm || true",
1368
- ]
1369
- )
1370
-
1371
1386
  # Create autostart entries for GUI apps
1372
1387
  autostart_apps = {
1373
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.9
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