clonebox 0.1.22__tar.gz → 0.1.23__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.
- {clonebox-0.1.22/src/clonebox.egg-info → clonebox-0.1.23}/PKG-INFO +2 -1
- {clonebox-0.1.22 → clonebox-0.1.23}/pyproject.toml +2 -1
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/cloner.py +701 -25
- {clonebox-0.1.22 → clonebox-0.1.23/src/clonebox.egg-info}/PKG-INFO +2 -1
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox.egg-info/requires.txt +1 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/tests/test_cloner.py +11 -5
- {clonebox-0.1.22 → clonebox-0.1.23}/LICENSE +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/README.md +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/setup.cfg +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/__init__.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/__main__.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/cli.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/container.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/dashboard.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/detector.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/models.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/profiles.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox/validator.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox.egg-info/SOURCES.txt +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox.egg-info/dependency_links.txt +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox.egg-info/entry_points.txt +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/src/clonebox.egg-info/top_level.txt +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/tests/test_cli.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/tests/test_container.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/tests/test_detector.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/tests/test_models.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/tests/test_network.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/tests/test_profiles.py +0 -0
- {clonebox-0.1.22 → clonebox-0.1.23}/tests/test_validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clonebox
|
|
3
|
-
Version: 0.1.
|
|
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"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clonebox"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.23"
|
|
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"}
|
|
@@ -37,6 +37,7 @@ dependencies = [
|
|
|
37
37
|
"psutil>=5.9.0",
|
|
38
38
|
"pyyaml>=6.0",
|
|
39
39
|
"pydantic>=2.0.0",
|
|
40
|
+
"python-dotenv>=1.0.0",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
[project.optional-dependencies]
|
|
@@ -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 =
|
|
38
|
-
vcpus: int = 4
|
|
39
|
-
disk_size_gb: int = 20
|
|
40
|
-
gui: bool =
|
|
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 =
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
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"
|
|
@@ -15,7 +15,7 @@ class TestVMConfig:
|
|
|
15
15
|
def test_default_values(self):
|
|
16
16
|
config = VMConfig()
|
|
17
17
|
assert config.name == "clonebox-vm"
|
|
18
|
-
assert config.ram_mb ==
|
|
18
|
+
assert config.ram_mb == 8192
|
|
19
19
|
assert config.vcpus == 4
|
|
20
20
|
assert config.disk_size_gb == 20
|
|
21
21
|
assert config.gui is True
|
|
@@ -53,12 +53,18 @@ class TestVMConfig:
|
|
|
53
53
|
class TestSelectiveVMClonerInit:
|
|
54
54
|
"""Test SelectiveVMCloner initialization."""
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
@patch("clonebox.cloner.libvirt")
|
|
57
|
+
def test_system_images_dir(self, mock_libvirt):
|
|
58
|
+
mock_libvirt.open.return_value = MagicMock()
|
|
59
|
+
cloner = SelectiveVMCloner()
|
|
60
|
+
assert cloner.SYSTEM_IMAGES_DIR == Path("/var/lib/libvirt/images")
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
@patch("clonebox.cloner.libvirt")
|
|
63
|
+
def test_user_images_dir(self, mock_libvirt):
|
|
64
|
+
mock_libvirt.open.return_value = MagicMock()
|
|
65
|
+
cloner = SelectiveVMCloner()
|
|
60
66
|
expected = Path.home() / ".local/share/libvirt/images"
|
|
61
|
-
assert
|
|
67
|
+
assert cloner.USER_IMAGES_DIR == expected
|
|
62
68
|
|
|
63
69
|
@pytest.mark.parametrize("user_session,expected_uri", [
|
|
64
70
|
(False, "qemu:///system"),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|