clonebox 0.1.22__py3-none-any.whl → 0.1.23__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/cloner.py CHANGED
@@ -3,6 +3,7 @@
3
3
  SelectiveVMCloner - Creates isolated VMs with only selected apps/paths/services.
4
4
  """
5
5
 
6
+ import json
6
7
  import os
7
8
  import subprocess
8
9
  import tempfile
@@ -13,6 +14,12 @@ from dataclasses import dataclass, field
13
14
  from pathlib import Path
14
15
  from typing import Optional
15
16
 
17
+ try:
18
+ from dotenv import load_dotenv
19
+ load_dotenv()
20
+ except ImportError:
21
+ pass # dotenv is optional
22
+
16
23
  try:
17
24
  import libvirt
18
25
  except ImportError:
@@ -33,21 +40,23 @@ DEFAULT_SNAP_INTERFACES = ['desktop', 'desktop-legacy', 'x11', 'home', 'network'
33
40
  class VMConfig:
34
41
  """Configuration for the VM to create."""
35
42
 
36
- name: str = "clonebox-vm"
37
- ram_mb: int = 4096
38
- vcpus: int = 4
39
- disk_size_gb: int = 20
40
- gui: bool = True
41
- base_image: Optional[str] = None
43
+ name: str = field(default_factory=lambda: os.getenv("VM_NAME", "clonebox-vm"))
44
+ ram_mb: int = field(default_factory=lambda: int(os.getenv("VM_RAM_MB", "8192")))
45
+ vcpus: int = field(default_factory=lambda: int(os.getenv("VM_VCPUS", "4")))
46
+ disk_size_gb: int = field(default_factory=lambda: int(os.getenv("VM_DISK_SIZE_GB", "20")))
47
+ gui: bool = field(default_factory=lambda: os.getenv("VM_GUI", "true").lower() == "true")
48
+ base_image: Optional[str] = field(default_factory=lambda: os.getenv("VM_BASE_IMAGE") or None)
42
49
  paths: dict = field(default_factory=dict)
43
50
  packages: list = field(default_factory=list)
44
51
  snap_packages: list = field(default_factory=list) # Snap packages to install
45
52
  services: list = field(default_factory=list)
46
53
  post_commands: list = field(default_factory=list) # Commands to run after setup
47
- user_session: bool = False # Use qemu:///session instead of qemu:///system
48
- network_mode: str = "auto" # auto|default|user
49
- username: str = "ubuntu" # VM default username
50
- password: str = "ubuntu" # VM default password
54
+ user_session: bool = field(default_factory=lambda: os.getenv("VM_USER_SESSION", "false").lower() == "true") # Use qemu:///session instead of qemu:///system
55
+ network_mode: str = field(default_factory=lambda: os.getenv("VM_NETWORK_MODE", "auto")) # auto|default|user
56
+ username: str = field(default_factory=lambda: os.getenv("VM_USERNAME", "ubuntu")) # VM default username
57
+ password: str = field(default_factory=lambda: os.getenv("VM_PASSWORD", "ubuntu")) # VM default password
58
+ autostart_apps: bool = field(default_factory=lambda: os.getenv("VM_AUTOSTART_APPS", "true").lower() == "true") # Auto-start GUI apps after login (desktop autostart)
59
+ web_services: list = field(default_factory=list) # Web services to start (uvicorn, etc.)
51
60
 
52
61
  def to_dict(self) -> dict:
53
62
  return {
@@ -63,15 +72,6 @@ class SelectiveVMCloner:
63
72
  Uses bind mounts instead of full disk cloning.
64
73
  """
65
74
 
66
- # Default images directories
67
- SYSTEM_IMAGES_DIR = Path("/var/lib/libvirt/images")
68
- USER_IMAGES_DIR = Path.home() / ".local/share/libvirt/images"
69
-
70
- DEFAULT_BASE_IMAGE_URL = (
71
- "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
72
- )
73
- DEFAULT_BASE_IMAGE_FILENAME = "clonebox-ubuntu-jammy-amd64.qcow2"
74
-
75
75
  def __init__(self, conn_uri: str = None, user_session: bool = False):
76
76
  self.user_session = user_session
77
77
  if conn_uri:
@@ -81,6 +81,28 @@ class SelectiveVMCloner:
81
81
  self.conn = None
82
82
  self._connect()
83
83
 
84
+ @property
85
+ def SYSTEM_IMAGES_DIR(self) -> Path:
86
+ return Path(os.getenv("CLONEBOX_SYSTEM_IMAGES_DIR", "/var/lib/libvirt/images"))
87
+
88
+ @property
89
+ def USER_IMAGES_DIR(self) -> Path:
90
+ return Path(os.getenv("CLONEBOX_USER_IMAGES_DIR", str(Path.home() / ".local/share/libvirt/images"))).expanduser()
91
+
92
+ @property
93
+ def DEFAULT_BASE_IMAGE_URL(self) -> str:
94
+ return os.getenv(
95
+ "CLONEBOX_BASE_IMAGE_URL",
96
+ "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
97
+ )
98
+
99
+ @property
100
+ def DEFAULT_BASE_IMAGE_FILENAME(self) -> str:
101
+ return os.getenv(
102
+ "CLONEBOX_BASE_IMAGE_FILENAME",
103
+ "clonebox-ubuntu-jammy-amd64.qcow2"
104
+ )
105
+
84
106
  def _connect(self):
85
107
  """Connect to libvirt."""
86
108
  if libvirt is None:
@@ -599,8 +621,48 @@ write_status "starting" "boot diagnostic starting"
599
621
  APT_PACKAGES=({apt_packages})
600
622
  SNAP_PACKAGES=({snap_packages})
601
623
  SERVICES=({services})
624
+ VM_USER="${{SUDO_USER:-ubuntu}}"
625
+ VM_HOME="/home/$VM_USER"
626
+
627
+ # ═══════════════════════════════════════════════════════════════════════════════
628
+ # Section 0: Fix permissions for GNOME directories (runs first!)
629
+ # ═══════════════════════════════════════════════════════════════════════════════
630
+ section "0/7" "Fixing directory permissions..."
631
+ write_status "fixing_permissions" "fixing directory permissions"
632
+
633
+ GNOME_DIRS=(
634
+ "$VM_HOME/.config"
635
+ "$VM_HOME/.config/pulse"
636
+ "$VM_HOME/.config/dconf"
637
+ "$VM_HOME/.config/ibus"
638
+ "$VM_HOME/.cache"
639
+ "$VM_HOME/.cache/ibus"
640
+ "$VM_HOME/.cache/tracker3"
641
+ "$VM_HOME/.cache/mesa_shader_cache"
642
+ "$VM_HOME/.local"
643
+ "$VM_HOME/.local/share"
644
+ "$VM_HOME/.local/share/applications"
645
+ "$VM_HOME/.local/share/keyrings"
646
+ )
647
+
648
+ for dir in "${{GNOME_DIRS[@]}}"; do
649
+ if [ ! -d "$dir" ]; then
650
+ mkdir -p "$dir" 2>/dev/null && log " Created $dir" || true
651
+ fi
652
+ done
653
+
654
+ # Fix ownership for all critical directories
655
+ chown -R 1000:1000 "$VM_HOME/.config" "$VM_HOME/.cache" "$VM_HOME/.local" 2>/dev/null || true
656
+ chmod 700 "$VM_HOME/.config" "$VM_HOME/.cache" 2>/dev/null || true
602
657
 
603
- section "1/5" "Checking APT packages..."
658
+ # Fix snap directories ownership
659
+ for snap_dir in "$VM_HOME/snap"/*; do
660
+ [ -d "$snap_dir" ] && chown -R 1000:1000 "$snap_dir" 2>/dev/null || true
661
+ done
662
+
663
+ ok "Directory permissions fixed"
664
+
665
+ section "1/7" "Checking APT packages..."
604
666
  write_status "checking_apt" "checking APT packages"
605
667
  for pkg in "${{APT_PACKAGES[@]}}"; do
606
668
  [ -z "$pkg" ] && continue
@@ -617,7 +679,7 @@ for pkg in "${{APT_PACKAGES[@]}}"; do
617
679
  fi
618
680
  done
619
681
 
620
- section "2/5" "Checking Snap packages..."
682
+ section "2/7" "Checking Snap packages..."
621
683
  write_status "checking_snaps" "checking snap packages"
622
684
  timeout 120 snap wait system seed.loaded 2>/dev/null || true
623
685
  for pkg in "${{SNAP_PACKAGES[@]}}"; do
@@ -635,7 +697,7 @@ for pkg in "${{SNAP_PACKAGES[@]}}"; do
635
697
  fi
636
698
  done
637
699
 
638
- section "3/5" "Connecting Snap interfaces..."
700
+ section "3/7" "Connecting Snap interfaces..."
639
701
  write_status "connecting_interfaces" "connecting snap interfaces"
640
702
  for pkg in "${{SNAP_PACKAGES[@]}}"; do
641
703
  [ -z "$pkg" ] && continue
@@ -643,7 +705,7 @@ for pkg in "${{SNAP_PACKAGES[@]}}"; do
643
705
  done
644
706
  systemctl restart snapd 2>/dev/null || true
645
707
 
646
- section "4/5" "Testing application launch..."
708
+ section "4/7" "Testing application launch..."
647
709
  write_status "testing_launch" "testing application launch"
648
710
  APPS_TO_TEST=()
649
711
  for pkg in "${{SNAP_PACKAGES[@]}}"; do
@@ -692,7 +754,7 @@ for app in "${{APPS_TO_TEST[@]}}"; do
692
754
  fi
693
755
  done
694
756
 
695
- section "5/6" "Checking mount points..."
757
+ section "5/7" "Checking mount points..."
696
758
  write_status "checking_mounts" "checking mount points"
697
759
  while IFS= read -r line; do
698
760
  tag=$(echo "$line" | awk '{{print $1}}')
@@ -713,7 +775,7 @@ while IFS= read -r line; do
713
775
  fi
714
776
  done < /etc/fstab
715
777
 
716
- section "6/6" "Checking services..."
778
+ section "6/7" "Checking services..."
717
779
  write_status "checking_services" "checking services"
718
780
  for svc in "${{SERVICES[@]}}"; do
719
781
  [ -z "$svc" ] && continue
@@ -1003,10 +1065,60 @@ fi
1003
1065
 
1004
1066
  # Add GUI setup if enabled - runs AFTER package installation completes
1005
1067
  if config.gui:
1068
+ # Create directories that GNOME services need BEFORE GUI starts
1069
+ # These may conflict with mounted host directories, so ensure they exist with correct perms
1006
1070
  runcmd_lines.extend([
1071
+ " - mkdir -p /home/ubuntu/.config/pulse /home/ubuntu/.cache/ibus /home/ubuntu/.local/share",
1072
+ " - mkdir -p /home/ubuntu/.config/dconf /home/ubuntu/.cache/tracker3",
1073
+ " - mkdir -p /home/ubuntu/.config/autostart",
1074
+ " - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
1075
+ " - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
1007
1076
  " - systemctl set-default graphical.target",
1008
1077
  " - systemctl enable gdm3 || systemctl enable gdm || true",
1009
1078
  ])
1079
+
1080
+ # Create autostart entries for GUI apps
1081
+ autostart_apps = {
1082
+ 'pycharm-community': ('PyCharm Community', '/snap/bin/pycharm-community', 'pycharm-community'),
1083
+ 'firefox': ('Firefox', '/snap/bin/firefox', 'firefox'),
1084
+ 'chromium': ('Chromium', '/snap/bin/chromium', 'chromium'),
1085
+ 'google-chrome': ('Google Chrome', 'google-chrome-stable', 'google-chrome'),
1086
+ }
1087
+
1088
+ for snap_pkg in config.snap_packages:
1089
+ if snap_pkg in autostart_apps:
1090
+ name, exec_cmd, icon = autostart_apps[snap_pkg]
1091
+ desktop_entry = f'''[Desktop Entry]
1092
+ Type=Application
1093
+ Name={name}
1094
+ Exec={exec_cmd}
1095
+ Icon={icon}
1096
+ X-GNOME-Autostart-enabled=true
1097
+ X-GNOME-Autostart-Delay=5
1098
+ Comment=CloneBox autostart
1099
+ '''
1100
+ import base64
1101
+ desktop_b64 = base64.b64encode(desktop_entry.encode()).decode()
1102
+ runcmd_lines.append(f" - echo '{desktop_b64}' | base64 -d > /home/ubuntu/.config/autostart/{snap_pkg}.desktop")
1103
+
1104
+ # Check if google-chrome is in paths (app_data_paths)
1105
+ wants_chrome = any('/google-chrome' in str(p) for p in (config.paths or {}).values())
1106
+ if wants_chrome:
1107
+ name, exec_cmd, icon = autostart_apps['google-chrome']
1108
+ desktop_entry = f'''[Desktop Entry]
1109
+ Type=Application
1110
+ Name={name}
1111
+ Exec={exec_cmd}
1112
+ Icon={icon}
1113
+ X-GNOME-Autostart-enabled=true
1114
+ X-GNOME-Autostart-Delay=5
1115
+ Comment=CloneBox autostart
1116
+ '''
1117
+ desktop_b64 = base64.b64encode(desktop_entry.encode()).decode()
1118
+ runcmd_lines.append(f" - echo '{desktop_b64}' | base64 -d > /home/ubuntu/.config/autostart/google-chrome.desktop")
1119
+
1120
+ # Fix ownership of autostart directory
1121
+ runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/.config/autostart")
1010
1122
 
1011
1123
  # Run user-defined post commands
1012
1124
  if config.post_commands:
@@ -1076,6 +1188,570 @@ echo ""'''
1076
1188
  runcmd_lines.append(f" - echo '{motd_b64}' | base64 -d > /etc/update-motd.d/99-clonebox")
1077
1189
  runcmd_lines.append(" - chmod +x /etc/update-motd.d/99-clonebox")
1078
1190
 
1191
+ # Create user-friendly clonebox-repair script
1192
+ repair_script = r'''#!/bin/bash
1193
+ # CloneBox Repair - User-friendly repair utility for CloneBox VMs
1194
+ # Usage: clonebox-repair [--auto|--status|--logs|--help]
1195
+
1196
+ set -uo pipefail
1197
+
1198
+ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' BOLD='\033[1m'
1199
+
1200
+ show_help() {
1201
+ echo -e "${BOLD}${CYAN}CloneBox Repair Utility${NC}"
1202
+ echo ""
1203
+ echo "Usage: clonebox-repair [OPTION]"
1204
+ echo ""
1205
+ echo "Options:"
1206
+ echo " --auto Run full automatic repair (same as boot diagnostic)"
1207
+ echo " --status Show current CloneBox status"
1208
+ echo " --logs Show recent repair logs"
1209
+ echo " --perms Fix directory permissions only"
1210
+ echo " --audio Fix audio (PulseAudio) and restart"
1211
+ echo " --keyring Reset GNOME Keyring (fixes password mismatch)"
1212
+ echo " --snaps Reconnect all snap interfaces only"
1213
+ echo " --mounts Remount all 9p filesystems only"
1214
+ echo " --all Run all fixes (perms + audio + snaps + mounts)"
1215
+ echo " --help Show this help message"
1216
+ echo ""
1217
+ echo "Without options, shows interactive menu."
1218
+ }
1219
+
1220
+ show_status() {
1221
+ echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════════${NC}"
1222
+ echo -e "${BOLD}${CYAN} CloneBox VM Status${NC}"
1223
+ echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════════${NC}"
1224
+
1225
+ if [ -f /var/run/clonebox-status ]; then
1226
+ source /var/run/clonebox-status
1227
+ if [ "${failed:-0}" -eq 0 ]; then
1228
+ echo -e " ${GREEN}✅ All systems operational${NC}"
1229
+ else
1230
+ echo -e " ${RED}⚠️ $failed checks failed${NC}"
1231
+ fi
1232
+ echo -e " Passed: ${passed:-0} | Repaired: ${repaired:-0} | Failed: ${failed:-0}"
1233
+ else
1234
+ echo -e " ${YELLOW}No status information available${NC}"
1235
+ fi
1236
+ echo ""
1237
+ echo -e " Last boot diagnostic: $(stat -c %y /var/log/clonebox-boot.log 2>/dev/null || echo 'never')"
1238
+ echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════════${NC}"
1239
+ }
1240
+
1241
+ show_logs() {
1242
+ echo -e "${BOLD}Recent repair logs:${NC}"
1243
+ echo ""
1244
+ tail -n 50 /var/log/clonebox-boot.log 2>/dev/null || echo "No logs found"
1245
+ }
1246
+
1247
+ fix_permissions() {
1248
+ echo -e "${CYAN}Fixing directory permissions...${NC}"
1249
+ VM_USER="${SUDO_USER:-ubuntu}"
1250
+ VM_HOME="/home/$VM_USER"
1251
+
1252
+ DIRS_TO_CREATE=(
1253
+ "$VM_HOME/.config"
1254
+ "$VM_HOME/.config/pulse"
1255
+ "$VM_HOME/.config/dconf"
1256
+ "$VM_HOME/.config/ibus"
1257
+ "$VM_HOME/.cache"
1258
+ "$VM_HOME/.cache/ibus"
1259
+ "$VM_HOME/.cache/tracker3"
1260
+ "$VM_HOME/.cache/mesa_shader_cache"
1261
+ "$VM_HOME/.local"
1262
+ "$VM_HOME/.local/share"
1263
+ "$VM_HOME/.local/share/applications"
1264
+ "$VM_HOME/.local/share/keyrings"
1265
+ )
1266
+
1267
+ for dir in "${DIRS_TO_CREATE[@]}"; do
1268
+ if [ ! -d "$dir" ]; then
1269
+ mkdir -p "$dir" 2>/dev/null && echo " Created $dir"
1270
+ fi
1271
+ done
1272
+
1273
+ chown -R 1000:1000 "$VM_HOME/.config" "$VM_HOME/.cache" "$VM_HOME/.local" 2>/dev/null
1274
+ chmod 700 "$VM_HOME/.config" "$VM_HOME/.cache" 2>/dev/null
1275
+
1276
+ for snap_dir in "$VM_HOME/snap"/*; do
1277
+ [ -d "$snap_dir" ] && chown -R 1000:1000 "$snap_dir" 2>/dev/null
1278
+ done
1279
+
1280
+ echo -e "${GREEN}✅ Permissions fixed${NC}"
1281
+ }
1282
+
1283
+ fix_audio() {
1284
+ echo -e "${CYAN}Fixing audio (PulseAudio/PipeWire)...${NC}"
1285
+ VM_USER="${SUDO_USER:-ubuntu}"
1286
+ VM_HOME="/home/$VM_USER"
1287
+
1288
+ # Create pulse config directory with correct permissions
1289
+ mkdir -p "$VM_HOME/.config/pulse" 2>/dev/null
1290
+ chown -R 1000:1000 "$VM_HOME/.config/pulse" 2>/dev/null
1291
+ chmod 700 "$VM_HOME/.config/pulse" 2>/dev/null
1292
+
1293
+ # Kill and restart audio services as user
1294
+ if [ -n "$SUDO_USER" ]; then
1295
+ sudo -u "$SUDO_USER" pulseaudio --kill 2>/dev/null || true
1296
+ sleep 1
1297
+ sudo -u "$SUDO_USER" pulseaudio --start 2>/dev/null || true
1298
+ echo " Restarted PulseAudio for $SUDO_USER"
1299
+ else
1300
+ pulseaudio --kill 2>/dev/null || true
1301
+ sleep 1
1302
+ pulseaudio --start 2>/dev/null || true
1303
+ echo " Restarted PulseAudio"
1304
+ fi
1305
+
1306
+ # Restart pipewire if available
1307
+ systemctl --user restart pipewire pipewire-pulse 2>/dev/null || true
1308
+
1309
+ echo -e "${GREEN}✅ Audio fixed${NC}"
1310
+ }
1311
+
1312
+ fix_keyring() {
1313
+ echo -e "${CYAN}Resetting GNOME Keyring...${NC}"
1314
+ VM_USER="${SUDO_USER:-ubuntu}"
1315
+ VM_HOME="/home/$VM_USER"
1316
+ KEYRING_DIR="$VM_HOME/.local/share/keyrings"
1317
+
1318
+ echo -e "${YELLOW}⚠️ This will delete existing keyrings and create a new one on next login${NC}"
1319
+ echo -e "${YELLOW} Stored passwords (WiFi, Chrome, etc.) will be lost!${NC}"
1320
+
1321
+ if [ -t 0 ]; then
1322
+ read -rp "Continue? [y/N] " confirm
1323
+ [[ "$confirm" != [yY]* ]] && { echo "Cancelled"; return; }
1324
+ fi
1325
+
1326
+ # Backup old keyrings
1327
+ if [ -d "$KEYRING_DIR" ] && [ "$(ls -A "$KEYRING_DIR" 2>/dev/null)" ]; then
1328
+ backup_dir="$VM_HOME/.local/share/keyrings.backup.$(date +%Y%m%d%H%M%S)"
1329
+ mv "$KEYRING_DIR" "$backup_dir" 2>/dev/null
1330
+ echo " Backed up to $backup_dir"
1331
+ fi
1332
+
1333
+ # Create fresh keyring directory
1334
+ mkdir -p "$KEYRING_DIR" 2>/dev/null
1335
+ chown -R 1000:1000 "$KEYRING_DIR" 2>/dev/null
1336
+ chmod 700 "$KEYRING_DIR" 2>/dev/null
1337
+
1338
+ # Kill gnome-keyring-daemon to force restart on next login
1339
+ pkill -u "$VM_USER" gnome-keyring-daemon 2>/dev/null || true
1340
+
1341
+ echo -e "${GREEN}✅ Keyring reset - log out and back in to create new keyring${NC}"
1342
+ }
1343
+
1344
+ fix_ibus() {
1345
+ echo -e "${CYAN}Fixing IBus input method...${NC}"
1346
+ VM_USER="${SUDO_USER:-ubuntu}"
1347
+ VM_HOME="/home/$VM_USER"
1348
+
1349
+ # Create ibus cache directory
1350
+ mkdir -p "$VM_HOME/.cache/ibus" 2>/dev/null
1351
+ chown -R 1000:1000 "$VM_HOME/.cache/ibus" 2>/dev/null
1352
+ chmod 700 "$VM_HOME/.cache/ibus" 2>/dev/null
1353
+
1354
+ # Restart ibus
1355
+ if [ -n "$SUDO_USER" ]; then
1356
+ sudo -u "$SUDO_USER" ibus restart 2>/dev/null || true
1357
+ else
1358
+ ibus restart 2>/dev/null || true
1359
+ fi
1360
+
1361
+ echo -e "${GREEN}✅ IBus fixed${NC}"
1362
+ }
1363
+
1364
+ fix_snaps() {
1365
+ echo -e "${CYAN}Reconnecting snap interfaces...${NC}"
1366
+ IFACES="desktop desktop-legacy x11 wayland home network audio-playback audio-record camera opengl"
1367
+
1368
+ for snap in $(snap list --color=never 2>/dev/null | tail -n +2 | awk '{print $1}'); do
1369
+ [[ "$snap" =~ ^(core|snapd|gnome-|gtk-|mesa-) ]] && continue
1370
+ echo -e " ${YELLOW}$snap${NC}"
1371
+ for iface in $IFACES; do
1372
+ snap connect "$snap:$iface" ":$iface" 2>/dev/null && echo " ✓ $iface" || true
1373
+ done
1374
+ done
1375
+
1376
+ systemctl restart snapd 2>/dev/null || true
1377
+ echo -e "${GREEN}✅ Snap interfaces reconnected${NC}"
1378
+ }
1379
+
1380
+ fix_mounts() {
1381
+ echo -e "${CYAN}Remounting filesystems...${NC}"
1382
+
1383
+ while IFS= read -r line; do
1384
+ tag=$(echo "$line" | awk '{print $1}')
1385
+ mp=$(echo "$line" | awk '{print $2}')
1386
+ if [[ "$tag" =~ ^mount[0-9]+$ ]] && [[ "$mp" == /* ]]; then
1387
+ if ! mountpoint -q "$mp" 2>/dev/null; then
1388
+ mkdir -p "$mp" 2>/dev/null
1389
+ if mount "$mp" 2>/dev/null; then
1390
+ echo -e " ${GREEN}✓${NC} $mp"
1391
+ else
1392
+ echo -e " ${RED}✗${NC} $mp (failed)"
1393
+ fi
1394
+ else
1395
+ echo -e " ${GREEN}✓${NC} $mp (already mounted)"
1396
+ fi
1397
+ fi
1398
+ done < /etc/fstab
1399
+
1400
+ echo -e "${GREEN}✅ Mounts checked${NC}"
1401
+ }
1402
+
1403
+ fix_all() {
1404
+ echo -e "${BOLD}${CYAN}Running all fixes...${NC}"
1405
+ echo ""
1406
+ fix_permissions
1407
+ echo ""
1408
+ fix_audio
1409
+ echo ""
1410
+ fix_ibus
1411
+ echo ""
1412
+ fix_snaps
1413
+ echo ""
1414
+ fix_mounts
1415
+ echo ""
1416
+ echo -e "${BOLD}${GREEN}All fixes completed!${NC}"
1417
+ }
1418
+
1419
+ interactive_menu() {
1420
+ while true; do
1421
+ echo ""
1422
+ echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════════${NC}"
1423
+ echo -e "${BOLD}${CYAN} CloneBox Repair Menu${NC}"
1424
+ echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════════${NC}"
1425
+ echo ""
1426
+ echo " 1) Run full automatic repair (boot diagnostic)"
1427
+ echo " 2) Run all quick fixes (perms + audio + snaps + mounts)"
1428
+ echo " 3) Fix permissions only"
1429
+ echo " 4) Fix audio (PulseAudio) only"
1430
+ echo " 5) Reset GNOME Keyring (⚠️ deletes saved passwords)"
1431
+ echo " 6) Reconnect snap interfaces only"
1432
+ echo " 7) Remount filesystems only"
1433
+ echo " 8) Show status"
1434
+ echo " 9) Show logs"
1435
+ echo " q) Quit"
1436
+ echo ""
1437
+ read -rp "Select option: " choice
1438
+
1439
+ case "$choice" in
1440
+ 1) sudo /usr/local/bin/clonebox-boot-diagnostic ;;
1441
+ 2) fix_all ;;
1442
+ 3) fix_permissions ;;
1443
+ 4) fix_audio ;;
1444
+ 5) fix_keyring ;;
1445
+ 6) fix_snaps ;;
1446
+ 7) fix_mounts ;;
1447
+ 8) show_status ;;
1448
+ 9) show_logs ;;
1449
+ q|Q) exit 0 ;;
1450
+ *) echo -e "${RED}Invalid option${NC}" ;;
1451
+ esac
1452
+ done
1453
+ }
1454
+
1455
+ # Main
1456
+ case "${1:-}" in
1457
+ --auto) exec sudo /usr/local/bin/clonebox-boot-diagnostic ;;
1458
+ --all) fix_all ;;
1459
+ --status) show_status ;;
1460
+ --logs) show_logs ;;
1461
+ --perms) fix_permissions ;;
1462
+ --audio) fix_audio ;;
1463
+ --keyring) fix_keyring ;;
1464
+ --snaps) fix_snaps ;;
1465
+ --mounts) fix_mounts ;;
1466
+ --help|-h) show_help ;;
1467
+ "") interactive_menu ;;
1468
+ *) show_help; exit 1 ;;
1469
+ esac
1470
+ '''
1471
+ repair_b64 = base64.b64encode(repair_script.encode()).decode()
1472
+ runcmd_lines.append(f" - echo '{repair_b64}' | base64 -d > /usr/local/bin/clonebox-repair")
1473
+ runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-repair")
1474
+ runcmd_lines.append(" - ln -sf /usr/local/bin/clonebox-repair /usr/local/bin/cb-repair")
1475
+
1476
+ # === AUTOSTART: Systemd user services + Desktop autostart files ===
1477
+ # Create directories for user systemd services and autostart
1478
+ runcmd_lines.append(f" - mkdir -p /home/{config.username}/.config/systemd/user")
1479
+ runcmd_lines.append(f" - mkdir -p /home/{config.username}/.config/autostart")
1480
+
1481
+ # Enable lingering for the user (allows user services to run without login)
1482
+ runcmd_lines.append(f" - loginctl enable-linger {config.username}")
1483
+
1484
+ # Generate autostart configurations based on installed apps (if enabled)
1485
+ autostart_apps = []
1486
+
1487
+ if getattr(config, 'autostart_apps', True):
1488
+ # Detect apps from snap_packages
1489
+ for snap_pkg in (config.snap_packages or []):
1490
+ if snap_pkg == "pycharm-community":
1491
+ autostart_apps.append({
1492
+ "name": "pycharm-community",
1493
+ "display_name": "PyCharm Community",
1494
+ "exec": "/snap/bin/pycharm-community %U",
1495
+ "type": "snap",
1496
+ "after": "graphical-session.target",
1497
+ })
1498
+ elif snap_pkg == "chromium":
1499
+ autostart_apps.append({
1500
+ "name": "chromium",
1501
+ "display_name": "Chromium Browser",
1502
+ "exec": "/snap/bin/chromium %U",
1503
+ "type": "snap",
1504
+ "after": "graphical-session.target",
1505
+ })
1506
+ elif snap_pkg == "firefox":
1507
+ autostart_apps.append({
1508
+ "name": "firefox",
1509
+ "display_name": "Firefox",
1510
+ "exec": "/snap/bin/firefox %U",
1511
+ "type": "snap",
1512
+ "after": "graphical-session.target",
1513
+ })
1514
+ elif snap_pkg == "code":
1515
+ autostart_apps.append({
1516
+ "name": "code",
1517
+ "display_name": "Visual Studio Code",
1518
+ "exec": "/snap/bin/code --new-window",
1519
+ "type": "snap",
1520
+ "after": "graphical-session.target",
1521
+ })
1522
+
1523
+ # Detect apps from packages (APT)
1524
+ for apt_pkg in (config.packages or []):
1525
+ if apt_pkg == "firefox":
1526
+ # Only add if not already added from snap
1527
+ if not any(a["name"] == "firefox" for a in autostart_apps):
1528
+ autostart_apps.append({
1529
+ "name": "firefox",
1530
+ "display_name": "Firefox",
1531
+ "exec": "/usr/bin/firefox %U",
1532
+ "type": "apt",
1533
+ "after": "graphical-session.target",
1534
+ })
1535
+
1536
+ # Check for google-chrome from app_data_paths
1537
+ for host_path, guest_path in (config.paths or {}).items():
1538
+ if guest_path == "/home/ubuntu/.config/google-chrome":
1539
+ autostart_apps.append({
1540
+ "name": "google-chrome",
1541
+ "display_name": "Google Chrome",
1542
+ "exec": "/usr/bin/google-chrome-stable %U",
1543
+ "type": "deb",
1544
+ "after": "graphical-session.target",
1545
+ })
1546
+ break
1547
+
1548
+ # Generate systemd user services for each app
1549
+ for app in autostart_apps:
1550
+ service_content = f'''[Unit]
1551
+ Description={app["display_name"]} Autostart
1552
+ After={app["after"]}
1553
+
1554
+ [Service]
1555
+ Type=simple
1556
+ Environment=DISPLAY=:0
1557
+ Environment=XDG_RUNTIME_DIR=/run/user/1000
1558
+ ExecStart={app["exec"]}
1559
+ Restart=on-failure
1560
+ RestartSec=5
1561
+
1562
+ [Install]
1563
+ WantedBy=default.target
1564
+ '''
1565
+ service_b64 = base64.b64encode(service_content.encode()).decode()
1566
+ service_path = f"/home/{config.username}/.config/systemd/user/{app['name']}.service"
1567
+ runcmd_lines.append(f" - echo '{service_b64}' | base64 -d > {service_path}")
1568
+
1569
+ # Generate desktop autostart files for GUI apps (alternative to systemd user services)
1570
+ for app in autostart_apps:
1571
+ desktop_content = f'''[Desktop Entry]
1572
+ Type=Application
1573
+ Name={app["display_name"]}
1574
+ Exec={app["exec"]}
1575
+ Hidden=false
1576
+ NoDisplay=false
1577
+ X-GNOME-Autostart-enabled=true
1578
+ X-GNOME-Autostart-Delay=5
1579
+ '''
1580
+ desktop_b64 = base64.b64encode(desktop_content.encode()).decode()
1581
+ desktop_path = f"/home/{config.username}/.config/autostart/{app['name']}.desktop"
1582
+ runcmd_lines.append(f" - echo '{desktop_b64}' | base64 -d > {desktop_path}")
1583
+
1584
+ # Fix ownership of all autostart files
1585
+ runcmd_lines.append(f" - chown -R 1000:1000 /home/{config.username}/.config/systemd")
1586
+ runcmd_lines.append(f" - chown -R 1000:1000 /home/{config.username}/.config/autostart")
1587
+
1588
+ # Enable systemd user services (must run as user)
1589
+ if autostart_apps:
1590
+ services_to_enable = " ".join(f"{app['name']}.service" for app in autostart_apps)
1591
+ runcmd_lines.append(f" - sudo -u {config.username} XDG_RUNTIME_DIR=/run/user/1000 systemctl --user daemon-reload || true")
1592
+ # Note: We don't enable services by default as desktop autostart is more reliable for GUI apps
1593
+ # User can enable them manually with: systemctl --user enable <service>
1594
+
1595
+ # === WEB SERVICES: System-wide services for uvicorn, nginx, etc. ===
1596
+ web_services = getattr(config, 'web_services', []) or []
1597
+ for svc in web_services:
1598
+ svc_name = svc.get("name", "clonebox-web")
1599
+ svc_desc = svc.get("description", f"CloneBox {svc_name}")
1600
+ svc_workdir = svc.get("workdir", "/mnt/project0")
1601
+ svc_exec = svc.get("exec", "uvicorn app:app --host 0.0.0.0 --port 8000")
1602
+ svc_user = svc.get("user", config.username)
1603
+ svc_after = svc.get("after", "network.target")
1604
+ svc_env = svc.get("environment", [])
1605
+
1606
+ env_lines = "\n".join(f"Environment={e}" for e in svc_env) if svc_env else ""
1607
+
1608
+ web_service_content = f'''[Unit]
1609
+ Description={svc_desc}
1610
+ After={svc_after}
1611
+
1612
+ [Service]
1613
+ Type=simple
1614
+ User={svc_user}
1615
+ WorkingDirectory={svc_workdir}
1616
+ {env_lines}
1617
+ ExecStart={svc_exec}
1618
+ Restart=always
1619
+ RestartSec=10
1620
+
1621
+ [Install]
1622
+ WantedBy=multi-user.target
1623
+ '''
1624
+ web_svc_b64 = base64.b64encode(web_service_content.encode()).decode()
1625
+ runcmd_lines.append(f" - echo '{web_svc_b64}' | base64 -d > /etc/systemd/system/{svc_name}.service")
1626
+ runcmd_lines.append(" - systemctl daemon-reload")
1627
+ runcmd_lines.append(f" - systemctl enable {svc_name}.service")
1628
+ runcmd_lines.append(f" - systemctl start {svc_name}.service || true")
1629
+
1630
+ # Create Python monitor service for continuous diagnostics
1631
+ monitor_script = f'''#!/usr/bin/env python3
1632
+ """CloneBox Monitor - Continuous diagnostics and app restart service."""
1633
+ import subprocess
1634
+ import time
1635
+ import os
1636
+ import sys
1637
+ import json
1638
+ from pathlib import Path
1639
+
1640
+ REQUIRED_APPS = {json.dumps([app["name"] for app in autostart_apps])}
1641
+ CHECK_INTERVAL = 60 # seconds
1642
+ LOG_FILE = "/var/log/clonebox-monitor.log"
1643
+ STATUS_FILE = "/var/run/clonebox-monitor-status.json"
1644
+
1645
+ def log(msg):
1646
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
1647
+ line = f"[{{timestamp}}] {{msg}}"
1648
+ print(line)
1649
+ try:
1650
+ with open(LOG_FILE, "a") as f:
1651
+ f.write(line + "\\n")
1652
+ except:
1653
+ pass
1654
+
1655
+ def get_running_processes():
1656
+ try:
1657
+ result = subprocess.run(["ps", "aux"], capture_output=True, text=True, timeout=10)
1658
+ return result.stdout
1659
+ except:
1660
+ return ""
1661
+
1662
+ def is_app_running(app_name, ps_output):
1663
+ patterns = {{
1664
+ "pycharm-community": ["pycharm", "idea"],
1665
+ "chromium": ["chromium"],
1666
+ "firefox": ["firefox", "firefox-esr"],
1667
+ "google-chrome": ["chrome", "google-chrome"],
1668
+ "code": ["code", "vscode"],
1669
+ }}
1670
+ for pattern in patterns.get(app_name, [app_name]):
1671
+ if pattern.lower() in ps_output.lower():
1672
+ return True
1673
+ return False
1674
+
1675
+ def restart_app(app_name):
1676
+ log(f"Restarting {{app_name}}...")
1677
+ try:
1678
+ subprocess.run(
1679
+ ["sudo", "-u", "{config.username}", "systemctl", "--user", "restart", f"{{app_name}}.service"],
1680
+ timeout=30, capture_output=True
1681
+ )
1682
+ return True
1683
+ except Exception as e:
1684
+ log(f"Failed to restart {{app_name}}: {{e}}")
1685
+ return False
1686
+
1687
+ def check_mounts():
1688
+ try:
1689
+ with open("/etc/fstab", "r") as f:
1690
+ fstab = f.read()
1691
+ for line in fstab.split("\\n"):
1692
+ parts = line.split()
1693
+ if len(parts) >= 2 and parts[0].startswith("mount"):
1694
+ mp = parts[1]
1695
+ result = subprocess.run(["mountpoint", "-q", mp], capture_output=True)
1696
+ if result.returncode != 0:
1697
+ log(f"Mount {{mp}} not active, attempting remount...")
1698
+ subprocess.run(["mount", mp], capture_output=True)
1699
+ except Exception as e:
1700
+ log(f"Mount check failed: {{e}}")
1701
+
1702
+ def write_status(status):
1703
+ try:
1704
+ with open(STATUS_FILE, "w") as f:
1705
+ json.dump(status, f)
1706
+ except:
1707
+ pass
1708
+
1709
+ def main():
1710
+ log("CloneBox Monitor started")
1711
+
1712
+ while True:
1713
+ status = {{"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), "apps": {{}}, "mounts_ok": True}}
1714
+
1715
+ # Check mounts
1716
+ check_mounts()
1717
+
1718
+ # Check apps (only if GUI session is active)
1719
+ if os.path.exists("/run/user/1000"):
1720
+ ps_output = get_running_processes()
1721
+ for app in REQUIRED_APPS:
1722
+ running = is_app_running(app, ps_output)
1723
+ status["apps"][app] = "running" if running else "stopped"
1724
+ # Don't auto-restart apps - user may have closed them intentionally
1725
+
1726
+ write_status(status)
1727
+ time.sleep(CHECK_INTERVAL)
1728
+
1729
+ if __name__ == "__main__":
1730
+ main()
1731
+ '''
1732
+ monitor_b64 = base64.b64encode(monitor_script.encode()).decode()
1733
+ runcmd_lines.append(f" - echo '{monitor_b64}' | base64 -d > /usr/local/bin/clonebox-monitor")
1734
+ runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-monitor")
1735
+
1736
+ # Create systemd service for the Python monitor
1737
+ monitor_service = '''[Unit]
1738
+ Description=CloneBox Monitor Service
1739
+ After=network.target graphical.target
1740
+
1741
+ [Service]
1742
+ Type=simple
1743
+ ExecStart=/usr/bin/python3 /usr/local/bin/clonebox-monitor
1744
+ Restart=always
1745
+ RestartSec=30
1746
+
1747
+ [Install]
1748
+ WantedBy=multi-user.target'''
1749
+ monitor_svc_b64 = base64.b64encode(monitor_service.encode()).decode()
1750
+ runcmd_lines.append(f" - echo '{monitor_svc_b64}' | base64 -d > /etc/systemd/system/clonebox-monitor.service")
1751
+ runcmd_lines.append(" - systemctl daemon-reload")
1752
+ runcmd_lines.append(" - systemctl enable clonebox-monitor.service")
1753
+ runcmd_lines.append(" - systemctl start clonebox-monitor.service || true")
1754
+
1079
1755
  # Add reboot command at the end if GUI is enabled
1080
1756
  if config.gui:
1081
1757
  runcmd_lines.append(" - echo 'Rebooting in 10 seconds to start GUI...'")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 0.1.22
3
+ Version: 0.1.23
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
@@ -31,6 +31,7 @@ Requires-Dist: questionary>=2.0.0
31
31
  Requires-Dist: psutil>=5.9.0
32
32
  Requires-Dist: pyyaml>=6.0
33
33
  Requires-Dist: pydantic>=2.0.0
34
+ Requires-Dist: python-dotenv>=1.0.0
34
35
  Provides-Extra: dev
35
36
  Requires-Dist: pytest>=7.0.0; extra == "dev"
36
37
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -1,7 +1,7 @@
1
1
  clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
2
2
  clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
3
3
  clonebox/cli.py,sha256=vbJ65ShdXG1nGkQteCaFtDTas0L2RNV--aay2Qx-6F0,110765
4
- clonebox/cloner.py,sha256=dX6K56goT3qZD3GOYjZBuAPMrAI0PriyFJWsJpQvyKc,46320
4
+ clonebox/cloner.py,sha256=a9IaIxTb-CaomRGOGkX-0wppYPFmmMRYv3lFxpBit84,73528
5
5
  clonebox/container.py,sha256=tiYK1ZB-DhdD6A2FuMA0h_sRNkUI7KfYcJ0tFOcdyeM,6105
6
6
  clonebox/dashboard.py,sha256=RhSPvR6kWglqXeLkCWesBZQid7wv2WpJa6w78mXbPjY,4268
7
7
  clonebox/detector.py,sha256=aS_QlbG93-DE3hsjRt88E7O-PGC2TUBgUbP9wqT9g60,23221
@@ -9,9 +9,9 @@ clonebox/models.py,sha256=yBRUlJejpeJHZjvCYMGq1nXPFcmhLFxN-LqkEyveWsA,7913
9
9
  clonebox/profiles.py,sha256=VaKVuxCrgyMxx-8_WOTcw7E8irwGxUPhZHVY6RxYYiE,2034
10
10
  clonebox/validator.py,sha256=z4YuIgVnX6ZqfIdJtjKIFwZ-iWlRUnpX7gmWwq-Jr88,35352
11
11
  clonebox/templates/profiles/ml-dev.yaml,sha256=MT7Wu3xGBnYIsO5mzZ2GDI4AAEFGOroIx0eU3XjNARg,140
12
- clonebox-0.1.22.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
13
- clonebox-0.1.22.dist-info/METADATA,sha256=MuI44ArtnU0ql1rF99Hf_4frTRHe7_AikJK9w2jk6tI,41591
14
- clonebox-0.1.22.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
- clonebox-0.1.22.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
16
- clonebox-0.1.22.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
17
- clonebox-0.1.22.dist-info/RECORD,,
12
+ clonebox-0.1.23.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
13
+ clonebox-0.1.23.dist-info/METADATA,sha256=Yr9EuaWmioFVeAQty15ueglVX6Ae85265PgonQ5DSHs,41627
14
+ clonebox-0.1.23.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ clonebox-0.1.23.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
16
+ clonebox-0.1.23.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
17
+ clonebox-0.1.23.dist-info/RECORD,,