clonebox 1.1.17__py3-none-any.whl → 1.1.18__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.

Potentially problematic release.


This version of clonebox might be problematic. Click here for more details.

clonebox/cli.py CHANGED
@@ -2498,6 +2498,18 @@ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout:
2498
2498
  "grep -E '\\[[0-9]/[0-9]\\]|→' /var/log/cloud-init-output.log 2>/dev/null | tail -n 5"
2499
2499
  )
2500
2500
 
2501
+ # Check disk space in real-time
2502
+ disk_info = _exec_in_vm_qga(
2503
+ vm_name,
2504
+ conn_uri,
2505
+ "df / --output=pcent | tail -n 1 | tr -dc '0-9'"
2506
+ )
2507
+ if disk_info and disk_info.isdigit():
2508
+ usage = int(disk_info)
2509
+ if usage > 90:
2510
+ console.print(f"[bold red]⚠️ WARNING: VM Disk is nearly full ({usage}%)![/]")
2511
+ console.print("[red] Installation may fail. Consider increasing --disk-size-gb.[/]")
2512
+
2501
2513
  if raw_info:
2502
2514
  lines = [l.strip() for l in raw_info.strip().split('\n') if l.strip()]
2503
2515
  for line in lines:
clonebox/cloner.py CHANGED
@@ -1124,7 +1124,7 @@ fi
1124
1124
  mount_checks.append(f'check_mount "{guest_path}" "mount{idx}"')
1125
1125
 
1126
1126
  # Add copied paths checks
1127
- copy_paths = config.copy_paths or config.app_data_paths
1127
+ copy_paths = config.copy_paths or getattr(config, "app_data_paths", {})
1128
1128
  if copy_paths:
1129
1129
  for idx, (host_path, guest_path) in enumerate(copy_paths.items()):
1130
1130
  mount_checks.append(f'check_copy_path "{guest_path}"')
@@ -1161,6 +1161,27 @@ log() {{
1161
1161
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$REPORT_FILE"
1162
1162
  }}
1163
1163
 
1164
+ check_disk_space() {{
1165
+ local usage
1166
+ usage=$(df / --output=pcent | tail -n 1 | tr -dc '0-9')
1167
+ local avail
1168
+ avail=$(df -h / --output=avail | tail -n 1 | tr -d ' ')
1169
+
1170
+ if [ "$usage" -gt 95 ]; then
1171
+ log "[FAIL] Disk space nearly full: ${{usage}}% used ($avail available)"
1172
+ ((FAILED++))
1173
+ return 1
1174
+ elif [ "$usage" -gt 85 ]; then
1175
+ log "[WARN] Disk usage high: ${{usage}}% used ($avail available)"
1176
+ ((WARNINGS++))
1177
+ return 0
1178
+ else
1179
+ log "[PASS] Disk space OK: ${{usage}}% used ($avail available)"
1180
+ ((PASSED++))
1181
+ return 0
1182
+ fi
1183
+ }}
1184
+
1164
1185
  check_apt_package() {{
1165
1186
  local pkg="$1"
1166
1187
  if dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"; then
@@ -1283,6 +1304,11 @@ log "VM Name: {config.name}"
1283
1304
  log "Date: $(date)"
1284
1305
  log "=========================================="
1285
1306
 
1307
+ log ""
1308
+ log "--- System Health ---"
1309
+ check_disk_space
1310
+ check_gui
1311
+
1286
1312
  log ""
1287
1313
  log "--- APT Packages ---"
1288
1314
  {apt_checks_str}
@@ -1507,7 +1533,8 @@ fi
1507
1533
  for i, pkg in enumerate(all_packages, 1):
1508
1534
  runcmd_lines.append(f" - echo ' → [{i}/{len(all_packages)}] Installing {pkg}...'")
1509
1535
  runcmd_lines.append(f" - apt-get install -y {pkg} || echo ' ⚠️ Failed to install {pkg}'")
1510
- runcmd_lines.append(" - echo ' ✓ APT packages installed'")
1536
+ runcmd_lines.append(" - apt-get clean")
1537
+ runcmd_lines.append(" - echo ' ✓ APT packages installed and cache cleaned'")
1511
1538
  runcmd_lines.append(" - echo ''")
1512
1539
  else:
1513
1540
  runcmd_lines.append(" - echo '[1/9] 📦 No APT packages to install'")
@@ -2454,25 +2481,21 @@ if __name__ == "__main__":
2454
2481
  # Note: The bash monitor is already installed above, no need to install Python monitor
2455
2482
 
2456
2483
  # Create logs disk for host access
2457
- # Use different paths based on session type
2458
- if user_session:
2459
- logs_disk_path = str(Path.home() / ".local/share/libvirt/images/clonebox-logs.qcow2")
2460
- else:
2461
- logs_disk_path = "/var/lib/libvirt/images/clonebox-logs.qcow2"
2484
+ # Inside the VM, we use a fixed path for the image file
2485
+ vm_logs_img_path = "/var/lib/clonebox/logs.img"
2462
2486
 
2463
2487
  runcmd_lines.extend(
2464
2488
  [
2465
- " - mkdir -p /mnt/logs",
2466
- f" - truncate -s 1G {logs_disk_path}",
2467
- f" - mkfs.ext4 -F {logs_disk_path}",
2468
- f" - echo '{logs_disk_path} /mnt/logs ext4 loop,defaults 0 0' >> /etc/fstab",
2489
+ " - mkdir -p /var/lib/clonebox /mnt/logs",
2490
+ f" - truncate -s 1G {vm_logs_img_path}",
2491
+ f" - mkfs.ext4 -F {vm_logs_img_path}",
2492
+ f" - echo '{vm_logs_img_path} /mnt/logs ext4 loop,defaults 0 0' >> /etc/fstab",
2469
2493
  " - mount -a",
2470
2494
  " - mkdir -p /mnt/logs/var/log",
2471
2495
  " - mkdir -p /mnt/logs/tmp",
2472
2496
  " - cp -r /var/log/clonebox*.log /mnt/logs/var/log/ 2>/dev/null || true",
2473
2497
  " - cp -r /tmp/*-error.log /mnt/logs/tmp/ 2>/dev/null || true",
2474
- f" - echo 'Logs disk mounted at /mnt/logs - accessible from host as {logs_disk_path}'",
2475
- f" - \"echo 'To view logs on host: sudo mount -o loop {logs_disk_path} /mnt/clonebox-logs'\"",
2498
+ f" - echo 'Logs disk mounted at /mnt/logs - backing file: {vm_logs_img_path}'",
2476
2499
  ]
2477
2500
  )
2478
2501
 
clonebox/validator.py CHANGED
@@ -76,39 +76,38 @@ class VMValidator:
76
76
 
77
77
  pid = response["return"]["pid"]
78
78
 
79
- # Wait a bit for command to complete
80
- time.sleep(0.3)
81
-
82
- # Get result
83
- status_result = subprocess.run(
84
- [
85
- "virsh",
86
- "--connect",
87
- self.conn_uri,
88
- "qemu-agent-command",
89
- self.vm_name,
90
- f'{{"execute":"guest-exec-status","arguments":{{"pid":{pid}}}}}',
91
- ],
92
- capture_output=True,
93
- text=True,
94
- timeout=5,
95
- )
79
+ deadline = time.time() + timeout
80
+ while time.time() < deadline:
81
+ status_result = subprocess.run(
82
+ [
83
+ "virsh",
84
+ "--connect",
85
+ self.conn_uri,
86
+ "qemu-agent-command",
87
+ self.vm_name,
88
+ f'{{"execute":"guest-exec-status","arguments":{{"pid":{pid}}}}}',
89
+ ],
90
+ capture_output=True,
91
+ text=True,
92
+ timeout=5,
93
+ )
96
94
 
97
- if status_result.returncode != 0:
98
- return None
95
+ if status_result.returncode != 0:
96
+ return None
99
97
 
100
- status_resp = json.loads(status_result.stdout)
101
- if "return" not in status_resp:
102
- return None
98
+ status_resp = json.loads(status_result.stdout)
99
+ if "return" not in status_resp:
100
+ return None
103
101
 
104
- ret = status_resp["return"]
105
- if not ret.get("exited", False):
106
- return None
102
+ ret = status_resp["return"]
103
+ if ret.get("exited", False):
104
+ if "out-data" in ret:
105
+ return base64.b64decode(ret["out-data"]).decode().strip()
106
+ return ""
107
107
 
108
- if "out-data" in ret:
109
- return base64.b64decode(ret["out-data"]).decode().strip()
108
+ time.sleep(0.2)
110
109
 
111
- return ""
110
+ return None
112
111
 
113
112
  except Exception:
114
113
  return None
@@ -397,6 +396,8 @@ class VMValidator:
397
396
  svc_table.add_column("PID", justify="right", style="dim")
398
397
  svc_table.add_column("Note", style="dim")
399
398
 
399
+ setup_in_progress = self._setup_in_progress() is True
400
+
400
401
  for service in services:
401
402
  if service in self.VM_EXCLUDED_SERVICES:
402
403
  svc_table.add_row(service, "[dim]—[/]", "[dim]—[/]", "[dim]—[/]", "host-only")
@@ -413,6 +414,7 @@ class VMValidator:
413
414
  continue
414
415
 
415
416
  self.results["services"]["total"] += 1
417
+ setup_in_progress = self._setup_in_progress() is True
416
418
 
417
419
  enabled_cmd = f"systemctl is-enabled {service} 2>/dev/null"
418
420
  enabled_status = self._exec_in_vm(enabled_cmd)
@@ -970,6 +972,138 @@ class VMValidator:
970
972
  except Exception:
971
973
  return False
972
974
 
975
+ def validate_disk_space(self) -> Dict:
976
+ """Validate disk space on root filesystem."""
977
+ self.console.print("\n[bold]💾 Validating Disk Space...[/]")
978
+
979
+ df_output = self._exec_in_vm("df -h / --output=pcent,avail,size | tail -n 1", timeout=20)
980
+ if not df_output:
981
+ self.console.print("[red]❌ Could not check disk space[/]")
982
+ return {"status": "error"}
983
+
984
+ try:
985
+ # Format: pcent avail size
986
+ # Example: 98% 100M 30G
987
+ parts = df_output.split()
988
+ usage_pct = int(parts[0].replace('%', ''))
989
+ avail = parts[1]
990
+ total = parts[2]
991
+
992
+ self.results["disk"] = {
993
+ "usage_pct": usage_pct,
994
+ "avail": avail,
995
+ "total": total
996
+ }
997
+
998
+ if usage_pct > 95:
999
+ self.console.print(f"[red]❌ Disk nearly full: {usage_pct}% used ({avail} available of {total})[/]")
1000
+ status = "fail"
1001
+ elif usage_pct > 80:
1002
+ self.console.print(f"[yellow]⚠️ Disk usage high: {usage_pct}% used ({avail} available of {total})[/]")
1003
+ status = "warning"
1004
+ else:
1005
+ self.console.print(f"[green]✅ Disk space OK: {usage_pct}% used ({avail} available of {total})[/]")
1006
+ status = "pass"
1007
+
1008
+ if usage_pct > 80:
1009
+ self._print_disk_usage_breakdown()
1010
+
1011
+ return self.results["disk"]
1012
+ except Exception as e:
1013
+ self.console.print(f"[red]❌ Error parsing df output: {e}[/]")
1014
+ return {"status": "error"}
1015
+
1016
+ def _print_disk_usage_breakdown(self) -> None:
1017
+ def _parse_du_lines(out: Optional[str]) -> List[Tuple[str, str]]:
1018
+ if not out:
1019
+ return []
1020
+ rows: List[Tuple[str, str]] = []
1021
+ for line in out.splitlines():
1022
+ line = line.strip()
1023
+ if not line:
1024
+ continue
1025
+ parts = line.split(maxsplit=1)
1026
+ if len(parts) != 2:
1027
+ continue
1028
+ size, path = parts
1029
+ rows.append((path, size))
1030
+ return rows
1031
+
1032
+ def _dir_size(path: str, timeout: int = 30) -> Optional[str]:
1033
+ out = self._exec_in_vm(f"du -x -s -h {path} 2>/dev/null | head -n 1 | cut -f1", timeout=timeout)
1034
+ return out.strip() if out else None
1035
+
1036
+ self.console.print("\n[bold]📁 Disk usage breakdown (largest directories)[/]")
1037
+
1038
+ top_level = self._exec_in_vm(
1039
+ "du -x -h --max-depth=1 / 2>/dev/null | sort -hr | head -n 15",
1040
+ timeout=60,
1041
+ )
1042
+ top_rows = _parse_du_lines(top_level)
1043
+
1044
+ if top_rows:
1045
+ table = Table(title="Disk Usage: / (Top 15)", border_style="cyan")
1046
+ table.add_column("Path", style="bold")
1047
+ table.add_column("Size", justify="right")
1048
+ for path, size in top_rows:
1049
+ table.add_row(path, size)
1050
+ self.console.print(table)
1051
+ else:
1052
+ self.console.print("[dim]Could not compute top-level directory sizes (du may be busy)[/]")
1053
+
1054
+ var_sz = _dir_size("/var")
1055
+ home_sz = _dir_size("/home")
1056
+ if var_sz or home_sz:
1057
+ sum_table = Table(title="Disk Usage: Key Directories", border_style="cyan")
1058
+ sum_table.add_column("Path", style="bold")
1059
+ sum_table.add_column("Size", justify="right")
1060
+ for p in ["/var", "/var/lib", "/var/log", "/var/cache", "/var/lib/snapd", "/home", "/home/ubuntu", "/tmp"]:
1061
+ sz = _dir_size(p, timeout=30)
1062
+ if sz:
1063
+ sum_table.add_row(p, sz)
1064
+ self.console.print(sum_table)
1065
+
1066
+ var_breakdown = self._exec_in_vm(
1067
+ "du -x -h --max-depth=1 /var 2>/dev/null | sort -hr | head -n 12",
1068
+ timeout=60,
1069
+ )
1070
+ var_rows = _parse_du_lines(var_breakdown)
1071
+ if var_rows:
1072
+ vtable = Table(title="Disk Usage: /var (Top 12)", border_style="cyan")
1073
+ vtable.add_column("Path", style="bold")
1074
+ vtable.add_column("Size", justify="right")
1075
+ for path, size in var_rows:
1076
+ vtable.add_row(path, size)
1077
+ self.console.print(vtable)
1078
+
1079
+ home_breakdown = self._exec_in_vm(
1080
+ "du -x -h --max-depth=2 /home/ubuntu 2>/dev/null | sort -hr | head -n 12",
1081
+ timeout=60,
1082
+ )
1083
+ home_rows = _parse_du_lines(home_breakdown)
1084
+ if home_rows:
1085
+ htable = Table(title="Disk Usage: /home/ubuntu (Top 12)", border_style="cyan")
1086
+ htable.add_column("Path", style="bold")
1087
+ htable.add_column("Size", justify="right")
1088
+ for path, size in home_rows:
1089
+ htable.add_row(path, size)
1090
+ self.console.print(htable)
1091
+
1092
+ copy_paths = self.config.get("copy_paths", None)
1093
+ if not isinstance(copy_paths, dict) or not copy_paths:
1094
+ copy_paths = self.config.get("app_data_paths", {})
1095
+ if copy_paths:
1096
+ ctable = Table(title="Disk Usage: Configured Imported Paths", border_style="cyan")
1097
+ ctable.add_column("Guest Path", style="bold")
1098
+ ctable.add_column("Size", justify="right")
1099
+ for _, guest_path in copy_paths.items():
1100
+ sz = _dir_size(guest_path, timeout=30)
1101
+ if sz:
1102
+ ctable.add_row(str(guest_path), sz)
1103
+ else:
1104
+ ctable.add_row(str(guest_path), "—")
1105
+ self.console.print(ctable)
1106
+
973
1107
  def validate_all(self) -> Dict:
974
1108
  """Run all validations and return comprehensive results."""
975
1109
  self.console.print("[bold cyan]🔍 Running Full Validation...[/]")
@@ -1026,6 +1160,7 @@ class VMValidator:
1026
1160
  )
1027
1161
 
1028
1162
  # Run all validations
1163
+ self.validate_disk_space()
1029
1164
  self.validate_mounts()
1030
1165
  self.validate_packages()
1031
1166
  self.validate_snap_packages()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.17
3
+ Version: 1.1.18
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,8 +1,8 @@
1
1
  clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
2
2
  clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
3
3
  clonebox/audit.py,sha256=1W9vaIjB0A--_p7CgE3cIP5RNckJG1RxJrL-tOb-QmU,14298
4
- clonebox/cli.py,sha256=k3q-JrzsiDcGPjhzHbm1jPKajxD2chYJxSGGftCbA20,178481
5
- clonebox/cloner.py,sha256=HI6un0wukNwftO61m2jASbeJ8SiowCEpWMKMb5Qsd8c,107319
4
+ clonebox/cli.py,sha256=x0VQ9WI1KZrNQGAh7FDf_036jDypdT5Q7vuTlr-dgi0,179087
5
+ clonebox/cloner.py,sha256=WEMNjhHVdXwiA2zCq14fr5eVD6vhLuX4vnJ47p3SiNI,107839
6
6
  clonebox/container.py,sha256=tiYK1ZB-DhdD6A2FuMA0h_sRNkUI7KfYcJ0tFOcdyeM,6105
7
7
  clonebox/dashboard.py,sha256=dMY6odvPq3j6FronhRRsX7aY3qdCwznB-aCWKEmHDNw,5768
8
8
  clonebox/detector.py,sha256=vS65cvFNPmUBCX1Y_TMTnSRljw6r1Ae9dlVtACs5XFc,23075
@@ -20,7 +20,7 @@ clonebox/resource_monitor.py,sha256=lDR9KyPbVtImeeOkOBPPVP-5yCgoL5hsVFPZ_UqsY0w,
20
20
  clonebox/resources.py,sha256=IkuM4OdSDV4qhyc0eIynwbAHBTv0aVSxxW-gghsnCAs,6815
21
21
  clonebox/rollback.py,sha256=hpwO-8Ehe1pW0wHuZvJkC_qxZ6yEo9otCJRhGIUArCo,5711
22
22
  clonebox/secrets.py,sha256=l1jwJcEPB1qMoGNLPjyrkKKr1khh9VmftFJI9BWhgK0,10628
23
- clonebox/validator.py,sha256=uXBkdrWSk8oBhRnD1y4mwxRdxOPduRraroSIs14bAo8,45342
23
+ clonebox/validator.py,sha256=PWPgkR9cjewTh6-uW_2YyjxuEj2wI9O7h7hk7NK8nAI,51181
24
24
  clonebox/backends/libvirt_backend.py,sha256=sIHFIvFO1hIOXEFR_foSkOGBgIzaJVQs-njOU8GdafA,7170
25
25
  clonebox/backends/qemu_disk.py,sha256=YsGjYX5sbEf35Y4yjTpNkZat73a4RGBxY-KTVzJhqIs,1687
26
26
  clonebox/backends/subprocess_runner.py,sha256=c-IyaMxM1cmUu64h654oAvulm83K5Mu-VQxXJ_0BOds,1506
@@ -40,9 +40,9 @@ clonebox/snapshots/manager.py,sha256=hGzM8V6ZJPXjTqj47c4Kr8idlE-c1Q3gPUvuw1HvS1A
40
40
  clonebox/snapshots/models.py,sha256=sRnn3OZE8JG9FZJlRuA3ihO-JXoPCQ3nD3SQytflAao,6206
41
41
  clonebox/templates/profiles/ml-dev.yaml,sha256=w07MToGh31xtxpjbeXTBk9BkpAN8A3gv8HeA3ESKG9M,461
42
42
  clonebox/templates/profiles/web-stack.yaml,sha256=EBnnGMzML5vAjXmIUbCpbTCwmRaNJiuWd3EcL43DOK8,485
43
- clonebox-1.1.17.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
44
- clonebox-1.1.17.dist-info/METADATA,sha256=Ns1tp6tNWuGaJWdTEi0BE4MjR7XCmb2LU5n1OZYqeSw,49052
45
- clonebox-1.1.17.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
46
- clonebox-1.1.17.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
47
- clonebox-1.1.17.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
48
- clonebox-1.1.17.dist-info/RECORD,,
43
+ clonebox-1.1.18.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
44
+ clonebox-1.1.18.dist-info/METADATA,sha256=n1VJE8-Mk-zf9QgHBbvt1Z5fbJMdpD1vjZVVjEBeaxE,49052
45
+ clonebox-1.1.18.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
46
+ clonebox-1.1.18.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
47
+ clonebox-1.1.18.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
48
+ clonebox-1.1.18.dist-info/RECORD,,