clonebox 0.1.2__py3-none-any.whl → 0.1.4__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/__main__.py +7 -0
- clonebox/cli.py +317 -265
- clonebox/cloner.py +177 -119
- clonebox/detector.py +186 -108
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/METADATA +110 -3
- clonebox-0.1.4.dist-info/RECORD +11 -0
- clonebox-0.1.2.dist-info/RECORD +0 -10
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/WHEEL +0 -0
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/entry_points.txt +0 -0
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/top_level.txt +0 -0
clonebox/cli.py
CHANGED
|
@@ -3,40 +3,38 @@
|
|
|
3
3
|
CloneBox CLI - Interactive command-line interface for creating VMs.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import sys
|
|
7
|
-
import os
|
|
8
|
-
import json
|
|
9
6
|
import argparse
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
12
10
|
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
13
12
|
|
|
13
|
+
import questionary
|
|
14
14
|
import yaml
|
|
15
|
-
|
|
15
|
+
from questionary import Style
|
|
16
16
|
from rich.console import Console
|
|
17
|
-
from rich.table import Table
|
|
18
17
|
from rich.panel import Panel
|
|
19
18
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
20
|
-
from rich import
|
|
21
|
-
import questionary
|
|
22
|
-
from questionary import Style
|
|
19
|
+
from rich.table import Table
|
|
23
20
|
|
|
24
21
|
from clonebox import __version__
|
|
25
22
|
from clonebox.cloner import SelectiveVMCloner, VMConfig
|
|
26
23
|
from clonebox.detector import SystemDetector
|
|
27
24
|
|
|
28
|
-
|
|
29
25
|
# Custom questionary style
|
|
30
|
-
custom_style = Style(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
custom_style = Style(
|
|
27
|
+
[
|
|
28
|
+
("qmark", "fg:cyan bold"),
|
|
29
|
+
("question", "bold"),
|
|
30
|
+
("answer", "fg:green"),
|
|
31
|
+
("pointer", "fg:cyan bold"),
|
|
32
|
+
("highlighted", "fg:cyan bold"),
|
|
33
|
+
("selected", "fg:green"),
|
|
34
|
+
("separator", "fg:gray"),
|
|
35
|
+
("instruction", "fg:gray italic"),
|
|
36
|
+
]
|
|
37
|
+
)
|
|
40
38
|
|
|
41
39
|
console = Console()
|
|
42
40
|
|
|
@@ -61,140 +59,140 @@ def print_banner():
|
|
|
61
59
|
def interactive_mode():
|
|
62
60
|
"""Run the interactive VM creation wizard."""
|
|
63
61
|
print_banner()
|
|
64
|
-
|
|
62
|
+
|
|
65
63
|
console.print("[bold cyan]🔍 Detecting system state...[/]\n")
|
|
66
|
-
|
|
64
|
+
|
|
67
65
|
with Progress(
|
|
68
66
|
SpinnerColumn(),
|
|
69
67
|
TextColumn("[progress.description]{task.description}"),
|
|
70
68
|
console=console,
|
|
71
|
-
transient=True
|
|
69
|
+
transient=True,
|
|
72
70
|
) as progress:
|
|
73
71
|
task = progress.add_task("Scanning services, apps, and paths...", total=None)
|
|
74
72
|
detector = SystemDetector()
|
|
75
73
|
snapshot = detector.detect_all()
|
|
76
74
|
sys_info = detector.get_system_info()
|
|
77
75
|
docker_containers = detector.detect_docker_containers()
|
|
78
|
-
|
|
76
|
+
|
|
79
77
|
# Show system info
|
|
80
|
-
console.print(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
console.print(
|
|
79
|
+
Panel(
|
|
80
|
+
f"[bold]Hostname:[/] {sys_info['hostname']}\n"
|
|
81
|
+
f"[bold]User:[/] {sys_info['user']}\n"
|
|
82
|
+
f"[bold]CPU:[/] {sys_info['cpu_count']} cores\n"
|
|
83
|
+
f"[bold]RAM:[/] {sys_info['memory_available_gb']:.1f} / {sys_info['memory_total_gb']:.1f} GB available\n"
|
|
84
|
+
f"[bold]Disk:[/] {sys_info['disk_free_gb']:.1f} / {sys_info['disk_total_gb']:.1f} GB free",
|
|
85
|
+
title="[bold cyan]System Info[/]",
|
|
86
|
+
border_style="cyan",
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
90
|
console.print()
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
# === VM Name ===
|
|
93
|
-
vm_name = questionary.text(
|
|
94
|
-
|
|
95
|
-
default="clonebox-vm",
|
|
96
|
-
style=custom_style
|
|
97
|
-
).ask()
|
|
98
|
-
|
|
93
|
+
vm_name = questionary.text("VM name:", default="clonebox-vm", style=custom_style).ask()
|
|
94
|
+
|
|
99
95
|
if not vm_name:
|
|
100
96
|
console.print("[red]Cancelled.[/]")
|
|
101
97
|
return
|
|
102
|
-
|
|
98
|
+
|
|
103
99
|
# === RAM ===
|
|
104
|
-
max_ram = int(sys_info[
|
|
100
|
+
max_ram = int(sys_info["memory_available_gb"] * 1024 * 0.75) # 75% of available
|
|
105
101
|
default_ram = min(4096, max_ram)
|
|
106
|
-
|
|
102
|
+
|
|
107
103
|
ram_mb = questionary.text(
|
|
108
|
-
f"RAM (MB) [max recommended: {max_ram}]:",
|
|
109
|
-
default=str(default_ram),
|
|
110
|
-
style=custom_style
|
|
104
|
+
f"RAM (MB) [max recommended: {max_ram}]:", default=str(default_ram), style=custom_style
|
|
111
105
|
).ask()
|
|
112
106
|
ram_mb = int(ram_mb) if ram_mb else default_ram
|
|
113
|
-
|
|
107
|
+
|
|
114
108
|
# === vCPUs ===
|
|
115
|
-
max_vcpus = sys_info[
|
|
109
|
+
max_vcpus = sys_info["cpu_count"]
|
|
116
110
|
default_vcpus = max(2, max_vcpus // 2)
|
|
117
|
-
|
|
111
|
+
|
|
118
112
|
vcpus = questionary.text(
|
|
119
|
-
f"vCPUs [max: {max_vcpus}]:",
|
|
120
|
-
default=str(default_vcpus),
|
|
121
|
-
style=custom_style
|
|
113
|
+
f"vCPUs [max: {max_vcpus}]:", default=str(default_vcpus), style=custom_style
|
|
122
114
|
).ask()
|
|
123
115
|
vcpus = int(vcpus) if vcpus else default_vcpus
|
|
124
|
-
|
|
116
|
+
|
|
125
117
|
# === Services Selection ===
|
|
126
118
|
console.print("\n[bold cyan]📦 Select services to include in VM:[/]")
|
|
127
|
-
|
|
119
|
+
|
|
128
120
|
service_choices = []
|
|
129
121
|
for svc in snapshot.running_services:
|
|
130
122
|
label = f"{svc.name} ({svc.status})"
|
|
131
123
|
if svc.description:
|
|
132
124
|
label += f" - {svc.description[:40]}"
|
|
133
125
|
service_choices.append(questionary.Choice(label, value=svc.name))
|
|
134
|
-
|
|
126
|
+
|
|
135
127
|
selected_services = []
|
|
136
128
|
if service_choices:
|
|
137
|
-
selected_services =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
129
|
+
selected_services = (
|
|
130
|
+
questionary.checkbox(
|
|
131
|
+
"Services (space to select, enter to confirm):",
|
|
132
|
+
choices=service_choices,
|
|
133
|
+
style=custom_style,
|
|
134
|
+
).ask()
|
|
135
|
+
or []
|
|
136
|
+
)
|
|
142
137
|
else:
|
|
143
138
|
console.print("[dim] No interesting services detected[/]")
|
|
144
|
-
|
|
139
|
+
|
|
145
140
|
# === Applications/Processes Selection ===
|
|
146
141
|
console.print("\n[bold cyan]🚀 Select applications to track:[/]")
|
|
147
|
-
|
|
142
|
+
|
|
148
143
|
app_choices = []
|
|
149
144
|
for app in snapshot.running_apps[:20]: # Limit to top 20
|
|
150
145
|
label = f"{app.name} (PID: {app.pid}, {app.memory_mb:.0f} MB)"
|
|
151
146
|
if app.working_dir:
|
|
152
147
|
label += f" @ {app.working_dir[:30]}"
|
|
153
148
|
app_choices.append(questionary.Choice(label, value=app))
|
|
154
|
-
|
|
149
|
+
|
|
155
150
|
selected_apps = []
|
|
156
151
|
if app_choices:
|
|
157
|
-
selected_apps =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
152
|
+
selected_apps = (
|
|
153
|
+
questionary.checkbox(
|
|
154
|
+
"Applications (will add their working dirs):",
|
|
155
|
+
choices=app_choices,
|
|
156
|
+
style=custom_style,
|
|
157
|
+
).ask()
|
|
158
|
+
or []
|
|
159
|
+
)
|
|
162
160
|
else:
|
|
163
161
|
console.print("[dim] No interesting applications detected[/]")
|
|
164
|
-
|
|
162
|
+
|
|
165
163
|
# === Docker Containers ===
|
|
166
164
|
if docker_containers:
|
|
167
165
|
console.print("\n[bold cyan]🐳 Docker containers detected:[/]")
|
|
168
|
-
|
|
166
|
+
|
|
169
167
|
container_choices = [
|
|
170
|
-
questionary.Choice(
|
|
171
|
-
f"{c['name']} ({c['image']}) - {c['status']}",
|
|
172
|
-
value=c['name']
|
|
173
|
-
)
|
|
168
|
+
questionary.Choice(f"{c['name']} ({c['image']}) - {c['status']}", value=c["name"])
|
|
174
169
|
for c in docker_containers
|
|
175
170
|
]
|
|
176
|
-
|
|
177
|
-
selected_containers =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
171
|
+
|
|
172
|
+
selected_containers = (
|
|
173
|
+
questionary.checkbox(
|
|
174
|
+
"Containers (will share docker socket):",
|
|
175
|
+
choices=container_choices,
|
|
176
|
+
style=custom_style,
|
|
177
|
+
).ask()
|
|
178
|
+
or []
|
|
179
|
+
)
|
|
180
|
+
|
|
183
181
|
# If any docker selected, add docker socket
|
|
184
182
|
if selected_containers:
|
|
185
183
|
if "docker" not in selected_services:
|
|
186
184
|
selected_services.append("docker")
|
|
187
|
-
|
|
185
|
+
|
|
188
186
|
# === Paths Selection ===
|
|
189
187
|
console.print("\n[bold cyan]📁 Select paths to mount in VM:[/]")
|
|
190
|
-
|
|
188
|
+
|
|
191
189
|
# Group paths by type
|
|
192
190
|
path_groups = {}
|
|
193
191
|
for p in snapshot.paths:
|
|
194
192
|
if p.type not in path_groups:
|
|
195
193
|
path_groups[p.type] = []
|
|
196
194
|
path_groups[p.type].append(p)
|
|
197
|
-
|
|
195
|
+
|
|
198
196
|
path_choices = []
|
|
199
197
|
for ptype in ["project", "config", "data"]:
|
|
200
198
|
if ptype in path_groups:
|
|
@@ -204,102 +202,114 @@ def interactive_mode():
|
|
|
204
202
|
if p.description:
|
|
205
203
|
label += f" - {p.description}"
|
|
206
204
|
path_choices.append(questionary.Choice(label, value=p.path))
|
|
207
|
-
|
|
205
|
+
|
|
208
206
|
selected_paths = []
|
|
209
207
|
if path_choices:
|
|
210
|
-
selected_paths =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
208
|
+
selected_paths = (
|
|
209
|
+
questionary.checkbox(
|
|
210
|
+
"Paths (will be bind-mounted read-write):", choices=path_choices, style=custom_style
|
|
211
|
+
).ask()
|
|
212
|
+
or []
|
|
213
|
+
)
|
|
214
|
+
|
|
216
215
|
# Add working directories from selected applications
|
|
217
216
|
for app in selected_apps:
|
|
218
217
|
if app.working_dir and app.working_dir not in selected_paths:
|
|
219
218
|
selected_paths.append(app.working_dir)
|
|
220
|
-
|
|
219
|
+
|
|
221
220
|
# === Additional Packages ===
|
|
222
221
|
console.print("\n[bold cyan]📦 Additional packages to install:[/]")
|
|
223
|
-
|
|
222
|
+
|
|
224
223
|
common_packages = [
|
|
225
|
-
"build-essential",
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
224
|
+
"build-essential",
|
|
225
|
+
"git",
|
|
226
|
+
"curl",
|
|
227
|
+
"wget",
|
|
228
|
+
"vim",
|
|
229
|
+
"htop",
|
|
230
|
+
"python3",
|
|
231
|
+
"python3-pip",
|
|
232
|
+
"python3-venv",
|
|
233
|
+
"nodejs",
|
|
234
|
+
"npm",
|
|
235
|
+
"docker.io",
|
|
236
|
+
"docker-compose",
|
|
237
|
+
"nginx",
|
|
238
|
+
"postgresql",
|
|
239
|
+
"redis",
|
|
230
240
|
]
|
|
231
|
-
|
|
241
|
+
|
|
232
242
|
pkg_choices = [questionary.Choice(pkg, value=pkg) for pkg in common_packages]
|
|
233
|
-
|
|
234
|
-
selected_packages =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
243
|
+
|
|
244
|
+
selected_packages = (
|
|
245
|
+
questionary.checkbox(
|
|
246
|
+
"Packages (space to select):", choices=pkg_choices, style=custom_style
|
|
247
|
+
).ask()
|
|
248
|
+
or []
|
|
249
|
+
)
|
|
250
|
+
|
|
240
251
|
# Add custom packages
|
|
241
252
|
custom_pkgs = questionary.text(
|
|
242
|
-
"Additional packages (space-separated):",
|
|
243
|
-
default="",
|
|
244
|
-
style=custom_style
|
|
253
|
+
"Additional packages (space-separated):", default="", style=custom_style
|
|
245
254
|
).ask()
|
|
246
|
-
|
|
255
|
+
|
|
247
256
|
if custom_pkgs:
|
|
248
257
|
selected_packages.extend(custom_pkgs.split())
|
|
249
|
-
|
|
258
|
+
|
|
250
259
|
# === Base Image ===
|
|
251
260
|
base_image = questionary.text(
|
|
252
|
-
"Base image path (optional, leave empty for blank disk):",
|
|
253
|
-
default="",
|
|
254
|
-
style=custom_style
|
|
261
|
+
"Base image path (optional, leave empty for blank disk):", default="", style=custom_style
|
|
255
262
|
).ask()
|
|
256
|
-
|
|
263
|
+
|
|
257
264
|
# === GUI ===
|
|
258
265
|
enable_gui = questionary.confirm(
|
|
259
|
-
"Enable SPICE graphics (GUI)?",
|
|
260
|
-
default=True,
|
|
261
|
-
style=custom_style
|
|
266
|
+
"Enable SPICE graphics (GUI)?", default=True, style=custom_style
|
|
262
267
|
).ask()
|
|
263
|
-
|
|
268
|
+
|
|
264
269
|
# === Summary ===
|
|
265
270
|
console.print("\n")
|
|
266
|
-
|
|
271
|
+
|
|
267
272
|
# Build paths mapping
|
|
268
273
|
paths_mapping = {}
|
|
269
274
|
for idx, host_path in enumerate(selected_paths):
|
|
270
275
|
guest_path = f"/mnt/host{idx}"
|
|
271
276
|
paths_mapping[host_path] = guest_path
|
|
272
|
-
|
|
277
|
+
|
|
273
278
|
# Summary table
|
|
274
279
|
summary_table = Table(title="VM Configuration Summary", border_style="cyan")
|
|
275
280
|
summary_table.add_column("Setting", style="bold")
|
|
276
281
|
summary_table.add_column("Value")
|
|
277
|
-
|
|
282
|
+
|
|
278
283
|
summary_table.add_row("Name", vm_name)
|
|
279
284
|
summary_table.add_row("RAM", f"{ram_mb} MB")
|
|
280
285
|
summary_table.add_row("vCPUs", str(vcpus))
|
|
281
286
|
summary_table.add_row("Services", ", ".join(selected_services) or "None")
|
|
282
|
-
summary_table.add_row(
|
|
287
|
+
summary_table.add_row(
|
|
288
|
+
"Packages",
|
|
289
|
+
", ".join(selected_packages[:5]) + ("..." if len(selected_packages) > 5 else "") or "None",
|
|
290
|
+
)
|
|
283
291
|
summary_table.add_row("Paths", f"{len(paths_mapping)} bind mounts")
|
|
284
292
|
summary_table.add_row("GUI", "Yes (SPICE)" if enable_gui else "No")
|
|
285
|
-
|
|
293
|
+
|
|
286
294
|
console.print(summary_table)
|
|
287
|
-
|
|
295
|
+
|
|
288
296
|
if paths_mapping:
|
|
289
297
|
console.print("\n[bold]Bind mounts:[/]")
|
|
290
298
|
for host, guest in paths_mapping.items():
|
|
291
299
|
console.print(f" [cyan]{host}[/] → [green]{guest}[/]")
|
|
292
|
-
|
|
300
|
+
|
|
293
301
|
console.print()
|
|
294
|
-
|
|
302
|
+
|
|
295
303
|
# === Confirm ===
|
|
296
|
-
if not questionary.confirm(
|
|
304
|
+
if not questionary.confirm(
|
|
305
|
+
"Create VM with these settings?", default=True, style=custom_style
|
|
306
|
+
).ask():
|
|
297
307
|
console.print("[yellow]Cancelled.[/]")
|
|
298
308
|
return
|
|
299
|
-
|
|
309
|
+
|
|
300
310
|
# === Create VM ===
|
|
301
311
|
console.print("\n[bold cyan]🔧 Creating VM...[/]\n")
|
|
302
|
-
|
|
312
|
+
|
|
303
313
|
config = VMConfig(
|
|
304
314
|
name=vm_name,
|
|
305
315
|
ram_mb=ram_mb,
|
|
@@ -310,10 +320,10 @@ def interactive_mode():
|
|
|
310
320
|
packages=selected_packages,
|
|
311
321
|
services=selected_services,
|
|
312
322
|
)
|
|
313
|
-
|
|
323
|
+
|
|
314
324
|
try:
|
|
315
|
-
cloner = SelectiveVMCloner()
|
|
316
|
-
|
|
325
|
+
cloner = SelectiveVMCloner(user_session=user_session)
|
|
326
|
+
|
|
317
327
|
# Check prerequisites
|
|
318
328
|
checks = cloner.check_prerequisites()
|
|
319
329
|
if not all(checks.values()):
|
|
@@ -321,26 +331,26 @@ def interactive_mode():
|
|
|
321
331
|
for check, passed in checks.items():
|
|
322
332
|
icon = "✅" if passed else "❌"
|
|
323
333
|
console.print(f" {icon} {check}")
|
|
324
|
-
|
|
334
|
+
|
|
325
335
|
if not checks["libvirt_connected"]:
|
|
326
336
|
console.print("\n[red]Cannot proceed without libvirt connection.[/]")
|
|
327
337
|
console.print("Try: [cyan]sudo systemctl start libvirtd[/]")
|
|
328
338
|
return
|
|
329
|
-
|
|
339
|
+
|
|
330
340
|
vm_uuid = cloner.create_vm(config, console=console)
|
|
331
|
-
|
|
341
|
+
|
|
332
342
|
# Ask to start
|
|
333
343
|
if questionary.confirm("Start VM now?", default=True, style=custom_style).ask():
|
|
334
344
|
cloner.start_vm(vm_name, open_viewer=enable_gui, console=console)
|
|
335
345
|
console.print("\n[bold green]🎉 VM is running![/]")
|
|
336
|
-
|
|
346
|
+
|
|
337
347
|
if paths_mapping:
|
|
338
348
|
console.print("\n[bold]Inside the VM, mount shared folders with:[/]")
|
|
339
349
|
for idx, (host, guest) in enumerate(paths_mapping.items()):
|
|
340
350
|
console.print(f" [cyan]sudo mount -t 9p -o trans=virtio mount{idx} {guest}[/]")
|
|
341
|
-
|
|
351
|
+
|
|
342
352
|
console.print(f"\n[dim]VM UUID: {vm_uuid}[/]")
|
|
343
|
-
|
|
353
|
+
|
|
344
354
|
except Exception as e:
|
|
345
355
|
console.print(f"\n[red]❌ Error: {e}[/]")
|
|
346
356
|
raise
|
|
@@ -349,7 +359,7 @@ def interactive_mode():
|
|
|
349
359
|
def cmd_create(args):
|
|
350
360
|
"""Create VM from JSON config."""
|
|
351
361
|
config_data = json.loads(args.config)
|
|
352
|
-
|
|
362
|
+
|
|
353
363
|
config = VMConfig(
|
|
354
364
|
name=args.name,
|
|
355
365
|
ram_mb=args.ram,
|
|
@@ -360,40 +370,40 @@ def cmd_create(args):
|
|
|
360
370
|
packages=config_data.get("packages", []),
|
|
361
371
|
services=config_data.get("services", []),
|
|
362
372
|
)
|
|
363
|
-
|
|
373
|
+
|
|
364
374
|
cloner = SelectiveVMCloner()
|
|
365
375
|
vm_uuid = cloner.create_vm(config, console=console)
|
|
366
|
-
|
|
376
|
+
|
|
367
377
|
if args.start:
|
|
368
378
|
cloner.start_vm(args.name, open_viewer=not args.no_gui, console=console)
|
|
369
|
-
|
|
379
|
+
|
|
370
380
|
console.print(f"[green]✅ VM created: {vm_uuid}[/]")
|
|
371
381
|
|
|
372
382
|
|
|
373
383
|
def cmd_start(args):
|
|
374
384
|
"""Start a VM or create from .clonebox.yaml."""
|
|
375
385
|
name = args.name
|
|
376
|
-
|
|
386
|
+
|
|
377
387
|
# Check if it's a path (contains / or . or ~)
|
|
378
388
|
if name and (name.startswith(".") or name.startswith("/") or name.startswith("~")):
|
|
379
389
|
# Treat as path - load .clonebox.yaml
|
|
380
390
|
target_path = Path(name).expanduser().resolve()
|
|
381
|
-
|
|
391
|
+
|
|
382
392
|
if target_path.is_dir():
|
|
383
393
|
config_file = target_path / CLONEBOX_CONFIG_FILE
|
|
384
394
|
else:
|
|
385
395
|
config_file = target_path
|
|
386
|
-
|
|
396
|
+
|
|
387
397
|
if not config_file.exists():
|
|
388
398
|
console.print(f"[red]❌ Config not found: {config_file}[/]")
|
|
389
399
|
console.print(f"[dim]Run 'clonebox clone {target_path}' first to generate config[/]")
|
|
390
400
|
return
|
|
391
|
-
|
|
401
|
+
|
|
392
402
|
console.print(f"[bold cyan]📦 Loading config: {config_file}[/]\n")
|
|
393
|
-
|
|
403
|
+
|
|
394
404
|
config = load_clonebox_config(config_file)
|
|
395
405
|
vm_name = config["vm"]["name"]
|
|
396
|
-
|
|
406
|
+
|
|
397
407
|
# Check if VM already exists
|
|
398
408
|
cloner = SelectiveVMCloner()
|
|
399
409
|
try:
|
|
@@ -404,19 +414,19 @@ def cmd_start(args):
|
|
|
404
414
|
return
|
|
405
415
|
except:
|
|
406
416
|
pass
|
|
407
|
-
|
|
417
|
+
|
|
408
418
|
# Create new VM from config
|
|
409
419
|
console.print(f"[cyan]Creating VM '{vm_name}' from config...[/]\n")
|
|
410
420
|
vm_uuid = create_vm_from_config(config, start=True)
|
|
411
421
|
console.print(f"\n[bold green]🎉 VM '{vm_name}' is running![/]")
|
|
412
422
|
console.print(f"[dim]UUID: {vm_uuid}[/]")
|
|
413
|
-
|
|
423
|
+
|
|
414
424
|
if config.get("paths"):
|
|
415
425
|
console.print("\n[bold]Inside VM, mount paths with:[/]")
|
|
416
426
|
for idx, (host, guest) in enumerate(config["paths"].items()):
|
|
417
427
|
console.print(f" [cyan]sudo mount -t 9p -o trans=virtio mount{idx} {guest}[/]")
|
|
418
428
|
return
|
|
419
|
-
|
|
429
|
+
|
|
420
430
|
# Default: treat as VM name
|
|
421
431
|
if not name:
|
|
422
432
|
# No argument - check current directory for .clonebox.yaml
|
|
@@ -426,10 +436,12 @@ def cmd_start(args):
|
|
|
426
436
|
args.name = "."
|
|
427
437
|
return cmd_start(args)
|
|
428
438
|
else:
|
|
429
|
-
console.print(
|
|
439
|
+
console.print(
|
|
440
|
+
"[red]❌ No VM name specified and no .clonebox.yaml in current directory[/]"
|
|
441
|
+
)
|
|
430
442
|
console.print("[dim]Usage: clonebox start <vm-name> or clonebox start .[/]")
|
|
431
443
|
return
|
|
432
|
-
|
|
444
|
+
|
|
433
445
|
cloner = SelectiveVMCloner()
|
|
434
446
|
cloner.start_vm(name, open_viewer=not args.no_viewer, console=console)
|
|
435
447
|
|
|
@@ -444,13 +456,11 @@ def cmd_delete(args):
|
|
|
444
456
|
"""Delete a VM."""
|
|
445
457
|
if not args.yes:
|
|
446
458
|
if not questionary.confirm(
|
|
447
|
-
f"Delete VM '{args.name}' and its storage?",
|
|
448
|
-
default=False,
|
|
449
|
-
style=custom_style
|
|
459
|
+
f"Delete VM '{args.name}' and its storage?", default=False, style=custom_style
|
|
450
460
|
).ask():
|
|
451
461
|
console.print("[yellow]Cancelled.[/]")
|
|
452
462
|
return
|
|
453
|
-
|
|
463
|
+
|
|
454
464
|
cloner = SelectiveVMCloner()
|
|
455
465
|
cloner.delete_vm(args.name, delete_storage=not args.keep_storage, console=console)
|
|
456
466
|
|
|
@@ -459,20 +469,20 @@ def cmd_list(args):
|
|
|
459
469
|
"""List all VMs."""
|
|
460
470
|
cloner = SelectiveVMCloner()
|
|
461
471
|
vms = cloner.list_vms()
|
|
462
|
-
|
|
472
|
+
|
|
463
473
|
if not vms:
|
|
464
474
|
console.print("[dim]No VMs found.[/]")
|
|
465
475
|
return
|
|
466
|
-
|
|
476
|
+
|
|
467
477
|
table = Table(title="Virtual Machines", border_style="cyan")
|
|
468
478
|
table.add_column("Name", style="bold")
|
|
469
479
|
table.add_column("State")
|
|
470
480
|
table.add_column("UUID", style="dim")
|
|
471
|
-
|
|
481
|
+
|
|
472
482
|
for vm in vms:
|
|
473
483
|
state_style = "green" if vm["state"] == "running" else "dim"
|
|
474
484
|
table.add_row(vm["name"], f"[{state_style}]{vm['state']}[/]", vm["uuid"][:8])
|
|
475
|
-
|
|
485
|
+
|
|
476
486
|
console.print(table)
|
|
477
487
|
|
|
478
488
|
|
|
@@ -491,64 +501,71 @@ def deduplicate_list(items: list, key=None) -> list:
|
|
|
491
501
|
return result
|
|
492
502
|
|
|
493
503
|
|
|
494
|
-
def generate_clonebox_yaml(
|
|
495
|
-
|
|
504
|
+
def generate_clonebox_yaml(
|
|
505
|
+
snapshot,
|
|
506
|
+
detector,
|
|
507
|
+
deduplicate: bool = True,
|
|
508
|
+
target_path: str = None,
|
|
509
|
+
vm_name: str = None,
|
|
510
|
+
network_mode: str = "auto",
|
|
511
|
+
) -> str:
|
|
496
512
|
"""Generate YAML config from system snapshot."""
|
|
497
513
|
sys_info = detector.get_system_info()
|
|
498
|
-
|
|
514
|
+
|
|
499
515
|
# Collect services
|
|
500
516
|
services = [s.name for s in snapshot.running_services]
|
|
501
517
|
if deduplicate:
|
|
502
518
|
services = deduplicate_list(services)
|
|
503
|
-
|
|
519
|
+
|
|
504
520
|
# Collect paths with types
|
|
505
521
|
paths_by_type = {"project": [], "config": [], "data": []}
|
|
506
522
|
for p in snapshot.paths:
|
|
507
523
|
if p.type in paths_by_type:
|
|
508
524
|
paths_by_type[p.type].append(p.path)
|
|
509
|
-
|
|
525
|
+
|
|
510
526
|
if deduplicate:
|
|
511
527
|
for ptype in paths_by_type:
|
|
512
528
|
paths_by_type[ptype] = deduplicate_list(paths_by_type[ptype])
|
|
513
|
-
|
|
529
|
+
|
|
514
530
|
# Collect working directories from running apps
|
|
515
531
|
working_dirs = []
|
|
516
532
|
for app in snapshot.applications:
|
|
517
533
|
if app.working_dir and app.working_dir != "/" and app.working_dir.startswith("/home"):
|
|
518
534
|
working_dirs.append(app.working_dir)
|
|
519
|
-
|
|
535
|
+
|
|
520
536
|
if deduplicate:
|
|
521
537
|
working_dirs = deduplicate_list(working_dirs)
|
|
522
|
-
|
|
538
|
+
|
|
523
539
|
# If target_path specified, prioritize it
|
|
524
540
|
if target_path:
|
|
525
|
-
target_path =
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
541
|
+
target_path = Path(target_path).resolve()
|
|
542
|
+
target_str = str(target_path)
|
|
543
|
+
if target_str not in paths_by_type["project"]:
|
|
544
|
+
paths_by_type["project"].insert(0, target_str)
|
|
545
|
+
|
|
529
546
|
# Build paths mapping
|
|
530
547
|
paths_mapping = {}
|
|
531
548
|
idx = 0
|
|
532
549
|
for host_path in paths_by_type["project"][:5]: # Limit projects
|
|
533
550
|
paths_mapping[host_path] = f"/mnt/project{idx}"
|
|
534
551
|
idx += 1
|
|
535
|
-
|
|
552
|
+
|
|
536
553
|
for host_path in working_dirs[:3]: # Limit working dirs
|
|
537
554
|
if host_path not in paths_mapping:
|
|
538
555
|
paths_mapping[host_path] = f"/mnt/workdir{idx}"
|
|
539
556
|
idx += 1
|
|
540
|
-
|
|
557
|
+
|
|
541
558
|
# Determine VM name
|
|
542
559
|
if not vm_name:
|
|
543
560
|
if target_path:
|
|
544
|
-
vm_name = f"clone-{
|
|
561
|
+
vm_name = f"clone-{target_path.name}"
|
|
545
562
|
else:
|
|
546
563
|
vm_name = f"clone-{sys_info['hostname']}"
|
|
547
|
-
|
|
564
|
+
|
|
548
565
|
# Calculate recommended resources
|
|
549
|
-
ram_mb = min(4096, int(sys_info[
|
|
550
|
-
vcpus = max(2, sys_info[
|
|
551
|
-
|
|
566
|
+
ram_mb = min(4096, int(sys_info["memory_available_gb"] * 1024 * 0.5))
|
|
567
|
+
vcpus = max(2, sys_info["cpu_count"] // 2)
|
|
568
|
+
|
|
552
569
|
# Build config
|
|
553
570
|
config = {
|
|
554
571
|
"version": "1",
|
|
@@ -559,11 +576,16 @@ def generate_clonebox_yaml(snapshot, detector, deduplicate: bool = True,
|
|
|
559
576
|
"vcpus": vcpus,
|
|
560
577
|
"gui": True,
|
|
561
578
|
"base_image": None,
|
|
579
|
+
"network_mode": network_mode,
|
|
562
580
|
},
|
|
563
581
|
"services": services,
|
|
564
582
|
"packages": [
|
|
565
|
-
"build-essential",
|
|
566
|
-
"
|
|
583
|
+
"build-essential",
|
|
584
|
+
"git",
|
|
585
|
+
"curl",
|
|
586
|
+
"vim",
|
|
587
|
+
"python3",
|
|
588
|
+
"python3-pip",
|
|
567
589
|
],
|
|
568
590
|
"paths": paths_mapping,
|
|
569
591
|
"detected": {
|
|
@@ -575,20 +597,20 @@ def generate_clonebox_yaml(snapshot, detector, deduplicate: bool = True,
|
|
|
575
597
|
"projects": paths_by_type["project"],
|
|
576
598
|
"configs": paths_by_type["config"][:5],
|
|
577
599
|
"data": paths_by_type["data"][:5],
|
|
578
|
-
}
|
|
579
|
-
}
|
|
600
|
+
},
|
|
601
|
+
},
|
|
580
602
|
}
|
|
581
|
-
|
|
603
|
+
|
|
582
604
|
return yaml.dump(config, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
583
605
|
|
|
584
606
|
|
|
585
607
|
def load_clonebox_config(path: Path) -> dict:
|
|
586
608
|
"""Load .clonebox.yaml config file."""
|
|
587
609
|
config_file = path / CLONEBOX_CONFIG_FILE if path.is_dir() else path
|
|
588
|
-
|
|
610
|
+
|
|
589
611
|
if not config_file.exists():
|
|
590
612
|
raise FileNotFoundError(f"Config file not found: {config_file}")
|
|
591
|
-
|
|
613
|
+
|
|
592
614
|
with open(config_file) as f:
|
|
593
615
|
return yaml.safe_load(f)
|
|
594
616
|
|
|
@@ -605,67 +627,74 @@ def create_vm_from_config(config: dict, start: bool = False, user_session: bool
|
|
|
605
627
|
packages=config.get("packages", []),
|
|
606
628
|
services=config.get("services", []),
|
|
607
629
|
user_session=user_session,
|
|
630
|
+
network_mode=config["vm"].get("network_mode", "auto"),
|
|
608
631
|
)
|
|
609
|
-
|
|
632
|
+
|
|
610
633
|
cloner = SelectiveVMCloner(user_session=user_session)
|
|
611
|
-
|
|
634
|
+
|
|
612
635
|
# Check prerequisites and show detailed info
|
|
613
636
|
checks = cloner.check_prerequisites()
|
|
614
|
-
|
|
637
|
+
|
|
615
638
|
if not checks["images_dir_writable"]:
|
|
616
639
|
console.print(f"[yellow]⚠️ Storage directory: {checks['images_dir']}[/]")
|
|
617
640
|
if "images_dir_error" in checks:
|
|
618
641
|
console.print(f"[red]{checks['images_dir_error']}[/]")
|
|
619
642
|
raise PermissionError(checks["images_dir_error"])
|
|
620
|
-
|
|
643
|
+
|
|
621
644
|
console.print(f"[dim]Session: {checks['session_type']}, Storage: {checks['images_dir']}[/]")
|
|
622
|
-
|
|
645
|
+
|
|
623
646
|
vm_uuid = cloner.create_vm(vm_config, console=console)
|
|
624
|
-
|
|
647
|
+
|
|
625
648
|
if start:
|
|
626
649
|
cloner.start_vm(vm_config.name, open_viewer=vm_config.gui, console=console)
|
|
627
|
-
|
|
650
|
+
|
|
628
651
|
return vm_uuid
|
|
629
652
|
|
|
630
653
|
|
|
631
654
|
def cmd_clone(args):
|
|
632
655
|
"""Generate clone config from path and optionally create VM."""
|
|
633
656
|
target_path = Path(args.path).resolve()
|
|
634
|
-
|
|
657
|
+
|
|
635
658
|
if not target_path.exists():
|
|
636
659
|
console.print(f"[red]❌ Path does not exist: {target_path}[/]")
|
|
637
660
|
return
|
|
638
|
-
|
|
661
|
+
|
|
639
662
|
console.print(f"[bold cyan]📦 Generating clone config for: {target_path}[/]\n")
|
|
640
|
-
|
|
663
|
+
|
|
641
664
|
# Detect system state
|
|
642
665
|
with Progress(
|
|
643
666
|
SpinnerColumn(),
|
|
644
667
|
TextColumn("[progress.description]{task.description}"),
|
|
645
668
|
console=console,
|
|
646
|
-
transient=True
|
|
669
|
+
transient=True,
|
|
647
670
|
) as progress:
|
|
648
671
|
progress.add_task("Scanning system...", total=None)
|
|
649
672
|
detector = SystemDetector()
|
|
650
673
|
snapshot = detector.detect_all()
|
|
651
|
-
|
|
674
|
+
|
|
652
675
|
# Generate config
|
|
653
676
|
vm_name = args.name or f"clone-{target_path.name}"
|
|
654
677
|
yaml_content = generate_clonebox_yaml(
|
|
655
|
-
snapshot,
|
|
678
|
+
snapshot,
|
|
679
|
+
detector,
|
|
656
680
|
deduplicate=args.dedupe,
|
|
657
681
|
target_path=str(target_path),
|
|
658
|
-
vm_name=vm_name
|
|
682
|
+
vm_name=vm_name,
|
|
683
|
+
network_mode=args.network,
|
|
659
684
|
)
|
|
660
|
-
|
|
685
|
+
|
|
661
686
|
# Save config file
|
|
662
|
-
config_file =
|
|
687
|
+
config_file = (
|
|
688
|
+
target_path / CLONEBOX_CONFIG_FILE
|
|
689
|
+
if target_path.is_dir()
|
|
690
|
+
else target_path.parent / CLONEBOX_CONFIG_FILE
|
|
691
|
+
)
|
|
663
692
|
config_file.write_text(yaml_content)
|
|
664
693
|
console.print(f"[green]✅ Config saved: {config_file}[/]\n")
|
|
665
|
-
|
|
694
|
+
|
|
666
695
|
# Show config
|
|
667
696
|
console.print(Panel(yaml_content, title="[bold].clonebox.yaml[/]", border_style="cyan"))
|
|
668
|
-
|
|
697
|
+
|
|
669
698
|
# Open in editor if requested
|
|
670
699
|
if args.edit:
|
|
671
700
|
editor = os.environ.get("EDITOR", "nano")
|
|
@@ -673,30 +702,28 @@ def cmd_clone(args):
|
|
|
673
702
|
os.system(f"{editor} {config_file}")
|
|
674
703
|
# Reload after edit
|
|
675
704
|
yaml_content = config_file.read_text()
|
|
676
|
-
|
|
705
|
+
|
|
677
706
|
# Ask to create VM
|
|
678
707
|
if args.run:
|
|
679
708
|
create_now = True
|
|
680
709
|
else:
|
|
681
710
|
create_now = questionary.confirm(
|
|
682
|
-
"Create VM with this config?",
|
|
683
|
-
default=True,
|
|
684
|
-
style=custom_style
|
|
711
|
+
"Create VM with this config?", default=True, style=custom_style
|
|
685
712
|
).ask()
|
|
686
|
-
|
|
713
|
+
|
|
687
714
|
if create_now:
|
|
688
715
|
config = yaml.safe_load(yaml_content)
|
|
689
|
-
user_session = getattr(args,
|
|
690
|
-
|
|
716
|
+
user_session = getattr(args, "user", False)
|
|
717
|
+
|
|
691
718
|
console.print("\n[bold cyan]🔧 Creating VM...[/]\n")
|
|
692
719
|
if user_session:
|
|
693
720
|
console.print("[cyan]Using user session (qemu:///session) - no root required[/]")
|
|
694
|
-
|
|
721
|
+
|
|
695
722
|
try:
|
|
696
723
|
vm_uuid = create_vm_from_config(config, start=True, user_session=user_session)
|
|
697
724
|
console.print(f"\n[bold green]🎉 VM '{config['vm']['name']}' is running![/]")
|
|
698
725
|
console.print(f"[dim]UUID: {vm_uuid}[/]")
|
|
699
|
-
|
|
726
|
+
|
|
700
727
|
# Show mount instructions
|
|
701
728
|
if config.get("paths"):
|
|
702
729
|
console.print("\n[bold]Inside VM, mount paths with:[/]")
|
|
@@ -704,36 +731,40 @@ def cmd_clone(args):
|
|
|
704
731
|
console.print(f" [cyan]sudo mount -t 9p -o trans=virtio mount{idx} {guest}[/]")
|
|
705
732
|
except PermissionError as e:
|
|
706
733
|
console.print(f"[red]❌ Permission Error:[/]\n{e}")
|
|
707
|
-
console.print(
|
|
734
|
+
console.print("\n[yellow]💡 Try running with --user flag:[/]")
|
|
708
735
|
console.print(f" [cyan]clonebox clone {target_path} --user[/]")
|
|
709
736
|
except Exception as e:
|
|
710
737
|
console.print(f"[red]❌ Error: {e}[/]")
|
|
711
738
|
else:
|
|
712
|
-
console.print(
|
|
739
|
+
console.print("\n[dim]To create VM later, run:[/]")
|
|
713
740
|
console.print(f" [cyan]clonebox start {target_path}[/]")
|
|
714
741
|
|
|
715
742
|
|
|
716
743
|
def cmd_detect(args):
|
|
717
744
|
"""Detect and show system state."""
|
|
718
745
|
console.print("[bold cyan]🔍 Detecting system state...[/]\n")
|
|
719
|
-
|
|
746
|
+
|
|
720
747
|
detector = SystemDetector()
|
|
721
748
|
snapshot = detector.detect_all()
|
|
722
|
-
|
|
749
|
+
|
|
723
750
|
# JSON output
|
|
724
751
|
if args.json:
|
|
725
752
|
result = {
|
|
726
753
|
"services": [{"name": s.name, "status": s.status} for s in snapshot.running_services],
|
|
727
|
-
"applications": [
|
|
728
|
-
|
|
754
|
+
"applications": [
|
|
755
|
+
{"name": a.name, "pid": a.pid, "cwd": a.working_dir} for a in snapshot.applications
|
|
756
|
+
],
|
|
757
|
+
"paths": [
|
|
758
|
+
{"path": p.path, "type": p.type, "size_mb": p.size_mb} for p in snapshot.paths
|
|
759
|
+
],
|
|
729
760
|
}
|
|
730
761
|
print(json.dumps(result, indent=2))
|
|
731
762
|
return
|
|
732
|
-
|
|
763
|
+
|
|
733
764
|
# YAML output
|
|
734
765
|
if args.yaml:
|
|
735
766
|
result = generate_clonebox_yaml(snapshot, detector, deduplicate=args.dedupe)
|
|
736
|
-
|
|
767
|
+
|
|
737
768
|
if args.output:
|
|
738
769
|
output_path = Path(args.output)
|
|
739
770
|
output_path.write_text(result)
|
|
@@ -741,25 +772,25 @@ def cmd_detect(args):
|
|
|
741
772
|
else:
|
|
742
773
|
print(result)
|
|
743
774
|
return
|
|
744
|
-
|
|
775
|
+
|
|
745
776
|
# Services
|
|
746
777
|
services = detector.detect_services()
|
|
747
778
|
running = [s for s in services if s.status == "running"]
|
|
748
|
-
|
|
779
|
+
|
|
749
780
|
if running:
|
|
750
781
|
table = Table(title="Running Services", border_style="green")
|
|
751
782
|
table.add_column("Service")
|
|
752
783
|
table.add_column("Status")
|
|
753
784
|
table.add_column("Enabled")
|
|
754
|
-
|
|
785
|
+
|
|
755
786
|
for svc in running:
|
|
756
787
|
table.add_row(svc.name, f"[green]{svc.status}[/]", "✓" if svc.enabled else "")
|
|
757
|
-
|
|
788
|
+
|
|
758
789
|
console.print(table)
|
|
759
|
-
|
|
790
|
+
|
|
760
791
|
# Applications
|
|
761
792
|
apps = detector.detect_applications()
|
|
762
|
-
|
|
793
|
+
|
|
763
794
|
if apps:
|
|
764
795
|
console.print()
|
|
765
796
|
table = Table(title="Running Applications", border_style="blue")
|
|
@@ -767,85 +798,88 @@ def cmd_detect(args):
|
|
|
767
798
|
table.add_column("PID")
|
|
768
799
|
table.add_column("Memory")
|
|
769
800
|
table.add_column("Working Dir")
|
|
770
|
-
|
|
801
|
+
|
|
771
802
|
for app in apps[:15]:
|
|
772
803
|
table.add_row(
|
|
773
|
-
app.name,
|
|
774
|
-
str(app.pid),
|
|
804
|
+
app.name,
|
|
805
|
+
str(app.pid),
|
|
775
806
|
f"{app.memory_mb:.0f} MB",
|
|
776
|
-
app.working_dir[:40] if app.working_dir else ""
|
|
807
|
+
app.working_dir[:40] if app.working_dir else "",
|
|
777
808
|
)
|
|
778
|
-
|
|
809
|
+
|
|
779
810
|
console.print(table)
|
|
780
|
-
|
|
811
|
+
|
|
781
812
|
# Paths
|
|
782
813
|
paths = detector.detect_paths()
|
|
783
|
-
|
|
814
|
+
|
|
784
815
|
if paths:
|
|
785
816
|
console.print()
|
|
786
817
|
table = Table(title="Detected Paths", border_style="yellow")
|
|
787
818
|
table.add_column("Type")
|
|
788
819
|
table.add_column("Path")
|
|
789
820
|
table.add_column("Size")
|
|
790
|
-
|
|
821
|
+
|
|
791
822
|
for p in paths[:20]:
|
|
792
823
|
table.add_row(
|
|
793
|
-
f"[cyan]{p.type}[/]",
|
|
794
|
-
p.path,
|
|
795
|
-
f"{p.size_mb:.0f} MB" if p.size_mb > 0 else "-"
|
|
824
|
+
f"[cyan]{p.type}[/]", p.path, f"{p.size_mb:.0f} MB" if p.size_mb > 0 else "-"
|
|
796
825
|
)
|
|
797
|
-
|
|
826
|
+
|
|
798
827
|
console.print(table)
|
|
799
828
|
|
|
800
829
|
|
|
801
830
|
def main():
|
|
802
831
|
"""Main entry point."""
|
|
803
832
|
parser = argparse.ArgumentParser(
|
|
804
|
-
prog="clonebox",
|
|
805
|
-
description="Clone your workstation environment to an isolated VM"
|
|
833
|
+
prog="clonebox", description="Clone your workstation environment to an isolated VM"
|
|
806
834
|
)
|
|
807
835
|
parser.add_argument("--version", action="version", version=f"clonebox {__version__}")
|
|
808
|
-
|
|
836
|
+
|
|
809
837
|
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
810
|
-
|
|
838
|
+
|
|
811
839
|
# Interactive mode (default)
|
|
812
840
|
parser.set_defaults(func=lambda args: interactive_mode())
|
|
813
|
-
|
|
841
|
+
|
|
814
842
|
# Create command
|
|
815
843
|
create_parser = subparsers.add_parser("create", help="Create VM from config")
|
|
816
844
|
create_parser.add_argument("--name", "-n", default="clonebox-vm", help="VM name")
|
|
817
|
-
create_parser.add_argument(
|
|
818
|
-
|
|
845
|
+
create_parser.add_argument(
|
|
846
|
+
"--config",
|
|
847
|
+
"-c",
|
|
848
|
+
required=True,
|
|
849
|
+
help='JSON config: {"paths": {}, "packages": [], "services": []}',
|
|
850
|
+
)
|
|
819
851
|
create_parser.add_argument("--ram", type=int, default=4096, help="RAM in MB")
|
|
820
852
|
create_parser.add_argument("--vcpus", type=int, default=4, help="Number of vCPUs")
|
|
821
853
|
create_parser.add_argument("--base-image", help="Path to base qcow2 image")
|
|
822
854
|
create_parser.add_argument("--no-gui", action="store_true", help="Disable SPICE graphics")
|
|
823
855
|
create_parser.add_argument("--start", "-s", action="store_true", help="Start VM after creation")
|
|
824
856
|
create_parser.set_defaults(func=cmd_create)
|
|
825
|
-
|
|
857
|
+
|
|
826
858
|
# Start command
|
|
827
859
|
start_parser = subparsers.add_parser("start", help="Start a VM")
|
|
828
|
-
start_parser.add_argument(
|
|
860
|
+
start_parser.add_argument(
|
|
861
|
+
"name", nargs="?", default=None, help="VM name or '.' to use .clonebox.yaml"
|
|
862
|
+
)
|
|
829
863
|
start_parser.add_argument("--no-viewer", action="store_true", help="Don't open virt-viewer")
|
|
830
864
|
start_parser.set_defaults(func=cmd_start)
|
|
831
|
-
|
|
865
|
+
|
|
832
866
|
# Stop command
|
|
833
867
|
stop_parser = subparsers.add_parser("stop", help="Stop a VM")
|
|
834
868
|
stop_parser.add_argument("name", help="VM name")
|
|
835
869
|
stop_parser.add_argument("--force", "-f", action="store_true", help="Force stop")
|
|
836
870
|
stop_parser.set_defaults(func=cmd_stop)
|
|
837
|
-
|
|
871
|
+
|
|
838
872
|
# Delete command
|
|
839
873
|
delete_parser = subparsers.add_parser("delete", help="Delete a VM")
|
|
840
874
|
delete_parser.add_argument("name", help="VM name")
|
|
841
875
|
delete_parser.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
|
|
842
876
|
delete_parser.add_argument("--keep-storage", action="store_true", help="Keep disk images")
|
|
843
877
|
delete_parser.set_defaults(func=cmd_delete)
|
|
844
|
-
|
|
878
|
+
|
|
845
879
|
# List command
|
|
846
880
|
list_parser = subparsers.add_parser("list", aliases=["ls"], help="List VMs")
|
|
847
881
|
list_parser.set_defaults(func=cmd_list)
|
|
848
|
-
|
|
882
|
+
|
|
849
883
|
# Detect command
|
|
850
884
|
detect_parser = subparsers.add_parser("detect", help="Detect system state")
|
|
851
885
|
detect_parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
@@ -853,20 +887,38 @@ def main():
|
|
|
853
887
|
detect_parser.add_argument("--dedupe", action="store_true", help="Remove duplicate entries")
|
|
854
888
|
detect_parser.add_argument("-o", "--output", help="Save output to file")
|
|
855
889
|
detect_parser.set_defaults(func=cmd_detect)
|
|
856
|
-
|
|
890
|
+
|
|
857
891
|
# Clone command
|
|
858
892
|
clone_parser = subparsers.add_parser("clone", help="Generate clone config from path")
|
|
859
|
-
clone_parser.add_argument(
|
|
893
|
+
clone_parser.add_argument(
|
|
894
|
+
"path", nargs="?", default=".", help="Path to clone (default: current dir)"
|
|
895
|
+
)
|
|
860
896
|
clone_parser.add_argument("--name", "-n", help="VM name (default: directory name)")
|
|
861
|
-
clone_parser.add_argument(
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
clone_parser.add_argument(
|
|
865
|
-
|
|
897
|
+
clone_parser.add_argument(
|
|
898
|
+
"--run", "-r", action="store_true", help="Create and start VM immediately"
|
|
899
|
+
)
|
|
900
|
+
clone_parser.add_argument(
|
|
901
|
+
"--edit", "-e", action="store_true", help="Open config in editor before creating"
|
|
902
|
+
)
|
|
903
|
+
clone_parser.add_argument(
|
|
904
|
+
"--dedupe", action="store_true", default=True, help="Remove duplicate entries"
|
|
905
|
+
)
|
|
906
|
+
clone_parser.add_argument(
|
|
907
|
+
"--user",
|
|
908
|
+
"-u",
|
|
909
|
+
action="store_true",
|
|
910
|
+
help="Use user session (qemu:///session) - no root required, stores in ~/.local/share/libvirt/",
|
|
911
|
+
)
|
|
912
|
+
clone_parser.add_argument(
|
|
913
|
+
"--network",
|
|
914
|
+
choices=["auto", "default", "user"],
|
|
915
|
+
default="auto",
|
|
916
|
+
help="Network mode: auto (default), default (libvirt network), user (slirp)",
|
|
917
|
+
)
|
|
866
918
|
clone_parser.set_defaults(func=cmd_clone)
|
|
867
|
-
|
|
919
|
+
|
|
868
920
|
args = parser.parse_args()
|
|
869
|
-
|
|
921
|
+
|
|
870
922
|
if hasattr(args, "func"):
|
|
871
923
|
try:
|
|
872
924
|
args.func(args)
|