clonebox 0.1.20__py3-none-any.whl → 0.1.22__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/cli.py +210 -6
- clonebox/cloner.py +345 -2
- clonebox/models.py +6 -2
- clonebox/validator.py +361 -29
- {clonebox-0.1.20.dist-info → clonebox-0.1.22.dist-info}/METADATA +196 -6
- clonebox-0.1.22.dist-info/RECORD +17 -0
- clonebox-0.1.20.dist-info/RECORD +0 -17
- {clonebox-0.1.20.dist-info → clonebox-0.1.22.dist-info}/WHEEL +0 -0
- {clonebox-0.1.20.dist-info → clonebox-0.1.22.dist-info}/entry_points.txt +0 -0
- {clonebox-0.1.20.dist-info → clonebox-0.1.22.dist-info}/licenses/LICENSE +0 -0
- {clonebox-0.1.20.dist-info → clonebox-0.1.22.dist-info}/top_level.txt +0 -0
clonebox/cloner.py
CHANGED
|
@@ -18,6 +18,16 @@ try:
|
|
|
18
18
|
except ImportError:
|
|
19
19
|
libvirt = None
|
|
20
20
|
|
|
21
|
+
SNAP_INTERFACES = {
|
|
22
|
+
'pycharm-community': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'network-bind', 'cups-control', 'removable-media'],
|
|
23
|
+
'chromium': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'audio-playback', 'camera'],
|
|
24
|
+
'firefox': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'audio-playback', 'removable-media'],
|
|
25
|
+
'code': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'ssh-keys'],
|
|
26
|
+
'slack': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'audio-playback'],
|
|
27
|
+
'spotify': ['desktop', 'x11', 'wayland', 'home', 'network', 'audio-playback'],
|
|
28
|
+
}
|
|
29
|
+
DEFAULT_SNAP_INTERFACES = ['desktop', 'desktop-legacy', 'x11', 'home', 'network']
|
|
30
|
+
|
|
21
31
|
|
|
22
32
|
@dataclass
|
|
23
33
|
class VMConfig:
|
|
@@ -26,7 +36,7 @@ class VMConfig:
|
|
|
26
36
|
name: str = "clonebox-vm"
|
|
27
37
|
ram_mb: int = 4096
|
|
28
38
|
vcpus: int = 4
|
|
29
|
-
disk_size_gb: int =
|
|
39
|
+
disk_size_gb: int = 20
|
|
30
40
|
gui: bool = True
|
|
31
41
|
base_image: Optional[str] = None
|
|
32
42
|
paths: dict = field(default_factory=dict)
|
|
@@ -477,6 +487,268 @@ class SelectiveVMCloner:
|
|
|
477
487
|
|
|
478
488
|
return ET.tostring(root, encoding="unicode")
|
|
479
489
|
|
|
490
|
+
def _generate_boot_diagnostic_script(self, config: VMConfig) -> str:
|
|
491
|
+
"""Generate boot diagnostic script with self-healing capabilities."""
|
|
492
|
+
import base64
|
|
493
|
+
|
|
494
|
+
wants_google_chrome = any(
|
|
495
|
+
p == "/home/ubuntu/.config/google-chrome" for p in (config.paths or {}).values()
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
apt_pkg_list = list(config.packages or [])
|
|
499
|
+
for base_pkg in ["qemu-guest-agent", "cloud-guest-utils"]:
|
|
500
|
+
if base_pkg not in apt_pkg_list:
|
|
501
|
+
apt_pkg_list.insert(0, base_pkg)
|
|
502
|
+
if config.gui:
|
|
503
|
+
for gui_pkg in ["ubuntu-desktop-minimal", "firefox"]:
|
|
504
|
+
if gui_pkg not in apt_pkg_list:
|
|
505
|
+
apt_pkg_list.append(gui_pkg)
|
|
506
|
+
|
|
507
|
+
apt_packages = " ".join(f'"{p}"' for p in apt_pkg_list) if apt_pkg_list else ""
|
|
508
|
+
snap_packages = " ".join(f'"{p}"' for p in config.snap_packages) if config.snap_packages else ""
|
|
509
|
+
services = " ".join(f'"{s}"' for s in config.services) if config.services else ""
|
|
510
|
+
|
|
511
|
+
snap_ifaces_bash = "\n".join(
|
|
512
|
+
f'SNAP_INTERFACES["{snap}"]="{" ".join(ifaces)}"'
|
|
513
|
+
for snap, ifaces in SNAP_INTERFACES.items()
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
script = f'''#!/bin/bash
|
|
517
|
+
set -uo pipefail
|
|
518
|
+
LOG="/var/log/clonebox-boot.log"
|
|
519
|
+
STATUS_KV="/var/run/clonebox-status"
|
|
520
|
+
STATUS_JSON="/var/run/clonebox-status.json"
|
|
521
|
+
MAX_RETRIES=3
|
|
522
|
+
PASSED=0 FAILED=0 REPAIRED=0 TOTAL=0
|
|
523
|
+
|
|
524
|
+
RED='\\033[0;31m' GREEN='\\033[0;32m' YELLOW='\\033[1;33m' CYAN='\\033[0;36m' NC='\\033[0m' BOLD='\\033[1m'
|
|
525
|
+
|
|
526
|
+
log() {{ echo -e "[$(date +%H:%M:%S)] $1" | tee -a "$LOG"; }}
|
|
527
|
+
ok() {{ log "${{GREEN}}✅ $1${{NC}}"; ((PASSED++)); ((TOTAL++)); }}
|
|
528
|
+
fail() {{ log "${{RED}}❌ $1${{NC}}"; ((FAILED++)); ((TOTAL++)); }}
|
|
529
|
+
repair() {{ log "${{YELLOW}}🔧 $1${{NC}}"; }}
|
|
530
|
+
section() {{ log ""; log "${{BOLD}}[$1] $2${{NC}}"; }}
|
|
531
|
+
|
|
532
|
+
write_status() {{
|
|
533
|
+
local phase="$1"
|
|
534
|
+
local current_task="${{2:-}}"
|
|
535
|
+
printf 'passed=%s failed=%s repaired=%s\n' "$PASSED" "$FAILED" "$REPAIRED" > "$STATUS_KV" 2>/dev/null || true
|
|
536
|
+
cat > "$STATUS_JSON" <<EOF
|
|
537
|
+
{{"phase":"$phase","current_task":"$current_task","total":$TOTAL,"passed":$PASSED,"failed":$FAILED,"repaired":$REPAIRED,"timestamp":"$(date -Iseconds)"}}
|
|
538
|
+
EOF
|
|
539
|
+
}}
|
|
540
|
+
|
|
541
|
+
header() {{
|
|
542
|
+
log ""
|
|
543
|
+
log "${{BOLD}}${{CYAN}}═══════════════════════════════════════════════════════════${{NC}}"
|
|
544
|
+
log "${{BOLD}}${{CYAN}} $1${{NC}}"
|
|
545
|
+
log "${{BOLD}}${{CYAN}}═══════════════════════════════════════════════════════════${{NC}}"
|
|
546
|
+
}}
|
|
547
|
+
|
|
548
|
+
declare -A SNAP_INTERFACES
|
|
549
|
+
{snap_ifaces_bash}
|
|
550
|
+
DEFAULT_IFACES="desktop desktop-legacy x11 home network"
|
|
551
|
+
|
|
552
|
+
check_apt() {{
|
|
553
|
+
dpkg -l "$1" 2>/dev/null | grep -q "^ii"
|
|
554
|
+
}}
|
|
555
|
+
|
|
556
|
+
install_apt() {{
|
|
557
|
+
for i in $(seq 1 $MAX_RETRIES); do
|
|
558
|
+
DEBIAN_FRONTEND=noninteractive apt-get install -y "$1" &>>"$LOG" && return 0
|
|
559
|
+
sleep 3
|
|
560
|
+
done
|
|
561
|
+
return 1
|
|
562
|
+
}}
|
|
563
|
+
|
|
564
|
+
check_snap() {{
|
|
565
|
+
snap list "$1" &>/dev/null
|
|
566
|
+
}}
|
|
567
|
+
|
|
568
|
+
install_snap() {{
|
|
569
|
+
timeout 60 snap wait system seed.loaded 2>/dev/null || true
|
|
570
|
+
for i in $(seq 1 $MAX_RETRIES); do
|
|
571
|
+
snap install "$1" --classic &>>"$LOG" && return 0
|
|
572
|
+
snap install "$1" &>>"$LOG" && return 0
|
|
573
|
+
sleep 5
|
|
574
|
+
done
|
|
575
|
+
return 1
|
|
576
|
+
}}
|
|
577
|
+
|
|
578
|
+
connect_interfaces() {{
|
|
579
|
+
local snap="$1"
|
|
580
|
+
local ifaces="${{SNAP_INTERFACES[$snap]:-$DEFAULT_IFACES}}"
|
|
581
|
+
for iface in $ifaces; do
|
|
582
|
+
snap connect "$snap:$iface" ":$iface" 2>/dev/null && log " ${{GREEN}}✓${{NC}} $snap:$iface" || true
|
|
583
|
+
done
|
|
584
|
+
}}
|
|
585
|
+
|
|
586
|
+
test_launch() {{
|
|
587
|
+
case "$1" in
|
|
588
|
+
pycharm-community) /snap/pycharm-community/current/jbr/bin/java -version &>/dev/null ;;
|
|
589
|
+
chromium) timeout 10 chromium --headless=new --dump-dom about:blank &>/dev/null ;;
|
|
590
|
+
firefox) timeout 10 firefox --headless --screenshot /tmp/ff-test.png about:blank &>/dev/null; rm -f /tmp/ff-test.png ;;
|
|
591
|
+
docker) docker info &>/dev/null ;;
|
|
592
|
+
*) command -v "$1" &>/dev/null ;;
|
|
593
|
+
esac
|
|
594
|
+
}}
|
|
595
|
+
|
|
596
|
+
header "CloneBox VM Boot Diagnostic"
|
|
597
|
+
write_status "starting" "boot diagnostic starting"
|
|
598
|
+
|
|
599
|
+
APT_PACKAGES=({apt_packages})
|
|
600
|
+
SNAP_PACKAGES=({snap_packages})
|
|
601
|
+
SERVICES=({services})
|
|
602
|
+
|
|
603
|
+
section "1/5" "Checking APT packages..."
|
|
604
|
+
write_status "checking_apt" "checking APT packages"
|
|
605
|
+
for pkg in "${{APT_PACKAGES[@]}}"; do
|
|
606
|
+
[ -z "$pkg" ] && continue
|
|
607
|
+
if check_apt "$pkg"; then
|
|
608
|
+
ok "$pkg"
|
|
609
|
+
else
|
|
610
|
+
repair "Installing $pkg..."
|
|
611
|
+
if install_apt "$pkg"; then
|
|
612
|
+
ok "$pkg installed"
|
|
613
|
+
((REPAIRED++))
|
|
614
|
+
else
|
|
615
|
+
fail "$pkg FAILED"
|
|
616
|
+
fi
|
|
617
|
+
fi
|
|
618
|
+
done
|
|
619
|
+
|
|
620
|
+
section "2/5" "Checking Snap packages..."
|
|
621
|
+
write_status "checking_snaps" "checking snap packages"
|
|
622
|
+
timeout 120 snap wait system seed.loaded 2>/dev/null || true
|
|
623
|
+
for pkg in "${{SNAP_PACKAGES[@]}}"; do
|
|
624
|
+
[ -z "$pkg" ] && continue
|
|
625
|
+
if check_snap "$pkg"; then
|
|
626
|
+
ok "$pkg (snap)"
|
|
627
|
+
else
|
|
628
|
+
repair "Installing $pkg..."
|
|
629
|
+
if install_snap "$pkg"; then
|
|
630
|
+
ok "$pkg installed"
|
|
631
|
+
((REPAIRED++))
|
|
632
|
+
else
|
|
633
|
+
fail "$pkg FAILED"
|
|
634
|
+
fi
|
|
635
|
+
fi
|
|
636
|
+
done
|
|
637
|
+
|
|
638
|
+
section "3/5" "Connecting Snap interfaces..."
|
|
639
|
+
write_status "connecting_interfaces" "connecting snap interfaces"
|
|
640
|
+
for pkg in "${{SNAP_PACKAGES[@]}}"; do
|
|
641
|
+
[ -z "$pkg" ] && continue
|
|
642
|
+
check_snap "$pkg" && connect_interfaces "$pkg"
|
|
643
|
+
done
|
|
644
|
+
systemctl restart snapd 2>/dev/null || true
|
|
645
|
+
|
|
646
|
+
section "4/5" "Testing application launch..."
|
|
647
|
+
write_status "testing_launch" "testing application launch"
|
|
648
|
+
APPS_TO_TEST=()
|
|
649
|
+
for pkg in "${{SNAP_PACKAGES[@]}}"; do
|
|
650
|
+
[ -z "$pkg" ] && continue
|
|
651
|
+
APPS_TO_TEST+=("$pkg")
|
|
652
|
+
done
|
|
653
|
+
if [ "{str(wants_google_chrome).lower()}" = "true" ]; then
|
|
654
|
+
APPS_TO_TEST+=("google-chrome")
|
|
655
|
+
fi
|
|
656
|
+
if printf '%s\n' "${{APT_PACKAGES[@]}}" | grep -qx "docker.io"; then
|
|
657
|
+
APPS_TO_TEST+=("docker")
|
|
658
|
+
fi
|
|
659
|
+
|
|
660
|
+
for app in "${{APPS_TO_TEST[@]}}"; do
|
|
661
|
+
[ -z "$app" ] && continue
|
|
662
|
+
case "$app" in
|
|
663
|
+
google-chrome)
|
|
664
|
+
if ! command -v google-chrome >/dev/null 2>&1 && ! command -v google-chrome-stable >/dev/null 2>&1; then
|
|
665
|
+
repair "Installing google-chrome..."
|
|
666
|
+
tmp_deb="/tmp/google-chrome-stable_current_amd64.deb"
|
|
667
|
+
if curl -fsSL -o "$tmp_deb" "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" \
|
|
668
|
+
&& DEBIAN_FRONTEND=noninteractive apt-get install -y "$tmp_deb" &>>"$LOG"; then
|
|
669
|
+
rm -f "$tmp_deb"
|
|
670
|
+
((REPAIRED++))
|
|
671
|
+
else
|
|
672
|
+
rm -f "$tmp_deb" 2>/dev/null || true
|
|
673
|
+
fi
|
|
674
|
+
fi
|
|
675
|
+
;;
|
|
676
|
+
docker)
|
|
677
|
+
check_apt "docker.io" || continue
|
|
678
|
+
;;
|
|
679
|
+
*)
|
|
680
|
+
if check_snap "$app"; then
|
|
681
|
+
:
|
|
682
|
+
else
|
|
683
|
+
continue
|
|
684
|
+
fi
|
|
685
|
+
;;
|
|
686
|
+
esac
|
|
687
|
+
|
|
688
|
+
if test_launch "$app"; then
|
|
689
|
+
ok "$app launches OK"
|
|
690
|
+
else
|
|
691
|
+
fail "$app launch test FAILED"
|
|
692
|
+
fi
|
|
693
|
+
done
|
|
694
|
+
|
|
695
|
+
section "5/6" "Checking mount points..."
|
|
696
|
+
write_status "checking_mounts" "checking mount points"
|
|
697
|
+
while IFS= read -r line; do
|
|
698
|
+
tag=$(echo "$line" | awk '{{print $1}}')
|
|
699
|
+
mp=$(echo "$line" | awk '{{print $2}}')
|
|
700
|
+
if [[ "$tag" =~ ^mount[0-9]+$ ]] && [[ "$mp" == /* ]]; then
|
|
701
|
+
if mountpoint -q "$mp" 2>/dev/null; then
|
|
702
|
+
ok "$mp mounted"
|
|
703
|
+
else
|
|
704
|
+
repair "Mounting $mp..."
|
|
705
|
+
mkdir -p "$mp" 2>/dev/null || true
|
|
706
|
+
if mount "$mp" &>>"$LOG"; then
|
|
707
|
+
ok "$mp mounted"
|
|
708
|
+
((REPAIRED++))
|
|
709
|
+
else
|
|
710
|
+
fail "$mp mount FAILED"
|
|
711
|
+
fi
|
|
712
|
+
fi
|
|
713
|
+
fi
|
|
714
|
+
done < /etc/fstab
|
|
715
|
+
|
|
716
|
+
section "6/6" "Checking services..."
|
|
717
|
+
write_status "checking_services" "checking services"
|
|
718
|
+
for svc in "${{SERVICES[@]}}"; do
|
|
719
|
+
[ -z "$svc" ] && continue
|
|
720
|
+
if systemctl is-active "$svc" &>/dev/null; then
|
|
721
|
+
ok "$svc running"
|
|
722
|
+
else
|
|
723
|
+
repair "Starting $svc..."
|
|
724
|
+
systemctl enable --now "$svc" &>/dev/null && ok "$svc started" && ((REPAIRED++)) || fail "$svc FAILED"
|
|
725
|
+
fi
|
|
726
|
+
done
|
|
727
|
+
|
|
728
|
+
header "Diagnostic Summary"
|
|
729
|
+
log ""
|
|
730
|
+
log " Total: $TOTAL"
|
|
731
|
+
log " ${{GREEN}}Passed:${{NC}} $PASSED"
|
|
732
|
+
log " ${{YELLOW}}Repaired:${{NC}} $REPAIRED"
|
|
733
|
+
log " ${{RED}}Failed:${{NC}} $FAILED"
|
|
734
|
+
log ""
|
|
735
|
+
|
|
736
|
+
write_status "complete" "complete"
|
|
737
|
+
|
|
738
|
+
if [ $FAILED -eq 0 ]; then
|
|
739
|
+
log "${{GREEN}}${{BOLD}}═══════════════════════════════════════════════════════════${{NC}}"
|
|
740
|
+
log "${{GREEN}}${{BOLD}} ✅ All checks passed! CloneBox VM is ready.${{NC}}"
|
|
741
|
+
log "${{GREEN}}${{BOLD}}═══════════════════════════════════════════════════════════${{NC}}"
|
|
742
|
+
exit 0
|
|
743
|
+
else
|
|
744
|
+
log "${{RED}}${{BOLD}}═══════════════════════════════════════════════════════════${{NC}}"
|
|
745
|
+
log "${{RED}}${{BOLD}} ⚠️ $FAILED checks failed. See /var/log/clonebox-boot.log${{NC}}"
|
|
746
|
+
log "${{RED}}${{BOLD}}═══════════════════════════════════════════════════════════${{NC}}"
|
|
747
|
+
exit 1
|
|
748
|
+
fi
|
|
749
|
+
'''
|
|
750
|
+
return base64.b64encode(script.encode()).decode()
|
|
751
|
+
|
|
480
752
|
def _generate_health_check_script(self, config: VMConfig) -> str:
|
|
481
753
|
"""Generate a health check script that validates all installed components."""
|
|
482
754
|
import base64
|
|
@@ -680,7 +952,7 @@ fi
|
|
|
680
952
|
|
|
681
953
|
# User-data
|
|
682
954
|
# Add desktop environment if GUI is enabled
|
|
683
|
-
base_packages = ["qemu-guest-agent"]
|
|
955
|
+
base_packages = ["qemu-guest-agent", "cloud-guest-utils"]
|
|
684
956
|
if config.gui:
|
|
685
957
|
base_packages.extend([
|
|
686
958
|
"ubuntu-desktop-minimal",
|
|
@@ -719,6 +991,15 @@ fi
|
|
|
719
991
|
runcmd_lines.append(" - echo 'Installing snap packages...'")
|
|
720
992
|
for snap_pkg in config.snap_packages:
|
|
721
993
|
runcmd_lines.append(f" - snap install {snap_pkg} --classic || snap install {snap_pkg} || true")
|
|
994
|
+
|
|
995
|
+
# Connect snap interfaces for GUI apps (not auto-connected via cloud-init)
|
|
996
|
+
runcmd_lines.append(" - echo 'Connecting snap interfaces...'")
|
|
997
|
+
for snap_pkg in config.snap_packages:
|
|
998
|
+
interfaces = SNAP_INTERFACES.get(snap_pkg, DEFAULT_SNAP_INTERFACES)
|
|
999
|
+
for iface in interfaces:
|
|
1000
|
+
runcmd_lines.append(f" - snap connect {snap_pkg}:{iface} :{iface} 2>/dev/null || true")
|
|
1001
|
+
|
|
1002
|
+
runcmd_lines.append(" - systemctl restart snapd || true")
|
|
722
1003
|
|
|
723
1004
|
# Add GUI setup if enabled - runs AFTER package installation completes
|
|
724
1005
|
if config.gui:
|
|
@@ -740,6 +1021,61 @@ fi
|
|
|
740
1021
|
runcmd_lines.append(" - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1")
|
|
741
1022
|
runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
|
|
742
1023
|
|
|
1024
|
+
# Generate boot diagnostic script (self-healing)
|
|
1025
|
+
boot_diag_script = self._generate_boot_diagnostic_script(config)
|
|
1026
|
+
runcmd_lines.append(f" - echo '{boot_diag_script}' | base64 -d > /usr/local/bin/clonebox-boot-diagnostic")
|
|
1027
|
+
runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-boot-diagnostic")
|
|
1028
|
+
|
|
1029
|
+
# Create systemd service for boot diagnostic (runs before GDM on subsequent boots)
|
|
1030
|
+
systemd_service = '''[Unit]
|
|
1031
|
+
Description=CloneBox Boot Diagnostic
|
|
1032
|
+
After=network-online.target snapd.service
|
|
1033
|
+
Before=gdm.service display-manager.service
|
|
1034
|
+
Wants=network-online.target
|
|
1035
|
+
|
|
1036
|
+
[Service]
|
|
1037
|
+
Type=oneshot
|
|
1038
|
+
ExecStart=/usr/local/bin/clonebox-boot-diagnostic
|
|
1039
|
+
StandardOutput=journal+console
|
|
1040
|
+
StandardError=journal+console
|
|
1041
|
+
TTYPath=/dev/tty1
|
|
1042
|
+
TTYReset=yes
|
|
1043
|
+
TTYVHangup=yes
|
|
1044
|
+
RemainAfterExit=yes
|
|
1045
|
+
TimeoutStartSec=600
|
|
1046
|
+
|
|
1047
|
+
[Install]
|
|
1048
|
+
WantedBy=multi-user.target'''
|
|
1049
|
+
import base64
|
|
1050
|
+
systemd_b64 = base64.b64encode(systemd_service.encode()).decode()
|
|
1051
|
+
runcmd_lines.append(f" - echo '{systemd_b64}' | base64 -d > /etc/systemd/system/clonebox-diagnostic.service")
|
|
1052
|
+
runcmd_lines.append(" - systemctl daemon-reload")
|
|
1053
|
+
runcmd_lines.append(" - systemctl enable clonebox-diagnostic.service")
|
|
1054
|
+
runcmd_lines.append(" - systemctl start clonebox-diagnostic.service || true")
|
|
1055
|
+
|
|
1056
|
+
# Create MOTD banner
|
|
1057
|
+
motd_banner = '''#!/bin/bash
|
|
1058
|
+
S="/var/run/clonebox-status"
|
|
1059
|
+
echo ""
|
|
1060
|
+
echo -e "\\033[1;34m═══════════════════════════════════════════════════════════\\033[0m"
|
|
1061
|
+
echo -e "\\033[1;34m CloneBox VM Status\\033[0m"
|
|
1062
|
+
echo -e "\\033[1;34m═══════════════════════════════════════════════════════════\\033[0m"
|
|
1063
|
+
if [ -f "$S" ]; then
|
|
1064
|
+
source "$S"
|
|
1065
|
+
if [ "${failed:-0}" -eq 0 ]; then
|
|
1066
|
+
echo -e " \\033[0;32m✅ All systems operational\\033[0m"
|
|
1067
|
+
else
|
|
1068
|
+
echo -e " \\033[0;31m⚠️ $failed checks failed\\033[0m"
|
|
1069
|
+
fi
|
|
1070
|
+
echo -e " Passed: ${passed:-0} | Repaired: ${repaired:-0} | Failed: ${failed:-0}"
|
|
1071
|
+
fi
|
|
1072
|
+
echo -e " Log: /var/log/clonebox-boot.log"
|
|
1073
|
+
echo -e "\\033[1;34m═══════════════════════════════════════════════════════════\\033[0m"
|
|
1074
|
+
echo ""'''
|
|
1075
|
+
motd_b64 = base64.b64encode(motd_banner.encode()).decode()
|
|
1076
|
+
runcmd_lines.append(f" - echo '{motd_b64}' | base64 -d > /etc/update-motd.d/99-clonebox")
|
|
1077
|
+
runcmd_lines.append(" - chmod +x /etc/update-motd.d/99-clonebox")
|
|
1078
|
+
|
|
743
1079
|
# Add reboot command at the end if GUI is enabled
|
|
744
1080
|
if config.gui:
|
|
745
1081
|
runcmd_lines.append(" - echo 'Rebooting in 10 seconds to start GUI...'")
|
|
@@ -770,6 +1106,13 @@ ssh_pwauth: true
|
|
|
770
1106
|
chpasswd:
|
|
771
1107
|
expire: false
|
|
772
1108
|
|
|
1109
|
+
# Make sure root partition + filesystem grows to fill the qcow2 disk size
|
|
1110
|
+
growpart:
|
|
1111
|
+
mode: auto
|
|
1112
|
+
devices: ["/"]
|
|
1113
|
+
ignore_growroot_disabled: false
|
|
1114
|
+
resize_rootfs: true
|
|
1115
|
+
|
|
773
1116
|
# Update package cache and upgrade
|
|
774
1117
|
package_update: true
|
|
775
1118
|
package_upgrade: false
|
clonebox/models.py
CHANGED
|
@@ -16,7 +16,7 @@ class VMSettings(BaseModel):
|
|
|
16
16
|
name: str = Field(default="clonebox-vm", description="VM name")
|
|
17
17
|
ram_mb: int = Field(default=4096, ge=512, le=131072, description="RAM in MB")
|
|
18
18
|
vcpus: int = Field(default=4, ge=1, le=128, description="Number of vCPUs")
|
|
19
|
-
disk_size_gb: int = Field(default=
|
|
19
|
+
disk_size_gb: int = Field(default=20, ge=1, le=2048, description="Disk size in GB")
|
|
20
20
|
gui: bool = Field(default=True, description="Enable SPICE graphics")
|
|
21
21
|
base_image: Optional[str] = Field(default=None, description="Path to base qcow2 image")
|
|
22
22
|
network_mode: str = Field(default="auto", description="Network mode: auto|default|user")
|
|
@@ -107,6 +107,10 @@ class CloneBoxConfig(BaseModel):
|
|
|
107
107
|
"""Convert to legacy VMConfig dataclass for compatibility."""
|
|
108
108
|
from clonebox.cloner import VMConfig as VMConfigDataclass
|
|
109
109
|
|
|
110
|
+
# Merge paths and app_data_paths
|
|
111
|
+
all_paths = dict(self.paths)
|
|
112
|
+
all_paths.update(self.app_data_paths)
|
|
113
|
+
|
|
110
114
|
return VMConfigDataclass(
|
|
111
115
|
name=self.vm.name,
|
|
112
116
|
ram_mb=self.vm.ram_mb,
|
|
@@ -114,7 +118,7 @@ class CloneBoxConfig(BaseModel):
|
|
|
114
118
|
disk_size_gb=self.vm.disk_size_gb,
|
|
115
119
|
gui=self.vm.gui,
|
|
116
120
|
base_image=self.vm.base_image,
|
|
117
|
-
paths=
|
|
121
|
+
paths=all_paths,
|
|
118
122
|
packages=self.packages,
|
|
119
123
|
snap_packages=self.snap_packages,
|
|
120
124
|
services=self.services,
|