clonebox 1.1.4__py3-none-any.whl → 1.1.6__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 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(
@@ -1850,18 +1852,21 @@ def cmd_test(args):
1850
1852
  all_paths.update(config.get("app_data_paths", {}))
1851
1853
 
1852
1854
  if all_paths:
1853
- for idx, (host_path, guest_path) in enumerate(all_paths.items()):
1854
- try:
1855
- # Use the same QGA helper as diagnose/status
1856
- is_accessible = _qga_exec(
1857
- vm_name, conn_uri, f"test -d {guest_path} && echo yes || echo no", timeout=5
1858
- )
1859
- if is_accessible == "yes":
1860
- console.print(f"[green]✅ {guest_path}[/]")
1861
- else:
1862
- console.print(f"[red]❌ {guest_path} (not accessible)[/]")
1863
- except Exception:
1864
- console.print(f"[yellow]⚠️ {guest_path} (could not check)[/]")
1855
+ if not _qga_ping(vm_name, conn_uri):
1856
+ console.print("[yellow]⚠️ QEMU guest agent not connected - cannot verify mounts[/]")
1857
+ else:
1858
+ for idx, (host_path, guest_path) in enumerate(all_paths.items()):
1859
+ try:
1860
+ # Use the same QGA helper as diagnose/status
1861
+ is_accessible = _qga_exec(
1862
+ vm_name, conn_uri, f"test -d {guest_path} && echo yes || echo no", timeout=5
1863
+ )
1864
+ if is_accessible == "yes":
1865
+ console.print(f"[green]✅ {guest_path}[/]")
1866
+ else:
1867
+ console.print(f"[red]❌ {guest_path} (not accessible)[/]")
1868
+ except Exception:
1869
+ console.print(f"[yellow]⚠️ {guest_path} (could not check)[/]")
1865
1870
  else:
1866
1871
  console.print("[dim]No mount points configured[/]")
1867
1872
 
@@ -2098,7 +2103,7 @@ def generate_clonebox_yaml(
2098
2103
  vm_name = f"clone-{sys_info['hostname']}"
2099
2104
 
2100
2105
  # Calculate recommended resources
2101
- ram_mb = min(4096, int(sys_info["memory_available_gb"] * 1024 * 0.5))
2106
+ ram_mb = min(8192, int(sys_info["memory_available_gb"] * 1024 * 0.5))
2102
2107
  vcpus = max(2, sys_info["cpu_count"] // 2)
2103
2108
 
2104
2109
  if disk_size_gb is None:
@@ -2331,8 +2336,8 @@ def create_vm_from_config(
2331
2336
 
2332
2337
  vm_config = VMConfig(
2333
2338
  name=config["vm"]["name"],
2334
- ram_mb=config["vm"].get("ram_mb", 4096),
2335
- vcpus=config["vm"].get("vcpus", 4),
2339
+ ram_mb=config["vm"].get("ram_mb", 8192),
2340
+ vcpus=config["vm"].get("vcpus", 8),
2336
2341
  disk_size_gb=config["vm"].get("disk_size_gb", 10),
2337
2342
  gui=config["vm"].get("gui", True),
2338
2343
  base_image=config["vm"].get("base_image"),
@@ -2670,7 +2675,11 @@ def cmd_monitor(args) -> None:
2670
2675
  for vm in vm_stats:
2671
2676
  state_color = "green" if vm.state == "running" else "yellow"
2672
2677
  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
2678
+ mem_pct = (
2679
+ (vm.memory_used_mb / vm.memory_total_mb * 100)
2680
+ if vm.memory_total_mb > 0
2681
+ else 0
2682
+ )
2674
2683
  mem_color = "red" if mem_pct > 80 else "green"
2675
2684
 
2676
2685
  table.add_row(
@@ -2700,7 +2709,9 @@ def cmd_monitor(args) -> None:
2700
2709
 
2701
2710
  for c in container_stats:
2702
2711
  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
2712
+ mem_pct = (
2713
+ (c.memory_used_mb / c.memory_limit_mb * 100) if c.memory_limit_mb > 0 else 0
2714
+ )
2704
2715
  mem_color = "red" if mem_pct > 80 else "green"
2705
2716
 
2706
2717
  table.add_row(
@@ -2751,6 +2762,154 @@ def cmd_exec(args) -> None:
2751
2762
  console.print(result)
2752
2763
 
2753
2764
 
2765
+ def cmd_snapshot_create(args) -> None:
2766
+ """Create VM snapshot."""
2767
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.vm_name)
2768
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2769
+
2770
+ snap_name = args.name or f"snap-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
2771
+ description = getattr(args, "description", None)
2772
+
2773
+ console.print(f"[cyan]📸 Creating snapshot: {snap_name}[/]")
2774
+
2775
+ try:
2776
+ manager = SnapshotManager(conn_uri)
2777
+ snapshot = manager.create(
2778
+ vm_name=vm_name,
2779
+ name=snap_name,
2780
+ description=description,
2781
+ snapshot_type=SnapshotType.DISK_ONLY,
2782
+ )
2783
+ console.print(f"[green]✅ Snapshot created: {snapshot.name}[/]")
2784
+ except Exception as e:
2785
+ console.print(f"[red]❌ Failed to create snapshot: {e}[/]")
2786
+ finally:
2787
+ manager.close()
2788
+
2789
+
2790
+ def cmd_snapshot_list(args) -> None:
2791
+ """List VM snapshots."""
2792
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.vm_name)
2793
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2794
+
2795
+ try:
2796
+ manager = SnapshotManager(conn_uri)
2797
+ snapshots = manager.list(vm_name)
2798
+
2799
+ if not snapshots:
2800
+ console.print("[dim]No snapshots found.[/]")
2801
+ return
2802
+
2803
+ table = Table(title=f"📸 Snapshots for {vm_name}", border_style="cyan")
2804
+ table.add_column("Name", style="bold")
2805
+ table.add_column("Created")
2806
+ table.add_column("Type")
2807
+ table.add_column("Description")
2808
+
2809
+ for snap in snapshots:
2810
+ table.add_row(
2811
+ snap.name,
2812
+ snap.created_at.strftime("%Y-%m-%d %H:%M"),
2813
+ snap.snapshot_type.value,
2814
+ snap.description or "-",
2815
+ )
2816
+
2817
+ console.print(table)
2818
+ except Exception as e:
2819
+ console.print(f"[red]❌ Error: {e}[/]")
2820
+ finally:
2821
+ manager.close()
2822
+
2823
+
2824
+ def cmd_snapshot_restore(args) -> None:
2825
+ """Restore VM to snapshot."""
2826
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.vm_name)
2827
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2828
+
2829
+ console.print(f"[cyan]🔄 Restoring snapshot: {args.name}[/]")
2830
+
2831
+ try:
2832
+ manager = SnapshotManager(conn_uri)
2833
+ manager.restore(vm_name, args.name, force=getattr(args, "force", False))
2834
+ console.print(f"[green]✅ Restored to snapshot: {args.name}[/]")
2835
+ except Exception as e:
2836
+ console.print(f"[red]❌ Failed to restore: {e}[/]")
2837
+ finally:
2838
+ manager.close()
2839
+
2840
+
2841
+ def cmd_snapshot_delete(args) -> None:
2842
+ """Delete VM snapshot."""
2843
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.vm_name)
2844
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2845
+
2846
+ console.print(f"[cyan]🗑️ Deleting snapshot: {args.name}[/]")
2847
+
2848
+ try:
2849
+ manager = SnapshotManager(conn_uri)
2850
+ manager.delete(vm_name, args.name)
2851
+ console.print(f"[green]✅ Snapshot deleted: {args.name}[/]")
2852
+ except Exception as e:
2853
+ console.print(f"[red]❌ Failed to delete: {e}[/]")
2854
+ finally:
2855
+ manager.close()
2856
+
2857
+
2858
+ def cmd_health(args) -> None:
2859
+ """Run health checks for VM."""
2860
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.name)
2861
+
2862
+ console.print(f"[cyan]🏥 Running health checks for: {vm_name}[/]")
2863
+
2864
+ manager = HealthCheckManager()
2865
+
2866
+ # Load probes from config or use defaults
2867
+ probes = []
2868
+ if config_file and config_file.exists():
2869
+ import yaml
2870
+
2871
+ config = yaml.safe_load(config_file.read_text())
2872
+ health_checks = config.get("health_checks", [])
2873
+ for hc in health_checks:
2874
+ probes.append(ProbeConfig.from_dict(hc))
2875
+
2876
+ # Also create probes for services
2877
+ services = config.get("services", [])
2878
+ if services:
2879
+ probes.extend(manager.create_default_probes(services))
2880
+
2881
+ if not probes:
2882
+ console.print(
2883
+ "[yellow]No health checks configured. Add 'health_checks' to .clonebox.yaml[/]"
2884
+ )
2885
+ return
2886
+
2887
+ state = manager.check(vm_name, probes)
2888
+
2889
+ # Display results
2890
+ status_color = "green" if state.overall_status.value == "healthy" else "red"
2891
+ console.print(
2892
+ f"\n[bold]Overall Status:[/] [{status_color}]{state.overall_status.value.upper()}[/]"
2893
+ )
2894
+
2895
+ table = Table(title="Health Check Results", border_style="cyan")
2896
+ table.add_column("Probe", style="bold")
2897
+ table.add_column("Status")
2898
+ table.add_column("Duration")
2899
+ table.add_column("Message")
2900
+
2901
+ for result in state.check_results:
2902
+ status_color = "green" if result.is_healthy else "red"
2903
+ table.add_row(
2904
+ result.probe_name,
2905
+ f"[{status_color}]{result.status.value}[/]",
2906
+ f"{result.duration_ms:.0f}ms",
2907
+ result.message or result.error or "-",
2908
+ )
2909
+
2910
+ console.print(table)
2911
+
2912
+
2754
2913
  def cmd_keygen(args) -> None:
2755
2914
  """Generate encryption key for secure P2P transfers."""
2756
2915
  key_path = SecureExporter.generate_key()
@@ -3319,9 +3478,7 @@ def main():
3319
3478
  monitor_parser.add_argument(
3320
3479
  "--refresh", "-r", type=float, default=2.0, help="Refresh interval in seconds (default: 2)"
3321
3480
  )
3322
- monitor_parser.add_argument(
3323
- "--once", action="store_true", help="Show stats once and exit"
3324
- )
3481
+ monitor_parser.add_argument("--once", action="store_true", help="Show stats once and exit")
3325
3482
  monitor_parser.set_defaults(func=cmd_monitor)
3326
3483
 
3327
3484
  # Exec command - execute command in VM
@@ -3329,9 +3486,7 @@ def main():
3329
3486
  exec_parser.add_argument(
3330
3487
  "name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
3331
3488
  )
3332
- exec_parser.add_argument(
3333
- "command", help="Command to execute in VM"
3334
- )
3489
+ exec_parser.add_argument("command", help="Command to execute in VM")
3335
3490
  exec_parser.add_argument(
3336
3491
  "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3337
3492
  )
@@ -3340,10 +3495,51 @@ def main():
3340
3495
  )
3341
3496
  exec_parser.set_defaults(func=cmd_exec)
3342
3497
 
3498
+ # === Snapshot Commands ===
3499
+ snapshot_parser = subparsers.add_parser("snapshot", help="Manage VM snapshots")
3500
+ snapshot_sub = snapshot_parser.add_subparsers(dest="snapshot_command", help="Snapshot commands")
3501
+
3502
+ snap_create = snapshot_sub.add_parser("create", help="Create snapshot")
3503
+ snap_create.add_argument("vm_name", help="VM name or '.' to use .clonebox.yaml")
3504
+ snap_create.add_argument("--name", "-n", help="Snapshot name (auto-generated if not provided)")
3505
+ snap_create.add_argument("--description", "-d", help="Snapshot description")
3506
+ snap_create.add_argument("-u", "--user", action="store_true", help="Use user session")
3507
+ snap_create.set_defaults(func=cmd_snapshot_create)
3508
+
3509
+ snap_list = snapshot_sub.add_parser("list", aliases=["ls"], help="List snapshots")
3510
+ snap_list.add_argument("vm_name", help="VM name or '.' to use .clonebox.yaml")
3511
+ snap_list.add_argument("-u", "--user", action="store_true", help="Use user session")
3512
+ snap_list.set_defaults(func=cmd_snapshot_list)
3513
+
3514
+ snap_restore = snapshot_sub.add_parser("restore", help="Restore to snapshot")
3515
+ snap_restore.add_argument("vm_name", help="VM name or '.' to use .clonebox.yaml")
3516
+ snap_restore.add_argument("--name", "-n", required=True, help="Snapshot name to restore")
3517
+ snap_restore.add_argument("-u", "--user", action="store_true", help="Use user session")
3518
+ snap_restore.add_argument(
3519
+ "-f", "--force", action="store_true", help="Force restore even if running"
3520
+ )
3521
+ snap_restore.set_defaults(func=cmd_snapshot_restore)
3522
+
3523
+ snap_delete = snapshot_sub.add_parser("delete", aliases=["rm"], help="Delete snapshot")
3524
+ snap_delete.add_argument("vm_name", help="VM name or '.' to use .clonebox.yaml")
3525
+ snap_delete.add_argument("--name", "-n", required=True, help="Snapshot name to delete")
3526
+ snap_delete.add_argument("-u", "--user", action="store_true", help="Use user session")
3527
+ snap_delete.set_defaults(func=cmd_snapshot_delete)
3528
+
3529
+ # === Health Check Commands ===
3530
+ health_parser = subparsers.add_parser("health", help="Run health checks for VM")
3531
+ health_parser.add_argument(
3532
+ "name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
3533
+ )
3534
+ health_parser.add_argument("-u", "--user", action="store_true", help="Use user session")
3535
+ health_parser.set_defaults(func=cmd_health)
3536
+
3343
3537
  # === P2P Secure Transfer Commands ===
3344
3538
 
3345
3539
  # Keygen command - generate encryption key
3346
- keygen_parser = subparsers.add_parser("keygen", help="Generate encryption key for secure transfers")
3540
+ keygen_parser = subparsers.add_parser(
3541
+ "keygen", help="Generate encryption key for secure transfers"
3542
+ )
3347
3543
  keygen_parser.set_defaults(func=cmd_keygen)
3348
3544
 
3349
3545
  # Export-encrypted command
@@ -3356,9 +3552,7 @@ def main():
3356
3552
  export_enc_parser.add_argument(
3357
3553
  "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3358
3554
  )
3359
- export_enc_parser.add_argument(
3360
- "-o", "--output", help="Output file (default: <vmname>.enc)"
3361
- )
3555
+ export_enc_parser.add_argument("-o", "--output", help="Output file (default: <vmname>.enc)")
3362
3556
  export_enc_parser.add_argument(
3363
3557
  "--user-data", action="store_true", help="Include user data (SSH keys, configs)"
3364
3558
  )
@@ -3376,9 +3570,7 @@ def main():
3376
3570
  "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3377
3571
  )
3378
3572
  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
- )
3573
+ import_enc_parser.add_argument("--user-data", action="store_true", help="Import user data")
3382
3574
  import_enc_parser.add_argument(
3383
3575
  "--include-data", "-d", action="store_true", help="Import app data"
3384
3576
  )
@@ -3390,15 +3582,11 @@ def main():
3390
3582
  )
3391
3583
  export_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3392
3584
  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
- )
3585
+ export_remote_parser.add_argument("-o", "--output", required=True, help="Local output file")
3396
3586
  export_remote_parser.add_argument(
3397
3587
  "--encrypted", "-e", action="store_true", help="Use encrypted export"
3398
3588
  )
3399
- export_remote_parser.add_argument(
3400
- "--user-data", action="store_true", help="Include user data"
3401
- )
3589
+ export_remote_parser.add_argument("--user-data", action="store_true", help="Include user data")
3402
3590
  export_remote_parser.add_argument(
3403
3591
  "--include-data", "-d", action="store_true", help="Include app data"
3404
3592
  )
@@ -3414,22 +3602,16 @@ def main():
3414
3602
  import_remote_parser.add_argument(
3415
3603
  "--encrypted", "-e", action="store_true", help="Use encrypted import"
3416
3604
  )
3417
- import_remote_parser.add_argument(
3418
- "--user-data", action="store_true", help="Import user data"
3419
- )
3605
+ import_remote_parser.add_argument("--user-data", action="store_true", help="Import user data")
3420
3606
  import_remote_parser.set_defaults(func=cmd_import_remote)
3421
3607
 
3422
3608
  # Sync-key command
3423
- sync_key_parser = subparsers.add_parser(
3424
- "sync-key", help="Sync encryption key to remote host"
3425
- )
3609
+ sync_key_parser = subparsers.add_parser("sync-key", help="Sync encryption key to remote host")
3426
3610
  sync_key_parser.add_argument("host", help="Remote host (user@hostname)")
3427
3611
  sync_key_parser.set_defaults(func=cmd_sync_key)
3428
3612
 
3429
3613
  # List-remote command
3430
- list_remote_parser = subparsers.add_parser(
3431
- "list-remote", help="List VMs on remote host"
3432
- )
3614
+ list_remote_parser = subparsers.add_parser("list-remote", help="List VMs on remote host")
3433
3615
  list_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3434
3616
  list_remote_parser.set_defaults(func=cmd_list_remote)
3435
3617