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/") 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//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//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