From c89d94dd66b0888e877731dc44367b93ad0a740e Mon Sep 17 00:00:00 2001 From: garrettmills Date: Sun, 23 Feb 2025 12:42:48 -0500 Subject: [PATCH] Port over initial implementation of API server and simplify configuration --- .gitignore | 1 + Cargo.lock | 296 +----------------- Cargo.toml | 3 +- Rocket.toml | 5 + package.sh | 18 ++ resources/views/settings.html.hbs | 27 -- src/api/cluster/carrier.rs | 36 ++- src/api/cluster/node.rs | 4 + src/api/cluster/volume.rs | 58 ++-- .../m20241101_000001_create_settings_table.rs | 58 ---- src/api/db/migrations/mod.rs | 2 - src/api/db/mod.rs | 8 +- src/api/entity/locks.rs | 7 + src/api/entity/mod.rs | 1 - src/api/entity/nodes.rs | 72 +++-- src/api/entity/settings.rs | 29 -- src/api/entity/volumes.rs | 12 +- src/api/mod.rs | 2 +- src/api/route/configure.rs | 51 --- src/api/route/mod.rs | 4 +- src/api/route/volume.rs | 25 +- src/api/services.rs | 49 ++- src/api/util.rs | 45 ++- src/main.rs | 5 + start.sh | 3 + 25 files changed, 260 insertions(+), 561 deletions(-) create mode 100755 package.sh delete mode 100644 resources/views/settings.html.hbs delete mode 100644 src/api/db/migrations/m20241101_000001_create_settings_table.rs delete mode 100644 src/api/entity/settings.rs delete mode 100644 src/api/route/configure.rs create mode 100755 start.sh diff --git a/.gitignore b/.gitignore index ed78c4e..7b6ade0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea k *.sqlite +.env diff --git a/Cargo.lock b/Cargo.lock index 4e137dd..1c27f8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,12 +188,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.4.0" @@ -675,6 +669,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dotenvy" version = "0.15.7" @@ -1023,25 +1023,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "h2" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.1.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "handlebars" version = "5.1.2" @@ -1180,29 +1161,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.1.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "pin-project-lite", -] - [[package]] name = "httparse" version = "1.9.5" @@ -1231,9 +1189,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2", "http 0.2.12", - "http-body 0.4.6", + "http-body", "httparse", "httpdate", "itoa", @@ -1245,78 +1203,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.6", - "http 1.1.0", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" -dependencies = [ - "futures-util", - "http 1.1.0", - "hyper 1.5.0", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.5.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "hyper 1.5.0", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1413,12 +1299,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "ipnet" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" - [[package]] name = "is-terminal" version = "0.4.13" @@ -1917,11 +1797,11 @@ name = "p5x" version = "0.1.0" dependencies = [ "async-trait", + "dotenv", "env_logger", "futures", "log", "proxmox-api", - "reqwest", "rocket", "rocket_dyn_templates", "sea-orm", @@ -1929,7 +1809,6 @@ dependencies = [ "sea-orm-rocket", "serde", "serde_json", - "serde_urlencoded", "ssh2", "tokio", "ureq", @@ -2392,49 +2271,6 @@ dependencies = [ "bytecheck", ] -[[package]] -name = "reqwest" -version = "0.12.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.4.6", - "http 1.1.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.5.0", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-registry", -] - [[package]] name = "ring" version = "0.17.8" @@ -2557,7 +2393,7 @@ dependencies = [ "either", "futures", "http 0.2.12", - "hyper 0.14.31", + "hyper", "indexmap", "log", "memchr", @@ -2644,15 +2480,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.10.0" @@ -3405,36 +3232,6 @@ dependencies = [ "syn 2.0.86", ] -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" -dependencies = [ - "futures-core", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.6.0", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tap" version = "1.0.1" @@ -3558,27 +3355,6 @@ dependencies = [ "syn 2.0.86", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" -dependencies = [ - "rustls", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.16" @@ -3906,18 +3682,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.95" @@ -3947,16 +3711,6 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" -[[package]] -name = "web-sys" -version = "0.3.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-roots" version = "0.26.6" @@ -4025,36 +3779,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result", - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 7218000..324820a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,8 @@ rocket_dyn_templates = { version = "0.2.0", features = ["handlebars"] } sea-orm-migration = "1.1.0" uuid = { version = "1.11.0", features = ["v4", "fast-rng"] } ssh2 = "0.9.4" -reqwest = "0.12.9" futures = { version = "0.3.31", features = ["executor"] } serde_json = "1.0.132" -serde_urlencoded = "0.7.1" proxmox-api = { git = "https://github.com/glmdev/p5x-proxmox-api", version = "0.1.2-pre", features = ["ureq-client"] } ureq = "2.10.1" +dotenv = "0.15.0" diff --git a/Rocket.toml b/Rocket.toml index 09a93a6..ebc2bef 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -1,5 +1,10 @@ [default] template_dir = "resources/views" +address = "0.0.0.0" +port = 3450 + +[default.p5x] +pvemaster = "" [default.databases.p5x_api] url = "sqlite://p5x_api.sqlite?mode=rwc" diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..af111b0 --- /dev/null +++ b/package.sh @@ -0,0 +1,18 @@ +#!/bin/bash -e + +P_NAME="p5x-$(arch)" + +cargo build --release +mkdir "$P_NAME" +cd "$P_NAME" + +cp -r ../resources . +cp -r ../Rocket.toml . +cp -r ../start.sh . +cp -r ../target/release/p5x . + +cd .. +zip -r "$P_NAME.zip" "$P_NAME" +rm -rf "$P_NAME" + + diff --git a/resources/views/settings.html.hbs b/resources/views/settings.html.hbs deleted file mode 100644 index ed546b5..0000000 --- a/resources/views/settings.html.hbs +++ /dev/null @@ -1,27 +0,0 @@ - - - Settings | P5x - - -

Configure P5x

- Current config version: {{#if settings.id}}v{{settings.id}}{{^}}v0{{/if}} -
-
Proxmox VE
- - -
- - -
- - -
- - -
- Storage pool must be network-attached (Ceph/iSCSI). NFS is discouraged because it lacks proper support for locking.
- -
-
- - diff --git a/src/api/cluster/carrier.rs b/src/api/cluster/carrier.rs index b298b1e..d74a3cd 100644 --- a/src/api/cluster/carrier.rs +++ b/src/api/cluster/carrier.rs @@ -3,14 +3,16 @@ use uuid::Uuid; use proxmox_api::nodes::node::lxc; use proxmox_api::nodes::node::lxc::vmid; use proxmox_api::types::VmId; -use sea_orm::{ActiveModelTrait, EntityTrait, Set, TryIntoModel}; +use sea_orm::*; use crate::api::cluster::node::wait_upid; use crate::api::entity::locks::lock_vmid; -use crate::api::entity::nodes; +use crate::api::entity::{locks, nodes}; use crate::api::entity::nodes::P5xError; use crate::api::services::{ssh_run_trimmed, Services}; use crate::api::services::SshError; + +/** Create an empty Proxmox LXC container that can be used to shuttle a volume between PVE nodes. */ pub async fn provision_carrier( svc: &Services<'_>, from_node: &nodes::Model, @@ -32,8 +34,7 @@ pub async fn provision_carrier( // Build the new container params let hostname = format!("carrier-{}", Uuid::new_v4().to_string()); - let storage = svc.setting_req(|s| &s.pve_storage_pool) - .map_err(P5xError::ServiceError)?; + let storage = &svc.config.pve_storage_pool; let mut params = lxc::PostParams::new("local:vztmpl/p5x-empty.tar.xz".to_string(), vm_id); params.cores = Some(1); @@ -41,7 +42,7 @@ pub async fn provision_carrier( params.hostname = Some(hostname.to_string()); params.memory = Some(16); // in MB, min 16 params.start = Some(false); - params.storage = Some(storage); + params.storage = Some(storage.to_string()); params.tags = Some("p5x".to_string()); // Ask the PVE API to start creating the carrier node based on our empty template @@ -68,9 +69,23 @@ pub async fn provision_carrier( .await .map_err(P5xError::DbErr)?; - node.try_into_model().map_err(P5xError::DbErr) + let node = node.try_into_model().map_err(P5xError::DbErr)?; + + // Create a lock instance for the new node + locks::ActiveModel { + lock_type: Set("nodes".to_string()), + lock_resource: Set(node.id.to_string()), + ..Default::default() + } + .save(svc.db) + .await + .map_err(P5xError::DbErr)?; + + Ok(node) } + +/** Make sure the tarball LXC template for the carrier container exists on the given node. */ pub async fn ensure_carrier_template( svc: &Services<'_>, node: &nodes::Model, @@ -100,6 +115,8 @@ pub async fn ensure_carrier_template( Ok(()) } + +/** Destroy the given carrier LXC container. */ pub async fn terminate_carrier( svc: &Services<'_>, carrier: nodes::Model, @@ -122,5 +139,12 @@ pub async fn terminate_carrier( .await .map_err(P5xError::DbErr)?; + locks::Entity::delete_many() + .filter(locks::Column::LockType.eq("nodes")) + .filter(locks::Column::LockResource.eq(carrier.id.to_string())) + .exec(svc.db) + .await + .map_err(P5xError::DbErr)?; + Ok(()) } diff --git a/src/api/cluster/node.rs b/src/api/cluster/node.rs index 92c94eb..6a40640 100644 --- a/src/api/cluster/node.rs +++ b/src/api/cluster/node.rs @@ -7,6 +7,8 @@ use crate::api::entity::nodes; use crate::api::entity::nodes::P5xError; use crate::api::services::Services; + +/** Migrate an LXC container from its current PVE node to the given PVE node. */ pub async fn migrate_node( svc: &Services<'_>, node: nodes::Model, @@ -37,6 +39,8 @@ pub async fn migrate_node( .ok_or(P5xError::BadPostcondition("Could not look up node after persisting")) } + +/** Wait for a PVE task to complete using its UPID */ pub async fn wait_upid(svc: &Services<'_>, node: &str, upid: &str) -> Result<(), P5xError> { info!("Waiting for UPID {upid} on node {node}"); let pve = svc.pve_node(node) diff --git a/src/api/cluster/volume.rs b/src/api/cluster/volume.rs index 9677fe3..c9362d9 100644 --- a/src/api/cluster/volume.rs +++ b/src/api/cluster/volume.rs @@ -5,16 +5,20 @@ use proxmox_api::nodes::node::lxc::vmid::config::PutParams; use proxmox_api::nodes::node::lxc::vmid::move_volume; use proxmox_api::types::VmId; use proxmox_api::UreqError; +use proxmox_api::UreqError::Ureq; use sea_orm::*; use sea_orm::ActiveValue::Set; use serde::{Deserialize, Serialize}; use tokio::time::sleep; +use ureq::Error::Status; use crate::api::cluster::carrier::{provision_carrier, terminate_carrier}; use crate::api::cluster::node::migrate_node; use crate::api::entity::nodes::{lock_first_available, P5xError}; use crate::api::entity::{nodes, volumes}; use crate::api::services::{ssh_run_trimmed, Services}; + +/** Parameters required from an API call to manage a volume. */ #[derive(Serialize, Deserialize, FromForm)] pub struct VolumeParams { pub id: Option, @@ -25,6 +29,7 @@ pub struct VolumeParams { } impl VolumeParams { + /** Look up a volume by name and get its params. */ pub async fn resolve(svc: &Services<'_>, name: &str) -> Result, DbErr> { volumes::Entity::find() .filter(volumes::Column::Name.eq(name)) @@ -34,6 +39,8 @@ impl VolumeParams { } } + +/** Create a new PVE volume of the given size. */ pub async fn create( svc: &Services<'_>, name: &str, @@ -63,8 +70,7 @@ pub async fn create( info!("Volume {name} will become mp{mp_id} on node {} ({})", node.hostname, node.pve_id); // Generate a new mountpoint entry for the node's config - let storage = svc.setting_req(|s| &s.pve_storage_pool) - .map_err(P5xError::ServiceError)?; + let storage = &svc.config.pve_storage_pool; let size_in_gib = max((size_in_bytes as u64).div_ceil(1024 * 1024 * 1024) as i64, 1); let line = format!("{storage}:{size_in_gib},mp=/mnt/p5x-{name},backup=1"); debug!("Volume {name}: {line}"); @@ -100,11 +106,12 @@ pub async fn create( debug!("Found mountpoint details for volume {name}: {mount}"); // Parse the disk name from the config - let name_offset = format!("{storage}:{}/", node.pve_id).len() + 1; + // synology-scsi-lun:vm-103-disk-1,mp=/mnt/p5x-test0,backup=1,size=1G // fixme: how does this behave for NFS? + let name_offset = storage.len() + 1; // 1 for the colon (:) let disk_name = mount[name_offset..].split(",").next().unwrap(); // Persist the volume - debug!("Inserting record into volumes table for volume {name}"); + debug!("Inserting record into volumes table for volume {name} ({disk_name})"); let res = volumes::ActiveModel { name: Set(name.to_string()), size_in_bytes: Set(size_in_bytes), @@ -126,6 +133,8 @@ pub async fn create( Ok(vol) } + +/** Mount a volume to the specified mountpoint on the LXC container it is attached to. */ pub async fn mount( svc: &Services<'_>, params: &VolumeParams, @@ -162,17 +171,21 @@ pub async fn mount( let mut put_params = PutParams::default(); put_params.mps.insert(mountpoint_identifier, mount_line); - debug!("Patching node config to mount volume {}", params.name); + debug!("Patching node config to mount volume {} ({put_params:?})", params.name); let res = pve_node.lxc() .vmid(node.vm_id()) .config() .put(put_params); + if let Err(Ureq(Status(_, ires))) = res { + debug!("PVE response: {}", ires.into_string().unwrap()); + } + // This is necessary because PUT returns {data: null} on success, // which the UreqClient flags as an unknown error. - if let Err(UreqError::EncounteredErrors(e)) = res { + /*if let Err(UreqError::EncounteredErrors(e)) = res { return Err(P5xError::PveError(UreqError::EncounteredErrors(e))); - } + }*/ // Persist the volume debug!("Persisting mount changes to volume {} in database", params.name); @@ -187,6 +200,8 @@ pub async fn mount( Ok(()) } + +/** Unmount a volume from the LXC container it is attached to. */ pub async fn unmount( svc: &Services<'_>, params: &VolumeParams, @@ -238,8 +253,8 @@ pub async fn unmount( ssh_run_trimmed(&pve_ssh, &cmd)?; // For LVM-type storage pools, we also need to deactivate the logical volume - let pool_name = svc.setting_req(|s| &s.pve_storage_pool).map_err(P5xError::ServiceError)?; - let pool_driver = svc.setting_req(|s| &s.pve_storage_driver).map_err(P5xError::ServiceError)?; + let pool_name = &svc.config.pve_storage_pool; + let pool_driver = &svc.config.pve_storage_driver; if pool_driver == "lvm" { let cmd = format!("lvchange -aln '/dev/{pool_name}/{}'", vol.disk_name.as_ref().unwrap()); ssh_run_trimmed(&pve_ssh, &cmd)?; @@ -288,6 +303,8 @@ pub async fn unmount( // This was *such* a pain in the ass to figure out, but it's a testament to open- // source that I was able to do it at all. + +/** Delete the given volume from the PVE cluster. */ pub async fn delete( svc: &Services<'_>, params: &VolumeParams, @@ -340,11 +357,13 @@ pub async fn delete( Ok(()) } + +/** Migrate a volume from its current LXC container to the specified LXC container. */ pub async fn transfer( svc: &Services<'_>, params: &VolumeParams, to_node: &nodes::Model, -) -> Result<(), P5xError> { +) -> Result { // Look up the volume from the params let vol = volumes::resolve(svc, params).await?; @@ -352,13 +371,12 @@ pub async fn transfer( // If the volume already resides on to_node, we're done if from_node.pve_id == to_node.pve_id { - return Ok(()); + return Ok(vol); } // If from_node and to_node are on the same physical host, transfer directly if from_node.pve_host == to_node.pve_host { - transfer_directly(svc, vol, &from_node, to_node).await?; - return Ok(()); + return transfer_directly(svc, vol, &from_node, to_node).await; } // If the nodes are on different physical hosts, we need to create a temporary @@ -368,13 +386,15 @@ pub async fn transfer( let vol = transfer_directly(svc, vol, &from_node, &carrier).await?; let carrier = migrate_node(svc, carrier, &to_node.pve_host).await?; - let _vol = transfer_directly(svc, vol, &carrier, &to_node).await?; + let vol = transfer_directly(svc, vol, &carrier, &to_node).await?; terminate_carrier(svc, carrier).await?; - Ok(()) + Ok(vol) } + +/** Migrate a volume from one LXC container to another LXC container, when both reside on the same PVE host. */ async fn transfer_directly( svc: &Services<'_>, vol: volumes::Model, @@ -431,11 +451,11 @@ async fn transfer_directly( .ok_or(P5xError::BadPostcondition("Could not find mountpoint config after transferring volume"))?; // Parse the disk name from the config - let storage = svc.setting_req(|s| &s.pve_storage_pool) - .map_err(P5xError::ServiceError)?; - let name_offset = format!("{storage}:{}/", to_node.pve_id).len() + 1; + // synology-scsi-lun:vm-103-disk-1,mp=/mnt/p5x-test0,backup=1,size=1G + let storage = &svc.config.pve_storage_pool; + let name_offset = storage.len() + 1; // 1 for the colon (:) let disk_name = mount[name_offset..].split(",").next().unwrap(); - +debug!("transfer_directly: mount {mount} | name_offset {name_offset} | disk_name {disk_name}"); // Persist the volume let mut vol = vol.into_active_model(); vol.pve_node_id = Set(to_node.pve_id); diff --git a/src/api/db/migrations/m20241101_000001_create_settings_table.rs b/src/api/db/migrations/m20241101_000001_create_settings_table.rs deleted file mode 100644 index f9354e6..0000000 --- a/src/api/db/migrations/m20241101_000001_create_settings_table.rs +++ /dev/null @@ -1,58 +0,0 @@ -use async_trait::async_trait; -use sea_orm_migration::{prelude::*, schema::*}; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Settings::Table) - .if_not_exists() - .col(pk_auto(Settings::Id)) - .col(string_null(Settings::PveMasterNode)) - .col(string_null(Settings::PveApiHost)) - .col(string_null(Settings::PveRootPassword)) - .col(string_null(Settings::PveStoragePool)) - .col(string_null(Settings::PveStorageDriver)) - .col(string_null(Settings::DnsDomain)) - .col(string_null(Settings::NodeNetworkBridge)) - .col(big_unsigned_null(Settings::NodeCpus)) - .col(big_unsigned_null(Settings::NodeRamInMib)) - .col(string_null(Settings::RootPassword)) - .col(text_null(Settings::SshPublicKey)) - .col(text_null(Settings::SshPrivateKey)) - .to_owned()) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table( - Table::drop() - .table(Settings::Table) - .to_owned()) - .await - } -} - -#[derive(DeriveIden)] -enum Settings { - Table, - Id, - PveMasterNode, - PveApiHost, - PveRootPassword, - PveStoragePool, - PveStorageDriver, - DnsDomain, - NodeNetworkBridge, - NodeCpus, - NodeRamInMib, - RootPassword, - SshPublicKey, - SshPrivateKey, -} diff --git a/src/api/db/migrations/mod.rs b/src/api/db/migrations/mod.rs index 6a03f53..4866926 100644 --- a/src/api/db/migrations/mod.rs +++ b/src/api/db/migrations/mod.rs @@ -5,7 +5,6 @@ pub use sea_orm_migration::prelude::*; use sea_orm_rocket::Database; use crate::api::Db; -mod m20241101_000001_create_settings_table; mod m20241102_000001_create_nodes_table; mod m20241102_000002_create_locks_table; mod m20241103_000001_create_volumes_table; @@ -16,7 +15,6 @@ pub struct Migrator; impl MigratorTrait for Migrator { fn migrations() -> Vec> { vec![ - Box::new(m20241101_000001_create_settings_table::Migration), Box::new(m20241102_000001_create_nodes_table::Migration), Box::new(m20241102_000002_create_locks_table::Migration), Box::new(m20241103_000001_create_volumes_table::Migration), diff --git a/src/api/db/mod.rs b/src/api/db/mod.rs index c449fc3..59effb2 100644 --- a/src/api/db/mod.rs +++ b/src/api/db/mod.rs @@ -26,9 +26,13 @@ impl Pool for DbPool { let config = figment.extract::().unwrap(); let mut options: ConnectOptions = config.url.into(); + debug!("max_connections: {} | min: {}", config.max_connections, config.min_connections.unwrap_or_default()); + options - .max_connections(config.max_connections as u32) - .min_connections(config.min_connections.unwrap_or_default()) + // .max_connections(config.max_connections as u32) + // .min_connections(config.min_connections.unwrap_or_default()) + .max_connections(512) + .min_connections(128) .connect_timeout(Duration::from_secs(config.connect_timeout)) .sqlx_logging(config.sqlx_logging); diff --git a/src/api/entity/locks.rs b/src/api/entity/locks.rs index df202f5..cae6904 100644 --- a/src/api/entity/locks.rs +++ b/src/api/entity/locks.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use tokio::time::sleep; use uuid::Uuid; +/** Try to acquire the given lock, if it is not held. */ pub async fn try_lock<'a>( db: &'a DatabaseConnection, lock_type: &str, @@ -36,6 +37,8 @@ pub async fn try_lock<'a>( lock.map(|lock | LockHandle::new(lock, db))) } + +/** Acquire the given lock, lazy-waiting until it is available. */ pub async fn lock<'a>( db: &'a DatabaseConnection, lock_type: &str, @@ -53,10 +56,14 @@ pub async fn lock<'a>( } } + +/** Acquire the global lock for an LXC container, lazy-waiting until it is available. */ pub async fn lock_vmid<'a>(db: &'a DatabaseConnection, lock_reason: Option<&str>) -> Result, DbErr> { lock(db, "global_vmid", "0", lock_reason).await } + +/** A held lock. */ #[derive(Clone)] pub struct LockHandle<'a> { pub lock: Model, diff --git a/src/api/entity/mod.rs b/src/api/entity/mod.rs index d9ec75c..0c2ce0d 100644 --- a/src/api/entity/mod.rs +++ b/src/api/entity/mod.rs @@ -1,4 +1,3 @@ -pub mod settings; pub mod locks; pub mod nodes; pub mod volumes; diff --git a/src/api/entity/nodes.rs b/src/api/entity/nodes.rs index 77a92ac..e7f5ff4 100644 --- a/src/api/entity/nodes.rs +++ b/src/api/entity/nodes.rs @@ -3,15 +3,17 @@ use std::net::TcpStream; use ssh2::{OpenFlags, OpenType, Session}; use std::io::{Read, Write}; use std::path::Path; +use std::str::FromStr; use std::time::Duration; use sea_orm::entity::prelude::*; use sea_orm::QueryOrder; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Deserializer, Serialize}; use tokio::time::sleep; use proxmox_api; use log::{warn, debug}; use proxmox_api::types::VmId; -use crate::api::entity::{locks, settings}; +use serde::de::Error; +use crate::api::entity::locks; use crate::api::entity::locks::{lock, try_lock, LockHandle}; use crate::api::services::{Services, ServiceError, SshError, ssh_run_trimmed}; @@ -34,6 +36,12 @@ pub enum P5xError { UpidFailed(String, String), } + +/** + * Acquire the first free LXC container lock. + * Useful for operations that can occur on an arbitrary node (e.g. creating a new volume). + * Lazy-wait until a lock is acquired. + */ pub async fn lock_first_available<'a>( db: &'a DatabaseConnection, lock_reason: Option<&str>, @@ -54,6 +62,11 @@ pub async fn lock_first_available<'a>( Err(NodeLockErr::WaitTimeLimitExceeded) } + +/** + * Try to acquire the first free LXC container lock. + * Useful for operations that can occur on an arbitrary node (e.g. creating a new volume). + */ pub async fn try_lock_first_available<'a>( db: &'a DatabaseConnection, lock_reason: Option<&str>, @@ -84,6 +97,7 @@ pub async fn try_lock_first_available<'a>( } +/** The PVE config file for an LXC container (i.e. /etc/pve/lxc/XYZ.conf) */ pub struct PveConfig { lines: Vec, } @@ -97,6 +111,7 @@ impl PveConfig { PveConfig { lines } } + /** Replace a line in the config file. */ pub fn replace( &mut self, matcher: F1, @@ -115,6 +130,7 @@ impl PveConfig { }); } + /** Run a map function on every line in the config file. */ pub fn map( &mut self, replacer: F @@ -128,15 +144,30 @@ impl PveConfig { .collect(); } + /** + * Get the config setting based on its key. + * Example: If key="fubar", it will find the line "fubar: something" + * and return "something" + */ pub fn get(&self, key: &str) -> Option { let key = format!("{key}:"); - self.lines + let val = self.lines .iter() .filter(|line| line.starts_with(&key)) .next() - .map(|s| s.to_string()) + .map(|s| s.to_string())?; + + let offset = key.len() + 1; + let val = val[offset..].trim(); + Some(val.to_string()) } + /** + * Proxmox stores configs for resources that can have duplicates (e.g. volumes) in a predictable + * format. For example, volumes will have "volume0: ..." / "volume1: ..." / &c. + * This method will find the first free number for a given prefix. + * Using that example, next_nth("volume") would return 2. + */ pub fn next_nth(&self, prefix: &str) -> u32 { let res = self.lines .iter() @@ -178,16 +209,12 @@ pub struct Model { } impl Model { - fn setting(&self, svc: &Services, f: F) -> Result - where - F: FnOnce(&settings::Model) -> &Option { - svc.setting_req(f).map_err(P5xError::ServiceError) - } - + /** Get the VM ID used by the Proxmox API library. */ pub fn vm_id(&self) -> VmId { VmId::new(i64::from(self.pve_id)).unwrap() } + /** Open an SSH session to this LXC container. */ pub fn ssh(&self, svc: &Services) -> Result { let addr = format!("{}:22", self.assigned_ip); let tcp = TcpStream::connect(addr) @@ -203,11 +230,10 @@ impl Model { .map_err(SshError::ClientError) .map_err(P5xError::SshError)?; - let pubkey = self.setting(svc, |s| &s.ssh_public_key)?; - let privkey = self.setting(svc, |s| &s.ssh_private_key)?; - log::debug!("privkey: {privkey}"); + let pubkey = &svc.config.ssh_pubkey; + let privkey = &svc.config.ssh_privkey; - sess.userauth_pubkey_memory("root", Some(&pubkey), &privkey, None) + sess.userauth_pubkey_memory("root", Some(pubkey), privkey, None) .map_err(SshError::ClientError) .map_err(P5xError::SshError)?; @@ -218,6 +244,7 @@ impl Model { Ok(sess) } + /** Load the LXC container config for this node. */ pub fn config(&self, svc: &Services) -> Result { let pve_ssh = svc.pve_ssh(&self.pve_host) .map_err(P5xError::ServiceError)?; @@ -243,6 +270,7 @@ impl Model { Ok(PveConfig::new(&contents)) } + /** Replace the LXC container config for this node. */ pub fn write_config(&self, svc: &Services, conf: &PveConfig) -> Result<(), P5xError> { let conf = conf.to_string(); let conf = conf.as_bytes(); @@ -267,33 +295,24 @@ impl Model { .map_err(SshError::IoError) .map_err(P5xError::SshError)?; - /*let mut remote_file = pve_ssh.scp_send(path, 0o640, conf.len() as u64, None) - .map_err(SshError::ClientError) - .map_err(P5xError::SshError)?; - - remote_file.write_all(conf).map_err(SshError::IoError).map_err(P5xError::SshError)?;*/ - - // Close the channel and wait for the whole content to be transferred - /*remote_file.send_eof().map_err(SshError::ClientError).map_err(P5xError::SshError)?; - remote_file.wait_eof().map_err(SshError::ClientError).map_err(P5xError::SshError)?; - remote_file.close().map_err(SshError::ClientError).map_err(P5xError::SshError)?; - remote_file.wait_close().map_err(SshError::ClientError).map_err(P5xError::SshError)?;*/ - Ok(()) } + /** Try to acquire the node lock for this LXC container. Used for any operations that impact the config. */ pub async fn try_lock<'a>(&self, svc: &Services<'a>, lock_reason: Option<&str>) -> Result>, NodeLockErr> { try_lock(svc.db, "nodes", &self.id.to_string(), lock_reason) .await .map_err(NodeLockErr::DbErr) } + /** Block until we acquire the node lock for this LXC container. Used for any operations that impact the config. */ pub async fn lock<'a>(&self, svc: &Services<'a>, lock_reason: Option<&str>) -> Result, NodeLockErr> { lock(svc.db, "nodes", &self.id.to_string(), lock_reason) .await .map_err(NodeLockErr::DbErr) } + /** Helper for loading, modifying, then replacing the config, while managing the lock automatically. */ pub async fn mutate_config( &self, svc: &Services<'_>, @@ -314,6 +333,7 @@ impl Model { self.write_config(svc, &config) } + /** Run an SSH command on the node and return the output of the command, with whitespace trimmed. */ pub fn ssh_run_trimmed(&self, svc: &Services, cmd: &str) -> Result { let node_ssh = self.ssh(svc)?; ssh_run_trimmed(&node_ssh, cmd) diff --git a/src/api/entity/settings.rs b/src/api/entity/settings.rs deleted file mode 100644 index e69c4ee..0000000 --- a/src/api/entity/settings.rs +++ /dev/null @@ -1,29 +0,0 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize, FromForm)] -#[sea_orm(table_name = "settings")] -pub struct Model { - #[sea_orm(primary_key)] - #[field(default = 0)] - pub id: i32, - pub pve_master_node: Option, - pub pve_api_host: Option, - pub pve_root_password: Option, - pub pve_storage_pool: Option, - pub pve_storage_driver: Option, - // pub dns_domain: Option, - // pub node_network_bridge: Option, - // pub node_cpus: Option, - // pub node_ram_in_mib: Option, - // pub root_password: Option, - #[sea_orm(column_type = "Text")] - pub ssh_public_key: Option, - #[sea_orm(column_type = "Text")] - pub ssh_private_key: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/src/api/entity/volumes.rs b/src/api/entity/volumes.rs index 094bd82..0e1009c 100644 --- a/src/api/entity/volumes.rs +++ b/src/api/entity/volumes.rs @@ -5,6 +5,8 @@ use crate::api::entity::nodes; use crate::api::entity::nodes::P5xError; use crate::api::services::Services; + +/** Given the API volume params, look up the Volume model instance. */ pub async fn resolve( svc: &Services<'_>, params: &VolumeParams, @@ -37,6 +39,7 @@ pub struct Model { } impl Model { + /** Get the Node model for the LXC container this volume is attached to. */ pub async fn node(&self, svc: &Services<'_>) -> Result { nodes::Entity::find() .filter(nodes::Column::PveId.eq(self.pve_node_id)) @@ -46,17 +49,14 @@ impl Model { .ok_or(P5xError::BadPrecondition("Could not find node for volume: pve_node_id does not match any nodes")) } + /** Get the name of this volume as it would appear in the LXC config file. */ pub async fn qualified_name(&self, svc: &Services<'_>) -> Result { let node = self.node(svc).await?; - let storage = svc.setting_req(|s| &s.pve_storage_pool) - .map_err(P5xError::ServiceError)?; - - let storage_type = svc.setting_req(|s| &s.pve_storage_driver) - .map_err(P5xError::ServiceError)?; + let storage = &svc.config.pve_storage_pool; + let storage_type = &svc.config.pve_storage_driver; let disk_name = format!("pve-{}.raw", self.name); let disk_name = self.disk_name.as_ref().unwrap_or(&disk_name); - let qn = match storage_type.as_str() { "lvm" => format!("{storage}:{disk_name}"), _ => format!("{storage}:{}/{disk_name}", node.pve_id), diff --git a/src/api/mod.rs b/src/api/mod.rs index 1e56358..339b147 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,7 +2,7 @@ use rocket::fairing::AdHoc; mod db; mod route; -mod util; +pub mod util; mod cluster; pub mod entity; pub use db::Db; diff --git a/src/api/route/configure.rs b/src/api/route/configure.rs deleted file mode 100644 index faac92e..0000000 --- a/src/api/route/configure.rs +++ /dev/null @@ -1,51 +0,0 @@ -use rocket::fairing::AdHoc; -use rocket::form::Form; -use rocket::response::{status}; -use rocket_dyn_templates::{context, Template}; -use sea_orm::*; -use sea_orm_rocket::Connection; -use crate::api; -use crate::api::entity::settings; -use crate::api::util::raise_500; - -async fn render(db: &DatabaseConnection) -> Result> { - let settings = settings::Entity::find().order_by_desc(settings::Column::Id).one(db).await.map_err(raise_500)?; - Ok(Template::render("settings", context! { - settings: settings, - })) -} - -#[get("/")] -async fn get( - conn: Connection<'_, api::Db>, -) -> Result> { - let db = conn.into_inner(); - render(db).await -} - -#[post("/", data = "")] -async fn save( - conn: Connection<'_, api::Db>, - input: Form, -) -> Result> { - let db = conn.into_inner(); - - settings::ActiveModel { - pve_master_node: Set(input.pve_master_node.to_owned()), - pve_api_host: Set(input.pve_api_host.to_owned()), - pve_root_password: Set(input.pve_root_password.to_owned()), - pve_storage_pool: Set(input.pve_storage_pool.to_owned()), - ..Default::default() - } - .save(db) - .await - .map_err(raise_500)?; - - render(db).await -} - -pub(super) fn init() -> AdHoc { - AdHoc::on_ignite("Routes: /configure", |rocket| async { - rocket.mount("/configure", routes![get, save]) - }) -} diff --git a/src/api/route/mod.rs b/src/api/route/mod.rs index 76d5415..29b27f5 100644 --- a/src/api/route/mod.rs +++ b/src/api/route/mod.rs @@ -1,11 +1,9 @@ use rocket::fairing::AdHoc; -mod configure; mod volume; pub(super) fn init() -> AdHoc { AdHoc::on_ignite("Registering routes", |rocket| async { - rocket.attach(configure::init()) - .attach(volume::init()) + rocket.attach(volume::init()) }) } diff --git a/src/api/route/volume.rs b/src/api/route/volume.rs index e57a575..b6f2620 100644 --- a/src/api/route/volume.rs +++ b/src/api/route/volume.rs @@ -11,6 +11,7 @@ use crate::api::entity::nodes::P5xError; use crate::api::services::Services; use crate::api::util::raise_500; + #[get("/")] async fn get_vol( conn: Connection<'_, api::Db>, @@ -27,6 +28,7 @@ async fn get_vol( Ok(Json(vol)) } + #[post("/", data = "")] async fn create_vol( conn: Connection<'_, api::Db>, @@ -46,11 +48,12 @@ async fn create_vol( Ok(Json(vol.into())) } + #[delete("/")] async fn delete_vol( conn: Connection<'_, api::Db>, name: &str, -) -> Result<(), status::Custom> { +) -> Result, status::Custom> { let db = conn.into_inner(); let svc = Services::build(db).await.map_err(raise_500)?; @@ -62,28 +65,30 @@ async fn delete_vol( cluster::volume::unmount(&svc, &vol).await.map_err(raise_500)?; cluster::volume::delete(&svc, &vol).await.map_err(raise_500)?; - Ok(()) + Ok(Json(serde_json::json!({}))) } + #[post("/mount", data = "")] async fn mount_vol( conn: Connection<'_, api::Db>, params: Json, -) -> Result<(), status::Custom> { +) -> Result, status::Custom> { let db = conn.into_inner(); let svc = Services::build(db).await.map_err(raise_500)?; let params = params.into_inner(); cluster::volume::mount(&svc, ¶ms).await.map_err(raise_500)?; - Ok(()) + Ok(Json(serde_json::json!({}))) } + #[post("/unmount/")] async fn unmount_vol( conn: Connection<'_, api::Db>, name: &str, -) -> Result<(), status::Custom> { +) -> Result, status::Custom> { let db = conn.into_inner(); let svc = Services::build(db).await.map_err(raise_500)?; @@ -94,15 +99,16 @@ async fn unmount_vol( cluster::volume::unmount(&svc, &vol).await.map_err(raise_500)?; - Ok(()) + Ok(Json(serde_json::json!({}))) } + #[post("/transfer//to/")] async fn transfer_vol( conn: Connection<'_, api::Db>, name: &str, node: &str, -) -> Result<(), status::Custom> { +) -> Result, status::Custom> { let db = conn.into_inner(); let svc = Services::build(db).await.map_err(raise_500)?; @@ -118,11 +124,12 @@ async fn transfer_vol( .ok_or(P5xError::BadPrecondition("Could not find a node with that name")) .map_err(raise_500)?; - cluster::volume::transfer(&svc, &vol, &node).await.map_err(raise_500)?; + let vol = cluster::volume::transfer(&svc, &vol, &node).await.map_err(raise_500)?; - Ok(()) + Ok(Json(vol.into())) } + pub(super) fn init() -> AdHoc { AdHoc::on_ignite("Routes: /volume", |rocket| async { rocket.mount("/volume", routes![create_vol, delete_vol, get_vol, mount_vol, unmount_vol, transfer_vol]) diff --git a/src/api/services.rs b/src/api/services.rs index 860963f..8a33bd1 100644 --- a/src/api/services.rs +++ b/src/api/services.rs @@ -2,10 +2,10 @@ use std::io::Read; use std::net::TcpStream; use proxmox_api::nodes::node::NodeClient; use proxmox_api::UreqClient; -use sea_orm::{DatabaseConnection, DbErr, EntityTrait, QueryOrder}; +use sea_orm::{DatabaseConnection, DbErr}; use ssh2::Session; use crate::api::entity::nodes::P5xError; -use crate::api::entity::settings; +use crate::api::util::{read_p5x_config, P5xConfig}; #[derive(Debug)] pub enum SshError { @@ -23,61 +23,46 @@ impl SshError { #[derive(Debug)] pub enum ServiceError { - MissingSetting, PveError(proxmox_api::UreqError), InvalidNetworkInterface, SshError(SshError), } + +/** Helper for managing singleton services. */ pub struct Services<'a> { - pub settings: Option, pub db: &'a DatabaseConnection, + pub config: P5xConfig, } impl<'a> Services<'a> { + /** Create a new services instance. */ pub async fn build(db: &'a DatabaseConnection) -> Result, DbErr> { - let settings = settings::Entity::find().order_by_desc(settings::Column::Id).one(db).await?; Ok(Services { db, - settings, + config: read_p5x_config(), }) } - pub fn setting(&self, f: F) -> Option - where - F: FnOnce(&settings::Model) -> &Option, - { - let model = self.settings.as_ref()?; - let res = f(model); - if let Some(res) = res { - return Some(res.clone()); - } - None - } - - pub fn setting_req(&self, f: F) -> Result - where - F: FnOnce(&settings::Model) -> &Option - { - self.setting(f).ok_or(ServiceError::MissingSetting) - } - - pub fn pve(&self) -> Result { - let host = self.setting_req(|s| &s.pve_api_host)?; - let pw = self.setting_req(|s| &s.pve_root_password)?; + /** Get a new Proxmox API client instance. */ + pub fn pve(&self) -> Result { + let host = &self.config.pve_api_host; + let pw = &self.config.pve_root_password; let host = format!("https://{host}:8006"); - let api = proxmox_api::UreqClient::new(&host, "root", "pam", &pw) + let api = UreqClient::new(&host, "root", "pam", &pw) .map_err(ServiceError::PveError)?; Ok(api) } + /** Get a Proxmox Node API client instance for the given node. */ pub fn pve_node(&self, host: &str) -> Result, ServiceError> { let pve = self.pve()?; Ok(proxmox_api::nodes::NodesClient::new(pve).node(host)) } + /** Given a PVE host name, get its network address. */ pub fn pve_addr(&self, host: &str) -> Result { let ifaces = self.pve_node(host)? .network() @@ -96,6 +81,7 @@ impl<'a> Services<'a> { Ok(addr.as_str().unwrap().to_string()) } + /** Open a new SSH session to the given PVE node. */ pub fn pve_ssh(&self, host: &str) -> Result { let addr = self.pve_addr(host)?; let addr = format!("{}:22", addr); @@ -112,8 +98,7 @@ impl<'a> Services<'a> { .map_err(SshError::ClientError) .map_err(ServiceError::SshError)?; - let pw = self.setting_req(|s| &s.pve_root_password)?; - sess.userauth_password("root", &pw) + sess.userauth_password("root", &self.config.pve_root_password) .map_err(SshError::ClientError) .map_err(ServiceError::SshError)?; @@ -125,6 +110,8 @@ impl<'a> Services<'a> { } } + +/** Run an SSH command and return the output as a string, with whitespace trimmed. */ pub fn ssh_run_trimmed(session: &Session, cmd: &str) -> Result { let mut channel = session.channel_session() .map_err(SshError::ClientError) diff --git a/src/api/util.rs b/src/api/util.rs index 2adc399..18e6c6c 100644 --- a/src/api/util.rs +++ b/src/api/util.rs @@ -1,7 +1,48 @@ +use std::{env, fs}; use std::fmt::Debug; use rocket::http; use rocket::response::status; +use log::error; +use rocket::serde::Deserialize; -pub fn raise_500(e: impl Debug) -> status::Custom { - status::Custom(http::Status::InternalServerError, format!("An unexpected error has occurred: {:?}", e)) + +/** Global config for the P5x application. */ +#[derive(Deserialize)] +pub struct P5xConfig { + pub pve_host_name: String, + pub pve_api_host: String, + pub pve_root_password: String, + pub pve_storage_pool: String, + pub pve_storage_driver: String, + pub k8s_root_password: String, + pub ssh_pubkey: String, + pub ssh_privkey: String, } + + +/** Read the P5xConfig instance from the corresponding environment variables. */ +pub fn read_p5x_config() -> P5xConfig { + let pubkey_path = env::var("P5X_SSH_PUBKEY_PATH").expect("Missing env: P5X_SSH_PUBKEY_PATH"); + let privkey_path = env::var("P5X_SSH_PRIVKEY_PATH").expect("Missing env: P5X_SSH_PRIVKEY_PATH"); + + let config = P5xConfig { + pve_host_name: env::var("P5X_NODE_HOSTNAME").expect("Missing env: P5X_NODE_HOSTNAME"), + pve_api_host: env::var("P5X_API_HOST").expect("Missing env: P5X_API_HOST"), + pve_root_password: env::var("P5X_API_ROOT_PASSWORD").expect("Missing env: P5X_API_ROOT_PASSWORD"), + pve_storage_pool: env::var("P5X_STORAGE_POOL").expect("Missing env: P5X_STORAGE_POOL"), + pve_storage_driver: env::var("P5X_STORAGE_DRIVER").expect("Missing env: P5X_STORAGE_DRIVER"), + k8s_root_password: env::var("P5X_K8S_ROOT_PASSWORD").expect("Missing env: P5X_K8S_ROOT_PASSWORD"), + ssh_pubkey: fs::read_to_string(&pubkey_path).expect(&format!("Could not read SSH pubkey from file: {pubkey_path}")), + ssh_privkey: fs::read_to_string(&privkey_path).expect(&format!("Could not read SSH privkey from file: {privkey_path}")), + }; + + config +} + + +/** Catch-all Rocket helper to generate an HTTP 500 response for the given error. */ +pub fn raise_500(e: impl Debug) -> status::Custom { + error!("Unhandled error: {e:?}"); + status::Custom(http::Status::InternalServerError, format!("An unexpected error has occurred: {e:?}")) +} + diff --git a/src/main.rs b/src/main.rs index 70ca95e..3766dfb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,13 @@ pub mod api; #[macro_use] extern crate rocket; +use dotenv::dotenv; use rocket::{Build, Rocket}; use log::{error, info}; use std::{env, process}; +use rocket::figment::Figment; +use rocket::figment::providers::{Env, Format, Toml}; use rocket_dyn_templates::{ Template}; +use serde::Deserialize; fn configure_rocket() -> Rocket { rocket::build() @@ -13,6 +17,7 @@ fn configure_rocket() -> Rocket { #[tokio::main] async fn main() { + dotenv().ok(); env_logger::init(); info!(target: "p5x", "Starting p5x..."); diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..5083f47 --- /dev/null +++ b/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash -xe + +env RUST_LOG=p5x=debug,sqlx=warn,info ./p5x api-server