clonebox 0.1.6__tar.gz → 0.1.8__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.6 → clonebox-0.1.8}/PKG-INFO +2 -1
- {clonebox-0.1.6 → clonebox-0.1.8}/README.md +1 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/pyproject.toml +1 -1
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox/cli.py +67 -16
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox/cloner.py +193 -1
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox/detector.py +232 -58
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox.egg-info/PKG-INFO +2 -1
- {clonebox-0.1.6 → clonebox-0.1.8}/tests/test_network.py +16 -4
- {clonebox-0.1.6 → clonebox-0.1.8}/LICENSE +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/setup.cfg +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox/__init__.py +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox/__main__.py +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox.egg-info/SOURCES.txt +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox.egg-info/dependency_links.txt +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox.egg-info/entry_points.txt +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox.egg-info/requires.txt +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/src/clonebox.egg-info/top_level.txt +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/tests/test_cli.py +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/tests/test_cloner.py +0 -0
- {clonebox-0.1.6 → clonebox-0.1.8}/tests/test_detector.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.8
|
|
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
|
|
@@ -38,6 +38,7 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
|
38
38
|
Dynamic: license-file
|
|
39
39
|
|
|
40
40
|
# CloneBox 📦
|
|
41
|
+

|
|
41
42
|
|
|
42
43
|
```commandline
|
|
43
44
|
╔═══════════════════════════════════════════════════════╗
|
|
@@ -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.8"
|
|
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"}
|
|
@@ -592,6 +592,28 @@ def generate_clonebox_yaml(
|
|
|
592
592
|
paths_mapping[host_path] = f"/mnt/workdir{idx}"
|
|
593
593
|
idx += 1
|
|
594
594
|
|
|
595
|
+
# Add default user folders (Downloads, Documents)
|
|
596
|
+
home_dir = Path.home()
|
|
597
|
+
default_folders = [
|
|
598
|
+
(home_dir / "Downloads", "/home/ubuntu/Downloads"),
|
|
599
|
+
(home_dir / "Documents", "/home/ubuntu/Documents"),
|
|
600
|
+
]
|
|
601
|
+
for host_folder, guest_folder in default_folders:
|
|
602
|
+
if host_folder.exists() and str(host_folder) not in paths_mapping:
|
|
603
|
+
paths_mapping[str(host_folder)] = guest_folder
|
|
604
|
+
|
|
605
|
+
# Detect and add app-specific data directories for running applications
|
|
606
|
+
# This includes browser profiles, IDE settings, credentials, extensions, etc.
|
|
607
|
+
app_data_dirs = detector.detect_app_data_dirs(snapshot.applications)
|
|
608
|
+
app_data_mapping = {}
|
|
609
|
+
for app_data in app_data_dirs:
|
|
610
|
+
host_path = app_data["path"]
|
|
611
|
+
if host_path not in paths_mapping:
|
|
612
|
+
# Map to same relative path in VM user home
|
|
613
|
+
rel_path = host_path.replace(str(home_dir), "").lstrip("/")
|
|
614
|
+
guest_path = f"/home/ubuntu/{rel_path}"
|
|
615
|
+
app_data_mapping[host_path] = guest_path
|
|
616
|
+
|
|
595
617
|
# Determine VM name
|
|
596
618
|
if not vm_name:
|
|
597
619
|
if target_path:
|
|
@@ -604,10 +626,10 @@ def generate_clonebox_yaml(
|
|
|
604
626
|
vcpus = max(2, sys_info["cpu_count"] // 2)
|
|
605
627
|
|
|
606
628
|
# Auto-detect packages from running applications and services
|
|
607
|
-
|
|
608
|
-
|
|
629
|
+
app_packages = detector.suggest_packages_for_apps(snapshot.applications)
|
|
630
|
+
service_packages = detector.suggest_packages_for_services(snapshot.running_services)
|
|
609
631
|
|
|
610
|
-
# Combine with base packages
|
|
632
|
+
# Combine with base packages (apt only)
|
|
611
633
|
base_packages = [
|
|
612
634
|
"build-essential",
|
|
613
635
|
"git",
|
|
@@ -615,10 +637,15 @@ def generate_clonebox_yaml(
|
|
|
615
637
|
"vim",
|
|
616
638
|
]
|
|
617
639
|
|
|
618
|
-
# Merge
|
|
619
|
-
|
|
640
|
+
# Merge apt packages and deduplicate
|
|
641
|
+
all_apt_packages = base_packages + app_packages["apt"] + service_packages["apt"]
|
|
642
|
+
if deduplicate:
|
|
643
|
+
all_apt_packages = deduplicate_list(all_apt_packages)
|
|
644
|
+
|
|
645
|
+
# Merge snap packages and deduplicate
|
|
646
|
+
all_snap_packages = app_packages["snap"] + service_packages["snap"]
|
|
620
647
|
if deduplicate:
|
|
621
|
-
|
|
648
|
+
all_snap_packages = deduplicate_list(all_snap_packages)
|
|
622
649
|
|
|
623
650
|
# Build config
|
|
624
651
|
config = {
|
|
@@ -635,17 +662,24 @@ def generate_clonebox_yaml(
|
|
|
635
662
|
"password": "${VM_PASSWORD}",
|
|
636
663
|
},
|
|
637
664
|
"services": services,
|
|
638
|
-
"packages":
|
|
665
|
+
"packages": all_apt_packages,
|
|
666
|
+
"snap_packages": all_snap_packages,
|
|
667
|
+
"post_commands": [], # User can add custom commands to run after setup
|
|
639
668
|
"paths": paths_mapping,
|
|
669
|
+
"app_data_paths": app_data_mapping, # App-specific config/data directories
|
|
640
670
|
"detected": {
|
|
641
671
|
"running_apps": [
|
|
642
|
-
{"name": a.name, "cwd": a.working_dir, "memory_mb": round(a.memory_mb)}
|
|
672
|
+
{"name": a.name, "cwd": a.working_dir or "", "memory_mb": round(a.memory_mb)}
|
|
643
673
|
for a in snapshot.applications[:10]
|
|
644
674
|
],
|
|
675
|
+
"app_data_dirs": [
|
|
676
|
+
{"path": d["path"], "app": d["app"], "size_mb": d["size_mb"]}
|
|
677
|
+
for d in app_data_dirs[:15]
|
|
678
|
+
],
|
|
645
679
|
"all_paths": {
|
|
646
|
-
"projects": paths_by_type["project"],
|
|
647
|
-
"configs": paths_by_type["config"][:5],
|
|
648
|
-
"data": paths_by_type["data"][:5],
|
|
680
|
+
"projects": list(paths_by_type["project"]),
|
|
681
|
+
"configs": list(paths_by_type["config"][:5]),
|
|
682
|
+
"data": list(paths_by_type["data"][:5]),
|
|
649
683
|
},
|
|
650
684
|
},
|
|
651
685
|
}
|
|
@@ -764,15 +798,21 @@ def create_vm_from_config(
|
|
|
764
798
|
replace: bool = False,
|
|
765
799
|
) -> str:
|
|
766
800
|
"""Create VM from YAML config dict."""
|
|
801
|
+
# Merge paths and app_data_paths
|
|
802
|
+
all_paths = config.get("paths", {}).copy()
|
|
803
|
+
all_paths.update(config.get("app_data_paths", {}))
|
|
804
|
+
|
|
767
805
|
vm_config = VMConfig(
|
|
768
806
|
name=config["vm"]["name"],
|
|
769
807
|
ram_mb=config["vm"].get("ram_mb", 4096),
|
|
770
808
|
vcpus=config["vm"].get("vcpus", 4),
|
|
771
809
|
gui=config["vm"].get("gui", True),
|
|
772
810
|
base_image=config["vm"].get("base_image"),
|
|
773
|
-
paths=
|
|
811
|
+
paths=all_paths,
|
|
774
812
|
packages=config.get("packages", []),
|
|
813
|
+
snap_packages=config.get("snap_packages", []),
|
|
775
814
|
services=config.get("services", []),
|
|
815
|
+
post_commands=config.get("post_commands", []),
|
|
776
816
|
user_session=user_session,
|
|
777
817
|
network_mode=config["vm"].get("network_mode", "auto"),
|
|
778
818
|
username=config["vm"].get("username", "ubuntu"),
|
|
@@ -896,16 +936,27 @@ def cmd_clone(args):
|
|
|
896
936
|
password = config['vm'].get('password', 'ubuntu')
|
|
897
937
|
console.print("\n[bold yellow]⏰ GUI Setup Process:[/]")
|
|
898
938
|
console.print(" [yellow]•[/] Installing desktop environment (~5-10 minutes)")
|
|
939
|
+
console.print(" [yellow]•[/] Running health checks on all components")
|
|
899
940
|
console.print(" [yellow]•[/] Automatic restart after installation")
|
|
900
941
|
console.print(" [yellow]•[/] GUI login screen will appear")
|
|
901
942
|
console.print(f" [yellow]•[/] Login: [cyan]{username}[/] / [cyan]{'*' * len(password)}[/] (from .env)")
|
|
902
943
|
console.print("\n[dim]💡 Progress will be monitored automatically below[/]")
|
|
903
944
|
|
|
945
|
+
# Show health check info
|
|
946
|
+
console.print("\n[bold]📊 Health Check (inside VM):[/]")
|
|
947
|
+
console.print(" [cyan]cat /var/log/clonebox-health.log[/] # View full report")
|
|
948
|
+
console.print(" [cyan]cat /var/log/clonebox-health-status[/] # Quick status")
|
|
949
|
+
console.print(" [cyan]clonebox-health[/] # Re-run health check")
|
|
950
|
+
|
|
904
951
|
# Show mount instructions
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
952
|
+
all_paths = config.get("paths", {}).copy()
|
|
953
|
+
all_paths.update(config.get("app_data_paths", {}))
|
|
954
|
+
if all_paths:
|
|
955
|
+
console.print("\n[bold]📁 Mounted paths (automatic):[/]")
|
|
956
|
+
for idx, (host, guest) in enumerate(list(all_paths.items())[:5]):
|
|
957
|
+
console.print(f" [dim]{host}[/] → [cyan]{guest}[/]")
|
|
958
|
+
if len(all_paths) > 5:
|
|
959
|
+
console.print(f" [dim]... and {len(all_paths) - 5} more paths[/]")
|
|
909
960
|
except PermissionError as e:
|
|
910
961
|
console.print(f"[red]❌ Permission Error:[/]\n{e}")
|
|
911
962
|
console.print("\n[yellow]💡 Try running with --user flag:[/]")
|
|
@@ -31,7 +31,9 @@ class VMConfig:
|
|
|
31
31
|
base_image: Optional[str] = None
|
|
32
32
|
paths: dict = field(default_factory=dict)
|
|
33
33
|
packages: list = field(default_factory=list)
|
|
34
|
+
snap_packages: list = field(default_factory=list) # Snap packages to install
|
|
34
35
|
services: list = field(default_factory=list)
|
|
36
|
+
post_commands: list = field(default_factory=list) # Commands to run after setup
|
|
35
37
|
user_session: bool = False # Use qemu:///session instead of qemu:///system
|
|
36
38
|
network_mode: str = "auto" # auto|default|user
|
|
37
39
|
username: str = "ubuntu" # VM default username
|
|
@@ -474,6 +476,179 @@ class SelectiveVMCloner:
|
|
|
474
476
|
|
|
475
477
|
return ET.tostring(root, encoding="unicode")
|
|
476
478
|
|
|
479
|
+
def _generate_health_check_script(self, config: VMConfig) -> str:
|
|
480
|
+
"""Generate a health check script that validates all installed components."""
|
|
481
|
+
import base64
|
|
482
|
+
|
|
483
|
+
# Build package check commands
|
|
484
|
+
apt_checks = []
|
|
485
|
+
for pkg in config.packages:
|
|
486
|
+
apt_checks.append(f'check_apt_package "{pkg}"')
|
|
487
|
+
|
|
488
|
+
snap_checks = []
|
|
489
|
+
for pkg in config.snap_packages:
|
|
490
|
+
snap_checks.append(f'check_snap_package "{pkg}"')
|
|
491
|
+
|
|
492
|
+
service_checks = []
|
|
493
|
+
for svc in config.services:
|
|
494
|
+
service_checks.append(f'check_service "{svc}"')
|
|
495
|
+
|
|
496
|
+
mount_checks = []
|
|
497
|
+
for idx, (host_path, guest_path) in enumerate(config.paths.items()):
|
|
498
|
+
mount_checks.append(f'check_mount "{guest_path}" "mount{idx}"')
|
|
499
|
+
|
|
500
|
+
apt_checks_str = "\n".join(apt_checks) if apt_checks else "echo 'No apt packages to check'"
|
|
501
|
+
snap_checks_str = "\n".join(snap_checks) if snap_checks else "echo 'No snap packages to check'"
|
|
502
|
+
service_checks_str = "\n".join(service_checks) if service_checks else "echo 'No services to check'"
|
|
503
|
+
mount_checks_str = "\n".join(mount_checks) if mount_checks else "echo 'No mounts to check'"
|
|
504
|
+
|
|
505
|
+
script = f'''#!/bin/bash
|
|
506
|
+
# CloneBox Health Check Script
|
|
507
|
+
# Generated automatically - validates all installed components
|
|
508
|
+
|
|
509
|
+
REPORT_FILE="/var/log/clonebox-health.log"
|
|
510
|
+
PASSED=0
|
|
511
|
+
FAILED=0
|
|
512
|
+
WARNINGS=0
|
|
513
|
+
|
|
514
|
+
# Colors for output
|
|
515
|
+
RED='\\033[0;31m'
|
|
516
|
+
GREEN='\\033[0;32m'
|
|
517
|
+
YELLOW='\\033[1;33m'
|
|
518
|
+
NC='\\033[0m'
|
|
519
|
+
|
|
520
|
+
log() {{
|
|
521
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$REPORT_FILE"
|
|
522
|
+
}}
|
|
523
|
+
|
|
524
|
+
check_apt_package() {{
|
|
525
|
+
local pkg="$1"
|
|
526
|
+
if dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"; then
|
|
527
|
+
log "[PASS] APT package '$pkg' is installed"
|
|
528
|
+
((PASSED++))
|
|
529
|
+
return 0
|
|
530
|
+
else
|
|
531
|
+
log "[FAIL] APT package '$pkg' is NOT installed"
|
|
532
|
+
((FAILED++))
|
|
533
|
+
return 1
|
|
534
|
+
fi
|
|
535
|
+
}}
|
|
536
|
+
|
|
537
|
+
check_snap_package() {{
|
|
538
|
+
local pkg="$1"
|
|
539
|
+
if snap list "$pkg" &>/dev/null; then
|
|
540
|
+
log "[PASS] Snap package '$pkg' is installed"
|
|
541
|
+
((PASSED++))
|
|
542
|
+
return 0
|
|
543
|
+
else
|
|
544
|
+
log "[FAIL] Snap package '$pkg' is NOT installed"
|
|
545
|
+
((FAILED++))
|
|
546
|
+
return 1
|
|
547
|
+
fi
|
|
548
|
+
}}
|
|
549
|
+
|
|
550
|
+
check_service() {{
|
|
551
|
+
local svc="$1"
|
|
552
|
+
if systemctl is-enabled "$svc" &>/dev/null; then
|
|
553
|
+
if systemctl is-active "$svc" &>/dev/null; then
|
|
554
|
+
log "[PASS] Service '$svc' is enabled and running"
|
|
555
|
+
((PASSED++))
|
|
556
|
+
return 0
|
|
557
|
+
else
|
|
558
|
+
log "[WARN] Service '$svc' is enabled but not running"
|
|
559
|
+
((WARNINGS++))
|
|
560
|
+
return 1
|
|
561
|
+
fi
|
|
562
|
+
else
|
|
563
|
+
log "[INFO] Service '$svc' is not enabled (may be optional)"
|
|
564
|
+
return 0
|
|
565
|
+
fi
|
|
566
|
+
}}
|
|
567
|
+
|
|
568
|
+
check_mount() {{
|
|
569
|
+
local path="$1"
|
|
570
|
+
local tag="$2"
|
|
571
|
+
if mountpoint -q "$path" 2>/dev/null; then
|
|
572
|
+
log "[PASS] Mount '$path' ($tag) is active"
|
|
573
|
+
((PASSED++))
|
|
574
|
+
return 0
|
|
575
|
+
elif [ -d "$path" ]; then
|
|
576
|
+
log "[WARN] Directory '$path' exists but not mounted"
|
|
577
|
+
((WARNINGS++))
|
|
578
|
+
return 1
|
|
579
|
+
else
|
|
580
|
+
log "[INFO] Mount point '$path' does not exist yet"
|
|
581
|
+
return 0
|
|
582
|
+
fi
|
|
583
|
+
}}
|
|
584
|
+
|
|
585
|
+
check_gui() {{
|
|
586
|
+
if systemctl get-default | grep -q graphical; then
|
|
587
|
+
log "[PASS] System configured for graphical target"
|
|
588
|
+
((PASSED++))
|
|
589
|
+
if systemctl is-active gdm3 &>/dev/null || systemctl is-active gdm &>/dev/null; then
|
|
590
|
+
log "[PASS] Display manager (GDM) is running"
|
|
591
|
+
((PASSED++))
|
|
592
|
+
else
|
|
593
|
+
log "[WARN] Display manager not yet running (may start after reboot)"
|
|
594
|
+
((WARNINGS++))
|
|
595
|
+
fi
|
|
596
|
+
else
|
|
597
|
+
log "[INFO] System not configured for GUI"
|
|
598
|
+
fi
|
|
599
|
+
}}
|
|
600
|
+
|
|
601
|
+
# Start health check
|
|
602
|
+
log "=========================================="
|
|
603
|
+
log "CloneBox Health Check Report"
|
|
604
|
+
log "VM Name: {config.name}"
|
|
605
|
+
log "Date: $(date)"
|
|
606
|
+
log "=========================================="
|
|
607
|
+
|
|
608
|
+
log ""
|
|
609
|
+
log "--- APT Packages ---"
|
|
610
|
+
{apt_checks_str}
|
|
611
|
+
|
|
612
|
+
log ""
|
|
613
|
+
log "--- Snap Packages ---"
|
|
614
|
+
{snap_checks_str}
|
|
615
|
+
|
|
616
|
+
log ""
|
|
617
|
+
log "--- Services ---"
|
|
618
|
+
{service_checks_str}
|
|
619
|
+
|
|
620
|
+
log ""
|
|
621
|
+
log "--- Mounts ---"
|
|
622
|
+
{mount_checks_str}
|
|
623
|
+
|
|
624
|
+
log ""
|
|
625
|
+
log "--- GUI Status ---"
|
|
626
|
+
check_gui
|
|
627
|
+
|
|
628
|
+
log ""
|
|
629
|
+
log "=========================================="
|
|
630
|
+
log "Health Check Summary"
|
|
631
|
+
log "=========================================="
|
|
632
|
+
log "Passed: $PASSED"
|
|
633
|
+
log "Failed: $FAILED"
|
|
634
|
+
log "Warnings: $WARNINGS"
|
|
635
|
+
|
|
636
|
+
if [ $FAILED -eq 0 ]; then
|
|
637
|
+
log ""
|
|
638
|
+
log "[SUCCESS] All critical checks passed!"
|
|
639
|
+
echo "HEALTH_STATUS=OK" > /var/log/clonebox-health-status
|
|
640
|
+
exit 0
|
|
641
|
+
else
|
|
642
|
+
log ""
|
|
643
|
+
log "[ERROR] Some checks failed. Review log for details."
|
|
644
|
+
echo "HEALTH_STATUS=FAILED" > /var/log/clonebox-health-status
|
|
645
|
+
exit 1
|
|
646
|
+
fi
|
|
647
|
+
'''
|
|
648
|
+
# Encode script to base64 for safe embedding in cloud-init
|
|
649
|
+
encoded = base64.b64encode(script.encode()).decode()
|
|
650
|
+
return encoded
|
|
651
|
+
|
|
477
652
|
def _create_cloudinit_iso(self, vm_dir: Path, config: VMConfig) -> Path:
|
|
478
653
|
"""Create cloud-init ISO with user-data and meta-data."""
|
|
479
654
|
|
|
@@ -508,7 +683,7 @@ class SelectiveVMCloner:
|
|
|
508
683
|
"\n".join(f" - {pkg}" for pkg in all_packages) if all_packages else ""
|
|
509
684
|
)
|
|
510
685
|
|
|
511
|
-
# Build runcmd - services
|
|
686
|
+
# Build runcmd - services, mounts, snaps, post_commands
|
|
512
687
|
runcmd_lines = []
|
|
513
688
|
|
|
514
689
|
# Add service enablement
|
|
@@ -519,6 +694,12 @@ class SelectiveVMCloner:
|
|
|
519
694
|
for cmd in mount_commands:
|
|
520
695
|
runcmd_lines.append(cmd)
|
|
521
696
|
|
|
697
|
+
# Install snap packages
|
|
698
|
+
if config.snap_packages:
|
|
699
|
+
runcmd_lines.append(" - echo 'Installing snap packages...'")
|
|
700
|
+
for snap_pkg in config.snap_packages:
|
|
701
|
+
runcmd_lines.append(f" - snap install {snap_pkg} --classic || snap install {snap_pkg} || true")
|
|
702
|
+
|
|
522
703
|
# Add GUI setup if enabled - runs AFTER package installation completes
|
|
523
704
|
if config.gui:
|
|
524
705
|
runcmd_lines.extend([
|
|
@@ -526,6 +707,17 @@ class SelectiveVMCloner:
|
|
|
526
707
|
" - systemctl enable gdm3 || systemctl enable gdm || true",
|
|
527
708
|
])
|
|
528
709
|
|
|
710
|
+
# Run user-defined post commands
|
|
711
|
+
if config.post_commands:
|
|
712
|
+
runcmd_lines.append(" - echo 'Running post-setup commands...'")
|
|
713
|
+
for cmd in config.post_commands:
|
|
714
|
+
runcmd_lines.append(f" - {cmd}")
|
|
715
|
+
|
|
716
|
+
# Generate health check script
|
|
717
|
+
health_script = self._generate_health_check_script(config)
|
|
718
|
+
runcmd_lines.append(f" - echo '{health_script}' | base64 -d > /usr/local/bin/clonebox-health")
|
|
719
|
+
runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-health")
|
|
720
|
+
runcmd_lines.append(" - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1")
|
|
529
721
|
runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
|
|
530
722
|
|
|
531
723
|
# Add reboot command at the end if GUI is enabled
|
|
@@ -144,59 +144,209 @@ class SystemDetector:
|
|
|
144
144
|
"esbuild",
|
|
145
145
|
"tmux",
|
|
146
146
|
"screen",
|
|
147
|
+
# IDEs and desktop apps
|
|
148
|
+
"pycharm",
|
|
149
|
+
"idea",
|
|
150
|
+
"webstorm",
|
|
151
|
+
"phpstorm",
|
|
152
|
+
"goland",
|
|
153
|
+
"clion",
|
|
154
|
+
"rider",
|
|
155
|
+
"datagrip",
|
|
156
|
+
"sublime",
|
|
157
|
+
"atom",
|
|
158
|
+
"slack",
|
|
159
|
+
"discord",
|
|
160
|
+
"telegram",
|
|
161
|
+
"spotify",
|
|
162
|
+
"vlc",
|
|
163
|
+
"gimp",
|
|
164
|
+
"inkscape",
|
|
165
|
+
"blender",
|
|
166
|
+
"obs",
|
|
167
|
+
"postman",
|
|
168
|
+
"insomnia",
|
|
169
|
+
"dbeaver",
|
|
147
170
|
]
|
|
148
171
|
|
|
149
|
-
# Map process/service names to Ubuntu packages
|
|
172
|
+
# Map process/service names to Ubuntu packages or snap packages
|
|
173
|
+
# Format: "process_name": ("package_name", "install_type") where install_type is "apt" or "snap"
|
|
150
174
|
APP_TO_PACKAGE_MAP = {
|
|
151
|
-
"python": "python3",
|
|
152
|
-
"python3": "python3",
|
|
153
|
-
"pip": "python3-pip",
|
|
154
|
-
"node": "nodejs",
|
|
155
|
-
"npm": "npm",
|
|
156
|
-
"yarn": "yarnpkg",
|
|
157
|
-
"docker": "docker.io",
|
|
158
|
-
"dockerd": "docker.io",
|
|
159
|
-
"docker-compose": "docker-compose",
|
|
160
|
-
"podman": "podman",
|
|
161
|
-
"nginx": "nginx",
|
|
162
|
-
"apache2": "apache2",
|
|
163
|
-
"httpd": "apache2",
|
|
164
|
-
"postgres": "postgresql",
|
|
165
|
-
"postgresql": "postgresql",
|
|
166
|
-
"mysql": "mysql-server",
|
|
167
|
-
"mysqld": "mysql-server",
|
|
168
|
-
"mongod": "mongodb",
|
|
169
|
-
"mongodb": "mongodb",
|
|
170
|
-
"redis-server": "redis-server",
|
|
171
|
-
"redis": "redis-server",
|
|
172
|
-
"vim": "vim",
|
|
173
|
-
"nvim": "neovim",
|
|
174
|
-
"emacs": "emacs",
|
|
175
|
-
"firefox": "firefox",
|
|
176
|
-
"chromium": "chromium-browser",
|
|
177
|
-
"jupyter": "jupyter-notebook",
|
|
178
|
-
"jupyter-lab": "jupyterlab",
|
|
179
|
-
"gunicorn": "gunicorn",
|
|
180
|
-
"uvicorn": "uvicorn",
|
|
181
|
-
"tmux": "tmux",
|
|
182
|
-
"screen": "screen",
|
|
183
|
-
"git": "git",
|
|
184
|
-
"curl": "curl",
|
|
185
|
-
"wget": "wget",
|
|
186
|
-
"ssh": "openssh-client",
|
|
187
|
-
"sshd": "openssh-server",
|
|
188
|
-
"go": "golang",
|
|
189
|
-
"cargo": "cargo",
|
|
190
|
-
"rustc": "rustc",
|
|
191
|
-
"java": "default-jdk",
|
|
192
|
-
"gradle": "gradle",
|
|
193
|
-
"mvn": "maven",
|
|
175
|
+
"python": ("python3", "apt"),
|
|
176
|
+
"python3": ("python3", "apt"),
|
|
177
|
+
"pip": ("python3-pip", "apt"),
|
|
178
|
+
"node": ("nodejs", "apt"),
|
|
179
|
+
"npm": ("npm", "apt"),
|
|
180
|
+
"yarn": ("yarnpkg", "apt"),
|
|
181
|
+
"docker": ("docker.io", "apt"),
|
|
182
|
+
"dockerd": ("docker.io", "apt"),
|
|
183
|
+
"docker-compose": ("docker-compose", "apt"),
|
|
184
|
+
"podman": ("podman", "apt"),
|
|
185
|
+
"nginx": ("nginx", "apt"),
|
|
186
|
+
"apache2": ("apache2", "apt"),
|
|
187
|
+
"httpd": ("apache2", "apt"),
|
|
188
|
+
"postgres": ("postgresql", "apt"),
|
|
189
|
+
"postgresql": ("postgresql", "apt"),
|
|
190
|
+
"mysql": ("mysql-server", "apt"),
|
|
191
|
+
"mysqld": ("mysql-server", "apt"),
|
|
192
|
+
"mongod": ("mongodb", "apt"),
|
|
193
|
+
"mongodb": ("mongodb", "apt"),
|
|
194
|
+
"redis-server": ("redis-server", "apt"),
|
|
195
|
+
"redis": ("redis-server", "apt"),
|
|
196
|
+
"vim": ("vim", "apt"),
|
|
197
|
+
"nvim": ("neovim", "apt"),
|
|
198
|
+
"emacs": ("emacs", "apt"),
|
|
199
|
+
"firefox": ("firefox", "apt"),
|
|
200
|
+
"chromium": ("chromium-browser", "apt"),
|
|
201
|
+
"jupyter": ("jupyter-notebook", "apt"),
|
|
202
|
+
"jupyter-lab": ("jupyterlab", "apt"),
|
|
203
|
+
"gunicorn": ("gunicorn", "apt"),
|
|
204
|
+
"uvicorn": ("uvicorn", "apt"),
|
|
205
|
+
"tmux": ("tmux", "apt"),
|
|
206
|
+
"screen": ("screen", "apt"),
|
|
207
|
+
"git": ("git", "apt"),
|
|
208
|
+
"curl": ("curl", "apt"),
|
|
209
|
+
"wget": ("wget", "apt"),
|
|
210
|
+
"ssh": ("openssh-client", "apt"),
|
|
211
|
+
"sshd": ("openssh-server", "apt"),
|
|
212
|
+
"go": ("golang", "apt"),
|
|
213
|
+
"cargo": ("cargo", "apt"),
|
|
214
|
+
"rustc": ("rustc", "apt"),
|
|
215
|
+
"java": ("default-jdk", "apt"),
|
|
216
|
+
"gradle": ("gradle", "apt"),
|
|
217
|
+
"mvn": ("maven", "apt"),
|
|
218
|
+
# Popular desktop apps (snap packages)
|
|
219
|
+
"chrome": ("chromium", "snap"),
|
|
220
|
+
"google-chrome": ("chromium", "snap"),
|
|
221
|
+
"pycharm": ("pycharm-community", "snap"),
|
|
222
|
+
"idea": ("intellij-idea-community", "snap"),
|
|
223
|
+
"code": ("code", "snap"),
|
|
224
|
+
"vscode": ("code", "snap"),
|
|
225
|
+
"slack": ("slack", "snap"),
|
|
226
|
+
"discord": ("discord", "snap"),
|
|
227
|
+
"spotify": ("spotify", "snap"),
|
|
228
|
+
"vlc": ("vlc", "apt"),
|
|
229
|
+
"gimp": ("gimp", "apt"),
|
|
230
|
+
"inkscape": ("inkscape", "apt"),
|
|
231
|
+
"blender": ("blender", "apt"),
|
|
232
|
+
"obs": ("obs-studio", "apt"),
|
|
233
|
+
"telegram": ("telegram-desktop", "snap"),
|
|
234
|
+
"postman": ("postman", "snap"),
|
|
235
|
+
"insomnia": ("insomnia", "snap"),
|
|
236
|
+
"dbeaver": ("dbeaver-ce", "snap"),
|
|
237
|
+
"sublime": ("sublime-text", "snap"),
|
|
238
|
+
"atom": ("atom", "snap"),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Map applications to their config/data directories for complete cloning
|
|
242
|
+
# These directories contain user settings, extensions, profiles, credentials
|
|
243
|
+
APP_DATA_DIRS = {
|
|
244
|
+
# Browsers - profiles, extensions, bookmarks, passwords
|
|
245
|
+
"chrome": [".config/google-chrome", ".config/chromium"],
|
|
246
|
+
"chromium": [".config/chromium"],
|
|
247
|
+
"firefox": [".mozilla/firefox", ".cache/mozilla/firefox"],
|
|
248
|
+
|
|
249
|
+
# IDEs and editors - settings, extensions, projects history
|
|
250
|
+
"code": [".config/Code", ".vscode", ".vscode-server"],
|
|
251
|
+
"vscode": [".config/Code", ".vscode", ".vscode-server"],
|
|
252
|
+
"pycharm": [".config/JetBrains", ".local/share/JetBrains", ".cache/JetBrains"],
|
|
253
|
+
"idea": [".config/JetBrains", ".local/share/JetBrains"],
|
|
254
|
+
"webstorm": [".config/JetBrains", ".local/share/JetBrains"],
|
|
255
|
+
"goland": [".config/JetBrains", ".local/share/JetBrains"],
|
|
256
|
+
"sublime": [".config/sublime-text", ".config/sublime-text-3"],
|
|
257
|
+
"atom": [".atom"],
|
|
258
|
+
"vim": [".vim", ".vimrc", ".config/nvim"],
|
|
259
|
+
"nvim": [".config/nvim", ".local/share/nvim"],
|
|
260
|
+
"emacs": [".emacs.d", ".emacs"],
|
|
261
|
+
"cursor": [".config/Cursor", ".cursor"],
|
|
262
|
+
|
|
263
|
+
# Development tools
|
|
264
|
+
"docker": [".docker"],
|
|
265
|
+
"git": [".gitconfig", ".git-credentials", ".config/git"],
|
|
266
|
+
"npm": [".npm", ".npmrc"],
|
|
267
|
+
"yarn": [".yarn", ".yarnrc"],
|
|
268
|
+
"pip": [".pip", ".config/pip"],
|
|
269
|
+
"cargo": [".cargo"],
|
|
270
|
+
"rustup": [".rustup"],
|
|
271
|
+
"go": [".go", "go"],
|
|
272
|
+
"gradle": [".gradle"],
|
|
273
|
+
"maven": [".m2"],
|
|
274
|
+
|
|
275
|
+
# Python environments
|
|
276
|
+
"python": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
|
|
277
|
+
"python3": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
|
|
278
|
+
"conda": [".conda", "anaconda3", "miniconda3"],
|
|
279
|
+
|
|
280
|
+
# Node.js
|
|
281
|
+
"node": [".nvm", ".node", ".npm"],
|
|
282
|
+
|
|
283
|
+
# Databases
|
|
284
|
+
"postgres": [".pgpass", ".psqlrc", ".psql_history"],
|
|
285
|
+
"mysql": [".my.cnf", ".mysql_history"],
|
|
286
|
+
"mongodb": [".mongorc.js", ".dbshell"],
|
|
287
|
+
"redis": [".rediscli_history"],
|
|
288
|
+
|
|
289
|
+
# Communication apps
|
|
290
|
+
"slack": [".config/Slack"],
|
|
291
|
+
"discord": [".config/discord"],
|
|
292
|
+
"telegram": [".local/share/TelegramDesktop"],
|
|
293
|
+
"teams": [".config/Microsoft/Microsoft Teams"],
|
|
294
|
+
|
|
295
|
+
# Other tools
|
|
296
|
+
"postman": [".config/Postman"],
|
|
297
|
+
"insomnia": [".config/Insomnia"],
|
|
298
|
+
"dbeaver": [".local/share/DBeaverData"],
|
|
299
|
+
"ssh": [".ssh"],
|
|
300
|
+
"gpg": [".gnupg"],
|
|
301
|
+
"aws": [".aws"],
|
|
302
|
+
"gcloud": [".config/gcloud"],
|
|
303
|
+
"kubectl": [".kube"],
|
|
304
|
+
"terraform": [".terraform.d"],
|
|
305
|
+
"ansible": [".ansible"],
|
|
306
|
+
|
|
307
|
+
# General app data
|
|
308
|
+
"spotify": [".config/spotify"],
|
|
309
|
+
"vlc": [".config/vlc"],
|
|
310
|
+
"gimp": [".config/GIMP", ".gimp-2.10"],
|
|
311
|
+
"obs": [".config/obs-studio"],
|
|
194
312
|
}
|
|
195
313
|
|
|
196
314
|
def __init__(self):
|
|
197
315
|
self.user = pwd.getpwuid(os.getuid()).pw_name
|
|
198
316
|
self.home = Path.home()
|
|
199
317
|
|
|
318
|
+
def detect_app_data_dirs(self, applications: list) -> list:
|
|
319
|
+
"""Detect config/data directories for running applications.
|
|
320
|
+
|
|
321
|
+
Returns list of paths that contain user data needed by running apps.
|
|
322
|
+
"""
|
|
323
|
+
app_data_paths = []
|
|
324
|
+
seen_paths = set()
|
|
325
|
+
|
|
326
|
+
for app in applications:
|
|
327
|
+
app_name = app.name.lower()
|
|
328
|
+
|
|
329
|
+
# Check each known app pattern
|
|
330
|
+
for pattern, dirs in self.APP_DATA_DIRS.items():
|
|
331
|
+
if pattern in app_name:
|
|
332
|
+
for dir_name in dirs:
|
|
333
|
+
full_path = self.home / dir_name
|
|
334
|
+
if full_path.exists() and str(full_path) not in seen_paths:
|
|
335
|
+
seen_paths.add(str(full_path))
|
|
336
|
+
# Calculate size
|
|
337
|
+
try:
|
|
338
|
+
size = self._get_dir_size(full_path, max_depth=2)
|
|
339
|
+
except:
|
|
340
|
+
size = 0
|
|
341
|
+
app_data_paths.append({
|
|
342
|
+
"path": str(full_path),
|
|
343
|
+
"app": app.name,
|
|
344
|
+
"type": "app_data",
|
|
345
|
+
"size_mb": round(size / 1024 / 1024, 1)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
return app_data_paths
|
|
349
|
+
|
|
200
350
|
def detect_all(self) -> SystemSnapshot:
|
|
201
351
|
"""Detect all services, applications and paths."""
|
|
202
352
|
return SystemSnapshot(
|
|
@@ -437,29 +587,53 @@ class SystemDetector:
|
|
|
437
587
|
pass
|
|
438
588
|
return containers
|
|
439
589
|
|
|
440
|
-
def suggest_packages_for_apps(self, applications: list) ->
|
|
441
|
-
"""Suggest
|
|
442
|
-
|
|
590
|
+
def suggest_packages_for_apps(self, applications: list) -> dict:
|
|
591
|
+
"""Suggest packages based on detected applications.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
dict with 'apt' and 'snap' keys containing lists of packages
|
|
595
|
+
"""
|
|
596
|
+
apt_packages = set()
|
|
597
|
+
snap_packages = set()
|
|
598
|
+
|
|
443
599
|
for app in applications:
|
|
444
600
|
app_name = app.name.lower()
|
|
445
|
-
|
|
446
|
-
for key, package in self.APP_TO_PACKAGE_MAP.items():
|
|
601
|
+
for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
|
|
447
602
|
if key in app_name:
|
|
448
|
-
|
|
603
|
+
if install_type == "snap":
|
|
604
|
+
snap_packages.add(package)
|
|
605
|
+
else:
|
|
606
|
+
apt_packages.add(package)
|
|
449
607
|
break
|
|
450
|
-
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
"apt": sorted(list(apt_packages)),
|
|
611
|
+
"snap": sorted(list(snap_packages))
|
|
612
|
+
}
|
|
451
613
|
|
|
452
|
-
def suggest_packages_for_services(self, services: list) ->
|
|
453
|
-
"""Suggest
|
|
454
|
-
|
|
614
|
+
def suggest_packages_for_services(self, services: list) -> dict:
|
|
615
|
+
"""Suggest packages based on detected services.
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
dict with 'apt' and 'snap' keys containing lists of packages
|
|
619
|
+
"""
|
|
620
|
+
apt_packages = set()
|
|
621
|
+
snap_packages = set()
|
|
622
|
+
|
|
455
623
|
for service in services:
|
|
456
624
|
service_name = service.name.lower()
|
|
457
|
-
|
|
458
|
-
for key, package in self.APP_TO_PACKAGE_MAP.items():
|
|
625
|
+
for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
|
|
459
626
|
if key in service_name:
|
|
460
|
-
|
|
627
|
+
if install_type == "snap":
|
|
628
|
+
snap_packages.add(package)
|
|
629
|
+
else:
|
|
630
|
+
apt_packages.add(package)
|
|
461
631
|
break
|
|
462
|
-
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
"apt": sorted(list(apt_packages)),
|
|
635
|
+
"snap": sorted(list(snap_packages))
|
|
636
|
+
}
|
|
463
637
|
|
|
464
638
|
def get_system_info(self) -> dict:
|
|
465
639
|
"""Get basic system information."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clonebox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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
|
|
@@ -38,6 +38,7 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
|
38
38
|
Dynamic: license-file
|
|
39
39
|
|
|
40
40
|
# CloneBox 📦
|
|
41
|
+

|
|
41
42
|
|
|
42
43
|
```commandline
|
|
43
44
|
╔═══════════════════════════════════════════════════════╗
|
|
@@ -50,10 +50,16 @@ class TestNetworkMode:
|
|
|
50
50
|
@patch("clonebox.cloner.libvirt")
|
|
51
51
|
def test_resolve_network_mode_auto_user_no_default(self, mock_libvirt):
|
|
52
52
|
"""Test auto mode with user session and no default network falls back to user."""
|
|
53
|
-
|
|
53
|
+
# Handle missing libvirt module in test environment
|
|
54
|
+
try:
|
|
55
|
+
import libvirt as real_libvirt
|
|
56
|
+
libvirt_error = real_libvirt.libvirtError
|
|
57
|
+
except ImportError:
|
|
58
|
+
class libvirt_error(Exception):
|
|
59
|
+
pass
|
|
54
60
|
|
|
55
61
|
mock_conn = MagicMock()
|
|
56
|
-
mock_conn.networkLookupByName.side_effect =
|
|
62
|
+
mock_conn.networkLookupByName.side_effect = libvirt_error("No network")
|
|
57
63
|
mock_libvirt.open.return_value = mock_conn
|
|
58
64
|
|
|
59
65
|
cloner = SelectiveVMCloner(user_session=True)
|
|
@@ -126,10 +132,16 @@ class TestNetworkMode:
|
|
|
126
132
|
@patch("clonebox.cloner.libvirt")
|
|
127
133
|
def test_default_network_active_not_found(self, mock_libvirt):
|
|
128
134
|
"""Test _default_network_active returns False when network not found."""
|
|
129
|
-
|
|
135
|
+
# Handle missing libvirt module in test environment
|
|
136
|
+
try:
|
|
137
|
+
import libvirt as real_libvirt
|
|
138
|
+
libvirt_error = real_libvirt.libvirtError
|
|
139
|
+
except ImportError:
|
|
140
|
+
class libvirt_error(Exception):
|
|
141
|
+
pass
|
|
130
142
|
|
|
131
143
|
mock_conn = MagicMock()
|
|
132
|
-
mock_conn.networkLookupByName.side_effect =
|
|
144
|
+
mock_conn.networkLookupByName.side_effect = libvirt_error("Not found")
|
|
133
145
|
mock_libvirt.open.return_value = mock_conn
|
|
134
146
|
|
|
135
147
|
cloner = SelectiveVMCloner()
|
|
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
|