260 lines
6.2 KiB
Bash
Executable File
260 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ACTION="${1:-deploy}"
|
|
GRAFANA_CONTAINER="${GRAFANA_CONTAINER:-mv_ucepsa_grafana}"
|
|
REPO_ROOT="${REGRESSION_DASHBOARD_REPO_ROOT:-/srv/mesavault/40-clients/ucepsa/edge-oee-demo}"
|
|
DASHBOARD_FILE="${REGRESSION_DASHBOARD_FILE:-$REPO_ROOT/grafana/dashboards/ucepsa-shopfloor-context-health.dashboard.json}"
|
|
FOLDER_UID="${REGRESSION_DASHBOARD_FOLDER_UID:-ucepsa-mesavault}"
|
|
DASHBOARD_UID="ucepsa-shopfloor-context-health"
|
|
DATASOURCE_UID="bfnbcasbm6hhca"
|
|
|
|
resolve_url() {
|
|
if [[ -n "${GRAFANA_URL:-}" ]]; then
|
|
printf '%s' "$GRAFANA_URL"
|
|
return
|
|
fi
|
|
|
|
local port_line host_port container_ip
|
|
port_line="$(
|
|
docker port "$GRAFANA_CONTAINER" 3000/tcp 2>/dev/null \
|
|
| head -n 1 || true
|
|
)"
|
|
|
|
if [[ -n "$port_line" ]]; then
|
|
host_port="${port_line##*:}"
|
|
printf 'http://127.0.0.1:%s' "$host_port"
|
|
return
|
|
fi
|
|
|
|
container_ip="$(
|
|
docker inspect "$GRAFANA_CONTAINER" \
|
|
--format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
|
|
)"
|
|
|
|
if [[ -z "$container_ip" ]]; then
|
|
echo "ERROR: no se pudo resolver Grafana." >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'http://%s:3000' "$container_ip"
|
|
}
|
|
|
|
if [[ -z "${GRAFANA_API_TOKEN:-}" ]]; then
|
|
echo "ERROR: falta GRAFANA_API_TOKEN." >&2
|
|
exit 1
|
|
fi
|
|
|
|
export GRAFANA_URL_RESOLVED
|
|
GRAFANA_URL_RESOLVED="$(resolve_url)"
|
|
|
|
python3 - \
|
|
"$ACTION" \
|
|
"$DASHBOARD_FILE" \
|
|
"$FOLDER_UID" \
|
|
"$DASHBOARD_UID" \
|
|
"$DATASOURCE_UID" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
action, dashboard_file, folder_uid, dashboard_uid, datasource_uid = sys.argv[1:]
|
|
base_url = os.environ["GRAFANA_URL_RESOLVED"].rstrip("/")
|
|
token = os.environ["GRAFANA_API_TOKEN"]
|
|
|
|
|
|
def request(method, path, payload=None):
|
|
body = None
|
|
headers = {
|
|
"Accept": "application/json",
|
|
"Authorization": f"Bearer {token}",
|
|
}
|
|
|
|
if payload is not None:
|
|
body = json.dumps(
|
|
payload,
|
|
ensure_ascii=False,
|
|
).encode("utf-8")
|
|
headers["Content-Type"] = "application/json"
|
|
|
|
req = urllib.request.Request(
|
|
base_url + path,
|
|
data=body,
|
|
headers=headers,
|
|
method=method,
|
|
)
|
|
|
|
try:
|
|
with urllib.request.urlopen(
|
|
req,
|
|
timeout=20,
|
|
) as response:
|
|
raw = response.read()
|
|
return (
|
|
json.loads(raw.decode("utf-8"))
|
|
if raw
|
|
else None
|
|
)
|
|
except urllib.error.HTTPError as exc:
|
|
detail = exc.read().decode(
|
|
"utf-8",
|
|
errors="replace",
|
|
)
|
|
raise RuntimeError(
|
|
f"Grafana API {method} {path}: "
|
|
f"HTTP {exc.code}: {detail}"
|
|
) from exc
|
|
|
|
|
|
health = request("GET", "/api/health")
|
|
if not health or health.get("database") != "ok":
|
|
raise RuntimeError(
|
|
f"Grafana no está saludable: {health!r}"
|
|
)
|
|
|
|
if action == "deploy":
|
|
with open(
|
|
dashboard_file,
|
|
encoding="utf-8",
|
|
) as handle:
|
|
dashboard = json.load(handle)
|
|
|
|
result = request(
|
|
"POST",
|
|
"/api/dashboards/db",
|
|
{
|
|
"dashboard": dashboard,
|
|
"folderUid": folder_uid,
|
|
"overwrite": True,
|
|
"message": (
|
|
"MESAVAULT regression hardening "
|
|
"SHADOW v0.3.13"
|
|
),
|
|
},
|
|
)
|
|
print(json.dumps(
|
|
{
|
|
"status": "deployed",
|
|
"dashboard_uid": dashboard_uid,
|
|
"folder_uid": folder_uid,
|
|
"response": result,
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
))
|
|
|
|
elif action == "validate":
|
|
result = request(
|
|
"GET",
|
|
"/api/dashboards/uid/"
|
|
+ urllib.parse.quote(dashboard_uid),
|
|
)
|
|
dashboard = result["dashboard"]
|
|
panels = {
|
|
panel.get("id"): panel
|
|
for panel in dashboard.get("panels", [])
|
|
}
|
|
|
|
datasource_uids = sorted({
|
|
panel.get("datasource", {}).get("uid")
|
|
for panel in dashboard.get("panels", [])
|
|
if isinstance(
|
|
panel.get("datasource"),
|
|
dict,
|
|
)
|
|
and panel.get(
|
|
"datasource",
|
|
{},
|
|
).get("uid")
|
|
})
|
|
|
|
errors = []
|
|
|
|
if len(dashboard.get("panels", [])) != 31:
|
|
errors.append(
|
|
"El dashboard no tiene 31 paneles"
|
|
)
|
|
|
|
if dashboard.get("refresh") != "15s":
|
|
errors.append(
|
|
"El refresco no está en 15s"
|
|
)
|
|
|
|
if datasource_uids != [datasource_uid]:
|
|
errors.append(
|
|
f"Datasource inesperado: {datasource_uids!r}"
|
|
)
|
|
|
|
expected = {
|
|
29: "li_context_regression_latest_run_v1",
|
|
30: "li_context_regression_failures_v1",
|
|
31: "li_context_regression_run_history_v1",
|
|
}
|
|
|
|
for panel_id, fragment in expected.items():
|
|
sql = (
|
|
panels.get(panel_id, {})
|
|
.get("targets", [{}])[0]
|
|
.get("rawSql", "")
|
|
)
|
|
if fragment not in sql:
|
|
errors.append(
|
|
f"Panel {panel_id} no consulta {fragment}"
|
|
)
|
|
|
|
output = {
|
|
"status":
|
|
"ok" if not errors else "error",
|
|
"dashboard_uid":
|
|
dashboard.get("uid"),
|
|
"folder_uid":
|
|
result.get(
|
|
"meta",
|
|
{},
|
|
).get("folderUid"),
|
|
"panel_count":
|
|
len(
|
|
dashboard.get(
|
|
"panels",
|
|
[],
|
|
)
|
|
),
|
|
"refresh":
|
|
dashboard.get("refresh"),
|
|
"datasource_uids":
|
|
datasource_uids,
|
|
"version":
|
|
dashboard.get("version"),
|
|
"url":
|
|
result.get(
|
|
"meta",
|
|
{},
|
|
).get("url"),
|
|
"regression_panels": {
|
|
"latest_run": 29 in panels,
|
|
"issues": 30 in panels,
|
|
"history": 31 in panels,
|
|
},
|
|
"errors":
|
|
errors,
|
|
}
|
|
|
|
print(json.dumps(
|
|
output,
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
))
|
|
|
|
if errors:
|
|
raise SystemExit(1)
|
|
|
|
else:
|
|
raise SystemExit(
|
|
"Uso: deploy_context_regression_dashboard_v0313.sh "
|
|
"{deploy|validate}"
|
|
)
|
|
PY
|