feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates. Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run a local read/write API using the existing production stores and both login providers.
|
||||
Requires the caller's existing ECS SSH authorization. Never logs or persists credentials.
|
||||
"""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def loopback_origin(value):
|
||||
try:
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
if (parsed.scheme != "http" or not ipaddress.ip_address(parsed.hostname).is_loopback
|
||||
or parsed.username is not None or parsed.password is not None
|
||||
or parsed.path or parsed.query or parsed.fragment
|
||||
or parsed.port is None or not 1024 <= parsed.port <= 65535
|
||||
or value != "http://" + parsed.netloc):
|
||||
raise ValueError()
|
||||
except (ValueError, TypeError):
|
||||
raise argparse.ArgumentTypeError("public origin must be an HTTP loopback origin with a port") from None
|
||||
return value
|
||||
|
||||
|
||||
def production_config(target):
|
||||
remote = '''import subprocess,pathlib,json
|
||||
pid=subprocess.check_output(["systemctl","show","lingniu-vehicle-platform.service","--property=MainPID"],universal_newlines=True).strip().split("=",1)[1]
|
||||
values=dict(x.split("=",1) for x in pathlib.Path("/proc/"+pid+"/environ").read_text().split("\\0") if "=" in x)
|
||||
keys=["MYSQL_DSN","TDENGINE_DSN","TDENGINE_DATABASE","TDENGINE_DRIVER","REDIS_ADDR","REDIS_USERNAME","REDIS_PASSWORD","REDIS_DB","AMAP_WEB_JS_KEY","AMAP_API_KEY","AMAP_SECURITY_JS_CODE"]
|
||||
print(json.dumps({k:values[k] for k in keys if k in values}))
|
||||
'''
|
||||
result = subprocess.run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", target, "python3 -"], input=remote, capture_output=True, text=True, check=True)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--ssh-target", default="root@115.29.187.205")
|
||||
parser.add_argument("--oneos-origin", choices=["https://dev-oneos.lnh2e.com", "https://lnoneos.com"], default="https://dev-oneos.lnh2e.com", help="OneOS authentication and vehicle data origin")
|
||||
parser.add_argument("--port", type=int, default=20400)
|
||||
parser.add_argument("--public-origin", type=loopback_origin, help="Browser origin when using a separate local frontend dev server")
|
||||
parser.add_argument("--skip-web-build", action="store_true")
|
||||
parser.add_argument("--reuse-tunnels", action="store_true", help="Reuse the active local backend tunnels without owning them")
|
||||
parser.add_argument("--no-hydrogen-worker", action="store_true", help="Do not launch customer hydrogen projection worker")
|
||||
parser.add_argument("--tdengine-transport", choices=["websocket", "rest"], default="websocket", help="TDengine transport for the local API; REST requires --no-hydrogen-worker")
|
||||
args = parser.parse_args()
|
||||
if args.tdengine_transport == "rest" and not args.no_hydrogen_worker:
|
||||
parser.error("--tdengine-transport rest requires --no-hydrogen-worker")
|
||||
if not 1024 <= args.port <= 65535:
|
||||
raise RuntimeError("invalid local port")
|
||||
# Refuse an occupied API port before connecting the production store.
|
||||
with socket.socket() as listener:
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listener.bind(("127.0.0.1", args.port))
|
||||
spec = importlib.util.spec_from_file_location("oneos_dev", Path(__file__).with_name("oneos-dev.py"))
|
||||
dev = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(dev)
|
||||
dev.DEV_ORIGIN = args.oneos_origin
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), dev.NoRedirect())
|
||||
public = dev.public_config(opener)
|
||||
run = Path(tempfile.mkdtemp(prefix="vehicle-oneos-backend-"))
|
||||
os.chmod(run, 0o700)
|
||||
clean = {key: os.environ[key] for key in ("PATH", "HOME", "TMPDIR") if key in os.environ}
|
||||
clean.update({"GOPROXY": "off", "GOSUMDB": "off", "GOTOOLCHAIN": "local"})
|
||||
subprocess.run([shutil.which("go"), "build", "-o", str(run / "platform"), "./cmd/platform-api"], cwd=ROOT / "apps/api", env=clean, check=True)
|
||||
if not args.no_hydrogen_worker:
|
||||
subprocess.run([shutil.which("go"), "build", "-o", str(run / "customer-hydrogen-worker"), "./cmd/stats-backfill"], cwd=ROOT.parent / "go/vehicle-gateway", env=clean, check=True)
|
||||
if not args.skip_web_build:
|
||||
with (run / "web-build.log").open("w") as output:
|
||||
subprocess.run([shutil.which("pnpm"), "build"], cwd=ROOT / "apps/web", env=clean, stdout=output, stderr=subprocess.STDOUT, check=True)
|
||||
values = production_config(args.ssh_target)
|
||||
forwards = []
|
||||
mysql = re.search(r"@tcp\(([^)]+)\)/([^?]+)", values.get("MYSQL_DSN", ""))
|
||||
if not mysql:
|
||||
raise RuntimeError("production MySQL TCP configuration unavailable")
|
||||
forwards.append((20430, mysql.group(1)))
|
||||
values["MYSQL_DSN"] = values["MYSQL_DSN"].replace("@tcp(" + mysql.group(1) + ")", "@tcp(127.0.0.1:20430)", 1)
|
||||
if values.get("TDENGINE_DSN"):
|
||||
td = re.search(r"@ws\(([^)]+)\)", values["TDENGINE_DSN"])
|
||||
if not td:
|
||||
raise RuntimeError("production TDengine websocket configuration unavailable")
|
||||
forwards.append((20441, td.group(1)))
|
||||
values["TDENGINE_DSN"] = values["TDENGINE_DSN"].replace("@ws(" + td.group(1) + ")", "@ws(127.0.0.1:20441)", 1)
|
||||
if args.tdengine_transport == "rest":
|
||||
values["TDENGINE_DRIVER"] = "taosRestful"
|
||||
values["TDENGINE_DSN"] = values["TDENGINE_DSN"].replace("@ws(127.0.0.1:20441)", "@http(127.0.0.1:20441)", 1)
|
||||
if values.get("REDIS_ADDR"):
|
||||
forwards.append((20479, values["REDIS_ADDR"]))
|
||||
values["REDIS_ADDR"] = "127.0.0.1:20479"
|
||||
command = ["ssh", "-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-o", "ConnectTimeout=10", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3"]
|
||||
for port, destination in forwards:
|
||||
command += ["-L", "127.0.0.1:%d:%s" % (port, destination)]
|
||||
command.append(args.ssh_target)
|
||||
tunnel = child = hydrogen_worker = None
|
||||
log = None
|
||||
origin = "http://127.0.0.1:%d" % args.port
|
||||
public_origin = args.public_origin or origin
|
||||
try:
|
||||
if not args.reuse_tunnels:
|
||||
tunnel = subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
deadline = time.monotonic() + 15
|
||||
for port, _ in forwards:
|
||||
while True:
|
||||
if tunnel is not None and tunnel.poll() is not None:
|
||||
raise RuntimeError("production tunnel could not start")
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=0.5):
|
||||
break
|
||||
except OSError:
|
||||
if time.monotonic() > deadline:
|
||||
raise RuntimeError("production tunnel timed out")
|
||||
time.sleep(0.1)
|
||||
env = dict(clean)
|
||||
env.update(values)
|
||||
env.update({
|
||||
"HTTP_ADDR": "127.0.0.1:%d" % args.port, "DATA_MODE": "production", "AUTH_MODE": "enforce",
|
||||
"STATIC_DIR": str(ROOT / "apps/web/dist"), "EXPORT_DIR": str(run / "exports"),
|
||||
"ONEOS_LOCAL_BACKEND": "true", "ONEOS_DEV_INTEGRATION": "true", "ONEOS_AUTH_ENABLED": "true",
|
||||
"ONEOS_PASSWORD_ENABLED": "true", "ONEOS_VEHICLE_QUERY_SCOPE": "true",
|
||||
"ONEOS_PROFILE_BASE_URL": dev.DEV_ORIGIN + "/prod-api", "ONEOS_PROFILE_ISSUER": dev.DEV_ORIGIN,
|
||||
"ONEOS_PROFILE_ALLOWED_TENANT": "000000", "ONEOS_PUBLIC_ORIGIN": public_origin,
|
||||
"ONEOS_PASSWORD_CLIENT_ID": public["VITE_GLOB_APP_CLIENT_ID"],
|
||||
"ONEOS_PASSWORD_RSA_PUBLIC_KEY": public["VITE_GLOB_RSA_PUBLIC_KEY"],
|
||||
"ONEOS_VEHICLE_SCOPE_PATH": "", "ONEOS_LOGIN_TICKET_EXCHANGE_PATH": "",
|
||||
"GODEBUG": "rsa1024min=0", "AMAP_SECURITY_SERVICE_HOST": public_origin + "/_AMapService",
|
||||
"ALERT_STREAM_MODE": "disabled", "HISTORY_EXPORT_CLEANUP_AUTOMATION_ENABLED": "false",
|
||||
"DAILY_GEOGRAPHY_ENABLED": "false", "REQUEST_TIMEOUT_MS": "60000",
|
||||
})
|
||||
log = (run / "platform.log").open("w")
|
||||
os.chmod(run / "platform.log", 0o600)
|
||||
child = subprocess.Popen([str(run / "platform")], env=env, stdout=log, stderr=subprocess.STDOUT)
|
||||
deadline = time.monotonic() + 20
|
||||
while True:
|
||||
try:
|
||||
with opener.open(origin + "/api/v2/auth/providers", timeout=2) as response:
|
||||
providers = json.load(response)["data"]
|
||||
if not providers["local"]["enabled"] or not providers["oneos"]["authorizationReady"]:
|
||||
raise RuntimeError("both login providers must be available")
|
||||
break
|
||||
except (OSError, ValueError, KeyError):
|
||||
if child.poll() is not None or time.monotonic() > deadline:
|
||||
raise RuntimeError("backend startup failed; see private runtime log")
|
||||
time.sleep(0.2)
|
||||
if not args.no_hydrogen_worker:
|
||||
worker_env = dict(env)
|
||||
worker_env.update({"BACKFILL_ENV_FILES": "/dev/null", "CUSTOMER_HYDROGEN_WORKER": "true", "BACKFILL_PROTOCOLS": "GB32960,YUTONG_MQTT", "BACKFILL_PROGRESS_EVERY": "10000"})
|
||||
hydrogen_worker = subprocess.Popen([str(run / "customer-hydrogen-worker")], env=worker_env, stdout=log, stderr=subprocess.STDOUT)
|
||||
print(json.dumps({"platform": origin, "oneos": dev.DEV_ORIGIN, "dataSource": "production", "readWrite": True, "localLogin": True, "oneosVehicleScope": "caller-vehicle-query", "tdengineTransport": args.tdengine_transport, "runDirectory": str(run)}, ensure_ascii=False), flush=True)
|
||||
while child.poll() is None:
|
||||
time.sleep(0.5)
|
||||
raise RuntimeError("local backend exited")
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
for process in (hydrogen_worker, child, tunnel):
|
||||
if process is not None and process.poll() is None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
if log:
|
||||
log.close()
|
||||
print("Local backend stopped. Evidence: " + str(run), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
|
||||
main()
|
||||
Reference in New Issue
Block a user