manta_server/service/
migrate.rs

1//! vCluster backup/restore and node migration between HSM groups.
2
3use std::collections::HashMap;
4
5use manta_backend_dispatcher::error::Error;
6use manta_backend_dispatcher::interfaces::{
7  hsm::group::GroupTrait, migrate_backup::MigrateBackupTrait,
8  migrate_restore::MigrateRestoreTrait,
9};
10
11use crate::server::common::{app_context::InfraContext, node_ops};
12
13/// Execute a migrate-backup operation against the backend.
14pub async fn migrate_backup(
15  infra: &InfraContext<'_>,
16  token: &str,
17  bos: Option<&str>,
18  destination: Option<&str>,
19) -> Result<(), Error> {
20  infra
21    .backend
22    .migrate_backup(
23      token,
24      infra.shasta_base_url,
25      infra.shasta_root_cert,
26      bos,
27      destination,
28    )
29    .await
30}
31
32/// Restore a vCluster from backup files; `overwrite` applies to all resource types.
33#[allow(clippy::too_many_arguments)]
34pub async fn migrate_restore(
35  infra: &InfraContext<'_>,
36  token: &str,
37  bos_file: Option<&str>,
38  cfs_file: Option<&str>,
39  hsm_file: Option<&str>,
40  ims_file: Option<&str>,
41  image_dir: Option<&str>,
42  overwrite: bool,
43) -> Result<(), Error> {
44  // The backend trait exposes four independent overwrite flags
45  // (overwrite_bos, overwrite_cfs, overwrite_hsm, overwrite_ims).
46  // The HTTP/CLI APIs currently expose a single `overwrite` knob that
47  // fans out to all four. Expose them individually if callers need
48  // per-resource control in the future.
49  infra
50    .backend
51    .migrate_restore(
52      token,
53      infra.shasta_base_url,
54      infra.shasta_root_cert,
55      bos_file,
56      cfs_file,
57      hsm_file,
58      ims_file,
59      image_dir,
60      overwrite, // overwrite_bos
61      overwrite, // overwrite_cfs
62      overwrite, // overwrite_hsm
63      overwrite, // overwrite_ims
64    )
65    .await
66}
67
68/// Result of migrating nodes for a single parent→target pair.
69#[derive(serde::Serialize)]
70pub struct NodeMigrationResult {
71  /// HSM group that received the nodes.
72  pub target_hsm_name: String,
73  /// HSM group that the nodes were moved out of.
74  pub parent_hsm_name: String,
75  /// Final member list of the target group after migration.
76  pub target_members: Vec<String>,
77  /// Remaining member list of the parent group after migration.
78  pub parent_members: Vec<String>,
79}
80
81/// Resolve hosts expression, curate HSM groups, validate targets,
82/// and migrate nodes between HSM groups.
83///
84/// Returns the list of xnames that were moved and the per-pair
85/// migration results for display.
86pub async fn migrate_nodes(
87  infra: &InfraContext<'_>,
88  token: &str,
89  target_hsm_name_vec: &[String],
90  parent_hsm_name_vec: &[String],
91  hosts_expression: &str,
92  dry_run: bool,
93  create_hsm_group: bool,
94) -> Result<(Vec<String>, Vec<NodeMigrationResult>), Error> {
95  let backend = infra.backend;
96
97  // Resolve hosts expression to xnames
98  let xname_to_move_vec =
99    node_ops::resolve_hosts_expression(backend, token, hosts_expression, false)
100      .await?;
101
102  if xname_to_move_vec.is_empty() {
103    return Err(Error::BadRequest(
104      "The list of nodes to operate is empty. Nothing to do".to_string(),
105    ));
106  }
107
108  let mut hsm_group_summary: HashMap<String, Vec<String>> =
109    node_ops::get_curated_hsm_group_from_xname_hostlist(
110      backend,
111      token,
112      &xname_to_move_vec,
113    )
114    .await?;
115
116  hsm_group_summary
117    .retain(|hsm_name, _| parent_hsm_name_vec.contains(hsm_name));
118
119  tracing::debug!("xnames to move: {:?}", xname_to_move_vec);
120
121  let mut results = Vec::new();
122
123  for target_hsm_name in target_hsm_name_vec {
124    if backend.get_group(token, target_hsm_name).await.is_ok() {
125      tracing::debug!("The group '{target_hsm_name}' exists, good.");
126    } else if create_hsm_group {
127      tracing::info!(
128        "The group {} does not exist, it will be created",
129        target_hsm_name
130      );
131      if dry_run {
132        return Err(Error::BadRequest(format!(
133          "Dry-run selected, the group '{target_hsm_name}' created"
134        )));
135      }
136    } else {
137      return Err(Error::NotFound(format!(
138        "The group '{target_hsm_name}' does not exist and the option \
139                 to create the group was not specified"
140      )));
141    }
142
143    for (parent_hsm_name, xnames) in &hsm_group_summary {
144      let (mut target_members, mut parent_members) = backend
145        .migrate_group_members(
146          token,
147          target_hsm_name,
148          parent_hsm_name,
149          &xnames.iter().map(String::as_str).collect::<Vec<&str>>(),
150          dry_run,
151        )
152        .await?;
153
154      target_members.sort();
155      parent_members.sort();
156
157      results.push(NodeMigrationResult {
158        target_hsm_name: target_hsm_name.clone(),
159        parent_hsm_name: parent_hsm_name.clone(),
160        target_members,
161        parent_members,
162      });
163    }
164  }
165
166  Ok((xname_to_move_vec, results))
167}