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/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
- ["virsh", "--connect", self.conn_uri, "qemu-agent-command", self.vm_name,
49
- f'{{"execute":"guest-exec","arguments":{{"path":"/bin/sh","arg":["-c","{command}"],"capture-output":true}}}}'],
50
- capture_output=True, text=True, timeout=timeout
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
- ["virsh", "--connect", self.conn_uri, "qemu-agent-command", self.vm_name,
68
- f'{{"execute":"guest-exec-status","arguments":{{"pid":{pid}}}}}'],
69
- capture_output=True, text=True, timeout=5
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('\n') if line.strip()]
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
- "path": guest_path,
155
- "mounted": is_mounted,
156
- "accessible": accessible,
157
- "files": file_count,
158
- "status": status
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(f"[dim]{self.results['mounts']['passed']}/{self.results['mounts']['total']} mounts working[/]")
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
- "installed": True,
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
- "installed": False,
201
- "version": None
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(f"[dim]{self.results['packages']['passed']}/{self.results['packages']['total']} packages installed[/]")
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
- "installed": True,
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
- "installed": False,
244
- "version": None
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(f"[dim]{self.results['snap_packages']['passed']}/{self.results['snap_packages']['total']} snap packages installed[/]")
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", "virtlogd", "libvirt-guests", "qemu-guest-agent",
255
- "bluetooth", "bluez", "upower", "thermald", "tlp", "power-profiles-daemon",
256
- "gdm", "gdm3", "sddm", "lightdm",
257
- "snap.cups.cups-browsed", "snap.cups.cupsd",
258
- "ModemManager", "wpa_supplicant",
259
- "accounts-daemon", "colord", "switcheroo-control",
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(f"systemctl show -p MainPID --value {service} 2>/dev/null")
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": ["desktop", "desktop-legacy", "x11", "wayland", "home", "network"],
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": ["desktop", "desktop-legacy", "x11", "wayland", "home", "network"],
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": ["desktop", "desktop-legacy", "x11", "wayland", "home", "network"],
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": ["desktop", "desktop-legacy", "x11", "wayland", "home", "network"],
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("journalctl -n 200 --no-pager 2>/dev/null | grep -i chrome | tail -n 60 || true", "Journal (chrome)")
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("journalctl -n 200 --no-pager 2>/dev/null | grep -i firefox | tail -n 60 || true", "Journal (firefox)")
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 = _find_first_pid(["google-chrome", "google-chrome-stable"]) if running else ""
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 "[green]✅[/]" if running is True else "[yellow]⚠️[/]" if running is False else "[dim]?[/]"
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(f"snap list {app} >/dev/null 2>&1 && echo yes || echo no", timeout=10)
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("command -v docker >/dev/null 2>&1 && echo yes || echo no", timeout=10)
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("command -v firefox >/dev/null 2>&1 && echo yes || echo no", timeout=10)
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(f"command -v {app} >/dev/null 2>&1 && echo yes || echo no", timeout=10)
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("timeout 20 docker info >/dev/null 2>&1 && echo yes || echo no", timeout=30)
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(f"timeout 20 {app} --version >/dev/null 2>&1 && echo yes || echo no", timeout=30)
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 = "[green]✅[/]" if installed is True else "[red]❌[/]" if installed is False else "[dim]?[/]"
707
- launch_icon = "[green]✅[/]" if launched is True else "[red]❌[/]" if launched is False else ("[dim]—[/]" if installed is not True else "[dim]?[/]")
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, text=True, timeout=5
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("journalctl -p err -n 30 --no-pager 2>/dev/null || true", timeout=20)
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(Panel(recent_err, title="Recent system errors", border_style="red"))
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("Mounts", str(self.results["mounts"]["passed"]),
807
- str(self.results["mounts"]["failed"]), "—",
808
- str(self.results["mounts"]["total"]))
809
- summary_table.add_row("APT Packages", str(self.results["packages"]["passed"]),
810
- str(self.results["packages"]["failed"]), "—",
811
- str(self.results["packages"]["total"]))
812
- summary_table.add_row("Snap Packages", str(self.results["snap_packages"]["passed"]),
813
- str(self.results["snap_packages"]["failed"]), "—",
814
- str(self.results["snap_packages"]["total"]))
815
- summary_table.add_row("Services", str(self.results["services"]["passed"]),
816
- str(self.results["services"]["failed"]),
817
- str(skipped_services),
818
- str(self.results["services"]["total"]))
819
- summary_table.add_row("Apps", str(self.results["apps"]["passed"]),
820
- str(self.results["apps"]["failed"]), "—",
821
- str(self.results["apps"]["total"]))
822
- summary_table.add_row("[bold]TOTAL", f"[bold green]{total_passed}",
823
- f"[bold red]{total_failed}",
824
- f"[dim]{skipped_services}[/]",
825
- f"[bold]{total_checks}")
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("[dim]Consider rebuilding VM: clonebox clone . --user --run --replace[/]")
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