Skip to content

Commit

Permalink
Feature vcluster migrate backup (#9)
Browse files Browse the repository at this point in the history
* Add note about CA certificates

* Update info about log file location.

* Feature: migrate/backup first commit (partial)

* Feature: migrate/backup ignore JetBrains stuff

* Feature: migrate/backup download all files of a bos session template

* Feature: migrate/backup fix count of artifacts in download info

* Feature: migrate/backup add support to produce a file with the list of xnames belonging to the HSM groups in the BOS session template.

* Feature: migrate/backup cleanup

* Feature: migrate/backup more cleanup
  • Loading branch information
miguelgila authored Dec 7, 2023
1 parent f43e581 commit c730abe
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
Cargo.lock
*.code-workspace
*.log
.idea/
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Manta aggregates information from multiple sources:

## Configuration

Manta needs a configuration file in `$HOME/.config/manta/config.toml` like shown below
Manta needs a configuration file in `${HOME}/.config/manta/config.toml` like shown below

```bash
log = "info"
Expand All @@ -58,7 +58,7 @@ vault_secret_path = "shasta"
vault_role_id = "b15517de-cabb-06ba-af98-633d216c6d99" # vault in hashicorp-vault.cscs.ch
```

Manta can log user's operations in /var/log/manta/ folder, please make sure this folder exists and all users have rwx access to it
Manta can log user's operations in `/var/log/manta/` (Linux) or `${PWD}` (MacOS), please make sure this folder exists and the current user has `rwx` access to it

```bash
mkdir /var/log/manta
Expand All @@ -82,6 +82,35 @@ chmod 777 -R /var/log/manta
| sites.site_name.vault_secret_path | yes | config file | path in vault to find secrets | shasta | prealps |
| sites.site_name.shasta_base_url | yes | config file | Shasta API base URL for Shasta related jobs submission | https://api-gw-service-nmn.local/apis |

### A note on certificates

Manta expects to have the CA of the CSM endpoint in PEM format in a file named `<SITE>_root_cert.pem>` under `${HOME}/.config/manta` (Linux) or `${HOME}/Library/Application\ Support/local.cscs.manta` (MacOS).
Please make sure **the file contains just one CA**, on MacOS if there are more than one in the file, and the native-tls module is used, the following part of the security framework crate will break Manta:
```rust
#[cfg(not(target_os = "ios"))]
pub fn from_pem(buf: &[u8]) -> Result<Certificate, Error> {
let mut items = SecItems::default();
ImportOptions::new().items(&mut items).import(buf)?;
if items.certificates.len() == 1 && items.identities.is_empty() && items.keys.is_empty() {
Ok(Certificate(items.certificates.pop().unwrap()))
} else {
Err(Error(base::Error::from(errSecParam)))
}
}
```

The error message thrown is usually difficult to interpret and is something like:
```
thread 'main' panicked at <somepath>/mesa/src/shasta/authentication.rs:65:10:
called `Result::unwrap()` on an `Err` value: reqwest::Error { kind: Builder, source: Error { code: -50, message: "One or more parameters passed to a function were not valid." } }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

It's easy to determine how many certs are in the file with `openssl`:
```bash
while openssl x509 -noout -subject; do :; done < ~/.config/manta/alps_root_cert.2certsin1.pem
```

## Example

### Get latest (most recent) session
Expand Down
37 changes: 36 additions & 1 deletion src/cli/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{arg, value_parser, ArgAction, ArgGroup, Command};
use clap::{arg, value_parser, ArgAction, ArgGroup, Command, Arg};

use std::path::PathBuf;

Expand Down Expand Up @@ -35,6 +35,14 @@ pub fn build_cli(hsm_group: Option<&String>) -> Command {
.arg(arg!(-i --"image-id" <IMAGE_ID> "Image ID to use as a container image").required(true))
),
)
.subcommand(
Command::new("migrate")
.alias("m")
.arg_required_else_help(true)
.about("Migrate vCluster")
.subcommand(subcommand_migrate_backup())
.subcommand(subcommand_migrate_restore()),
)
.subcommand(
Command::new("update")
.alias("u")
Expand Down Expand Up @@ -561,3 +569,30 @@ pub fn subcommand_update_hsm_group(hsm_group: Option<&String>) -> Command {

update_hsm_group
}

pub fn subcommand_migrate_backup() -> Command {
let mut migrate_backup = Command::new("backup")
.aliases(["mb"])
.arg_required_else_help(true)
.about("Backup the configuration (BOS, CFS, image and HSM group) of a given vCluster/BOS session template.")
.arg(arg!(-b --"bos" <SESSIONTEMPLATE> "BOS Sessiontemplate to use to derive CFS, boot parameters and HSM group"))
.arg(arg!(-d --"destination" <FOLDER> "Destination folder to store the backup on"));

migrate_backup
}
// TODO
pub fn subcommand_migrate_restore() -> Command {
let mut migrate_restore = Command::new("restore")
.aliases(["mr"])
.arg_required_else_help(true)
.about("MIGRATE RESTORE of all the nodes in a HSM group. Boot configuration means updating the image used to boot the machine. Configuration of a node means the CFS configuration with the ansible scripts running once a node has been rebooted.\neg:\nmanta update hsm-group --boot-image <boot cfs configuration name> --desired-configuration <desired cfs configuration name>")
.arg(arg!(-b --"boot-image" <CFS_CONFIG> "CFS configuration name related to the image to boot the nodes"))
.arg(arg!(-d --"desired-configuration" <CFS_CONFIG> "CFS configuration name to configure the nodes after booting"));

// migrate_restore = match hsm_group {
// Some(_) => update_hsm_group,
// None => update_hsm_group.arg(arg!(<HSM_GROUP_NAME> "HSM group name").required(true)),
// };

migrate_restore
}
4 changes: 3 additions & 1 deletion src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ pub mod get_nodes;
pub mod get_session;
pub mod get_template;
pub mod log;
pub mod migrate_backup;
pub mod migrate_restore;
pub mod update_hsm_group;
pub mod update_node;
pub mod update_node;
176 changes: 176 additions & 0 deletions src/cli/commands/migrate_backup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use std::collections::HashMap;
use std::path::Path;
use mesa::shasta::{bos, hsm, ims};
use mesa::manta;
use std::fs::File;
use mesa::shasta::ims::s3::s3::{s3_auth, s3_download_object};

pub async fn exec(
shasta_token: &str,
shasta_base_url: &str,
shasta_root_cert: &[u8],
bos: Option<&String>,
destination: Option<&String>
) {
println!("Migrate_backup; BOS Template={}, Destination folder={}",bos.unwrap(), destination.unwrap());
let dest_path = Path::new(destination.unwrap());
let bucket_name = "boot-images";
let files2download = ["manifest.json", "initrd", "kernel", "rootfs"];
// let files2download = ["manifest.json"];

log::debug!("Create directory '{}'", destination.unwrap());
match std::fs::create_dir_all(dest_path) {
Ok(_ok) => _ok,
Err(error) => panic!("Unable to create directory {}. Error returned: {}", &dest_path.to_string_lossy(), error.to_string())
};
let _empty_hsm_group_name: Vec<String> = Vec::new();
let bos_templates = bos::template::http_client::filter(
shasta_token,
shasta_base_url,
shasta_root_cert,
&_empty_hsm_group_name,
Option::from(bos.unwrap()),
None,
).await.unwrap_or_default();
let mut download_counter = 1;

if bos_templates.is_empty() {
println!("No BOS template found!");
std::process::exit(0);
} else {
// BOS ------------------------------------------------------------------------------------
let bos_file_name = String::from(bos.unwrap()) + ".json";
let bos_file_path= dest_path.join(bos_file_name);
let bos_file = File::create(&bos_file_path)
.expect("bos.json file could not be created.");
println!("Downloading BOS session template {} to {} [{}/{}]", &bos.unwrap(), &bos_file_path.clone().to_string_lossy(), &download_counter, &files2download.len()+3);

// Save to file only the first one returned, we don't expect other BOS templates in the array
let _bosjson = serde_json::to_writer(&bos_file, &bos_templates[0]);
download_counter = download_counter + 1;

// HSM group -----------------------------------------------------------------------------
let hsm_file_name = String::from(bos.unwrap()) + "-hsm.json";
let hsm_file_path= dest_path.join(hsm_file_name);
let hsm_file = File::create(&hsm_file_path)
.expect("HSM file could not be created.");
println!("Downloading HSM configuration in bos template {} to {} [{}/{}]", &bos.unwrap(), &hsm_file_path.clone().to_string_lossy(), &download_counter, &files2download.len()+3);
download_counter = download_counter + 1;

let my_hsm_groups_vec = &bos_templates[0]["boot_sets"]["compute"]["node_groups"].as_array().unwrap().to_owned();
let v2: Vec<String> = my_hsm_groups_vec.iter().map(|s| s.to_string().replace("\"", "")).collect();

let mut hsm_map = HashMap::new();

for v3 in v2 {
let v4 = vec![v3.clone()];
let xnames: Vec<String> = hsm::utils::get_member_vec_from_hsm_name_vec(
shasta_token,
shasta_base_url,
shasta_root_cert,
&v4,
).await;
hsm_map.insert(v3, xnames);
}
// println!("hsm_map={:?}", hsm_map);

let _hsmjson = serde_json::to_writer(&hsm_file, &hsm_map);

// CFS ------------------------------------------------------------------------------------
let configuration_name = &bos_templates[0]["cfs"]["configuration"].to_owned().to_string();
let mut cn = configuration_name.chars();
cn.next();
cn.next_back();
// cn.as_str();
let configuration_name_clean = String::from(cn.as_str());
let cfs_configurations = manta::cfs::configuration::get_configuration(
shasta_token,
shasta_base_url,
shasta_root_cert,
Option::from(configuration_name_clean).as_ref(),
&_empty_hsm_group_name,
Option::from(true),
None,
).await;
let cfs_file_name = String::from(cn.clone().as_str()) + ".json";
let cfs_file_path= dest_path.join(&cfs_file_name);
let cfs_file = File::create(&cfs_file_path)
.expect("cfs.json file could not be created.");
println!("Downloading CFS configuration {} to {} [{}/{}]", cn.clone().as_str(), &cfs_file_path.clone().to_string_lossy(), &download_counter, &files2download.len()+2);

// Save to file only the first one returned, we don't expect other BOS templates in the array
let _cfsjson = serde_json::to_writer(&cfs_file, &cfs_configurations[0]);
download_counter = download_counter + 1;

// Image ----------------------------------------------------------------------------------
for (_boot_sets_param, boot_sets_value) in bos_templates[0]["boot_sets"]
.as_object()
.unwrap()
{
if let Some(path) = boot_sets_value.get("path") {
let image_id_related_to_bos_sessiontemplate = path
.as_str()
.unwrap()
.trim_start_matches("s3://boot-images/")
.trim_end_matches("/manifest.json")
.to_string();

log::info!(
"Get image details for ID {}",
image_id_related_to_bos_sessiontemplate
);

if ims::image::http_client::get(
shasta_token,
shasta_base_url,
shasta_root_cert,
&_empty_hsm_group_name,
Some(&image_id_related_to_bos_sessiontemplate),
None,
None
)
.await
.is_ok()
{
let image_id = image_id_related_to_bos_sessiontemplate.clone().to_string();
log::info!(
"Image ID found related to BOS sessiontemplate {} is {}",
&bos.unwrap(),
image_id_related_to_bos_sessiontemplate
);

let sts_value = match s3_auth(&shasta_token, &shasta_base_url, &shasta_root_cert).await
{
Ok(sts_value) => {
log::debug!("Debug - STS token:\n{:#?}", sts_value);
sts_value
}
Err(error) => panic!("{}", error.to_string())
};
for file in files2download {
let dest = String::from(destination.unwrap()) + "/" + &image_id;
let src = image_id.clone() + "/" + file;
println!("Downloading image file {} to {}/{} [{}/{}]", &src, &dest, &file, &download_counter, &files2download.len()+2);
let _result = match s3_download_object(&sts_value,
&src,
&bucket_name,
&dest).await {
Ok(_result) => {
download_counter = download_counter + 1;
},
Err(error) => panic!("Unable to download file {} from s3. Error returned: {}", &src, error.to_string())
};
}
// Here the image should be downloaded already
};
}
}

// bos::template::utils::print_table(bos_templates);
}

// Extract in json format:
// - the contents of the HSM group referred in the bos-session template

std::process::exit(0);
}
37 changes: 37 additions & 0 deletions src/cli/commands/migrate_restore.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use mesa::shasta::bos;

pub async fn exec(
shasta_token: &str,
shasta_base_url: &str,
hsm_group_name: Option<&String>,
template_name: Option<&String>,
most_recent: Option<bool>,
limit: Option<&u8>,
) {
let limit_number;

if let Some(true) = most_recent {
limit_number = Some(&1);
} else if let Some(false) = most_recent {
limit_number = limit;
} else {
limit_number = None;
}
//
// let bos_templates = bos::template::http_client::get(
// shasta_token,
// shasta_base_url,
// hsm_group_name,
// template_name,
// limit_number,
// )
// .await
// .unwrap_or_default();

// if bos_templates.is_empty() {
// println!("No BOS template found!");
// std::process::exit(0);
// } else {
// bos::template::utils::print_table(bos_templates);
// }
}
22 changes: 18 additions & 4 deletions src/cli/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use super::commands::{
apply_node_on, apply_node_reset, apply_session, config_set_hsm, config_set_log,
config_set_site, config_show, config_unset_auth, config_unset_hsm,
console_cfs_session_image_target_ansible, console_node, get_configuration, get_hsm, get_images,
get_nodes, get_session, get_template, update_hsm_group, update_node,
get_nodes, get_session, get_template, update_hsm_group, update_node, migrate_backup, migrate_restore,
};

pub async fn process_cli(
Expand Down Expand Up @@ -678,7 +678,7 @@ pub async fn process_cli(
k8s_api_url,
cli_console_node.get_one::<String>("XNAME").unwrap(),
)
.await;
.await;
} else if let Some(cli_console_target_ansible) =
cli_console.subcommand_matches("target-ansible")
{
Expand All @@ -695,7 +695,7 @@ pub async fn process_cli(
shasta_base_url,
shasta_root_cert,
)
.await
.await
};

console_cfs_session_image_target_ansible::exec(
Expand All @@ -712,7 +712,21 @@ pub async fn process_cli(
.get_one::<String>("SESSION_NAME")
.unwrap(),
)
.await;
.await;
}
} else if let Some(cli_migrate) = cli_root.subcommand_matches("migrate") {
if let Some(cli_migrate) = cli_migrate.subcommand_matches("backup") {
let bos = cli_migrate.get_one::<String>("bos");
let destination = cli_migrate.get_one::<String>("destination");
migrate_backup::exec(
shasta_token,
shasta_base_url,
shasta_root_cert,
bos,
destination
).await;
} else if let Some(cli_migrate) = cli_migrate.subcommand_matches("restore") {
log::info!(">>> MIGRATE RESTORE not implemented yet")
}
} else if let Some(cli_delete) = cli_root.subcommand_matches("delete") {
let hsm_name_available_vec = if let Some(hsm_name) = hsm_group_name_opt {
Expand Down

0 comments on commit c730abe

Please sign in to comment.