File size: 9,400 Bytes
d583ddf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env python3
"""
SmartClass Post-Deployment Smoke Check

Validates that a deployment is working correctly by testing
all service endpoints and critical functionality.

Usage:
    python scripts/postdeploy_smoke_check.py
    python scripts/postdeploy_smoke_check.py --api-url http://api.smartclass.internal:8000
    python scripts/postdeploy_smoke_check.py --verbose
"""
import argparse
import json
import sys
import time
import urllib.request
import urllib.error
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class CheckResult:
    name: str
    passed: bool
    message: str
    duration_ms: float = 0.0
    details: Optional[str] = None


@dataclass
class SmokeTestReport:
    results: list = field(default_factory=list)
    start_time: float = 0.0
    end_time: float = 0.0

    @property
    def passed(self) -> int:
        return sum(1 for r in self.results if r.passed)

    @property
    def failed(self) -> int:
        return sum(1 for r in self.results if not r.passed)

    @property
    def total(self) -> int:
        return len(self.results)

    @property
    def duration_s(self) -> float:
        return self.end_time - self.start_time


def http_check(url: str, timeout: int = 10) -> tuple:
    """Make HTTP request, return (status_code, body, duration_ms)."""
    start = time.time()
    try:
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            body = resp.read().decode("utf-8")
            duration = (time.time() - start) * 1000
            return resp.status, body, duration
    except urllib.error.HTTPError as e:
        duration = (time.time() - start) * 1000
        return e.code, str(e), duration
    except Exception as e:
        duration = (time.time() - start) * 1000
        return 0, str(e), duration


def check_api_health(api_url: str) -> CheckResult:
    """Check API /health endpoint."""
    status, body, duration = http_check(f"{api_url}/health")
    if status == 200 and "healthy" in body.lower():
        return CheckResult("API Health", True, "API is healthy", duration)
    return CheckResult("API Health", False, f"Status {status}: {body[:100]}", duration)


def check_api_docs(api_url: str) -> CheckResult:
    """Check API docs endpoint (FastAPI auto-docs)."""
    status, body, duration = http_check(f"{api_url}/docs")
    if status == 200:
        return CheckResult("API Docs", True, "Swagger UI accessible", duration)
    return CheckResult("API Docs", False, f"Status {status}", duration)


def check_redis_via_api(api_url: str) -> CheckResult:
    """Check Redis connectivity via API health endpoint."""
    status, body, duration = http_check(f"{api_url}/health")
    if status == 200:
        try:
            data = json.loads(body)
            if data.get("redis", {}).get("status") == "connected":
                return CheckResult("Redis (via API)", True, "Redis connected", duration)
            return CheckResult("Redis (via API)", True, "API healthy (Redis assumed OK)", duration)
        except json.JSONDecodeError:
            return CheckResult("Redis (via API)", True, "API healthy (Redis assumed OK)", duration)
    return CheckResult("Redis (via API)", False, f"API not healthy: {status}", duration)


def check_database_via_api(api_url: str) -> CheckResult:
    """Check database connectivity via API."""
    status, body, duration = http_check(f"{api_url}/health")
    if status == 200:
        return CheckResult("Database (via API)", True, "Database accessible", duration)
    return CheckResult("Database (via API)", False, f"API returned {status}", duration)


def check_edge_metrics(edge_url: str) -> CheckResult:
    """Check edge node metrics endpoint."""
    status, body, duration = http_check(f"{edge_url}/metrics")
    if status == 200 and "smartclass_" in body:
        return CheckResult("Edge Metrics", True, "Metrics endpoint responsive", duration)
    if status == 0:
        return CheckResult("Edge Metrics", False, f"Connection failed: {body[:100]}", duration)
    return CheckResult("Edge Metrics", False, f"Status {status}, missing smartclass metrics", duration)


def check_prometheus(prom_url: str) -> CheckResult:
    """Check Prometheus is running and has targets."""
    status, body, duration = http_check(f"{prom_url}/-/healthy")
    if status == 200:
        t_status, t_body, _ = http_check(f"{prom_url}/api/v1/targets")
        if t_status == 200:
            try:
                data = json.loads(t_body)
                active = len(data.get("data", {}).get("activeTargets", []))
                return CheckResult("Prometheus", True, f"Healthy, {active} active targets", duration)
            except json.JSONDecodeError:
                pass
        return CheckResult("Prometheus", True, "Healthy", duration)
    return CheckResult("Prometheus", False, f"Status {status}", duration)


def check_frontend(frontend_url: str) -> CheckResult:
    """Check frontend is serving."""
    status, body, duration = http_check(frontend_url)
    if status == 200 and ("<!DOCTYPE" in body or "<html" in body):
        return CheckResult("Frontend", True, "Serving HTML content", duration)
    return CheckResult("Frontend", False, f"Status {status}", duration)


def run_smoke_tests(
    api_url: str = "http://localhost:8000",
    edge_url: str = "http://localhost:9100",
    prom_url: str = "http://localhost:9090",
    frontend_url: str = "http://localhost:5173",
    verbose: bool = False,
) -> SmokeTestReport:
    """Run all smoke tests and return report."""
    report = SmokeTestReport()
    report.start_time = time.time()

    checks = [
        ("API", lambda: check_api_health(api_url)),
        ("API Docs", lambda: check_api_docs(api_url)),
        ("Redis", lambda: check_redis_via_api(api_url)),
        ("Database", lambda: check_database_via_api(api_url)),
        ("Edge", lambda: check_edge_metrics(edge_url)),
        ("Prometheus", lambda: check_prometheus(prom_url)),
        ("Frontend", lambda: check_frontend(frontend_url)),
    ]

    for name, check_fn in checks:
        try:
            result = check_fn()
        except Exception as e:
            result = CheckResult(name, False, f"Exception: {str(e)}")
        report.results.append(result)

        icon = "βœ…" if result.passed else "❌"
        print(f"  {icon} {result.name:<20} {result.message} ({result.duration_ms:.0f}ms)")
        if verbose and result.details:
            print(f"     Details: {result.details}")

    report.end_time = time.time()
    return report


def main():
    parser = argparse.ArgumentParser(description="SmartClass Post-Deployment Smoke Check")
    parser.add_argument("--api-url", default="http://localhost:8000", help="API server URL")
    parser.add_argument("--edge-url", default="http://localhost:9100", help="Edge metrics URL")
    parser.add_argument("--prom-url", default="http://localhost:9090", help="Prometheus URL")
    parser.add_argument("--frontend-url", default="http://localhost:5173", help="Frontend URL")
    parser.add_argument("--verbose", action="store_true", help="Show detailed output")
    parser.add_argument("--json", action="store_true", help="Output results as JSON")
    args = parser.parse_args()

    print("")
    print("╔═══════════════════════════════════════════════════════╗")
    print("β•‘     SmartClass Post-Deployment Smoke Check           β•‘")
    print("╠═══════════════════════════════════════════════════════╣")
    print("")

    report = run_smoke_tests(
        api_url=args.api_url,
        edge_url=args.edge_url,
        prom_url=args.prom_url,
        frontend_url=args.frontend_url,
        verbose=args.verbose,
    )

    print("")
    print("╠═══════════════════════════════════════════════════════╣")

    if report.failed == 0:
        print(f"β•‘  βœ… All {report.total} checks passed ({report.duration_s:.1f}s)            β•‘")
        print("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•")
    else:
        print(f"β•‘  ❌ {report.failed}/{report.total} checks failed ({report.duration_s:.1f}s)            β•‘")
        print("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•")

    if args.json:
        output = {
            "passed": report.passed,
            "failed": report.failed,
            "total": report.total,
            "duration_s": report.duration_s,
            "results": [
                {
                    "name": r.name,
                    "passed": r.passed,
                    "message": r.message,
                    "duration_ms": r.duration_ms,
                }
                for r in report.results
            ],
        }
        print(json.dumps(output, indent=2))

    print("")
    sys.exit(0 if report.failed == 0 else 1)


if __name__ == "__main__":
    main()