Files
mcp-server/spark_executor/models.py
T
ClaudeandClaude Fable 5 7fbad97a87 feat: add list_applications tool for YARN application enumeration
Add a new MCP tool that queries YARN's /ws/v1/cluster/apps endpoint
through a named Connection, returning a list of ApplicationSummary
records. Bypasses the local JobStore — useful for enumerating apps
that were not submitted through this service.

API:
  list_applications(
    connection_name: str,           # required, which YARN cluster
    state: str | None = None,       # YARN state filter: NEW/NEW_SAVING/
                                    # SUBMITTED/ACCEPTED/RUNNING/
                                    # FINISHED/FAILED/KILLED
    queue: str | None = None,       # YARN queue filter
    limit: int = 100,               # cap on returned apps (YARN has no
                                    # offset-based pagination; combine
                                    # state/queue filters for big clusters)
  ) -> list[ApplicationSummary]

Implementation:
  - yarn_client.list_applications(config, *, state, queue, limit) -> list[dict]
    Returns raw YARN app dicts; raises YarnError on 4xx/5xx; returns
    [] on 404 (no apps match). Uses the existing _request helper,
    which now accepts a "params" kwarg for query strings (one-line
    additive change).
  - external_jobs.list_applications(connection_name, state, queue, limit)
    -> list[ApplicationSummary]. Looks up the Connection, builds the
    YarnClientConfig, calls the yarn_client function, maps each raw
    YARN dict to ApplicationSummary (mirroring the manual field-mapping
    style of get_job_result). The yarn_client function is imported
    as "list_applications_yarn" to avoid name collision.
  - ApplicationSummary: 12-field Pydantic model with snake_case names
    (application_id, name, user, queue, state, final_status,
    application_type, application_tags, started_time, finished_time,
    tracking_url, progress). Unused YARN fields (memorySeconds,
    vcoreSeconds, preemptedResource*, etc.) are not exposed.
  - ListApplicationsRequest: Pydantic body model with Field(description=)
    for LLM-facing schema.
  - /list_applications route registered with operation_id=
    "list_applications", placed next to the other external YARN tools.

Tests:
  - 8 new unit tests in test_external_jobs.py (happy path, state/queue/
    limit pass-through, default limit, empty list, missing connection,
    full field mapping).
  - test_mcp_routes.py: assert 23 tool routes.
  - README: list_applications row added to the Spark Executor table.

Tests: 390 passed (was 382, +8 net).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:40:20 +08:00

153 lines
4.6 KiB
Python

# coding=utf-8
"""
@Time :2026/6/24
@Author :tao.chen
"""
from datetime import datetime
from pydantic import BaseModel, Field, field_validator
class Job(BaseModel):
job_id: str
application_id: str
script_path: str
queue: str
submit_time: datetime
connection: str
yarn_rm_url: str | None = None
class JobStatus(BaseModel):
application_id: str
state: str
raw: str = Field(default="")
class JobResult(BaseModel):
application_id: str
state: str
final_status: str | None = None
diagnostics: str | None = None
tracking_url: str | None = None
started_time: int | None = None
finished_time: int | None = None
class SubmitResult(BaseModel):
job_id: str
application_id: str
tracking_url: str | None = None
class FetchUrlResult(BaseModel):
url: str
status_code: int
content_type: str
body: str
truncated: bool = False
class ApplicationSummary(BaseModel):
"""A YARN application summary from /ws/v1/cluster/apps.
Field names are mapped from the YARN JSON keys to clearer
snake_case names by the tool function. Unused YARN fields
(memorySeconds, vcoreSeconds, preemptedResource*, etc.) are
not exposed — the LLM doesn't need them.
"""
application_id: str
name: str
user: str
queue: str
state: str
final_status: str | None = None
application_type: str | None = None
application_tags: str = ""
started_time: int = 0
finished_time: int = 0
tracking_url: str | None = None
progress: float | None = None
class Connection(BaseModel):
name: str
# Defaults to "yarn" because that's the literal string spark-submit wants
# for --master when targeting YARN. Override for Standalone (spark://...),
# Kubernetes (k8s://...), or local mode.
master: str = "yarn"
deploy_mode: str = "cluster"
yarn_rm_url: str | None = None
spark_conf: dict[str, str] = Field(default_factory=dict)
# None means "fall back to Settings.ssl_verify_default". Explicit True/False
# overrides the global default for this connection.
ssl_verify: bool | None = None
ssl_ca_bundle: str | None = None
# Authentication for YARN REST calls.
auth_type: str = "none" # "none" | "simple" | "basic" | "kerberos"
auth_user: str | None = None
auth_password: str | None = None
# Display/audit only for kerberos; actual SPNEGO uses the system cache.
auth_principal: str | None = None
auth_keytab: str | None = None
url_allowlist: list[str] = Field(
default_factory=list,
description=(
"List of fnmatch glob patterns for hosts the fetch_url tool may access. "
"The list is mandatory-opt-in: an empty list (the default) denies all "
"hosts, so you must populate it before fetch_url can access any URL. "
"Useful for clusters whose hostnames do NOT share a common suffix — "
"e.g. single-label hosts like 'ccam1'-'ccam99' (configure ['ccam*']) "
"or HDFS namenode on a different subdomain ('*.hadoop.internal'). "
"Patterns are matched against the URL host only (no port, no path). "
"fnmatch rules apply: '*' does NOT match '.', so 'ccam*' matches "
"'ccam50' but not 'ccam50.evil.com'."
),
)
@field_validator("master")
@classmethod
def _check_master(cls, v: str) -> str:
"""Catch common typos like 'yarn-cluster' or 'http://...'. """
if v == "yarn":
return v
if v.startswith(("spark://", "k8s://", "mesos://", "local")):
return v
raise ValueError(
f"master must be 'yarn', 'spark://...', 'k8s://...', 'mesos://...', "
f"or 'local[/N]'; got {v!r}"
)
@field_validator("auth_type")
@classmethod
def _check_auth_type(cls, v: str) -> str:
if v not in {"none", "simple", "basic", "kerberos"}:
raise ValueError(
f"auth_type must be one of none/simple/basic/kerberos; got {v!r}"
)
return v
class PendingSubmission(BaseModel):
pending_id: str
app_name: str | None = None
connection: str
master: str
deploy_mode: str
yarn_rm_url: str | None = None
script_path: str
queue: str
executor_memory: str
executor_cores: int
num_executors: int
spark_conf: dict[str, str] = Field(default_factory=dict)
extra_args: dict[str, str] = Field(default_factory=dict)
created_at: datetime
status: str = "PENDING" # PENDING | SUBMITTED | CANCELLED | FAILED
error: str | None = None
job_id: str | None = None
application_id: str | None = None
tracking_url: str | None = None