manta_server/server/handlers/
template.rs

1//! Template + post_template_session 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::{
13  ErrorResponse, RequestCtx, SiteHeader, serialize_or_500, to_handler_error,
14};
15use crate::service;
16
17// ---------------------------------------------------------------------------
18// GET /api/v1/templates
19// ---------------------------------------------------------------------------
20
21/// Query parameters for `GET /templates`.
22#[derive(Deserialize, IntoParams)]
23pub struct TemplateQuery {
24  /// Exact template name.
25  pub name: Option<String>,
26  /// HSM group whose associated templates should be returned.
27  pub hsm_group: Option<String>,
28  /// Cap on the number of templates returned (most recent first).
29  pub limit: Option<u8>,
30}
31
32/// GET /templates — list BOS session templates with optional filters.
33#[utoipa::path(get, path = "/templates", tag = "templates",
34  params(TemplateQuery, SiteHeader),
35  security(("bearerAuth" = [])),
36  responses(
37    (status = 200, description = "List of session templates", body = serde_json::Value),
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_templates(
44  ctx: RequestCtx,
45  Query(q): Query<TemplateQuery>,
46) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
47  let infra = ctx.infra();
48
49  let params = service::template::GetTemplateParams {
50    name: q.name,
51    hsm_group: q.hsm_group,
52    settings_hsm_group_name: None,
53    limit: q.limit,
54  };
55
56  let templates = service::template::get_templates(&infra, &ctx.token, &params)
57    .await
58    .map_err(to_handler_error)?;
59
60  Ok(Json(templates))
61}
62
63// ---------------------------------------------------------------------------
64// POST /api/v1/templates/{name}/sessions — Create BOS session from template
65// ---------------------------------------------------------------------------
66
67/// BOS session operation to run against the template's node list.
68#[derive(Debug, Deserialize, ToSchema)]
69#[serde(rename_all = "lowercase")]
70pub enum BosOperation {
71  /// Boot nodes that are currently off.
72  Boot,
73  /// Reboot (power-cycle) nodes.
74  Reboot,
75  /// Shut down nodes.
76  Shutdown,
77}
78
79impl BosOperation {
80  fn as_str(&self) -> &'static str {
81    match self {
82      Self::Boot => "boot",
83      Self::Reboot => "reboot",
84      Self::Shutdown => "shutdown",
85    }
86  }
87}
88
89/// Request body for `POST /templates/{name}/sessions`.
90#[derive(Deserialize, ToSchema)]
91pub struct PostTemplateSessionRequest {
92  /// BOS operation to run (boot, reboot, or shutdown).
93  pub operation: BosOperation,
94  /// Ansible limit expression restricting which template nodes are targeted.
95  pub limit: String,
96  /// Optional explicit name for the BOS session.
97  pub session_name: Option<String>,
98  /// When true, include nodes marked as disabled.
99  #[serde(default)]
100  pub include_disabled: bool,
101  /// When true, validates the session parameters without creating a BOS session.
102  #[serde(default)]
103  pub dry_run: bool,
104}
105
106/// `POST /api/v1/templates/{name}/sessions` — create a BOS session from a session template.
107#[utoipa::path(post, path = "/templates/{name}/sessions", tag = "templates",
108  params(("name" = String, Path, description = "Template name"), SiteHeader),
109  request_body = PostTemplateSessionRequest,
110  security(("bearerAuth" = [])),
111  responses(
112    (status = 200, description = "Dry run preview",  body = serde_json::Value),
113    (status = 201, description = "Session created",  body = serde_json::Value),
114    (status = 400, description = "Bad request",      body = ErrorResponse),
115    (status = 401, description = "Unauthorized",     body = ErrorResponse),
116    (status = 500, description = "Internal error",   body = ErrorResponse),
117  )
118)]
119#[tracing::instrument(skip_all)]
120pub async fn post_template_session(
121  ctx: RequestCtx,
122  Path(name): Path<String>,
123  Json(body): Json<PostTemplateSessionRequest>,
124) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
125  tracing::info!(
126    "post_template_session template={} op={:?} dry_run={}",
127    name,
128    body.operation,
129    body.dry_run
130  );
131  let infra = ctx.infra();
132
133  let params = service::template::ApplyTemplateParams {
134    bos_session_name: body.session_name,
135    bos_sessiontemplate_name: name,
136    bos_session_operation: body.operation.as_str().to_string(),
137    limit: body.limit,
138    include_disabled: body.include_disabled,
139  };
140
141  let (bos_session, _) =
142    service::template::validate_and_prepare_template_session(
143      &infra, &ctx.token, &params,
144    )
145    .await
146    .map_err(to_handler_error)?;
147
148  if body.dry_run {
149    return Ok((StatusCode::OK, Json(serialize_or_500(&bos_session)?)));
150  }
151
152  let created =
153    service::template::create_bos_session(&infra, &ctx.token, bos_session)
154      .await
155      .map_err(to_handler_error)?;
156
157  Ok((StatusCode::CREATED, Json(serialize_or_500(&created)?)))
158}