manta_server/server/handlers/
migrate.rs

1//! Migrate nodes/backup/restore handlers.
2
3use axum::{Json, http::StatusCode, response::IntoResponse};
4use serde::Deserialize;
5use utoipa::ToSchema;
6
7use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
8use crate::service;
9
10// ---------------------------------------------------------------------------
11// POST /api/v1/migrate/nodes — Migrate nodes between HSM groups
12// ---------------------------------------------------------------------------
13
14/// Request body for `POST /migrate/nodes`.
15#[derive(Deserialize, ToSchema)]
16pub struct MigrateNodesRequest {
17  /// Destination HSM group names to move nodes into.
18  pub target_hsm_names: Vec<String>,
19  /// Source HSM group names the nodes currently belong to.
20  pub parent_hsm_names: Vec<String>,
21  /// Node-set expression selecting which nodes to migrate.
22  pub hosts_expression: String,
23  /// When true, validates the migration plan without modifying group membership.
24  #[serde(default)]
25  pub dry_run: bool,
26  /// Create the target HSM group if it does not already exist.
27  #[serde(default)]
28  pub create_hsm_group: bool,
29}
30
31/// `POST /api/v1/migrate/nodes` — move nodes between HSM groups.
32#[utoipa::path(post, path = "/migrate/nodes", tag = "migrate",
33  params(SiteHeader),
34  request_body = MigrateNodesRequest,
35  security(("bearerAuth" = [])),
36  responses(
37    (status = 200, description = "Migration result", body = serde_json::Value),
38    (status = 400, description = "Bad request",      body = ErrorResponse),
39    (status = 401, description = "Unauthorized",     body = ErrorResponse),
40    (status = 500, description = "Internal error",   body = ErrorResponse),
41  )
42)]
43#[tracing::instrument(skip_all)]
44pub async fn migrate_nodes(
45  ctx: RequestCtx,
46  Json(body): Json<MigrateNodesRequest>,
47) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
48  tracing::info!("migrate_nodes dry_run={}", body.dry_run);
49  let infra = ctx.infra();
50
51  // Authorization: every named group on both sides must be accessible.
52  for name in body
53    .target_hsm_names
54    .iter()
55    .chain(body.parent_hsm_names.iter())
56  {
57    service::group::validate_hsm_group_access(&infra, &ctx.token, name)
58      .await
59      .map_err(to_handler_error)?;
60  }
61
62  let (xnames, results) = service::migrate::migrate_nodes(
63    &infra,
64    &ctx.token,
65    &body.target_hsm_names,
66    &body.parent_hsm_names,
67    &body.hosts_expression,
68    body.dry_run,
69    body.create_hsm_group,
70  )
71  .await
72  .map_err(to_handler_error)?;
73
74  Ok(Json(serde_json::json!({
75    "xnames": xnames,
76    "results": results,
77  })))
78}
79
80// ---------------------------------------------------------------------------
81// POST /api/v1/migrate/backup — Backup BOS session templates
82// ---------------------------------------------------------------------------
83
84/// Request body for `POST /migrate/backup`.
85#[derive(Deserialize, ToSchema)]
86pub struct MigrateBackupRequest {
87  /// BOS session template name (or filter) to back up.
88  pub bos: Option<String>,
89  /// Filesystem path where backup files will be written.
90  pub destination: Option<String>,
91}
92
93/// `POST /api/v1/migrate/backup` — export BOS session templates to backup files.
94#[utoipa::path(post, path = "/migrate/backup", tag = "migrate",
95  params(SiteHeader),
96  request_body = MigrateBackupRequest,
97  security(("bearerAuth" = [])),
98  responses(
99    (status = 200, description = "Backup completed",      body = serde_json::Value),
100    (status = 401, description = "Unauthorized",          body = ErrorResponse),
101    (status = 500, description = "Internal error",        body = ErrorResponse),
102  )
103)]
104#[tracing::instrument(skip_all)]
105pub async fn migrate_backup(
106  ctx: RequestCtx,
107  Json(body): Json<MigrateBackupRequest>,
108) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
109  tracing::info!("migrate_backup");
110  let infra = ctx.infra();
111
112  service::migrate::migrate_backup(
113    &infra,
114    &ctx.token,
115    body.bos.as_deref(),
116    body.destination.as_deref(),
117  )
118  .await
119  .map_err(to_handler_error)?;
120
121  Ok(Json(serde_json::json!({ "completed": true })))
122}
123
124// ---------------------------------------------------------------------------
125// POST /api/v1/migrate/restore — Restore from backup files
126// ---------------------------------------------------------------------------
127
128/// Request body for `POST /migrate/restore`.
129#[derive(Deserialize, ToSchema)]
130pub struct MigrateRestoreRequest {
131  /// Path to the BOS session template backup file.
132  pub bos_file: Option<String>,
133  /// Path to the CFS configuration backup file.
134  pub cfs_file: Option<String>,
135  /// Path to the HSM group backup file.
136  pub hsm_file: Option<String>,
137  /// Path to the IMS image metadata backup file.
138  pub ims_file: Option<String>,
139  /// Directory containing the image layer tarballs.
140  pub image_dir: Option<String>,
141  /// When true, overwrite existing resources that conflict with the backup.
142  #[serde(default)]
143  pub overwrite: bool,
144}
145
146/// `POST /api/v1/migrate/restore` — import BOS session templates and related artifacts from backup.
147#[utoipa::path(post, path = "/migrate/restore", tag = "migrate",
148  params(SiteHeader),
149  request_body = MigrateRestoreRequest,
150  security(("bearerAuth" = [])),
151  responses(
152    (status = 200, description = "Restore completed",  body = serde_json::Value),
153    (status = 401, description = "Unauthorized",       body = ErrorResponse),
154    (status = 500, description = "Internal error",     body = ErrorResponse),
155  )
156)]
157#[tracing::instrument(skip_all)]
158pub async fn migrate_restore(
159  ctx: RequestCtx,
160  Json(body): Json<MigrateRestoreRequest>,
161) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
162  tracing::info!("migrate_restore overwrite={}", body.overwrite);
163  let infra = ctx.infra();
164
165  service::migrate::migrate_restore(
166    &infra,
167    &ctx.token,
168    body.bos_file.as_deref(),
169    body.cfs_file.as_deref(),
170    body.hsm_file.as_deref(),
171    body.ims_file.as_deref(),
172    body.image_dir.as_deref(),
173    body.overwrite,
174  )
175  .await
176  .map_err(to_handler_error)?;
177
178  Ok(Json(serde_json::json!({ "completed": true })))
179}