clonebox 1.1.2__tar.gz → 1.1.4__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.
Files changed (44) hide show
  1. {clonebox-1.1.2/src/clonebox.egg-info → clonebox-1.1.4}/PKG-INFO +28 -2
  2. {clonebox-1.1.2 → clonebox-1.1.4}/README.md +27 -1
  3. {clonebox-1.1.2 → clonebox-1.1.4}/pyproject.toml +1 -1
  4. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/cli.py +359 -0
  5. clonebox-1.1.4/src/clonebox/exporter.py +189 -0
  6. clonebox-1.1.4/src/clonebox/health/__init__.py +16 -0
  7. clonebox-1.1.4/src/clonebox/health/models.py +194 -0
  8. clonebox-1.1.4/src/clonebox/importer.py +220 -0
  9. clonebox-1.1.4/src/clonebox/monitor.py +269 -0
  10. clonebox-1.1.4/src/clonebox/p2p.py +184 -0
  11. clonebox-1.1.4/src/clonebox/snapshots/__init__.py +12 -0
  12. clonebox-1.1.4/src/clonebox/snapshots/manager.py +355 -0
  13. clonebox-1.1.4/src/clonebox/snapshots/models.py +187 -0
  14. {clonebox-1.1.2 → clonebox-1.1.4/src/clonebox.egg-info}/PKG-INFO +28 -2
  15. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox.egg-info/SOURCES.txt +9 -0
  16. {clonebox-1.1.2 → clonebox-1.1.4}/LICENSE +0 -0
  17. {clonebox-1.1.2 → clonebox-1.1.4}/setup.cfg +0 -0
  18. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/__init__.py +0 -0
  19. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/__main__.py +0 -0
  20. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/cloner.py +0 -0
  21. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/container.py +0 -0
  22. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/dashboard.py +0 -0
  23. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/detector.py +0 -0
  24. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/models.py +0 -0
  25. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/profiles.py +0 -0
  26. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
  27. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/templates/profiles/web-stack.yaml +0 -0
  28. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox/validator.py +0 -0
  29. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox.egg-info/dependency_links.txt +0 -0
  30. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox.egg-info/entry_points.txt +0 -0
  31. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox.egg-info/requires.txt +0 -0
  32. {clonebox-1.1.2 → clonebox-1.1.4}/src/clonebox.egg-info/top_level.txt +0 -0
  33. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_cli.py +0 -0
  34. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_cloner.py +0 -0
  35. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_cloner_simple.py +0 -0
  36. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_container.py +0 -0
  37. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_coverage_additional.py +0 -0
  38. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_coverage_boost_final.py +0 -0
  39. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_dashboard_coverage.py +0 -0
  40. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_detector.py +0 -0
  41. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_models.py +0 -0
  42. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_network.py +0 -0
  43. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_profiles.py +0 -0
  44. {clonebox-1.1.2 → clonebox-1.1.4}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.2
3
+ Version: 1.1.4
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
@@ -104,7 +104,7 @@ CloneBox excels in scenarios where developers need:
104
104
 
105
105
  ## What's New in v1.1
106
106
 
107
- **v1.1.0** is production-ready with two full runtimes:
107
+ **v1.1.2** is production-ready with two full runtimes and P2P secure sharing:
108
108
 
109
109
  | Feature | Status |
110
110
  |---------|--------|
@@ -113,8 +113,34 @@ CloneBox excels in scenarios where developers need:
113
113
  | 📊 Web Dashboard (FastAPI + HTMX + Tailwind) | ✅ Stable |
114
114
  | 🎛️ Profiles System (`ml-dev`, `web-stack`) | ✅ Stable |
115
115
  | 🔍 Auto-detection (services, apps, paths) | ✅ Stable |
116
+ | 🔒 P2P Secure Transfer (AES-256) | ✅ **NEW** |
116
117
  | 🧪 95%+ Test Coverage | ✅ |
117
118
 
119
+ ### P2P Secure VM Sharing
120
+
121
+ Share VMs between workstations with AES-256 encryption:
122
+
123
+ ```bash
124
+ # Generate team encryption key (once per team)
125
+ clonebox keygen
126
+ # 🔑 Key saved: ~/.clonebox.key
127
+
128
+ # Export encrypted VM
129
+ clonebox export-encrypted my-dev-vm -o team-env.enc --user-data
130
+
131
+ # Transfer via SCP/SMB/USB
132
+ scp team-env.enc user@workstationB:~/
133
+
134
+ # Import on another machine (needs same key)
135
+ clonebox import-encrypted team-env.enc --name my-dev-copy
136
+
137
+ # Or use P2P commands directly
138
+ clonebox export-remote user@hostA my-vm -o local.enc --encrypted
139
+ clonebox import-remote local.enc user@hostB --encrypted
140
+ clonebox sync-key user@hostB # Sync encryption key
141
+ clonebox list-remote user@hostB # List remote VMs
142
+ ```
143
+
118
144
  ### Roadmap
119
145
 
120
146
  - **v1.2.0**: `clonebox exec` command, VM snapshots, snapshot restore
@@ -53,7 +53,7 @@ CloneBox excels in scenarios where developers need:
53
53
 
54
54
  ## What's New in v1.1
55
55
 
56
- **v1.1.0** is production-ready with two full runtimes:
56
+ **v1.1.2** is production-ready with two full runtimes and P2P secure sharing:
57
57
 
58
58
  | Feature | Status |
59
59
  |---------|--------|
@@ -62,8 +62,34 @@ CloneBox excels in scenarios where developers need:
62
62
  | 📊 Web Dashboard (FastAPI + HTMX + Tailwind) | ✅ Stable |
63
63
  | 🎛️ Profiles System (`ml-dev`, `web-stack`) | ✅ Stable |
64
64
  | 🔍 Auto-detection (services, apps, paths) | ✅ Stable |
65
+ | 🔒 P2P Secure Transfer (AES-256) | ✅ **NEW** |
65
66
  | 🧪 95%+ Test Coverage | ✅ |
66
67
 
68
+ ### P2P Secure VM Sharing
69
+
70
+ Share VMs between workstations with AES-256 encryption:
71
+
72
+ ```bash
73
+ # Generate team encryption key (once per team)
74
+ clonebox keygen
75
+ # 🔑 Key saved: ~/.clonebox.key
76
+
77
+ # Export encrypted VM
78
+ clonebox export-encrypted my-dev-vm -o team-env.enc --user-data
79
+
80
+ # Transfer via SCP/SMB/USB
81
+ scp team-env.enc user@workstationB:~/
82
+
83
+ # Import on another machine (needs same key)
84
+ clonebox import-encrypted team-env.enc --name my-dev-copy
85
+
86
+ # Or use P2P commands directly
87
+ clonebox export-remote user@hostA my-vm -o local.enc --encrypted
88
+ clonebox import-remote local.enc user@hostB --encrypted
89
+ clonebox sync-key user@hostB # Sync encryption key
90
+ clonebox list-remote user@hostB # List remote VMs
91
+ ```
92
+
67
93
  ### Roadmap
68
94
 
69
95
  - **v1.2.0**: `clonebox exec` command, VM snapshots, snapshot restore
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clonebox"
7
- version = "1.1.2"
7
+ version = "1.1.4"
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"}
@@ -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"):
@@ -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
+ ]