130 lines
4.2 KiB
Python
130 lines
4.2 KiB
Python
# common.py — общие примитивы нагрузочных тестов IoT.
|
||
#
|
||
# Конфигурация через env (дефолты — прод-платформа):
|
||
# IOT_API — базовый URL API (https://iot.containerk8s.dev.nubes.ru)
|
||
# IOT_WS — wss URL EMQX (wss://exqx.containerk8s.dev.nubes.ru/mqtt)
|
||
# IOT_NS_PREFIX — префикс namespace для тестов (lt)
|
||
# IOT_CLEANUP — удалять созданные устройства после теста (1)
|
||
import base64
|
||
import json
|
||
import os
|
||
import ssl
|
||
import threading
|
||
import time
|
||
import urllib.request
|
||
from urllib.parse import urlparse
|
||
|
||
API = os.environ.get("IOT_API", "https://iot.containerk8s.dev.nubes.ru")
|
||
WS = os.environ.get("IOT_WS", "wss://exqx.containerk8s.dev.nubes.ru/mqtt")
|
||
NS_PREFIX = os.environ.get("IOT_NS_PREFIX", "lt")
|
||
CLEANUP = os.environ.get("IOT_CLEANUP", "1") == "1"
|
||
|
||
_ws = urlparse(WS)
|
||
WS_HOST = _ws.hostname or ""
|
||
WS_PORT = _ws.port or 443
|
||
WS_PATH = _ws.path or "/mqtt"
|
||
|
||
# Платформа рвёт внешние wss ~150с: паблишер должен уметь реконнектиться.
|
||
EDGE_BREAK_SEC = 150
|
||
|
||
|
||
def mint_jwt(sub="loadtest", ttl=86400):
|
||
h = base64.urlsafe_b64encode(b'{"alg":"none","typ":"JWT"}').rstrip(b"=").decode()
|
||
p = base64.urlsafe_b64encode(
|
||
json.dumps({"sub": sub, "exp": int(time.time()) + ttl}).encode()
|
||
).rstrip(b"=").decode()
|
||
return h + "." + p + ".sig"
|
||
|
||
|
||
class API:
|
||
"""Тонкая обёртка над REST API монолита."""
|
||
|
||
def __init__(self, base=API):
|
||
self.base = base.rstrip("/")
|
||
self.token = mint_jwt()
|
||
self.ctx = ssl.create_default_context()
|
||
self.ctx.check_hostname = False
|
||
self.ctx.verify_mode = ssl.CERT_NONE
|
||
|
||
def _req(self, method, path, body=None):
|
||
url = self.base + path
|
||
data = json.dumps(body).encode() if body is not None else None
|
||
req = urllib.request.Request(
|
||
url, data=data, method=method,
|
||
headers={"Authorization": "Bearer " + self.token,
|
||
"Content-Type": "application/json"},
|
||
)
|
||
with urllib.request.urlopen(req, timeout=30, context=self.ctx) as r:
|
||
return json.loads(r.read().decode())
|
||
|
||
def create_device(self, ns, name, device_id):
|
||
return self._req("POST", f"/v1/namespaces/{ns}/iot/devices",
|
||
{"name": name, "device_id": device_id})
|
||
|
||
def get_device(self, ns, name):
|
||
return self._req("GET", f"/v1/namespaces/{ns}/iot/devices/{name}")
|
||
|
||
def list_devices(self, ns):
|
||
return self._req("GET", f"/v1/namespaces/{ns}/iot/devices")
|
||
|
||
def delete_device(self, ns, name):
|
||
return self._req("DELETE", f"/v1/namespaces/{ns}/iot/devices/{name}")
|
||
|
||
def telemetry(self, ns, device_id="", limit=1000):
|
||
q = f"?limit={limit}"
|
||
if device_id:
|
||
q += f"&device_id={device_id}"
|
||
return self._req("GET", f"/v1/namespaces/{ns}/iot/telemetry{q}")
|
||
|
||
|
||
class Counter:
|
||
"""Потокобезопасные счётчики метрик."""
|
||
|
||
def __init__(self):
|
||
self._l = threading.Lock()
|
||
self.v = {}
|
||
|
||
def inc(self, key, n=1):
|
||
with self._l:
|
||
self.v[key] = self.v.get(key, 0) + n
|
||
|
||
def set(self, key, val):
|
||
with self._l:
|
||
self.v[key] = val
|
||
|
||
def get(self, key, default=0):
|
||
with self._l:
|
||
return self.v.get(key, default)
|
||
|
||
def snapshot(self):
|
||
with self._l:
|
||
return dict(self.v)
|
||
|
||
|
||
def ws_tls_opts(client):
|
||
"""wss с самоподписанным/платформенным сертификатом — без проверки."""
|
||
client.tls_set(cert_reqs=ssl.CERT_NONE)
|
||
client.tls_insecure_set(True)
|
||
|
||
|
||
def percentile(values, q):
|
||
if not values:
|
||
return 0.0
|
||
s = sorted(values)
|
||
i = min(len(s) - 1, int(round(q * (len(s) - 1))))
|
||
return s[i]
|
||
|
||
|
||
def latency_stats(ms_list):
|
||
return {
|
||
"n": len(ms_list),
|
||
"p50_ms": round(percentile(ms_list, 0.50), 1),
|
||
"p95_ms": round(percentile(ms_list, 0.95), 1),
|
||
"p99_ms": round(percentile(ms_list, 0.99), 1),
|
||
"max_ms": round(max(ms_list), 1) if ms_list else 0.0,
|
||
}
|
||
|
||
|
||
def now_ms():
|
||
return int(time.time() * 1000)
|