manta_server/service/
migrate.rs

1//! Node migration between HSM groups.
2//!
3//! vCluster backup/restore are 1:1 pass-throughs to the backend
4//! dispatcher; handlers call those directly. Only `migrate_nodes`
5//! carries real orchestration (hosts-expression resolution,
6//! HSM-group curation, per-pair member migration).
7
8use std::collections::HashMap;
9
10use manta_backend_dispatcher::error::Error;
11use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
12
13use crate::server::common::app_context::InfraContext;
14use crate::service::authorization::validate_user_group_members_access;
15use crate::service::node_ops;
16
17/// Result of migrating nodes for a single parent→target pair.
18#[derive(serde::Serialize, utoipa::ToSchema)]
19pub struct NodeMigrationResult {
20  /// HSM group that received the nodes.
21  pub target_hsm_name: String,
22  /// HSM group that the nodes were moved out of.
23  pub parent_hsm_name: String,
24  /// Final member list of the target group after migration.
25  pub target_members: Vec<String>,
26  /// Remaining member list of the parent group after migration.
27  pub parent_members: Vec<String>,
28}
29
30/// Move the nodes resolved from `hosts_expression` out of any group
31/// in `parent_name_vec` and into every group in
32/// `target_group_name_vec`.
33///
34/// The xname set is curated through
35/// [`node_ops::get_curated_group_from_xname_hostlist`] and then
36/// filtered to the requested parents — nodes that don't currently
37/// belong to one of those parents are silently skipped, which keeps
38/// the call idempotent when the user passes the same expression
39/// twice. Each `target_name` is required to exist unless
40/// `create_group` is true (in dry-run mode the missing-group case
41/// is reported as a `BadRequest` so the operator sees what would have
42/// been created). Returns the moved xnames and one
43/// [`NodeMigrationResult`] per (target, parent) pair, with both
44/// membership lists sorted for stable rendering.
45pub async fn migrate_nodes(
46  infra: &InfraContext<'_>,
47  token: &str,
48  target_group_name_vec: &[String],
49  parent_group_name_vec: &[String],
50  hosts_expression: &str,
51  dry_run: bool,
52  create_group: bool,
53) -> Result<(Vec<String>, Vec<NodeMigrationResult>), Error> {
54  let xname_to_move_vec = node_ops::from_user_hosts_expression_to_xname_vec(
55    infra,
56    token,
57    hosts_expression,
58    false,
59  )
60  .await?;
61
62  if xname_to_move_vec.is_empty() {
63    return Err(Error::BadRequest(
64      "The list of nodes to operate is empty. Nothing to do".to_string(),
65    ));
66  }
67
68  // Defence in depth: the handler already validates every named
69  // target/parent group, and the `retain` below filters out any
70  // resolved xname that isn't in a parent group the caller can
71  // reach — so the migration itself is bounded. We still gate on
72  // member access here so the resolved `xname_to_move_vec` returned
73  // in the response doesn't disclose nodes outside the caller's
74  // groups (the resolver runs against full cluster metadata).
75  validate_user_group_members_access(infra, token, &xname_to_move_vec).await?;
76
77  let mut group_summary: HashMap<String, Vec<String>> =
78    node_ops::get_curated_group_from_xname_hostlist(
79      infra,
80      token,
81      &xname_to_move_vec,
82    )
83    .await?;
84
85  group_summary.retain(|hsm_name, _| parent_group_name_vec.contains(hsm_name));
86
87  tracing::debug!("xnames to move: {:?}", xname_to_move_vec);
88
89  let mut results = Vec::new();
90
91  for target_name in target_group_name_vec {
92    if infra.backend.get_group(token, target_name).await.is_ok() {
93      tracing::debug!("The group '{target_name}' exists, good.");
94    } else if create_group {
95      tracing::info!(
96        "The group {} does not exist, it will be created",
97        target_name
98      );
99      if dry_run {
100        return Err(Error::BadRequest(format!(
101          "Dry-run selected, the group '{target_name}' created"
102        )));
103      }
104    } else {
105      return Err(Error::NotFound(format!(
106        "The group '{target_name}' does not exist and the option \
107                 to create the group was not specified"
108      )));
109    }
110
111    for (parent_group_name, xnames) in &group_summary {
112      let xnames_ref: Vec<&str> = xnames.iter().map(String::as_str).collect();
113      let (mut target_members, mut parent_members) = infra
114        .backend
115        .migrate_group_members(
116          token,
117          target_name,
118          parent_group_name,
119          &xnames_ref,
120          dry_run,
121        )
122        .await?;
123
124      target_members.sort();
125      parent_members.sort();
126
127      results.push(NodeMigrationResult {
128        target_hsm_name: target_name.clone(),
129        parent_hsm_name: parent_group_name.clone(),
130        target_members,
131        parent_members,
132      });
133    }
134  }
135
136  Ok((xname_to_move_vec, results))
137}