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/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 = 10
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=10, ge=1, le=2048, description="Disk size in GB")
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=self.paths,
121
+ paths=all_paths,
118
122
  packages=self.packages,
119
123
  snap_packages=self.snap_packages,
120
124
  services=self.services,