clonebox 1.1.2__py3-none-any.whl → 1.1.4__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 +359 -0
- clonebox/exporter.py +189 -0
- clonebox/health/__init__.py +16 -0
- clonebox/health/models.py +194 -0
- clonebox/importer.py +220 -0
- clonebox/monitor.py +269 -0
- clonebox/p2p.py +184 -0
- clonebox/snapshots/__init__.py +12 -0
- clonebox/snapshots/manager.py +355 -0
- clonebox/snapshots/models.py +187 -0
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/METADATA +28 -2
- clonebox-1.1.4.dist-info/RECORD +27 -0
- clonebox-1.1.2.dist-info/RECORD +0 -18
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/WHEEL +0 -0
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/entry_points.txt +0 -0
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/licenses/LICENSE +0 -0
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/top_level.txt +0 -0
clonebox/cli.py
CHANGED
|
@@ -28,6 +28,10 @@ from clonebox.container import ContainerCloner
|
|
|
28
28
|
from clonebox.detector import SystemDetector
|
|
29
29
|
from clonebox.models import ContainerConfig
|
|
30
30
|
from clonebox.profiles import merge_with_profile
|
|
31
|
+
from clonebox.exporter import SecureExporter, VMExporter
|
|
32
|
+
from clonebox.importer import SecureImporter, VMImporter
|
|
33
|
+
from clonebox.monitor import ResourceMonitor, format_bytes
|
|
34
|
+
from clonebox.p2p import P2PManager
|
|
31
35
|
|
|
32
36
|
# Custom questionary style
|
|
33
37
|
custom_style = Style(
|
|
@@ -2635,6 +2639,239 @@ def cmd_detect(args):
|
|
|
2635
2639
|
console.print(table)
|
|
2636
2640
|
|
|
2637
2641
|
|
|
2642
|
+
def cmd_monitor(args) -> None:
|
|
2643
|
+
"""Real-time resource monitoring for VMs and containers."""
|
|
2644
|
+
conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
|
|
2645
|
+
refresh = getattr(args, "refresh", 2.0)
|
|
2646
|
+
once = getattr(args, "once", False)
|
|
2647
|
+
|
|
2648
|
+
monitor = ResourceMonitor(conn_uri)
|
|
2649
|
+
|
|
2650
|
+
try:
|
|
2651
|
+
while True:
|
|
2652
|
+
# Clear screen for live update
|
|
2653
|
+
if not once:
|
|
2654
|
+
console.clear()
|
|
2655
|
+
|
|
2656
|
+
console.print("[bold cyan]📊 CloneBox Resource Monitor[/]")
|
|
2657
|
+
console.print()
|
|
2658
|
+
|
|
2659
|
+
# VM Stats
|
|
2660
|
+
vm_stats = monitor.get_all_vm_stats()
|
|
2661
|
+
if vm_stats:
|
|
2662
|
+
table = Table(title="🖥️ Virtual Machines", border_style="cyan")
|
|
2663
|
+
table.add_column("Name", style="bold")
|
|
2664
|
+
table.add_column("State")
|
|
2665
|
+
table.add_column("CPU %")
|
|
2666
|
+
table.add_column("Memory")
|
|
2667
|
+
table.add_column("Disk")
|
|
2668
|
+
table.add_column("Network I/O")
|
|
2669
|
+
|
|
2670
|
+
for vm in vm_stats:
|
|
2671
|
+
state_color = "green" if vm.state == "running" else "yellow"
|
|
2672
|
+
cpu_color = "red" if vm.cpu_percent > 80 else "green"
|
|
2673
|
+
mem_pct = (vm.memory_used_mb / vm.memory_total_mb * 100) if vm.memory_total_mb > 0 else 0
|
|
2674
|
+
mem_color = "red" if mem_pct > 80 else "green"
|
|
2675
|
+
|
|
2676
|
+
table.add_row(
|
|
2677
|
+
vm.name,
|
|
2678
|
+
f"[{state_color}]{vm.state}[/]",
|
|
2679
|
+
f"[{cpu_color}]{vm.cpu_percent:.1f}%[/]",
|
|
2680
|
+
f"[{mem_color}]{vm.memory_used_mb}/{vm.memory_total_mb} MB[/]",
|
|
2681
|
+
f"{vm.disk_used_gb:.1f}/{vm.disk_total_gb:.1f} GB",
|
|
2682
|
+
f"↓{format_bytes(vm.network_rx_bytes)} ↑{format_bytes(vm.network_tx_bytes)}",
|
|
2683
|
+
)
|
|
2684
|
+
console.print(table)
|
|
2685
|
+
else:
|
|
2686
|
+
console.print("[dim]No VMs found.[/]")
|
|
2687
|
+
|
|
2688
|
+
console.print()
|
|
2689
|
+
|
|
2690
|
+
# Container Stats
|
|
2691
|
+
container_stats = monitor.get_container_stats()
|
|
2692
|
+
if container_stats:
|
|
2693
|
+
table = Table(title="🐳 Containers", border_style="blue")
|
|
2694
|
+
table.add_column("Name", style="bold")
|
|
2695
|
+
table.add_column("State")
|
|
2696
|
+
table.add_column("CPU %")
|
|
2697
|
+
table.add_column("Memory")
|
|
2698
|
+
table.add_column("Network I/O")
|
|
2699
|
+
table.add_column("PIDs")
|
|
2700
|
+
|
|
2701
|
+
for c in container_stats:
|
|
2702
|
+
cpu_color = "red" if c.cpu_percent > 80 else "green"
|
|
2703
|
+
mem_pct = (c.memory_used_mb / c.memory_limit_mb * 100) if c.memory_limit_mb > 0 else 0
|
|
2704
|
+
mem_color = "red" if mem_pct > 80 else "green"
|
|
2705
|
+
|
|
2706
|
+
table.add_row(
|
|
2707
|
+
c.name,
|
|
2708
|
+
f"[green]{c.state}[/]",
|
|
2709
|
+
f"[{cpu_color}]{c.cpu_percent:.1f}%[/]",
|
|
2710
|
+
f"[{mem_color}]{c.memory_used_mb}/{c.memory_limit_mb} MB[/]",
|
|
2711
|
+
f"↓{format_bytes(c.network_rx_bytes)} ↑{format_bytes(c.network_tx_bytes)}",
|
|
2712
|
+
str(c.pids),
|
|
2713
|
+
)
|
|
2714
|
+
console.print(table)
|
|
2715
|
+
else:
|
|
2716
|
+
console.print("[dim]No containers running.[/]")
|
|
2717
|
+
|
|
2718
|
+
if once:
|
|
2719
|
+
break
|
|
2720
|
+
|
|
2721
|
+
console.print(f"\n[dim]Refreshing every {refresh}s. Press Ctrl+C to exit.[/]")
|
|
2722
|
+
time.sleep(refresh)
|
|
2723
|
+
|
|
2724
|
+
except KeyboardInterrupt:
|
|
2725
|
+
console.print("\n[yellow]Monitoring stopped.[/]")
|
|
2726
|
+
finally:
|
|
2727
|
+
monitor.close()
|
|
2728
|
+
|
|
2729
|
+
|
|
2730
|
+
def cmd_exec(args) -> None:
|
|
2731
|
+
"""Execute command in VM via QEMU Guest Agent."""
|
|
2732
|
+
vm_name, config_file = _resolve_vm_name_and_config_file(args.name)
|
|
2733
|
+
conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
|
|
2734
|
+
command = args.command
|
|
2735
|
+
timeout = getattr(args, "timeout", 30)
|
|
2736
|
+
|
|
2737
|
+
if not _qga_ping(vm_name, conn_uri):
|
|
2738
|
+
console.print(f"[red]❌ Cannot connect to VM '{vm_name}' via QEMU Guest Agent[/]")
|
|
2739
|
+
console.print("[dim]Make sure the VM is running and qemu-guest-agent is installed.[/]")
|
|
2740
|
+
return
|
|
2741
|
+
|
|
2742
|
+
console.print(f"[cyan]▶ Executing in {vm_name}:[/] {command}")
|
|
2743
|
+
|
|
2744
|
+
result = _qga_exec(vm_name, conn_uri, command, timeout=timeout)
|
|
2745
|
+
|
|
2746
|
+
if result is None:
|
|
2747
|
+
console.print("[red]❌ Command execution failed or timed out[/]")
|
|
2748
|
+
elif result == "":
|
|
2749
|
+
console.print("[dim](no output)[/]")
|
|
2750
|
+
else:
|
|
2751
|
+
console.print(result)
|
|
2752
|
+
|
|
2753
|
+
|
|
2754
|
+
def cmd_keygen(args) -> None:
|
|
2755
|
+
"""Generate encryption key for secure P2P transfers."""
|
|
2756
|
+
key_path = SecureExporter.generate_key()
|
|
2757
|
+
console.print(f"[green]🔑 Encryption key generated: {key_path}[/]")
|
|
2758
|
+
console.print("[dim]Share this key with team members for encrypted transfers.[/]")
|
|
2759
|
+
|
|
2760
|
+
|
|
2761
|
+
def cmd_export_encrypted(args) -> None:
|
|
2762
|
+
"""Export VM with AES-256 encryption."""
|
|
2763
|
+
vm_name, config_file = _resolve_vm_name_and_config_file(args.name)
|
|
2764
|
+
conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
|
|
2765
|
+
output = Path(args.output) if args.output else Path(f"{vm_name}.enc")
|
|
2766
|
+
|
|
2767
|
+
console.print(f"[cyan]🔒 Exporting encrypted: {vm_name} → {output}[/]")
|
|
2768
|
+
|
|
2769
|
+
try:
|
|
2770
|
+
exporter = SecureExporter(conn_uri)
|
|
2771
|
+
exporter.export_encrypted(
|
|
2772
|
+
vm_name=vm_name,
|
|
2773
|
+
output_path=output,
|
|
2774
|
+
include_user_data=getattr(args, "user_data", False),
|
|
2775
|
+
include_app_data=getattr(args, "include_data", False),
|
|
2776
|
+
)
|
|
2777
|
+
console.print(f"[green]✅ Encrypted export complete: {output}[/]")
|
|
2778
|
+
except FileNotFoundError as e:
|
|
2779
|
+
console.print(f"[red]❌ {e}[/]")
|
|
2780
|
+
console.print("[yellow]Run: clonebox keygen[/]")
|
|
2781
|
+
finally:
|
|
2782
|
+
exporter.close()
|
|
2783
|
+
|
|
2784
|
+
|
|
2785
|
+
def cmd_import_encrypted(args) -> None:
|
|
2786
|
+
"""Import VM with AES-256 decryption."""
|
|
2787
|
+
archive = Path(args.archive)
|
|
2788
|
+
conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
|
|
2789
|
+
|
|
2790
|
+
console.print(f"[cyan]🔓 Importing encrypted: {archive}[/]")
|
|
2791
|
+
|
|
2792
|
+
try:
|
|
2793
|
+
importer = SecureImporter(conn_uri)
|
|
2794
|
+
vm_name = importer.import_decrypted(
|
|
2795
|
+
encrypted_path=archive,
|
|
2796
|
+
import_user_data=getattr(args, "user_data", False),
|
|
2797
|
+
import_app_data=getattr(args, "include_data", False),
|
|
2798
|
+
new_name=getattr(args, "name", None),
|
|
2799
|
+
)
|
|
2800
|
+
console.print(f"[green]✅ Import complete: {vm_name}[/]")
|
|
2801
|
+
except FileNotFoundError as e:
|
|
2802
|
+
console.print(f"[red]❌ {e}[/]")
|
|
2803
|
+
finally:
|
|
2804
|
+
importer.close()
|
|
2805
|
+
|
|
2806
|
+
|
|
2807
|
+
def cmd_export_remote(args) -> None:
|
|
2808
|
+
"""Export VM from remote host."""
|
|
2809
|
+
p2p = P2PManager()
|
|
2810
|
+
|
|
2811
|
+
console.print(f"[cyan]📤 Remote export: {args.host}:{args.vm_name}[/]")
|
|
2812
|
+
|
|
2813
|
+
try:
|
|
2814
|
+
output = Path(args.output)
|
|
2815
|
+
p2p.export_remote(
|
|
2816
|
+
host=args.host,
|
|
2817
|
+
vm_name=args.vm_name,
|
|
2818
|
+
output=output,
|
|
2819
|
+
encrypted=getattr(args, "encrypted", False),
|
|
2820
|
+
include_user_data=getattr(args, "user_data", False),
|
|
2821
|
+
include_app_data=getattr(args, "include_data", False),
|
|
2822
|
+
)
|
|
2823
|
+
console.print(f"[green]✅ Remote export complete: {output}[/]")
|
|
2824
|
+
except RuntimeError as e:
|
|
2825
|
+
console.print(f"[red]❌ {e}[/]")
|
|
2826
|
+
|
|
2827
|
+
|
|
2828
|
+
def cmd_import_remote(args) -> None:
|
|
2829
|
+
"""Import VM to remote host."""
|
|
2830
|
+
p2p = P2PManager()
|
|
2831
|
+
archive = Path(args.archive)
|
|
2832
|
+
|
|
2833
|
+
console.print(f"[cyan]📥 Remote import: {archive} → {args.host}[/]")
|
|
2834
|
+
|
|
2835
|
+
try:
|
|
2836
|
+
p2p.import_remote(
|
|
2837
|
+
host=args.host,
|
|
2838
|
+
archive_path=archive,
|
|
2839
|
+
encrypted=getattr(args, "encrypted", False),
|
|
2840
|
+
import_user_data=getattr(args, "user_data", False),
|
|
2841
|
+
new_name=getattr(args, "name", None),
|
|
2842
|
+
)
|
|
2843
|
+
console.print(f"[green]✅ Remote import complete[/]")
|
|
2844
|
+
except RuntimeError as e:
|
|
2845
|
+
console.print(f"[red]❌ {e}[/]")
|
|
2846
|
+
|
|
2847
|
+
|
|
2848
|
+
def cmd_sync_key(args) -> None:
|
|
2849
|
+
"""Sync encryption key to remote host."""
|
|
2850
|
+
p2p = P2PManager()
|
|
2851
|
+
|
|
2852
|
+
console.print(f"[cyan]🔑 Syncing key to: {args.host}[/]")
|
|
2853
|
+
|
|
2854
|
+
try:
|
|
2855
|
+
p2p.sync_key(args.host)
|
|
2856
|
+
console.print(f"[green]✅ Key synced to {args.host}[/]")
|
|
2857
|
+
except (FileNotFoundError, RuntimeError) as e:
|
|
2858
|
+
console.print(f"[red]❌ {e}[/]")
|
|
2859
|
+
|
|
2860
|
+
|
|
2861
|
+
def cmd_list_remote(args) -> None:
|
|
2862
|
+
"""List VMs on remote host."""
|
|
2863
|
+
p2p = P2PManager()
|
|
2864
|
+
|
|
2865
|
+
console.print(f"[cyan]🔍 Listing VMs on: {args.host}[/]")
|
|
2866
|
+
|
|
2867
|
+
vms = p2p.list_remote_vms(args.host)
|
|
2868
|
+
if vms:
|
|
2869
|
+
for vm in vms:
|
|
2870
|
+
console.print(f" • {vm}")
|
|
2871
|
+
else:
|
|
2872
|
+
console.print("[yellow]No VMs found on remote host.[/]")
|
|
2873
|
+
|
|
2874
|
+
|
|
2638
2875
|
def main():
|
|
2639
2876
|
"""Main entry point."""
|
|
2640
2877
|
parser = argparse.ArgumentParser(
|
|
@@ -3074,6 +3311,128 @@ def main():
|
|
|
3074
3311
|
)
|
|
3075
3312
|
test_parser.set_defaults(func=cmd_test)
|
|
3076
3313
|
|
|
3314
|
+
# Monitor command - real-time resource monitoring
|
|
3315
|
+
monitor_parser = subparsers.add_parser("monitor", help="Real-time resource monitoring")
|
|
3316
|
+
monitor_parser.add_argument(
|
|
3317
|
+
"-u", "--user", action="store_true", help="Use user session (qemu:///session)"
|
|
3318
|
+
)
|
|
3319
|
+
monitor_parser.add_argument(
|
|
3320
|
+
"--refresh", "-r", type=float, default=2.0, help="Refresh interval in seconds (default: 2)"
|
|
3321
|
+
)
|
|
3322
|
+
monitor_parser.add_argument(
|
|
3323
|
+
"--once", action="store_true", help="Show stats once and exit"
|
|
3324
|
+
)
|
|
3325
|
+
monitor_parser.set_defaults(func=cmd_monitor)
|
|
3326
|
+
|
|
3327
|
+
# Exec command - execute command in VM
|
|
3328
|
+
exec_parser = subparsers.add_parser("exec", help="Execute command in VM via QEMU Guest Agent")
|
|
3329
|
+
exec_parser.add_argument(
|
|
3330
|
+
"name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
|
|
3331
|
+
)
|
|
3332
|
+
exec_parser.add_argument(
|
|
3333
|
+
"command", help="Command to execute in VM"
|
|
3334
|
+
)
|
|
3335
|
+
exec_parser.add_argument(
|
|
3336
|
+
"-u", "--user", action="store_true", help="Use user session (qemu:///session)"
|
|
3337
|
+
)
|
|
3338
|
+
exec_parser.add_argument(
|
|
3339
|
+
"--timeout", "-t", type=int, default=30, help="Command timeout in seconds (default: 30)"
|
|
3340
|
+
)
|
|
3341
|
+
exec_parser.set_defaults(func=cmd_exec)
|
|
3342
|
+
|
|
3343
|
+
# === P2P Secure Transfer Commands ===
|
|
3344
|
+
|
|
3345
|
+
# Keygen command - generate encryption key
|
|
3346
|
+
keygen_parser = subparsers.add_parser("keygen", help="Generate encryption key for secure transfers")
|
|
3347
|
+
keygen_parser.set_defaults(func=cmd_keygen)
|
|
3348
|
+
|
|
3349
|
+
# Export-encrypted command
|
|
3350
|
+
export_enc_parser = subparsers.add_parser(
|
|
3351
|
+
"export-encrypted", help="Export VM with AES-256 encryption"
|
|
3352
|
+
)
|
|
3353
|
+
export_enc_parser.add_argument(
|
|
3354
|
+
"name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
|
|
3355
|
+
)
|
|
3356
|
+
export_enc_parser.add_argument(
|
|
3357
|
+
"-u", "--user", action="store_true", help="Use user session (qemu:///session)"
|
|
3358
|
+
)
|
|
3359
|
+
export_enc_parser.add_argument(
|
|
3360
|
+
"-o", "--output", help="Output file (default: <vmname>.enc)"
|
|
3361
|
+
)
|
|
3362
|
+
export_enc_parser.add_argument(
|
|
3363
|
+
"--user-data", action="store_true", help="Include user data (SSH keys, configs)"
|
|
3364
|
+
)
|
|
3365
|
+
export_enc_parser.add_argument(
|
|
3366
|
+
"--include-data", "-d", action="store_true", help="Include app data"
|
|
3367
|
+
)
|
|
3368
|
+
export_enc_parser.set_defaults(func=cmd_export_encrypted)
|
|
3369
|
+
|
|
3370
|
+
# Import-encrypted command
|
|
3371
|
+
import_enc_parser = subparsers.add_parser(
|
|
3372
|
+
"import-encrypted", help="Import VM with AES-256 decryption"
|
|
3373
|
+
)
|
|
3374
|
+
import_enc_parser.add_argument("archive", help="Path to encrypted archive (.enc)")
|
|
3375
|
+
import_enc_parser.add_argument(
|
|
3376
|
+
"-u", "--user", action="store_true", help="Use user session (qemu:///session)"
|
|
3377
|
+
)
|
|
3378
|
+
import_enc_parser.add_argument("--name", "-n", help="New name for imported VM")
|
|
3379
|
+
import_enc_parser.add_argument(
|
|
3380
|
+
"--user-data", action="store_true", help="Import user data"
|
|
3381
|
+
)
|
|
3382
|
+
import_enc_parser.add_argument(
|
|
3383
|
+
"--include-data", "-d", action="store_true", help="Import app data"
|
|
3384
|
+
)
|
|
3385
|
+
import_enc_parser.set_defaults(func=cmd_import_encrypted)
|
|
3386
|
+
|
|
3387
|
+
# Export-remote command
|
|
3388
|
+
export_remote_parser = subparsers.add_parser(
|
|
3389
|
+
"export-remote", help="Export VM from remote host via SSH"
|
|
3390
|
+
)
|
|
3391
|
+
export_remote_parser.add_argument("host", help="Remote host (user@hostname)")
|
|
3392
|
+
export_remote_parser.add_argument("vm_name", help="VM name on remote host")
|
|
3393
|
+
export_remote_parser.add_argument(
|
|
3394
|
+
"-o", "--output", required=True, help="Local output file"
|
|
3395
|
+
)
|
|
3396
|
+
export_remote_parser.add_argument(
|
|
3397
|
+
"--encrypted", "-e", action="store_true", help="Use encrypted export"
|
|
3398
|
+
)
|
|
3399
|
+
export_remote_parser.add_argument(
|
|
3400
|
+
"--user-data", action="store_true", help="Include user data"
|
|
3401
|
+
)
|
|
3402
|
+
export_remote_parser.add_argument(
|
|
3403
|
+
"--include-data", "-d", action="store_true", help="Include app data"
|
|
3404
|
+
)
|
|
3405
|
+
export_remote_parser.set_defaults(func=cmd_export_remote)
|
|
3406
|
+
|
|
3407
|
+
# Import-remote command
|
|
3408
|
+
import_remote_parser = subparsers.add_parser(
|
|
3409
|
+
"import-remote", help="Import VM to remote host via SSH"
|
|
3410
|
+
)
|
|
3411
|
+
import_remote_parser.add_argument("archive", help="Local archive to upload")
|
|
3412
|
+
import_remote_parser.add_argument("host", help="Remote host (user@hostname)")
|
|
3413
|
+
import_remote_parser.add_argument("--name", "-n", help="New name for VM on remote")
|
|
3414
|
+
import_remote_parser.add_argument(
|
|
3415
|
+
"--encrypted", "-e", action="store_true", help="Use encrypted import"
|
|
3416
|
+
)
|
|
3417
|
+
import_remote_parser.add_argument(
|
|
3418
|
+
"--user-data", action="store_true", help="Import user data"
|
|
3419
|
+
)
|
|
3420
|
+
import_remote_parser.set_defaults(func=cmd_import_remote)
|
|
3421
|
+
|
|
3422
|
+
# Sync-key command
|
|
3423
|
+
sync_key_parser = subparsers.add_parser(
|
|
3424
|
+
"sync-key", help="Sync encryption key to remote host"
|
|
3425
|
+
)
|
|
3426
|
+
sync_key_parser.add_argument("host", help="Remote host (user@hostname)")
|
|
3427
|
+
sync_key_parser.set_defaults(func=cmd_sync_key)
|
|
3428
|
+
|
|
3429
|
+
# List-remote command
|
|
3430
|
+
list_remote_parser = subparsers.add_parser(
|
|
3431
|
+
"list-remote", help="List VMs on remote host"
|
|
3432
|
+
)
|
|
3433
|
+
list_remote_parser.add_argument("host", help="Remote host (user@hostname)")
|
|
3434
|
+
list_remote_parser.set_defaults(func=cmd_list_remote)
|
|
3435
|
+
|
|
3077
3436
|
args = parser.parse_args()
|
|
3078
3437
|
|
|
3079
3438
|
if hasattr(args, "func"):
|
clonebox/exporter.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
VM Exporter - Export VM with all data and optional AES-256 encryption.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import tarfile
|
|
8
|
+
import tempfile
|
|
9
|
+
import xml.etree.ElementTree as ET
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from cryptography.fernet import Fernet
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import libvirt
|
|
17
|
+
except ImportError:
|
|
18
|
+
libvirt = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VMExporter:
|
|
22
|
+
"""Export VM with disks, app data, and user data."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, conn_uri: str = "qemu:///system"):
|
|
25
|
+
self.conn_uri = conn_uri
|
|
26
|
+
self._conn = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def conn(self):
|
|
30
|
+
if self._conn is None:
|
|
31
|
+
if libvirt is None:
|
|
32
|
+
raise RuntimeError("libvirt-python not installed")
|
|
33
|
+
self._conn = libvirt.open(self.conn_uri)
|
|
34
|
+
return self._conn
|
|
35
|
+
|
|
36
|
+
def export_vm(
|
|
37
|
+
self,
|
|
38
|
+
vm_name: str,
|
|
39
|
+
output_path: Path,
|
|
40
|
+
include_user_data: bool = False,
|
|
41
|
+
include_app_data: bool = False,
|
|
42
|
+
) -> Path:
|
|
43
|
+
"""Full export of VM with disks and optional data."""
|
|
44
|
+
vm = self.conn.lookupByName(vm_name)
|
|
45
|
+
vm_xml = vm.XMLDesc()
|
|
46
|
+
root = ET.fromstring(vm_xml)
|
|
47
|
+
|
|
48
|
+
# Find all disk files
|
|
49
|
+
disks: List[Path] = []
|
|
50
|
+
for disk in root.findall(".//disk[@type='file']"):
|
|
51
|
+
source = disk.find(".//source")
|
|
52
|
+
if source is not None and source.get("file"):
|
|
53
|
+
disk_path = Path(source.get("file"))
|
|
54
|
+
if disk_path.exists():
|
|
55
|
+
disks.append(disk_path)
|
|
56
|
+
|
|
57
|
+
# Create archive
|
|
58
|
+
with tarfile.open(output_path, "w:gz") as tar:
|
|
59
|
+
# Add XML config
|
|
60
|
+
xml_tmp = Path(tempfile.gettempdir()) / f"{vm_name}.xml"
|
|
61
|
+
xml_tmp.write_text(vm_xml)
|
|
62
|
+
tar.add(xml_tmp, arcname=f"{vm_name}.xml")
|
|
63
|
+
xml_tmp.unlink()
|
|
64
|
+
|
|
65
|
+
# Add disks
|
|
66
|
+
for disk in disks:
|
|
67
|
+
arcname = f"disks/{disk.name}"
|
|
68
|
+
tar.add(disk, arcname=arcname)
|
|
69
|
+
print(f" 💾 Added disk: {disk}")
|
|
70
|
+
|
|
71
|
+
# Add app data
|
|
72
|
+
if include_app_data:
|
|
73
|
+
self._export_app_data(tar)
|
|
74
|
+
|
|
75
|
+
# Add user data
|
|
76
|
+
if include_user_data:
|
|
77
|
+
self._export_user_data(tar)
|
|
78
|
+
|
|
79
|
+
return output_path
|
|
80
|
+
|
|
81
|
+
def _export_app_data(self, tar: tarfile.TarFile) -> None:
|
|
82
|
+
"""Export common application data paths."""
|
|
83
|
+
common_paths = [
|
|
84
|
+
Path.home() / "projects",
|
|
85
|
+
Path.home() / ".docker",
|
|
86
|
+
Path("/opt/myapp"),
|
|
87
|
+
Path("/var/www"),
|
|
88
|
+
Path("/srv/docker"),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
for path in common_paths:
|
|
92
|
+
if path.exists():
|
|
93
|
+
arcname = f"app-data/{path.name}"
|
|
94
|
+
try:
|
|
95
|
+
tar.add(path, arcname=arcname, recursive=True)
|
|
96
|
+
print(f" 📁 App data: {path}")
|
|
97
|
+
except PermissionError:
|
|
98
|
+
print(f" ⚠️ Permission denied: {path}")
|
|
99
|
+
|
|
100
|
+
def _export_user_data(self, tar: tarfile.TarFile) -> None:
|
|
101
|
+
"""Export user data (home, SSH keys)."""
|
|
102
|
+
user_paths = [
|
|
103
|
+
Path.home() / ".ssh",
|
|
104
|
+
Path.home() / ".gitconfig",
|
|
105
|
+
Path.home() / ".bashrc",
|
|
106
|
+
Path.home() / ".zshrc",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
for path in user_paths:
|
|
110
|
+
if path.exists():
|
|
111
|
+
arcname = f"user-data/{path.name}"
|
|
112
|
+
try:
|
|
113
|
+
tar.add(path, arcname=arcname, recursive=True)
|
|
114
|
+
print(f" 👤 User data: {path}")
|
|
115
|
+
except PermissionError:
|
|
116
|
+
print(f" ⚠️ Permission denied: {path}")
|
|
117
|
+
|
|
118
|
+
def close(self) -> None:
|
|
119
|
+
if self._conn is not None:
|
|
120
|
+
self._conn.close()
|
|
121
|
+
self._conn = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SecureExporter:
|
|
125
|
+
"""AES-256 encrypted VM export."""
|
|
126
|
+
|
|
127
|
+
KEY_PATH = Path.home() / ".clonebox.key"
|
|
128
|
+
|
|
129
|
+
def __init__(self, conn_uri: str = "qemu:///system"):
|
|
130
|
+
self.exporter = VMExporter(conn_uri)
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def generate_key(cls) -> Path:
|
|
134
|
+
"""Generate and save team encryption key."""
|
|
135
|
+
key = Fernet.generate_key()
|
|
136
|
+
cls.KEY_PATH.write_bytes(key)
|
|
137
|
+
os.chmod(str(cls.KEY_PATH), 0o600)
|
|
138
|
+
return cls.KEY_PATH
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def load_key(cls) -> Optional[bytes]:
|
|
142
|
+
"""Load encryption key from file."""
|
|
143
|
+
if cls.KEY_PATH.exists():
|
|
144
|
+
return cls.KEY_PATH.read_bytes()
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def export_encrypted(
|
|
148
|
+
self,
|
|
149
|
+
vm_name: str,
|
|
150
|
+
output_path: Path,
|
|
151
|
+
include_user_data: bool = False,
|
|
152
|
+
include_app_data: bool = False,
|
|
153
|
+
) -> Path:
|
|
154
|
+
"""Export VM with AES-256 encryption."""
|
|
155
|
+
key = self.load_key()
|
|
156
|
+
if key is None:
|
|
157
|
+
raise FileNotFoundError(
|
|
158
|
+
f"No encryption key found at {self.KEY_PATH}. Run: clonebox keygen"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
fernet = Fernet(key)
|
|
162
|
+
|
|
163
|
+
# Create temporary unencrypted archive
|
|
164
|
+
with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp:
|
|
165
|
+
tmp_path = Path(tmp.name)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
# Export to temp file
|
|
169
|
+
self.exporter.export_vm(
|
|
170
|
+
vm_name=vm_name,
|
|
171
|
+
output_path=tmp_path,
|
|
172
|
+
include_user_data=include_user_data,
|
|
173
|
+
include_app_data=include_app_data,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Encrypt
|
|
177
|
+
data = tmp_path.read_bytes()
|
|
178
|
+
encrypted = fernet.encrypt(data)
|
|
179
|
+
output_path.write_bytes(encrypted)
|
|
180
|
+
|
|
181
|
+
finally:
|
|
182
|
+
# Cleanup temp file
|
|
183
|
+
if tmp_path.exists():
|
|
184
|
+
tmp_path.unlink()
|
|
185
|
+
|
|
186
|
+
return output_path
|
|
187
|
+
|
|
188
|
+
def close(self) -> None:
|
|
189
|
+
self.exporter.close()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Health check system for CloneBox VMs."""
|
|
2
|
+
|
|
3
|
+
from .models import HealthCheckResult, HealthStatus, ProbeConfig
|
|
4
|
+
from .probes import HTTPProbe, TCPProbe, CommandProbe, ScriptProbe
|
|
5
|
+
from .manager import HealthCheckManager
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"HealthCheckResult",
|
|
9
|
+
"HealthStatus",
|
|
10
|
+
"ProbeConfig",
|
|
11
|
+
"HTTPProbe",
|
|
12
|
+
"TCPProbe",
|
|
13
|
+
"CommandProbe",
|
|
14
|
+
"ScriptProbe",
|
|
15
|
+
"HealthCheckManager",
|
|
16
|
+
]
|