clonebox 0.1.5__tar.gz → 0.1.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 0.1.5
3
+ Version: 0.1.7
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
@@ -38,6 +38,7 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
38
38
  Dynamic: license-file
39
39
 
40
40
  # CloneBox 📦
41
+ ![img.png](img.png)
41
42
 
42
43
  ```commandline
43
44
  ╔═══════════════════════════════════════════════════════╗
@@ -62,6 +63,7 @@ CloneBox lets you create isolated virtual machines with only the applications, d
62
63
  - ☁️ **Cloud-init** - Automatic package installation and service setup
63
64
  - 🖥️ **GUI support** - SPICE graphics with virt-viewer integration
64
65
  - ⚡ **Fast creation** - No full disk cloning, VMs are ready in seconds
66
+ - 📥 **Auto-download** - Automatically downloads and caches Ubuntu cloud images (stored in ~/Downloads)
65
67
 
66
68
  ## Installation
67
69
 
@@ -139,6 +141,8 @@ Simply run `clonebox` to start the interactive wizard:
139
141
 
140
142
  ```bash
141
143
  clonebox
144
+ clonebox clone . --user --run --replace --base-image ~/ubuntu-22.04-cloud.qcow2
145
+
142
146
  ```
143
147
 
144
148
  The wizard will:
@@ -259,6 +263,7 @@ The fastest way to clone your current working directory:
259
263
 
260
264
  ```bash
261
265
  # Clone current directory - generates .clonebox.yaml and asks to create VM
266
+ # Base OS image is automatically downloaded to ~/Downloads on first run
262
267
  clonebox clone .
263
268
 
264
269
  # Clone specific path
@@ -269,6 +274,15 @@ clonebox clone ~/projects/my-app --name my-dev-vm --run
269
274
 
270
275
  # Clone and edit config before creating
271
276
  clonebox clone . --edit
277
+
278
+ # Replace existing VM (stops, deletes, and recreates)
279
+ clonebox clone . --replace
280
+
281
+ # Use custom base image instead of auto-download
282
+ clonebox clone . --base-image ~/ubuntu-22.04-cloud.qcow2
283
+
284
+ # User session mode (no root required)
285
+ clonebox clone . --user
272
286
  ```
273
287
 
274
288
  Later, start the VM from any directory with `.clonebox.yaml`:
@@ -291,6 +305,63 @@ clonebox detect --yaml --dedupe
291
305
  clonebox detect --yaml --dedupe -o my-config.yaml
292
306
  ```
293
307
 
308
+ ### Base Images
309
+
310
+ CloneBox automatically downloads a bootable Ubuntu cloud image on first run:
311
+
312
+ ```bash
313
+ # Auto-download (default) - downloads Ubuntu 22.04 to ~/Downloads on first run
314
+ clonebox clone .
315
+
316
+ # Use custom base image
317
+ clonebox clone . --base-image ~/my-custom-image.qcow2
318
+
319
+ # Manual download (optional - clonebox does this automatically)
320
+ wget -O ~/Downloads/clonebox-ubuntu-jammy-amd64.qcow2 \
321
+ https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img
322
+ ```
323
+
324
+ **Base image behavior:**
325
+ - If no `--base-image` is specified, Ubuntu 22.04 cloud image is auto-downloaded
326
+ - Downloaded images are cached in `~/Downloads/clonebox-ubuntu-jammy-amd64.qcow2`
327
+ - Subsequent VMs reuse the cached image (no re-download)
328
+ - Each VM gets its own disk using the base image as a backing file (copy-on-write)
329
+
330
+ ### VM Login Credentials
331
+
332
+ VM credentials are managed through `.env` file for security:
333
+
334
+ **Setup:**
335
+ 1. Copy `.env.example` to `.env`:
336
+ ```bash
337
+ cp .env.example .env
338
+ ```
339
+
340
+ 2. Edit `.env` and set your password:
341
+ ```bash
342
+ # .env file
343
+ VM_PASSWORD=your_secure_password
344
+ VM_USERNAME=ubuntu
345
+ ```
346
+
347
+ 3. The `.clonebox.yaml` file references the password from `.env`:
348
+ ```yaml
349
+ vm:
350
+ username: ubuntu
351
+ password: ${VM_PASSWORD} # Loaded from .env
352
+ ```
353
+
354
+ **Default credentials (if .env not configured):**
355
+ - **Username:** `ubuntu`
356
+ - **Password:** `ubuntu`
357
+
358
+ **Security notes:**
359
+ - `.env` is automatically gitignored (never committed)
360
+ - Username is stored in YAML (not sensitive)
361
+ - Password is stored in `.env` (sensitive, not committed)
362
+ - Change password after first login: `passwd`
363
+ - User has passwordless sudo access
364
+
294
365
  ### User Session & Networking
295
366
 
296
367
  CloneBox supports creating VMs in user session (no root required) with automatic network fallback:
@@ -322,7 +393,9 @@ clonebox clone . --network auto
322
393
  | `clonebox clone <path>` | Generate `.clonebox.yaml` from path + running processes |
323
394
  | `clonebox clone . --run` | Clone and immediately start VM |
324
395
  | `clonebox clone . --edit` | Clone, edit config, then create |
396
+ | `clonebox clone . --replace` | Replace existing VM (stop, delete, recreate) |
325
397
  | `clonebox clone . --user` | Clone in user session (no root) |
398
+ | `clonebox clone . --base-image <path>` | Use custom base image |
326
399
  | `clonebox clone . --network user` | Use user-mode networking (slirp) |
327
400
  | `clonebox clone . --network auto` | Auto-detect network mode (default) |
328
401
  | `clonebox start .` | Start VM from `.clonebox.yaml` in current dir |
@@ -377,18 +450,21 @@ sudo usermod -aG kvm $USER
377
450
 
378
451
  ### VM Already Exists
379
452
 
380
- If you get "domain already exists" error:
453
+ If you get "VM already exists" error:
381
454
 
382
455
  ```bash
383
- # List VMs
384
- clonebox list
456
+ # Option 1: Use --replace flag to automatically replace it
457
+ clonebox clone . --replace
385
458
 
386
- # Stop and delete the existing VM
459
+ # Option 2: Delete manually first
387
460
  clonebox delete <vm-name>
388
461
 
389
- # Or use virsh directly
462
+ # Option 3: Use virsh directly
390
463
  virsh --connect qemu:///session destroy <vm-name>
391
464
  virsh --connect qemu:///session undefine <vm-name>
465
+
466
+ # Option 4: Start the existing VM instead
467
+ clonebox start <vm-name>
392
468
  ```
393
469
 
394
470
  ### virt-viewer not found
@@ -1,4 +1,5 @@
1
1
  # CloneBox 📦
2
+ ![img.png](img.png)
2
3
 
3
4
  ```commandline
4
5
  ╔═══════════════════════════════════════════════════════╗
@@ -23,6 +24,7 @@ CloneBox lets you create isolated virtual machines with only the applications, d
23
24
  - ☁️ **Cloud-init** - Automatic package installation and service setup
24
25
  - 🖥️ **GUI support** - SPICE graphics with virt-viewer integration
25
26
  - ⚡ **Fast creation** - No full disk cloning, VMs are ready in seconds
27
+ - 📥 **Auto-download** - Automatically downloads and caches Ubuntu cloud images (stored in ~/Downloads)
26
28
 
27
29
  ## Installation
28
30
 
@@ -100,6 +102,8 @@ Simply run `clonebox` to start the interactive wizard:
100
102
 
101
103
  ```bash
102
104
  clonebox
105
+ clonebox clone . --user --run --replace --base-image ~/ubuntu-22.04-cloud.qcow2
106
+
103
107
  ```
104
108
 
105
109
  The wizard will:
@@ -220,6 +224,7 @@ The fastest way to clone your current working directory:
220
224
 
221
225
  ```bash
222
226
  # Clone current directory - generates .clonebox.yaml and asks to create VM
227
+ # Base OS image is automatically downloaded to ~/Downloads on first run
223
228
  clonebox clone .
224
229
 
225
230
  # Clone specific path
@@ -230,6 +235,15 @@ clonebox clone ~/projects/my-app --name my-dev-vm --run
230
235
 
231
236
  # Clone and edit config before creating
232
237
  clonebox clone . --edit
238
+
239
+ # Replace existing VM (stops, deletes, and recreates)
240
+ clonebox clone . --replace
241
+
242
+ # Use custom base image instead of auto-download
243
+ clonebox clone . --base-image ~/ubuntu-22.04-cloud.qcow2
244
+
245
+ # User session mode (no root required)
246
+ clonebox clone . --user
233
247
  ```
234
248
 
235
249
  Later, start the VM from any directory with `.clonebox.yaml`:
@@ -252,6 +266,63 @@ clonebox detect --yaml --dedupe
252
266
  clonebox detect --yaml --dedupe -o my-config.yaml
253
267
  ```
254
268
 
269
+ ### Base Images
270
+
271
+ CloneBox automatically downloads a bootable Ubuntu cloud image on first run:
272
+
273
+ ```bash
274
+ # Auto-download (default) - downloads Ubuntu 22.04 to ~/Downloads on first run
275
+ clonebox clone .
276
+
277
+ # Use custom base image
278
+ clonebox clone . --base-image ~/my-custom-image.qcow2
279
+
280
+ # Manual download (optional - clonebox does this automatically)
281
+ wget -O ~/Downloads/clonebox-ubuntu-jammy-amd64.qcow2 \
282
+ https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img
283
+ ```
284
+
285
+ **Base image behavior:**
286
+ - If no `--base-image` is specified, Ubuntu 22.04 cloud image is auto-downloaded
287
+ - Downloaded images are cached in `~/Downloads/clonebox-ubuntu-jammy-amd64.qcow2`
288
+ - Subsequent VMs reuse the cached image (no re-download)
289
+ - Each VM gets its own disk using the base image as a backing file (copy-on-write)
290
+
291
+ ### VM Login Credentials
292
+
293
+ VM credentials are managed through `.env` file for security:
294
+
295
+ **Setup:**
296
+ 1. Copy `.env.example` to `.env`:
297
+ ```bash
298
+ cp .env.example .env
299
+ ```
300
+
301
+ 2. Edit `.env` and set your password:
302
+ ```bash
303
+ # .env file
304
+ VM_PASSWORD=your_secure_password
305
+ VM_USERNAME=ubuntu
306
+ ```
307
+
308
+ 3. The `.clonebox.yaml` file references the password from `.env`:
309
+ ```yaml
310
+ vm:
311
+ username: ubuntu
312
+ password: ${VM_PASSWORD} # Loaded from .env
313
+ ```
314
+
315
+ **Default credentials (if .env not configured):**
316
+ - **Username:** `ubuntu`
317
+ - **Password:** `ubuntu`
318
+
319
+ **Security notes:**
320
+ - `.env` is automatically gitignored (never committed)
321
+ - Username is stored in YAML (not sensitive)
322
+ - Password is stored in `.env` (sensitive, not committed)
323
+ - Change password after first login: `passwd`
324
+ - User has passwordless sudo access
325
+
255
326
  ### User Session & Networking
256
327
 
257
328
  CloneBox supports creating VMs in user session (no root required) with automatic network fallback:
@@ -283,7 +354,9 @@ clonebox clone . --network auto
283
354
  | `clonebox clone <path>` | Generate `.clonebox.yaml` from path + running processes |
284
355
  | `clonebox clone . --run` | Clone and immediately start VM |
285
356
  | `clonebox clone . --edit` | Clone, edit config, then create |
357
+ | `clonebox clone . --replace` | Replace existing VM (stop, delete, recreate) |
286
358
  | `clonebox clone . --user` | Clone in user session (no root) |
359
+ | `clonebox clone . --base-image <path>` | Use custom base image |
287
360
  | `clonebox clone . --network user` | Use user-mode networking (slirp) |
288
361
  | `clonebox clone . --network auto` | Auto-detect network mode (default) |
289
362
  | `clonebox start .` | Start VM from `.clonebox.yaml` in current dir |
@@ -338,18 +411,21 @@ sudo usermod -aG kvm $USER
338
411
 
339
412
  ### VM Already Exists
340
413
 
341
- If you get "domain already exists" error:
414
+ If you get "VM already exists" error:
342
415
 
343
416
  ```bash
344
- # List VMs
345
- clonebox list
417
+ # Option 1: Use --replace flag to automatically replace it
418
+ clonebox clone . --replace
346
419
 
347
- # Stop and delete the existing VM
420
+ # Option 2: Delete manually first
348
421
  clonebox delete <vm-name>
349
422
 
350
- # Or use virsh directly
423
+ # Option 3: Use virsh directly
351
424
  virsh --connect qemu:///session destroy <vm-name>
352
425
  virsh --connect qemu:///session undefine <vm-name>
426
+
427
+ # Option 4: Start the existing VM instead
428
+ clonebox start <vm-name>
353
429
  ```
354
430
 
355
431
  ### virt-viewer not found
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clonebox"
7
- version = "0.1.5"
7
+ version = "0.1.7"
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"}
@@ -6,7 +6,9 @@ CloneBox CLI - Interactive command-line interface for creating VMs.
6
6
  import argparse
7
7
  import json
8
8
  import os
9
+ import re
9
10
  import sys
11
+ from typing import Optional
10
12
  from datetime import datetime
11
13
  from pathlib import Path
12
14
 
@@ -487,6 +489,40 @@ def cmd_list(args):
487
489
 
488
490
 
489
491
  CLONEBOX_CONFIG_FILE = ".clonebox.yaml"
492
+ CLONEBOX_ENV_FILE = ".env"
493
+
494
+
495
+ def load_env_file(env_path: Path) -> dict:
496
+ """Load environment variables from .env file."""
497
+ env_vars = {}
498
+ if not env_path.exists():
499
+ return env_vars
500
+
501
+ with open(env_path) as f:
502
+ for line in f:
503
+ line = line.strip()
504
+ if not line or line.startswith('#'):
505
+ continue
506
+ if '=' in line:
507
+ key, value = line.split('=', 1)
508
+ env_vars[key.strip()] = value.strip()
509
+
510
+ return env_vars
511
+
512
+
513
+ def expand_env_vars(value, env_vars: dict):
514
+ """Expand environment variables in string values like ${VAR_NAME}."""
515
+ if isinstance(value, str):
516
+ # Replace ${VAR_NAME} with value from env_vars or os.environ
517
+ def replacer(match):
518
+ var_name = match.group(1)
519
+ return env_vars.get(var_name, os.environ.get(var_name, match.group(0)))
520
+ return re.sub(r'\$\{([^}]+)\}', replacer, value)
521
+ elif isinstance(value, dict):
522
+ return {k: expand_env_vars(v, env_vars) for k, v in value.items()}
523
+ elif isinstance(value, list):
524
+ return [expand_env_vars(item, env_vars) for item in value]
525
+ return value
490
526
 
491
527
 
492
528
  def deduplicate_list(items: list, key=None) -> list:
@@ -508,6 +544,7 @@ def generate_clonebox_yaml(
508
544
  target_path: str = None,
509
545
  vm_name: str = None,
510
546
  network_mode: str = "auto",
547
+ base_image: Optional[str] = None,
511
548
  ) -> str:
512
549
  """Generate YAML config from system snapshot."""
513
550
  sys_info = detector.get_system_info()
@@ -555,6 +592,16 @@ def generate_clonebox_yaml(
555
592
  paths_mapping[host_path] = f"/mnt/workdir{idx}"
556
593
  idx += 1
557
594
 
595
+ # Add default user folders (Downloads, Documents)
596
+ home_dir = Path.home()
597
+ default_folders = [
598
+ (home_dir / "Downloads", "/home/ubuntu/Downloads"),
599
+ (home_dir / "Documents", "/home/ubuntu/Documents"),
600
+ ]
601
+ for host_folder, guest_folder in default_folders:
602
+ if host_folder.exists() and str(host_folder) not in paths_mapping:
603
+ paths_mapping[str(host_folder)] = guest_folder
604
+
558
605
  # Determine VM name
559
606
  if not vm_name:
560
607
  if target_path:
@@ -566,6 +613,28 @@ def generate_clonebox_yaml(
566
613
  ram_mb = min(4096, int(sys_info["memory_available_gb"] * 1024 * 0.5))
567
614
  vcpus = max(2, sys_info["cpu_count"] // 2)
568
615
 
616
+ # Auto-detect packages from running applications and services
617
+ app_packages = detector.suggest_packages_for_apps(snapshot.applications)
618
+ service_packages = detector.suggest_packages_for_services(snapshot.running_services)
619
+
620
+ # Combine with base packages (apt only)
621
+ base_packages = [
622
+ "build-essential",
623
+ "git",
624
+ "curl",
625
+ "vim",
626
+ ]
627
+
628
+ # Merge apt packages and deduplicate
629
+ all_apt_packages = base_packages + app_packages["apt"] + service_packages["apt"]
630
+ if deduplicate:
631
+ all_apt_packages = deduplicate_list(all_apt_packages)
632
+
633
+ # Merge snap packages and deduplicate
634
+ all_snap_packages = app_packages["snap"] + service_packages["snap"]
635
+ if deduplicate:
636
+ all_snap_packages = deduplicate_list(all_snap_packages)
637
+
569
638
  # Build config
570
639
  config = {
571
640
  "version": "1",
@@ -575,18 +644,15 @@ def generate_clonebox_yaml(
575
644
  "ram_mb": ram_mb,
576
645
  "vcpus": vcpus,
577
646
  "gui": True,
578
- "base_image": None,
647
+ "base_image": base_image,
579
648
  "network_mode": network_mode,
649
+ "username": "ubuntu",
650
+ "password": "${VM_PASSWORD}",
580
651
  },
581
652
  "services": services,
582
- "packages": [
583
- "build-essential",
584
- "git",
585
- "curl",
586
- "vim",
587
- "python3",
588
- "python3-pip",
589
- ],
653
+ "packages": all_apt_packages,
654
+ "snap_packages": all_snap_packages,
655
+ "post_commands": [], # User can add custom commands to run after setup
590
656
  "paths": paths_mapping,
591
657
  "detected": {
592
658
  "running_apps": [
@@ -605,17 +671,115 @@ def generate_clonebox_yaml(
605
671
 
606
672
 
607
673
  def load_clonebox_config(path: Path) -> dict:
608
- """Load .clonebox.yaml config file."""
674
+ """Load .clonebox.yaml config file and expand environment variables from .env."""
609
675
  config_file = path / CLONEBOX_CONFIG_FILE if path.is_dir() else path
610
676
 
611
677
  if not config_file.exists():
612
678
  raise FileNotFoundError(f"Config file not found: {config_file}")
613
679
 
614
- with open(config_file) as f:
615
- return yaml.safe_load(f)
616
-
680
+ # Load .env file from same directory
681
+ config_dir = config_file.parent
682
+ env_file = config_dir / CLONEBOX_ENV_FILE
683
+ env_vars = load_env_file(env_file)
617
684
 
618
- def create_vm_from_config(config: dict, start: bool = False, user_session: bool = False) -> str:
685
+ # Load YAML config
686
+ with open(config_file) as f:
687
+ config = yaml.safe_load(f)
688
+
689
+ # Expand environment variables in config
690
+ config = expand_env_vars(config, env_vars)
691
+
692
+ return config
693
+
694
+
695
+ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout: int = 600):
696
+ """Monitor cloud-init status in VM and show progress."""
697
+ import subprocess
698
+ import time
699
+
700
+ conn_uri = "qemu:///session" if user_session else "qemu:///system"
701
+ start_time = time.time()
702
+ shutdown_count = 0 # Count consecutive shutdown detections
703
+ restart_detected = False
704
+
705
+ with Progress(
706
+ SpinnerColumn(),
707
+ TextColumn("[progress.description]{task.description}"),
708
+ console=console,
709
+ ) as progress:
710
+ task = progress.add_task("[cyan]Starting VM and initializing...", total=None)
711
+
712
+ while time.time() - start_time < timeout:
713
+ try:
714
+ elapsed = int(time.time() - start_time)
715
+ minutes = elapsed // 60
716
+ seconds = elapsed % 60
717
+
718
+ # Check VM state
719
+ result = subprocess.run(
720
+ ["virsh", "--connect", conn_uri, "domstate", vm_name],
721
+ capture_output=True,
722
+ text=True,
723
+ timeout=5
724
+ )
725
+
726
+ vm_state = result.stdout.strip().lower()
727
+
728
+ if "shut off" in vm_state or "shutting down" in vm_state:
729
+ # VM is shutting down - count consecutive detections
730
+ shutdown_count += 1
731
+ if shutdown_count >= 3 and not restart_detected:
732
+ # Confirmed shutdown after 3 consecutive checks
733
+ restart_detected = True
734
+ progress.update(task, description="[yellow]⟳ VM restarting after package installation...")
735
+ time.sleep(3)
736
+ continue
737
+ else:
738
+ # VM is running - reset shutdown counter
739
+ if shutdown_count > 0 and shutdown_count < 3:
740
+ # Was a brief glitch, not a real shutdown
741
+ shutdown_count = 0
742
+
743
+ if restart_detected and "running" in vm_state and shutdown_count >= 3:
744
+ # VM restarted successfully - GUI should be ready
745
+ progress.update(task, description=f"[green]✓ GUI ready! Total time: {minutes}m {seconds}s")
746
+ time.sleep(2)
747
+ break
748
+
749
+ # Estimate remaining time
750
+ if elapsed < 60:
751
+ remaining = "~9-10 minutes"
752
+ elif elapsed < 180:
753
+ remaining = f"~{8 - minutes} minutes"
754
+ elif elapsed < 300:
755
+ remaining = f"~{6 - minutes} minutes"
756
+ else:
757
+ remaining = "finishing soon"
758
+
759
+ if restart_detected:
760
+ progress.update(task, description=f"[cyan]Starting GUI... ({minutes}m {seconds}s, {remaining})")
761
+ else:
762
+ progress.update(task, description=f"[cyan]Installing desktop packages... ({minutes}m {seconds}s, {remaining})")
763
+
764
+ except (subprocess.TimeoutExpired, Exception) as e:
765
+ elapsed = int(time.time() - start_time)
766
+ minutes = elapsed // 60
767
+ seconds = elapsed % 60
768
+ progress.update(task, description=f"[cyan]Configuring VM... ({minutes}m {seconds}s)")
769
+
770
+ time.sleep(3)
771
+
772
+ # Final status
773
+ if time.time() - start_time >= timeout:
774
+ progress.update(task, description="[yellow]⚠ Monitoring timeout - VM continues in background")
775
+
776
+
777
+ def create_vm_from_config(
778
+ config: dict,
779
+ start: bool = False,
780
+ user_session: bool = False,
781
+ replace: bool = False,
782
+ ) -> str:
619
783
  """Create VM from YAML config dict."""
620
784
  vm_config = VMConfig(
621
785
  name=config["vm"]["name"],
@@ -625,9 +789,13 @@ def create_vm_from_config(config: dict, start: bool = False, user_session: bool
625
789
  base_image=config["vm"].get("base_image"),
626
790
  paths=config.get("paths", {}),
627
791
  packages=config.get("packages", []),
792
+ snap_packages=config.get("snap_packages", []),
628
793
  services=config.get("services", []),
794
+ post_commands=config.get("post_commands", []),
629
795
  user_session=user_session,
630
796
  network_mode=config["vm"].get("network_mode", "auto"),
797
+ username=config["vm"].get("username", "ubuntu"),
798
+ password=config["vm"].get("password", "ubuntu"),
631
799
  )
632
800
 
633
801
  cloner = SelectiveVMCloner(user_session=user_session)
@@ -643,10 +811,20 @@ def create_vm_from_config(config: dict, start: bool = False, user_session: bool
643
811
 
644
812
  console.print(f"[dim]Session: {checks['session_type']}, Storage: {checks['images_dir']}[/]")
645
813
 
646
- vm_uuid = cloner.create_vm(vm_config, console=console)
814
+ vm_uuid = cloner.create_vm(vm_config, console=console, replace=replace)
647
815
 
648
816
  if start:
649
817
  cloner.start_vm(vm_config.name, open_viewer=vm_config.gui, console=console)
818
+
819
+ # Monitor cloud-init progress if GUI is enabled
820
+ if vm_config.gui:
821
+ console.print("\n[bold cyan]📊 Monitoring setup progress...[/]")
822
+ try:
823
+ monitor_cloud_init_status(vm_config.name, user_session=user_session)
824
+ except KeyboardInterrupt:
825
+ console.print("\n[yellow]Monitoring stopped. VM continues setup in background.[/]")
826
+ except Exception as e:
827
+ console.print(f"\n[dim]Note: Could not monitor status ({e}). VM continues setup in background.[/]")
650
828
 
651
829
  return vm_uuid
652
830
 
@@ -681,6 +859,7 @@ def cmd_clone(args):
681
859
  target_path=str(target_path),
682
860
  vm_name=vm_name,
683
861
  network_mode=args.network,
862
+ base_image=getattr(args, "base_image", None),
684
863
  )
685
864
 
686
865
  # Save config file
@@ -712,7 +891,8 @@ def cmd_clone(args):
712
891
  ).ask()
713
892
 
714
893
  if create_now:
715
- config = yaml.safe_load(yaml_content)
894
+ # Load config with environment variable expansion
895
+ config = load_clonebox_config(config_file.parent)
716
896
  user_session = getattr(args, "user", False)
717
897
 
718
898
  console.print("\n[bold cyan]🔧 Creating VM...[/]\n")
@@ -720,10 +900,26 @@ def cmd_clone(args):
720
900
  console.print("[cyan]Using user session (qemu:///session) - no root required[/]")
721
901
 
722
902
  try:
723
- vm_uuid = create_vm_from_config(config, start=True, user_session=user_session)
903
+ vm_uuid = create_vm_from_config(
904
+ config,
905
+ start=True,
906
+ user_session=user_session,
907
+ replace=getattr(args, "replace", False),
908
+ )
724
909
  console.print(f"\n[bold green]🎉 VM '{config['vm']['name']}' is running![/]")
725
910
  console.print(f"[dim]UUID: {vm_uuid}[/]")
726
911
 
912
+ # Show GUI startup info if GUI is enabled
913
+ if config.get("vm", {}).get("gui", False):
914
+ username = config['vm'].get('username', 'ubuntu')
915
+ password = config['vm'].get('password', 'ubuntu')
916
+ console.print("\n[bold yellow]⏰ GUI Setup Process:[/]")
917
+ console.print(" [yellow]•[/] Installing desktop environment (~5-10 minutes)")
918
+ console.print(" [yellow]•[/] Automatic restart after installation")
919
+ console.print(" [yellow]•[/] GUI login screen will appear")
920
+ console.print(f" [yellow]•[/] Login: [cyan]{username}[/] / [cyan]{'*' * len(password)}[/] (from .env)")
921
+ console.print("\n[dim]💡 Progress will be monitored automatically below[/]")
922
+
727
923
  # Show mount instructions
728
924
  if config.get("paths"):
729
925
  console.print("\n[bold]Inside VM, mount paths with:[/]")
@@ -939,6 +1135,15 @@ def main():
939
1135
  default="auto",
940
1136
  help="Network mode: auto (default), default (libvirt network), user (slirp)",
941
1137
  )
1138
+ clone_parser.add_argument(
1139
+ "--base-image",
1140
+ help="Path to a bootable qcow2 image to use as a base disk",
1141
+ )
1142
+ clone_parser.add_argument(
1143
+ "--replace",
1144
+ action="store_true",
1145
+ help="If VM already exists, stop+undefine it and recreate (also deletes its storage)",
1146
+ )
942
1147
  clone_parser.set_defaults(func=cmd_clone)
943
1148
 
944
1149
  args = parser.parse_args()