Compare commits
10 Commits
7ffff1e698
...
06f7beb26f
Author | SHA1 | Date | |
---|---|---|---|
06f7beb26f | |||
de55d34086 | |||
265d91dd83 | |||
e51bd2722e | |||
7bfeba0ad1 | |||
3e7e0a789b | |||
8641eeeca8 | |||
7270ecf6ba | |||
0f9b439b78 | |||
81b233a047 |
243
Cargo.lock
generated
243
Cargo.lock
generated
@ -169,6 +169,37 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-utility"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a349201d80b4aa18d17a34a182bdd7f8ddf845e9e57d2ea130a12e10ef1e3a47"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"gloo-timers",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-wsocket"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d50cb541e6d09e119e717c64c46ed33f49be7fa592fa805d56c11d6a7ff093c"
|
||||||
|
dependencies = [
|
||||||
|
"async-utility",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.26.0",
|
||||||
|
"tokio-socks",
|
||||||
|
"tokio-tungstenite 0.24.0",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atoi"
|
name = "atoi"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@ -193,6 +224,15 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-destructor"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d919cb60ba95c87ba42777e9e246c4e8d658057299b437b7512531ce0a09a23"
|
||||||
|
dependencies = [
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@ -324,9 +364,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin"
|
name = "bitcoin"
|
||||||
version = "0.32.4"
|
version = "0.32.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "788902099d47c8682efe6a7afb01c8d58b9794ba66c06affd81c3d6b560743eb"
|
checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base58ck",
|
"base58ck",
|
||||||
"bech32",
|
"bech32",
|
||||||
@ -772,6 +812,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.9"
|
version = "0.7.9"
|
||||||
@ -1055,6 +1101,12 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -1215,6 +1267,18 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-timers"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "group"
|
name = "group"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@ -1285,6 +1349,11 @@ name = "hashbrown"
|
|||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
@ -1980,16 +2049,20 @@ dependencies = [
|
|||||||
"lettre",
|
"lettre",
|
||||||
"lnvps_db",
|
"lnvps_db",
|
||||||
"log",
|
"log",
|
||||||
|
"native-tls",
|
||||||
"nostr",
|
"nostr",
|
||||||
|
"nostr-sdk",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"rand",
|
"rand",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
|
"rocket_ws",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ssh-key",
|
"ssh-key",
|
||||||
"ssh2",
|
"ssh2",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite 0.21.0",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2037,6 +2110,15 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.15.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -2191,6 +2273,55 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nostr-database"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23696338d51e45cd44e061823847f4b0d1d362eca80d5033facf9c184149f72f"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"lru",
|
||||||
|
"nostr",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nostr-relay-pool"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15fcc6e3f0ca54d0fc779009bc5f2684cea9147be3b6aa68a7d301ea590f95f5"
|
||||||
|
dependencies = [
|
||||||
|
"async-utility",
|
||||||
|
"async-wsocket",
|
||||||
|
"atomic-destructor",
|
||||||
|
"negentropy 0.3.1",
|
||||||
|
"negentropy 0.4.3",
|
||||||
|
"nostr",
|
||||||
|
"nostr-database",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nostr-sdk"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "491221fc89b1aa189a0de640127127d68b4e7c5c1d44371b04d9a6d10694b5af"
|
||||||
|
dependencies = [
|
||||||
|
"async-utility",
|
||||||
|
"atomic-destructor",
|
||||||
|
"nostr",
|
||||||
|
"nostr-database",
|
||||||
|
"nostr-relay-pool",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
@ -3038,6 +3169,16 @@ dependencies = [
|
|||||||
"uncased",
|
"uncased",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rocket_ws"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25f1877668c937b701177c349f21383c556cd3bb4ba8fa1d07fa96ccb3a8782e"
|
||||||
|
dependencies = [
|
||||||
|
"rocket",
|
||||||
|
"tokio-tungstenite 0.21.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ron"
|
name = "ron"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@ -3119,6 +3260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1"
|
checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki 0.102.8",
|
"rustls-webpki 0.102.8",
|
||||||
"subtle",
|
"subtle",
|
||||||
@ -4044,6 +4186,18 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-socks"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"futures-util",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.16"
|
version = "0.1.16"
|
||||||
@ -4055,6 +4209,36 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tungstenite 0.21.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"rustls 0.23.19",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.26.0",
|
||||||
|
"tungstenite 0.24.0",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.12"
|
version = "0.7.12"
|
||||||
@ -4245,6 +4429,46 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http 1.1.0",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"native-tls",
|
||||||
|
"rand",
|
||||||
|
"sha1",
|
||||||
|
"thiserror",
|
||||||
|
"url",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http 1.1.0",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand",
|
||||||
|
"rustls 0.23.19",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"sha1",
|
||||||
|
"thiserror",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@ -4355,6 +4579,12 @@ version = "2.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf-8"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf16_iter"
|
name = "utf16_iter"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@ -4489,6 +4719,15 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
|
13
Cargo.toml
13
Cargo.toml
@ -6,6 +6,10 @@ edition = "2021"
|
|||||||
[[bin]]
|
[[bin]]
|
||||||
name = "api"
|
name = "api"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["mikrotik", "nostr-dm"]
|
||||||
|
mikrotik = []
|
||||||
|
nostr-dm = ["dep:nostr-sdk"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lnvps_db = { path = "lnvps_db" }
|
lnvps_db = { path = "lnvps_db" }
|
||||||
@ -20,7 +24,7 @@ serde_json = "1.0.132"
|
|||||||
rocket = { version = "0.5.1", features = ["json"] }
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
nostr = { version = "0.37.0", default-features = false, features = ["std"] }
|
nostr = { version = "0.37.0", default-features = false, features = ["std"] }
|
||||||
base64 = "0.22.1"
|
base64 = { version = "0.22.1", features = ["alloc"] }
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] }
|
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] }
|
||||||
ipnetwork = "0.20.0"
|
ipnetwork = "0.20.0"
|
||||||
@ -29,3 +33,10 @@ clap = { version = "4.5.21", features = ["derive"] }
|
|||||||
ssh2 = "0.9.4"
|
ssh2 = "0.9.4"
|
||||||
ssh-key = "0.6.7"
|
ssh-key = "0.6.7"
|
||||||
lettre = { version = "0.11.10", features = ["tokio1-native-tls"] }
|
lettre = { version = "0.11.10", features = ["tokio1-native-tls"] }
|
||||||
|
ws = { package = "rocket_ws", version = "0.1.0" }
|
||||||
|
tokio-tungstenite = { version = "^0.21", features = ["native-tls"] }
|
||||||
|
native-tls = "0.2.12"
|
||||||
|
|
||||||
|
#nostr-dm
|
||||||
|
nostr-sdk = { version = "0.37.0", optional = true, default-features = false, features = ["nip44", "nip59"] }
|
||||||
|
|
||||||
|
94
README.md
Normal file
94
README.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
## LNVPS
|
||||||
|
|
||||||
|
A bitcoin powered VPS system.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- MySql database
|
||||||
|
- LND node
|
||||||
|
- Proxmox server
|
||||||
|
|
||||||
|
## Required Config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# MySql database connection string
|
||||||
|
db: "mysql://root:root@localhost:3376/lnvps"
|
||||||
|
|
||||||
|
# LND node connection details
|
||||||
|
lnd:
|
||||||
|
url: "https://127.0.0.1:10003"
|
||||||
|
cert: "$HOME/.lnd/tls.cert"
|
||||||
|
macaroon: "$HOME/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"
|
||||||
|
|
||||||
|
# Number of days after a VM expires to delete
|
||||||
|
delete_after: 3
|
||||||
|
|
||||||
|
# Provisioner is the main process which handles creating/deleting VM's
|
||||||
|
# Currently supports: Proxmox
|
||||||
|
provisioner:
|
||||||
|
proxmox:
|
||||||
|
# Read-only mode prevents spawning VM's
|
||||||
|
read_only: false
|
||||||
|
# Proxmox (QEMU) settings used for spawning VM's
|
||||||
|
qemu:
|
||||||
|
bios: "ovmf"
|
||||||
|
machine: "q35"
|
||||||
|
os_type: "l26"
|
||||||
|
bridge: "vmbr0"
|
||||||
|
cpu: "kvm64"
|
||||||
|
vlan: 100
|
||||||
|
kvm: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email notifications
|
||||||
|
Email notifications can be enabled, this is primarily intended for admin notifications.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# (Optional)
|
||||||
|
# Email notifications settings
|
||||||
|
smtp:
|
||||||
|
# Admin user id, used to send notifications of failed jobs etc. (optional)
|
||||||
|
admin: 1
|
||||||
|
# SMTP server url
|
||||||
|
server: "smtp.gmail.com"
|
||||||
|
# From header used in the email (optional)
|
||||||
|
from: "LNVPS <no-reply@example.com>"
|
||||||
|
username: "no-reply@example.com"
|
||||||
|
password: "mypassword123"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nostr notifications (NIP-17)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# (Optional)
|
||||||
|
# Nostr connection settings for notifications
|
||||||
|
nostr:
|
||||||
|
# Nostr relays to publish notifications to
|
||||||
|
relays:
|
||||||
|
- "wss://relay.snort.social"
|
||||||
|
- "wss://relay.damus.io"
|
||||||
|
- "wss://nos.lol"
|
||||||
|
# Private key used to sign notifications
|
||||||
|
nsec: "nsec1234xxx"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Setup (Advanced)
|
||||||
|
|
||||||
|
When ARP is disabled (reply-only) on your router you may need to create static ARP entries when allocating
|
||||||
|
IPs, we support managing ARP entries on routers directly as part of the provisioning process.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# (Optional)
|
||||||
|
# When allocating IPs for VM's it may be necessary to create static ARP entries on
|
||||||
|
# your router, at least one router can be configured
|
||||||
|
#
|
||||||
|
# Currently supports: Mikrotik
|
||||||
|
router:
|
||||||
|
mikrotik:
|
||||||
|
# !! MAKE SURE TO USE HTTPS !!
|
||||||
|
url: "https://my-router.net"
|
||||||
|
username: "admin"
|
||||||
|
password: "admin"
|
||||||
|
# Interface where the static ARP entry is added
|
||||||
|
arp_interface: "bridge1"
|
||||||
|
```
|
54
email.html
Normal file
54
email.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>LNVPS</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: "Source Code Pro", monospace;
|
||||||
|
color: white;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
margin-left: 4rem;
|
||||||
|
margin-right: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="header">
|
||||||
|
LNVPS
|
||||||
|
<img height="48" src="https://lnvps.net/logo.jpg" alt="logo"/>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<p>%%_MESSAGE_%%</p>
|
||||||
|
<hr/>
|
||||||
|
<small>
|
||||||
|
(c) 2024 LNVPS.net
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -93,6 +93,9 @@ pub trait LNVpsDb: Sync + Send {
|
|||||||
/// Delete a VM by id
|
/// Delete a VM by id
|
||||||
async fn delete_vm(&self, vm_id: u64) -> Result<()>;
|
async fn delete_vm(&self, vm_id: u64) -> Result<()>;
|
||||||
|
|
||||||
|
/// Update a VM
|
||||||
|
async fn update_vm(&self, vm: &Vm) -> Result<()>;
|
||||||
|
|
||||||
/// List VM ip assignments
|
/// List VM ip assignments
|
||||||
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64>;
|
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64>;
|
||||||
|
|
||||||
|
@ -31,10 +31,11 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn upsert_user(&self, pubkey: &[u8; 32]) -> Result<u64> {
|
async fn upsert_user(&self, pubkey: &[u8; 32]) -> Result<u64> {
|
||||||
let res = sqlx::query("insert ignore into users(pubkey) values(?) returning id")
|
let res =
|
||||||
.bind(pubkey.as_slice())
|
sqlx::query("insert ignore into users(pubkey,contact_nip17) values(?,1) returning id")
|
||||||
.fetch_optional(&self.db)
|
.bind(pubkey.as_slice())
|
||||||
.await?;
|
.fetch_optional(&self.db)
|
||||||
|
.await?;
|
||||||
match res {
|
match res {
|
||||||
None => sqlx::query("select id from users where pubkey = ?")
|
None => sqlx::query("select id from users where pubkey = ?")
|
||||||
.bind(pubkey.as_slice())
|
.bind(pubkey.as_slice())
|
||||||
@ -249,6 +250,23 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_vm(&self, vm: &Vm) -> Result<()> {
|
||||||
|
sqlx::query("update vm set image_id=?,template_id=?,ssh_key_id=?,expires=?,cpu=?,memory=?,disk_size=?,disk_id=? where id=?")
|
||||||
|
.bind(vm.image_id)
|
||||||
|
.bind(vm.template_id)
|
||||||
|
.bind(vm.ssh_key_id)
|
||||||
|
.bind(vm.expires)
|
||||||
|
.bind(vm.cpu)
|
||||||
|
.bind(vm.memory)
|
||||||
|
.bind(vm.disk_size)
|
||||||
|
.bind(vm.disk_id)
|
||||||
|
.bind(vm.id)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64> {
|
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64> {
|
||||||
Ok(sqlx::query(
|
Ok(sqlx::query(
|
||||||
"insert into vm_ip_assignment(vm_id,ip_range_id,ip) values(?, ?, ?) returning id",
|
"insert into vm_ip_assignment(vm_id,ip_range_id,ip) values(?, ?, ?) returning id",
|
||||||
|
149
src/api.rs
149
src/api.rs
@ -2,14 +2,19 @@ use crate::nip98::Nip98Auth;
|
|||||||
use crate::provisioner::Provisioner;
|
use crate::provisioner::Provisioner;
|
||||||
use crate::status::{VmState, VmStateCache};
|
use crate::status::{VmState, VmStateCache};
|
||||||
use crate::worker::WorkJob;
|
use crate::worker::WorkJob;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
use lnvps_db::hydrate::Hydrate;
|
use lnvps_db::hydrate::Hydrate;
|
||||||
use lnvps_db::{LNVpsDb, UserSshKey, Vm, VmOsImage, VmPayment, VmTemplate};
|
use lnvps_db::{LNVpsDb, UserSshKey, Vm, VmOsImage, VmPayment, VmTemplate};
|
||||||
|
use log::{debug, error};
|
||||||
use nostr::util::hex;
|
use nostr::util::hex;
|
||||||
|
use rocket::futures::{Sink, SinkExt, StreamExt};
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::{get, patch, post, routes, Responder, Route, State};
|
use rocket::{get, patch, post, routes, Responder, Route, State};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ssh_key::PublicKey;
|
use ssh_key::PublicKey;
|
||||||
|
use std::fmt::Display;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
use ws::Message;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![
|
||||||
@ -24,7 +29,9 @@ pub fn routes() -> Vec<Route> {
|
|||||||
v1_get_payment,
|
v1_get_payment,
|
||||||
v1_start_vm,
|
v1_start_vm,
|
||||||
v1_stop_vm,
|
v1_stop_vm,
|
||||||
v1_restart_vm
|
v1_restart_vm,
|
||||||
|
v1_terminal_proxy,
|
||||||
|
v1_patch_vm
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +72,11 @@ struct ApiVmStatus {
|
|||||||
pub status: VmState,
|
pub status: VmState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct VMPatchRequest {
|
||||||
|
pub ssh_key_id: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/api/v1/vm")]
|
#[get("/api/v1/vm")]
|
||||||
async fn v1_list_vms(
|
async fn v1_list_vms(
|
||||||
auth: Nip98Auth,
|
auth: Nip98Auth,
|
||||||
@ -117,6 +129,35 @@ async fn v1_get_vm(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[patch("/api/v1/vm/<id>", data = "<data>", format = "json")]
|
||||||
|
async fn v1_patch_vm(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Box<dyn LNVpsDb>>,
|
||||||
|
provisioner: &State<Box<dyn Provisioner>>,
|
||||||
|
id: u64,
|
||||||
|
data: Json<VMPatchRequest>,
|
||||||
|
) -> ApiResult<()> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
let mut vm = db.get_vm(id).await?;
|
||||||
|
if vm.user_id != uid {
|
||||||
|
return ApiData::err("VM doesnt belong to you");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(k) = data.ssh_key_id {
|
||||||
|
let ssh_key = db.get_user_ssh_key(k).await?;
|
||||||
|
if ssh_key.user_id != uid {
|
||||||
|
return ApiData::err("SSH key doesnt belong to you");
|
||||||
|
}
|
||||||
|
vm.ssh_key_id = ssh_key.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update_vm(&vm).await?;
|
||||||
|
provisioner.patch_vm(vm.id).await?;
|
||||||
|
|
||||||
|
ApiData::ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/api/v1/image")]
|
#[get("/api/v1/image")]
|
||||||
async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmOsImage>> {
|
async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmOsImage>> {
|
||||||
let vms = db.list_os_image().await?;
|
let vms = db.list_os_image().await?;
|
||||||
@ -289,6 +330,112 @@ async fn v1_get_payment(
|
|||||||
ApiData::ok(payment)
|
ApiData::ok(payment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/api/v1/console/<id>?<auth>")]
|
||||||
|
async fn v1_terminal_proxy(
|
||||||
|
auth: &str,
|
||||||
|
db: &State<Box<dyn LNVpsDb>>,
|
||||||
|
provisioner: &State<Box<dyn Provisioner>>,
|
||||||
|
id: u64,
|
||||||
|
ws: ws::WebSocket,
|
||||||
|
) -> Result<ws::Channel<'static>, &'static str> {
|
||||||
|
let auth = Nip98Auth::from_base64(auth).map_err(|_| "Missing or invalid auth param")?;
|
||||||
|
if auth.check(&format!("/api/v1/console/{id}"), "GET").is_err() {
|
||||||
|
return Err("Invalid auth event");
|
||||||
|
}
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await.map_err(|_| "Insert failed")?;
|
||||||
|
let vm = db.get_vm(id).await.map_err(|_| "VM not found")?;
|
||||||
|
if uid != vm.user_id {
|
||||||
|
return Err("VM does not belong to you");
|
||||||
|
}
|
||||||
|
|
||||||
|
let ws_upstream = provisioner.terminal_proxy(vm.id).await.map_err(|e| {
|
||||||
|
error!("Failed to start terminal proxy: {}", e);
|
||||||
|
"Failed to open terminal proxy"
|
||||||
|
})?;
|
||||||
|
let ws = ws.config(Default::default());
|
||||||
|
Ok(ws.channel(move |stream| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let (mut tx_upstream, mut rx_upstream) = ws_upstream.split();
|
||||||
|
let (mut tx_client, mut rx_client) = stream.split();
|
||||||
|
|
||||||
|
async fn process_client<S, E>(
|
||||||
|
msg: Result<Message, E>,
|
||||||
|
tx_upstream: &mut S,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
S: SinkExt<Message> + Unpin,
|
||||||
|
<S as Sink<Message>>::Error: Display,
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
match msg {
|
||||||
|
Ok(m) => {
|
||||||
|
let m_up = match m {
|
||||||
|
Message::Text(t) => Message::Text(format!("0:{}:{}", t.len(), t)),
|
||||||
|
_ => panic!("todo"),
|
||||||
|
};
|
||||||
|
debug!("Sending data to upstream: {:?}", m_up);
|
||||||
|
if let Err(e) = tx_upstream.send(m_up).await {
|
||||||
|
bail!("Failed to send msg to upstream: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
bail!("Failed to read from client: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_upstream<S, E>(
|
||||||
|
msg: Result<Message, E>,
|
||||||
|
tx_client: &mut S,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
S: SinkExt<Message> + Unpin,
|
||||||
|
<S as Sink<Message>>::Error: Display,
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
match msg {
|
||||||
|
Ok(m) => {
|
||||||
|
let m_down = match m {
|
||||||
|
Message::Binary(data) => {
|
||||||
|
Message::Text(String::from_utf8_lossy(&data).to_string())
|
||||||
|
}
|
||||||
|
_ => panic!("todo"),
|
||||||
|
};
|
||||||
|
debug!("Sending data to downstream: {:?}", m_down);
|
||||||
|
if let Err(e) = tx_client.send(m_down).await {
|
||||||
|
bail!("Failed to msg to client: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
bail!("Failed to read from upstream: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Some(msg) = rx_client.next() => {
|
||||||
|
if let Err(e) = process_client(msg, &mut tx_upstream).await {
|
||||||
|
error!("{}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(msg) = rx_upstream.next() => {
|
||||||
|
if let Err(e) = process_upstream(msg, &mut tx_client).await {
|
||||||
|
error!("{}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct CreateVmRequest {
|
struct CreateVmRequest {
|
||||||
template_id: u64,
|
template_id: u64,
|
||||||
|
@ -12,8 +12,11 @@ use lnvps::status::VmStateCache;
|
|||||||
use lnvps::worker::{WorkJob, Worker};
|
use lnvps::worker::{WorkJob, Worker};
|
||||||
use lnvps_db::{LNVpsDb, LNVpsDbMysql};
|
use lnvps_db::{LNVpsDb, LNVpsDbMysql};
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use nostr::Keys;
|
||||||
|
use nostr_sdk::Client;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(about, version, author)]
|
#[clap(about, version, author)]
|
||||||
@ -50,14 +53,31 @@ async fn main() -> Result<(), Error> {
|
|||||||
db.execute(setup_script).await?;
|
db.execute(setup_script).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nostr_client = if let Some(ref c) = settings.nostr {
|
||||||
|
let cx = Client::builder().signer(Keys::parse(&c.nsec)?).build();
|
||||||
|
for r in &c.relays {
|
||||||
|
cx.add_relay(r.clone()).await?;
|
||||||
|
}
|
||||||
|
cx.connect().await;
|
||||||
|
Some(cx)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let router = settings.router.as_ref().map(|r| r.get_router());
|
||||||
let status = VmStateCache::new();
|
let status = VmStateCache::new();
|
||||||
let worker_provisioner =
|
let worker_provisioner =
|
||||||
settings
|
settings
|
||||||
.provisioner
|
.provisioner
|
||||||
.get_provisioner(db.clone(), lnd.clone(), exchange.clone());
|
.get_provisioner(db.clone(), router, lnd.clone(), exchange.clone());
|
||||||
worker_provisioner.init().await?;
|
worker_provisioner.init().await?;
|
||||||
|
|
||||||
let mut worker = Worker::new(db.clone(), worker_provisioner, &settings, status.clone());
|
let mut worker = Worker::new(
|
||||||
|
db.clone(),
|
||||||
|
worker_provisioner,
|
||||||
|
&settings,
|
||||||
|
status.clone(),
|
||||||
|
nostr_client.clone(),
|
||||||
|
);
|
||||||
let sender = worker.sender();
|
let sender = worker.sender();
|
||||||
|
|
||||||
// send a startup notification
|
// send a startup notification
|
||||||
@ -82,6 +102,7 @@ async fn main() -> Result<(), Error> {
|
|||||||
if let Err(e) = handler.listen().await {
|
if let Err(e) = handler.listen().await {
|
||||||
error!("invoice-error: {}", e);
|
error!("invoice-error: {}", e);
|
||||||
}
|
}
|
||||||
|
sleep(Duration::from_secs(5)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// request work every 30s to check vm status
|
// request work every 30s to check vm status
|
||||||
@ -110,10 +131,11 @@ async fn main() -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let router = settings.router.as_ref().map(|r| r.get_router());
|
||||||
let provisioner =
|
let provisioner =
|
||||||
settings
|
settings
|
||||||
.provisioner
|
.provisioner
|
||||||
.get_provisioner(db.clone(), lnd.clone(), exchange.clone());
|
.get_provisioner(db.clone(), router, lnd.clone(), exchange.clone());
|
||||||
|
|
||||||
let db: Box<dyn LNVpsDb> = Box::new(db.clone());
|
let db: Box<dyn LNVpsDb> = Box::new(db.clone());
|
||||||
let pv: Box<dyn Provisioner> = Box::new(provisioner);
|
let pv: Box<dyn Provisioner> = Box::new(provisioner);
|
||||||
|
@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
use tokio_tungstenite::tungstenite::handshake::client::{generate_key, Request};
|
||||||
|
use tokio_tungstenite::{Connector, MaybeTlsStream, WebSocketStream};
|
||||||
|
|
||||||
pub struct ProxmoxClient {
|
pub struct ProxmoxClient {
|
||||||
base: Url,
|
base: Url,
|
||||||
@ -253,7 +256,68 @@ impl ProxmoxClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create terminal proxy session
|
||||||
|
pub async fn terminal_proxy(&self, node: &str, vm: u64) -> Result<TerminalProxyTicket> {
|
||||||
|
let rsp: ResponseBase<TerminalProxyTicket> = self
|
||||||
|
.post(
|
||||||
|
&format!("/api2/json/nodes/{}/qemu/{}/termproxy", node, vm),
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(rsp.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open websocket connection to terminal proxy
|
||||||
|
pub async fn open_terminal_proxy(
|
||||||
|
&self,
|
||||||
|
node: &str,
|
||||||
|
vm: u64,
|
||||||
|
req: TerminalProxyTicket,
|
||||||
|
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
||||||
|
self.get_task_status(&TaskId {
|
||||||
|
id: req.upid,
|
||||||
|
node: node.to_string(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut url: Url = self.base.join(&format!(
|
||||||
|
"/api2/json/nodes/{}/qemu/{}/vncwebsocket",
|
||||||
|
node, vm
|
||||||
|
))?;
|
||||||
|
url.set_scheme("wss").unwrap();
|
||||||
|
url.query_pairs_mut().append_pair("port", &req.port);
|
||||||
|
url.query_pairs_mut().append_pair("vncticket", &req.ticket);
|
||||||
|
|
||||||
|
let r = Request::builder()
|
||||||
|
.method("GET")
|
||||||
|
.header("Host", url.host().unwrap().to_string())
|
||||||
|
.header("Connection", "Upgrade")
|
||||||
|
.header("Upgrade", "websocket")
|
||||||
|
.header("Sec-WebSocket-Version", "13")
|
||||||
|
.header("Sec-WebSocket-Key", generate_key())
|
||||||
|
.header("Sec-WebSocket-Protocol", "binary")
|
||||||
|
.header("Authorization", format!("PVEAPIToken={}", self.token))
|
||||||
|
.uri(url.as_str())
|
||||||
|
.body(())?;
|
||||||
|
|
||||||
|
debug!("Connecting terminal proxy: {:?}", &r);
|
||||||
|
let (ws, _rsp) = tokio_tungstenite::connect_async_tls_with_config(
|
||||||
|
r,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
Some(Connector::NativeTls(
|
||||||
|
native_tls::TlsConnector::builder()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()?,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ws)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
|
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
|
||||||
|
debug!(">> GET {}", path);
|
||||||
let rsp = self
|
let rsp = self
|
||||||
.client
|
.client
|
||||||
.get(self.base.join(path)?)
|
.get(self.base.join(path)?)
|
||||||
@ -282,10 +346,10 @@ impl ProxmoxClient {
|
|||||||
body: R,
|
body: R,
|
||||||
) -> Result<T> {
|
) -> Result<T> {
|
||||||
let body = serde_json::to_string(&body)?;
|
let body = serde_json::to_string(&body)?;
|
||||||
debug!("{}", &body);
|
debug!(">> {} {}: {}", method.clone(), path, &body);
|
||||||
let rsp = self
|
let rsp = self
|
||||||
.client
|
.client
|
||||||
.request(method, self.base.join(path)?)
|
.request(method.clone(), self.base.join(path)?)
|
||||||
.header("Authorization", format!("PVEAPIToken={}", self.token))
|
.header("Authorization", format!("PVEAPIToken={}", self.token))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
@ -299,11 +363,19 @@ impl ProxmoxClient {
|
|||||||
if status.is_success() {
|
if status.is_success() {
|
||||||
Ok(serde_json::from_str(&text)?)
|
Ok(serde_json::from_str(&text)?)
|
||||||
} else {
|
} else {
|
||||||
bail!("{}", status);
|
bail!("{} {}: {}", method, path, status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct TerminalProxyTicket {
|
||||||
|
pub port: String,
|
||||||
|
pub ticket: String,
|
||||||
|
pub upid: String,
|
||||||
|
pub user: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TaskId {
|
pub struct TaskId {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -579,5 +651,5 @@ pub struct VmConfig {
|
|||||||
pub kvm: Option<bool>,
|
pub kvm: Option<bool>,
|
||||||
#[serde(rename = "serial0")]
|
#[serde(rename = "serial0")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub serial_0: Option<String>
|
pub serial_0: Option<String>,
|
||||||
}
|
}
|
||||||
|
162
src/nip98.rs
162
src/nip98.rs
@ -1,6 +1,7 @@
|
|||||||
|
use anyhow::bail;
|
||||||
use base64::prelude::BASE64_STANDARD;
|
use base64::prelude::BASE64_STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use log::{debug, info};
|
use log::debug;
|
||||||
use nostr::{Event, JsonUtil, Kind, Timestamp};
|
use nostr::{Event, JsonUtil, Kind, Timestamp};
|
||||||
use rocket::http::uri::{Absolute, Uri};
|
use rocket::http::uri::{Absolute, Uri};
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
@ -11,82 +12,97 @@ pub struct Nip98Auth {
|
|||||||
pub event: Event,
|
pub event: Event,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
impl Nip98Auth {
|
||||||
impl<'r> FromRequest<'r> for Nip98Auth {
|
pub fn check(&self, path: &str, method: &str) -> anyhow::Result<()> {
|
||||||
type Error = &'static str;
|
if self.event.kind != Kind::HttpAuth {
|
||||||
|
bail!("Wrong event kind");
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.event
|
||||||
|
.created_at
|
||||||
|
.as_u64()
|
||||||
|
.abs_diff(Timestamp::now().as_u64())
|
||||||
|
> 600
|
||||||
|
{
|
||||||
|
bail!("Created timestamp is out of range");
|
||||||
|
}
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
// check url tag
|
||||||
if let Some(auth) = request.headers().get_one("authorization") {
|
if let Some(url) = self.event.tags.iter().find_map(|t| {
|
||||||
if auth.starts_with("Nostr ") {
|
let vec = t.as_slice();
|
||||||
let event = if let Ok(j) = BASE64_STANDARD.decode(&auth[6..]) {
|
if vec[0] == "u" {
|
||||||
if let Ok(ev) = Event::from_json(j) {
|
Some(vec[1].clone())
|
||||||
ev
|
|
||||||
} else {
|
|
||||||
return Outcome::Error((Status::new(403), "Invalid nostr event"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Outcome::Error((Status::new(403), "Invalid auth string"));
|
|
||||||
};
|
|
||||||
|
|
||||||
if event.kind != Kind::HttpAuth {
|
|
||||||
return Outcome::Error((Status::new(401), "Wrong event kind"));
|
|
||||||
}
|
|
||||||
if event
|
|
||||||
.created_at
|
|
||||||
.as_u64()
|
|
||||||
.abs_diff(Timestamp::now().as_u64())
|
|
||||||
> 600
|
|
||||||
{
|
|
||||||
return Outcome::Error((Status::new(401), "Created timestamp is out of range"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// check url tag
|
|
||||||
if let Some(url) = event.tags.iter().find_map(|t| {
|
|
||||||
let vec = t.as_slice();
|
|
||||||
if vec[0] == "u" {
|
|
||||||
Some(vec[1].clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
if let Ok(u_req) = Uri::parse::<Absolute>(&url) {
|
|
||||||
if request.uri().path() != u_req.absolute().unwrap().path() {
|
|
||||||
return Outcome::Error((Status::new(401), "U tag does not match"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Outcome::Error((Status::new(401), "Invalid U tag"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Outcome::Error((Status::new(401), "Missing url tag"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// check method tag
|
|
||||||
if let Some(method) = event.tags.iter().find_map(|t| {
|
|
||||||
let vec = t.as_slice();
|
|
||||||
if vec[0] == "method" {
|
|
||||||
Some(vec[1].clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
if request.method().to_string() != *method {
|
|
||||||
return Outcome::Error((Status::new(401), "Method tag incorrect"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Outcome::Error((Status::new(401), "Missing method tag"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(_err) = event.verify() {
|
|
||||||
return Outcome::Error((Status::new(401), "Event signature invalid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("{}", event.as_json());
|
|
||||||
Outcome::Success(Nip98Auth { event })
|
|
||||||
} else {
|
} else {
|
||||||
Outcome::Error((Status::new(403), "Auth scheme must be Nostr"))
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if let Ok(u_req) = Uri::parse::<Absolute>(&url) {
|
||||||
|
if path != u_req.absolute().unwrap().path() {
|
||||||
|
bail!("U tag does not match");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("Invalid U tag");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Outcome::Error((Status::new(403), "Auth header not found"))
|
bail!("Missing url tag");
|
||||||
|
}
|
||||||
|
|
||||||
|
// check method tag
|
||||||
|
if let Some(t_method) = self.event.tags.iter().find_map(|t| {
|
||||||
|
let vec = t.as_slice();
|
||||||
|
if vec[0] == "method" {
|
||||||
|
Some(vec[1].clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if method != t_method {
|
||||||
|
bail!("Method tag incorrect")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("Missing method tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(_err) = self.event.verify() {
|
||||||
|
bail!("Event signature invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("{}", self.event.as_json());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_base64(i: &str) -> anyhow::Result<Self> {
|
||||||
|
if let Ok(j) = BASE64_STANDARD.decode(i) {
|
||||||
|
if let Ok(ev) = Event::from_json(j) {
|
||||||
|
Ok(Self { event: ev })
|
||||||
|
} else {
|
||||||
|
bail!("Invalid nostr event")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("Invalid auth string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for Nip98Auth {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
if let Some(auth) = request.headers().get_one("authorization") {
|
||||||
|
if !auth.starts_with("Nostr ") {
|
||||||
|
return Outcome::Error((Status::new(403), "Auth scheme must be Nostr".to_string()));
|
||||||
|
}
|
||||||
|
let auth = Nip98Auth::from_base64(&auth[6..]).unwrap();
|
||||||
|
match auth.check(
|
||||||
|
request.uri().to_string().as_str(),
|
||||||
|
request.method().as_str(),
|
||||||
|
) {
|
||||||
|
Ok(_) => Outcome::Success(auth),
|
||||||
|
Err(e) => Outcome::Error((Status::new(401), e.to_string())),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Outcome::Error((Status::new(403), "Auth header not found".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use crate::exchange::{ExchangeRateCache, Ticker};
|
use crate::exchange::{ExchangeRateCache, Ticker};
|
||||||
use crate::host::get_host_client;
|
use crate::host::get_host_client;
|
||||||
use crate::host::proxmox::{
|
use crate::host::proxmox::{
|
||||||
CreateVm, DownloadUrlRequest, ProxmoxClient, ResizeDiskRequest, StorageContent, VmBios,
|
ConfigureVm, CreateVm, DownloadUrlRequest, ProxmoxClient, ResizeDiskRequest, StorageContent,
|
||||||
VmConfig,
|
VmBios, VmConfig,
|
||||||
};
|
};
|
||||||
use crate::provisioner::Provisioner;
|
use crate::provisioner::Provisioner;
|
||||||
|
use crate::router::Router;
|
||||||
use crate::settings::{QemuConfig, SshConfig};
|
use crate::settings::{QemuConfig, SshConfig};
|
||||||
use crate::ssh_client::SshClient;
|
use crate::ssh_client::SshClient;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
@ -15,18 +16,23 @@ use fedimint_tonic_lnd::Client;
|
|||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use lnvps_db::hydrate::Hydrate;
|
use lnvps_db::hydrate::Hydrate;
|
||||||
use lnvps_db::{IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
|
use lnvps_db::{IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
|
||||||
use log::info;
|
use log::{debug, info, warn};
|
||||||
use nostr::util::hex;
|
use nostr::util::hex;
|
||||||
use rand::random;
|
use rand::random;
|
||||||
use rand::seq::IteratorRandom;
|
use rand::seq::IteratorRandom;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
use rocket::futures::{SinkExt, StreamExt};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||||
|
|
||||||
pub struct LNVpsProvisioner {
|
pub struct LNVpsProvisioner {
|
||||||
db: Box<dyn LNVpsDb>,
|
db: Box<dyn LNVpsDb>,
|
||||||
|
router: Option<Box<dyn Router>>,
|
||||||
lnd: Client,
|
lnd: Client,
|
||||||
rates: ExchangeRateCache,
|
rates: ExchangeRateCache,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
@ -40,11 +46,13 @@ impl LNVpsProvisioner {
|
|||||||
config: QemuConfig,
|
config: QemuConfig,
|
||||||
ssh: Option<SshConfig>,
|
ssh: Option<SshConfig>,
|
||||||
db: impl LNVpsDb + 'static,
|
db: impl LNVpsDb + 'static,
|
||||||
|
router: Option<impl Router + 'static>,
|
||||||
lnd: Client,
|
lnd: Client,
|
||||||
rates: ExchangeRateCache,
|
rates: ExchangeRateCache,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
db: Box::new(db),
|
db: Box::new(db),
|
||||||
|
router: router.map(|r| Box::new(r) as Box<dyn Router>),
|
||||||
lnd,
|
lnd,
|
||||||
rates,
|
rates,
|
||||||
config,
|
config,
|
||||||
@ -64,6 +72,76 @@ impl LNVpsProvisioner {
|
|||||||
bail!("No image storage found");
|
bail!("No image storage found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_vm_config(&self, vm: &Vm) -> Result<VmConfig> {
|
||||||
|
let ssh_key = self.db.get_user_ssh_key(vm.ssh_key_id).await?;
|
||||||
|
|
||||||
|
let mut ips = self.db.list_vm_ip_assignments(vm.id).await?;
|
||||||
|
if ips.is_empty() {
|
||||||
|
ips = self.allocate_ips(vm.id).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load ranges
|
||||||
|
for ip in &mut ips {
|
||||||
|
ip.hydrate_up(&self.db).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ip_config = ips
|
||||||
|
.iter()
|
||||||
|
.map_while(|ip| {
|
||||||
|
if let Ok(net) = ip.ip.parse::<IpNetwork>() {
|
||||||
|
Some(match net {
|
||||||
|
IpNetwork::V4(addr) => {
|
||||||
|
format!(
|
||||||
|
"ip={},gw={}",
|
||||||
|
addr,
|
||||||
|
ip.ip_range.as_ref().map(|r| &r.gateway).unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IpNetwork::V6(addr) => format!("ip6={}", addr),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ip_config.push("ip6=auto".to_string());
|
||||||
|
|
||||||
|
let mut net = vec![
|
||||||
|
format!("virtio={}", vm.mac_address),
|
||||||
|
format!("bridge={}", self.config.bridge),
|
||||||
|
];
|
||||||
|
if let Some(t) = self.config.vlan {
|
||||||
|
net.push(format!("tag={}", t));
|
||||||
|
}
|
||||||
|
|
||||||
|
let drives = self.db.list_host_disks(vm.host_id).await?;
|
||||||
|
let drive = if let Some(d) = drives.iter().find(|d| d.enabled) {
|
||||||
|
d
|
||||||
|
} else {
|
||||||
|
bail!("No host drive found!")
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(VmConfig {
|
||||||
|
cpu: Some(self.config.cpu.clone()),
|
||||||
|
kvm: Some(self.config.kvm),
|
||||||
|
ip_config: Some(ip_config.join(",")),
|
||||||
|
machine: Some(self.config.machine.clone()),
|
||||||
|
net: Some(net.join(",")),
|
||||||
|
os_type: Some(self.config.os_type.clone()),
|
||||||
|
on_boot: Some(true),
|
||||||
|
bios: Some(VmBios::OVMF),
|
||||||
|
boot: Some("order=scsi0".to_string()),
|
||||||
|
cores: Some(vm.cpu as i32),
|
||||||
|
memory: Some((vm.memory / 1024 / 1024).to_string()),
|
||||||
|
scsi_hw: Some("virtio-scsi-pci".to_string()),
|
||||||
|
serial_0: Some("socket".to_string()),
|
||||||
|
scsi_1: Some(format!("{}:cloudinit", &drive.name)),
|
||||||
|
ssh_keys: Some(urlencoding::encode(&ssh_key.key_data).to_string()),
|
||||||
|
efi_disk_0: Some(format!("{}:0,efitype=4m", &drive.name)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -265,6 +343,12 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
ip: ip_net.to_string(),
|
ip: ip_net.to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// add arp entry for router
|
||||||
|
if let Some(r) = self.router.as_ref() {
|
||||||
|
r.add_arp_entry(ip, &vm.mac_address, Some(&format!("VM{}", vm.id)))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
let id = self.db.insert_vm_ip_assignment(&assignment).await?;
|
let id = self.db.insert_vm_ip_assignment(&assignment).await?;
|
||||||
assignment.id = id;
|
assignment.id = id;
|
||||||
|
|
||||||
@ -274,6 +358,10 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ret.is_empty() {
|
||||||
|
bail!("No ip ranges found in this region");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,55 +372,6 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
let vm = self.db.get_vm(vm_id).await?;
|
let vm = self.db.get_vm(vm_id).await?;
|
||||||
let host = self.db.get_host(vm.host_id).await?;
|
let host = self.db.get_host(vm.host_id).await?;
|
||||||
let client = get_host_client(&host)?;
|
let client = get_host_client(&host)?;
|
||||||
|
|
||||||
let mut ips = self.db.list_vm_ip_assignments(vm.id).await?;
|
|
||||||
if ips.is_empty() {
|
|
||||||
ips = self.allocate_ips(vm.id).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// load ranges
|
|
||||||
for ip in &mut ips {
|
|
||||||
ip.hydrate_up(&self.db).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ip_config = ips
|
|
||||||
.iter()
|
|
||||||
.map_while(|ip| {
|
|
||||||
if let Ok(net) = ip.ip.parse::<IpNetwork>() {
|
|
||||||
Some(match net {
|
|
||||||
IpNetwork::V4(addr) => {
|
|
||||||
format!(
|
|
||||||
"ip={},gw={}",
|
|
||||||
addr,
|
|
||||||
ip.ip_range.as_ref().map(|r| &r.gateway).unwrap()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IpNetwork::V6(addr) => format!("ip6={}", addr),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
ip_config.push("ip6=auto".to_string());
|
|
||||||
|
|
||||||
let drives = self.db.list_host_disks(vm.host_id).await?;
|
|
||||||
let drive = if let Some(d) = drives.iter().find(|d| d.enabled) {
|
|
||||||
d
|
|
||||||
} else {
|
|
||||||
bail!("No host drive found!")
|
|
||||||
};
|
|
||||||
|
|
||||||
let ssh_key = self.db.get_user_ssh_key(vm.ssh_key_id).await?;
|
|
||||||
|
|
||||||
let mut net = vec![
|
|
||||||
format!("virtio={}", vm.mac_address),
|
|
||||||
format!("bridge={}", self.config.bridge),
|
|
||||||
];
|
|
||||||
if let Some(t) = self.config.vlan {
|
|
||||||
net.push(format!("tag={}", t));
|
|
||||||
}
|
|
||||||
|
|
||||||
let vm_id = 100 + vm.id as i32;
|
let vm_id = 100 + vm.id as i32;
|
||||||
|
|
||||||
// create VM
|
// create VM
|
||||||
@ -340,25 +379,7 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
.create_vm(CreateVm {
|
.create_vm(CreateVm {
|
||||||
node: host.name.clone(),
|
node: host.name.clone(),
|
||||||
vm_id,
|
vm_id,
|
||||||
config: VmConfig {
|
config: self.get_vm_config(&vm).await?,
|
||||||
on_boot: Some(true),
|
|
||||||
bios: Some(VmBios::OVMF),
|
|
||||||
boot: Some("order=scsi0".to_string()),
|
|
||||||
cores: Some(vm.cpu as i32),
|
|
||||||
cpu: Some(self.config.cpu.clone()),
|
|
||||||
kvm: Some(self.config.kvm),
|
|
||||||
ip_config: Some(ip_config.join(",")),
|
|
||||||
machine: Some(self.config.machine.clone()),
|
|
||||||
memory: Some((vm.memory / 1024 / 1024).to_string()),
|
|
||||||
net: Some(net.join(",")),
|
|
||||||
os_type: Some(self.config.os_type.clone()),
|
|
||||||
scsi_1: Some(format!("{}:cloudinit", &drive.name)),
|
|
||||||
scsi_hw: Some("virtio-scsi-pci".to_string()),
|
|
||||||
ssh_keys: Some(urlencoding::encode(&ssh_key.key_data).to_string()),
|
|
||||||
efi_disk_0: Some(format!("{}:0,efitype=4m", &drive.name)),
|
|
||||||
serial_0: Some("socket".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
client.wait_for_task(&t_create).await?;
|
client.wait_for_task(&t_create).await?;
|
||||||
@ -376,6 +397,12 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let drives = self.db.list_host_disks(vm.host_id).await?;
|
||||||
|
let drive = if let Some(d) = drives.iter().find(|d| d.enabled) {
|
||||||
|
d
|
||||||
|
} else {
|
||||||
|
bail!("No host drive found!")
|
||||||
|
};
|
||||||
let cmd = format!(
|
let cmd = format!(
|
||||||
"/usr/sbin/qm set {} --scsi0 {}:0,import-from=/var/lib/vz/template/iso/{}",
|
"/usr/sbin/qm set {} --scsi0 {}:0,import-from=/var/lib/vz/template/iso/{}",
|
||||||
vm_id,
|
vm_id,
|
||||||
@ -443,17 +470,82 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
|
|
||||||
async fn delete_vm(&self, vm_id: u64) -> Result<()> {
|
async fn delete_vm(&self, vm_id: u64) -> Result<()> {
|
||||||
let vm = self.db.get_vm(vm_id).await?;
|
let vm = self.db.get_vm(vm_id).await?;
|
||||||
let host = self.db.get_host(vm.host_id).await?;
|
//let host = self.db.get_host(vm.host_id).await?;
|
||||||
|
|
||||||
let client = get_host_client(&host)?;
|
|
||||||
// TODO: delete not implemented, stop only
|
// TODO: delete not implemented, stop only
|
||||||
|
//let client = get_host_client(&host)?;
|
||||||
//let j_start = client.delete_vm(&host.name, vm.id + 100).await?;
|
//let j_start = client.delete_vm(&host.name, vm.id + 100).await?;
|
||||||
let j_stop = client.stop_vm(&host.name, vm.id + 100).await?;
|
//let j_stop = client.stop_vm(&host.name, vm.id + 100).await?;
|
||||||
client.wait_for_task(&j_stop).await?;
|
//client.wait_for_task(&j_stop).await?;
|
||||||
|
|
||||||
|
if let Some(r) = self.router.as_ref() {
|
||||||
|
let ent = r.list_arp_entry().await?;
|
||||||
|
if let Some(ent) = ent.iter().find(|e| {
|
||||||
|
e.mac_address
|
||||||
|
.as_ref()
|
||||||
|
.map(|m| m.eq_ignore_ascii_case(&vm.mac_address))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}) {
|
||||||
|
r.remove_arp_entry(ent.id.as_ref().unwrap().as_str())
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
warn!("ARP entry not found, skipping")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.db.delete_vm_ip_assignment(vm.id).await?;
|
self.db.delete_vm_ip_assignment(vm.id).await?;
|
||||||
self.db.delete_vm(vm.id).await?;
|
self.db.delete_vm(vm.id).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn terminal_proxy(
|
||||||
|
&self,
|
||||||
|
vm_id: u64,
|
||||||
|
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
||||||
|
let vm = self.db.get_vm(vm_id).await?;
|
||||||
|
let host = self.db.get_host(vm.host_id).await?;
|
||||||
|
let client = get_host_client(&host)?;
|
||||||
|
|
||||||
|
let host_vm_id = vm.id + 100;
|
||||||
|
let term = client.terminal_proxy(&host.name, host_vm_id).await?;
|
||||||
|
|
||||||
|
let login_msg = format!("{}:{}\n", term.user, term.ticket);
|
||||||
|
let mut ws = client
|
||||||
|
.open_terminal_proxy(&host.name, host_vm_id, term)
|
||||||
|
.await?;
|
||||||
|
debug!("Sending login msg: {}", login_msg);
|
||||||
|
ws.send(Message::Text(login_msg)).await?;
|
||||||
|
if let Some(n) = ws.next().await {
|
||||||
|
debug!("{:?}", n);
|
||||||
|
} else {
|
||||||
|
bail!("No response from terminal_proxy");
|
||||||
|
}
|
||||||
|
ws.send(Message::Text("1:86:24:".to_string())).await?;
|
||||||
|
Ok(ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn patch_vm(&self, vm_id: u64) -> Result<()> {
|
||||||
|
let vm = self.db.get_vm(vm_id).await?;
|
||||||
|
let host = self.db.get_host(vm.host_id).await?;
|
||||||
|
let client = get_host_client(&host)?;
|
||||||
|
let host_vm_id = vm.id + 100;
|
||||||
|
|
||||||
|
let t = client
|
||||||
|
.configure_vm(ConfigureVm {
|
||||||
|
node: host.name.clone(),
|
||||||
|
vm_id: host_vm_id as i32,
|
||||||
|
current: None,
|
||||||
|
snapshot: None,
|
||||||
|
config: VmConfig {
|
||||||
|
scsi_0: None,
|
||||||
|
scsi_1: None,
|
||||||
|
efi_disk_0: None,
|
||||||
|
..self.get_vm_config(&vm).await?
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
client.wait_for_task(&t).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use lnvps_db::{Vm, VmIpAssignment, VmPayment};
|
use lnvps_db::{Vm, VmIpAssignment, VmPayment};
|
||||||
use rocket::async_trait;
|
use rocket::async_trait;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||||
|
|
||||||
pub mod lnvps;
|
pub mod lnvps;
|
||||||
|
|
||||||
@ -42,4 +44,13 @@ pub trait Provisioner: Send + Sync {
|
|||||||
|
|
||||||
/// Delete a VM
|
/// Delete a VM
|
||||||
async fn delete_vm(&self, vm_id: u64) -> Result<()>;
|
async fn delete_vm(&self, vm_id: u64) -> Result<()>;
|
||||||
|
|
||||||
|
/// Open terminal proxy connection
|
||||||
|
async fn terminal_proxy(
|
||||||
|
&self,
|
||||||
|
vm_id: u64,
|
||||||
|
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>>;
|
||||||
|
|
||||||
|
/// Re-Configure VM
|
||||||
|
async fn patch_vm(&self, vm_id: u64) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,101 @@
|
|||||||
use crate::router::Router;
|
use crate::router::{ArpEntry, Router};
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use base64::Engine;
|
||||||
|
use log::debug;
|
||||||
|
use reqwest::{Client, Method, Url};
|
||||||
use rocket::async_trait;
|
use rocket::async_trait;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
pub struct MikrotikRouter {
|
pub struct MikrotikRouter {
|
||||||
url: String,
|
url: Url,
|
||||||
token: String,
|
username: String,
|
||||||
|
password: String,
|
||||||
|
client: Client,
|
||||||
|
arp_interface: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MikrotikRouter {
|
impl MikrotikRouter {
|
||||||
pub fn new(url: &str, token: &str) -> Self {
|
pub fn new(url: &str, username: &str, password: &str, arp_interface: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
url: url.to_string(),
|
url: url.parse().unwrap(),
|
||||||
token: token.to_string(),
|
username: username.to_string(),
|
||||||
|
password: password.to_string(),
|
||||||
|
client: Client::builder()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
arp_interface: arp_interface.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn req<T: DeserializeOwned, R: Serialize>(
|
||||||
|
&self,
|
||||||
|
method: Method,
|
||||||
|
path: &str,
|
||||||
|
body: R,
|
||||||
|
) -> Result<T> {
|
||||||
|
let body = serde_json::to_string(&body)?;
|
||||||
|
debug!(">> {} {}: {}", method.clone(), path, &body);
|
||||||
|
let rsp = self
|
||||||
|
.client
|
||||||
|
.request(method.clone(), self.url.join(path)?)
|
||||||
|
.header(
|
||||||
|
"Authorization",
|
||||||
|
format!(
|
||||||
|
"Basic {}",
|
||||||
|
STANDARD.encode(format!("{}:{}", self.username, self.password))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.body(body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let status = rsp.status();
|
||||||
|
let text = rsp.text().await?;
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
debug!("<< {}", text);
|
||||||
|
if status.is_success() {
|
||||||
|
Ok(serde_json::from_str(&text)?)
|
||||||
|
} else {
|
||||||
|
bail!("{} {}: {}", method, path, status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Router for MikrotikRouter {
|
impl Router for MikrotikRouter {
|
||||||
async fn add_arp_entry(
|
async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>> {
|
||||||
&self,
|
let rsp: Vec<ArpEntry> = self.req(Method::GET, "/rest/ip/arp", ()).await?;
|
||||||
ip: IpAddr,
|
Ok(rsp)
|
||||||
mac: &[u8; 6],
|
}
|
||||||
comment: Option<&str>,
|
|
||||||
) -> anyhow::Result<()> {
|
async fn add_arp_entry(&self, ip: IpAddr, mac: &str, comment: Option<&str>) -> Result<()> {
|
||||||
todo!()
|
let _rsp: ArpEntry = self
|
||||||
|
.req(
|
||||||
|
Method::PUT,
|
||||||
|
"/rest/ip/arp",
|
||||||
|
ArpEntry {
|
||||||
|
address: ip.to_string(),
|
||||||
|
mac_address: Some(mac.to_string()),
|
||||||
|
interface: self.arp_interface.to_string(),
|
||||||
|
comment: comment.map(|c| c.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
|
||||||
|
let _rsp: ArpEntry = self
|
||||||
|
.req(Method::DELETE, &format!("/rest/ip/arp/{id}"), ())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use rocket::async_trait;
|
use rocket::async_trait;
|
||||||
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
/// Router defines a network device used to access the hosts
|
/// Router defines a network device used to access the hosts
|
||||||
@ -10,9 +11,27 @@ use std::net::IpAddr;
|
|||||||
///
|
///
|
||||||
/// It also prevents people from re-assigning their IP to another in the range,
|
/// It also prevents people from re-assigning their IP to another in the range,
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Router {
|
pub trait Router: Send + Sync {
|
||||||
async fn add_arp_entry(&self, ip: IpAddr, mac: &[u8; 6], comment: Option<&str>) -> Result<()>;
|
async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>>;
|
||||||
|
async fn add_arp_entry(&self, ip: IpAddr, mac: &str, comment: Option<&str>) -> Result<()>;
|
||||||
|
async fn remove_arp_entry(&self, id: &str) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct ArpEntry {
|
||||||
|
#[serde(rename = ".id")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub address: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "mac-address")]
|
||||||
|
pub mac_address: Option<String>,
|
||||||
|
pub interface: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub comment: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mikrotik")]
|
||||||
mod mikrotik;
|
mod mikrotik;
|
||||||
|
#[cfg(feature = "mikrotik")]
|
||||||
pub use mikrotik::*;
|
pub use mikrotik::*;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::exchange::ExchangeRateCache;
|
use crate::exchange::ExchangeRateCache;
|
||||||
use crate::provisioner::lnvps::LNVpsProvisioner;
|
use crate::provisioner::lnvps::LNVpsProvisioner;
|
||||||
use crate::provisioner::Provisioner;
|
use crate::provisioner::Provisioner;
|
||||||
|
use crate::router::{MikrotikRouter, Router};
|
||||||
use fedimint_tonic_lnd::Client;
|
use fedimint_tonic_lnd::Client;
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -20,6 +21,12 @@ pub struct Settings {
|
|||||||
|
|
||||||
/// SMTP settings for sending emails
|
/// SMTP settings for sending emails
|
||||||
pub smtp: Option<SmtpConfig>,
|
pub smtp: Option<SmtpConfig>,
|
||||||
|
|
||||||
|
/// Network router config
|
||||||
|
pub router: Option<RouterConfig>,
|
||||||
|
|
||||||
|
/// Nostr config for sending DM's
|
||||||
|
pub nostr: Option<NostrConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
@ -29,6 +36,24 @@ pub struct LndConfig {
|
|||||||
pub macaroon: PathBuf,
|
pub macaroon: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct NostrConfig {
|
||||||
|
pub relays: Vec<String>,
|
||||||
|
pub nsec: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub enum RouterConfig {
|
||||||
|
Mikrotik {
|
||||||
|
url: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
|
||||||
|
/// Interface used to add arp entries
|
||||||
|
arp_interface: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct SmtpConfig {
|
pub struct SmtpConfig {
|
||||||
/// Admin user id, for sending system notifications
|
/// Admin user id, for sending system notifications
|
||||||
@ -92,6 +117,7 @@ impl ProvisionerConfig {
|
|||||||
pub fn get_provisioner(
|
pub fn get_provisioner(
|
||||||
&self,
|
&self,
|
||||||
db: impl LNVpsDb + 'static,
|
db: impl LNVpsDb + 'static,
|
||||||
|
router: Option<impl Router + 'static>,
|
||||||
lnd: Client,
|
lnd: Client,
|
||||||
exchange: ExchangeRateCache,
|
exchange: ExchangeRateCache,
|
||||||
) -> impl Provisioner + 'static {
|
) -> impl Provisioner + 'static {
|
||||||
@ -100,7 +126,28 @@ impl ProvisionerConfig {
|
|||||||
qemu,
|
qemu,
|
||||||
ssh,
|
ssh,
|
||||||
read_only,
|
read_only,
|
||||||
} => LNVpsProvisioner::new(*read_only, qemu.clone(), ssh.clone(), db, lnd, exchange),
|
} => LNVpsProvisioner::new(
|
||||||
|
*read_only,
|
||||||
|
qemu.clone(),
|
||||||
|
ssh.clone(),
|
||||||
|
db,
|
||||||
|
router,
|
||||||
|
lnd,
|
||||||
|
exchange,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RouterConfig {
|
||||||
|
pub fn get_router(&self) -> impl Router + 'static {
|
||||||
|
match self {
|
||||||
|
RouterConfig::Mikrotik {
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
arp_interface,
|
||||||
|
} => MikrotikRouter::new(&url, &username, &password, &arp_interface),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use ssh2::Channel;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||||
@ -28,6 +29,11 @@ impl SshClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn open_channel(&mut self) -> Result<Channel> {
|
||||||
|
let channel = self.session.channel_session()?;
|
||||||
|
Ok(channel)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn execute(&mut self, command: &str) -> Result<(i32, String)> {
|
pub async fn execute(&mut self, command: &str) -> Result<(i32, String)> {
|
||||||
info!("Executing command: {}", command);
|
info!("Executing command: {}", command);
|
||||||
let mut channel = self.session.channel_session()?;
|
let mut channel = self.session.channel_session()?;
|
||||||
|
@ -5,15 +5,15 @@ use crate::settings::{Settings, SmtpConfig};
|
|||||||
use crate::status::{VmRunningState, VmState, VmStateCache};
|
use crate::status::{VmRunningState, VmState, VmStateCache};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::{DateTime, Days, Utc};
|
use chrono::{DateTime, Days, Utc};
|
||||||
use lettre::message::MessageBuilder;
|
use lettre::message::{MessageBuilder, MultiPart};
|
||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
use lettre::transport::smtp::SmtpTransportBuilder;
|
|
||||||
use lettre::AsyncTransport;
|
use lettre::AsyncTransport;
|
||||||
use lettre::{AsyncSmtpTransport, SmtpTransport, Tokio1Executor, Transport};
|
use lettre::{AsyncSmtpTransport, Tokio1Executor};
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use rocket::futures::SinkExt;
|
use nostr::{EventBuilder, PublicKey};
|
||||||
use std::ops::Add;
|
use nostr_sdk::Client;
|
||||||
|
use std::ops::{Add, Sub};
|
||||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -39,7 +39,8 @@ pub struct Worker {
|
|||||||
vm_state_cache: VmStateCache,
|
vm_state_cache: VmStateCache,
|
||||||
tx: UnboundedSender<WorkJob>,
|
tx: UnboundedSender<WorkJob>,
|
||||||
rx: UnboundedReceiver<WorkJob>,
|
rx: UnboundedReceiver<WorkJob>,
|
||||||
last_check_vms: u64,
|
client: Option<Client>,
|
||||||
|
last_check_vms: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WorkerSettings {
|
pub struct WorkerSettings {
|
||||||
@ -62,6 +63,7 @@ impl Worker {
|
|||||||
provisioner: P,
|
provisioner: P,
|
||||||
settings: impl Into<WorkerSettings>,
|
settings: impl Into<WorkerSettings>,
|
||||||
vm_state_cache: VmStateCache,
|
vm_state_cache: VmStateCache,
|
||||||
|
client: Option<Client>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (tx, rx) = unbounded_channel();
|
let (tx, rx) = unbounded_channel();
|
||||||
Self {
|
Self {
|
||||||
@ -71,7 +73,8 @@ impl Worker {
|
|||||||
settings: settings.into(),
|
settings: settings.into(),
|
||||||
tx,
|
tx,
|
||||||
rx,
|
rx,
|
||||||
last_check_vms: Utc::now().timestamp() as u64,
|
client,
|
||||||
|
last_check_vms: Utc::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +102,23 @@ impl Worker {
|
|||||||
self.vm_state_cache.set_state(db_id, state).await?;
|
self.vm_state_cache.set_state(db_id, state).await?;
|
||||||
|
|
||||||
if let Ok(db_vm) = self.db.get_vm(db_id).await {
|
if let Ok(db_vm) = self.db.get_vm(db_id).await {
|
||||||
|
const BEFORE_EXPIRE_NOTIFICATION: u64 = 1;
|
||||||
|
|
||||||
|
// Send notification of VM expiring soon
|
||||||
|
if db_vm.expires < Utc::now().add(Days::new(BEFORE_EXPIRE_NOTIFICATION))
|
||||||
|
&& db_vm.expires
|
||||||
|
> self
|
||||||
|
.last_check_vms
|
||||||
|
.add(Days::new(BEFORE_EXPIRE_NOTIFICATION))
|
||||||
|
{
|
||||||
|
info!("Sending expire soon notification VM {}", db_vm.id);
|
||||||
|
self.tx.send(WorkJob::SendNotification {
|
||||||
|
user_id: db_vm.user_id,
|
||||||
|
title: Some(format!("[VM{}] Expiring Soon", db_vm.id)),
|
||||||
|
message: format!("Your VM #{} will expire soon, please renew in the next {} days or your VM will be stopped.", db_vm.id, BEFORE_EXPIRE_NOTIFICATION)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
// Stop VM if expired and is running
|
// Stop VM if expired and is running
|
||||||
if db_vm.expires < Utc::now() && s.status == VmStatus::Running {
|
if db_vm.expires < Utc::now() && s.status == VmStatus::Running {
|
||||||
info!("Stopping expired VM {}", db_vm.id);
|
info!("Stopping expired VM {}", db_vm.id);
|
||||||
@ -118,11 +138,16 @@ impl Worker {
|
|||||||
{
|
{
|
||||||
info!("Deleting expired VM {}", db_vm.id);
|
info!("Deleting expired VM {}", db_vm.id);
|
||||||
self.provisioner.delete_vm(db_vm.id).await?;
|
self.provisioner.delete_vm(db_vm.id).await?;
|
||||||
|
let title = Some(format!("[VM{}] Deleted", db_vm.id));
|
||||||
self.tx.send(WorkJob::SendNotification {
|
self.tx.send(WorkJob::SendNotification {
|
||||||
user_id: db_vm.user_id,
|
user_id: db_vm.user_id,
|
||||||
title: Some(format!("[VM{}] Deleted", db_vm.id)),
|
title: title.clone(),
|
||||||
message: format!("Your VM #{} has been deleted!", db_vm.id),
|
message: format!("Your VM #{} has been deleted!", db_vm.id),
|
||||||
})?;
|
})?;
|
||||||
|
self.queue_admin_notification(
|
||||||
|
format!("VM{} is ready for deletion", db_vm.id),
|
||||||
|
title,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,16 +188,16 @@ impl Worker {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_vms(&self) -> Result<()> {
|
pub async fn check_vms(&mut self) -> Result<()> {
|
||||||
let hosts = self.db.list_hosts().await?;
|
let hosts = self.db.list_hosts().await?;
|
||||||
for host in hosts {
|
for host in hosts {
|
||||||
let client = get_host_client(&host)?;
|
let client = get_host_client(&host)?;
|
||||||
|
|
||||||
for node in client.list_nodes().await? {
|
for node in client.list_nodes().await? {
|
||||||
info!("Checking vms for {}", node.name);
|
debug!("Checking vms for {}", node.name);
|
||||||
for vm in client.list_vms(&node.name).await? {
|
for vm in client.list_vms(&node.name).await? {
|
||||||
let vm_id = vm.vm_id;
|
let vm_id = vm.vm_id;
|
||||||
info!("\t{}: {:?}", vm_id, vm.status);
|
debug!("\t{}: {:?}", vm_id, vm.status);
|
||||||
if let Err(e) = self.handle_vm_info(vm).await {
|
if let Err(e) = self.handle_vm_info(vm).await {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
self.queue_admin_notification(
|
self.queue_admin_notification(
|
||||||
@ -184,6 +209,7 @@ impl Worker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check VM status from db vm list
|
||||||
let db_vms = self.db.list_vms().await?;
|
let db_vms = self.db.list_vms().await?;
|
||||||
for vm in db_vms {
|
for vm in db_vms {
|
||||||
let state = if let Some(s) = self.vm_state_cache.get_state(vm.id).await {
|
let state = if let Some(s) = self.vm_state_cache.get_state(vm.id).await {
|
||||||
@ -196,10 +222,19 @@ impl Worker {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if state.is_none() && vm.expires > Utc::now() {
|
// create VM if not spawned yet
|
||||||
|
if vm.expires > Utc::now() && state.is_none() {
|
||||||
self.check_vm(vm.id).await?;
|
self.check_vm(vm.id).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete vm if not paid (in new state)
|
||||||
|
if !vm.deleted && vm.expires < Utc::now().sub(Days::new(1)) && state.is_none() {
|
||||||
|
info!("Deleting unpaid VM {}", vm.id);
|
||||||
|
self.provisioner.delete_vm(vm.id).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.last_check_vms = Utc::now();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,9 +255,15 @@ impl Worker {
|
|||||||
if let Some(f) = &smtp.from {
|
if let Some(f) = &smtp.from {
|
||||||
b = b.from(f.parse()?);
|
b = b.from(f.parse()?);
|
||||||
}
|
}
|
||||||
let msg = b.body(message)?;
|
let template = include_str!("../email.html");
|
||||||
|
let html = MultiPart::alternative_plain_html(
|
||||||
|
message.clone(),
|
||||||
|
template.replace("%%_MESSAGE_%%", &message),
|
||||||
|
);
|
||||||
|
|
||||||
let mut sender = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.server)?
|
let msg = b.multipart(html)?;
|
||||||
|
|
||||||
|
let sender = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.server)?
|
||||||
.credentials(Credentials::new(
|
.credentials(Credentials::new(
|
||||||
smtp.username.to_string(),
|
smtp.username.to_string(),
|
||||||
smtp.password.to_string(),
|
smtp.password.to_string(),
|
||||||
@ -233,10 +274,20 @@ impl Worker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if user.contact_nip4 {
|
if user.contact_nip4 {
|
||||||
// send DM
|
// send dm
|
||||||
}
|
}
|
||||||
if user.contact_nip17 {
|
if user.contact_nip17 {
|
||||||
// send dm
|
if let Some(c) = self.client.as_ref() {
|
||||||
|
let sig = c.signer().await?;
|
||||||
|
let ev = EventBuilder::private_msg(
|
||||||
|
&sig,
|
||||||
|
PublicKey::from_slice(&user.pubkey)?,
|
||||||
|
message,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
c.send_event(ev).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user