manta_server/server/handlers/
node.rs

1//! Node CRUD handlers.
2
3use axum::{
4  Json,
5  extract::{Path, Query},
6  http::StatusCode,
7  response::IntoResponse,
8};
9use serde::Deserialize;
10use utoipa::{IntoParams, ToSchema};
11
12use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
13use crate::service;
14
15// ---------------------------------------------------------------------------
16// GET /api/v1/nodes
17// ---------------------------------------------------------------------------
18
19/// Query parameters for `GET /nodes`.
20#[derive(Deserialize, IntoParams)]
21pub struct NodesQuery {
22  /// Comma-separated xnames, NIDs, or hostlist expression
23  /// (e.g. `x3000c0s1b0n[0-3]`).
24  pub xname: String,
25  /// Expand results to include nodes sharing the same power supply.
26  pub include_siblings: Option<bool>,
27  /// Optional power-status filter (e.g. `ON`, `OFF`, `READY`).
28  pub status: Option<String>,
29}
30
31/// GET /nodes — fetch node details for a given xname expression.
32#[utoipa::path(get, path = "/nodes", tag = "nodes",
33  params(NodesQuery, SiteHeader),
34  security(("bearerAuth" = [])),
35  responses(
36    (status = 200, description = "Node details",  body = serde_json::Value),
37    (status = 400, description = "Bad request",   body = ErrorResponse),
38    (status = 401, description = "Unauthorized",  body = ErrorResponse),
39    (status = 500, description = "Internal error", body = ErrorResponse),
40  )
41)]
42#[tracing::instrument(skip_all)]
43pub async fn get_nodes(
44  ctx: RequestCtx,
45  Query(q): Query<NodesQuery>,
46) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
47  let infra = ctx.infra();
48
49  let params = service::node::GetNodesParams {
50    xname: q.xname,
51    include_siblings: q.include_siblings.unwrap_or(false),
52    status_filter: q.status,
53  };
54
55  let nodes = service::node::get_nodes(&infra, &ctx.token, &params)
56    .await
57    .map_err(to_handler_error)?;
58
59  Ok(Json(nodes))
60}
61
62// ---------------------------------------------------------------------------
63// DELETE /api/v1/nodes/{id}
64// ---------------------------------------------------------------------------
65
66/// DELETE /nodes/{id} — remove a node from HSM by xname or NID.
67#[utoipa::path(delete, path = "/nodes/{id}", tag = "nodes",
68  params(("id" = String, Path, description = "Node xname or NID"), SiteHeader),
69  security(("bearerAuth" = [])),
70  responses(
71    (status = 204, description = "Node removed"),
72    (status = 401, description = "Unauthorized", body = ErrorResponse),
73    (status = 404, description = "Not found",    body = ErrorResponse),
74    (status = 500, description = "Internal error", body = ErrorResponse),
75  )
76)]
77#[tracing::instrument(skip_all)]
78pub async fn delete_node(
79  ctx: RequestCtx,
80  Path(id): Path<String>,
81) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
82  tracing::info!("delete_node id={}", id);
83  let infra = ctx.infra();
84
85  service::node::delete_node(&infra, &ctx.token, &id)
86    .await
87    .map_err(to_handler_error)?;
88
89  Ok(StatusCode::NO_CONTENT)
90}
91
92// ---------------------------------------------------------------------------
93// POST /api/v1/nodes
94// ---------------------------------------------------------------------------
95
96/// Body for `POST /nodes`.
97#[derive(Deserialize, ToSchema)]
98pub struct AddNodeRequest {
99  /// Physical location ID (xname) of the node, e.g. `x3000c0s1b0n0`.
100  pub id: String,
101  /// Initial HSM group the node belongs to.
102  pub group: String,
103  /// Whether to register the node as enabled. Defaults to `false`
104  /// (disabled) per serde's default for `bool`; CLI's
105  /// `manta add node` flips the polarity via `--disabled`.
106  #[serde(default)]
107  pub enabled: bool,
108  /// Optional architecture tag: `"X86"`, `"ARM"`, or `"Other"`.
109  pub arch: Option<String>,
110}
111
112/// POST /nodes — register a new node in HSM and add it to a group.
113#[utoipa::path(post, path = "/nodes", tag = "nodes",
114  params(SiteHeader),
115  request_body = AddNodeRequest,
116  security(("bearerAuth" = [])),
117  responses(
118    (status = 201, description = "Node registered",  body = serde_json::Value),
119    (status = 400, description = "Bad request",      body = ErrorResponse),
120    (status = 401, description = "Unauthorized",     body = ErrorResponse),
121    (status = 500, description = "Internal error",   body = ErrorResponse),
122  )
123)]
124#[tracing::instrument(skip_all)]
125pub async fn add_node(
126  ctx: RequestCtx,
127  Json(body): Json<AddNodeRequest>,
128) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
129  tracing::info!("add_node id={} group={}", body.id, body.group);
130  let infra = ctx.infra();
131
132  service::node::add_node(
133    &infra,
134    &ctx.token,
135    &body.id,
136    &body.group,
137    body.enabled,
138    body.arch,
139    None, // hardware_file_path not applicable via HTTP
140  )
141  .await
142  .map_err(to_handler_error)?;
143
144  Ok((
145    StatusCode::CREATED,
146    Json(serde_json::json!({ "id": body.id })),
147  ))
148}