clonebox 1.1.13__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 +40 -12
- 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.13.dist-info → clonebox-1.1.14.dist-info}/METADATA +1 -1
- {clonebox-1.1.13.dist-info → clonebox-1.1.14.dist-info}/RECORD +13 -8
- {clonebox-1.1.13.dist-info → clonebox-1.1.14.dist-info}/WHEEL +0 -0
- {clonebox-1.1.13.dist-info → clonebox-1.1.14.dist-info}/entry_points.txt +0 -0
- {clonebox-1.1.13.dist-info → clonebox-1.1.14.dist-info}/licenses/LICENSE +0 -0
- {clonebox-1.1.13.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
|
@@ -1095,6 +1095,10 @@ REPORT_FILE="/var/log/clonebox-health.log"
|
|
|
1095
1095
|
PASSED=0
|
|
1096
1096
|
FAILED=0
|
|
1097
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
|
|
1098
1102
|
|
|
1099
1103
|
# Colors for output
|
|
1100
1104
|
RED='\\033[0;31m'
|
|
@@ -1113,22 +1117,36 @@ check_apt_package() {{
|
|
|
1113
1117
|
((PASSED++))
|
|
1114
1118
|
return 0
|
|
1115
1119
|
else
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
|
1119
1129
|
fi
|
|
1120
1130
|
}}
|
|
1121
1131
|
|
|
1122
1132
|
check_snap_package() {{
|
|
1123
1133
|
local pkg="$1"
|
|
1124
|
-
|
|
1134
|
+
local out
|
|
1135
|
+
out=$(snap list "$pkg" 2>&1)
|
|
1136
|
+
if [ $? -eq 0 ]; then
|
|
1125
1137
|
log "[PASS] Snap package '$pkg' is installed"
|
|
1126
1138
|
((PASSED++))
|
|
1127
1139
|
return 0
|
|
1128
1140
|
else
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
|
1132
1150
|
fi
|
|
1133
1151
|
}}
|
|
1134
1152
|
|
|
@@ -1221,13 +1239,23 @@ log "Warnings: $WARNINGS"
|
|
|
1221
1239
|
if [ $FAILED -eq 0 ]; then
|
|
1222
1240
|
log ""
|
|
1223
1241
|
log "[SUCCESS] All critical checks passed!"
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
|
1226
1249
|
else
|
|
1227
1250
|
log ""
|
|
1228
1251
|
log "[ERROR] Some checks failed. Review log for details."
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
|
1231
1259
|
fi
|
|
1232
1260
|
"""
|
|
1233
1261
|
# Encode script to base64 for safe embedding in cloud-init
|
|
@@ -1511,7 +1539,7 @@ Comment=CloneBox autostart
|
|
|
1511
1539
|
)
|
|
1512
1540
|
runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-health")
|
|
1513
1541
|
runcmd_lines.append(
|
|
1514
|
-
" - /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"
|
|
1515
1543
|
)
|
|
1516
1544
|
runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
|
|
1517
1545
|
|