clonebox 1.1.4__py3-none-any.whl → 1.1.5__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.
@@ -0,0 +1,217 @@
1
+ """libvirt hypervisor backend implementation."""
2
+
3
+ import base64
4
+ import json
5
+ import time
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ try:
9
+ import libvirt
10
+ except ImportError:
11
+ libvirt = None
12
+
13
+ from ..interfaces.hypervisor import HypervisorBackend, VMInfo
14
+
15
+
16
+ class LibvirtBackend(HypervisorBackend):
17
+ """libvirt hypervisor backend."""
18
+
19
+ name = "libvirt"
20
+
21
+ def __init__(
22
+ self,
23
+ uri: Optional[str] = None,
24
+ user_session: bool = False,
25
+ ):
26
+ self.uri = uri or ("qemu:///session" if user_session else "qemu:///system")
27
+ self._conn: Optional[libvirt.virConnect] = None
28
+
29
+ def connect(self) -> None:
30
+ """Establish connection to libvirt."""
31
+ if self._conn is not None:
32
+ try:
33
+ if self._conn.isAlive():
34
+ return
35
+ except libvirt.libvirtError:
36
+ pass
37
+
38
+ try:
39
+ self._conn = libvirt.open(self.uri)
40
+ except libvirt.libvirtError as e:
41
+ raise ConnectionError(f"Failed to connect to libvirt at {self.uri}: {e}")
42
+
43
+ def disconnect(self) -> None:
44
+ """Close connection."""
45
+ if self._conn:
46
+ try:
47
+ self._conn.close()
48
+ except libvirt.libvirtError:
49
+ pass
50
+ self._conn = None
51
+
52
+ @property
53
+ def conn(self) -> libvirt.virConnect:
54
+ """Get active libvirt connection."""
55
+ if self._conn is None:
56
+ self.connect()
57
+ return self._conn
58
+
59
+ def define_vm(self, config: Any) -> str:
60
+ """Define a new VM. Returns VM name."""
61
+ # This normally takes XML, but for DI we might want to pass config
62
+ # and let the backend handle XML generation if it's backend-specific.
63
+ # For now, we assume the caller provides the XML via config.xml or similar
64
+ # if they want to use this generic interface, or we refactor cloner to use backend.
65
+ if hasattr(config, "xml"):
66
+ domain = self.conn.defineXML(config.xml)
67
+ return domain.name()
68
+ raise ValueError("Config must provide XML for libvirt backend")
69
+
70
+ def undefine_vm(self, name: str) -> None:
71
+ """Remove VM definition."""
72
+ try:
73
+ domain = self.conn.lookupByName(name)
74
+ if domain.isActive():
75
+ domain.destroy()
76
+ domain.undefine()
77
+ except libvirt.libvirtError as e:
78
+ if "not found" not in str(e).lower():
79
+ raise
80
+
81
+ def start_vm(self, name: str) -> None:
82
+ """Start a VM."""
83
+ domain = self.conn.lookupByName(name)
84
+ if not domain.isActive():
85
+ domain.create()
86
+
87
+ def stop_vm(self, name: str, force: bool = False) -> None:
88
+ """Stop a VM."""
89
+ domain = self.conn.lookupByName(name)
90
+ if domain.isActive():
91
+ if force:
92
+ domain.destroy()
93
+ else:
94
+ domain.shutdown()
95
+
96
+ def get_vm_info(self, name: str) -> Optional[VMInfo]:
97
+ """Get VM information."""
98
+ try:
99
+ domain = self.conn.lookupByName(name)
100
+ except libvirt.libvirtError:
101
+ return None
102
+
103
+ info = domain.info()
104
+ state_map = {
105
+ libvirt.VIR_DOMAIN_RUNNING: "running",
106
+ libvirt.VIR_DOMAIN_BLOCKED: "blocked",
107
+ libvirt.VIR_DOMAIN_PAUSED: "paused",
108
+ libvirt.VIR_DOMAIN_SHUTDOWN: "shutdown",
109
+ libvirt.VIR_DOMAIN_SHUTOFF: "shutoff",
110
+ libvirt.VIR_DOMAIN_CRASHED: "crashed",
111
+ libvirt.VIR_DOMAIN_PMSUSPENDED: "pmsuspended",
112
+ }
113
+
114
+ return VMInfo(
115
+ name=domain.name(),
116
+ state=state_map.get(info[0], "unknown"),
117
+ uuid=domain.UUIDString(),
118
+ memory_mb=info[1] // 1024,
119
+ vcpus=info[3],
120
+ ip_addresses=self._get_ip_addresses(domain),
121
+ )
122
+
123
+ def list_vms(self) -> List[VMInfo]:
124
+ """List all VMs."""
125
+ vms = []
126
+ try:
127
+ # List all domains (running and defined)
128
+ for domain in self.conn.listAllDomains():
129
+ info = self.get_vm_info(domain.name())
130
+ if info:
131
+ vms.append(info)
132
+ except libvirt.libvirtError:
133
+ pass
134
+ return vms
135
+
136
+ def vm_exists(self, name: str) -> bool:
137
+ """Check if VM exists."""
138
+ try:
139
+ self.conn.lookupByName(name)
140
+ return True
141
+ except libvirt.libvirtError:
142
+ return False
143
+
144
+ def is_running(self, name: str) -> bool:
145
+ """Check if VM is running."""
146
+ try:
147
+ domain = self.conn.lookupByName(name)
148
+ return domain.isActive() == 1
149
+ except libvirt.libvirtError:
150
+ return False
151
+
152
+ def execute_command(
153
+ self,
154
+ name: str,
155
+ command: str,
156
+ timeout: int = 30,
157
+ ) -> Optional[str]:
158
+ """Execute command in VM via QEMU Guest Agent."""
159
+ domain = self.conn.lookupByName(name)
160
+
161
+ # Build QGA guest-exec command
162
+ qga_cmd = {
163
+ "execute": "guest-exec",
164
+ "arguments": {
165
+ "path": "/bin/bash",
166
+ "arg": ["-c", command],
167
+ "capture-output": True,
168
+ },
169
+ }
170
+
171
+ try:
172
+ result_json = domain.qemuAgentCommand(json.dumps(qga_cmd), timeout)
173
+ result_data = json.loads(result_json)
174
+ pid = result_data["return"]["pid"]
175
+
176
+ # Poll for completion
177
+ status_cmd = {"execute": "guest-exec-status", "arguments": {"pid": pid}}
178
+ start_time = time.time()
179
+ while time.time() - start_time < timeout:
180
+ status_json = domain.qemuAgentCommand(json.dumps(status_cmd), timeout)
181
+ status_data = json.loads(status_json)
182
+
183
+ if status_data["return"]["exited"]:
184
+ if "out-data" in status_data["return"]:
185
+ return base64.b64decode(status_data["return"]["out-data"]).decode(
186
+ "utf-8", errors="replace"
187
+ )
188
+ return ""
189
+
190
+ time.sleep(0.5)
191
+
192
+ return None # Timeout
193
+
194
+ except Exception:
195
+ return None
196
+
197
+ def _get_ip_addresses(self, domain) -> List[str]:
198
+ """Get IP addresses from domain via guest agent or lease."""
199
+ ips = []
200
+ try:
201
+ # Try guest agent first (more accurate)
202
+ ifaces = domain.interfaceAddresses(libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT)
203
+ for iface_data in ifaces.values():
204
+ for addr in iface_data.get("addrs", []):
205
+ if addr["type"] == 0: # IPv4
206
+ ips.append(addr["addr"])
207
+ except libvirt.libvirtError:
208
+ # Fallback to network leases
209
+ try:
210
+ ifaces = domain.interfaceAddresses(libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE)
211
+ for iface_data in ifaces.values():
212
+ for addr in iface_data.get("addrs", []):
213
+ if addr["type"] == 0:
214
+ ips.append(addr["addr"])
215
+ except libvirt.libvirtError:
216
+ pass
217
+ return ips
@@ -0,0 +1,52 @@
1
+ """QEMU disk manager implementation."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional
6
+
7
+ from ..interfaces.disk import DiskManager
8
+
9
+
10
+ class QemuDiskManager(DiskManager):
11
+ """Manage VM disks using qemu-img."""
12
+
13
+ def create_disk(
14
+ self,
15
+ path: Path,
16
+ size_gb: int,
17
+ format: str = "qcow2",
18
+ backing_file: Optional[Path] = None,
19
+ ) -> Path:
20
+ """Create a disk image."""
21
+ cmd = ["qemu-img", "create", "-f", format]
22
+
23
+ if backing_file:
24
+ cmd.extend(["-b", str(backing_file), "-F", format])
25
+
26
+ cmd.extend([str(path), f"{size_gb}G"])
27
+
28
+ subprocess.run(cmd, check=True, capture_output=True)
29
+ return path
30
+
31
+ def resize_disk(self, path: Path, new_size_gb: int) -> None:
32
+ """Resize a disk image."""
33
+ cmd = ["qemu-img", "resize", str(path), f"{new_size_gb}G"]
34
+ subprocess.run(cmd, check=True, capture_output=True)
35
+
36
+ def get_disk_info(self, path: Path) -> Dict[str, Any]:
37
+ """Get disk image information."""
38
+ import json
39
+ cmd = ["qemu-img", "info", "--output=json", str(path)]
40
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
41
+ return json.loads(result.stdout)
42
+
43
+ def create_snapshot(self, path: Path, snapshot_name: str) -> Path:
44
+ """Create internal disk snapshot."""
45
+ cmd = ["qemu-img", "snapshot", "-c", snapshot_name, str(path)]
46
+ subprocess.run(cmd, check=True, capture_output=True)
47
+ return path
48
+
49
+ def delete_disk(self, path: Path) -> None:
50
+ """Delete disk image."""
51
+ if path.exists():
52
+ path.unlink()
@@ -0,0 +1,56 @@
1
+ """Subprocess process runner implementation."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+
7
+ from ..interfaces.process import ProcessResult, ProcessRunner
8
+
9
+
10
+ class SubprocessRunner(ProcessRunner):
11
+ """Run processes using the subprocess module."""
12
+
13
+ def run(
14
+ self,
15
+ command: List[str],
16
+ capture_output: bool = True,
17
+ timeout: Optional[int] = None,
18
+ check: bool = True,
19
+ cwd: Optional[Path] = None,
20
+ env: Optional[Dict[str, str]] = None,
21
+ ) -> ProcessResult:
22
+ """Run a command."""
23
+ result = subprocess.run(
24
+ command,
25
+ capture_output=capture_output,
26
+ timeout=timeout,
27
+ check=check,
28
+ cwd=str(cwd) if cwd else None,
29
+ env=env,
30
+ text=True,
31
+ )
32
+ return ProcessResult(
33
+ returncode=result.returncode,
34
+ stdout=result.stdout,
35
+ stderr=result.stderr,
36
+ )
37
+
38
+ def run_shell(
39
+ self,
40
+ command: str,
41
+ capture_output: bool = True,
42
+ timeout: Optional[int] = None,
43
+ ) -> ProcessResult:
44
+ """Run a shell command."""
45
+ result = subprocess.run(
46
+ command,
47
+ shell=True,
48
+ capture_output=capture_output,
49
+ timeout=timeout,
50
+ text=True,
51
+ )
52
+ return ProcessResult(
53
+ returncode=result.returncode,
54
+ stdout=result.stdout,
55
+ stderr=result.stderr,
56
+ )
clonebox/cli.py CHANGED
@@ -32,6 +32,8 @@ from clonebox.exporter import SecureExporter, VMExporter
32
32
  from clonebox.importer import SecureImporter, VMImporter
33
33
  from clonebox.monitor import ResourceMonitor, format_bytes
34
34
  from clonebox.p2p import P2PManager
35
+ from clonebox.snapshots import SnapshotManager, SnapshotType
36
+ from clonebox.health import HealthCheckManager, ProbeConfig, ProbeType
35
37
 
36
38
  # Custom questionary style
37
39
  custom_style = Style(
@@ -2670,7 +2672,11 @@ def cmd_monitor(args) -> None:
2670
2672
  for vm in vm_stats:
2671
2673
  state_color = "green" if vm.state == "running" else "yellow"
2672
2674
  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
2675
+ mem_pct = (
2676
+ (vm.memory_used_mb / vm.memory_total_mb * 100)
2677
+ if vm.memory_total_mb > 0
2678
+ else 0
2679
+ )
2674
2680
  mem_color = "red" if mem_pct > 80 else "green"
2675
2681
 
2676
2682
  table.add_row(
@@ -2700,7 +2706,9 @@ def cmd_monitor(args) -> None:
2700
2706
 
2701
2707
  for c in container_stats:
2702
2708
  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
2709
+ mem_pct = (
2710
+ (c.memory_used_mb / c.memory_limit_mb * 100) if c.memory_limit_mb > 0 else 0
2711
+ )
2704
2712
  mem_color = "red" if mem_pct > 80 else "green"
2705
2713
 
2706
2714
  table.add_row(
@@ -2751,6 +2759,154 @@ def cmd_exec(args) -> None:
2751
2759
  console.print(result)
2752
2760
 
2753
2761
 
2762
+ def cmd_snapshot_create(args) -> None:
2763
+ """Create VM snapshot."""
2764
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.vm_name)
2765
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2766
+
2767
+ snap_name = args.name or f"snap-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
2768
+ description = getattr(args, "description", None)
2769
+
2770
+ console.print(f"[cyan]📸 Creating snapshot: {snap_name}[/]")
2771
+
2772
+ try:
2773
+ manager = SnapshotManager(conn_uri)
2774
+ snapshot = manager.create(
2775
+ vm_name=vm_name,
2776
+ name=snap_name,
2777
+ description=description,
2778
+ snapshot_type=SnapshotType.DISK_ONLY,
2779
+ )
2780
+ console.print(f"[green]✅ Snapshot created: {snapshot.name}[/]")
2781
+ except Exception as e:
2782
+ console.print(f"[red]❌ Failed to create snapshot: {e}[/]")
2783
+ finally:
2784
+ manager.close()
2785
+
2786
+
2787
+ def cmd_snapshot_list(args) -> None:
2788
+ """List VM snapshots."""
2789
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.vm_name)
2790
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2791
+
2792
+ try:
2793
+ manager = SnapshotManager(conn_uri)
2794
+ snapshots = manager.list(vm_name)
2795
+
2796
+ if not snapshots:
2797
+ console.print("[dim]No snapshots found.[/]")
2798
+ return
2799
+
2800
+ table = Table(title=f"📸 Snapshots for {vm_name}", border_style="cyan")
2801
+ table.add_column("Name", style="bold")
2802
+ table.add_column("Created")
2803
+ table.add_column("Type")
2804
+ table.add_column("Description")
2805
+
2806
+ for snap in snapshots:
2807
+ table.add_row(
2808
+ snap.name,
2809
+ snap.created_at.strftime("%Y-%m-%d %H:%M"),
2810
+ snap.snapshot_type.value,
2811
+ snap.description or "-",
2812
+ )
2813
+
2814
+ console.print(table)
2815
+ except Exception as e:
2816
+ console.print(f"[red]❌ Error: {e}[/]")
2817
+ finally:
2818
+ manager.close()
2819
+
2820
+
2821
+ def cmd_snapshot_restore(args) -> None:
2822
+ """Restore VM to snapshot."""
2823
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.vm_name)
2824
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2825
+
2826
+ console.print(f"[cyan]🔄 Restoring snapshot: {args.name}[/]")
2827
+
2828
+ try:
2829
+ manager = SnapshotManager(conn_uri)
2830
+ manager.restore(vm_name, args.name, force=getattr(args, "force", False))
2831
+ console.print(f"[green]✅ Restored to snapshot: {args.name}[/]")
2832
+ except Exception as e:
2833
+ console.print(f"[red]❌ Failed to restore: {e}[/]")
2834
+ finally:
2835
+ manager.close()
2836
+
2837
+
2838
+ def cmd_snapshot_delete(args) -> None:
2839
+ """Delete VM snapshot."""
2840
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.vm_name)
2841
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2842
+
2843
+ console.print(f"[cyan]🗑️ Deleting snapshot: {args.name}[/]")
2844
+
2845
+ try:
2846
+ manager = SnapshotManager(conn_uri)
2847
+ manager.delete(vm_name, args.name)
2848
+ console.print(f"[green]✅ Snapshot deleted: {args.name}[/]")
2849
+ except Exception as e:
2850
+ console.print(f"[red]❌ Failed to delete: {e}[/]")
2851
+ finally:
2852
+ manager.close()
2853
+
2854
+
2855
+ def cmd_health(args) -> None:
2856
+ """Run health checks for VM."""
2857
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.name)
2858
+
2859
+ console.print(f"[cyan]🏥 Running health checks for: {vm_name}[/]")
2860
+
2861
+ manager = HealthCheckManager()
2862
+
2863
+ # Load probes from config or use defaults
2864
+ probes = []
2865
+ if config_file and config_file.exists():
2866
+ import yaml
2867
+
2868
+ config = yaml.safe_load(config_file.read_text())
2869
+ health_checks = config.get("health_checks", [])
2870
+ for hc in health_checks:
2871
+ probes.append(ProbeConfig.from_dict(hc))
2872
+
2873
+ # Also create probes for services
2874
+ services = config.get("services", [])
2875
+ if services:
2876
+ probes.extend(manager.create_default_probes(services))
2877
+
2878
+ if not probes:
2879
+ console.print(
2880
+ "[yellow]No health checks configured. Add 'health_checks' to .clonebox.yaml[/]"
2881
+ )
2882
+ return
2883
+
2884
+ state = manager.check(vm_name, probes)
2885
+
2886
+ # Display results
2887
+ status_color = "green" if state.overall_status.value == "healthy" else "red"
2888
+ console.print(
2889
+ f"\n[bold]Overall Status:[/] [{status_color}]{state.overall_status.value.upper()}[/]"
2890
+ )
2891
+
2892
+ table = Table(title="Health Check Results", border_style="cyan")
2893
+ table.add_column("Probe", style="bold")
2894
+ table.add_column("Status")
2895
+ table.add_column("Duration")
2896
+ table.add_column("Message")
2897
+
2898
+ for result in state.check_results:
2899
+ status_color = "green" if result.is_healthy else "red"
2900
+ table.add_row(
2901
+ result.probe_name,
2902
+ f"[{status_color}]{result.status.value}[/]",
2903
+ f"{result.duration_ms:.0f}ms",
2904
+ result.message or result.error or "-",
2905
+ )
2906
+
2907
+ console.print(table)
2908
+
2909
+
2754
2910
  def cmd_keygen(args) -> None:
2755
2911
  """Generate encryption key for secure P2P transfers."""
2756
2912
  key_path = SecureExporter.generate_key()
@@ -3319,9 +3475,7 @@ def main():
3319
3475
  monitor_parser.add_argument(
3320
3476
  "--refresh", "-r", type=float, default=2.0, help="Refresh interval in seconds (default: 2)"
3321
3477
  )
3322
- monitor_parser.add_argument(
3323
- "--once", action="store_true", help="Show stats once and exit"
3324
- )
3478
+ monitor_parser.add_argument("--once", action="store_true", help="Show stats once and exit")
3325
3479
  monitor_parser.set_defaults(func=cmd_monitor)
3326
3480
 
3327
3481
  # Exec command - execute command in VM
@@ -3329,9 +3483,7 @@ def main():
3329
3483
  exec_parser.add_argument(
3330
3484
  "name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
3331
3485
  )
3332
- exec_parser.add_argument(
3333
- "command", help="Command to execute in VM"
3334
- )
3486
+ exec_parser.add_argument("command", help="Command to execute in VM")
3335
3487
  exec_parser.add_argument(
3336
3488
  "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3337
3489
  )
@@ -3340,10 +3492,51 @@ def main():
3340
3492
  )
3341
3493
  exec_parser.set_defaults(func=cmd_exec)
3342
3494
 
3495
+ # === Snapshot Commands ===
3496
+ snapshot_parser = subparsers.add_parser("snapshot", help="Manage VM snapshots")
3497
+ snapshot_sub = snapshot_parser.add_subparsers(dest="snapshot_command", help="Snapshot commands")
3498
+
3499
+ snap_create = snapshot_sub.add_parser("create", help="Create snapshot")
3500
+ snap_create.add_argument("vm_name", help="VM name or '.' to use .clonebox.yaml")
3501
+ snap_create.add_argument("--name", "-n", help="Snapshot name (auto-generated if not provided)")
3502
+ snap_create.add_argument("--description", "-d", help="Snapshot description")
3503
+ snap_create.add_argument("-u", "--user", action="store_true", help="Use user session")
3504
+ snap_create.set_defaults(func=cmd_snapshot_create)
3505
+
3506
+ snap_list = snapshot_sub.add_parser("list", aliases=["ls"], help="List snapshots")
3507
+ snap_list.add_argument("vm_name", help="VM name or '.' to use .clonebox.yaml")
3508
+ snap_list.add_argument("-u", "--user", action="store_true", help="Use user session")
3509
+ snap_list.set_defaults(func=cmd_snapshot_list)
3510
+
3511
+ snap_restore = snapshot_sub.add_parser("restore", help="Restore to snapshot")
3512
+ snap_restore.add_argument("vm_name", help="VM name or '.' to use .clonebox.yaml")
3513
+ snap_restore.add_argument("--name", "-n", required=True, help="Snapshot name to restore")
3514
+ snap_restore.add_argument("-u", "--user", action="store_true", help="Use user session")
3515
+ snap_restore.add_argument(
3516
+ "-f", "--force", action="store_true", help="Force restore even if running"
3517
+ )
3518
+ snap_restore.set_defaults(func=cmd_snapshot_restore)
3519
+
3520
+ snap_delete = snapshot_sub.add_parser("delete", aliases=["rm"], help="Delete snapshot")
3521
+ snap_delete.add_argument("vm_name", help="VM name or '.' to use .clonebox.yaml")
3522
+ snap_delete.add_argument("--name", "-n", required=True, help="Snapshot name to delete")
3523
+ snap_delete.add_argument("-u", "--user", action="store_true", help="Use user session")
3524
+ snap_delete.set_defaults(func=cmd_snapshot_delete)
3525
+
3526
+ # === Health Check Commands ===
3527
+ health_parser = subparsers.add_parser("health", help="Run health checks for VM")
3528
+ health_parser.add_argument(
3529
+ "name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
3530
+ )
3531
+ health_parser.add_argument("-u", "--user", action="store_true", help="Use user session")
3532
+ health_parser.set_defaults(func=cmd_health)
3533
+
3343
3534
  # === P2P Secure Transfer Commands ===
3344
3535
 
3345
3536
  # Keygen command - generate encryption key
3346
- keygen_parser = subparsers.add_parser("keygen", help="Generate encryption key for secure transfers")
3537
+ keygen_parser = subparsers.add_parser(
3538
+ "keygen", help="Generate encryption key for secure transfers"
3539
+ )
3347
3540
  keygen_parser.set_defaults(func=cmd_keygen)
3348
3541
 
3349
3542
  # Export-encrypted command
@@ -3356,9 +3549,7 @@ def main():
3356
3549
  export_enc_parser.add_argument(
3357
3550
  "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3358
3551
  )
3359
- export_enc_parser.add_argument(
3360
- "-o", "--output", help="Output file (default: <vmname>.enc)"
3361
- )
3552
+ export_enc_parser.add_argument("-o", "--output", help="Output file (default: <vmname>.enc)")
3362
3553
  export_enc_parser.add_argument(
3363
3554
  "--user-data", action="store_true", help="Include user data (SSH keys, configs)"
3364
3555
  )
@@ -3376,9 +3567,7 @@ def main():
3376
3567
  "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3377
3568
  )
3378
3569
  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
- )
3570
+ import_enc_parser.add_argument("--user-data", action="store_true", help="Import user data")
3382
3571
  import_enc_parser.add_argument(
3383
3572
  "--include-data", "-d", action="store_true", help="Import app data"
3384
3573
  )
@@ -3390,15 +3579,11 @@ def main():
3390
3579
  )
3391
3580
  export_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3392
3581
  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
- )
3582
+ export_remote_parser.add_argument("-o", "--output", required=True, help="Local output file")
3396
3583
  export_remote_parser.add_argument(
3397
3584
  "--encrypted", "-e", action="store_true", help="Use encrypted export"
3398
3585
  )
3399
- export_remote_parser.add_argument(
3400
- "--user-data", action="store_true", help="Include user data"
3401
- )
3586
+ export_remote_parser.add_argument("--user-data", action="store_true", help="Include user data")
3402
3587
  export_remote_parser.add_argument(
3403
3588
  "--include-data", "-d", action="store_true", help="Include app data"
3404
3589
  )
@@ -3414,22 +3599,16 @@ def main():
3414
3599
  import_remote_parser.add_argument(
3415
3600
  "--encrypted", "-e", action="store_true", help="Use encrypted import"
3416
3601
  )
3417
- import_remote_parser.add_argument(
3418
- "--user-data", action="store_true", help="Import user data"
3419
- )
3602
+ import_remote_parser.add_argument("--user-data", action="store_true", help="Import user data")
3420
3603
  import_remote_parser.set_defaults(func=cmd_import_remote)
3421
3604
 
3422
3605
  # Sync-key command
3423
- sync_key_parser = subparsers.add_parser(
3424
- "sync-key", help="Sync encryption key to remote host"
3425
- )
3606
+ sync_key_parser = subparsers.add_parser("sync-key", help="Sync encryption key to remote host")
3426
3607
  sync_key_parser.add_argument("host", help="Remote host (user@hostname)")
3427
3608
  sync_key_parser.set_defaults(func=cmd_sync_key)
3428
3609
 
3429
3610
  # List-remote command
3430
- list_remote_parser = subparsers.add_parser(
3431
- "list-remote", help="List VMs on remote host"
3432
- )
3611
+ list_remote_parser = subparsers.add_parser("list-remote", help="List VMs on remote host")
3433
3612
  list_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3434
3613
  list_remote_parser.set_defaults(func=cmd_list_remote)
3435
3614