clonebox 0.1.25__py3-none-any.whl → 0.1.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clonebox/cli.py +391 -230
- clonebox/cloner.py +335 -206
- clonebox/dashboard.py +4 -4
- clonebox/detector.py +19 -31
- clonebox/models.py +19 -2
- clonebox/profiles.py +1 -5
- clonebox/validator.py +275 -145
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/METADATA +1 -1
- clonebox-0.1.27.dist-info/RECORD +17 -0
- clonebox-0.1.25.dist-info/RECORD +0 -17
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/WHEEL +0 -0
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/entry_points.txt +0 -0
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/licenses/LICENSE +0 -0
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/top_level.txt +0 -0
clonebox/validator.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
VM validation module - validates VM state against YAML configuration.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import subprocess
|
|
5
6
|
import json
|
|
6
7
|
import base64
|
|
@@ -14,7 +15,7 @@ from rich.table import Table
|
|
|
14
15
|
|
|
15
16
|
class VMValidator:
|
|
16
17
|
"""Validates VM configuration against expected state from YAML."""
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
def __init__(
|
|
19
20
|
self,
|
|
20
21
|
config: dict,
|
|
@@ -37,100 +38,116 @@ class VMValidator:
|
|
|
37
38
|
"services": {"passed": 0, "failed": 0, "total": 0, "details": []},
|
|
38
39
|
"apps": {"passed": 0, "failed": 0, "total": 0, "details": []},
|
|
39
40
|
"smoke": {"passed": 0, "failed": 0, "total": 0, "details": []},
|
|
40
|
-
"overall": "unknown"
|
|
41
|
+
"overall": "unknown",
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
|
|
43
44
|
def _exec_in_vm(self, command: str, timeout: int = 10) -> Optional[str]:
|
|
44
45
|
"""Execute command in VM using QEMU guest agent."""
|
|
45
46
|
try:
|
|
46
47
|
# Execute command
|
|
47
48
|
result = subprocess.run(
|
|
48
|
-
[
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
[
|
|
50
|
+
"virsh",
|
|
51
|
+
"--connect",
|
|
52
|
+
self.conn_uri,
|
|
53
|
+
"qemu-agent-command",
|
|
54
|
+
self.vm_name,
|
|
55
|
+
f'{{"execute":"guest-exec","arguments":{{"path":"/bin/sh","arg":["-c","{command}"],"capture-output":true}}}}',
|
|
56
|
+
],
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
timeout=timeout,
|
|
51
60
|
)
|
|
52
|
-
|
|
61
|
+
|
|
53
62
|
if result.returncode != 0:
|
|
54
63
|
return None
|
|
55
|
-
|
|
64
|
+
|
|
56
65
|
response = json.loads(result.stdout)
|
|
57
66
|
if "return" not in response or "pid" not in response["return"]:
|
|
58
67
|
return None
|
|
59
|
-
|
|
68
|
+
|
|
60
69
|
pid = response["return"]["pid"]
|
|
61
|
-
|
|
70
|
+
|
|
62
71
|
# Wait a bit for command to complete
|
|
63
72
|
time.sleep(0.3)
|
|
64
|
-
|
|
73
|
+
|
|
65
74
|
# Get result
|
|
66
75
|
status_result = subprocess.run(
|
|
67
|
-
[
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
[
|
|
77
|
+
"virsh",
|
|
78
|
+
"--connect",
|
|
79
|
+
self.conn_uri,
|
|
80
|
+
"qemu-agent-command",
|
|
81
|
+
self.vm_name,
|
|
82
|
+
f'{{"execute":"guest-exec-status","arguments":{{"pid":{pid}}}}}',
|
|
83
|
+
],
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
timeout=5,
|
|
70
87
|
)
|
|
71
|
-
|
|
88
|
+
|
|
72
89
|
if status_result.returncode != 0:
|
|
73
90
|
return None
|
|
74
|
-
|
|
91
|
+
|
|
75
92
|
status_resp = json.loads(status_result.stdout)
|
|
76
93
|
if "return" not in status_resp:
|
|
77
94
|
return None
|
|
78
|
-
|
|
95
|
+
|
|
79
96
|
ret = status_resp["return"]
|
|
80
97
|
if not ret.get("exited", False):
|
|
81
98
|
return None
|
|
82
|
-
|
|
99
|
+
|
|
83
100
|
if "out-data" in ret:
|
|
84
101
|
return base64.b64decode(ret["out-data"]).decode().strip()
|
|
85
|
-
|
|
102
|
+
|
|
86
103
|
return ""
|
|
87
|
-
|
|
104
|
+
|
|
88
105
|
except Exception:
|
|
89
106
|
return None
|
|
90
|
-
|
|
107
|
+
|
|
91
108
|
def validate_mounts(self) -> Dict:
|
|
92
109
|
"""Validate all mount points are accessible and contain data."""
|
|
93
110
|
self.console.print("\n[bold]💾 Validating Mount Points...[/]")
|
|
94
|
-
|
|
111
|
+
|
|
95
112
|
all_paths = self.config.get("paths", {}).copy()
|
|
96
113
|
all_paths.update(self.config.get("app_data_paths", {}))
|
|
97
|
-
|
|
114
|
+
|
|
98
115
|
if not all_paths:
|
|
99
116
|
self.console.print("[dim]No mount points configured[/]")
|
|
100
117
|
return self.results["mounts"]
|
|
101
|
-
|
|
118
|
+
|
|
102
119
|
# Get mounted filesystems
|
|
103
120
|
mount_output = self._exec_in_vm("mount | grep 9p")
|
|
104
121
|
mounted_paths = []
|
|
105
122
|
if mount_output:
|
|
106
|
-
mounted_paths = [line.split()[2] for line in mount_output.split(
|
|
107
|
-
|
|
123
|
+
mounted_paths = [line.split()[2] for line in mount_output.split("\n") if line.strip()]
|
|
124
|
+
|
|
108
125
|
mount_table = Table(title="Mount Validation", border_style="cyan")
|
|
109
126
|
mount_table.add_column("Guest Path", style="bold")
|
|
110
127
|
mount_table.add_column("Mounted", justify="center")
|
|
111
128
|
mount_table.add_column("Accessible", justify="center")
|
|
112
129
|
mount_table.add_column("Files", justify="right")
|
|
113
|
-
|
|
130
|
+
|
|
114
131
|
for host_path, guest_path in all_paths.items():
|
|
115
132
|
self.results["mounts"]["total"] += 1
|
|
116
|
-
|
|
133
|
+
|
|
117
134
|
# Check if mounted
|
|
118
135
|
is_mounted = any(guest_path in mp for mp in mounted_paths)
|
|
119
|
-
|
|
136
|
+
|
|
120
137
|
# Check if accessible
|
|
121
138
|
accessible = False
|
|
122
139
|
file_count = "?"
|
|
123
|
-
|
|
140
|
+
|
|
124
141
|
if is_mounted:
|
|
125
142
|
test_result = self._exec_in_vm(f"test -d {guest_path} && echo 'yes' || echo 'no'")
|
|
126
143
|
accessible = test_result == "yes"
|
|
127
|
-
|
|
144
|
+
|
|
128
145
|
if accessible:
|
|
129
146
|
# Get file count
|
|
130
147
|
count_str = self._exec_in_vm(f"ls -A {guest_path} 2>/dev/null | wc -l")
|
|
131
148
|
if count_str and count_str.isdigit():
|
|
132
149
|
file_count = count_str
|
|
133
|
-
|
|
150
|
+
|
|
134
151
|
# Determine status
|
|
135
152
|
if is_mounted and accessible:
|
|
136
153
|
mount_status = "[green]✅[/]"
|
|
@@ -147,122 +164,137 @@ class VMValidator:
|
|
|
147
164
|
access_status = "[dim]N/A[/]"
|
|
148
165
|
self.results["mounts"]["failed"] += 1
|
|
149
166
|
status = "not_mounted"
|
|
150
|
-
|
|
167
|
+
|
|
151
168
|
mount_table.add_row(guest_path, mount_status, access_status, str(file_count))
|
|
152
|
-
|
|
153
|
-
self.results["mounts"]["details"].append(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
|
|
170
|
+
self.results["mounts"]["details"].append(
|
|
171
|
+
{
|
|
172
|
+
"path": guest_path,
|
|
173
|
+
"mounted": is_mounted,
|
|
174
|
+
"accessible": accessible,
|
|
175
|
+
"files": file_count,
|
|
176
|
+
"status": status,
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
|
|
161
180
|
self.console.print(mount_table)
|
|
162
|
-
self.console.print(
|
|
163
|
-
|
|
181
|
+
self.console.print(
|
|
182
|
+
f"[dim]{self.results['mounts']['passed']}/{self.results['mounts']['total']} mounts working[/]"
|
|
183
|
+
)
|
|
184
|
+
|
|
164
185
|
return self.results["mounts"]
|
|
165
|
-
|
|
186
|
+
|
|
166
187
|
def validate_packages(self) -> Dict:
|
|
167
188
|
"""Validate APT packages are installed."""
|
|
168
189
|
self.console.print("\n[bold]📦 Validating APT Packages...[/]")
|
|
169
|
-
|
|
190
|
+
|
|
170
191
|
packages = self.config.get("packages", [])
|
|
171
192
|
if not packages:
|
|
172
193
|
self.console.print("[dim]No APT packages configured[/]")
|
|
173
194
|
return self.results["packages"]
|
|
174
|
-
|
|
195
|
+
|
|
175
196
|
pkg_table = Table(title="Package Validation", border_style="cyan")
|
|
176
197
|
pkg_table.add_column("Package", style="bold")
|
|
177
198
|
pkg_table.add_column("Status", justify="center")
|
|
178
199
|
pkg_table.add_column("Version", style="dim")
|
|
179
|
-
|
|
200
|
+
|
|
180
201
|
for package in packages:
|
|
181
202
|
self.results["packages"]["total"] += 1
|
|
182
|
-
|
|
203
|
+
|
|
183
204
|
# Check if installed
|
|
184
205
|
check_cmd = f"dpkg -l | grep -E '^ii {package}' | awk '{{print $3}}'"
|
|
185
206
|
version = self._exec_in_vm(check_cmd)
|
|
186
|
-
|
|
207
|
+
|
|
187
208
|
if version:
|
|
188
209
|
pkg_table.add_row(package, "[green]✅ Installed[/]", version[:40])
|
|
189
210
|
self.results["packages"]["passed"] += 1
|
|
190
|
-
self.results["packages"]["details"].append(
|
|
191
|
-
"package": package,
|
|
192
|
-
|
|
193
|
-
"version": version
|
|
194
|
-
})
|
|
211
|
+
self.results["packages"]["details"].append(
|
|
212
|
+
{"package": package, "installed": True, "version": version}
|
|
213
|
+
)
|
|
195
214
|
else:
|
|
196
215
|
pkg_table.add_row(package, "[red]❌ Missing[/]", "")
|
|
197
216
|
self.results["packages"]["failed"] += 1
|
|
198
|
-
self.results["packages"]["details"].append(
|
|
199
|
-
"package": package,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
})
|
|
203
|
-
|
|
217
|
+
self.results["packages"]["details"].append(
|
|
218
|
+
{"package": package, "installed": False, "version": None}
|
|
219
|
+
)
|
|
220
|
+
|
|
204
221
|
self.console.print(pkg_table)
|
|
205
|
-
self.console.print(
|
|
206
|
-
|
|
222
|
+
self.console.print(
|
|
223
|
+
f"[dim]{self.results['packages']['passed']}/{self.results['packages']['total']} packages installed[/]"
|
|
224
|
+
)
|
|
225
|
+
|
|
207
226
|
return self.results["packages"]
|
|
208
|
-
|
|
227
|
+
|
|
209
228
|
def validate_snap_packages(self) -> Dict:
|
|
210
229
|
"""Validate snap packages are installed."""
|
|
211
230
|
self.console.print("\n[bold]📦 Validating Snap Packages...[/]")
|
|
212
|
-
|
|
231
|
+
|
|
213
232
|
snap_packages = self.config.get("snap_packages", [])
|
|
214
233
|
if not snap_packages:
|
|
215
234
|
self.console.print("[dim]No snap packages configured[/]")
|
|
216
235
|
return self.results["snap_packages"]
|
|
217
|
-
|
|
236
|
+
|
|
218
237
|
snap_table = Table(title="Snap Package Validation", border_style="cyan")
|
|
219
238
|
snap_table.add_column("Package", style="bold")
|
|
220
239
|
snap_table.add_column("Status", justify="center")
|
|
221
240
|
snap_table.add_column("Version", style="dim")
|
|
222
|
-
|
|
241
|
+
|
|
223
242
|
for package in snap_packages:
|
|
224
243
|
self.results["snap_packages"]["total"] += 1
|
|
225
|
-
|
|
244
|
+
|
|
226
245
|
# Check if installed
|
|
227
246
|
check_cmd = f"snap list | grep '^{package}' | awk '{{print $2}}'"
|
|
228
247
|
version = self._exec_in_vm(check_cmd)
|
|
229
|
-
|
|
248
|
+
|
|
230
249
|
if version:
|
|
231
250
|
snap_table.add_row(package, "[green]✅ Installed[/]", version[:40])
|
|
232
251
|
self.results["snap_packages"]["passed"] += 1
|
|
233
|
-
self.results["snap_packages"]["details"].append(
|
|
234
|
-
"package": package,
|
|
235
|
-
|
|
236
|
-
"version": version
|
|
237
|
-
})
|
|
252
|
+
self.results["snap_packages"]["details"].append(
|
|
253
|
+
{"package": package, "installed": True, "version": version}
|
|
254
|
+
)
|
|
238
255
|
else:
|
|
239
256
|
snap_table.add_row(package, "[red]❌ Missing[/]", "")
|
|
240
257
|
self.results["snap_packages"]["failed"] += 1
|
|
241
|
-
self.results["snap_packages"]["details"].append(
|
|
242
|
-
"package": package,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
})
|
|
246
|
-
|
|
258
|
+
self.results["snap_packages"]["details"].append(
|
|
259
|
+
{"package": package, "installed": False, "version": None}
|
|
260
|
+
)
|
|
261
|
+
|
|
247
262
|
self.console.print(snap_table)
|
|
248
|
-
self.console.print(
|
|
249
|
-
|
|
263
|
+
self.console.print(
|
|
264
|
+
f"[dim]{self.results['snap_packages']['passed']}/{self.results['snap_packages']['total']} snap packages installed[/]"
|
|
265
|
+
)
|
|
266
|
+
|
|
250
267
|
return self.results["snap_packages"]
|
|
251
|
-
|
|
268
|
+
|
|
252
269
|
# Services that should NOT be validated in VM (host-specific)
|
|
253
270
|
VM_EXCLUDED_SERVICES = {
|
|
254
|
-
"libvirtd",
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
271
|
+
"libvirtd",
|
|
272
|
+
"virtlogd",
|
|
273
|
+
"libvirt-guests",
|
|
274
|
+
"qemu-guest-agent",
|
|
275
|
+
"bluetooth",
|
|
276
|
+
"bluez",
|
|
277
|
+
"upower",
|
|
278
|
+
"thermald",
|
|
279
|
+
"tlp",
|
|
280
|
+
"power-profiles-daemon",
|
|
281
|
+
"gdm",
|
|
282
|
+
"gdm3",
|
|
283
|
+
"sddm",
|
|
284
|
+
"lightdm",
|
|
285
|
+
"snap.cups.cups-browsed",
|
|
286
|
+
"snap.cups.cupsd",
|
|
287
|
+
"ModemManager",
|
|
288
|
+
"wpa_supplicant",
|
|
289
|
+
"accounts-daemon",
|
|
290
|
+
"colord",
|
|
291
|
+
"switcheroo-control",
|
|
260
292
|
}
|
|
261
293
|
|
|
262
294
|
def validate_services(self) -> Dict:
|
|
263
295
|
"""Validate services are enabled and running."""
|
|
264
296
|
self.console.print("\n[bold]⚙️ Validating Services...[/]")
|
|
265
|
-
|
|
297
|
+
|
|
266
298
|
services = self.config.get("services", [])
|
|
267
299
|
if not services:
|
|
268
300
|
self.console.print("[dim]No services configured[/]")
|
|
@@ -305,7 +337,9 @@ class VMValidator:
|
|
|
305
337
|
|
|
306
338
|
pid_value = ""
|
|
307
339
|
if is_running:
|
|
308
|
-
pid_out = self._exec_in_vm(
|
|
340
|
+
pid_out = self._exec_in_vm(
|
|
341
|
+
f"systemctl show -p MainPID --value {service} 2>/dev/null"
|
|
342
|
+
)
|
|
309
343
|
if pid_out is None:
|
|
310
344
|
pid_value = "?"
|
|
311
345
|
else:
|
|
@@ -351,19 +385,47 @@ class VMValidator:
|
|
|
351
385
|
snap_app_specs = {
|
|
352
386
|
"pycharm-community": {
|
|
353
387
|
"process_patterns": ["pycharm-community", "pycharm", "jetbrains"],
|
|
354
|
-
"required_interfaces": [
|
|
388
|
+
"required_interfaces": [
|
|
389
|
+
"desktop",
|
|
390
|
+
"desktop-legacy",
|
|
391
|
+
"x11",
|
|
392
|
+
"wayland",
|
|
393
|
+
"home",
|
|
394
|
+
"network",
|
|
395
|
+
],
|
|
355
396
|
},
|
|
356
397
|
"chromium": {
|
|
357
398
|
"process_patterns": ["chromium", "chromium-browser"],
|
|
358
|
-
"required_interfaces": [
|
|
399
|
+
"required_interfaces": [
|
|
400
|
+
"desktop",
|
|
401
|
+
"desktop-legacy",
|
|
402
|
+
"x11",
|
|
403
|
+
"wayland",
|
|
404
|
+
"home",
|
|
405
|
+
"network",
|
|
406
|
+
],
|
|
359
407
|
},
|
|
360
408
|
"firefox": {
|
|
361
409
|
"process_patterns": ["firefox"],
|
|
362
|
-
"required_interfaces": [
|
|
410
|
+
"required_interfaces": [
|
|
411
|
+
"desktop",
|
|
412
|
+
"desktop-legacy",
|
|
413
|
+
"x11",
|
|
414
|
+
"wayland",
|
|
415
|
+
"home",
|
|
416
|
+
"network",
|
|
417
|
+
],
|
|
363
418
|
},
|
|
364
419
|
"code": {
|
|
365
420
|
"process_patterns": ["code"],
|
|
366
|
-
"required_interfaces": [
|
|
421
|
+
"required_interfaces": [
|
|
422
|
+
"desktop",
|
|
423
|
+
"desktop-legacy",
|
|
424
|
+
"x11",
|
|
425
|
+
"wayland",
|
|
426
|
+
"home",
|
|
427
|
+
"network",
|
|
428
|
+
],
|
|
367
429
|
},
|
|
368
430
|
}
|
|
369
431
|
|
|
@@ -449,9 +511,15 @@ class VMValidator:
|
|
|
449
511
|
)
|
|
450
512
|
|
|
451
513
|
if app_name == "google-chrome":
|
|
452
|
-
add(
|
|
514
|
+
add(
|
|
515
|
+
"journalctl -n 200 --no-pager 2>/dev/null | grep -i chrome | tail -n 60 || true",
|
|
516
|
+
"Journal (chrome)",
|
|
517
|
+
)
|
|
453
518
|
if app_name == "firefox":
|
|
454
|
-
add(
|
|
519
|
+
add(
|
|
520
|
+
"journalctl -n 200 --no-pager 2>/dev/null | grep -i firefox | tail -n 60 || true",
|
|
521
|
+
"Journal (firefox)",
|
|
522
|
+
)
|
|
455
523
|
|
|
456
524
|
return "\n\n".join(chunks)
|
|
457
525
|
|
|
@@ -501,7 +569,7 @@ class VMValidator:
|
|
|
501
569
|
profile_ok = True
|
|
502
570
|
|
|
503
571
|
if installed:
|
|
504
|
-
running = _check_any_process_running(["firefox"])
|
|
572
|
+
running = _check_any_process_running(["firefox"])
|
|
505
573
|
pid = _find_first_pid(["firefox"]) if running else ""
|
|
506
574
|
|
|
507
575
|
elif app in snap_app_specs:
|
|
@@ -543,7 +611,11 @@ class VMValidator:
|
|
|
543
611
|
|
|
544
612
|
if installed:
|
|
545
613
|
running = _check_any_process_running(["google-chrome", "google-chrome-stable"])
|
|
546
|
-
pid =
|
|
614
|
+
pid = (
|
|
615
|
+
_find_first_pid(["google-chrome", "google-chrome-stable"])
|
|
616
|
+
if running
|
|
617
|
+
else ""
|
|
618
|
+
)
|
|
547
619
|
|
|
548
620
|
if self.require_running_apps and installed and profile_ok and running is None:
|
|
549
621
|
note = note or "running unknown"
|
|
@@ -551,7 +623,11 @@ class VMValidator:
|
|
|
551
623
|
running_icon = (
|
|
552
624
|
"[dim]—[/]"
|
|
553
625
|
if not installed
|
|
554
|
-
else
|
|
626
|
+
else (
|
|
627
|
+
"[green]✅[/]"
|
|
628
|
+
if running is True
|
|
629
|
+
else "[yellow]⚠️[/]" if running is False else "[dim]?[/]"
|
|
630
|
+
)
|
|
555
631
|
)
|
|
556
632
|
|
|
557
633
|
pid_value = "—" if not installed else ("?" if pid is None else (pid or "—"))
|
|
@@ -622,7 +698,9 @@ class VMValidator:
|
|
|
622
698
|
|
|
623
699
|
def _installed(app: str) -> Optional[bool]:
|
|
624
700
|
if app in {"pycharm-community", "chromium", "firefox", "code"}:
|
|
625
|
-
out = self._exec_in_vm(
|
|
701
|
+
out = self._exec_in_vm(
|
|
702
|
+
f"snap list {app} >/dev/null 2>&1 && echo yes || echo no", timeout=10
|
|
703
|
+
)
|
|
626
704
|
return None if out is None else out.strip() == "yes"
|
|
627
705
|
|
|
628
706
|
if app == "google-chrome":
|
|
@@ -633,14 +711,20 @@ class VMValidator:
|
|
|
633
711
|
return None if out is None else out.strip() == "yes"
|
|
634
712
|
|
|
635
713
|
if app == "docker":
|
|
636
|
-
out = self._exec_in_vm(
|
|
714
|
+
out = self._exec_in_vm(
|
|
715
|
+
"command -v docker >/dev/null 2>&1 && echo yes || echo no", timeout=10
|
|
716
|
+
)
|
|
637
717
|
return None if out is None else out.strip() == "yes"
|
|
638
718
|
|
|
639
719
|
if app == "firefox":
|
|
640
|
-
out = self._exec_in_vm(
|
|
720
|
+
out = self._exec_in_vm(
|
|
721
|
+
"command -v firefox >/dev/null 2>&1 && echo yes || echo no", timeout=10
|
|
722
|
+
)
|
|
641
723
|
return None if out is None else out.strip() == "yes"
|
|
642
724
|
|
|
643
|
-
out = self._exec_in_vm(
|
|
725
|
+
out = self._exec_in_vm(
|
|
726
|
+
f"command -v {app} >/dev/null 2>&1 && echo yes || echo no", timeout=10
|
|
727
|
+
)
|
|
644
728
|
return None if out is None else out.strip() == "yes"
|
|
645
729
|
|
|
646
730
|
def _run_test(app: str) -> Optional[bool]:
|
|
@@ -675,10 +759,14 @@ class VMValidator:
|
|
|
675
759
|
return None if out is None else out.strip() == "yes"
|
|
676
760
|
|
|
677
761
|
if app == "docker":
|
|
678
|
-
out = self._exec_in_vm(
|
|
762
|
+
out = self._exec_in_vm(
|
|
763
|
+
"timeout 20 docker info >/dev/null 2>&1 && echo yes || echo no", timeout=30
|
|
764
|
+
)
|
|
679
765
|
return None if out is None else out.strip() == "yes"
|
|
680
766
|
|
|
681
|
-
out = self._exec_in_vm(
|
|
767
|
+
out = self._exec_in_vm(
|
|
768
|
+
f"timeout 20 {app} --version >/dev/null 2>&1 && echo yes || echo no", timeout=30
|
|
769
|
+
)
|
|
682
770
|
return None if out is None else out.strip() == "yes"
|
|
683
771
|
|
|
684
772
|
self.console.print("\n[bold]🧪 Smoke Tests (installed ≠ works)...[/]")
|
|
@@ -703,8 +791,20 @@ class VMValidator:
|
|
|
703
791
|
else:
|
|
704
792
|
note = "install status unknown"
|
|
705
793
|
|
|
706
|
-
installed_icon =
|
|
707
|
-
|
|
794
|
+
installed_icon = (
|
|
795
|
+
"[green]✅[/]"
|
|
796
|
+
if installed is True
|
|
797
|
+
else "[red]❌[/]" if installed is False else "[dim]?[/]"
|
|
798
|
+
)
|
|
799
|
+
launch_icon = (
|
|
800
|
+
"[green]✅[/]"
|
|
801
|
+
if launched is True
|
|
802
|
+
else (
|
|
803
|
+
"[red]❌[/]"
|
|
804
|
+
if launched is False
|
|
805
|
+
else ("[dim]—[/]" if installed is not True else "[dim]?[/]")
|
|
806
|
+
)
|
|
807
|
+
)
|
|
708
808
|
|
|
709
809
|
table.add_row(app, installed_icon, launch_icon, note)
|
|
710
810
|
|
|
@@ -725,19 +825,21 @@ class VMValidator:
|
|
|
725
825
|
|
|
726
826
|
self.console.print(table)
|
|
727
827
|
return self.results["smoke"]
|
|
728
|
-
|
|
828
|
+
|
|
729
829
|
def validate_all(self) -> Dict:
|
|
730
830
|
"""Run all validations and return comprehensive results."""
|
|
731
831
|
self.console.print("[bold cyan]🔍 Running Full Validation...[/]")
|
|
732
|
-
|
|
832
|
+
|
|
733
833
|
# Check if VM is running
|
|
734
834
|
try:
|
|
735
835
|
result = subprocess.run(
|
|
736
836
|
["virsh", "--connect", self.conn_uri, "domstate", self.vm_name],
|
|
737
|
-
capture_output=True,
|
|
837
|
+
capture_output=True,
|
|
838
|
+
text=True,
|
|
839
|
+
timeout=5,
|
|
738
840
|
)
|
|
739
841
|
vm_state = result.stdout.strip()
|
|
740
|
-
|
|
842
|
+
|
|
741
843
|
if "running" not in vm_state.lower():
|
|
742
844
|
self.console.print(f"[yellow]⚠️ VM is not running (state: {vm_state})[/]")
|
|
743
845
|
self.console.print("[dim]Start VM with: clonebox start .[/]")
|
|
@@ -747,7 +849,7 @@ class VMValidator:
|
|
|
747
849
|
self.console.print(f"[red]❌ Cannot check VM state: {e}[/]")
|
|
748
850
|
self.results["overall"] = "error"
|
|
749
851
|
return self.results
|
|
750
|
-
|
|
852
|
+
|
|
751
853
|
# Run all validations
|
|
752
854
|
self.validate_mounts()
|
|
753
855
|
self.validate_packages()
|
|
@@ -757,12 +859,16 @@ class VMValidator:
|
|
|
757
859
|
if self.smoke_test:
|
|
758
860
|
self.validate_smoke_tests()
|
|
759
861
|
|
|
760
|
-
recent_err = self._exec_in_vm(
|
|
862
|
+
recent_err = self._exec_in_vm(
|
|
863
|
+
"journalctl -p err -n 30 --no-pager 2>/dev/null || true", timeout=20
|
|
864
|
+
)
|
|
761
865
|
if recent_err:
|
|
762
866
|
recent_err = recent_err.strip()
|
|
763
867
|
if recent_err:
|
|
764
|
-
self.console.print(
|
|
765
|
-
|
|
868
|
+
self.console.print(
|
|
869
|
+
Panel(recent_err, title="Recent system errors", border_style="red")
|
|
870
|
+
)
|
|
871
|
+
|
|
766
872
|
# Calculate overall status
|
|
767
873
|
total_checks = (
|
|
768
874
|
self.results["mounts"]["total"]
|
|
@@ -772,7 +878,7 @@ class VMValidator:
|
|
|
772
878
|
+ self.results["apps"]["total"]
|
|
773
879
|
+ (self.results["smoke"]["total"] if self.smoke_test else 0)
|
|
774
880
|
)
|
|
775
|
-
|
|
881
|
+
|
|
776
882
|
total_passed = (
|
|
777
883
|
self.results["mounts"]["passed"]
|
|
778
884
|
+ self.results["packages"]["passed"]
|
|
@@ -781,7 +887,7 @@ class VMValidator:
|
|
|
781
887
|
+ self.results["apps"]["passed"]
|
|
782
888
|
+ (self.results["smoke"]["passed"] if self.smoke_test else 0)
|
|
783
889
|
)
|
|
784
|
-
|
|
890
|
+
|
|
785
891
|
total_failed = (
|
|
786
892
|
self.results["mounts"]["failed"]
|
|
787
893
|
+ self.results["packages"]["failed"]
|
|
@@ -790,10 +896,10 @@ class VMValidator:
|
|
|
790
896
|
+ self.results["apps"]["failed"]
|
|
791
897
|
+ (self.results["smoke"]["failed"] if self.smoke_test else 0)
|
|
792
898
|
)
|
|
793
|
-
|
|
899
|
+
|
|
794
900
|
# Get skipped services count
|
|
795
901
|
skipped_services = self.results["services"].get("skipped", 0)
|
|
796
|
-
|
|
902
|
+
|
|
797
903
|
# Print summary
|
|
798
904
|
self.console.print("\n[bold]📊 Validation Summary[/]")
|
|
799
905
|
summary_table = Table(border_style="cyan")
|
|
@@ -802,30 +908,52 @@ class VMValidator:
|
|
|
802
908
|
summary_table.add_column("Failed", justify="right", style="red")
|
|
803
909
|
summary_table.add_column("Skipped", justify="right", style="dim")
|
|
804
910
|
summary_table.add_column("Total", justify="right")
|
|
805
|
-
|
|
806
|
-
summary_table.add_row(
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
911
|
+
|
|
912
|
+
summary_table.add_row(
|
|
913
|
+
"Mounts",
|
|
914
|
+
str(self.results["mounts"]["passed"]),
|
|
915
|
+
str(self.results["mounts"]["failed"]),
|
|
916
|
+
"—",
|
|
917
|
+
str(self.results["mounts"]["total"]),
|
|
918
|
+
)
|
|
919
|
+
summary_table.add_row(
|
|
920
|
+
"APT Packages",
|
|
921
|
+
str(self.results["packages"]["passed"]),
|
|
922
|
+
str(self.results["packages"]["failed"]),
|
|
923
|
+
"—",
|
|
924
|
+
str(self.results["packages"]["total"]),
|
|
925
|
+
)
|
|
926
|
+
summary_table.add_row(
|
|
927
|
+
"Snap Packages",
|
|
928
|
+
str(self.results["snap_packages"]["passed"]),
|
|
929
|
+
str(self.results["snap_packages"]["failed"]),
|
|
930
|
+
"—",
|
|
931
|
+
str(self.results["snap_packages"]["total"]),
|
|
932
|
+
)
|
|
933
|
+
summary_table.add_row(
|
|
934
|
+
"Services",
|
|
935
|
+
str(self.results["services"]["passed"]),
|
|
936
|
+
str(self.results["services"]["failed"]),
|
|
937
|
+
str(skipped_services),
|
|
938
|
+
str(self.results["services"]["total"]),
|
|
939
|
+
)
|
|
940
|
+
summary_table.add_row(
|
|
941
|
+
"Apps",
|
|
942
|
+
str(self.results["apps"]["passed"]),
|
|
943
|
+
str(self.results["apps"]["failed"]),
|
|
944
|
+
"—",
|
|
945
|
+
str(self.results["apps"]["total"]),
|
|
946
|
+
)
|
|
947
|
+
summary_table.add_row(
|
|
948
|
+
"[bold]TOTAL",
|
|
949
|
+
f"[bold green]{total_passed}",
|
|
950
|
+
f"[bold red]{total_failed}",
|
|
951
|
+
f"[dim]{skipped_services}[/]",
|
|
952
|
+
f"[bold]{total_checks}",
|
|
953
|
+
)
|
|
954
|
+
|
|
827
955
|
self.console.print(summary_table)
|
|
828
|
-
|
|
956
|
+
|
|
829
957
|
# Determine overall status
|
|
830
958
|
if total_failed == 0 and total_checks > 0:
|
|
831
959
|
self.results["overall"] = "pass"
|
|
@@ -833,9 +961,11 @@ class VMValidator:
|
|
|
833
961
|
elif total_failed > 0:
|
|
834
962
|
self.results["overall"] = "partial"
|
|
835
963
|
self.console.print(f"\n[bold yellow]⚠️ {total_failed}/{total_checks} checks failed[/]")
|
|
836
|
-
self.console.print(
|
|
964
|
+
self.console.print(
|
|
965
|
+
"[dim]Consider rebuilding VM: clonebox clone . --user --run --replace[/]"
|
|
966
|
+
)
|
|
837
967
|
else:
|
|
838
968
|
self.results["overall"] = "no_checks"
|
|
839
969
|
self.console.print("\n[dim]No validation checks configured[/]")
|
|
840
|
-
|
|
970
|
+
|
|
841
971
|
return self.results
|