Skip to main content

manta_server/server/handlers/
node.rs

1//! Node CRUD handlers.
2//!
3//! - `GET    /v2/nodes`        → [`get_nodes`] — wraps
4//!   `service::node::get_nodes`.
5//! - `POST   /v2/nodes`        → [`add_node`].
6//! - `DELETE /v2/nodes/{id}`   → [`delete_node`].
7//!
8//! `get_nodes` accepts an xname expression query parameter and an
9//! optional `status` filter; the `include_siblings` flag fans out
10//! to neighbour nodes sharing the same chassis.
11
12use axum::{
13  Json,
14  extract::{Path, Query},
15  http::StatusCode,
16  response::IntoResponse,
17};
18
19use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
20use crate::server::audit::AuditNote;
21use crate::service;
22
23// ---------------------------------------------------------------------------
24// GET /v2/nodes
25// ---------------------------------------------------------------------------
26
27pub use manta_shared::types::api::queries::NodesQuery;
28
29/// GET /nodes — fetch node details for a given xname expression.
30#[utoipa::path(get, path = "/nodes", tag = "nodes",
31  params(NodesQuery, SiteHeader),
32  security(("bearerAuth" = [])),
33  responses(
34    (status = 200, description = "Node details",  body = Vec<manta_shared::types::dto::NodeDetails>),
35    (status = 400, description = "Bad request",   body = ErrorResponse),
36    (status = 401, description = "Unauthorized",  body = ErrorResponse),
37    (status = 500, description = "Internal error", body = ErrorResponse),
38  )
39)]
40#[tracing::instrument(skip_all)]
41pub async fn get_nodes(
42  ctx: RequestCtx,
43  Query(q): Query<NodesQuery>,
44) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
45  let infra = ctx.infra();
46
47  let params = service::node::GetNodesParams {
48    host_expression: q.xname,
49    include_siblings: q.include_siblings.unwrap_or(false),
50    status_filter: q.status,
51  };
52
53  let nodes = service::node::get_nodes(&infra, &ctx.token, &params)
54    .await
55    .map_err(to_handler_error)?;
56
57  Ok(Json(nodes))
58}
59
60// ---------------------------------------------------------------------------
61// DELETE /v2/nodes/{id}
62// ---------------------------------------------------------------------------
63
64/// DELETE /nodes/{id} — remove a node from HSM by xname or NID.
65#[utoipa::path(delete, path = "/nodes/{id}", tag = "nodes",
66  params(("id" = String, Path, description = "Node xname or NID"), SiteHeader),
67  security(("bearerAuth" = [])),
68  responses(
69    (status = 204, description = "Node removed"),
70    (status = 401, description = "Unauthorized", body = ErrorResponse),
71    (status = 404, description = "Not found",    body = ErrorResponse),
72    (status = 500, description = "Internal error", body = ErrorResponse),
73  )
74)]
75#[tracing::instrument(skip_all)]
76pub async fn delete_node(
77  ctx: RequestCtx,
78  note: AuditNote,
79  Path(id): Path<String>,
80) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
81  note.action("node.delete").target("node", &id);
82
83  tracing::info!("delete_node id={}", id);
84  let infra = ctx.infra();
85
86  service::node::delete_node(&infra, &ctx.token, &id)
87    .await
88    .map_err(to_handler_error)?;
89
90  Ok(StatusCode::NO_CONTENT)
91}
92
93// ---------------------------------------------------------------------------
94// POST /v2/nodes
95// ---------------------------------------------------------------------------
96
97pub use manta_shared::types::api::node::AddNodeRequest;
98
99/// POST /nodes — register a new node in HSM and add it to a group.
100#[utoipa::path(post, path = "/nodes", tag = "nodes",
101  params(SiteHeader),
102  request_body = AddNodeRequest,
103  security(("bearerAuth" = [])),
104  responses(
105    (status = 201, description = "Node registered",  body = manta_shared::types::api::responses::AddNodeResponse),
106    (status = 400, description = "Bad request",      body = ErrorResponse),
107    (status = 401, description = "Unauthorized",     body = ErrorResponse),
108    (status = 500, description = "Internal error",   body = ErrorResponse),
109  )
110)]
111#[tracing::instrument(skip_all)]
112pub async fn add_node(
113  ctx: RequestCtx,
114  note: AuditNote,
115  Json(body): Json<AddNodeRequest>,
116) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
117  note
118    .action("node.add")
119    .target("node", &body.id)
120    .target("group", &body.group)
121    .detail("enabled", body.enabled);
122
123  tracing::info!("add_node id={} group={}", body.id, body.group);
124  let infra = ctx.infra();
125
126  service::node::add_node(
127    &infra,
128    &ctx.token,
129    &body.id,
130    &body.group,
131    body.enabled,
132    body.arch,
133    None, // hardware_file_path not applicable via HTTP
134  )
135  .await
136  .map_err(to_handler_error)?;
137
138  Ok((
139    StatusCode::CREATED,
140    Json(serde_json::json!({ "id": body.id })),
141  ))
142}