clonebox 1.1.1__tar.gz → 1.1.3__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 (38) hide show
  1. {clonebox-1.1.1/src/clonebox.egg-info → clonebox-1.1.3}/PKG-INFO +28 -2
  2. {clonebox-1.1.1 → clonebox-1.1.3}/README.md +27 -1
  3. {clonebox-1.1.1 → clonebox-1.1.3}/pyproject.toml +1 -1
  4. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/cli.py +217 -0
  5. clonebox-1.1.3/src/clonebox/exporter.py +189 -0
  6. clonebox-1.1.3/src/clonebox/importer.py +220 -0
  7. clonebox-1.1.3/src/clonebox/p2p.py +184 -0
  8. {clonebox-1.1.1 → clonebox-1.1.3/src/clonebox.egg-info}/PKG-INFO +28 -2
  9. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox.egg-info/SOURCES.txt +3 -0
  10. {clonebox-1.1.1 → clonebox-1.1.3}/LICENSE +0 -0
  11. {clonebox-1.1.1 → clonebox-1.1.3}/setup.cfg +0 -0
  12. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/__init__.py +0 -0
  13. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/__main__.py +0 -0
  14. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/cloner.py +0 -0
  15. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/container.py +0 -0
  16. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/dashboard.py +0 -0
  17. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/detector.py +0 -0
  18. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/models.py +0 -0
  19. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/profiles.py +0 -0
  20. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
  21. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/templates/profiles/web-stack.yaml +0 -0
  22. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox/validator.py +0 -0
  23. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox.egg-info/dependency_links.txt +0 -0
  24. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox.egg-info/entry_points.txt +0 -0
  25. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox.egg-info/requires.txt +0 -0
  26. {clonebox-1.1.1 → clonebox-1.1.3}/src/clonebox.egg-info/top_level.txt +0 -0
  27. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_cli.py +0 -0
  28. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_cloner.py +0 -0
  29. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_cloner_simple.py +0 -0
  30. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_container.py +0 -0
  31. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_coverage_additional.py +0 -0
  32. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_coverage_boost_final.py +0 -0
  33. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_dashboard_coverage.py +0 -0
  34. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_detector.py +0 -0
  35. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_models.py +0 -0
  36. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_network.py +0 -0
  37. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_profiles.py +0 -0
  38. {clonebox-1.1.1 → clonebox-1.1.3}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.1
3
+ Version: 1.1.3
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.1"
7
+ version = "1.1.3"
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,9 @@ 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.p2p import P2PManager
31
34
 
32
35
  # Custom questionary style
33
36
  custom_style = Style(
@@ -2635,6 +2638,127 @@ def cmd_detect(args):
2635
2638
  console.print(table)
2636
2639
 
2637
2640
 
2641
+ def cmd_keygen(args) -> None:
2642
+ """Generate encryption key for secure P2P transfers."""
2643
+ key_path = SecureExporter.generate_key()
2644
+ console.print(f"[green]🔑 Encryption key generated: {key_path}[/]")
2645
+ console.print("[dim]Share this key with team members for encrypted transfers.[/]")
2646
+
2647
+
2648
+ def cmd_export_encrypted(args) -> None:
2649
+ """Export VM with AES-256 encryption."""
2650
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.name)
2651
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2652
+ output = Path(args.output) if args.output else Path(f"{vm_name}.enc")
2653
+
2654
+ console.print(f"[cyan]🔒 Exporting encrypted: {vm_name} → {output}[/]")
2655
+
2656
+ try:
2657
+ exporter = SecureExporter(conn_uri)
2658
+ exporter.export_encrypted(
2659
+ vm_name=vm_name,
2660
+ output_path=output,
2661
+ include_user_data=getattr(args, "user_data", False),
2662
+ include_app_data=getattr(args, "include_data", False),
2663
+ )
2664
+ console.print(f"[green]✅ Encrypted export complete: {output}[/]")
2665
+ except FileNotFoundError as e:
2666
+ console.print(f"[red]❌ {e}[/]")
2667
+ console.print("[yellow]Run: clonebox keygen[/]")
2668
+ finally:
2669
+ exporter.close()
2670
+
2671
+
2672
+ def cmd_import_encrypted(args) -> None:
2673
+ """Import VM with AES-256 decryption."""
2674
+ archive = Path(args.archive)
2675
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2676
+
2677
+ console.print(f"[cyan]🔓 Importing encrypted: {archive}[/]")
2678
+
2679
+ try:
2680
+ importer = SecureImporter(conn_uri)
2681
+ vm_name = importer.import_decrypted(
2682
+ encrypted_path=archive,
2683
+ import_user_data=getattr(args, "user_data", False),
2684
+ import_app_data=getattr(args, "include_data", False),
2685
+ new_name=getattr(args, "name", None),
2686
+ )
2687
+ console.print(f"[green]✅ Import complete: {vm_name}[/]")
2688
+ except FileNotFoundError as e:
2689
+ console.print(f"[red]❌ {e}[/]")
2690
+ finally:
2691
+ importer.close()
2692
+
2693
+
2694
+ def cmd_export_remote(args) -> None:
2695
+ """Export VM from remote host."""
2696
+ p2p = P2PManager()
2697
+
2698
+ console.print(f"[cyan]📤 Remote export: {args.host}:{args.vm_name}[/]")
2699
+
2700
+ try:
2701
+ output = Path(args.output)
2702
+ p2p.export_remote(
2703
+ host=args.host,
2704
+ vm_name=args.vm_name,
2705
+ output=output,
2706
+ encrypted=getattr(args, "encrypted", False),
2707
+ include_user_data=getattr(args, "user_data", False),
2708
+ include_app_data=getattr(args, "include_data", False),
2709
+ )
2710
+ console.print(f"[green]✅ Remote export complete: {output}[/]")
2711
+ except RuntimeError as e:
2712
+ console.print(f"[red]❌ {e}[/]")
2713
+
2714
+
2715
+ def cmd_import_remote(args) -> None:
2716
+ """Import VM to remote host."""
2717
+ p2p = P2PManager()
2718
+ archive = Path(args.archive)
2719
+
2720
+ console.print(f"[cyan]📥 Remote import: {archive} → {args.host}[/]")
2721
+
2722
+ try:
2723
+ p2p.import_remote(
2724
+ host=args.host,
2725
+ archive_path=archive,
2726
+ encrypted=getattr(args, "encrypted", False),
2727
+ import_user_data=getattr(args, "user_data", False),
2728
+ new_name=getattr(args, "name", None),
2729
+ )
2730
+ console.print(f"[green]✅ Remote import complete[/]")
2731
+ except RuntimeError as e:
2732
+ console.print(f"[red]❌ {e}[/]")
2733
+
2734
+
2735
+ def cmd_sync_key(args) -> None:
2736
+ """Sync encryption key to remote host."""
2737
+ p2p = P2PManager()
2738
+
2739
+ console.print(f"[cyan]🔑 Syncing key to: {args.host}[/]")
2740
+
2741
+ try:
2742
+ p2p.sync_key(args.host)
2743
+ console.print(f"[green]✅ Key synced to {args.host}[/]")
2744
+ except (FileNotFoundError, RuntimeError) as e:
2745
+ console.print(f"[red]❌ {e}[/]")
2746
+
2747
+
2748
+ def cmd_list_remote(args) -> None:
2749
+ """List VMs on remote host."""
2750
+ p2p = P2PManager()
2751
+
2752
+ console.print(f"[cyan]🔍 Listing VMs on: {args.host}[/]")
2753
+
2754
+ vms = p2p.list_remote_vms(args.host)
2755
+ if vms:
2756
+ for vm in vms:
2757
+ console.print(f" • {vm}")
2758
+ else:
2759
+ console.print("[yellow]No VMs found on remote host.[/]")
2760
+
2761
+
2638
2762
  def main():
2639
2763
  """Main entry point."""
2640
2764
  parser = argparse.ArgumentParser(
@@ -3074,6 +3198,99 @@ def main():
3074
3198
  )
3075
3199
  test_parser.set_defaults(func=cmd_test)
3076
3200
 
3201
+ # === P2P Secure Transfer Commands ===
3202
+
3203
+ # Keygen command - generate encryption key
3204
+ keygen_parser = subparsers.add_parser("keygen", help="Generate encryption key for secure transfers")
3205
+ keygen_parser.set_defaults(func=cmd_keygen)
3206
+
3207
+ # Export-encrypted command
3208
+ export_enc_parser = subparsers.add_parser(
3209
+ "export-encrypted", help="Export VM with AES-256 encryption"
3210
+ )
3211
+ export_enc_parser.add_argument(
3212
+ "name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
3213
+ )
3214
+ export_enc_parser.add_argument(
3215
+ "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3216
+ )
3217
+ export_enc_parser.add_argument(
3218
+ "-o", "--output", help="Output file (default: <vmname>.enc)"
3219
+ )
3220
+ export_enc_parser.add_argument(
3221
+ "--user-data", action="store_true", help="Include user data (SSH keys, configs)"
3222
+ )
3223
+ export_enc_parser.add_argument(
3224
+ "--include-data", "-d", action="store_true", help="Include app data"
3225
+ )
3226
+ export_enc_parser.set_defaults(func=cmd_export_encrypted)
3227
+
3228
+ # Import-encrypted command
3229
+ import_enc_parser = subparsers.add_parser(
3230
+ "import-encrypted", help="Import VM with AES-256 decryption"
3231
+ )
3232
+ import_enc_parser.add_argument("archive", help="Path to encrypted archive (.enc)")
3233
+ import_enc_parser.add_argument(
3234
+ "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3235
+ )
3236
+ import_enc_parser.add_argument("--name", "-n", help="New name for imported VM")
3237
+ import_enc_parser.add_argument(
3238
+ "--user-data", action="store_true", help="Import user data"
3239
+ )
3240
+ import_enc_parser.add_argument(
3241
+ "--include-data", "-d", action="store_true", help="Import app data"
3242
+ )
3243
+ import_enc_parser.set_defaults(func=cmd_import_encrypted)
3244
+
3245
+ # Export-remote command
3246
+ export_remote_parser = subparsers.add_parser(
3247
+ "export-remote", help="Export VM from remote host via SSH"
3248
+ )
3249
+ export_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3250
+ export_remote_parser.add_argument("vm_name", help="VM name on remote host")
3251
+ export_remote_parser.add_argument(
3252
+ "-o", "--output", required=True, help="Local output file"
3253
+ )
3254
+ export_remote_parser.add_argument(
3255
+ "--encrypted", "-e", action="store_true", help="Use encrypted export"
3256
+ )
3257
+ export_remote_parser.add_argument(
3258
+ "--user-data", action="store_true", help="Include user data"
3259
+ )
3260
+ export_remote_parser.add_argument(
3261
+ "--include-data", "-d", action="store_true", help="Include app data"
3262
+ )
3263
+ export_remote_parser.set_defaults(func=cmd_export_remote)
3264
+
3265
+ # Import-remote command
3266
+ import_remote_parser = subparsers.add_parser(
3267
+ "import-remote", help="Import VM to remote host via SSH"
3268
+ )
3269
+ import_remote_parser.add_argument("archive", help="Local archive to upload")
3270
+ import_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3271
+ import_remote_parser.add_argument("--name", "-n", help="New name for VM on remote")
3272
+ import_remote_parser.add_argument(
3273
+ "--encrypted", "-e", action="store_true", help="Use encrypted import"
3274
+ )
3275
+ import_remote_parser.add_argument(
3276
+ "--user-data", action="store_true", help="Import user data"
3277
+ )
3278
+ import_remote_parser.set_defaults(func=cmd_import_remote)
3279
+
3280
+ # Sync-key command
3281
+ sync_key_parser = subparsers.add_parser(
3282
+ "sync-key", help="Sync encryption key to remote host"
3283
+ )
3284
+ sync_key_parser.add_argument("host", help="Remote host (user@hostname)")
3285
+ sync_key_parser.set_defaults(func=cmd_sync_key)
3286
+
3287
+ # List-remote command
3288
+ list_remote_parser = subparsers.add_parser(
3289
+ "list-remote", help="List VMs on remote host"
3290
+ )
3291
+ list_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3292
+ list_remote_parser.set_defaults(func=cmd_list_remote)
3293
+
3077
3294
  args = parser.parse_args()
3078
3295
 
3079
3296
  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,220 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ VM Importer - Import VM with path reconfiguration and decryption.
4
+ """
5
+
6
+ import shutil
7
+ import subprocess
8
+ import tarfile
9
+ import tempfile
10
+ import xml.etree.ElementTree as ET
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from cryptography.fernet import Fernet
15
+
16
+ try:
17
+ import libvirt
18
+ except ImportError:
19
+ libvirt = None
20
+
21
+
22
+ class VMImporter:
23
+ """Import VM with disk path reconfiguration."""
24
+
25
+ DEFAULT_DISK_DIR = Path("/var/lib/libvirt/images")
26
+
27
+ def __init__(self, conn_uri: str = "qemu:///system"):
28
+ self.conn_uri = conn_uri
29
+ self._conn = None
30
+
31
+ @property
32
+ def conn(self):
33
+ if self._conn is None:
34
+ if libvirt is None:
35
+ raise RuntimeError("libvirt-python not installed")
36
+ self._conn = libvirt.open(self.conn_uri)
37
+ return self._conn
38
+
39
+ def import_vm(
40
+ self,
41
+ archive_path: Path,
42
+ import_user_data: bool = False,
43
+ import_app_data: bool = False,
44
+ new_name: Optional[str] = None,
45
+ disk_dir: Optional[Path] = None,
46
+ ) -> str:
47
+ """Import VM from archive with full path reconfiguration."""
48
+ disk_dir = disk_dir or self.DEFAULT_DISK_DIR
49
+
50
+ with tempfile.TemporaryDirectory(prefix="clonebox-import-") as tmp_dir:
51
+ tmp_path = Path(tmp_dir)
52
+
53
+ # Extract archive
54
+ with tarfile.open(archive_path) as tar:
55
+ tar.extractall(tmp_path)
56
+
57
+ # Find XML file
58
+ xml_files = list(tmp_path.glob("*.xml"))
59
+ if not xml_files:
60
+ raise FileNotFoundError("No XML configuration found in archive")
61
+ xml_file = xml_files[0]
62
+ vm_name = xml_file.stem
63
+
64
+ # Move disks to libvirt images directory
65
+ disks_dir = tmp_path / "disks"
66
+ disk_mapping = {}
67
+ if disks_dir.exists():
68
+ for disk_file in disks_dir.iterdir():
69
+ dest = disk_dir / disk_file.name
70
+ shutil.copy2(disk_file, dest)
71
+ disk_mapping[disk_file.name] = dest
72
+ print(f" 💾 Copied disk: {dest}")
73
+
74
+ # Reconfigure disk paths in XML
75
+ vm_xml = self._reconfigure_paths(xml_file, disk_mapping, new_name)
76
+
77
+ # Define and create VM
78
+ vm = self.conn.defineXML(vm_xml)
79
+ final_name = new_name or vm_name
80
+ print(f" ✅ VM defined: {final_name}")
81
+
82
+ # Import user/app data
83
+ if import_user_data:
84
+ self._import_user_data(tmp_path / "user-data")
85
+ if import_app_data:
86
+ self._import_app_data(tmp_path / "app-data")
87
+
88
+ # Start VM
89
+ vm.create()
90
+ print(f" 🚀 VM started: {final_name}")
91
+
92
+ return final_name
93
+
94
+ def _reconfigure_paths(
95
+ self,
96
+ xml_file: Path,
97
+ disk_mapping: dict,
98
+ new_name: Optional[str] = None,
99
+ ) -> str:
100
+ """Update disk paths and optionally rename VM."""
101
+ tree = ET.parse(xml_file)
102
+ root = tree.getroot()
103
+
104
+ # Update name if requested
105
+ if new_name:
106
+ name_elem = root.find("name")
107
+ if name_elem is not None:
108
+ name_elem.text = new_name
109
+
110
+ # Update disk paths
111
+ for disk in root.findall(".//disk[@type='file']"):
112
+ source = disk.find(".//source")
113
+ if source is not None:
114
+ old_path = source.get("file")
115
+ if old_path:
116
+ disk_name = Path(old_path).name
117
+ if disk_name in disk_mapping:
118
+ source.set("file", str(disk_mapping[disk_name]))
119
+ print(f" 🔄 Remapped: {disk_name} → {disk_mapping[disk_name]}")
120
+
121
+ return ET.tostring(root, encoding="unicode")
122
+
123
+ def _import_user_data(self, user_data_dir: Path) -> None:
124
+ """Restore user data."""
125
+ if user_data_dir.exists():
126
+ for item in user_data_dir.iterdir():
127
+ dest = Path.home() / item.name
128
+ if item.is_dir():
129
+ shutil.copytree(item, dest, dirs_exist_ok=True)
130
+ else:
131
+ shutil.copy2(item, dest)
132
+ print(f" 👤 Restored: {dest}")
133
+
134
+ def _import_app_data(self, app_data_dir: Path) -> None:
135
+ """Restore application data."""
136
+ if app_data_dir.exists():
137
+ for item in app_data_dir.iterdir():
138
+ # Map back to original paths
139
+ dest_map = {
140
+ "projects": Path.home() / "projects",
141
+ ".docker": Path.home() / ".docker",
142
+ "myapp": Path("/opt/myapp"),
143
+ "www": Path("/var/www"),
144
+ "docker": Path("/srv/docker"),
145
+ }
146
+ dest = dest_map.get(item.name, Path.home() / item.name)
147
+ try:
148
+ if item.is_dir():
149
+ shutil.copytree(item, dest, dirs_exist_ok=True)
150
+ else:
151
+ shutil.copy2(item, dest)
152
+ print(f" 📁 Restored: {dest}")
153
+ except PermissionError:
154
+ print(f" ⚠️ Permission denied: {dest}")
155
+
156
+ def close(self) -> None:
157
+ if self._conn is not None:
158
+ self._conn.close()
159
+ self._conn = None
160
+
161
+
162
+ class SecureImporter:
163
+ """AES-256 decrypting VM importer."""
164
+
165
+ KEY_PATH = Path.home() / ".clonebox.key"
166
+
167
+ def __init__(self, conn_uri: str = "qemu:///system"):
168
+ self.importer = VMImporter(conn_uri)
169
+
170
+ @classmethod
171
+ def load_key(cls) -> Optional[bytes]:
172
+ """Load decryption key from file."""
173
+ if cls.KEY_PATH.exists():
174
+ return cls.KEY_PATH.read_bytes()
175
+ return None
176
+
177
+ def import_decrypted(
178
+ self,
179
+ encrypted_path: Path,
180
+ import_user_data: bool = False,
181
+ import_app_data: bool = False,
182
+ new_name: Optional[str] = None,
183
+ ) -> str:
184
+ """Import VM with AES-256 decryption."""
185
+ key = self.load_key()
186
+ if key is None:
187
+ raise FileNotFoundError(
188
+ f"No decryption key found at {self.KEY_PATH}. "
189
+ "Copy the team key to this location."
190
+ )
191
+
192
+ fernet = Fernet(key)
193
+
194
+ # Create temporary decrypted archive
195
+ with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp:
196
+ tmp_path = Path(tmp.name)
197
+
198
+ try:
199
+ # Decrypt
200
+ encrypted_data = encrypted_path.read_bytes()
201
+ decrypted = fernet.decrypt(encrypted_data)
202
+ tmp_path.write_bytes(decrypted)
203
+
204
+ # Import
205
+ vm_name = self.importer.import_vm(
206
+ archive_path=tmp_path,
207
+ import_user_data=import_user_data,
208
+ import_app_data=import_app_data,
209
+ new_name=new_name,
210
+ )
211
+
212
+ finally:
213
+ # Cleanup
214
+ if tmp_path.exists():
215
+ tmp_path.unlink()
216
+
217
+ return vm_name
218
+
219
+ def close(self) -> None:
220
+ self.importer.close()
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ P2P Manager - Transfer VMs between workstations via SSH/SCP.
4
+ """
5
+
6
+ import subprocess
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+
12
+ class P2PManager:
13
+ """Manage P2P VM transfers between workstations."""
14
+
15
+ def __init__(self, ssh_options: Optional[list] = None):
16
+ self.ssh_options = ssh_options or [
17
+ "-o", "StrictHostKeyChecking=no",
18
+ "-o", "UserKnownHostsFile=/dev/null",
19
+ ]
20
+
21
+ def _run_ssh(self, host: str, command: str) -> subprocess.CompletedProcess:
22
+ """Execute command on remote host via SSH."""
23
+ cmd = ["ssh"] + self.ssh_options + [host, command]
24
+ return subprocess.run(cmd, capture_output=True, text=True)
25
+
26
+ def _run_scp(
27
+ self,
28
+ source: str,
29
+ destination: str,
30
+ recursive: bool = False,
31
+ ) -> subprocess.CompletedProcess:
32
+ """Copy files via SCP."""
33
+ cmd = ["scp"] + self.ssh_options
34
+ if recursive:
35
+ cmd.append("-r")
36
+ cmd.extend([source, destination])
37
+ return subprocess.run(cmd, capture_output=True, text=True)
38
+
39
+ def export_remote(
40
+ self,
41
+ host: str,
42
+ vm_name: str,
43
+ output: Path,
44
+ encrypted: bool = False,
45
+ include_user_data: bool = False,
46
+ include_app_data: bool = False,
47
+ ) -> Path:
48
+ """Export VM from remote host to local file.
49
+
50
+ Args:
51
+ host: Remote host in format user@hostname
52
+ vm_name: Name of VM to export
53
+ output: Local output path
54
+ encrypted: Use encrypted export
55
+ include_user_data: Include user data
56
+ include_app_data: Include app data
57
+ """
58
+ remote_tmp = f"/tmp/clonebox-{vm_name}.tar.gz"
59
+ if encrypted:
60
+ remote_tmp = f"/tmp/clonebox-{vm_name}.enc"
61
+
62
+ # Build export command
63
+ export_cmd = f"clonebox export {vm_name} -o {remote_tmp}"
64
+ if encrypted:
65
+ export_cmd = f"clonebox export-encrypted {vm_name} -o {remote_tmp}"
66
+ if include_user_data:
67
+ export_cmd += " --user"
68
+ if include_app_data:
69
+ export_cmd += " --include-data"
70
+
71
+ print(f"📤 Exporting {vm_name} from {host}...")
72
+
73
+ # Execute remote export
74
+ result = self._run_ssh(host, export_cmd)
75
+ if result.returncode != 0:
76
+ raise RuntimeError(f"Remote export failed: {result.stderr}")
77
+
78
+ # Download file
79
+ print(f"⬇️ Downloading to {output}...")
80
+ result = self._run_scp(f"{host}:{remote_tmp}", str(output))
81
+ if result.returncode != 0:
82
+ raise RuntimeError(f"SCP download failed: {result.stderr}")
83
+
84
+ # Cleanup remote temp file
85
+ self._run_ssh(host, f"rm -f {remote_tmp}")
86
+
87
+ print(f"✅ Downloaded: {output}")
88
+ return output
89
+
90
+ def import_remote(
91
+ self,
92
+ host: str,
93
+ archive_path: Path,
94
+ encrypted: bool = False,
95
+ import_user_data: bool = False,
96
+ new_name: Optional[str] = None,
97
+ ) -> str:
98
+ """Upload and import VM on remote host.
99
+
100
+ Args:
101
+ host: Remote host in format user@hostname
102
+ archive_path: Local archive to upload
103
+ encrypted: Use decrypted import
104
+ import_user_data: Import user data
105
+ new_name: New name for VM on remote
106
+ """
107
+ remote_tmp = f"/tmp/{archive_path.name}"
108
+
109
+ print(f"⬆️ Uploading {archive_path} to {host}...")
110
+
111
+ # Upload file
112
+ result = self._run_scp(str(archive_path), f"{host}:{remote_tmp}")
113
+ if result.returncode != 0:
114
+ raise RuntimeError(f"SCP upload failed: {result.stderr}")
115
+
116
+ # Build import command
117
+ if encrypted:
118
+ import_cmd = f"clonebox import-encrypted {remote_tmp}"
119
+ else:
120
+ import_cmd = f"clonebox import {remote_tmp}"
121
+
122
+ if import_user_data:
123
+ import_cmd += " --user"
124
+ if new_name:
125
+ import_cmd += f" --name {new_name}"
126
+
127
+ print(f"📥 Importing on {host}...")
128
+
129
+ # Execute remote import
130
+ result = self._run_ssh(host, import_cmd)
131
+ if result.returncode != 0:
132
+ raise RuntimeError(f"Remote import failed: {result.stderr}")
133
+
134
+ # Cleanup remote temp file
135
+ self._run_ssh(host, f"rm -f {remote_tmp}")
136
+
137
+ print(f"✅ Import complete on {host}")
138
+ return new_name or archive_path.stem
139
+
140
+ def sync_key(self, host: str) -> bool:
141
+ """Sync encryption key to remote host.
142
+
143
+ Args:
144
+ host: Remote host in format user@hostname
145
+
146
+ Returns:
147
+ True if key was synced successfully
148
+ """
149
+ key_path = Path.home() / ".clonebox.key"
150
+ if not key_path.exists():
151
+ raise FileNotFoundError(f"No local key found at {key_path}")
152
+
153
+ print(f"🔑 Syncing encryption key to {host}...")
154
+
155
+ result = self._run_scp(str(key_path), f"{host}:~/.clonebox.key")
156
+ if result.returncode != 0:
157
+ raise RuntimeError(f"Key sync failed: {result.stderr}")
158
+
159
+ # Set proper permissions on remote
160
+ self._run_ssh(host, "chmod 600 ~/.clonebox.key")
161
+
162
+ print(f"✅ Key synced to {host}")
163
+ return True
164
+
165
+ def list_remote_vms(self, host: str) -> list:
166
+ """List VMs on remote host.
167
+
168
+ Args:
169
+ host: Remote host in format user@hostname
170
+
171
+ Returns:
172
+ List of VM names
173
+ """
174
+ result = self._run_ssh(host, "virsh list --all --name")
175
+ if result.returncode != 0:
176
+ return []
177
+
178
+ vms = [line.strip() for line in result.stdout.splitlines() if line.strip()]
179
+ return vms
180
+
181
+ def check_clonebox_installed(self, host: str) -> bool:
182
+ """Check if clonebox is installed on remote host."""
183
+ result = self._run_ssh(host, "which clonebox || command -v clonebox")
184
+ return result.returncode == 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.1
3
+ Version: 1.1.3
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
@@ -8,7 +8,10 @@ src/clonebox/cloner.py
8
8
  src/clonebox/container.py
9
9
  src/clonebox/dashboard.py
10
10
  src/clonebox/detector.py
11
+ src/clonebox/exporter.py
12
+ src/clonebox/importer.py
11
13
  src/clonebox/models.py
14
+ src/clonebox/p2p.py
12
15
  src/clonebox/profiles.py
13
16
  src/clonebox/validator.py
14
17
  src/clonebox.egg-info/PKG-INFO
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes