156 lines
5.0 KiB
Python
156 lines
5.0 KiB
Python
from __future__ import annotations
|
||
|
||
import logging
|
||
from http import HTTPStatus
|
||
from typing import Any, Mapping
|
||
|
||
from flask import Blueprint, jsonify, request
|
||
|
||
from .models import (
|
||
ValidationError,
|
||
validate_config_payload,
|
||
validate_registration_payload,
|
||
validate_status_payload,
|
||
)
|
||
from .scheduler import StatusScheduler
|
||
from .storage import Storage
|
||
from .util import to_iso, utcnow
|
||
|
||
|
||
def create_nodes_blueprint(storage: Storage, scheduler: StatusScheduler) -> Blueprint:
|
||
bp = Blueprint("nodes", __name__)
|
||
logger = logging.getLogger("argus.master.api")
|
||
|
||
def _json_error(message: str, status: HTTPStatus, code: str) -> Any:
|
||
response = jsonify({"error": message, "code": code})
|
||
response.status_code = status
|
||
return response
|
||
|
||
@bp.errorhandler(ValidationError)
|
||
def _handle_validation_error(err: ValidationError):
|
||
return _json_error(str(err), HTTPStatus.BAD_REQUEST, "invalid_request")
|
||
|
||
@bp.get("/nodes")
|
||
def list_nodes():
|
||
nodes = storage.list_nodes()
|
||
return jsonify(nodes)
|
||
|
||
@bp.get("/nodes/<node_id>")
|
||
def get_node(node_id: str):
|
||
node = storage.get_node(node_id)
|
||
if node is None:
|
||
return _json_error("Node not found", HTTPStatus.NOT_FOUND, "not_found")
|
||
return jsonify(node)
|
||
|
||
@bp.post("/nodes")
|
||
def register_node():
|
||
payload = _get_json()
|
||
data = validate_registration_payload(payload)
|
||
now = utcnow()
|
||
now_iso = to_iso(now)
|
||
node_id = data["id"]
|
||
name = data["name"]
|
||
node_type = data["type"]
|
||
version = data["version"]
|
||
meta = data["meta_data"]
|
||
|
||
if node_id:
|
||
# 携带 id 说明是重注册,需要校验名称一致性
|
||
existing_row = storage.get_node_raw(node_id)
|
||
if existing_row is None:
|
||
return _json_error("Node not found", HTTPStatus.NOT_FOUND, "not_found")
|
||
if existing_row["name"] != name:
|
||
return _json_error(
|
||
"Node id and name mismatch during re-registration",
|
||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||
"id_name_mismatch",
|
||
)
|
||
updated = storage.update_node_meta(
|
||
node_id,
|
||
node_type=node_type,
|
||
version=version,
|
||
meta_data=meta,
|
||
last_updated_iso=now_iso,
|
||
)
|
||
scheduler.trigger_nodes_json_refresh()
|
||
return jsonify(updated), HTTPStatus.OK
|
||
|
||
# No id provided → search by name
|
||
existing_by_name = storage.get_node_by_name(name)
|
||
if existing_by_name:
|
||
# 同名节点已存在,视为无 id 重注册
|
||
updated = storage.update_node_meta(
|
||
existing_by_name["id"],
|
||
node_type=node_type,
|
||
version=version,
|
||
meta_data=meta,
|
||
last_updated_iso=now_iso,
|
||
)
|
||
scheduler.trigger_nodes_json_refresh()
|
||
return jsonify(updated), HTTPStatus.OK
|
||
|
||
new_id = storage.allocate_node_id()
|
||
created = storage.create_node(
|
||
new_id,
|
||
name,
|
||
node_type,
|
||
version,
|
||
meta,
|
||
status="initialized",
|
||
register_time_iso=now_iso,
|
||
last_updated_iso=now_iso,
|
||
)
|
||
scheduler.trigger_nodes_json_refresh()
|
||
return jsonify(created), HTTPStatus.CREATED
|
||
|
||
@bp.put("/nodes/<node_id>/config")
|
||
def update_node_config(node_id: str):
|
||
payload = _get_json()
|
||
updates = validate_config_payload(payload)
|
||
try:
|
||
updated = storage.update_config_and_labels(
|
||
node_id,
|
||
config=updates.get("config"),
|
||
labels=updates.get("label"),
|
||
)
|
||
except KeyError:
|
||
return _json_error("Node not found", HTTPStatus.NOT_FOUND, "not_found")
|
||
|
||
if "label" in updates:
|
||
scheduler.trigger_nodes_json_refresh()
|
||
return jsonify(updated)
|
||
|
||
@bp.get("/nodes/statistics")
|
||
def node_statistics():
|
||
stats = storage.get_statistics()
|
||
return jsonify(stats)
|
||
|
||
@bp.put("/nodes/<node_id>/status")
|
||
def update_status(node_id: str):
|
||
payload = _get_json()
|
||
data = validate_status_payload(payload)
|
||
try:
|
||
# master 负责写入 last_report,状态由调度器计算
|
||
updated = storage.update_last_report(
|
||
node_id,
|
||
server_timestamp_iso=to_iso(utcnow()),
|
||
agent_timestamp_iso=data["timestamp"],
|
||
health=data["health"],
|
||
)
|
||
except KeyError:
|
||
return _json_error("Node not found", HTTPStatus.NOT_FOUND, "not_found")
|
||
|
||
scheduler.trigger_nodes_json_refresh()
|
||
return jsonify(updated)
|
||
|
||
return bp
|
||
|
||
|
||
def _get_json() -> Mapping[str, Any]:
|
||
data = request.get_json(silent=True)
|
||
if data is None:
|
||
raise ValidationError("Request body must be valid JSON")
|
||
if not isinstance(data, Mapping):
|
||
raise ValidationError("Request body must be a JSON object")
|
||
return data
|