run_uat_full_module_refresh.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. #!/usr/bin/env python3
  2. """Generate S2 schedules and rebuild S1-S7 for all UAT tenants."""
  3. from __future__ import annotations
  4. import argparse
  5. import json
  6. import time
  7. from typing import Any
  8. import run_wp_wb8_api_loop as wb8
  9. from apply_sql_file import connect
  10. PASSWORD = "1234567890dop"
  11. TENANTS = (wb8.A, wb8.B, wb8.DEMO)
  12. MODULES = ("S1", "S2", "S3", "S4", "S5", "S6", "S7")
  13. TERMINAL = {"SUCCESS", "FAILED", "CANCELLED"}
  14. def value(body: Any, *names: str, default: Any = None) -> Any:
  15. data = wb8.unwrap(body)
  16. return wb8.pick(data, *names, default=default)
  17. def call(method: str, path: str, token: str, timeout: int = 300) -> Any:
  18. status, body = wb8.request(method, path, token=token, timeout=timeout)
  19. if status < 200 or status >= 300:
  20. raise RuntimeError(f"{method} {path}: HTTP {status} {body}")
  21. return body
  22. def wait_schedule(token: str, run_id: int, timeout_seconds: int) -> dict[str, Any]:
  23. deadline = time.monotonic() + timeout_seconds
  24. while time.monotonic() < deadline:
  25. body = call("GET", f"/api/Production/scheduling/generate-status?runId={run_id}", token)
  26. status = str(value(body, "status", "Status", default="")).upper()
  27. if status in TERMINAL:
  28. return {"runId": run_id, "status": status, "message": value(body, "message", "Message")}
  29. time.sleep(3)
  30. return {"runId": run_id, "status": "TIMEOUT"}
  31. def running_schedule_id(tenant_id: str) -> int:
  32. conn = connect()
  33. try:
  34. with conn.cursor() as cursor:
  35. cursor.execute(
  36. """
  37. SELECT id
  38. FROM aidop_action_run_log
  39. WHERE tenant_id=%s AND action_code='S2_SCHEDULE_GENERATE' AND status='RUNNING'
  40. ORDER BY id DESC
  41. LIMIT 1
  42. """,
  43. (int(tenant_id),),
  44. )
  45. row = cursor.fetchone()
  46. return int(row["id"]) if row else 0
  47. finally:
  48. conn.close()
  49. def wait_rebuild(token: str, module: str, factory_id: str, job_id: int, timeout_seconds: int) -> dict[str, Any]:
  50. deadline = time.monotonic() + timeout_seconds
  51. path = f"/api/AidopKanban/{module}/rebuild-jobs/{job_id}?factoryId={factory_id}"
  52. while time.monotonic() < deadline:
  53. body = call("GET", path, token)
  54. status = str(value(body, "status", "Status", default="")).upper()
  55. if status in TERMINAL:
  56. return {
  57. "jobId": job_id,
  58. "status": status,
  59. "error": value(body, "errorMessage", "ErrorMessage"),
  60. }
  61. time.sleep(5)
  62. return {"jobId": job_id, "status": "TIMEOUT"}
  63. def run(
  64. timeout_seconds: int,
  65. modules: tuple[str, ...],
  66. skip_schedule: bool,
  67. factory_id_override: str | None = None,
  68. tenant_codes: set[str] | None = None,
  69. ) -> dict[str, Any]:
  70. result: dict[str, Any] = {"base": wb8.BASE, "tenants": {}}
  71. sessions: list[tuple[dict[str, Any], str]] = []
  72. selected_tenants = tuple(
  73. tenant for tenant in TENANTS
  74. if not tenant_codes or tenant["code"].upper() in tenant_codes
  75. )
  76. for tenant in selected_tenants:
  77. login = wb8.login(tenant["admin"]["account"], tenant["tenant_id"], PASSWORD)
  78. sessions.append((tenant, login["token"]))
  79. if not skip_schedule:
  80. # Production schedules are globally serialized by the backend gate.
  81. for tenant, token in sessions:
  82. code = tenant["code"]
  83. run_id = running_schedule_id(tenant["tenant_id"])
  84. if not run_id:
  85. body = call(
  86. "POST",
  87. f"/api/Production/scheduling/generate?domain={tenant['tenant_id']}&enableCapacityConstraint=false",
  88. token,
  89. )
  90. run_id = int(value(body, "runId", "RunId", "id", "Id", default=0) or 0)
  91. else:
  92. body = {"result": {"runId": run_id}}
  93. result["tenants"].setdefault(code, {})["schedule"] = (
  94. wait_schedule(token, run_id, timeout_seconds) if run_id else {"status": "NO_RUN_ID", "body": wb8.unwrap(body)}
  95. )
  96. jobs: list[tuple[dict[str, Any], str, str, int, str]] = []
  97. for tenant, token in sessions:
  98. code = tenant["code"]
  99. factory_id = factory_id_override or tenant["factory_id"]
  100. result["tenants"].setdefault(code, {})["rebuilds"] = {}
  101. for module in modules:
  102. status_code, body = wb8.request(
  103. "POST",
  104. f"/api/AidopKanban/{module}/rebuild-jobs?factoryId={factory_id}",
  105. token=token,
  106. )
  107. if status_code not in (200, 201, 202, 409):
  108. raise RuntimeError(f"POST {module} rebuild: HTTP {status_code} {body}")
  109. job_id = int(value(body, "jobId", "JobId", "id", "Id", default=0) or 0)
  110. result["tenants"][code]["rebuilds"][module] = {"jobId": job_id, "status": "QUEUED"}
  111. if job_id:
  112. jobs.append((tenant, token, module, job_id, factory_id))
  113. deadline = time.monotonic() + timeout_seconds
  114. pending = list(jobs)
  115. while pending and time.monotonic() < deadline:
  116. next_pending: list[tuple[dict[str, Any], str, str, int, str]] = []
  117. for tenant, token, module, job_id, factory_id in pending:
  118. path = f"/api/AidopKanban/{module}/rebuild-jobs/{job_id}?factoryId={factory_id}"
  119. body = call("GET", path, token)
  120. status = str(value(body, "status", "Status", default="")).upper()
  121. if status in TERMINAL:
  122. result["tenants"][tenant["code"]]["rebuilds"][module] = {
  123. "jobId": job_id,
  124. "status": status,
  125. "error": value(body, "errorMessage", "ErrorMessage"),
  126. }
  127. else:
  128. next_pending.append((tenant, token, module, job_id, factory_id))
  129. pending = next_pending
  130. if pending:
  131. time.sleep(5)
  132. for tenant, _, module, job_id, _ in pending:
  133. result["tenants"][tenant["code"]]["rebuilds"][module] = {"jobId": job_id, "status": "TIMEOUT"}
  134. return result
  135. def main() -> int:
  136. parser = argparse.ArgumentParser()
  137. parser.add_argument("--base", default="http://127.0.0.1:5005")
  138. parser.add_argument("--timeout-seconds", type=int, default=1800)
  139. parser.add_argument("--modules", default=",".join(MODULES))
  140. parser.add_argument("--skip-schedule", action="store_true")
  141. parser.add_argument("--factory-id", help="override each tenant's KPI factory id")
  142. parser.add_argument("--tenants", default="A,B,DEMO", help="comma-separated tenant codes")
  143. args = parser.parse_args()
  144. wb8.BASE = args.base.rstrip("/")
  145. modules = tuple(x.strip().upper() for x in args.modules.split(",") if x.strip())
  146. invalid = sorted(set(modules) - set(MODULES))
  147. if invalid:
  148. parser.error(f"unsupported modules: {','.join(invalid)}")
  149. tenant_codes = {x.strip().upper() for x in args.tenants.split(",") if x.strip()}
  150. valid_tenant_codes = {tenant["code"].upper() for tenant in TENANTS}
  151. invalid_tenants = sorted(tenant_codes - valid_tenant_codes)
  152. if invalid_tenants:
  153. parser.error(f"unsupported tenants: {','.join(invalid_tenants)}")
  154. result = run(args.timeout_seconds, modules, args.skip_schedule, args.factory_id, tenant_codes)
  155. print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
  156. failed = [
  157. f"{tenant}:{module}:{data['status']}"
  158. for tenant, tenant_data in result["tenants"].items()
  159. for module, data in tenant_data.get("rebuilds", {}).items()
  160. if data.get("status") != "SUCCESS"
  161. ]
  162. return 1 if failed else 0
  163. if __name__ == "__main__":
  164. raise SystemExit(main())