clonebox 1.1.12__py3-none-any.whl → 1.1.14__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/audit.py +448 -0
- clonebox/cli.py +398 -5
- clonebox/cloner.py +58 -22
- clonebox/orchestrator.py +568 -0
- clonebox/plugins/__init__.py +24 -0
- clonebox/plugins/base.py +319 -0
- clonebox/plugins/manager.py +438 -0
- {clonebox-1.1.12.dist-info → clonebox-1.1.14.dist-info}/METADATA +1 -1
- {clonebox-1.1.12.dist-info → clonebox-1.1.14.dist-info}/RECORD +13 -8
- {clonebox-1.1.12.dist-info → clonebox-1.1.14.dist-info}/WHEEL +0 -0
- {clonebox-1.1.12.dist-info → clonebox-1.1.14.dist-info}/entry_points.txt +0 -0
- {clonebox-1.1.12.dist-info → clonebox-1.1.14.dist-info}/licenses/LICENSE +0 -0
- {clonebox-1.1.12.dist-info → clonebox-1.1.14.dist-info}/top_level.txt +0 -0
clonebox/cli.py
CHANGED
|
@@ -34,6 +34,9 @@ from clonebox.monitor import ResourceMonitor, format_bytes
|
|
|
34
34
|
from clonebox.p2p import P2PManager
|
|
35
35
|
from clonebox.snapshots import SnapshotManager, SnapshotType
|
|
36
36
|
from clonebox.health import HealthCheckManager, ProbeConfig, ProbeType
|
|
37
|
+
from clonebox.audit import get_audit_logger, AuditQuery, AuditEventType, AuditOutcome
|
|
38
|
+
from clonebox.orchestrator import Orchestrator, OrchestrationResult
|
|
39
|
+
from clonebox.plugins import get_plugin_manager, PluginHook, PluginContext
|
|
37
40
|
|
|
38
41
|
# Custom questionary style
|
|
39
42
|
custom_style = Style(
|
|
@@ -458,6 +461,9 @@ def run_vm_diagnostics(
|
|
|
458
461
|
if health_status and "HEALTH_STATUS=OK" in health_status:
|
|
459
462
|
result["health"]["status"] = "ok"
|
|
460
463
|
console.print("[green]✅ Health: All checks passed[/]")
|
|
464
|
+
elif health_status and "HEALTH_STATUS=PENDING" in health_status:
|
|
465
|
+
result["health"]["status"] = "pending"
|
|
466
|
+
console.print("[yellow]⏳ Health: Setup in progress[/]")
|
|
461
467
|
elif health_status and "HEALTH_STATUS=FAILED" in health_status:
|
|
462
468
|
result["health"]["status"] = "failed"
|
|
463
469
|
console.print("[red]❌ Health: Some checks failed[/]")
|
|
@@ -1800,6 +1806,8 @@ def cmd_test(args):
|
|
|
1800
1806
|
console.print()
|
|
1801
1807
|
|
|
1802
1808
|
# Test 3: Check cloud-init status (if running)
|
|
1809
|
+
cloud_init_complete: Optional[bool] = None
|
|
1810
|
+
cloud_init_running: bool = False
|
|
1803
1811
|
if not quick and state == "running":
|
|
1804
1812
|
console.print("[bold]3. Cloud-init Status[/]")
|
|
1805
1813
|
try:
|
|
@@ -1809,16 +1817,23 @@ def cmd_test(args):
|
|
|
1809
1817
|
status = _qga_exec(vm_name, conn_uri, "cloud-init status 2>/dev/null || true", timeout=15)
|
|
1810
1818
|
if status is None:
|
|
1811
1819
|
console.print("[yellow]⚠️ Could not check cloud-init (QGA command failed)[/]")
|
|
1820
|
+
cloud_init_complete = None
|
|
1812
1821
|
elif "done" in status.lower():
|
|
1813
1822
|
console.print("[green]✅ Cloud-init completed[/]")
|
|
1823
|
+
cloud_init_complete = True
|
|
1814
1824
|
elif "running" in status.lower():
|
|
1815
1825
|
console.print("[yellow]⚠️ Cloud-init still running[/]")
|
|
1826
|
+
cloud_init_complete = False
|
|
1827
|
+
cloud_init_running = True
|
|
1816
1828
|
elif status.strip():
|
|
1817
1829
|
console.print(f"[yellow]⚠️ Cloud-init status: {status.strip()}[/]")
|
|
1830
|
+
cloud_init_complete = None
|
|
1818
1831
|
else:
|
|
1819
1832
|
console.print("[yellow]⚠️ Cloud-init status: unknown[/]")
|
|
1833
|
+
cloud_init_complete = None
|
|
1820
1834
|
except Exception:
|
|
1821
1835
|
console.print("[yellow]⚠️ Could not check cloud-init (QEMU agent may not be running)[/]")
|
|
1836
|
+
cloud_init_complete = None
|
|
1822
1837
|
|
|
1823
1838
|
console.print()
|
|
1824
1839
|
|
|
@@ -1877,17 +1892,33 @@ def cmd_test(args):
|
|
|
1877
1892
|
timeout=10,
|
|
1878
1893
|
)
|
|
1879
1894
|
if exists and exists.strip() == "yes":
|
|
1880
|
-
|
|
1895
|
+
_qga_exec(
|
|
1881
1896
|
vm_name,
|
|
1882
1897
|
conn_uri,
|
|
1883
|
-
"/usr/local/bin/clonebox-health >/dev/null 2>&1
|
|
1898
|
+
"/usr/local/bin/clonebox-health >/dev/null 2>&1 || true",
|
|
1884
1899
|
timeout=60,
|
|
1885
1900
|
)
|
|
1886
|
-
|
|
1887
|
-
|
|
1901
|
+
health_status = _qga_exec(
|
|
1902
|
+
vm_name,
|
|
1903
|
+
conn_uri,
|
|
1904
|
+
"cat /var/log/clonebox-health-status 2>/dev/null || true",
|
|
1905
|
+
timeout=10,
|
|
1906
|
+
)
|
|
1907
|
+
if health_status and "HEALTH_STATUS=OK" in health_status:
|
|
1908
|
+
console.print("[green]✅ Health check passed[/]")
|
|
1888
1909
|
console.print(" View results in VM: cat /var/log/clonebox-health.log")
|
|
1910
|
+
elif health_status and "HEALTH_STATUS=PENDING" in health_status:
|
|
1911
|
+
console.print("[yellow]⚠️ Health check pending (setup in progress)[/]")
|
|
1912
|
+
if cloud_init_running:
|
|
1913
|
+
console.print(" Cloud-init is still running; re-check after it completes")
|
|
1914
|
+
console.print(" View logs in VM: cat /var/log/clonebox-health.log")
|
|
1915
|
+
elif health_status and "HEALTH_STATUS=FAILED" in health_status:
|
|
1916
|
+
console.print("[yellow]⚠️ Health check reports failures[/]")
|
|
1917
|
+
if cloud_init_running:
|
|
1918
|
+
console.print(" Cloud-init is still running; some failures may be transient")
|
|
1919
|
+
console.print(" View logs in VM: cat /var/log/clonebox-health.log")
|
|
1889
1920
|
else:
|
|
1890
|
-
console.print("[yellow]⚠️ Health check
|
|
1921
|
+
console.print("[yellow]⚠️ Health check status not available yet[/]")
|
|
1891
1922
|
console.print(" View logs in VM: cat /var/log/clonebox-health.log")
|
|
1892
1923
|
else:
|
|
1893
1924
|
console.print("[yellow]⚠️ Health check script not found[/]")
|
|
@@ -3098,6 +3129,307 @@ def cmd_list_remote(args) -> None:
|
|
|
3098
3129
|
console.print("[yellow]No VMs found on remote host.[/]")
|
|
3099
3130
|
|
|
3100
3131
|
|
|
3132
|
+
# === Audit Commands ===
|
|
3133
|
+
|
|
3134
|
+
|
|
3135
|
+
def cmd_audit_list(args) -> None:
|
|
3136
|
+
"""List audit events."""
|
|
3137
|
+
query = AuditQuery()
|
|
3138
|
+
|
|
3139
|
+
# Build filters
|
|
3140
|
+
event_type = None
|
|
3141
|
+
if hasattr(args, "type") and args.type:
|
|
3142
|
+
try:
|
|
3143
|
+
event_type = AuditEventType(args.type)
|
|
3144
|
+
except ValueError:
|
|
3145
|
+
console.print(f"[red]Unknown event type: {args.type}[/]")
|
|
3146
|
+
return
|
|
3147
|
+
|
|
3148
|
+
outcome = None
|
|
3149
|
+
if hasattr(args, "outcome") and args.outcome:
|
|
3150
|
+
try:
|
|
3151
|
+
outcome = AuditOutcome(args.outcome)
|
|
3152
|
+
except ValueError:
|
|
3153
|
+
console.print(f"[red]Unknown outcome: {args.outcome}[/]")
|
|
3154
|
+
return
|
|
3155
|
+
|
|
3156
|
+
limit = getattr(args, "limit", 50)
|
|
3157
|
+
target = getattr(args, "target", None)
|
|
3158
|
+
|
|
3159
|
+
events = query.query(
|
|
3160
|
+
event_type=event_type,
|
|
3161
|
+
target_name=target,
|
|
3162
|
+
outcome=outcome,
|
|
3163
|
+
limit=limit,
|
|
3164
|
+
)
|
|
3165
|
+
|
|
3166
|
+
if not events:
|
|
3167
|
+
console.print("[yellow]No audit events found.[/]")
|
|
3168
|
+
return
|
|
3169
|
+
|
|
3170
|
+
if getattr(args, "json", False):
|
|
3171
|
+
console.print_json(json.dumps([e.to_dict() for e in events], default=str))
|
|
3172
|
+
return
|
|
3173
|
+
|
|
3174
|
+
table = Table(title="Audit Events", border_style="cyan")
|
|
3175
|
+
table.add_column("Time", style="dim")
|
|
3176
|
+
table.add_column("Event")
|
|
3177
|
+
table.add_column("Target")
|
|
3178
|
+
table.add_column("Outcome")
|
|
3179
|
+
table.add_column("User")
|
|
3180
|
+
|
|
3181
|
+
for event in reversed(events[-limit:]):
|
|
3182
|
+
outcome_style = {
|
|
3183
|
+
"success": "green",
|
|
3184
|
+
"failure": "red",
|
|
3185
|
+
"partial": "yellow",
|
|
3186
|
+
"denied": "red bold",
|
|
3187
|
+
"skipped": "dim",
|
|
3188
|
+
}.get(event.outcome.value, "white")
|
|
3189
|
+
|
|
3190
|
+
target_str = event.target_name or "-"
|
|
3191
|
+
table.add_row(
|
|
3192
|
+
event.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
|
|
3193
|
+
event.event_type.value,
|
|
3194
|
+
target_str,
|
|
3195
|
+
f"[{outcome_style}]{event.outcome.value}[/]",
|
|
3196
|
+
event.user,
|
|
3197
|
+
)
|
|
3198
|
+
|
|
3199
|
+
console.print(table)
|
|
3200
|
+
|
|
3201
|
+
|
|
3202
|
+
def cmd_audit_show(args) -> None:
|
|
3203
|
+
"""Show audit event details."""
|
|
3204
|
+
query = AuditQuery()
|
|
3205
|
+
events = query.query(limit=1000)
|
|
3206
|
+
|
|
3207
|
+
for event in events:
|
|
3208
|
+
if event.event_id == args.event_id:
|
|
3209
|
+
console.print_json(json.dumps(event.to_dict(), indent=2, default=str))
|
|
3210
|
+
return
|
|
3211
|
+
|
|
3212
|
+
console.print(f"[red]Event not found: {args.event_id}[/]")
|
|
3213
|
+
|
|
3214
|
+
|
|
3215
|
+
def cmd_audit_failures(args) -> None:
|
|
3216
|
+
"""Show recent failures."""
|
|
3217
|
+
query = AuditQuery()
|
|
3218
|
+
events = query.get_failures(limit=getattr(args, "limit", 20))
|
|
3219
|
+
|
|
3220
|
+
if not events:
|
|
3221
|
+
console.print("[green]No failures recorded.[/]")
|
|
3222
|
+
return
|
|
3223
|
+
|
|
3224
|
+
table = Table(title="Recent Failures", border_style="red")
|
|
3225
|
+
table.add_column("Time", style="dim")
|
|
3226
|
+
table.add_column("Event")
|
|
3227
|
+
table.add_column("Target")
|
|
3228
|
+
table.add_column("Error")
|
|
3229
|
+
|
|
3230
|
+
for event in reversed(events):
|
|
3231
|
+
table.add_row(
|
|
3232
|
+
event.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
|
|
3233
|
+
event.event_type.value,
|
|
3234
|
+
event.target_name or "-",
|
|
3235
|
+
(event.error_message or "-")[:50],
|
|
3236
|
+
)
|
|
3237
|
+
|
|
3238
|
+
console.print(table)
|
|
3239
|
+
|
|
3240
|
+
|
|
3241
|
+
# === Orchestration Commands ===
|
|
3242
|
+
|
|
3243
|
+
|
|
3244
|
+
def cmd_compose_up(args) -> None:
|
|
3245
|
+
"""Start VMs from compose file."""
|
|
3246
|
+
compose_file = Path(args.file) if hasattr(args, "file") and args.file else Path("clonebox-compose.yaml")
|
|
3247
|
+
|
|
3248
|
+
if not compose_file.exists():
|
|
3249
|
+
console.print(f"[red]Compose file not found: {compose_file}[/]")
|
|
3250
|
+
return
|
|
3251
|
+
|
|
3252
|
+
user_session = getattr(args, "user", False)
|
|
3253
|
+
services = args.services if hasattr(args, "services") and args.services else None
|
|
3254
|
+
|
|
3255
|
+
console.print(f"[cyan]🚀 Starting VMs from: {compose_file}[/]")
|
|
3256
|
+
|
|
3257
|
+
try:
|
|
3258
|
+
orch = Orchestrator.from_file(compose_file, user_session=user_session)
|
|
3259
|
+
result = orch.up(services=services, console=console)
|
|
3260
|
+
|
|
3261
|
+
if result.success:
|
|
3262
|
+
console.print("[green]✅ All VMs started successfully[/]")
|
|
3263
|
+
else:
|
|
3264
|
+
console.print("[yellow]⚠️ Some VMs failed to start:[/]")
|
|
3265
|
+
for name, error in result.errors.items():
|
|
3266
|
+
console.print(f" [red]{name}:[/] {error}")
|
|
3267
|
+
|
|
3268
|
+
console.print(f"[dim]Duration: {result.duration_seconds:.1f}s[/]")
|
|
3269
|
+
|
|
3270
|
+
except Exception as e:
|
|
3271
|
+
console.print(f"[red]❌ Orchestration failed: {e}[/]")
|
|
3272
|
+
|
|
3273
|
+
|
|
3274
|
+
def cmd_compose_down(args) -> None:
|
|
3275
|
+
"""Stop VMs from compose file."""
|
|
3276
|
+
compose_file = Path(args.file) if hasattr(args, "file") and args.file else Path("clonebox-compose.yaml")
|
|
3277
|
+
|
|
3278
|
+
if not compose_file.exists():
|
|
3279
|
+
console.print(f"[red]Compose file not found: {compose_file}[/]")
|
|
3280
|
+
return
|
|
3281
|
+
|
|
3282
|
+
user_session = getattr(args, "user", False)
|
|
3283
|
+
services = args.services if hasattr(args, "services") and args.services else None
|
|
3284
|
+
force = getattr(args, "force", False)
|
|
3285
|
+
|
|
3286
|
+
console.print(f"[cyan]🛑 Stopping VMs from: {compose_file}[/]")
|
|
3287
|
+
|
|
3288
|
+
try:
|
|
3289
|
+
orch = Orchestrator.from_file(compose_file, user_session=user_session)
|
|
3290
|
+
result = orch.down(services=services, force=force, console=console)
|
|
3291
|
+
|
|
3292
|
+
if result.success:
|
|
3293
|
+
console.print("[green]✅ All VMs stopped successfully[/]")
|
|
3294
|
+
else:
|
|
3295
|
+
console.print("[yellow]⚠️ Some VMs failed to stop:[/]")
|
|
3296
|
+
for name, error in result.errors.items():
|
|
3297
|
+
console.print(f" [red]{name}:[/] {error}")
|
|
3298
|
+
|
|
3299
|
+
except Exception as e:
|
|
3300
|
+
console.print(f"[red]❌ Stop failed: {e}[/]")
|
|
3301
|
+
|
|
3302
|
+
|
|
3303
|
+
def cmd_compose_status(args) -> None:
|
|
3304
|
+
"""Show status of VMs from compose file."""
|
|
3305
|
+
compose_file = Path(args.file) if hasattr(args, "file") and args.file else Path("clonebox-compose.yaml")
|
|
3306
|
+
|
|
3307
|
+
if not compose_file.exists():
|
|
3308
|
+
console.print(f"[red]Compose file not found: {compose_file}[/]")
|
|
3309
|
+
return
|
|
3310
|
+
|
|
3311
|
+
user_session = getattr(args, "user", False)
|
|
3312
|
+
|
|
3313
|
+
try:
|
|
3314
|
+
orch = Orchestrator.from_file(compose_file, user_session=user_session)
|
|
3315
|
+
status = orch.status()
|
|
3316
|
+
|
|
3317
|
+
if getattr(args, "json", False):
|
|
3318
|
+
console.print_json(json.dumps(status, default=str))
|
|
3319
|
+
return
|
|
3320
|
+
|
|
3321
|
+
table = Table(title=f"Compose Status: {compose_file.name}", border_style="cyan")
|
|
3322
|
+
table.add_column("VM")
|
|
3323
|
+
table.add_column("State")
|
|
3324
|
+
table.add_column("Actual")
|
|
3325
|
+
table.add_column("Health")
|
|
3326
|
+
table.add_column("Depends On")
|
|
3327
|
+
|
|
3328
|
+
for name, info in status.items():
|
|
3329
|
+
state = info["orchestration_state"]
|
|
3330
|
+
actual = info["actual_state"]
|
|
3331
|
+
health = "✅" if info["health_check_passed"] else "⏳"
|
|
3332
|
+
deps = ", ".join(info["depends_on"]) or "-"
|
|
3333
|
+
|
|
3334
|
+
state_style = {
|
|
3335
|
+
"running": "green",
|
|
3336
|
+
"healthy": "green bold",
|
|
3337
|
+
"stopped": "dim",
|
|
3338
|
+
"failed": "red",
|
|
3339
|
+
"pending": "yellow",
|
|
3340
|
+
}.get(state, "white")
|
|
3341
|
+
|
|
3342
|
+
table.add_row(
|
|
3343
|
+
name,
|
|
3344
|
+
f"[{state_style}]{state}[/]",
|
|
3345
|
+
actual,
|
|
3346
|
+
health,
|
|
3347
|
+
deps,
|
|
3348
|
+
)
|
|
3349
|
+
|
|
3350
|
+
console.print(table)
|
|
3351
|
+
|
|
3352
|
+
except Exception as e:
|
|
3353
|
+
console.print(f"[red]❌ Failed to get status: {e}[/]")
|
|
3354
|
+
|
|
3355
|
+
|
|
3356
|
+
# === Plugin Commands ===
|
|
3357
|
+
|
|
3358
|
+
|
|
3359
|
+
def cmd_plugin_list(args) -> None:
|
|
3360
|
+
"""List installed plugins."""
|
|
3361
|
+
manager = get_plugin_manager()
|
|
3362
|
+
|
|
3363
|
+
# Load plugins if not already loaded
|
|
3364
|
+
if not manager.list_plugins():
|
|
3365
|
+
manager.load_all()
|
|
3366
|
+
|
|
3367
|
+
plugins = manager.list_plugins()
|
|
3368
|
+
|
|
3369
|
+
if not plugins:
|
|
3370
|
+
console.print("[yellow]No plugins installed.[/]")
|
|
3371
|
+
console.print("[dim]Plugin directories:[/]")
|
|
3372
|
+
for d in manager.plugin_dirs:
|
|
3373
|
+
console.print(f" {d}")
|
|
3374
|
+
return
|
|
3375
|
+
|
|
3376
|
+
table = Table(title="Installed Plugins", border_style="cyan")
|
|
3377
|
+
table.add_column("Name")
|
|
3378
|
+
table.add_column("Version")
|
|
3379
|
+
table.add_column("Enabled")
|
|
3380
|
+
table.add_column("Description")
|
|
3381
|
+
|
|
3382
|
+
for plugin in plugins:
|
|
3383
|
+
enabled = "[green]✅[/]" if plugin["enabled"] else "[red]❌[/]"
|
|
3384
|
+
table.add_row(
|
|
3385
|
+
plugin["name"],
|
|
3386
|
+
plugin["version"],
|
|
3387
|
+
enabled,
|
|
3388
|
+
(plugin.get("description", "") or "")[:40],
|
|
3389
|
+
)
|
|
3390
|
+
|
|
3391
|
+
console.print(table)
|
|
3392
|
+
|
|
3393
|
+
|
|
3394
|
+
def cmd_plugin_enable(args) -> None:
|
|
3395
|
+
"""Enable a plugin."""
|
|
3396
|
+
manager = get_plugin_manager()
|
|
3397
|
+
manager.load_all()
|
|
3398
|
+
|
|
3399
|
+
if manager.enable(args.name):
|
|
3400
|
+
console.print(f"[green]✅ Plugin '{args.name}' enabled[/]")
|
|
3401
|
+
else:
|
|
3402
|
+
console.print(f"[red]Plugin '{args.name}' not found[/]")
|
|
3403
|
+
|
|
3404
|
+
|
|
3405
|
+
def cmd_plugin_disable(args) -> None:
|
|
3406
|
+
"""Disable a plugin."""
|
|
3407
|
+
manager = get_plugin_manager()
|
|
3408
|
+
manager.load_all()
|
|
3409
|
+
|
|
3410
|
+
if manager.disable(args.name):
|
|
3411
|
+
console.print(f"[yellow]⚠️ Plugin '{args.name}' disabled[/]")
|
|
3412
|
+
else:
|
|
3413
|
+
console.print(f"[red]Plugin '{args.name}' not found[/]")
|
|
3414
|
+
|
|
3415
|
+
|
|
3416
|
+
def cmd_plugin_discover(args) -> None:
|
|
3417
|
+
"""Discover available plugins."""
|
|
3418
|
+
manager = get_plugin_manager()
|
|
3419
|
+
discovered = manager.discover()
|
|
3420
|
+
|
|
3421
|
+
if not discovered:
|
|
3422
|
+
console.print("[yellow]No plugins discovered.[/]")
|
|
3423
|
+
console.print("[dim]Plugin directories:[/]")
|
|
3424
|
+
for d in manager.plugin_dirs:
|
|
3425
|
+
console.print(f" {d}")
|
|
3426
|
+
return
|
|
3427
|
+
|
|
3428
|
+
console.print("[bold]Discovered plugins:[/]")
|
|
3429
|
+
for name in discovered:
|
|
3430
|
+
console.print(f" • {name}")
|
|
3431
|
+
|
|
3432
|
+
|
|
3101
3433
|
def main():
|
|
3102
3434
|
"""Main entry point."""
|
|
3103
3435
|
parser = argparse.ArgumentParser(
|
|
@@ -3684,6 +4016,67 @@ def main():
|
|
|
3684
4016
|
list_remote_parser.add_argument("host", help="Remote host (user@hostname)")
|
|
3685
4017
|
list_remote_parser.set_defaults(func=cmd_list_remote)
|
|
3686
4018
|
|
|
4019
|
+
# === Audit Commands ===
|
|
4020
|
+
audit_parser = subparsers.add_parser("audit", help="View audit logs")
|
|
4021
|
+
audit_sub = audit_parser.add_subparsers(dest="audit_command", help="Audit commands")
|
|
4022
|
+
|
|
4023
|
+
audit_list = audit_sub.add_parser("list", aliases=["ls"], help="List audit events")
|
|
4024
|
+
audit_list.add_argument("--type", "-t", help="Filter by event type (e.g., vm.create)")
|
|
4025
|
+
audit_list.add_argument("--target", help="Filter by target name")
|
|
4026
|
+
audit_list.add_argument("--outcome", "-o", choices=["success", "failure", "partial"], help="Filter by outcome")
|
|
4027
|
+
audit_list.add_argument("--limit", "-n", type=int, default=50, help="Max events to show")
|
|
4028
|
+
audit_list.add_argument("--json", action="store_true", help="Output as JSON")
|
|
4029
|
+
audit_list.set_defaults(func=cmd_audit_list)
|
|
4030
|
+
|
|
4031
|
+
audit_show = audit_sub.add_parser("show", help="Show audit event details")
|
|
4032
|
+
audit_show.add_argument("event_id", help="Event ID to show")
|
|
4033
|
+
audit_show.set_defaults(func=cmd_audit_show)
|
|
4034
|
+
|
|
4035
|
+
audit_failures = audit_sub.add_parser("failures", help="Show recent failures")
|
|
4036
|
+
audit_failures.add_argument("--limit", "-n", type=int, default=20, help="Max events to show")
|
|
4037
|
+
audit_failures.set_defaults(func=cmd_audit_failures)
|
|
4038
|
+
|
|
4039
|
+
# === Compose/Orchestration Commands ===
|
|
4040
|
+
compose_parser = subparsers.add_parser("compose", help="Multi-VM orchestration")
|
|
4041
|
+
compose_sub = compose_parser.add_subparsers(dest="compose_command", help="Compose commands")
|
|
4042
|
+
|
|
4043
|
+
compose_up = compose_sub.add_parser("up", help="Start VMs from compose file")
|
|
4044
|
+
compose_up.add_argument("-f", "--file", default="clonebox-compose.yaml", help="Compose file")
|
|
4045
|
+
compose_up.add_argument("-u", "--user", action="store_true", help="Use user session")
|
|
4046
|
+
compose_up.add_argument("services", nargs="*", help="Specific services to start")
|
|
4047
|
+
compose_up.set_defaults(func=cmd_compose_up)
|
|
4048
|
+
|
|
4049
|
+
compose_down = compose_sub.add_parser("down", help="Stop VMs from compose file")
|
|
4050
|
+
compose_down.add_argument("-f", "--file", default="clonebox-compose.yaml", help="Compose file")
|
|
4051
|
+
compose_down.add_argument("-u", "--user", action="store_true", help="Use user session")
|
|
4052
|
+
compose_down.add_argument("--force", action="store_true", help="Force stop")
|
|
4053
|
+
compose_down.add_argument("services", nargs="*", help="Specific services to stop")
|
|
4054
|
+
compose_down.set_defaults(func=cmd_compose_down)
|
|
4055
|
+
|
|
4056
|
+
compose_status = compose_sub.add_parser("status", aliases=["ps"], help="Show compose status")
|
|
4057
|
+
compose_status.add_argument("-f", "--file", default="clonebox-compose.yaml", help="Compose file")
|
|
4058
|
+
compose_status.add_argument("-u", "--user", action="store_true", help="Use user session")
|
|
4059
|
+
compose_status.add_argument("--json", action="store_true", help="Output as JSON")
|
|
4060
|
+
compose_status.set_defaults(func=cmd_compose_status)
|
|
4061
|
+
|
|
4062
|
+
# === Plugin Commands ===
|
|
4063
|
+
plugin_parser = subparsers.add_parser("plugin", help="Manage plugins")
|
|
4064
|
+
plugin_sub = plugin_parser.add_subparsers(dest="plugin_command", help="Plugin commands")
|
|
4065
|
+
|
|
4066
|
+
plugin_list = plugin_sub.add_parser("list", aliases=["ls"], help="List plugins")
|
|
4067
|
+
plugin_list.set_defaults(func=cmd_plugin_list)
|
|
4068
|
+
|
|
4069
|
+
plugin_enable = plugin_sub.add_parser("enable", help="Enable a plugin")
|
|
4070
|
+
plugin_enable.add_argument("name", help="Plugin name")
|
|
4071
|
+
plugin_enable.set_defaults(func=cmd_plugin_enable)
|
|
4072
|
+
|
|
4073
|
+
plugin_disable = plugin_sub.add_parser("disable", help="Disable a plugin")
|
|
4074
|
+
plugin_disable.add_argument("name", help="Plugin name")
|
|
4075
|
+
plugin_disable.set_defaults(func=cmd_plugin_disable)
|
|
4076
|
+
|
|
4077
|
+
plugin_discover = plugin_sub.add_parser("discover", help="Discover available plugins")
|
|
4078
|
+
plugin_discover.set_defaults(func=cmd_plugin_discover)
|
|
4079
|
+
|
|
3687
4080
|
args = parser.parse_args()
|
|
3688
4081
|
|
|
3689
4082
|
if hasattr(args, "func"):
|
clonebox/cloner.py
CHANGED
|
@@ -263,13 +263,21 @@ class SelectiveVMCloner:
|
|
|
263
263
|
|
|
264
264
|
return cached_path
|
|
265
265
|
|
|
266
|
-
def
|
|
267
|
-
"""Check if libvirt default network is active."""
|
|
266
|
+
def _default_network_state(self) -> str:
|
|
268
267
|
try:
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
active = self.conn.listNetworks() or []
|
|
269
|
+
if "default" in active:
|
|
270
|
+
return "active"
|
|
271
|
+
defined = self.conn.listDefinedNetworks() or []
|
|
272
|
+
if "default" in defined:
|
|
273
|
+
return "inactive"
|
|
274
|
+
return "missing"
|
|
271
275
|
except Exception:
|
|
272
|
-
return
|
|
276
|
+
return "unknown"
|
|
277
|
+
|
|
278
|
+
def _default_network_active(self) -> bool:
|
|
279
|
+
"""Check if libvirt default network is active."""
|
|
280
|
+
return self._default_network_state() == "active"
|
|
273
281
|
|
|
274
282
|
def resolve_network_mode(self, config: VMConfig) -> str:
|
|
275
283
|
"""Resolve network mode based on config and session type."""
|
|
@@ -310,10 +318,9 @@ class SelectiveVMCloner:
|
|
|
310
318
|
)
|
|
311
319
|
|
|
312
320
|
# Check default network
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
except libvirt.libvirtError:
|
|
321
|
+
default_net_state = self._default_network_state()
|
|
322
|
+
checks["default_network"] = default_net_state == "active"
|
|
323
|
+
if default_net_state in {"inactive", "missing", "unknown"}:
|
|
317
324
|
checks["network_error"] = (
|
|
318
325
|
"Default network not found or inactive.\n"
|
|
319
326
|
" For user session, CloneBox can use user-mode networking (slirp) automatically.\n"
|
|
@@ -1088,6 +1095,10 @@ REPORT_FILE="/var/log/clonebox-health.log"
|
|
|
1088
1095
|
PASSED=0
|
|
1089
1096
|
FAILED=0
|
|
1090
1097
|
WARNINGS=0
|
|
1098
|
+
SETUP_IN_PROGRESS=0
|
|
1099
|
+
if [ ! -f /var/lib/cloud/instance/boot-finished ]; then
|
|
1100
|
+
SETUP_IN_PROGRESS=1
|
|
1101
|
+
fi
|
|
1091
1102
|
|
|
1092
1103
|
# Colors for output
|
|
1093
1104
|
RED='\\033[0;31m'
|
|
@@ -1106,22 +1117,36 @@ check_apt_package() {{
|
|
|
1106
1117
|
((PASSED++))
|
|
1107
1118
|
return 0
|
|
1108
1119
|
else
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1120
|
+
if [ $SETUP_IN_PROGRESS -eq 1 ]; then
|
|
1121
|
+
log "[WARN] APT package '$pkg' is not installed yet"
|
|
1122
|
+
((WARNINGS++))
|
|
1123
|
+
return 1
|
|
1124
|
+
else
|
|
1125
|
+
log "[FAIL] APT package '$pkg' is NOT installed"
|
|
1126
|
+
((FAILED++))
|
|
1127
|
+
return 1
|
|
1128
|
+
fi
|
|
1112
1129
|
fi
|
|
1113
1130
|
}}
|
|
1114
1131
|
|
|
1115
1132
|
check_snap_package() {{
|
|
1116
1133
|
local pkg="$1"
|
|
1117
|
-
|
|
1134
|
+
local out
|
|
1135
|
+
out=$(snap list "$pkg" 2>&1)
|
|
1136
|
+
if [ $? -eq 0 ]; then
|
|
1118
1137
|
log "[PASS] Snap package '$pkg' is installed"
|
|
1119
1138
|
((PASSED++))
|
|
1120
1139
|
return 0
|
|
1121
1140
|
else
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1141
|
+
if [ $SETUP_IN_PROGRESS -eq 1 ]; then
|
|
1142
|
+
log "[WARN] Snap package '$pkg' is not installed yet"
|
|
1143
|
+
((WARNINGS++))
|
|
1144
|
+
return 1
|
|
1145
|
+
else
|
|
1146
|
+
log "[FAIL] Snap package '$pkg' is NOT installed"
|
|
1147
|
+
((FAILED++))
|
|
1148
|
+
return 1
|
|
1149
|
+
fi
|
|
1125
1150
|
fi
|
|
1126
1151
|
}}
|
|
1127
1152
|
|
|
@@ -1214,13 +1239,23 @@ log "Warnings: $WARNINGS"
|
|
|
1214
1239
|
if [ $FAILED -eq 0 ]; then
|
|
1215
1240
|
log ""
|
|
1216
1241
|
log "[SUCCESS] All critical checks passed!"
|
|
1217
|
-
|
|
1218
|
-
|
|
1242
|
+
if [ $SETUP_IN_PROGRESS -eq 1 ]; then
|
|
1243
|
+
echo "HEALTH_STATUS=PENDING" > /var/log/clonebox-health-status
|
|
1244
|
+
exit 0
|
|
1245
|
+
else
|
|
1246
|
+
echo "HEALTH_STATUS=OK" > /var/log/clonebox-health-status
|
|
1247
|
+
exit 0
|
|
1248
|
+
fi
|
|
1219
1249
|
else
|
|
1220
1250
|
log ""
|
|
1221
1251
|
log "[ERROR] Some checks failed. Review log for details."
|
|
1222
|
-
|
|
1223
|
-
|
|
1252
|
+
if [ $SETUP_IN_PROGRESS -eq 1 ]; then
|
|
1253
|
+
echo "HEALTH_STATUS=PENDING" > /var/log/clonebox-health-status
|
|
1254
|
+
exit 0
|
|
1255
|
+
else
|
|
1256
|
+
echo "HEALTH_STATUS=FAILED" > /var/log/clonebox-health-status
|
|
1257
|
+
exit 1
|
|
1258
|
+
fi
|
|
1224
1259
|
fi
|
|
1225
1260
|
"""
|
|
1226
1261
|
# Encode script to base64 for safe embedding in cloud-init
|
|
@@ -1404,7 +1439,8 @@ fi
|
|
|
1404
1439
|
" - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
|
|
1405
1440
|
" - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
|
|
1406
1441
|
" - systemctl set-default graphical.target",
|
|
1407
|
-
" - systemctl enable gdm3 || systemctl enable gdm || true",
|
|
1442
|
+
" - systemctl enable --now gdm3 || systemctl enable --now gdm || true",
|
|
1443
|
+
" - systemctl start display-manager || true",
|
|
1408
1444
|
]
|
|
1409
1445
|
)
|
|
1410
1446
|
|
|
@@ -1503,7 +1539,7 @@ Comment=CloneBox autostart
|
|
|
1503
1539
|
)
|
|
1504
1540
|
runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-health")
|
|
1505
1541
|
runcmd_lines.append(
|
|
1506
|
-
" - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1"
|
|
1542
|
+
" - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1 || true"
|
|
1507
1543
|
)
|
|
1508
1544
|
runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
|
|
1509
1545
|
|