clonebox 1.1.3__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.
clonebox/cli.py CHANGED
@@ -30,7 +30,10 @@ from clonebox.models import ContainerConfig
30
30
  from clonebox.profiles import merge_with_profile
31
31
  from clonebox.exporter import SecureExporter, VMExporter
32
32
  from clonebox.importer import SecureImporter, VMImporter
33
+ from clonebox.monitor import ResourceMonitor, format_bytes
33
34
  from clonebox.p2p import P2PManager
35
+ from clonebox.snapshots import SnapshotManager, SnapshotType
36
+ from clonebox.health import HealthCheckManager, ProbeConfig, ProbeType
34
37
 
35
38
  # Custom questionary style
36
39
  custom_style = Style(
@@ -2638,6 +2641,272 @@ def cmd_detect(args):
2638
2641
  console.print(table)
2639
2642
 
2640
2643
 
2644
+ def cmd_monitor(args) -> None:
2645
+ """Real-time resource monitoring for VMs and containers."""
2646
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2647
+ refresh = getattr(args, "refresh", 2.0)
2648
+ once = getattr(args, "once", False)
2649
+
2650
+ monitor = ResourceMonitor(conn_uri)
2651
+
2652
+ try:
2653
+ while True:
2654
+ # Clear screen for live update
2655
+ if not once:
2656
+ console.clear()
2657
+
2658
+ console.print("[bold cyan]📊 CloneBox Resource Monitor[/]")
2659
+ console.print()
2660
+
2661
+ # VM Stats
2662
+ vm_stats = monitor.get_all_vm_stats()
2663
+ if vm_stats:
2664
+ table = Table(title="🖥️ Virtual Machines", border_style="cyan")
2665
+ table.add_column("Name", style="bold")
2666
+ table.add_column("State")
2667
+ table.add_column("CPU %")
2668
+ table.add_column("Memory")
2669
+ table.add_column("Disk")
2670
+ table.add_column("Network I/O")
2671
+
2672
+ for vm in vm_stats:
2673
+ state_color = "green" if vm.state == "running" else "yellow"
2674
+ cpu_color = "red" if vm.cpu_percent > 80 else "green"
2675
+ mem_pct = (
2676
+ (vm.memory_used_mb / vm.memory_total_mb * 100)
2677
+ if vm.memory_total_mb > 0
2678
+ else 0
2679
+ )
2680
+ mem_color = "red" if mem_pct > 80 else "green"
2681
+
2682
+ table.add_row(
2683
+ vm.name,
2684
+ f"[{state_color}]{vm.state}[/]",
2685
+ f"[{cpu_color}]{vm.cpu_percent:.1f}%[/]",
2686
+ f"[{mem_color}]{vm.memory_used_mb}/{vm.memory_total_mb} MB[/]",
2687
+ f"{vm.disk_used_gb:.1f}/{vm.disk_total_gb:.1f} GB",
2688
+ f"↓{format_bytes(vm.network_rx_bytes)} ↑{format_bytes(vm.network_tx_bytes)}",
2689
+ )
2690
+ console.print(table)
2691
+ else:
2692
+ console.print("[dim]No VMs found.[/]")
2693
+
2694
+ console.print()
2695
+
2696
+ # Container Stats
2697
+ container_stats = monitor.get_container_stats()
2698
+ if container_stats:
2699
+ table = Table(title="🐳 Containers", border_style="blue")
2700
+ table.add_column("Name", style="bold")
2701
+ table.add_column("State")
2702
+ table.add_column("CPU %")
2703
+ table.add_column("Memory")
2704
+ table.add_column("Network I/O")
2705
+ table.add_column("PIDs")
2706
+
2707
+ for c in container_stats:
2708
+ cpu_color = "red" if c.cpu_percent > 80 else "green"
2709
+ mem_pct = (
2710
+ (c.memory_used_mb / c.memory_limit_mb * 100) if c.memory_limit_mb > 0 else 0
2711
+ )
2712
+ mem_color = "red" if mem_pct > 80 else "green"
2713
+
2714
+ table.add_row(
2715
+ c.name,
2716
+ f"[green]{c.state}[/]",
2717
+ f"[{cpu_color}]{c.cpu_percent:.1f}%[/]",
2718
+ f"[{mem_color}]{c.memory_used_mb}/{c.memory_limit_mb} MB[/]",
2719
+ f"↓{format_bytes(c.network_rx_bytes)} ↑{format_bytes(c.network_tx_bytes)}",
2720
+ str(c.pids),
2721
+ )
2722
+ console.print(table)
2723
+ else:
2724
+ console.print("[dim]No containers running.[/]")
2725
+
2726
+ if once:
2727
+ break
2728
+
2729
+ console.print(f"\n[dim]Refreshing every {refresh}s. Press Ctrl+C to exit.[/]")
2730
+ time.sleep(refresh)
2731
+
2732
+ except KeyboardInterrupt:
2733
+ console.print("\n[yellow]Monitoring stopped.[/]")
2734
+ finally:
2735
+ monitor.close()
2736
+
2737
+
2738
+ def cmd_exec(args) -> None:
2739
+ """Execute command in VM via QEMU Guest Agent."""
2740
+ vm_name, config_file = _resolve_vm_name_and_config_file(args.name)
2741
+ conn_uri = "qemu:///session" if getattr(args, "user", False) else "qemu:///system"
2742
+ command = args.command
2743
+ timeout = getattr(args, "timeout", 30)
2744
+
2745
+ if not _qga_ping(vm_name, conn_uri):
2746
+ console.print(f"[red]❌ Cannot connect to VM '{vm_name}' via QEMU Guest Agent[/]")
2747
+ console.print("[dim]Make sure the VM is running and qemu-guest-agent is installed.[/]")
2748
+ return
2749
+
2750
+ console.print(f"[cyan]▶ Executing in {vm_name}:[/] {command}")
2751
+
2752
+ result = _qga_exec(vm_name, conn_uri, command, timeout=timeout)
2753
+
2754
+ if result is None:
2755
+ console.print("[red]❌ Command execution failed or timed out[/]")
2756
+ elif result == "":
2757
+ console.print("[dim](no output)[/]")
2758
+ else:
2759
+ console.print(result)
2760
+
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
+
2641
2910
  def cmd_keygen(args) -> None:
2642
2911
  """Generate encryption key for secure P2P transfers."""
2643
2912
  key_path = SecureExporter.generate_key()
@@ -3198,10 +3467,76 @@ def main():
3198
3467
  )
3199
3468
  test_parser.set_defaults(func=cmd_test)
3200
3469
 
3470
+ # Monitor command - real-time resource monitoring
3471
+ monitor_parser = subparsers.add_parser("monitor", help="Real-time resource monitoring")
3472
+ monitor_parser.add_argument(
3473
+ "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3474
+ )
3475
+ monitor_parser.add_argument(
3476
+ "--refresh", "-r", type=float, default=2.0, help="Refresh interval in seconds (default: 2)"
3477
+ )
3478
+ monitor_parser.add_argument("--once", action="store_true", help="Show stats once and exit")
3479
+ monitor_parser.set_defaults(func=cmd_monitor)
3480
+
3481
+ # Exec command - execute command in VM
3482
+ exec_parser = subparsers.add_parser("exec", help="Execute command in VM via QEMU Guest Agent")
3483
+ exec_parser.add_argument(
3484
+ "name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
3485
+ )
3486
+ exec_parser.add_argument("command", help="Command to execute in VM")
3487
+ exec_parser.add_argument(
3488
+ "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3489
+ )
3490
+ exec_parser.add_argument(
3491
+ "--timeout", "-t", type=int, default=30, help="Command timeout in seconds (default: 30)"
3492
+ )
3493
+ exec_parser.set_defaults(func=cmd_exec)
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
+
3201
3534
  # === P2P Secure Transfer Commands ===
3202
3535
 
3203
3536
  # Keygen command - generate encryption key
3204
- 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
+ )
3205
3540
  keygen_parser.set_defaults(func=cmd_keygen)
3206
3541
 
3207
3542
  # Export-encrypted command
@@ -3214,9 +3549,7 @@ def main():
3214
3549
  export_enc_parser.add_argument(
3215
3550
  "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3216
3551
  )
3217
- export_enc_parser.add_argument(
3218
- "-o", "--output", help="Output file (default: <vmname>.enc)"
3219
- )
3552
+ export_enc_parser.add_argument("-o", "--output", help="Output file (default: <vmname>.enc)")
3220
3553
  export_enc_parser.add_argument(
3221
3554
  "--user-data", action="store_true", help="Include user data (SSH keys, configs)"
3222
3555
  )
@@ -3234,9 +3567,7 @@ def main():
3234
3567
  "-u", "--user", action="store_true", help="Use user session (qemu:///session)"
3235
3568
  )
3236
3569
  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
- )
3570
+ import_enc_parser.add_argument("--user-data", action="store_true", help="Import user data")
3240
3571
  import_enc_parser.add_argument(
3241
3572
  "--include-data", "-d", action="store_true", help="Import app data"
3242
3573
  )
@@ -3248,15 +3579,11 @@ def main():
3248
3579
  )
3249
3580
  export_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3250
3581
  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
- )
3582
+ export_remote_parser.add_argument("-o", "--output", required=True, help="Local output file")
3254
3583
  export_remote_parser.add_argument(
3255
3584
  "--encrypted", "-e", action="store_true", help="Use encrypted export"
3256
3585
  )
3257
- export_remote_parser.add_argument(
3258
- "--user-data", action="store_true", help="Include user data"
3259
- )
3586
+ export_remote_parser.add_argument("--user-data", action="store_true", help="Include user data")
3260
3587
  export_remote_parser.add_argument(
3261
3588
  "--include-data", "-d", action="store_true", help="Include app data"
3262
3589
  )
@@ -3272,22 +3599,16 @@ def main():
3272
3599
  import_remote_parser.add_argument(
3273
3600
  "--encrypted", "-e", action="store_true", help="Use encrypted import"
3274
3601
  )
3275
- import_remote_parser.add_argument(
3276
- "--user-data", action="store_true", help="Import user data"
3277
- )
3602
+ import_remote_parser.add_argument("--user-data", action="store_true", help="Import user data")
3278
3603
  import_remote_parser.set_defaults(func=cmd_import_remote)
3279
3604
 
3280
3605
  # Sync-key command
3281
- sync_key_parser = subparsers.add_parser(
3282
- "sync-key", help="Sync encryption key to remote host"
3283
- )
3606
+ sync_key_parser = subparsers.add_parser("sync-key", help="Sync encryption key to remote host")
3284
3607
  sync_key_parser.add_argument("host", help="Remote host (user@hostname)")
3285
3608
  sync_key_parser.set_defaults(func=cmd_sync_key)
3286
3609
 
3287
3610
  # List-remote command
3288
- list_remote_parser = subparsers.add_parser(
3289
- "list-remote", help="List VMs on remote host"
3290
- )
3611
+ list_remote_parser = subparsers.add_parser("list-remote", help="List VMs on remote host")
3291
3612
  list_remote_parser.add_argument("host", help="Remote host (user@hostname)")
3292
3613
  list_remote_parser.set_defaults(func=cmd_list_remote)
3293
3614