feat(maintenance): ui/ux audit improvements for BI analyst persona
- read-only access to /maintenance for analysts (sidebar + hidden management) - hub badge: message tooltip, link to events, accessible aria-label, localized end - keep hub badge fresh via shared maintenance WS (init on dashboards page) - events table: message column, auto-end indicator, localized statuses - confirm dialog before starting maintenance with affected-dashboards summary - surface load errors inline; localize store toasts - auto-end discoverability hints in form and table - form: multiple tables, end>start validation, timezone note - status colors: active -> warning; completed tab dashboards expandable - settings: timezone select, fieldset, localized units/aria labels - backend: expose auto_end in event items, message in banner states
This commit is contained in:
389
examples/maintenance/maintenance-api-bash.sh
Executable file
389
examples/maintenance/maintenance-api-bash.sh
Executable file
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Example: External tool triggers maintenance via superset-tools API (bash)
|
||||
#
|
||||
# This script shows how to call the superset-tools maintenance API from any
|
||||
# shell environment — CI/CD pipeline, cron job, or ad-hoc debugging.
|
||||
#
|
||||
# ============================================================================
|
||||
# API SPECIFICATION / СПЕЦИФИКАЦИЯ API ОБСЛУЖИВАНИЯ
|
||||
# ============================================================================
|
||||
# Базовый URL: {BASE_URL}/api/maintenance
|
||||
# Аутентификация: заголовок X-API-Key: <API_KEY> (или JWT-токен)
|
||||
#
|
||||
# КЛЮЧ API — РАЗРЕШЕНИЯ (permissions), которые должны быть у ключа:
|
||||
# maintenance:start — создание события обслуживания (POST /start)
|
||||
# maintenance:end — завершение конкретного события (POST /{id}/end)
|
||||
# maintenance:end_all — массовое завершение всех событий (POST /end-all)
|
||||
#
|
||||
# Все ответы приходят в конверте-обёртке {status, data, error, meta}.
|
||||
# Все мутационные (изменяющие) эндпоинты возвращают HTTP 202 и task_id —
|
||||
# операция выполняется асинхронно через планировщик (TaskManager).
|
||||
#
|
||||
# ----------------------------------------------------------------------------
|
||||
# 1) POST /api/maintenance/start — начать обслуживание таблиц
|
||||
# HTTP 202 Accepted (задача создана)
|
||||
# Разрешение: maintenance:start (или RBAC maintenance / WRITE)
|
||||
#
|
||||
# Тело запроса (JSON):
|
||||
# tables: [string] ОБЯЗАТЕЛЬНО. Список таблиц (1..100).
|
||||
# start_time: datetime ОБЯЗАТЕЛЬНО. Начало окна (ISO 8601).
|
||||
# end_time: datetime|optional Окончание окна; должно быть
|
||||
# позже start_time (необязательно).
|
||||
# auto_end: bool По умолчанию false. Если true и задан end_time,
|
||||
# планировщик сам завершит обслуживание в end_time.
|
||||
# message: string|optional Текст баннера (макс. 500 симв.).
|
||||
# environment_id: string ОБЯЗАТЕЛЬНО. Целевое окружение (ss-dev, ss-prod…).
|
||||
#
|
||||
# Ответ 202:
|
||||
# { "task_id": "…", "maintenance_id": "…", "status": "pending" }
|
||||
#
|
||||
# Ошибки:
|
||||
# 400 — невалидные данные (end_time <= start_time; start_time слишком в
|
||||
# прошлом — допуск 1 час; > 100 таблиц);
|
||||
# 401 — неверный/отозванный API-ключ;
|
||||
# 403 — недостаточно прав;
|
||||
# 404 — неизвестное environment_id;
|
||||
# 409 — идемпотентность: такое же (tables, start_time, end_time) уже активно:
|
||||
# { "maintenance_id": "…", "status": "already_active" }.
|
||||
#
|
||||
# ----------------------------------------------------------------------------
|
||||
# 2) POST /api/maintenance/{maintenance_id}/end — завершить конкретное событие
|
||||
# HTTP 202 Accepted
|
||||
# Разрешение: maintenance:end (или RBAC maintenance / WRITE)
|
||||
# Тело: отсутствует.
|
||||
# Ответ 202: { "task_id": "…", "status": "pending" }
|
||||
# Ошибки:
|
||||
# 404 — событие не найдено; если status == "already_completed" — событие
|
||||
# уже завершено (идемпотентно, считается успехом);
|
||||
# 401/403 — аутентификация/права.
|
||||
#
|
||||
# ----------------------------------------------------------------------------
|
||||
# 3) POST /api/maintenance/end-all — завершить ВСЕ активные события
|
||||
# HTTP 202 Accepted (ОСТОРОЖНО: снимает баннеры со всех дашбордов!)
|
||||
# Разрешение: maintenance:end_all (или RBAC maintenance / WRITE)
|
||||
# Тело (JSON, опционально): { "environment_id": "…" }
|
||||
# Если не указан — используется окружение из ключа API.
|
||||
# Ответ 202: { "task_id": "…", "status": "pending" }
|
||||
#
|
||||
# ----------------------------------------------------------------------------
|
||||
# Прочие (read-only) эндпоинты того же модуля (для справки, требуют READ):
|
||||
# GET /api/maintenance/events — списки active и completed событий
|
||||
# GET /api/maintenance/dashboard-banners — состояние баннеров по дашбордам
|
||||
# POST /api/maintenance/preview-dashboards — какие дашборды затронут таблицы
|
||||
# GET /api/maintenance/settings — настройки обслуживания
|
||||
# PUT /api/maintenance/settings — обновить настройки (только admin)
|
||||
#
|
||||
# ============================================================================
|
||||
# ИНСТРУКЦИЯ ПО ИСПОЛЬЗОВАНИЮ (РУССКИЙ)
|
||||
# ============================================================================
|
||||
# Назначение: ставить и снимать баннер «ведутся технические работы» на
|
||||
# дашбордах Superset при обновлении таблиц (ETL, cron, CI/CD).
|
||||
#
|
||||
# ПРЕДВАРИТЕЛЬНЫЕ ШАГИ:
|
||||
# 1. Убедитесь, что установлены curl и (желательно) jq.
|
||||
# 2. Создайте API-ключ superset-tools с нужными разрешениями
|
||||
# (maintenance:start / maintenance:end / maintenance:end_all).
|
||||
# 3. Задайте переменные окружения:
|
||||
# export SS_TOOLS_URL=http://localhost:8000
|
||||
# export SS_TOOLS_API_KEY=ssk_ваш_ключ
|
||||
# (или замените значения прямо в скрипте).
|
||||
#
|
||||
# КОМАНДЫ:
|
||||
# Запустить обслуживание на 4 часа:
|
||||
# ./maintenance-api-bash.sh start public.messages 4 ss-dev
|
||||
# Несколько таблиц через запятую, с сообщением и автоснятием баннера:
|
||||
# ./maintenance-api-bash.sh start public.messages,public.users 4 ss-prod \
|
||||
# "Плановый ETL" --auto-end
|
||||
# Завершить конкретное событие:
|
||||
# ./maintenance-api-bash.sh end m-abc123
|
||||
# Аварийно снять ВСЕ баннеры в окружении ss-dev:
|
||||
# ./maintenance-api-bash.sh end-all ss-dev
|
||||
#
|
||||
# О ПАРАМЕТРЕ --auto-end:
|
||||
# start_time и end_time вычисляются автоматически (сейчас и сейчас+часы).
|
||||
# Без --auto-end end_time носит справочный характер — баннер нужно снять
|
||||
# вручную командой `end`. С --auto-end баннер снимут автоматически ровно
|
||||
# в end_time.
|
||||
#
|
||||
# ВЫХОДНЫЕ КОДЫ: 0 — успех; 1 — ошибка (неверные данные, отсутствие прав,
|
||||
# неверный ключ, недоступность сервера).
|
||||
#
|
||||
# ЗАМЕЧАНИЯ ПО БЕЗОПАСНОСТИ:
|
||||
# - Не передавайте ключ API в аргументах командной строки (попадает в
|
||||
# историю shell/ps). Используйте переменные окружения.
|
||||
# - `end-all` подтверждается в интерактивном режиме; в неинтерактивных
|
||||
# окружениях (cron/CI) запрос подтверждения пропускается.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────
|
||||
# Set these via environment variables or replace inline
|
||||
BASE_URL="${SS_TOOLS_URL:-http://localhost:8000}"
|
||||
API_KEY="${SS_TOOLS_API_KEY:-}"
|
||||
|
||||
# ── Auto-end flag (opt-in automatic ending at end_time) ───────────────────
|
||||
# Pass `--auto-end` to `start`: the banner is removed automatically at end_time.
|
||||
# Without it, end_time is informational only and maintenance must be ended manually.
|
||||
AUTO_END=0
|
||||
|
||||
# ── Color helpers (disabled in non-TTY) ────────────────────────────────────
|
||||
if [[ -t 1 ]]; then
|
||||
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||
else
|
||||
GREEN=''; RED=''; YELLOW=''; CYAN=''; BOLD=''; NC=''
|
||||
fi
|
||||
|
||||
ok() { echo -e "${GREEN}✅${NC} $*"; }
|
||||
info() { echo -e "${CYAN}ℹ️ ${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}⚠️ ${NC} $*"; }
|
||||
fail() { echo -e "${RED}❌${NC} $*"; exit 1; }
|
||||
|
||||
# ── Auth check ─────────────────────────────────────────────────────────────
|
||||
check_auth() {
|
||||
if [[ -z "$API_KEY" ]]; then
|
||||
fail "API key not set. Export SS_TOOLS_API_KEY or edit the script."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── API helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
# Call the API with X-API-Key auth and handle common errors
|
||||
api_call() {
|
||||
local method="$1" # GET / POST
|
||||
local endpoint="$2" # /api/maintenance/start
|
||||
local data="$3" # JSON payload or empty string
|
||||
|
||||
local curl_args=(
|
||||
-X "$method"
|
||||
-sS
|
||||
-H "X-API-Key: $API_KEY"
|
||||
-H "Content-Type: application/json"
|
||||
)
|
||||
|
||||
if [[ -n "$data" ]]; then
|
||||
curl_args+=(-d "$data")
|
||||
fi
|
||||
|
||||
local http_code
|
||||
local response_file
|
||||
response_file=$(mktemp)
|
||||
|
||||
http_code=$(curl -w '%{http_code}' "${curl_args[@]}" \
|
||||
"${BASE_URL}${endpoint}" \
|
||||
-o "$response_file" 2>/dev/null)
|
||||
|
||||
local body
|
||||
body=$(<"$response_file")
|
||||
rm -f "$response_file"
|
||||
|
||||
case "$http_code" in
|
||||
202)
|
||||
echo "$body" # success
|
||||
return 0
|
||||
;;
|
||||
400)
|
||||
warn "Validation error:"
|
||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
||||
return 1
|
||||
;;
|
||||
401)
|
||||
fail "Authentication failed: invalid or revoked API key"
|
||||
;;
|
||||
403)
|
||||
fail "Permission denied: API key lacks required permission"
|
||||
;;
|
||||
404)
|
||||
local status
|
||||
status=$(echo "$body" | jq -r '.status // "not_found"' 2>/dev/null)
|
||||
if [[ "$status" == "already_completed" ]]; then
|
||||
info "Event was already completed (idempotent)"
|
||||
return 0
|
||||
fi
|
||||
warn "Not found (404):"
|
||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
||||
return 1
|
||||
;;
|
||||
409)
|
||||
local status
|
||||
status=$(echo "$body" | jq -r '.status // ""' 2>/dev/null)
|
||||
if [[ "$status" == "already_active" ]]; then
|
||||
info "Event already active (idempotent)"
|
||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
||||
return 0
|
||||
fi
|
||||
warn "Conflict (409):"
|
||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
fail "Unexpected HTTP $http_code: $(echo "$body" | head -c 500)"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Commands ───────────────────────────────────────────────────────────────
|
||||
|
||||
cmd_start() {
|
||||
local tables="$1"
|
||||
local duration_hours="${2:-4}"
|
||||
local environment_id="$3"
|
||||
local message="${4:-}"
|
||||
|
||||
local start_time
|
||||
local end_time
|
||||
start_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
end_time=$(date -u -d "+${duration_hours} hours" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|
||||
|| date -u -v "+${duration_hours}H" +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Build JSON payload
|
||||
local payload
|
||||
payload=$(cat <<EOF
|
||||
{
|
||||
"tables": [$(echo "$tables" | sed 's/[^,]*/"&"/g')],
|
||||
"start_time": "$start_time",
|
||||
"end_time": "$end_time",
|
||||
"environment_id": "$environment_id"
|
||||
EOF
|
||||
)
|
||||
if [[ "$AUTO_END" -eq 1 ]]; then
|
||||
payload="$payload"$',\n "auto_end": true'
|
||||
info "Auto-end enabled: banner will be removed at ${end_time}"
|
||||
fi
|
||||
if [[ -n "$message" ]]; then
|
||||
payload="$payload"$',\n "message": "'"$message"'"'
|
||||
fi
|
||||
payload="$payload"$'\n}'
|
||||
|
||||
info "Starting maintenance: tables=${tables}, env=${environment_id}, ${duration_hours}h"
|
||||
info "Window: ${start_time} → ${end_time}"
|
||||
|
||||
local response
|
||||
if response=$(api_call POST "/api/maintenance/start" "$payload"); then
|
||||
local event_id
|
||||
event_id=$(echo "$response" | jq -r '.maintenance_id' 2>/dev/null || echo "unknown")
|
||||
local task_id
|
||||
task_id=$(echo "$response" | jq -r '.task_id' 2>/dev/null || echo "unknown")
|
||||
ok "Maintenance event created: ${BOLD}${event_id}${NC}"
|
||||
info "Task: ${task_id}"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_end() {
|
||||
local event_id="$1"
|
||||
|
||||
info "Ending maintenance event: ${event_id}"
|
||||
|
||||
local response
|
||||
if response=$(api_call POST "/api/maintenance/${event_id}/end" ""); then
|
||||
ok "Maintenance event end scheduled"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_end_all() {
|
||||
local environment_id="${1:-}"
|
||||
|
||||
warn "This will remove banners from ALL affected dashboards!"
|
||||
if [[ -n "$environment_id" ]]; then
|
||||
info "Scope: environment=${environment_id}"
|
||||
else
|
||||
info "Scope: all environments (API key default)"
|
||||
fi
|
||||
|
||||
# Prompt for confirmation in interactive mode
|
||||
if [[ -t 0 ]]; then
|
||||
echo -n "Continue? [y/N] "
|
||||
read -r confirm
|
||||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||||
info "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local payload="{}"
|
||||
if [[ -n "$environment_id" ]]; then
|
||||
payload=$(cat <<EOF
|
||||
{"environment_id": "$environment_id"}
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
|
||||
local response
|
||||
if response=$(api_call POST "/api/maintenance/end-all" "$payload"); then
|
||||
ok "End-all scheduled"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
check_auth
|
||||
|
||||
# Filter the --auto-end flag out of the positional args
|
||||
ARGS=()
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--auto-end" ]]; then
|
||||
AUTO_END=1
|
||||
else
|
||||
ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
set -- "${ARGS[@]}"
|
||||
|
||||
case "${1:-help}" in
|
||||
start)
|
||||
if [[ $# -lt 3 ]]; then
|
||||
echo "Usage: $0 start <tables> [duration_hours] <environment> [message] [--auto-end]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 start public.messages 4 ss-dev"
|
||||
echo " $0 start public.messages,public.users 4 ss-prod \"Scheduled ETL\" --auto-end"
|
||||
echo ""
|
||||
echo "--auto-end: also pass auto_end=true so the banner is removed automatically"
|
||||
echo " at end_time. Without it, end_time is informational only."
|
||||
exit 1
|
||||
fi
|
||||
cmd_start "$2" "${3:-4}" "${4:-}" "${5:-}"
|
||||
;;
|
||||
end)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Usage: $0 end <event-id>"
|
||||
exit 1
|
||||
fi
|
||||
cmd_end "$2"
|
||||
;;
|
||||
end-all)
|
||||
cmd_end_all "${2:-}"
|
||||
;;
|
||||
*)
|
||||
cat <<EOF
|
||||
superset-tools Maintenance CLI
|
||||
|
||||
Usage:
|
||||
$0 start <tables> [hours=4] <environment> [message] [--auto-end]
|
||||
Start maintenance on tables (comma-separated).
|
||||
--auto-end also removes the banner automatically at end_time (API param auto_end=true).
|
||||
|
||||
$0 end <event-id>
|
||||
End a specific maintenance event
|
||||
|
||||
$0 end-all [environment]
|
||||
End ALL maintenance (use with caution)
|
||||
|
||||
Examples:
|
||||
$0 start public.messages 4 ss-dev
|
||||
$0 start public.messages 4 ss-dev "ETL refresh" --auto-end
|
||||
$0 end m-abc123
|
||||
$0 end-all ss-dev
|
||||
|
||||
Environment:
|
||||
SS_TOOLS_URL superset-tools base URL (default: http://localhost:8000)
|
||||
SS_TOOLS_API_KEY API key with required permissions (required)
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
461
examples/maintenance/maintenance-api-python.py
Normal file
461
examples/maintenance/maintenance-api-python.py
Normal file
@@ -0,0 +1,461 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: External tool triggers maintenance via superset-tools API.
|
||||
|
||||
This script demonstrates how an ETL pipeline (Airflow DAG, cron job, CI/CD)
|
||||
can start and end maintenance banners on Superset dashboards using the
|
||||
superset-tools API Key authentication.
|
||||
|
||||
Requirements: Python 3.10+, requests (pip install requests)
|
||||
|
||||
Usage:
|
||||
# Start maintenance
|
||||
python maintenance-api-python.py start \
|
||||
--api-key ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3 \
|
||||
--base-url https://superset-tools.example.com \
|
||||
--tables public.messages,public.users \
|
||||
--duration-hours 4 \
|
||||
--message "Scheduled ETL refresh"
|
||||
|
||||
# Same, but auto-end the maintenance at end_time (API param auto_end=true)
|
||||
python maintenance-api-python.py start \
|
||||
--api-key ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3 \
|
||||
--base-url https://superset-tools.example.com \
|
||||
--tables public.messages \
|
||||
--duration-hours 4 \
|
||||
--auto-end
|
||||
|
||||
# End maintenance by event ID
|
||||
python maintenance-api-python.py end \
|
||||
--api-key ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3 \
|
||||
--base-url https://superset-tools.example.com \
|
||||
--event-id m-abc123
|
||||
|
||||
# End ALL maintenance (use with caution!)
|
||||
python maintenance-api-python.py end-all \
|
||||
--api-key ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3 \
|
||||
--base-url https://superset-tools.example.com \
|
||||
--environment ss-dev
|
||||
|
||||
=============================== API SPECIFICATION ============================
|
||||
СПЕЦИФИКАЦИЯ API ОБСЛУЖИВАНИЯ
|
||||
--------------------------------
|
||||
Базовый URL: {BASE_URL}/api/maintenance
|
||||
Аутентификация: заголовок X-API-Key: <API_KEY> (или JWT-токен)
|
||||
|
||||
РАЗРЕШЕНИЯ API-КЛЮЧА (permissions), требуемые скриптом:
|
||||
maintenance:start — создание события (POST /start)
|
||||
maintenance:end — завершение конкретного события (POST /{id}/end)
|
||||
maintenance:end_all — массовое завершение всех событий (POST /end-all)
|
||||
|
||||
Все ответы приходят в обёртке {status, data, error, meta}.
|
||||
Все мутационные эндпоинты возвращают HTTP 202 и task_id — операция
|
||||
выполняется асинхронно планировщиком (TaskManager).
|
||||
|
||||
--- POST /api/maintenance/start ---
|
||||
HTTP 202 Accepted | Разрешение: maintenance:start
|
||||
Тело запроса (JSON):
|
||||
tables [string] ОБЯЗАТЕЛЬНО. Список таблиц (1..100).
|
||||
start_time datetime ОБЯЗАТЕЛЬНО. Начало окна (ISO 8601).
|
||||
end_time datetime Опционально. Должно быть позже start_time.
|
||||
auto_end bool По умолчанию false. Если true и задан end_time,
|
||||
планировщик сам завершит обслуживание в end_time.
|
||||
message string Опционально. Текст баннера (макс. 500 симв.).
|
||||
environment_id string ОБЯЗАТЕЛЬНО. Целевое окружение (ss-dev и т.п.).
|
||||
Ответ 202: { "task_id": "...", "maintenance_id": "...", "status": "pending" }
|
||||
Ошибки:
|
||||
400 — невалидные данные (end_time <= start_time; start_time слишком в
|
||||
прошлом, допуск 1 час; > 100 таблиц);
|
||||
401 — неверный/отозванный ключ;
|
||||
403 — недостаточно прав;
|
||||
404 — неизвестное environment_id;
|
||||
409 — идемпотентность (такое же tables/start_time/end_time уже активно):
|
||||
{ "maintenance_id": "...", "status": "already_active" }.
|
||||
|
||||
--- POST /api/maintenance/{maintenance_id}/end ---
|
||||
HTTP 202 Accepted | Разрешение: maintenance:end | Тело: отсутствует.
|
||||
Ответ 202: { "task_id": "...", "status": "pending" }
|
||||
Ошибки:
|
||||
404 — событие не найдено; если status == "already_completed" — уже
|
||||
завершено (идемпотентно, успех);
|
||||
401/403 — аутентификация/права.
|
||||
|
||||
--- POST /api/maintenance/end-all ---
|
||||
HTTP 202 Accepted | Разрешение: maintenance:end_all
|
||||
ОСТОРОЖНО: снимает баннеры со ВСЕХ дашбордов.
|
||||
Тело (JSON, опционально): { "environment_id": "..." }
|
||||
Если не указан — используется окружение из ключа API.
|
||||
Ответ 202: { "task_id": "...", "status": "pending" }
|
||||
|
||||
--- Прочие (read-only) эндпоинты модуля, требуют READ ---
|
||||
GET /api/maintenance/events — списки active и completed событий
|
||||
GET /api/maintenance/dashboard-banners — состояние баннеров по дашбордам
|
||||
POST /api/maintenance/preview-dashboards — какие дашборды затронут таблицы
|
||||
GET /api/maintenance/settings — настройки обслуживания
|
||||
PUT /api/maintenance/settings — обновить настройки (только admin)
|
||||
|
||||
=========================== ИНСТРУКЦИЯ (РУССКИЙ) ==============================
|
||||
Назначение: ставить и снимать баннер «ведутся технические работы» на
|
||||
дашбордах Superset при обновлении таблиц (ETL, cron, CI/CD).
|
||||
|
||||
ПРЕДВАРИТЕЛЬНЫЕ ШАГИ:
|
||||
1. Установите зависимости: pip install requests
|
||||
2. Создайте API-ключ superset-tools с нужными разрешениями
|
||||
(maintenance:start / maintenance:end / maintenance:end_all).
|
||||
3. Укажите --base-url (адрес сервера) и --api-key.
|
||||
|
||||
КОМАНДЫ:
|
||||
Обслуживание public.messages на 4 часа в ss-dev:
|
||||
python maintenance-api-python.py start \
|
||||
--api-key ssk_... --base-url https://superset-tools.example.com \
|
||||
--tables public.messages --environment ss-dev --duration-hours 4
|
||||
Несколько таблиц, с сообщением и автоснятием баннера:
|
||||
python maintenance-api-python.py start \
|
||||
--api-key ssk_... --tables public.messages,public.users \
|
||||
--environment ss-prod --duration-hours 4 \
|
||||
--message "Плановый ETL" --auto-end
|
||||
Завершить конкретное событие:
|
||||
python maintenance-api-python.py end \
|
||||
--api-key ssk_... --event-id m-abc123
|
||||
Аварийно снять ВСЕ баннеры в ss-dev:
|
||||
python maintenance-api-python.py end-all \
|
||||
--api-key ssk_... --environment ss-dev
|
||||
|
||||
О ПАРАМЕТРЕ --auto-end:
|
||||
start_time и end_time вычисляются автоматически (сейчас и сейчас+часы).
|
||||
Без --auto-end end_time справочный — баннер снимают вручную командой `end`.
|
||||
С --auto-end баннер снимут автоматически ровно в end_time.
|
||||
|
||||
ВЫХОДНОЙ КОД: 0 — успех; 1 — ошибка (HTTPError от сервера).
|
||||
|
||||
ЗАМЕЧАНИЯ ПО БЕЗОПАСНОСТИ:
|
||||
- Не передавайте ключ API через аргументы в реальных CI/cron, если это
|
||||
небезопасно; используйте защищённые переменные окружения/секреты.
|
||||
- `end-all` снимает баннеры со всех дашбордов — используйте с осторожностью.
|
||||
================================================================================
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Error: 'requests' library is required. Install with: pip install requests")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
API_KEY_HEADER = "X-API-Key"
|
||||
|
||||
|
||||
def start_maintenance(base_url: str, api_key: str, tables: list[str],
|
||||
environment_id: str, duration_hours: int = 4,
|
||||
message: str | None = None, auto_end: bool = False,
|
||||
timeout: int = 30) -> dict:
|
||||
"""
|
||||
Start a maintenance event.
|
||||
|
||||
Args:
|
||||
base_url: superset-tools base URL (e.g. https://superset-tools.example.com)
|
||||
api_key: API key with 'maintenance:start' permission
|
||||
tables: List of table names to flag (e.g. ["public.messages"])
|
||||
environment_id: Superset environment (e.g. "ss-dev", "ss-prod")
|
||||
duration_hours: How long the maintenance window lasts
|
||||
message: Optional custom message for the banner
|
||||
auto_end: When True, sends auto_end=true so the scheduler removes the banner
|
||||
automatically at end_time. When False (default), end_time is informational
|
||||
only and maintenance must be ended manually via `end`.
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
API response dict with task_id and maintenance_id
|
||||
|
||||
Raises:
|
||||
requests.HTTPError: On 4xx/5xx response with error details
|
||||
|
||||
Note:
|
||||
Idempotent retry: identical (tables, start_time, end_time) against an active
|
||||
event returns 409 with status='already_active' and the existing maintenance_id
|
||||
— treated as success here.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
start_time = now.isoformat()
|
||||
end_time = (now + timedelta(hours=duration_hours)).isoformat()
|
||||
|
||||
payload = {
|
||||
"tables": tables,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"environment_id": environment_id,
|
||||
}
|
||||
if auto_end:
|
||||
payload["auto_end"] = True
|
||||
if message:
|
||||
payload["message"] = message
|
||||
|
||||
print(f"[maintenance] Starting maintenance for tables: {tables}")
|
||||
print(f"[maintenance] Window: {start_time} → {end_time}")
|
||||
print(f"[maintenance] Environment: {environment_id}")
|
||||
if auto_end:
|
||||
print("[maintenance] Auto-end: ENABLED (banner will be removed at end_time)")
|
||||
|
||||
response = requests.post(
|
||||
f"{base_url}/api/maintenance/start",
|
||||
json=payload,
|
||||
headers={API_KEY_HEADER: api_key},
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if response.status_code == 202:
|
||||
result = response.json()
|
||||
print(f"[maintenance] ✅ Event created (id={result['maintenance_id']})")
|
||||
print(f"[maintenance] Task: {result.get('task_id', 'N/A')}")
|
||||
return result
|
||||
|
||||
if response.status_code == 409:
|
||||
result = response.json()
|
||||
if result.get("status") == "already_active":
|
||||
print(f"[maintenance] ℹ️ Event already active (idempotent, id={result.get('maintenance_id', 'N/A')})")
|
||||
return result
|
||||
|
||||
if response.status_code == 400:
|
||||
error = _format_error(response)
|
||||
print(f"[maintenance] ❌ Validation error: {error}")
|
||||
elif response.status_code == 401:
|
||||
print("[maintenance] ❌ Authentication failed: invalid or revoked API key")
|
||||
elif response.status_code == 403:
|
||||
print("[maintenance] ❌ Permission denied: API key lacks 'maintenance:start'")
|
||||
else:
|
||||
print(f"[maintenance] ❌ Unexpected error ({response.status_code}): {response.text[:500]}")
|
||||
|
||||
response.raise_for_status()
|
||||
return {} # unreachable
|
||||
|
||||
|
||||
def end_maintenance(base_url: str, api_key: str, event_id: str,
|
||||
timeout: int = 30) -> dict:
|
||||
"""
|
||||
End a specific maintenance event and remove its banners.
|
||||
|
||||
Args:
|
||||
base_url: superset-tools base URL
|
||||
api_key: API key with 'maintenance:end' permission
|
||||
event_id: The event ID returned by start_maintenance
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
API response dict
|
||||
|
||||
Raises:
|
||||
requests.HTTPError: On error
|
||||
"""
|
||||
print(f"[maintenance] Ending maintenance event: {event_id}")
|
||||
|
||||
response = requests.post(
|
||||
f"{base_url}/api/maintenance/{event_id}/end",
|
||||
headers={API_KEY_HEADER: api_key},
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if response.status_code == 202:
|
||||
result = response.json()
|
||||
print(f"[maintenance] ✅ Event end scheduled (task={result.get('task_id', 'N/A')})")
|
||||
return result
|
||||
|
||||
if response.status_code == 404:
|
||||
data = response.json()
|
||||
if data.get("status") == "already_completed":
|
||||
print("[maintenance] ℹ️ Event was already completed (idempotent)")
|
||||
return data
|
||||
print(f"[maintenance] ❌ Event not found: {event_id}")
|
||||
elif response.status_code == 401:
|
||||
print("[maintenance] ❌ Authentication failed: invalid or revoked API key")
|
||||
elif response.status_code == 403:
|
||||
print("[maintenance] ❌ Permission denied: API key lacks 'maintenance:end'")
|
||||
else:
|
||||
print(f"[maintenance] ❌ Unexpected error ({response.status_code}): {response.text[:500]}")
|
||||
|
||||
response.raise_for_status()
|
||||
return {}
|
||||
|
||||
|
||||
def end_all_maintenance(base_url: str, api_key: str,
|
||||
environment_id: str | None = None,
|
||||
timeout: int = 30) -> dict:
|
||||
"""
|
||||
End ALL active maintenance events in the given environment.
|
||||
USE WITH CAUTION — this removes banners from all dashboards.
|
||||
|
||||
Args:
|
||||
base_url: superset-tools base URL
|
||||
api_key: API key with 'maintenance:end_all' permission
|
||||
environment_id: If set, only end events in this environment.
|
||||
If None, uses the API key's default scope.
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
API response dict
|
||||
|
||||
Raises:
|
||||
requests.HTTPError: On error
|
||||
"""
|
||||
print(f"[maintenance] Ending ALL maintenance events "
|
||||
f"(env={environment_id or 'key-scope'})")
|
||||
print("[maintenance] ⚠️ This will remove banners from ALL affected dashboards!")
|
||||
|
||||
payload = {}
|
||||
if environment_id:
|
||||
payload["environment_id"] = environment_id
|
||||
|
||||
response = requests.post(
|
||||
f"{base_url}/api/maintenance/end-all",
|
||||
json=payload,
|
||||
headers={API_KEY_HEADER: api_key},
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if response.status_code == 202:
|
||||
result = response.json()
|
||||
print(f"[maintenance] ✅ All events end scheduled (task={result.get('task_id', 'N/A')})")
|
||||
return result
|
||||
|
||||
if response.status_code == 401:
|
||||
print("[maintenance] ❌ Authentication failed: invalid or revoked API key")
|
||||
elif response.status_code == 403:
|
||||
print("[maintenance] ❌ Permission denied: API key lacks 'maintenance:end_all'")
|
||||
elif response.status_code == 400:
|
||||
print(f"[maintenance] ❌ {_format_error(response)}")
|
||||
else:
|
||||
print(f"[maintenance] ❌ Unexpected error ({response.status_code}): {response.text[:500]}")
|
||||
|
||||
response.raise_for_status()
|
||||
return {}
|
||||
|
||||
|
||||
def _format_error(response: requests.Response) -> str:
|
||||
"""Extract human-readable error from API response."""
|
||||
try:
|
||||
data = response.json()
|
||||
if "detail" in data:
|
||||
detail = data["detail"]
|
||||
if isinstance(detail, list):
|
||||
return "; ".join(d.get("msg", str(d)) for d in detail)
|
||||
return str(detail)
|
||||
return response.text[:200]
|
||||
except Exception:
|
||||
return response.text[:200]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="superset-tools Maintenance Banner CLI",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Start 4-hour maintenance on public.messages
|
||||
%(prog)s start --api-key ssk_... --tables public.messages \\\\
|
||||
--environment ss-dev --duration-hours 4
|
||||
|
||||
# End maintenance by event ID
|
||||
%(prog)s end --api-key ssk_... --event-id m-abc123
|
||||
|
||||
# Emergency: end ALL maintenance in ss-dev
|
||||
%(prog)s end-all --api-key ssk_... --environment ss-dev
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-url",
|
||||
default="http://localhost:8000",
|
||||
help="superset-tools base URL (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-key",
|
||||
required=True,
|
||||
help="API key with required permissions",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# start
|
||||
start_parser = subparsers.add_parser("start", help="Start maintenance")
|
||||
start_parser.add_argument(
|
||||
"--tables",
|
||||
required=True,
|
||||
help="Comma-separated table names (e.g. 'public.messages,public.users')",
|
||||
)
|
||||
start_parser.add_argument(
|
||||
"--environment",
|
||||
required=True,
|
||||
dest="environment_id",
|
||||
help="Superset environment ID (e.g. 'ss-dev', 'ss-prod')",
|
||||
)
|
||||
start_parser.add_argument(
|
||||
"--duration-hours",
|
||||
type=int,
|
||||
default=4,
|
||||
help="Maintenance window in hours (default: %(default)s)",
|
||||
)
|
||||
start_parser.add_argument(
|
||||
"--message",
|
||||
default=None,
|
||||
help="Optional message for the banner",
|
||||
)
|
||||
start_parser.add_argument(
|
||||
"--auto-end",
|
||||
action="store_true",
|
||||
help="Also send auto_end=true so the scheduler removes the banner automatically "
|
||||
"at end_time. Without it, end_time is informational only.",
|
||||
)
|
||||
|
||||
# end
|
||||
end_parser = subparsers.add_parser("end", help="End a specific maintenance event")
|
||||
end_parser.add_argument(
|
||||
"--event-id",
|
||||
required=True,
|
||||
help="Maintenance event ID (from start response)",
|
||||
)
|
||||
|
||||
# end-all
|
||||
end_all_parser = subparsers.add_parser(
|
||||
"end-all", help="End ALL maintenance events (use with caution)"
|
||||
)
|
||||
end_all_parser.add_argument(
|
||||
"--environment",
|
||||
dest="environment_id",
|
||||
default=None,
|
||||
help="Limit to specific environment (optional)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.command == "start":
|
||||
tables = [t.strip() for t in args.tables.split(",")]
|
||||
start_maintenance(
|
||||
base_url=args.base_url,
|
||||
api_key=args.api_key,
|
||||
tables=tables,
|
||||
environment_id=args.environment_id,
|
||||
duration_hours=args.duration_hours,
|
||||
message=args.message,
|
||||
auto_end=args.auto_end,
|
||||
)
|
||||
elif args.command == "end":
|
||||
end_maintenance(
|
||||
base_url=args.base_url,
|
||||
api_key=args.api_key,
|
||||
event_id=args.event_id,
|
||||
)
|
||||
elif args.command == "end-all":
|
||||
end_all_maintenance(
|
||||
base_url=args.base_url,
|
||||
api_key=args.api_key,
|
||||
environment_id=args.environment_id,
|
||||
)
|
||||
except requests.HTTPError:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user