Port over initial implementation of API server and simplify configuration
This commit is contained in:
parent
cbddc5db18
commit
c89d94dd66
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
.idea
|
.idea
|
||||||
k
|
k
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
.env
|
||||||
|
296
Cargo.lock
generated
296
Cargo.lock
generated
@ -188,12 +188,6 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atomic-waker"
|
|
||||||
version = "1.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -675,6 +669,12 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
@ -1023,25 +1023,6 @@ dependencies = [
|
|||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "handlebars"
|
name = "handlebars"
|
||||||
version = "5.1.2"
|
version = "5.1.2"
|
||||||
@ -1180,29 +1161,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"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]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.9.5"
|
version = "1.9.5"
|
||||||
@ -1231,9 +1189,9 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.3.26",
|
"h2",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http-body 0.4.6",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
@ -1245,78 +1203,6 @@ dependencies = [
|
|||||||
"want",
|
"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]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.61"
|
version = "0.1.61"
|
||||||
@ -1413,12 +1299,6 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ipnet"
|
|
||||||
version = "2.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is-terminal"
|
name = "is-terminal"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@ -1917,11 +1797,11 @@ name = "p5x"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"log",
|
"log",
|
||||||
"proxmox-api",
|
"proxmox-api",
|
||||||
"reqwest",
|
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_dyn_templates",
|
"rocket_dyn_templates",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
@ -1929,7 +1809,6 @@ dependencies = [
|
|||||||
"sea-orm-rocket",
|
"sea-orm-rocket",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
|
||||||
"ssh2",
|
"ssh2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ureq",
|
"ureq",
|
||||||
@ -2392,49 +2271,6 @@ dependencies = [
|
|||||||
"bytecheck",
|
"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]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.8"
|
version = "0.17.8"
|
||||||
@ -2557,7 +2393,7 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
"futures",
|
"futures",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"hyper 0.14.31",
|
"hyper",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -2644,15 +2480,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
@ -3405,36 +3232,6 @@ dependencies = [
|
|||||||
"syn 2.0.86",
|
"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]]
|
[[package]]
|
||||||
name = "tap"
|
name = "tap"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -3558,27 +3355,6 @@ dependencies = [
|
|||||||
"syn 2.0.86",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.16"
|
version = "0.1.16"
|
||||||
@ -3906,18 +3682,6 @@ dependencies = [
|
|||||||
"wasm-bindgen-shared",
|
"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]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.95"
|
version = "0.2.95"
|
||||||
@ -3947,16 +3711,6 @@ version = "0.2.95"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
|
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]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.26.6"
|
version = "0.26.6"
|
||||||
@ -4025,36 +3779,6 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
@ -16,9 +16,8 @@ rocket_dyn_templates = { version = "0.2.0", features = ["handlebars"] }
|
|||||||
sea-orm-migration = "1.1.0"
|
sea-orm-migration = "1.1.0"
|
||||||
uuid = { version = "1.11.0", features = ["v4", "fast-rng"] }
|
uuid = { version = "1.11.0", features = ["v4", "fast-rng"] }
|
||||||
ssh2 = "0.9.4"
|
ssh2 = "0.9.4"
|
||||||
reqwest = "0.12.9"
|
|
||||||
futures = { version = "0.3.31", features = ["executor"] }
|
futures = { version = "0.3.31", features = ["executor"] }
|
||||||
serde_json = "1.0.132"
|
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"] }
|
proxmox-api = { git = "https://github.com/glmdev/p5x-proxmox-api", version = "0.1.2-pre", features = ["ureq-client"] }
|
||||||
ureq = "2.10.1"
|
ureq = "2.10.1"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
[default]
|
[default]
|
||||||
template_dir = "resources/views"
|
template_dir = "resources/views"
|
||||||
|
address = "0.0.0.0"
|
||||||
|
port = 3450
|
||||||
|
|
||||||
|
[default.p5x]
|
||||||
|
pvemaster = ""
|
||||||
|
|
||||||
[default.databases.p5x_api]
|
[default.databases.p5x_api]
|
||||||
url = "sqlite://p5x_api.sqlite?mode=rwc"
|
url = "sqlite://p5x_api.sqlite?mode=rwc"
|
||||||
|
18
package.sh
Executable file
18
package.sh
Executable file
@ -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"
|
||||||
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
|||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Settings | P5x</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Configure P5x</h1>
|
|
||||||
<small>Current config version: {{#if settings.id}}v{{settings.id}}{{^}}v0{{/if}}</small>
|
|
||||||
<form action="/configure" method="post">
|
|
||||||
<br><b>Proxmox VE</b><br>
|
|
||||||
|
|
||||||
<label for="pve_master_node">PVE Master Node Hostname:</label>
|
|
||||||
<input type="text" name="pve_master_node" id="pve_master_node" placeholder="node-name" value="{{{ settings.pve_master_node }}}"><br>
|
|
||||||
|
|
||||||
<label for="pve_api_host">PVE API Host:</label>
|
|
||||||
<input type="text" name="pve_api_host" id="pve_api_host" placeholder="192.168.1.X" value="{{{ settings.pve_api_host }}}"><br>
|
|
||||||
|
|
||||||
<label for="pve_root_password">PVE Root Password:</label>
|
|
||||||
<input type="password" name="pve_root_password" id="pve_root_password" value="{{{ settings.pve_root_password }}}"><br>
|
|
||||||
|
|
||||||
<label for="pve_storage_pool">PVE Storage Pool:</label>
|
|
||||||
<input type="text" name="pve_storage_pool" id="pve_storage_pool" value="{{{ settings.pve_storage_pool }}}"><br>
|
|
||||||
<small>Storage pool must be network-attached (Ceph/iSCSI). NFS is discouraged because it lacks proper support for locking.</small><br>
|
|
||||||
|
|
||||||
<br><button type="submit">Save</button>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -3,14 +3,16 @@ use uuid::Uuid;
|
|||||||
use proxmox_api::nodes::node::lxc;
|
use proxmox_api::nodes::node::lxc;
|
||||||
use proxmox_api::nodes::node::lxc::vmid;
|
use proxmox_api::nodes::node::lxc::vmid;
|
||||||
use proxmox_api::types::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::cluster::node::wait_upid;
|
||||||
use crate::api::entity::locks::lock_vmid;
|
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::entity::nodes::P5xError;
|
||||||
use crate::api::services::{ssh_run_trimmed, Services};
|
use crate::api::services::{ssh_run_trimmed, Services};
|
||||||
use crate::api::services::SshError;
|
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(
|
pub async fn provision_carrier(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
from_node: &nodes::Model,
|
from_node: &nodes::Model,
|
||||||
@ -32,8 +34,7 @@ pub async fn provision_carrier(
|
|||||||
|
|
||||||
// Build the new container params
|
// Build the new container params
|
||||||
let hostname = format!("carrier-{}", Uuid::new_v4().to_string());
|
let hostname = format!("carrier-{}", Uuid::new_v4().to_string());
|
||||||
let storage = svc.setting_req(|s| &s.pve_storage_pool)
|
let storage = &svc.config.pve_storage_pool;
|
||||||
.map_err(P5xError::ServiceError)?;
|
|
||||||
|
|
||||||
let mut params = lxc::PostParams::new("local:vztmpl/p5x-empty.tar.xz".to_string(), vm_id);
|
let mut params = lxc::PostParams::new("local:vztmpl/p5x-empty.tar.xz".to_string(), vm_id);
|
||||||
params.cores = Some(1);
|
params.cores = Some(1);
|
||||||
@ -41,7 +42,7 @@ pub async fn provision_carrier(
|
|||||||
params.hostname = Some(hostname.to_string());
|
params.hostname = Some(hostname.to_string());
|
||||||
params.memory = Some(16); // in MB, min 16
|
params.memory = Some(16); // in MB, min 16
|
||||||
params.start = Some(false);
|
params.start = Some(false);
|
||||||
params.storage = Some(storage);
|
params.storage = Some(storage.to_string());
|
||||||
params.tags = Some("p5x".to_string());
|
params.tags = Some("p5x".to_string());
|
||||||
|
|
||||||
// Ask the PVE API to start creating the carrier node based on our empty template
|
// 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
|
.await
|
||||||
.map_err(P5xError::DbErr)?;
|
.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(
|
pub async fn ensure_carrier_template(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
node: &nodes::Model,
|
node: &nodes::Model,
|
||||||
@ -100,6 +115,8 @@ pub async fn ensure_carrier_template(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Destroy the given carrier LXC container. */
|
||||||
pub async fn terminate_carrier(
|
pub async fn terminate_carrier(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
carrier: nodes::Model,
|
carrier: nodes::Model,
|
||||||
@ -122,5 +139,12 @@ pub async fn terminate_carrier(
|
|||||||
.await
|
.await
|
||||||
.map_err(P5xError::DbErr)?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ use crate::api::entity::nodes;
|
|||||||
use crate::api::entity::nodes::P5xError;
|
use crate::api::entity::nodes::P5xError;
|
||||||
use crate::api::services::Services;
|
use crate::api::services::Services;
|
||||||
|
|
||||||
|
|
||||||
|
/** Migrate an LXC container from its current PVE node to the given PVE node. */
|
||||||
pub async fn migrate_node(
|
pub async fn migrate_node(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
node: nodes::Model,
|
node: nodes::Model,
|
||||||
@ -37,6 +39,8 @@ pub async fn migrate_node(
|
|||||||
.ok_or(P5xError::BadPostcondition("Could not look up node after persisting"))
|
.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> {
|
pub async fn wait_upid(svc: &Services<'_>, node: &str, upid: &str) -> Result<(), P5xError> {
|
||||||
info!("Waiting for UPID {upid} on node {node}");
|
info!("Waiting for UPID {upid} on node {node}");
|
||||||
let pve = svc.pve_node(node)
|
let pve = svc.pve_node(node)
|
||||||
|
@ -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::nodes::node::lxc::vmid::move_volume;
|
||||||
use proxmox_api::types::VmId;
|
use proxmox_api::types::VmId;
|
||||||
use proxmox_api::UreqError;
|
use proxmox_api::UreqError;
|
||||||
|
use proxmox_api::UreqError::Ureq;
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
use ureq::Error::Status;
|
||||||
use crate::api::cluster::carrier::{provision_carrier, terminate_carrier};
|
use crate::api::cluster::carrier::{provision_carrier, terminate_carrier};
|
||||||
use crate::api::cluster::node::migrate_node;
|
use crate::api::cluster::node::migrate_node;
|
||||||
use crate::api::entity::nodes::{lock_first_available, P5xError};
|
use crate::api::entity::nodes::{lock_first_available, P5xError};
|
||||||
use crate::api::entity::{nodes, volumes};
|
use crate::api::entity::{nodes, volumes};
|
||||||
use crate::api::services::{ssh_run_trimmed, Services};
|
use crate::api::services::{ssh_run_trimmed, Services};
|
||||||
|
|
||||||
|
|
||||||
|
/** Parameters required from an API call to manage a volume. */
|
||||||
#[derive(Serialize, Deserialize, FromForm)]
|
#[derive(Serialize, Deserialize, FromForm)]
|
||||||
pub struct VolumeParams {
|
pub struct VolumeParams {
|
||||||
pub id: Option<i32>,
|
pub id: Option<i32>,
|
||||||
@ -25,6 +29,7 @@ pub struct VolumeParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VolumeParams {
|
impl VolumeParams {
|
||||||
|
/** Look up a volume by name and get its params. */
|
||||||
pub async fn resolve(svc: &Services<'_>, name: &str) -> Result<Option<VolumeParams>, DbErr> {
|
pub async fn resolve(svc: &Services<'_>, name: &str) -> Result<Option<VolumeParams>, DbErr> {
|
||||||
volumes::Entity::find()
|
volumes::Entity::find()
|
||||||
.filter(volumes::Column::Name.eq(name))
|
.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(
|
pub async fn create(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
name: &str,
|
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);
|
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
|
// Generate a new mountpoint entry for the node's config
|
||||||
let storage = svc.setting_req(|s| &s.pve_storage_pool)
|
let storage = &svc.config.pve_storage_pool;
|
||||||
.map_err(P5xError::ServiceError)?;
|
|
||||||
let size_in_gib = max((size_in_bytes as u64).div_ceil(1024 * 1024 * 1024) as i64, 1);
|
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");
|
let line = format!("{storage}:{size_in_gib},mp=/mnt/p5x-{name},backup=1");
|
||||||
debug!("Volume {name}: {line}");
|
debug!("Volume {name}: {line}");
|
||||||
@ -100,11 +106,12 @@ pub async fn create(
|
|||||||
debug!("Found mountpoint details for volume {name}: {mount}");
|
debug!("Found mountpoint details for volume {name}: {mount}");
|
||||||
|
|
||||||
// Parse the disk name from the config
|
// 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();
|
let disk_name = mount[name_offset..].split(",").next().unwrap();
|
||||||
|
|
||||||
// Persist the volume
|
// 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 {
|
let res = volumes::ActiveModel {
|
||||||
name: Set(name.to_string()),
|
name: Set(name.to_string()),
|
||||||
size_in_bytes: Set(size_in_bytes),
|
size_in_bytes: Set(size_in_bytes),
|
||||||
@ -126,6 +133,8 @@ pub async fn create(
|
|||||||
Ok(vol)
|
Ok(vol)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Mount a volume to the specified mountpoint on the LXC container it is attached to. */
|
||||||
pub async fn mount(
|
pub async fn mount(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
params: &VolumeParams,
|
params: &VolumeParams,
|
||||||
@ -162,17 +171,21 @@ pub async fn mount(
|
|||||||
let mut put_params = PutParams::default();
|
let mut put_params = PutParams::default();
|
||||||
put_params.mps.insert(mountpoint_identifier, mount_line);
|
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()
|
let res = pve_node.lxc()
|
||||||
.vmid(node.vm_id())
|
.vmid(node.vm_id())
|
||||||
.config()
|
.config()
|
||||||
.put(put_params);
|
.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,
|
// This is necessary because PUT returns {data: null} on success,
|
||||||
// which the UreqClient flags as an unknown error.
|
// 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)));
|
return Err(P5xError::PveError(UreqError::EncounteredErrors(e)));
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Persist the volume
|
// Persist the volume
|
||||||
debug!("Persisting mount changes to volume {} in database", params.name);
|
debug!("Persisting mount changes to volume {} in database", params.name);
|
||||||
@ -187,6 +200,8 @@ pub async fn mount(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Unmount a volume from the LXC container it is attached to. */
|
||||||
pub async fn unmount(
|
pub async fn unmount(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
params: &VolumeParams,
|
params: &VolumeParams,
|
||||||
@ -238,8 +253,8 @@ pub async fn unmount(
|
|||||||
ssh_run_trimmed(&pve_ssh, &cmd)?;
|
ssh_run_trimmed(&pve_ssh, &cmd)?;
|
||||||
|
|
||||||
// For LVM-type storage pools, we also need to deactivate the logical volume
|
// 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_name = &svc.config.pve_storage_pool;
|
||||||
let pool_driver = svc.setting_req(|s| &s.pve_storage_driver).map_err(P5xError::ServiceError)?;
|
let pool_driver = &svc.config.pve_storage_driver;
|
||||||
if pool_driver == "lvm" {
|
if pool_driver == "lvm" {
|
||||||
let cmd = format!("lvchange -aln '/dev/{pool_name}/{}'", vol.disk_name.as_ref().unwrap());
|
let cmd = format!("lvchange -aln '/dev/{pool_name}/{}'", vol.disk_name.as_ref().unwrap());
|
||||||
ssh_run_trimmed(&pve_ssh, &cmd)?;
|
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-
|
// 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.
|
// source that I was able to do it at all.
|
||||||
|
|
||||||
|
|
||||||
|
/** Delete the given volume from the PVE cluster. */
|
||||||
pub async fn delete(
|
pub async fn delete(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
params: &VolumeParams,
|
params: &VolumeParams,
|
||||||
@ -340,11 +357,13 @@ pub async fn delete(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Migrate a volume from its current LXC container to the specified LXC container. */
|
||||||
pub async fn transfer(
|
pub async fn transfer(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
params: &VolumeParams,
|
params: &VolumeParams,
|
||||||
to_node: &nodes::Model,
|
to_node: &nodes::Model,
|
||||||
) -> Result<(), P5xError> {
|
) -> Result<volumes::Model, P5xError> {
|
||||||
|
|
||||||
// Look up the volume from the params
|
// Look up the volume from the params
|
||||||
let vol = volumes::resolve(svc, params).await?;
|
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 the volume already resides on to_node, we're done
|
||||||
if from_node.pve_id == to_node.pve_id {
|
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 and to_node are on the same physical host, transfer directly
|
||||||
if from_node.pve_host == to_node.pve_host {
|
if from_node.pve_host == to_node.pve_host {
|
||||||
transfer_directly(svc, vol, &from_node, to_node).await?;
|
return transfer_directly(svc, vol, &from_node, to_node).await;
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the nodes are on different physical hosts, we need to create a temporary
|
// 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 vol = transfer_directly(svc, vol, &from_node, &carrier).await?;
|
||||||
|
|
||||||
let carrier = migrate_node(svc, carrier, &to_node.pve_host).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?;
|
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(
|
async fn transfer_directly(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
vol: volumes::Model,
|
vol: volumes::Model,
|
||||||
@ -431,11 +451,11 @@ async fn transfer_directly(
|
|||||||
.ok_or(P5xError::BadPostcondition("Could not find mountpoint config after transferring volume"))?;
|
.ok_or(P5xError::BadPostcondition("Could not find mountpoint config after transferring volume"))?;
|
||||||
|
|
||||||
// Parse the disk name from the config
|
// Parse the disk name from the config
|
||||||
let storage = svc.setting_req(|s| &s.pve_storage_pool)
|
// synology-scsi-lun:vm-103-disk-1,mp=/mnt/p5x-test0,backup=1,size=1G
|
||||||
.map_err(P5xError::ServiceError)?;
|
let storage = &svc.config.pve_storage_pool;
|
||||||
let name_offset = format!("{storage}:{}/", to_node.pve_id).len() + 1;
|
let name_offset = storage.len() + 1; // 1 for the colon (:)
|
||||||
let disk_name = mount[name_offset..].split(",").next().unwrap();
|
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
|
// Persist the volume
|
||||||
let mut vol = vol.into_active_model();
|
let mut vol = vol.into_active_model();
|
||||||
vol.pve_node_id = Set(to_node.pve_id);
|
vol.pve_node_id = Set(to_node.pve_id);
|
||||||
|
@ -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,
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ pub use sea_orm_migration::prelude::*;
|
|||||||
use sea_orm_rocket::Database;
|
use sea_orm_rocket::Database;
|
||||||
use crate::api::Db;
|
use crate::api::Db;
|
||||||
|
|
||||||
mod m20241101_000001_create_settings_table;
|
|
||||||
mod m20241102_000001_create_nodes_table;
|
mod m20241102_000001_create_nodes_table;
|
||||||
mod m20241102_000002_create_locks_table;
|
mod m20241102_000002_create_locks_table;
|
||||||
mod m20241103_000001_create_volumes_table;
|
mod m20241103_000001_create_volumes_table;
|
||||||
@ -16,7 +15,6 @@ pub struct Migrator;
|
|||||||
impl MigratorTrait for Migrator {
|
impl MigratorTrait for Migrator {
|
||||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
vec![
|
vec![
|
||||||
Box::new(m20241101_000001_create_settings_table::Migration),
|
|
||||||
Box::new(m20241102_000001_create_nodes_table::Migration),
|
Box::new(m20241102_000001_create_nodes_table::Migration),
|
||||||
Box::new(m20241102_000002_create_locks_table::Migration),
|
Box::new(m20241102_000002_create_locks_table::Migration),
|
||||||
Box::new(m20241103_000001_create_volumes_table::Migration),
|
Box::new(m20241103_000001_create_volumes_table::Migration),
|
||||||
|
@ -26,9 +26,13 @@ impl Pool for DbPool {
|
|||||||
let config = figment.extract::<Config>().unwrap();
|
let config = figment.extract::<Config>().unwrap();
|
||||||
let mut options: ConnectOptions = config.url.into();
|
let mut options: ConnectOptions = config.url.into();
|
||||||
|
|
||||||
|
debug!("max_connections: {} | min: {}", config.max_connections, config.min_connections.unwrap_or_default());
|
||||||
|
|
||||||
options
|
options
|
||||||
.max_connections(config.max_connections as u32)
|
// .max_connections(config.max_connections as u32)
|
||||||
.min_connections(config.min_connections.unwrap_or_default())
|
// .min_connections(config.min_connections.unwrap_or_default())
|
||||||
|
.max_connections(512)
|
||||||
|
.min_connections(128)
|
||||||
.connect_timeout(Duration::from_secs(config.connect_timeout))
|
.connect_timeout(Duration::from_secs(config.connect_timeout))
|
||||||
.sqlx_logging(config.sqlx_logging);
|
.sqlx_logging(config.sqlx_logging);
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/** Try to acquire the given lock, if it is not held. */
|
||||||
pub async fn try_lock<'a>(
|
pub async fn try_lock<'a>(
|
||||||
db: &'a DatabaseConnection,
|
db: &'a DatabaseConnection,
|
||||||
lock_type: &str,
|
lock_type: &str,
|
||||||
@ -36,6 +37,8 @@ pub async fn try_lock<'a>(
|
|||||||
lock.map(|lock | LockHandle::new(lock, db)))
|
lock.map(|lock | LockHandle::new(lock, db)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Acquire the given lock, lazy-waiting until it is available. */
|
||||||
pub async fn lock<'a>(
|
pub async fn lock<'a>(
|
||||||
db: &'a DatabaseConnection,
|
db: &'a DatabaseConnection,
|
||||||
lock_type: &str,
|
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<LockHandle<'a>, DbErr> {
|
pub async fn lock_vmid<'a>(db: &'a DatabaseConnection, lock_reason: Option<&str>) -> Result<LockHandle<'a>, DbErr> {
|
||||||
lock(db, "global_vmid", "0", lock_reason).await
|
lock(db, "global_vmid", "0", lock_reason).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** A held lock. */
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LockHandle<'a> {
|
pub struct LockHandle<'a> {
|
||||||
pub lock: Model,
|
pub lock: Model,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
pub mod settings;
|
|
||||||
pub mod locks;
|
pub mod locks;
|
||||||
pub mod nodes;
|
pub mod nodes;
|
||||||
pub mod volumes;
|
pub mod volumes;
|
||||||
|
@ -3,15 +3,17 @@ use std::net::TcpStream;
|
|||||||
use ssh2::{OpenFlags, OpenType, Session};
|
use ssh2::{OpenFlags, OpenType, Session};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::QueryOrder;
|
use sea_orm::QueryOrder;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use proxmox_api;
|
use proxmox_api;
|
||||||
use log::{warn, debug};
|
use log::{warn, debug};
|
||||||
use proxmox_api::types::VmId;
|
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::entity::locks::{lock, try_lock, LockHandle};
|
||||||
use crate::api::services::{Services, ServiceError, SshError, ssh_run_trimmed};
|
use crate::api::services::{Services, ServiceError, SshError, ssh_run_trimmed};
|
||||||
|
|
||||||
@ -34,6 +36,12 @@ pub enum P5xError {
|
|||||||
UpidFailed(String, String),
|
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>(
|
pub async fn lock_first_available<'a>(
|
||||||
db: &'a DatabaseConnection,
|
db: &'a DatabaseConnection,
|
||||||
lock_reason: Option<&str>,
|
lock_reason: Option<&str>,
|
||||||
@ -54,6 +62,11 @@ pub async fn lock_first_available<'a>(
|
|||||||
Err(NodeLockErr::WaitTimeLimitExceeded)
|
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>(
|
pub async fn try_lock_first_available<'a>(
|
||||||
db: &'a DatabaseConnection,
|
db: &'a DatabaseConnection,
|
||||||
lock_reason: Option<&str>,
|
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 {
|
pub struct PveConfig {
|
||||||
lines: Vec<String>,
|
lines: Vec<String>,
|
||||||
}
|
}
|
||||||
@ -97,6 +111,7 @@ impl PveConfig {
|
|||||||
PveConfig { lines }
|
PveConfig { lines }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Replace a line in the config file. */
|
||||||
pub fn replace<F1, F2>(
|
pub fn replace<F1, F2>(
|
||||||
&mut self,
|
&mut self,
|
||||||
matcher: F1,
|
matcher: F1,
|
||||||
@ -115,6 +130,7 @@ impl PveConfig {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Run a map function on every line in the config file. */
|
||||||
pub fn map<F>(
|
pub fn map<F>(
|
||||||
&mut self,
|
&mut self,
|
||||||
replacer: F
|
replacer: F
|
||||||
@ -128,15 +144,30 @@ impl PveConfig {
|
|||||||
.collect();
|
.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<String> {
|
pub fn get(&self, key: &str) -> Option<String> {
|
||||||
let key = format!("{key}:");
|
let key = format!("{key}:");
|
||||||
self.lines
|
let val = self.lines
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|line| line.starts_with(&key))
|
.filter(|line| line.starts_with(&key))
|
||||||
.next()
|
.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 {
|
pub fn next_nth(&self, prefix: &str) -> u32 {
|
||||||
let res = self.lines
|
let res = self.lines
|
||||||
.iter()
|
.iter()
|
||||||
@ -178,16 +209,12 @@ pub struct Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
fn setting<F>(&self, svc: &Services, f: F) -> Result<String, P5xError>
|
/** Get the VM ID used by the Proxmox API library. */
|
||||||
where
|
|
||||||
F: FnOnce(&settings::Model) -> &Option<String> {
|
|
||||||
svc.setting_req(f).map_err(P5xError::ServiceError)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn vm_id(&self) -> VmId {
|
pub fn vm_id(&self) -> VmId {
|
||||||
VmId::new(i64::from(self.pve_id)).unwrap()
|
VmId::new(i64::from(self.pve_id)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Open an SSH session to this LXC container. */
|
||||||
pub fn ssh(&self, svc: &Services) -> Result<Session, P5xError> {
|
pub fn ssh(&self, svc: &Services) -> Result<Session, P5xError> {
|
||||||
let addr = format!("{}:22", self.assigned_ip);
|
let addr = format!("{}:22", self.assigned_ip);
|
||||||
let tcp = TcpStream::connect(addr)
|
let tcp = TcpStream::connect(addr)
|
||||||
@ -203,11 +230,10 @@ impl Model {
|
|||||||
.map_err(SshError::ClientError)
|
.map_err(SshError::ClientError)
|
||||||
.map_err(P5xError::SshError)?;
|
.map_err(P5xError::SshError)?;
|
||||||
|
|
||||||
let pubkey = self.setting(svc, |s| &s.ssh_public_key)?;
|
let pubkey = &svc.config.ssh_pubkey;
|
||||||
let privkey = self.setting(svc, |s| &s.ssh_private_key)?;
|
let privkey = &svc.config.ssh_privkey;
|
||||||
log::debug!("privkey: {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(SshError::ClientError)
|
||||||
.map_err(P5xError::SshError)?;
|
.map_err(P5xError::SshError)?;
|
||||||
|
|
||||||
@ -218,6 +244,7 @@ impl Model {
|
|||||||
Ok(sess)
|
Ok(sess)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Load the LXC container config for this node. */
|
||||||
pub fn config(&self, svc: &Services) -> Result<PveConfig, P5xError> {
|
pub fn config(&self, svc: &Services) -> Result<PveConfig, P5xError> {
|
||||||
let pve_ssh = svc.pve_ssh(&self.pve_host)
|
let pve_ssh = svc.pve_ssh(&self.pve_host)
|
||||||
.map_err(P5xError::ServiceError)?;
|
.map_err(P5xError::ServiceError)?;
|
||||||
@ -243,6 +270,7 @@ impl Model {
|
|||||||
Ok(PveConfig::new(&contents))
|
Ok(PveConfig::new(&contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Replace the LXC container config for this node. */
|
||||||
pub fn write_config(&self, svc: &Services, conf: &PveConfig) -> Result<(), P5xError> {
|
pub fn write_config(&self, svc: &Services, conf: &PveConfig) -> Result<(), P5xError> {
|
||||||
let conf = conf.to_string();
|
let conf = conf.to_string();
|
||||||
let conf = conf.as_bytes();
|
let conf = conf.as_bytes();
|
||||||
@ -267,33 +295,24 @@ impl Model {
|
|||||||
.map_err(SshError::IoError)
|
.map_err(SshError::IoError)
|
||||||
.map_err(P5xError::SshError)?;
|
.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(())
|
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<Option<LockHandle<'a>>, NodeLockErr> {
|
pub async fn try_lock<'a>(&self, svc: &Services<'a>, lock_reason: Option<&str>) -> Result<Option<LockHandle<'a>>, NodeLockErr> {
|
||||||
try_lock(svc.db, "nodes", &self.id.to_string(), lock_reason)
|
try_lock(svc.db, "nodes", &self.id.to_string(), lock_reason)
|
||||||
.await
|
.await
|
||||||
.map_err(NodeLockErr::DbErr)
|
.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<LockHandle<'a>, NodeLockErr> {
|
pub async fn lock<'a>(&self, svc: &Services<'a>, lock_reason: Option<&str>) -> Result<LockHandle<'a>, NodeLockErr> {
|
||||||
lock(svc.db, "nodes", &self.id.to_string(), lock_reason)
|
lock(svc.db, "nodes", &self.id.to_string(), lock_reason)
|
||||||
.await
|
.await
|
||||||
.map_err(NodeLockErr::DbErr)
|
.map_err(NodeLockErr::DbErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Helper for loading, modifying, then replacing the config, while managing the lock automatically. */
|
||||||
pub async fn mutate_config<F>(
|
pub async fn mutate_config<F>(
|
||||||
&self,
|
&self,
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
@ -314,6 +333,7 @@ impl Model {
|
|||||||
self.write_config(svc, &config)
|
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<String, P5xError> {
|
pub fn ssh_run_trimmed(&self, svc: &Services, cmd: &str) -> Result<String, P5xError> {
|
||||||
let node_ssh = self.ssh(svc)?;
|
let node_ssh = self.ssh(svc)?;
|
||||||
ssh_run_trimmed(&node_ssh, cmd)
|
ssh_run_trimmed(&node_ssh, cmd)
|
||||||
|
@ -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<String>,
|
|
||||||
pub pve_api_host: Option<String>,
|
|
||||||
pub pve_root_password: Option<String>,
|
|
||||||
pub pve_storage_pool: Option<String>,
|
|
||||||
pub pve_storage_driver: Option<String>,
|
|
||||||
// pub dns_domain: Option<String>,
|
|
||||||
// pub node_network_bridge: Option<String>,
|
|
||||||
// pub node_cpus: Option<u64>,
|
|
||||||
// pub node_ram_in_mib: Option<u64>,
|
|
||||||
// pub root_password: Option<String>,
|
|
||||||
#[sea_orm(column_type = "Text")]
|
|
||||||
pub ssh_public_key: Option<String>,
|
|
||||||
#[sea_orm(column_type = "Text")]
|
|
||||||
pub ssh_private_key: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -5,6 +5,8 @@ use crate::api::entity::nodes;
|
|||||||
use crate::api::entity::nodes::P5xError;
|
use crate::api::entity::nodes::P5xError;
|
||||||
use crate::api::services::Services;
|
use crate::api::services::Services;
|
||||||
|
|
||||||
|
|
||||||
|
/** Given the API volume params, look up the Volume model instance. */
|
||||||
pub async fn resolve(
|
pub async fn resolve(
|
||||||
svc: &Services<'_>,
|
svc: &Services<'_>,
|
||||||
params: &VolumeParams,
|
params: &VolumeParams,
|
||||||
@ -37,6 +39,7 @@ pub struct Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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::Model, P5xError> {
|
pub async fn node(&self, svc: &Services<'_>) -> Result<nodes::Model, P5xError> {
|
||||||
nodes::Entity::find()
|
nodes::Entity::find()
|
||||||
.filter(nodes::Column::PveId.eq(self.pve_node_id))
|
.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"))
|
.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<String, P5xError> {
|
pub async fn qualified_name(&self, svc: &Services<'_>) -> Result<String, P5xError> {
|
||||||
let node = self.node(svc).await?;
|
let node = self.node(svc).await?;
|
||||||
let storage = svc.setting_req(|s| &s.pve_storage_pool)
|
let storage = &svc.config.pve_storage_pool;
|
||||||
.map_err(P5xError::ServiceError)?;
|
let storage_type = &svc.config.pve_storage_driver;
|
||||||
|
|
||||||
let storage_type = svc.setting_req(|s| &s.pve_storage_driver)
|
|
||||||
.map_err(P5xError::ServiceError)?;
|
|
||||||
|
|
||||||
let disk_name = format!("pve-{}.raw", self.name);
|
let disk_name = format!("pve-{}.raw", self.name);
|
||||||
let disk_name = self.disk_name.as_ref().unwrap_or(&disk_name);
|
let disk_name = self.disk_name.as_ref().unwrap_or(&disk_name);
|
||||||
|
|
||||||
let qn = match storage_type.as_str() {
|
let qn = match storage_type.as_str() {
|
||||||
"lvm" => format!("{storage}:{disk_name}"),
|
"lvm" => format!("{storage}:{disk_name}"),
|
||||||
_ => format!("{storage}:{}/{disk_name}", node.pve_id),
|
_ => format!("{storage}:{}/{disk_name}", node.pve_id),
|
||||||
|
@ -2,7 +2,7 @@ use rocket::fairing::AdHoc;
|
|||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
mod route;
|
mod route;
|
||||||
mod util;
|
pub mod util;
|
||||||
mod cluster;
|
mod cluster;
|
||||||
pub mod entity;
|
pub mod entity;
|
||||||
pub use db::Db;
|
pub use db::Db;
|
||||||
|
@ -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<Template, status::Custom<String>> {
|
|
||||||
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<Template, status::Custom<String>> {
|
|
||||||
let db = conn.into_inner();
|
|
||||||
render(db).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/", data = "<input>")]
|
|
||||||
async fn save(
|
|
||||||
conn: Connection<'_, api::Db>,
|
|
||||||
input: Form<settings::Model>,
|
|
||||||
) -> Result<Template, status::Custom<String>> {
|
|
||||||
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])
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,11 +1,9 @@
|
|||||||
use rocket::fairing::AdHoc;
|
use rocket::fairing::AdHoc;
|
||||||
|
|
||||||
mod configure;
|
|
||||||
mod volume;
|
mod volume;
|
||||||
|
|
||||||
pub(super) fn init() -> AdHoc {
|
pub(super) fn init() -> AdHoc {
|
||||||
AdHoc::on_ignite("Registering routes", |rocket| async {
|
AdHoc::on_ignite("Registering routes", |rocket| async {
|
||||||
rocket.attach(configure::init())
|
rocket.attach(volume::init())
|
||||||
.attach(volume::init())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ use crate::api::entity::nodes::P5xError;
|
|||||||
use crate::api::services::Services;
|
use crate::api::services::Services;
|
||||||
use crate::api::util::raise_500;
|
use crate::api::util::raise_500;
|
||||||
|
|
||||||
|
|
||||||
#[get("/<name>")]
|
#[get("/<name>")]
|
||||||
async fn get_vol(
|
async fn get_vol(
|
||||||
conn: Connection<'_, api::Db>,
|
conn: Connection<'_, api::Db>,
|
||||||
@ -27,6 +28,7 @@ async fn get_vol(
|
|||||||
Ok(Json(vol))
|
Ok(Json(vol))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[post("/", data = "<input>")]
|
#[post("/", data = "<input>")]
|
||||||
async fn create_vol(
|
async fn create_vol(
|
||||||
conn: Connection<'_, api::Db>,
|
conn: Connection<'_, api::Db>,
|
||||||
@ -46,11 +48,12 @@ async fn create_vol(
|
|||||||
Ok(Json(vol.into()))
|
Ok(Json(vol.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[delete("/<name>")]
|
#[delete("/<name>")]
|
||||||
async fn delete_vol(
|
async fn delete_vol(
|
||||||
conn: Connection<'_, api::Db>,
|
conn: Connection<'_, api::Db>,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<(), status::Custom<String>> {
|
) -> Result<Json<serde_json::Value>, status::Custom<String>> {
|
||||||
let db = conn.into_inner();
|
let db = conn.into_inner();
|
||||||
let svc = Services::build(db).await.map_err(raise_500)?;
|
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::unmount(&svc, &vol).await.map_err(raise_500)?;
|
||||||
cluster::volume::delete(&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 = "<params>")]
|
#[post("/mount", data = "<params>")]
|
||||||
async fn mount_vol(
|
async fn mount_vol(
|
||||||
conn: Connection<'_, api::Db>,
|
conn: Connection<'_, api::Db>,
|
||||||
params: Json<VolumeParams>,
|
params: Json<VolumeParams>,
|
||||||
) -> Result<(), status::Custom<String>> {
|
) -> Result<Json<serde_json::Value>, status::Custom<String>> {
|
||||||
let db = conn.into_inner();
|
let db = conn.into_inner();
|
||||||
let svc = Services::build(db).await.map_err(raise_500)?;
|
let svc = Services::build(db).await.map_err(raise_500)?;
|
||||||
let params = params.into_inner();
|
let params = params.into_inner();
|
||||||
|
|
||||||
cluster::volume::mount(&svc, ¶ms).await.map_err(raise_500)?;
|
cluster::volume::mount(&svc, ¶ms).await.map_err(raise_500)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(Json(serde_json::json!({})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[post("/unmount/<name>")]
|
#[post("/unmount/<name>")]
|
||||||
async fn unmount_vol(
|
async fn unmount_vol(
|
||||||
conn: Connection<'_, api::Db>,
|
conn: Connection<'_, api::Db>,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<(), status::Custom<String>> {
|
) -> Result<Json<serde_json::Value>, status::Custom<String>> {
|
||||||
let db = conn.into_inner();
|
let db = conn.into_inner();
|
||||||
let svc = Services::build(db).await.map_err(raise_500)?;
|
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)?;
|
cluster::volume::unmount(&svc, &vol).await.map_err(raise_500)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(Json(serde_json::json!({})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[post("/transfer/<name>/to/<node>")]
|
#[post("/transfer/<name>/to/<node>")]
|
||||||
async fn transfer_vol(
|
async fn transfer_vol(
|
||||||
conn: Connection<'_, api::Db>,
|
conn: Connection<'_, api::Db>,
|
||||||
name: &str,
|
name: &str,
|
||||||
node: &str,
|
node: &str,
|
||||||
) -> Result<(), status::Custom<String>> {
|
) -> Result<Json<VolumeParams>, status::Custom<String>> {
|
||||||
let db = conn.into_inner();
|
let db = conn.into_inner();
|
||||||
let svc = Services::build(db).await.map_err(raise_500)?;
|
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"))
|
.ok_or(P5xError::BadPrecondition("Could not find a node with that name"))
|
||||||
.map_err(raise_500)?;
|
.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 {
|
pub(super) fn init() -> AdHoc {
|
||||||
AdHoc::on_ignite("Routes: /volume", |rocket| async {
|
AdHoc::on_ignite("Routes: /volume", |rocket| async {
|
||||||
rocket.mount("/volume", routes![create_vol, delete_vol, get_vol, mount_vol, unmount_vol, transfer_vol])
|
rocket.mount("/volume", routes![create_vol, delete_vol, get_vol, mount_vol, unmount_vol, transfer_vol])
|
||||||
|
@ -2,10 +2,10 @@ use std::io::Read;
|
|||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use proxmox_api::nodes::node::NodeClient;
|
use proxmox_api::nodes::node::NodeClient;
|
||||||
use proxmox_api::UreqClient;
|
use proxmox_api::UreqClient;
|
||||||
use sea_orm::{DatabaseConnection, DbErr, EntityTrait, QueryOrder};
|
use sea_orm::{DatabaseConnection, DbErr};
|
||||||
use ssh2::Session;
|
use ssh2::Session;
|
||||||
use crate::api::entity::nodes::P5xError;
|
use crate::api::entity::nodes::P5xError;
|
||||||
use crate::api::entity::settings;
|
use crate::api::util::{read_p5x_config, P5xConfig};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SshError {
|
pub enum SshError {
|
||||||
@ -23,61 +23,46 @@ impl SshError {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ServiceError {
|
pub enum ServiceError {
|
||||||
MissingSetting,
|
|
||||||
PveError(proxmox_api::UreqError),
|
PveError(proxmox_api::UreqError),
|
||||||
InvalidNetworkInterface,
|
InvalidNetworkInterface,
|
||||||
SshError(SshError),
|
SshError(SshError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Helper for managing singleton services. */
|
||||||
pub struct Services<'a> {
|
pub struct Services<'a> {
|
||||||
pub settings: Option<settings::Model>,
|
|
||||||
pub db: &'a DatabaseConnection,
|
pub db: &'a DatabaseConnection,
|
||||||
|
pub config: P5xConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Services<'a> {
|
impl<'a> Services<'a> {
|
||||||
|
/** Create a new services instance. */
|
||||||
pub async fn build(db: &'a DatabaseConnection) -> Result<Services<'a>, DbErr> {
|
pub async fn build(db: &'a DatabaseConnection) -> Result<Services<'a>, DbErr> {
|
||||||
let settings = settings::Entity::find().order_by_desc(settings::Column::Id).one(db).await?;
|
|
||||||
Ok(Services {
|
Ok(Services {
|
||||||
db,
|
db,
|
||||||
settings,
|
config: read_p5x_config(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setting<F>(&self, f: F) -> Option<String>
|
/** Get a new Proxmox API client instance. */
|
||||||
where
|
pub fn pve(&self) -> Result<UreqClient, ServiceError> {
|
||||||
F: FnOnce(&settings::Model) -> &Option<String>,
|
let host = &self.config.pve_api_host;
|
||||||
{
|
let pw = &self.config.pve_root_password;
|
||||||
let model = self.settings.as_ref()?;
|
|
||||||
let res = f(model);
|
|
||||||
if let Some(res) = res {
|
|
||||||
return Some(res.clone());
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setting_req<F>(&self, f: F) -> Result<String, ServiceError>
|
|
||||||
where
|
|
||||||
F: FnOnce(&settings::Model) -> &Option<String>
|
|
||||||
{
|
|
||||||
self.setting(f).ok_or(ServiceError::MissingSetting)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pve(&self) -> Result<proxmox_api::UreqClient, ServiceError> {
|
|
||||||
let host = self.setting_req(|s| &s.pve_api_host)?;
|
|
||||||
let pw = self.setting_req(|s| &s.pve_root_password)?;
|
|
||||||
|
|
||||||
let host = format!("https://{host}:8006");
|
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)?;
|
.map_err(ServiceError::PveError)?;
|
||||||
|
|
||||||
Ok(api)
|
Ok(api)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get a Proxmox Node API client instance for the given node. */
|
||||||
pub fn pve_node(&self, host: &str) -> Result<NodeClient<UreqClient>, ServiceError> {
|
pub fn pve_node(&self, host: &str) -> Result<NodeClient<UreqClient>, ServiceError> {
|
||||||
let pve = self.pve()?;
|
let pve = self.pve()?;
|
||||||
Ok(proxmox_api::nodes::NodesClient::new(pve).node(host))
|
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<String, ServiceError> {
|
pub fn pve_addr(&self, host: &str) -> Result<String, ServiceError> {
|
||||||
let ifaces = self.pve_node(host)?
|
let ifaces = self.pve_node(host)?
|
||||||
.network()
|
.network()
|
||||||
@ -96,6 +81,7 @@ impl<'a> Services<'a> {
|
|||||||
Ok(addr.as_str().unwrap().to_string())
|
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<Session, ServiceError> {
|
pub fn pve_ssh(&self, host: &str) -> Result<Session, ServiceError> {
|
||||||
let addr = self.pve_addr(host)?;
|
let addr = self.pve_addr(host)?;
|
||||||
let addr = format!("{}:22", addr);
|
let addr = format!("{}:22", addr);
|
||||||
@ -112,8 +98,7 @@ impl<'a> Services<'a> {
|
|||||||
.map_err(SshError::ClientError)
|
.map_err(SshError::ClientError)
|
||||||
.map_err(ServiceError::SshError)?;
|
.map_err(ServiceError::SshError)?;
|
||||||
|
|
||||||
let pw = self.setting_req(|s| &s.pve_root_password)?;
|
sess.userauth_password("root", &self.config.pve_root_password)
|
||||||
sess.userauth_password("root", &pw)
|
|
||||||
.map_err(SshError::ClientError)
|
.map_err(SshError::ClientError)
|
||||||
.map_err(ServiceError::SshError)?;
|
.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<String, P5xError> {
|
pub fn ssh_run_trimmed(session: &Session, cmd: &str) -> Result<String, P5xError> {
|
||||||
let mut channel = session.channel_session()
|
let mut channel = session.channel_session()
|
||||||
.map_err(SshError::ClientError)
|
.map_err(SshError::ClientError)
|
||||||
|
@ -1,7 +1,48 @@
|
|||||||
|
use std::{env, fs};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use rocket::http;
|
use rocket::http;
|
||||||
use rocket::response::status;
|
use rocket::response::status;
|
||||||
|
use log::error;
|
||||||
|
use rocket::serde::Deserialize;
|
||||||
|
|
||||||
pub fn raise_500(e: impl Debug) -> status::Custom<String> {
|
|
||||||
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<String> {
|
||||||
|
error!("Unhandled error: {e:?}");
|
||||||
|
status::Custom(http::Status::InternalServerError, format!("An unexpected error has occurred: {e:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
#[macro_use] extern crate rocket;
|
#[macro_use] extern crate rocket;
|
||||||
|
use dotenv::dotenv;
|
||||||
use rocket::{Build, Rocket};
|
use rocket::{Build, Rocket};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use std::{env, process};
|
use std::{env, process};
|
||||||
|
use rocket::figment::Figment;
|
||||||
|
use rocket::figment::providers::{Env, Format, Toml};
|
||||||
use rocket_dyn_templates::{ Template};
|
use rocket_dyn_templates::{ Template};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
fn configure_rocket() -> Rocket<Build> {
|
fn configure_rocket() -> Rocket<Build> {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
@ -13,6 +17,7 @@ fn configure_rocket() -> Rocket<Build> {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
dotenv().ok();
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
info!(target: "p5x", "Starting p5x...");
|
info!(target: "p5x", "Starting p5x...");
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user