clonebox 1.1.21__py3-none-any.whl → 1.1.23__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 +8 -3
- clonebox/cloner.py +66 -2
- clonebox/validation/__init__.py +1 -0
- clonebox/validation/apps.py +303 -0
- clonebox/validation/core.py +202 -0
- clonebox/validation/disk.py +149 -0
- clonebox/validation/mounts.py +128 -0
- clonebox/validation/overall.py +240 -0
- clonebox/validation/packages.py +117 -0
- clonebox/validation/services.py +125 -0
- clonebox/validation/smoke.py +177 -0
- clonebox/validation/validator.py +24 -0
- clonebox/validator.py +2 -1414
- {clonebox-1.1.21.dist-info → clonebox-1.1.23.dist-info}/METADATA +1 -1
- {clonebox-1.1.21.dist-info → clonebox-1.1.23.dist-info}/RECORD +19 -9
- {clonebox-1.1.21.dist-info → clonebox-1.1.23.dist-info}/WHEEL +0 -0
- {clonebox-1.1.21.dist-info → clonebox-1.1.23.dist-info}/entry_points.txt +0 -0
- {clonebox-1.1.21.dist-info → clonebox-1.1.23.dist-info}/licenses/LICENSE +0 -0
- {clonebox-1.1.21.dist-info → clonebox-1.1.23.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ServiceValidationMixin:
|
|
7
|
+
# Services that should NOT be validated in VM (host-specific)
|
|
8
|
+
VM_EXCLUDED_SERVICES = {
|
|
9
|
+
"libvirtd",
|
|
10
|
+
"virtlogd",
|
|
11
|
+
"libvirt-guests",
|
|
12
|
+
"qemu-guest-agent",
|
|
13
|
+
"bluetooth",
|
|
14
|
+
"bluez",
|
|
15
|
+
"upower",
|
|
16
|
+
"thermald",
|
|
17
|
+
"tlp",
|
|
18
|
+
"power-profiles-daemon",
|
|
19
|
+
"gdm",
|
|
20
|
+
"gdm3",
|
|
21
|
+
"sddm",
|
|
22
|
+
"lightdm",
|
|
23
|
+
"snap.cups.cups-browsed",
|
|
24
|
+
"snap.cups.cupsd",
|
|
25
|
+
"ModemManager",
|
|
26
|
+
"wpa_supplicant",
|
|
27
|
+
"accounts-daemon",
|
|
28
|
+
"colord",
|
|
29
|
+
"switcheroo-control",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def validate_services(self) -> Dict:
|
|
33
|
+
"""Validate services are enabled and running."""
|
|
34
|
+
setup_in_progress = self._setup_in_progress() is True
|
|
35
|
+
self.console.print("\n[bold]⚙️ Validating Services...[/]")
|
|
36
|
+
|
|
37
|
+
services = self.config.get("services", [])
|
|
38
|
+
if not services:
|
|
39
|
+
self.console.print("[dim]No services configured[/]")
|
|
40
|
+
return self.results["services"]
|
|
41
|
+
|
|
42
|
+
total_svcs = len(services)
|
|
43
|
+
self.console.print(f"[dim]Checking {total_svcs} services via QGA...[/]")
|
|
44
|
+
|
|
45
|
+
if "skipped" not in self.results["services"]:
|
|
46
|
+
self.results["services"]["skipped"] = 0
|
|
47
|
+
|
|
48
|
+
svc_table = Table(title="Service Validation", border_style="cyan")
|
|
49
|
+
svc_table.add_column("Service", style="bold")
|
|
50
|
+
svc_table.add_column("Enabled", justify="center")
|
|
51
|
+
svc_table.add_column("Running", justify="center")
|
|
52
|
+
svc_table.add_column("PID", justify="right", style="dim")
|
|
53
|
+
svc_table.add_column("Note", style="dim")
|
|
54
|
+
|
|
55
|
+
for idx, service in enumerate(services, 1):
|
|
56
|
+
if idx == 1 or idx % 25 == 0 or idx == total_svcs:
|
|
57
|
+
self.console.print(f"[dim] ...services progress: {idx}/{total_svcs}[/]")
|
|
58
|
+
if service in self.VM_EXCLUDED_SERVICES:
|
|
59
|
+
svc_table.add_row(service, "[dim]—[/]", "[dim]—[/]", "[dim]—[/]", "host-only")
|
|
60
|
+
self.results["services"]["skipped"] += 1
|
|
61
|
+
self.results["services"]["details"].append(
|
|
62
|
+
{
|
|
63
|
+
"service": service,
|
|
64
|
+
"enabled": None,
|
|
65
|
+
"running": None,
|
|
66
|
+
"skipped": True,
|
|
67
|
+
"reason": "host-specific service",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
self.results["services"]["total"] += 1
|
|
73
|
+
|
|
74
|
+
enabled_cmd = f"systemctl is-enabled {service} 2>/dev/null"
|
|
75
|
+
enabled_status = self._exec_in_vm(enabled_cmd)
|
|
76
|
+
is_enabled = enabled_status == "enabled"
|
|
77
|
+
|
|
78
|
+
running_cmd = f"systemctl is-active {service} 2>/dev/null"
|
|
79
|
+
running_status = self._exec_in_vm(running_cmd)
|
|
80
|
+
is_running = running_status == "active"
|
|
81
|
+
|
|
82
|
+
pid_value = ""
|
|
83
|
+
if is_running:
|
|
84
|
+
pid_out = self._exec_in_vm(f"systemctl show -p MainPID --value {service} 2>/dev/null")
|
|
85
|
+
if pid_out is None:
|
|
86
|
+
pid_value = "?"
|
|
87
|
+
else:
|
|
88
|
+
pid_value = pid_out.strip() or "?"
|
|
89
|
+
else:
|
|
90
|
+
pid_value = "—"
|
|
91
|
+
|
|
92
|
+
enabled_icon = (
|
|
93
|
+
"[green]✅[/]" if is_enabled else ("[yellow]⏳[/]" if setup_in_progress else "[yellow]⚠️[/]")
|
|
94
|
+
)
|
|
95
|
+
running_icon = (
|
|
96
|
+
"[green]✅[/]" if is_running else ("[yellow]⏳[/]" if setup_in_progress else "[red]❌[/]")
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
svc_table.add_row(service, enabled_icon, running_icon, pid_value, "")
|
|
100
|
+
|
|
101
|
+
if is_enabled and is_running:
|
|
102
|
+
self.results["services"]["passed"] += 1
|
|
103
|
+
elif setup_in_progress:
|
|
104
|
+
self.results["services"]["skipped"] += 1
|
|
105
|
+
else:
|
|
106
|
+
self.results["services"]["failed"] += 1
|
|
107
|
+
|
|
108
|
+
self.results["services"]["details"].append(
|
|
109
|
+
{
|
|
110
|
+
"service": service,
|
|
111
|
+
"enabled": is_enabled,
|
|
112
|
+
"running": is_running,
|
|
113
|
+
"pid": None if pid_value in ("", "—", "?") else pid_value,
|
|
114
|
+
"skipped": False,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self.console.print(svc_table)
|
|
119
|
+
skipped = self.results["services"].get("skipped", 0)
|
|
120
|
+
msg = f"{self.results['services']['passed']}/{self.results['services']['total']} services active"
|
|
121
|
+
if skipped > 0:
|
|
122
|
+
msg += f" ({skipped} host-only skipped)"
|
|
123
|
+
self.console.print(f"[dim]{msg}[/]")
|
|
124
|
+
|
|
125
|
+
return self.results["services"]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SmokeValidationMixin:
|
|
7
|
+
def validate_smoke_tests(self) -> Dict:
|
|
8
|
+
setup_in_progress = self._setup_in_progress() is True
|
|
9
|
+
packages = self.config.get("packages", [])
|
|
10
|
+
snap_packages = self.config.get("snap_packages", [])
|
|
11
|
+
copy_paths = self.config.get("copy_paths", None)
|
|
12
|
+
if not isinstance(copy_paths, dict) or not copy_paths:
|
|
13
|
+
copy_paths = self.config.get("app_data_paths", {})
|
|
14
|
+
vm_user = self.config.get("vm", {}).get("username", "ubuntu")
|
|
15
|
+
|
|
16
|
+
expected = []
|
|
17
|
+
|
|
18
|
+
if "firefox" in packages:
|
|
19
|
+
expected.append("firefox")
|
|
20
|
+
|
|
21
|
+
for snap_pkg in snap_packages:
|
|
22
|
+
if snap_pkg in {"pycharm-community", "chromium", "firefox", "code"}:
|
|
23
|
+
expected.append(snap_pkg)
|
|
24
|
+
|
|
25
|
+
for _, guest_path in copy_paths.items():
|
|
26
|
+
if guest_path == "/home/ubuntu/.config/google-chrome":
|
|
27
|
+
expected.append("google-chrome")
|
|
28
|
+
break
|
|
29
|
+
|
|
30
|
+
if "docker" in (self.config.get("services", []) or []) or "docker.io" in packages:
|
|
31
|
+
expected.append("docker")
|
|
32
|
+
|
|
33
|
+
expected = sorted(set(expected))
|
|
34
|
+
if not expected:
|
|
35
|
+
return self.results["smoke"]
|
|
36
|
+
|
|
37
|
+
def _installed(app: str) -> Optional[bool]:
|
|
38
|
+
if app in {"pycharm-community", "chromium", "firefox", "code"}:
|
|
39
|
+
out = self._exec_in_vm(f"snap list {app} >/dev/null 2>&1 && echo yes || echo no", timeout=10)
|
|
40
|
+
return None if out is None else out.strip() == "yes"
|
|
41
|
+
|
|
42
|
+
if app == "google-chrome":
|
|
43
|
+
out = self._exec_in_vm(
|
|
44
|
+
"(command -v google-chrome >/dev/null 2>&1 || command -v google-chrome-stable >/dev/null 2>&1) && echo yes || echo no",
|
|
45
|
+
timeout=10,
|
|
46
|
+
)
|
|
47
|
+
return None if out is None else out.strip() == "yes"
|
|
48
|
+
|
|
49
|
+
if app == "docker":
|
|
50
|
+
out = self._exec_in_vm("command -v docker >/dev/null 2>&1 && echo yes || echo no", timeout=10)
|
|
51
|
+
return None if out is None else out.strip() == "yes"
|
|
52
|
+
|
|
53
|
+
if app == "firefox":
|
|
54
|
+
out = self._exec_in_vm("command -v firefox >/dev/null 2>&1 && echo yes || echo no", timeout=10)
|
|
55
|
+
return None if out is None else out.strip() == "yes"
|
|
56
|
+
|
|
57
|
+
out = self._exec_in_vm(f"command -v {app} >/dev/null 2>&1 && echo yes || echo no", timeout=10)
|
|
58
|
+
return None if out is None else out.strip() == "yes"
|
|
59
|
+
|
|
60
|
+
def _run_test(app: str) -> Optional[bool]:
|
|
61
|
+
uid_out = self._exec_in_vm(f"id -u {vm_user} 2>/dev/null || true", timeout=10)
|
|
62
|
+
vm_uid = (uid_out or "").strip()
|
|
63
|
+
if not vm_uid.isdigit():
|
|
64
|
+
vm_uid = "1000"
|
|
65
|
+
|
|
66
|
+
runtime_dir = f"/run/user/{vm_uid}"
|
|
67
|
+
self._exec_in_vm(
|
|
68
|
+
f"mkdir -p {runtime_dir} && chown {vm_uid}:{vm_uid} {runtime_dir} && chmod 700 {runtime_dir}",
|
|
69
|
+
timeout=10,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
user_env = (
|
|
73
|
+
f"sudo -u {vm_user} env HOME=/home/{vm_user} USER={vm_user} LOGNAME={vm_user} XDG_RUNTIME_DIR={runtime_dir}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if app == "pycharm-community":
|
|
77
|
+
out = self._exec_in_vm(
|
|
78
|
+
"/snap/pycharm-community/current/jbr/bin/java -version >/dev/null 2>&1 && echo yes || echo no",
|
|
79
|
+
timeout=20,
|
|
80
|
+
)
|
|
81
|
+
return None if out is None else out.strip() == "yes"
|
|
82
|
+
|
|
83
|
+
if app == "chromium":
|
|
84
|
+
out = self._exec_in_vm(
|
|
85
|
+
f"{user_env} timeout 20 chromium --headless=new --no-sandbox --disable-gpu --dump-dom about:blank >/dev/null 2>&1 && echo yes || echo no",
|
|
86
|
+
timeout=30,
|
|
87
|
+
)
|
|
88
|
+
return None if out is None else out.strip() == "yes"
|
|
89
|
+
|
|
90
|
+
if app == "firefox":
|
|
91
|
+
out = self._exec_in_vm(
|
|
92
|
+
f"{user_env} timeout 20 firefox --headless --version >/dev/null 2>&1 && echo yes || echo no",
|
|
93
|
+
timeout=30,
|
|
94
|
+
)
|
|
95
|
+
return None if out is None else out.strip() == "yes"
|
|
96
|
+
|
|
97
|
+
if app == "google-chrome":
|
|
98
|
+
out = self._exec_in_vm(
|
|
99
|
+
f"{user_env} timeout 20 google-chrome --headless=new --no-sandbox --disable-gpu --dump-dom about:blank >/dev/null 2>&1 && echo yes || echo no",
|
|
100
|
+
timeout=30,
|
|
101
|
+
)
|
|
102
|
+
return None if out is None else out.strip() == "yes"
|
|
103
|
+
|
|
104
|
+
if app == "docker":
|
|
105
|
+
out = self._exec_in_vm("timeout 20 docker info >/dev/null 2>&1 && echo yes || echo no", timeout=30)
|
|
106
|
+
return None if out is None else out.strip() == "yes"
|
|
107
|
+
|
|
108
|
+
out = self._exec_in_vm(f"timeout 20 {app} --version >/dev/null 2>&1 && echo yes || echo no", timeout=30)
|
|
109
|
+
return None if out is None else out.strip() == "yes"
|
|
110
|
+
|
|
111
|
+
self.console.print("\n[bold]🧪 Smoke Tests (installed ≠ works)...[/]")
|
|
112
|
+
table = Table(title="Smoke Tests", border_style="cyan")
|
|
113
|
+
table.add_column("App", style="bold")
|
|
114
|
+
table.add_column("Installed", justify="center")
|
|
115
|
+
table.add_column("Launch", justify="center")
|
|
116
|
+
table.add_column("Note", style="dim")
|
|
117
|
+
|
|
118
|
+
for app in expected:
|
|
119
|
+
self.results["smoke"]["total"] += 1
|
|
120
|
+
installed = _installed(app)
|
|
121
|
+
launched: Optional[bool] = None
|
|
122
|
+
note = ""
|
|
123
|
+
pending = False
|
|
124
|
+
|
|
125
|
+
if installed is True:
|
|
126
|
+
launched = _run_test(app)
|
|
127
|
+
if launched is None:
|
|
128
|
+
note = "test failed to execute"
|
|
129
|
+
elif launched is False and setup_in_progress:
|
|
130
|
+
pending = True
|
|
131
|
+
note = note or "setup in progress"
|
|
132
|
+
elif installed is False:
|
|
133
|
+
if setup_in_progress:
|
|
134
|
+
pending = True
|
|
135
|
+
note = "setup in progress"
|
|
136
|
+
else:
|
|
137
|
+
note = "not installed"
|
|
138
|
+
else:
|
|
139
|
+
note = "install status unknown"
|
|
140
|
+
|
|
141
|
+
installed_icon = (
|
|
142
|
+
"[green]✅[/]"
|
|
143
|
+
if installed is True
|
|
144
|
+
else ("[yellow]⏳[/]" if pending else "[red]❌[/]")
|
|
145
|
+
if installed is False
|
|
146
|
+
else "[dim]?[/]"
|
|
147
|
+
)
|
|
148
|
+
launch_icon = (
|
|
149
|
+
"[green]✅[/]"
|
|
150
|
+
if launched is True
|
|
151
|
+
else ("[yellow]⏳[/]" if pending else "[red]❌[/]")
|
|
152
|
+
if launched is False
|
|
153
|
+
else ("[dim]—[/]" if installed is not True else "[dim]?[/]")
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
table.add_row(app, installed_icon, launch_icon, note)
|
|
157
|
+
|
|
158
|
+
passed = installed is True and launched is True
|
|
159
|
+
if pending:
|
|
160
|
+
self.results["smoke"]["skipped"] += 1
|
|
161
|
+
elif passed:
|
|
162
|
+
self.results["smoke"]["passed"] += 1
|
|
163
|
+
else:
|
|
164
|
+
self.results["smoke"]["failed"] += 1
|
|
165
|
+
|
|
166
|
+
self.results["smoke"]["details"].append(
|
|
167
|
+
{
|
|
168
|
+
"app": app,
|
|
169
|
+
"installed": installed,
|
|
170
|
+
"launched": launched,
|
|
171
|
+
"note": note,
|
|
172
|
+
"pending": pending,
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
self.console.print(table)
|
|
177
|
+
return self.results["smoke"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from clonebox.validation.apps import AppValidationMixin
|
|
2
|
+
from clonebox.validation.core import VMValidatorCore
|
|
3
|
+
from clonebox.validation.disk import DiskValidationMixin
|
|
4
|
+
from clonebox.validation.mounts import MountValidationMixin
|
|
5
|
+
from clonebox.validation.overall import OverallValidationMixin
|
|
6
|
+
from clonebox.validation.packages import PackageValidationMixin
|
|
7
|
+
from clonebox.validation.services import ServiceValidationMixin
|
|
8
|
+
from clonebox.validation.smoke import SmokeValidationMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VMValidator(
|
|
12
|
+
VMValidatorCore,
|
|
13
|
+
MountValidationMixin,
|
|
14
|
+
PackageValidationMixin,
|
|
15
|
+
ServiceValidationMixin,
|
|
16
|
+
AppValidationMixin,
|
|
17
|
+
SmokeValidationMixin,
|
|
18
|
+
DiskValidationMixin,
|
|
19
|
+
OverallValidationMixin,
|
|
20
|
+
):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["VMValidator"]
|