Compare commits
30 Commits
1bb03762bd
...
master
Author | SHA1 | Date | |
---|---|---|---|
b6c4ab43fc
|
|||
51b8f42407
|
|||
ee01eae498
|
|||
e463e06a35
|
|||
1dda3a561d
|
|||
179d70edb0
|
|||
c859c153c1
|
|||
cd7ac9e967
|
|||
f6a756db78
|
|||
7e10e0dd6e
|
|||
4db6aa1897
|
|||
4dfc33bca2
|
|||
c432f603ec
|
|||
a4850b4e06
|
|||
9296e571ec
|
|||
2ae158c31a
|
|||
b7d7027eec
|
|||
b9bec36843
|
|||
97d631ce5d
|
|||
36069bb6a7
|
|||
6a8f1826bb
|
|||
7deed82a7c
|
|||
6ca8283040
|
|||
b8ab61c48f
|
|||
70a4d9c638
|
|||
d316ccacd7
|
|||
396cb8a7ef
|
|||
8068b7d5bc
|
|||
e2d6d84439
|
|||
ea6499558d
|
295
Cargo.lock
generated
295
Cargo.lock
generated
@ -27,6 +27,17 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
@ -309,6 +320,16 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base58ck"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
|
||||||
|
dependencies = [
|
||||||
|
"bitcoin-internals 0.3.0",
|
||||||
|
"bitcoin_hashes 0.14.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.7"
|
version = "0.21.7"
|
||||||
@ -350,25 +371,62 @@ dependencies = [
|
|||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitcoin"
|
||||||
|
version = "0.32.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026"
|
||||||
|
dependencies = [
|
||||||
|
"base58ck",
|
||||||
|
"bech32",
|
||||||
|
"bitcoin-internals 0.3.0",
|
||||||
|
"bitcoin-io",
|
||||||
|
"bitcoin-units",
|
||||||
|
"bitcoin_hashes 0.14.0",
|
||||||
|
"hex-conservative 0.2.1",
|
||||||
|
"hex_lit",
|
||||||
|
"secp256k1",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin-internals"
|
name = "bitcoin-internals"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
|
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitcoin-internals"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin-io"
|
name = "bitcoin-io"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf"
|
checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitcoin-units"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2"
|
||||||
|
dependencies = [
|
||||||
|
"bitcoin-internals 0.3.0",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin_hashes"
|
name = "bitcoin_hashes"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
|
checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin-internals",
|
"bitcoin-internals 0.2.0",
|
||||||
"hex-conservative 0.1.2",
|
"hex-conservative 0.1.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -526,9 +584,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.34"
|
version = "4.5.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
|
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@ -536,9 +594,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.34"
|
version = "4.5.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
|
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@ -920,9 +978,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "email_address"
|
name = "email_address"
|
||||||
version = "0.2.9"
|
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 = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
checksum = "c1019fa28f600f5b581b7a603d515c3f1635da041ca211b5055804788673abfe"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
@ -939,21 +1000,21 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.11.7"
|
version = "0.11.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697"
|
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"env_filter",
|
"env_filter",
|
||||||
"jiff",
|
"jiff",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1205,7 +1266,7 @@ checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"windows 0.48.0",
|
"windows 0.48.0",
|
||||||
]
|
]
|
||||||
@ -1394,6 +1455,12 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex_lit"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hkdf"
|
name = "hkdf"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
@ -1605,9 +1672,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
|
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@ -1615,6 +1682,7 @@ dependencies = [
|
|||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"hyper 1.6.0",
|
"hyper 1.6.0",
|
||||||
|
"libc",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -1624,17 +1692,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
|
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android_system_properties",
|
"android_system_properties",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"iana-time-zone-haiku",
|
"iana-time-zone-haiku",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core",
|
"windows-core 0.61.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1901,7 +1969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260"
|
checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jiff-static",
|
"jiff-static",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"portable-atomic-util",
|
"portable-atomic-util",
|
||||||
"serde",
|
"serde",
|
||||||
@ -2037,7 +2105,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
|
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lnvps"
|
name = "lnurl-rs"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41eacdd87b675792f7752f3dd0937a00241a504c3956c47f72986490662e1db4"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"anyhow",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bech32",
|
||||||
|
"bitcoin",
|
||||||
|
"cbc",
|
||||||
|
"email_address",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lnvps_api"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -2053,11 +2139,15 @@ dependencies = [
|
|||||||
"ipnetwork",
|
"ipnetwork",
|
||||||
"isocountry",
|
"isocountry",
|
||||||
"lettre",
|
"lettre",
|
||||||
|
"lnurl-rs",
|
||||||
|
"lnvps_common",
|
||||||
"lnvps_db",
|
"lnvps_db",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
|
"mustache",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
|
"quick-xml",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
@ -2073,9 +2163,17 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-tungstenite 0.21.0",
|
"tokio-tungstenite 0.21.0",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
"uuid",
|
||||||
"virt",
|
"virt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lnvps_common"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"rocket",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lnvps_db"
|
name = "lnvps_db"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -2087,6 +2185,23 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lnvps_nostr"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"config",
|
||||||
|
"env_logger",
|
||||||
|
"hex",
|
||||||
|
"lnvps_common",
|
||||||
|
"lnvps_db",
|
||||||
|
"log 0.4.27",
|
||||||
|
"rocket",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@ -2097,6 +2212,15 @@ dependencies = [
|
|||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
|
||||||
|
dependencies = [
|
||||||
|
"log 0.4.27",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.27"
|
version = "0.4.27"
|
||||||
@ -2206,6 +2330,16 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mustache"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51956ef1c5d20a1384524d91e616fb44dfc7d8f249bf696d49c97dd3289ecab5"
|
||||||
|
dependencies = [
|
||||||
|
"log 0.3.9",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@ -2213,7 +2347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"openssl",
|
"openssl",
|
||||||
"openssl-probe",
|
"openssl-probe",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
@ -2399,7 +2533,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a64853d7ab065474e87696f7601cee817d200e86c42e04004e005cb3e20c3c5"
|
checksum = "9a64853d7ab065474e87696f7601cee817d200e86c42e04004e005cb3e20c3c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -2407,9 +2541,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.1"
|
version = "1.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opaque-debug"
|
name = "opaque-debug"
|
||||||
@ -2824,7 +2958,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"heck",
|
"heck",
|
||||||
"itertools",
|
"itertools",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"multimap",
|
"multimap",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
@ -2867,6 +3001,16 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.37.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@ -3042,7 +3186,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"mime",
|
"mime",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -3104,7 +3248,7 @@ dependencies = [
|
|||||||
"figment",
|
"figment",
|
||||||
"futures",
|
"futures",
|
||||||
"indexmap 2.8.0",
|
"indexmap 2.8.0",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"memchr",
|
"memchr",
|
||||||
"multer",
|
"multer",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
@ -3156,7 +3300,7 @@ dependencies = [
|
|||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"hyper 0.14.32",
|
"hyper 0.14.32",
|
||||||
"indexmap 2.8.0",
|
"indexmap 2.8.0",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"memchr",
|
"memchr",
|
||||||
"pear",
|
"pear",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@ -3177,7 +3321,7 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "074297bec35db2fc7ebb6ade6a955b5566de66f83d9af5b5602a350a71bdef43"
|
checksum = "074297bec35db2fc7ebb6ade6a955b5566de66f83d9af5b5602a350a71bdef43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"okapi",
|
"okapi",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_okapi_codegen",
|
"rocket_okapi_codegen",
|
||||||
@ -3261,9 +3405,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.0.3"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
|
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"errno",
|
"errno",
|
||||||
@ -3278,7 +3422,7 @@ version = "0.21.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-webpki 0.101.7",
|
"rustls-webpki 0.101.7",
|
||||||
"sct",
|
"sct",
|
||||||
@ -3453,6 +3597,7 @@ version = "0.29.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitcoin_hashes 0.14.0",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"secp256k1-sys",
|
"secp256k1-sys",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3630,9 +3775,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
@ -3689,7 +3834,7 @@ dependencies = [
|
|||||||
"hashbrown 0.15.2",
|
"hashbrown 0.15.2",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
"indexmap 2.8.0",
|
"indexmap 2.8.0",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@ -3768,7 +3913,7 @@ dependencies = [
|
|||||||
"hkdf",
|
"hkdf",
|
||||||
"hmac",
|
"hmac",
|
||||||
"itoa",
|
"itoa",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -3808,7 +3953,7 @@ dependencies = [
|
|||||||
"hmac",
|
"hmac",
|
||||||
"home",
|
"home",
|
||||||
"itoa",
|
"itoa",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -3839,7 +3984,7 @@ dependencies = [
|
|||||||
"futures-intrusive",
|
"futures-intrusive",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@ -4263,7 +4408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
@ -4277,7 +4422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"rustls 0.23.25",
|
"rustls 0.23.25",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -4429,7 +4574,7 @@ version = "0.1.41"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
@ -4462,7 +4607,7 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
@ -4508,7 +4653,7 @@ dependencies = [
|
|||||||
"data-encoding",
|
"data-encoding",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"sha1",
|
"sha1",
|
||||||
@ -4527,7 +4672,7 @@ dependencies = [
|
|||||||
"data-encoding",
|
"data-encoding",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"rustls 0.23.25",
|
"rustls 0.23.25",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@ -4669,6 +4814,10 @@ name = "uuid"
|
|||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.2",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
@ -4691,8 +4840,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "virt"
|
name = "virt"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://gitlab.com/libvirt/libvirt-rust.git#70394aad4d9597c9ff87c0ada6711ed4f9528991"
|
||||||
checksum = "77a05f77c836efa9be343b5419663cf829d75203b813579993cdd9c44f51767e"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"uuid",
|
"uuid",
|
||||||
@ -4702,8 +4850,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "virt-sys"
|
name = "virt-sys"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://gitlab.com/libvirt/libvirt-rust.git#70394aad4d9597c9ff87c0ada6711ed4f9528991"
|
||||||
checksum = "c504e459878f09177f41bf2f8bb3e9a8af4fca7a09e73152fee02535d501601c"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
@ -4758,7 +4905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.100",
|
"syn 2.0.100",
|
||||||
@ -4876,7 +5023,7 @@ version = "0.52.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.52.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4889,6 +5036,41 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.61.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings 0.4.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.100",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.100",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -4902,7 +5084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-result",
|
"windows-result",
|
||||||
"windows-strings",
|
"windows-strings 0.3.1",
|
||||||
"windows-targets 0.53.0",
|
"windows-targets 0.53.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4924,6 +5106,15 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
72
Cargo.toml
72
Cargo.toml
@ -1,67 +1,19 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "lnvps"
|
resolver = "3"
|
||||||
version = "0.1.0"
|
members = [
|
||||||
edition = "2021"
|
"lnvps_db",
|
||||||
|
"lnvps_api",
|
||||||
|
"lnvps_nostr",
|
||||||
|
"lnvps_common"
|
||||||
|
]
|
||||||
|
|
||||||
[[bin]]
|
[workspace.dependencies]
|
||||||
name = "api"
|
tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["mikrotik", "nostr-dm", "nostr-dvm", "proxmox", "lnd", "cloudflare", "revolut", "bitvora"]
|
|
||||||
mikrotik = ["dep:reqwest"]
|
|
||||||
nostr-dm = ["dep:nostr-sdk"]
|
|
||||||
nostr-dvm = ["dep:nostr-sdk"]
|
|
||||||
proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"]
|
|
||||||
libvirt = ["dep:virt"]
|
|
||||||
lnd = ["dep:fedimint-tonic-lnd"]
|
|
||||||
bitvora = ["dep:reqwest", "dep:tokio-stream"]
|
|
||||||
cloudflare = ["dep:reqwest"]
|
|
||||||
revolut = ["dep:reqwest", "dep:sha2", "dep:hmac"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
lnvps_db = { path = "lnvps_db" }
|
|
||||||
tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros", "sync", "io-util"] }
|
|
||||||
anyhow = "1.0.83"
|
anyhow = "1.0.83"
|
||||||
config = { version = "0.15.8", features = ["yaml"] }
|
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
|
env_logger = "0.11.7"
|
||||||
serde = { version = "1.0.213", features = ["derive"] }
|
serde = { version = "1.0.213", features = ["derive"] }
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.132"
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
rocket_okapi = { version = "0.9.0", features = ["swagger"] }
|
config = { version = "0.15.8", features = ["yaml"] }
|
||||||
schemars = { version = "0.8.22", features = ["chrono"] }
|
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
|
||||||
base64 = { version = "0.22.1", features = ["alloc"] }
|
|
||||||
urlencoding = "2.1.3"
|
|
||||||
ipnetwork = { git = "https://git.v0l.io/Kieran/ipnetwork.git", rev = "35977adc8103cfc232bc95fbc32f4e34f2b6a6d7" }
|
|
||||||
rand = "0.9.0"
|
|
||||||
clap = { version = "4.5.21", features = ["derive"] }
|
|
||||||
ssh-key = "0.6.7"
|
|
||||||
lettre = { version = "0.11.10", features = ["tokio1-native-tls"] }
|
|
||||||
ws = { package = "rocket_ws", version = "0.1.1" }
|
|
||||||
native-tls = "0.2.12"
|
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
futures = "0.3.31"
|
|
||||||
isocountry = "0.3.2"
|
|
||||||
|
|
||||||
#nostr-dm
|
|
||||||
nostr = { version = "0.40.0", default-features = false, features = ["std"] }
|
|
||||||
nostr-sdk = { version = "0.40.0", optional = true, default-features = false, features = ["nip44", "nip59"] }
|
|
||||||
|
|
||||||
#proxmox
|
|
||||||
tokio-tungstenite = { version = "^0.21", features = ["native-tls"], optional = true }
|
|
||||||
ssh2 = { version = "0.9.4", optional = true }
|
|
||||||
reqwest = { version = "0.12.8", optional = true }
|
|
||||||
|
|
||||||
#libvirt
|
|
||||||
virt = { version = "0.4.2", optional = true }
|
|
||||||
|
|
||||||
#lnd
|
|
||||||
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"], optional = true }
|
|
||||||
|
|
||||||
#bitvora
|
|
||||||
tokio-stream = { version = "0.1.17", features = ["sync"], optional = true }
|
|
||||||
|
|
||||||
#revolut
|
|
||||||
sha2 = { version = "0.10.8", optional = true }
|
|
||||||
hmac = { version = "0.12.1", optional = true }
|
|
||||||
env_logger = "0.11.7"
|
|
||||||
|
@ -3,10 +3,12 @@ ARG IMAGE=rust:bookworm
|
|||||||
FROM $IMAGE AS build
|
FROM $IMAGE AS build
|
||||||
WORKDIR /app/src
|
WORKDIR /app/src
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN apt update && apt -y install protobuf-compiler
|
RUN apt update && apt -y install protobuf-compiler libvirt-dev
|
||||||
RUN cargo test && cargo install --path . --root /app/build
|
RUN cargo test \
|
||||||
|
&& cargo install --root /app/build --path lnvps_api \
|
||||||
|
&& cargo install --root /app/build --path lnvps_nostr
|
||||||
|
|
||||||
FROM $IMAGE AS runner
|
FROM $IMAGE AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/build .
|
COPY --from=build /app/build .
|
||||||
ENTRYPOINT ["./bin/api"]
|
ENTRYPOINT ["./bin/lnvps_api"]
|
13
README.md
13
README.md
@ -13,6 +13,7 @@ A bitcoin powered VPS system.
|
|||||||
- [RevolutPay](https://www.revolut.com/business/revolut-pay/)
|
- [RevolutPay](https://www.revolut.com/business/revolut-pay/)
|
||||||
- VM Backend:
|
- VM Backend:
|
||||||
- Proxmox
|
- Proxmox
|
||||||
|
- LibVirt (WIP)
|
||||||
- Network Resources:
|
- Network Resources:
|
||||||
- Mikrotik JSON-API
|
- Mikrotik JSON-API
|
||||||
- OVH API (dedicated server virtual mac)
|
- OVH API (dedicated server virtual mac)
|
||||||
@ -42,10 +43,18 @@ delete-after: 3
|
|||||||
read-only: false
|
read-only: false
|
||||||
|
|
||||||
# Provisioner is the main process which handles creating/deleting VM's
|
# Provisioner is the main process which handles creating/deleting VM's
|
||||||
# Currently supports: Proxmox
|
|
||||||
provisioner:
|
provisioner:
|
||||||
proxmox:
|
proxmox:
|
||||||
# Proxmox (QEMU) settings used for spawning VM's
|
# QEMU settings used for spawning VM's
|
||||||
|
qemu:
|
||||||
|
bios: "ovmf"
|
||||||
|
machine: "q35"
|
||||||
|
os-type: "l26"
|
||||||
|
bridge: "vmbr0"
|
||||||
|
cpu: "kvm64"
|
||||||
|
kvm: false
|
||||||
|
libvirt:
|
||||||
|
# QEMU settings used for spawning VM's
|
||||||
qemu:
|
qemu:
|
||||||
bios: "ovmf"
|
bios: "ovmf"
|
||||||
machine: "q35"
|
machine: "q35"
|
||||||
|
861
grafana.json
Normal file
861
grafana.json
Normal file
@ -0,0 +1,861 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 1,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "mysql",
|
||||||
|
"uid": "cegjfe9u9181sf"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"filterable": false,
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "cpu"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 62
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "gb_memory"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 88
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "gb_ssd"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 81
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "gb_hdd"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 75
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "load_factor"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 93
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "sold_cpu"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 87
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "sold_gb_memory"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 133
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "sold_gb_ssd_disk"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 141
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "name"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 205
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "vms"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 63
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.5.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"dataset": "lnvps",
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT \nh.name,\nh.cpu,\nh.memory / 1024 / 1024 / 1024 gb_memory,\n(select count(*) from vm where vm.host_id = h.id and vm.expires > current_timestamp and vm.deleted = 0) as vms,\n(select sum(size) from vm_host_disk hd where hd.host_id = h.id and hd.enabled = 1 and hd.kind = 1) / 1024 / 1024 / 1024 gb_ssd,\n(select sum(size) from vm_host_disk hd where hd.host_id = h.id and hd.enabled = 1 and hd.kind = 0) / 1024 / 1024 / 1024 gb_hdd,\n(select sum(case when v.template_id is null then (select cpu from vm_custom_template vct where vct.id = v.custom_template_id) else (select cpu from vm_template vt where vt.id = v.template_id) end) from vm v where v.host_id = h.id and expires > current_timestamp()) sold_cpu,\n(select sum(case when v.template_id is null then (select memory from vm_custom_template vct where vct.id = v.custom_template_id) else (select memory from vm_template vt where vt.id = v.template_id) end) from vm v where v.host_id = h.id and expires > current_timestamp()) / 1024 / 1024 / 1024 sold_gb_memory,\n(select sum(case when v.template_id is null then (select disk_size from vm_custom_template vct where vct.id = v.custom_template_id and vct.disk_type = 1) else (select disk_size from vm_template vt where vt.id = v.template_id and vt.disk_type = 1) end) from vm v where v.host_id = h.id and expires > current_timestamp()) / 1024 / 1024 / 1024 sold_gb_ssd_disk,\n(select sum(case when v.template_id is null then (select disk_size from vm_custom_template vct where vct.id = v.custom_template_id and vct.disk_type = 0) else (select disk_size from vm_template vt where vt.id = v.template_id and vt.disk_type = 0) end) from vm v where v.host_id = h.id and expires > current_timestamp()) / 1024 / 1024 / 1024 sold_gb_hdd_disk,\n(select sum(case when v.template_id is null then 0 else (select (case when cp.currency = 'BTC' then (cp.amount/1e9) else cp.amount end) from vm_template vt,vm_cost_plan cp where vt.id = v.template_id and vt.cost_plan_id = cp.id) end) from vm v where v.host_id = h.id and deleted = 0) income\nfrom vm_host h",
|
||||||
|
"refId": "A",
|
||||||
|
"sql": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"parameters": [],
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupBy": [
|
||||||
|
{
|
||||||
|
"property": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "groupBy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limit": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Host Allocation",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "mysql",
|
||||||
|
"uid": "cegjfe9u9181sf"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 13,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.5.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"dataset": "lnvps",
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "select\nv.id,\ndatediff(current_timestamp(), v.created) age,\ndatediff(v.expires, current_timestamp()) days_to_expire,\n(select sum((case when currency = 'BTC' then amount / 1e11 else amount end) * rate) from lnvps.vm_payment where vm_id = v.id and is_paid = 1) total_payments\nfrom vm v\nwhere v.deleted = 0\norder by 3 asc",
|
||||||
|
"refId": "A",
|
||||||
|
"sql": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"parameters": [],
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupBy": [
|
||||||
|
{
|
||||||
|
"property": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "groupBy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limit": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Renewals",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "mysql",
|
||||||
|
"uid": "cegjfe9u9181sf"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "id"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 49
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "ref_code"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 91
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "created"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "amount"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 71
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "currency"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 78
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
|
"id": 4,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.5.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"dataset": "lnvps",
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "select v.id, \nv.ref_code, v.created, \n(case when vp.currency = 'BTC' then vp.amount / 1000 else vp.amount / 100 end) amount,\nvp.currency,\n(case when vp.currency = 'BTC' then vp.amount / 1000 else vp.amount / 100 end) * 0.33 comission\nfrom vm v, vm_payment vp\nwhere v.ref_code is not null\nand v.id = vp.vm_id\nand vp.is_paid = 1\norder by vp.created desc",
|
||||||
|
"refId": "A",
|
||||||
|
"sql": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"parameters": [],
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupBy": [
|
||||||
|
{
|
||||||
|
"property": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "groupBy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limit": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "RefCodes",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "mysql",
|
||||||
|
"uid": "cegjfe9u9181sf"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 13
|
||||||
|
},
|
||||||
|
"id": 5,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.5.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"dataset": "lnvps",
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "select vm_id, created, \n(case when currency = 'BTC' then (amount / 1e3) else amount / 100 end) amount, \n(case when currency = 'BTC' then (amount / 1e11) * rate else amount * rate end) amount_eur,\ncurrency,\n(case when payment_method = 0 then 'LN' else 'Revolut' end) method\nfrom vm_payment\nwhere is_paid = 1\norder by created desc\nlimit 20",
|
||||||
|
"refId": "A",
|
||||||
|
"sql": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"parameters": [],
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupBy": [
|
||||||
|
{
|
||||||
|
"property": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "groupBy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limit": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Payments",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "mysql",
|
||||||
|
"uid": "cegjfe9u9181sf"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "free"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "percentunit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "region"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 70
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "used"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 59
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "size"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 70
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "size"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "sishort"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 18
|
||||||
|
},
|
||||||
|
"id": 1,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.5.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"dataset": "lnvps",
|
||||||
|
"datasource": {
|
||||||
|
"type": "mysql",
|
||||||
|
"uid": "cegjfe9u9181sf"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "select i.cidr, i.region, i.used, i.size, (1-i.used/i.size) as free\nfrom (\nselect r.cidr, \n(select count(id) from lnvps.vm_ip_assignment where ip_range_id = r.id and deleted = 0) used,\nhr.name as region,\npow(2, (case when r.cidr like '%:%' then 128 else 32 end)-substring_index(r.cidr, '/', -1)) as size\nfrom ip_range r, vm_host_region hr\nwhere r.region_id = hr.id) i",
|
||||||
|
"refId": "A",
|
||||||
|
"sql": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"parameters": [],
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupBy": [
|
||||||
|
{
|
||||||
|
"property": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "groupBy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limit": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "IP Ranges",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "mysql",
|
||||||
|
"uid": "cegjfe9u9181sf"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"lineWidth": 1,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "amount_sats"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "locale"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "amount_eur"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "currencyEUR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 23
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"options": {
|
||||||
|
"barRadius": 0,
|
||||||
|
"barWidth": 0.97,
|
||||||
|
"fullHighlight": false,
|
||||||
|
"groupWidth": 0.7,
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"orientation": "auto",
|
||||||
|
"showValue": "auto",
|
||||||
|
"stacking": "none",
|
||||||
|
"tooltip": {
|
||||||
|
"hideZeros": false,
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
},
|
||||||
|
"xTickLabelRotation": 0,
|
||||||
|
"xTickLabelSpacing": 0
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.5.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"dataset": "lnvps",
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "select DATE_FORMAT(created, '%Y-%m') as month,\nsum((case when currency = 'BTC' then (amount / 1e3) else amount / 100 end)) amount_sats, \nsum((case when currency = 'BTC' then (amount / 1e11) * rate else amount * rate end)) amount_eur\nfrom vm_payment\nwhere is_paid = 1\ngroup by DATE_FORMAT(created, '%Y-%m')\norder by created asc\n",
|
||||||
|
"refId": "A",
|
||||||
|
"sql": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"parameters": [],
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupBy": [
|
||||||
|
{
|
||||||
|
"property": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "groupBy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limit": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Income",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "convertFieldType",
|
||||||
|
"options": {
|
||||||
|
"conversions": [
|
||||||
|
{
|
||||||
|
"destinationType": "time",
|
||||||
|
"targetField": "month"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "barchart"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"preload": false,
|
||||||
|
"refresh": "",
|
||||||
|
"schemaVersion": 40,
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "LNVPS",
|
||||||
|
"uid": "begjfxfrjwu80e",
|
||||||
|
"version": 26,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
5314
lnvps_api/Cargo.lock
generated
Normal file
5314
lnvps_api/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
88
lnvps_api/Cargo.toml
Normal file
88
lnvps_api/Cargo.toml
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
[package]
|
||||||
|
name = "lnvps_api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "lnvps_api"
|
||||||
|
path = "src/bin/api.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = [
|
||||||
|
"mikrotik",
|
||||||
|
"nostr-dm",
|
||||||
|
"nostr-dvm",
|
||||||
|
"nostr-domain",
|
||||||
|
"proxmox",
|
||||||
|
"lnd",
|
||||||
|
"cloudflare",
|
||||||
|
"revolut",
|
||||||
|
"bitvora",
|
||||||
|
"tokio/sync",
|
||||||
|
"tokio/io-util"
|
||||||
|
]
|
||||||
|
mikrotik = ["dep:reqwest"]
|
||||||
|
nostr-dm = ["dep:nostr-sdk"]
|
||||||
|
nostr-dvm = ["dep:nostr-sdk"]
|
||||||
|
nostr-domain = ["lnvps_db/nostr-domain"]
|
||||||
|
proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"]
|
||||||
|
libvirt = ["dep:virt", "dep:uuid", "dep:quick-xml"]
|
||||||
|
lnd = ["dep:fedimint-tonic-lnd"]
|
||||||
|
bitvora = ["dep:reqwest", "dep:tokio-stream"]
|
||||||
|
cloudflare = ["dep:reqwest"]
|
||||||
|
revolut = ["dep:reqwest", "dep:sha2", "dep:hmac"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lnvps_db = { path = "../lnvps_db" }
|
||||||
|
lnvps_common = { path = "../lnvps_common" }
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
config.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
rocket.workspace = true
|
||||||
|
hex.workspace = true
|
||||||
|
rocket_okapi = { version = "0.9.0", features = ["swagger"] }
|
||||||
|
schemars = { version = "0.8.22", features = ["chrono"] }
|
||||||
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
base64 = { version = "0.22.1", features = ["alloc"] }
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
ipnetwork = { git = "https://git.v0l.io/Kieran/ipnetwork.git", rev = "35977adc8103cfc232bc95fbc32f4e34f2b6a6d7" }
|
||||||
|
rand = "0.9.0"
|
||||||
|
clap = { version = "4.5.21", features = ["derive"] }
|
||||||
|
ssh-key = "0.6.7"
|
||||||
|
lettre = { version = "0.11.10", features = ["tokio1-native-tls"] }
|
||||||
|
ws = { package = "rocket_ws", version = "0.1.1" }
|
||||||
|
native-tls = "0.2.12"
|
||||||
|
lnurl-rs = { version = "0.9.0", default-features = false }
|
||||||
|
mustache = "0.9.0"
|
||||||
|
|
||||||
|
futures = "0.3.31"
|
||||||
|
isocountry = "0.3.2"
|
||||||
|
|
||||||
|
#nostr-dm
|
||||||
|
nostr = { version = "0.40.0", default-features = false, features = ["std"] }
|
||||||
|
nostr-sdk = { version = "0.40.0", optional = true, default-features = false, features = ["nip44", "nip59"] }
|
||||||
|
|
||||||
|
#proxmox
|
||||||
|
tokio-tungstenite = { version = "^0.21", features = ["native-tls"], optional = true }
|
||||||
|
ssh2 = { version = "0.9.4", optional = true }
|
||||||
|
reqwest = { version = "0.12.8", optional = true }
|
||||||
|
|
||||||
|
#libvirt
|
||||||
|
virt = { git = "https://gitlab.com/libvirt/libvirt-rust.git", optional = true }
|
||||||
|
#virtxml = {git = "https://gitlab.com/libvirt/libvirt-rust-xml.git", optional = true}
|
||||||
|
uuid = { version = "1.16.0", features = ["v4", "serde"], optional = true }
|
||||||
|
quick-xml = { version = "0.37.3", features = ["serde", "serialize"], optional = true }
|
||||||
|
|
||||||
|
#lnd
|
||||||
|
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"], optional = true }
|
||||||
|
|
||||||
|
#bitvora
|
||||||
|
tokio-stream = { version = "0.1.17", features = ["sync"], optional = true }
|
||||||
|
|
||||||
|
#revolut
|
||||||
|
sha2 = { version = "0.10.8", optional = true }
|
||||||
|
hmac = { version = "0.12.1", optional = true }
|
161
lnvps_api/invoice.html
Normal file
161
lnvps_api/invoice.html
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{payment.id}}</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen {
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 0.4em 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
text-align: end;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.5em 0.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="header">
|
||||||
|
LNVPS
|
||||||
|
<img height="48" width="48" src="https://lnvps.net/logo.jpg" alt="logo"/>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<h2>Invoice</h2>
|
||||||
|
<div class="flex-col">
|
||||||
|
<div>
|
||||||
|
<b>ID:</b>
|
||||||
|
{{payment.id}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Date:</b>
|
||||||
|
{{payment.created}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Status:</b>
|
||||||
|
{{#payment.is_paid}}Paid{{/payment.is_paid}}
|
||||||
|
{{^payment.is_paid}}Unpaid{{/payment.is_paid}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Nostr Pubkey:</b>
|
||||||
|
{{npub}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="billing">
|
||||||
|
<div class="flex-col">
|
||||||
|
<h2>Bill To:</h2>
|
||||||
|
<div>{{user.name}}</div>
|
||||||
|
<div>{{user.address_1}}</div>
|
||||||
|
<div>{{user.address_2}}</div>
|
||||||
|
<div>{{user.city}}</div>
|
||||||
|
<div>{{user.state}}</div>
|
||||||
|
<div>{{user.postcode}}</div>
|
||||||
|
<div>{{user.country}}</div>
|
||||||
|
<div>{{user.country_code}}</div>
|
||||||
|
<div>{{user.tax_id}}</div>
|
||||||
|
</div>
|
||||||
|
{{#company}}
|
||||||
|
<div class="flex-col">
|
||||||
|
<h2> </h2>
|
||||||
|
<div>{{company.name}}</div>
|
||||||
|
<div>{{company.address_1}}</div>
|
||||||
|
<div>{{company.address_2}}</div>
|
||||||
|
<div>{{company.city}}</div>
|
||||||
|
<div>{{company.state}}</div>
|
||||||
|
<div>{{company.postcode}}</div>
|
||||||
|
<div>{{company.country}}</div>
|
||||||
|
<div>{{company.country_code}}</div>
|
||||||
|
<div>{{company.tax_id}}</div>
|
||||||
|
</div>
|
||||||
|
{{/company}}
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<h2>Details:</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Currency</th>
|
||||||
|
<th>Gross</th>
|
||||||
|
<th>Taxes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
VM Renewal #{{vm.id}}
|
||||||
|
- {{vm.template.name}}
|
||||||
|
- {{vm.image.distribution}} {{vm.image.version}}
|
||||||
|
- {{payment.time}} seconds
|
||||||
|
</td>
|
||||||
|
<td>{{payment.currency}}</td>
|
||||||
|
<td>{{payment.amount}}</td>
|
||||||
|
<td>{{payment.tax}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="total">
|
||||||
|
Total: {{total}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br/>
|
||||||
|
<b>
|
||||||
|
All BTC amounts are in milli-satoshis and all fiat amounts are in cents.
|
||||||
|
</b>
|
||||||
|
<hr/>
|
||||||
|
<small>
|
||||||
|
(c) {{year}} LNVPS.net - Generated at {{current_date}}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,6 +1,8 @@
|
|||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
|
|
||||||
mod model;
|
mod model;
|
||||||
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
mod nostr_domain;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod webhook;
|
mod webhook;
|
||||||
|
|
@ -15,7 +15,7 @@ use std::collections::HashMap;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiVmStatus {
|
pub struct ApiVmStatus {
|
||||||
/// Unique VM ID (Same in proxmox)
|
/// Unique VM ID (Same in proxmox)
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
@ -37,7 +37,7 @@ pub struct ApiVmStatus {
|
|||||||
pub status: VmState,
|
pub status: VmState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiUserSshKey {
|
pub struct ApiUserSshKey {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -54,7 +54,7 @@ impl From<lnvps_db::UserSshKey> for ApiUserSshKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiVmIpAssignment {
|
pub struct ApiVmIpAssignment {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub ip: String,
|
pub ip: String,
|
||||||
@ -80,7 +80,7 @@ impl ApiVmIpAssignment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum DiskType {
|
pub enum DiskType {
|
||||||
HDD = 0,
|
HDD = 0,
|
||||||
@ -105,7 +105,7 @@ impl From<DiskType> for lnvps_db::DiskType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum DiskInterface {
|
pub enum DiskInterface {
|
||||||
SATA = 0,
|
SATA = 0,
|
||||||
@ -133,7 +133,7 @@ impl From<DiskInterface> for lnvps_db::DiskInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiTemplatesResponse {
|
pub struct ApiTemplatesResponse {
|
||||||
pub templates: Vec<ApiVmTemplate>,
|
pub templates: Vec<ApiVmTemplate>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@ -149,7 +149,7 @@ impl ApiTemplatesResponse {
|
|||||||
CurrencyAmount::from_f32(template.cost_plan.currency, template.cost_plan.amount);
|
CurrencyAmount::from_f32(template.cost_plan.currency, template.cost_plan.amount);
|
||||||
for alt_price in alt_prices(&rates, list_price) {
|
for alt_price in alt_prices(&rates, list_price) {
|
||||||
template.cost_plan.other_price.push(ApiPrice {
|
template.cost_plan.other_price.push(ApiPrice {
|
||||||
currency: alt_price.0,
|
currency: alt_price.currency(),
|
||||||
amount: alt_price.value_f32(),
|
amount: alt_price.value_f32(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -157,7 +157,7 @@ impl ApiTemplatesResponse {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiCustomTemplateParams {
|
pub struct ApiCustomTemplateParams {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -166,8 +166,6 @@ pub struct ApiCustomTemplateParams {
|
|||||||
pub min_cpu: u16,
|
pub min_cpu: u16,
|
||||||
pub min_memory: u64,
|
pub min_memory: u64,
|
||||||
pub max_memory: u64,
|
pub max_memory: u64,
|
||||||
pub min_disk: u64,
|
|
||||||
pub max_disk: u64,
|
|
||||||
pub disks: Vec<ApiCustomTemplateDiskParam>,
|
pub disks: Vec<ApiCustomTemplateDiskParam>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +176,7 @@ impl ApiCustomTemplateParams {
|
|||||||
region: &VmHostRegion,
|
region: &VmHostRegion,
|
||||||
max_cpu: u16,
|
max_cpu: u16,
|
||||||
max_memory: u64,
|
max_memory: u64,
|
||||||
max_disk: u64,
|
max_disk: &HashMap<(DiskType, DiskInterface), u64>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
const GB: u64 = 1024 * 1024 * 1024;
|
const GB: u64 = 1024 * 1024 * 1024;
|
||||||
Ok(ApiCustomTemplateParams {
|
Ok(ApiCustomTemplateParams {
|
||||||
@ -192,21 +190,25 @@ impl ApiCustomTemplateParams {
|
|||||||
min_cpu: 1,
|
min_cpu: 1,
|
||||||
min_memory: GB,
|
min_memory: GB,
|
||||||
max_memory,
|
max_memory,
|
||||||
min_disk: GB * 5,
|
|
||||||
max_disk,
|
|
||||||
disks: disks
|
disks: disks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|d| d.pricing_id == pricing.id)
|
.filter(|d| d.pricing_id == pricing.id)
|
||||||
.map(|d| ApiCustomTemplateDiskParam {
|
.filter_map(|d| {
|
||||||
|
Some(ApiCustomTemplateDiskParam {
|
||||||
|
min_disk: GB * 5,
|
||||||
|
max_disk: *max_disk.get(&(d.kind.into(), d.interface.into()))?,
|
||||||
disk_type: d.kind.into(),
|
disk_type: d.kind.into(),
|
||||||
disk_interface: d.interface.into(),
|
disk_interface: d.interface.into(),
|
||||||
})
|
})
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct ApiCustomTemplateDiskParam {
|
pub struct ApiCustomTemplateDiskParam {
|
||||||
|
pub min_disk: u64,
|
||||||
|
pub max_disk: u64,
|
||||||
pub disk_type: DiskType,
|
pub disk_type: DiskType,
|
||||||
pub disk_interface: DiskInterface,
|
pub disk_interface: DiskInterface,
|
||||||
}
|
}
|
||||||
@ -253,7 +255,7 @@ pub struct ApiPrice {
|
|||||||
impl From<CurrencyAmount> for ApiPrice {
|
impl From<CurrencyAmount> for ApiPrice {
|
||||||
fn from(value: CurrencyAmount) -> Self {
|
fn from(value: CurrencyAmount) -> Self {
|
||||||
Self {
|
Self {
|
||||||
currency: value.0,
|
currency: value.currency(),
|
||||||
amount: value.value_f32(),
|
amount: value.value_f32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -401,10 +403,44 @@ pub struct VMPatchRequest {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct AccountPatchRequest {
|
pub struct AccountPatchRequest {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub contact_nip17: bool,
|
pub contact_nip17: bool,
|
||||||
pub contact_email: bool,
|
pub contact_email: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub country_code: Option<String>,
|
pub country_code: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub address_1: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub address_2: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub state: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub city: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub postcode: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tax_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lnvps_db::User> for AccountPatchRequest {
|
||||||
|
fn from(user: lnvps_db::User) -> Self {
|
||||||
|
AccountPatchRequest {
|
||||||
|
email: user.email,
|
||||||
|
contact_nip17: user.contact_nip17,
|
||||||
|
contact_email: user.contact_email,
|
||||||
|
country_code: user.country_code,
|
||||||
|
name: user.billing_name,
|
||||||
|
address_1: user.billing_address_1,
|
||||||
|
address_2: user.billing_address_2,
|
||||||
|
state: user.billing_state,
|
||||||
|
city: user.billing_city,
|
||||||
|
postcode: user.billing_postcode,
|
||||||
|
tax_id: user.billing_tax_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
@ -455,6 +491,7 @@ pub struct ApiVmOsImage {
|
|||||||
pub flavour: String,
|
pub flavour: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub release_date: DateTime<Utc>,
|
pub release_date: DateTime<Utc>,
|
||||||
|
pub default_username: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<lnvps_db::VmOsImage> for ApiVmOsImage {
|
impl From<lnvps_db::VmOsImage> for ApiVmOsImage {
|
||||||
@ -465,6 +502,7 @@ impl From<lnvps_db::VmOsImage> for ApiVmOsImage {
|
|||||||
flavour: image.flavour,
|
flavour: image.flavour,
|
||||||
version: image.version,
|
version: image.version,
|
||||||
release_date: image.release_date,
|
release_date: image.release_date,
|
||||||
|
default_username: image.default_username,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -480,6 +518,7 @@ pub struct ApiVmPayment {
|
|||||||
pub currency: String,
|
pub currency: String,
|
||||||
pub is_paid: bool,
|
pub is_paid: bool,
|
||||||
pub data: ApiPaymentData,
|
pub data: ApiPaymentData,
|
||||||
|
pub time: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<lnvps_db::VmPayment> for ApiVmPayment {
|
impl From<lnvps_db::VmPayment> for ApiVmPayment {
|
||||||
@ -493,6 +532,7 @@ impl From<lnvps_db::VmPayment> for ApiVmPayment {
|
|||||||
tax: value.tax,
|
tax: value.tax,
|
||||||
currency: value.currency,
|
currency: value.currency,
|
||||||
is_paid: value.is_paid,
|
is_paid: value.is_paid,
|
||||||
|
time: value.time_value,
|
||||||
data: match &value.payment_method {
|
data: match &value.payment_method {
|
||||||
PaymentMethod::Lightning => ApiPaymentData::Lightning(value.external_data),
|
PaymentMethod::Lightning => ApiPaymentData::Lightning(value.external_data),
|
||||||
PaymentMethod::Revolut => {
|
PaymentMethod::Revolut => {
|
||||||
@ -552,3 +592,45 @@ impl From<PaymentMethod> for ApiPaymentMethod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct ApiCompany {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub email: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub country_code: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub address_1: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub address_2: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub state: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub city: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub postcode: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tax_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub phone: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lnvps_db::Company> for ApiCompany {
|
||||||
|
fn from(value: lnvps_db::Company) -> Self {
|
||||||
|
Self {
|
||||||
|
email: value.email,
|
||||||
|
country_code: value.country_code,
|
||||||
|
name: value.name,
|
||||||
|
id: value.id,
|
||||||
|
address_1: value.address_1,
|
||||||
|
address_2: value.address_2,
|
||||||
|
state: value.state,
|
||||||
|
city: value.city,
|
||||||
|
postcode: value.postcode,
|
||||||
|
tax_id: value.tax_id,
|
||||||
|
phone: value.phone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
202
lnvps_api/src/api/nostr_domain.rs
Normal file
202
lnvps_api/src/api/nostr_domain.rs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
use crate::api::routes::{ApiData, ApiResult};
|
||||||
|
use crate::nip98::Nip98Auth;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use lnvps_db::{LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
|
use rocket::{delete, get, post, routes, Route, State};
|
||||||
|
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||||
|
use rocket_okapi::settings::OpenApiSettings;
|
||||||
|
use rocket_okapi::{openapi, openapi_get_routes, openapi_routes, JsonSchema};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![
|
||||||
|
v1_nostr_domains,
|
||||||
|
v1_create_nostr_domain,
|
||||||
|
v1_list_nostr_domain_handles,
|
||||||
|
v1_create_nostr_domain_handle,
|
||||||
|
v1_delete_nostr_domain_handle
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[get("/api/v1/nostr/domain")]
|
||||||
|
async fn v1_nostr_domains(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
settings: &State<Settings>,
|
||||||
|
) -> ApiResult<ApiDomainsResponse> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let domains = db.list_domains(uid).await?;
|
||||||
|
ApiData::ok(ApiDomainsResponse {
|
||||||
|
domains: domains.into_iter().map(|d| d.into()).collect(),
|
||||||
|
cname: settings.nostr_address_host.clone().unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[post("/api/v1/nostr/domain", format = "json", data = "<data>")]
|
||||||
|
async fn v1_create_nostr_domain(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
data: Json<NameRequest>,
|
||||||
|
) -> ApiResult<ApiNostrDomain> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let mut dom = NostrDomain {
|
||||||
|
owner_id: uid,
|
||||||
|
name: data.name.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let dom_id = db.insert_domain(&dom).await?;
|
||||||
|
dom.id = dom_id;
|
||||||
|
|
||||||
|
ApiData::ok(dom.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[get("/api/v1/nostr/domain/<dom>/handle")]
|
||||||
|
async fn v1_list_nostr_domain_handles(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
dom: u64,
|
||||||
|
) -> ApiResult<Vec<ApiNostrDomainHandle>> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let domain = db.get_domain(dom).await?;
|
||||||
|
if domain.owner_id != uid {
|
||||||
|
return ApiData::err("Access denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
let handles = db.list_handles(domain.id).await?;
|
||||||
|
ApiData::ok(handles.into_iter().map(|h| h.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[post("/api/v1/nostr/domain/<dom>/handle", format = "json", data = "<data>")]
|
||||||
|
async fn v1_create_nostr_domain_handle(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
dom: u64,
|
||||||
|
data: Json<HandleRequest>,
|
||||||
|
) -> ApiResult<ApiNostrDomainHandle> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let domain = db.get_domain(dom).await?;
|
||||||
|
if domain.owner_id != uid {
|
||||||
|
return ApiData::err("Access denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
let h_pubkey = hex::decode(&data.pubkey)?;
|
||||||
|
if h_pubkey.len() != 32 {
|
||||||
|
return ApiData::err("Invalid public key");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut handle = NostrDomainHandle {
|
||||||
|
domain_id: domain.id,
|
||||||
|
handle: data.name.clone(),
|
||||||
|
pubkey: h_pubkey,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let id = db.insert_handle(&handle).await?;
|
||||||
|
handle.id = id;
|
||||||
|
|
||||||
|
ApiData::ok(handle.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[delete("/api/v1/nostr/domain/<dom>/handle/<handle>")]
|
||||||
|
async fn v1_delete_nostr_domain_handle(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
dom: u64,
|
||||||
|
handle: u64,
|
||||||
|
) -> ApiResult<()> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let domain = db.get_domain(dom).await?;
|
||||||
|
if domain.owner_id != uid {
|
||||||
|
return ApiData::err("Access denied");
|
||||||
|
}
|
||||||
|
db.delete_handle(handle).await?;
|
||||||
|
ApiData::ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, JsonSchema)]
|
||||||
|
struct NameRequest {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, JsonSchema)]
|
||||||
|
struct HandleRequest {
|
||||||
|
pub pubkey: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, JsonSchema)]
|
||||||
|
struct ApiNostrDomain {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub handles: u64,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub relays: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NostrDomain> for ApiNostrDomain {
|
||||||
|
fn from(value: NostrDomain) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
enabled: value.enabled,
|
||||||
|
handles: value.handles as u64,
|
||||||
|
created: value.created,
|
||||||
|
relays: if let Some(r) = value.relays {
|
||||||
|
r.split(',').map(|s| s.to_string()).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, JsonSchema)]
|
||||||
|
struct ApiNostrDomainHandle {
|
||||||
|
pub id: u64,
|
||||||
|
pub domain_id: u64,
|
||||||
|
pub handle: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub pubkey: String,
|
||||||
|
pub relays: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NostrDomainHandle> for ApiNostrDomainHandle {
|
||||||
|
fn from(value: NostrDomainHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
domain_id: value.domain_id,
|
||||||
|
created: value.created,
|
||||||
|
handle: value.handle,
|
||||||
|
pubkey: hex::encode(value.pubkey),
|
||||||
|
relays: if let Some(r) = value.relays {
|
||||||
|
r.split(',').map(|s| s.to_string()).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, JsonSchema)]
|
||||||
|
struct ApiDomainsResponse {
|
||||||
|
pub domains: Vec<ApiNostrDomain>,
|
||||||
|
pub cname: String,
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
use crate::api::model::{
|
use crate::api::model::{
|
||||||
AccountPatchRequest, ApiCustomTemplateParams, ApiCustomVmOrder, ApiCustomVmRequest,
|
AccountPatchRequest, ApiCompany, ApiCustomTemplateParams, ApiCustomVmOrder, ApiCustomVmRequest,
|
||||||
ApiPaymentInfo, ApiPaymentMethod, ApiPrice, ApiTemplatesResponse, ApiUserSshKey,
|
ApiPaymentInfo, ApiPaymentMethod, ApiPrice, ApiTemplatesResponse, ApiUserSshKey,
|
||||||
ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus, ApiVmTemplate, CreateSshKey,
|
ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus, ApiVmTemplate, CreateSshKey,
|
||||||
CreateVmRequest, VMPatchRequest,
|
CreateVmRequest, VMPatchRequest,
|
||||||
};
|
};
|
||||||
use crate::exchange::{Currency, ExchangeRateService};
|
use crate::exchange::{Currency, CurrencyAmount, ExchangeRateService};
|
||||||
use crate::host::{get_host_client, FullVmInfo, TimeSeries, TimeSeriesData};
|
use crate::host::{get_host_client, FullVmInfo, TimeSeries, TimeSeriesData};
|
||||||
use crate::nip98::Nip98Auth;
|
use crate::nip98::Nip98Auth;
|
||||||
use crate::provisioner::{HostCapacityService, LNVpsProvisioner, PricingEngine};
|
use crate::provisioner::{HostCapacityService, LNVpsProvisioner, PricingEngine};
|
||||||
@ -12,25 +12,31 @@ use crate::settings::Settings;
|
|||||||
use crate::status::{VmState, VmStateCache};
|
use crate::status::{VmState, VmStateCache};
|
||||||
use crate::worker::WorkJob;
|
use crate::worker::WorkJob;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
use chrono::{DateTime, Datelike, Utc};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
use isocountry::CountryCode;
|
use isocountry::CountryCode;
|
||||||
|
use lnurl::pay::{LnURLPayInvoice, PayResponse};
|
||||||
|
use lnurl::Tag;
|
||||||
use lnvps_db::{
|
use lnvps_db::{
|
||||||
IpRange, LNVpsDb, PaymentMethod, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate,
|
IpRange, LNVpsDb, PaymentMethod, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate,
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use nostr::util::hex;
|
use nostr::util::hex;
|
||||||
|
use nostr::{ToBech32, Url};
|
||||||
|
use rocket::http::ContentType;
|
||||||
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 rocket_okapi::gen::OpenApiGenerator;
|
use rocket_okapi::gen::OpenApiGenerator;
|
||||||
use rocket_okapi::okapi::openapi3::Responses;
|
use rocket_okapi::okapi::openapi3::Responses;
|
||||||
use rocket_okapi::response::OpenApiResponderInner;
|
use rocket_okapi::response::OpenApiResponderInner;
|
||||||
use rocket_okapi::{openapi, openapi_get_routes};
|
use rocket_okapi::{openapi, openapi_get_routes, openapi_routes};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ssh_key::PublicKey;
|
use ssh_key::PublicKey;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::io::{BufWriter, Cursor};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc::{Sender, UnboundedSender};
|
use tokio::sync::mpsc::{Sender, UnboundedSender};
|
||||||
@ -38,7 +44,7 @@ use tokio::sync::mpsc::{Sender, UnboundedSender};
|
|||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut routes = vec![];
|
let mut routes = vec![];
|
||||||
|
|
||||||
routes.append(&mut openapi_get_routes![
|
let mut api_routes = openapi_get_routes![
|
||||||
v1_get_account,
|
v1_get_account,
|
||||||
v1_patch_account,
|
v1_patch_account,
|
||||||
v1_list_vms,
|
v1_list_vms,
|
||||||
@ -58,18 +64,27 @@ pub fn routes() -> Vec<Route> {
|
|||||||
v1_time_series,
|
v1_time_series,
|
||||||
v1_custom_template_calc,
|
v1_custom_template_calc,
|
||||||
v1_create_custom_vm_order,
|
v1_create_custom_vm_order,
|
||||||
v1_get_payment_methods
|
v1_get_payment_methods,
|
||||||
]);
|
v1_payment_history
|
||||||
|
];
|
||||||
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
api_routes.append(&mut super::nostr_domain::routes());
|
||||||
|
routes.append(&mut api_routes);
|
||||||
|
|
||||||
routes.append(&mut routes![v1_terminal_proxy]);
|
routes.append(&mut routes![
|
||||||
|
v1_terminal_proxy,
|
||||||
|
v1_lnurlp,
|
||||||
|
v1_renew_vm_lnurlp,
|
||||||
|
v1_get_payment_invoice
|
||||||
|
]);
|
||||||
|
|
||||||
routes
|
routes
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>;
|
pub type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
struct ApiData<T: Serialize> {
|
pub struct ApiData<T: Serialize> {
|
||||||
pub data: T,
|
pub data: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +99,7 @@ impl<T: Serialize> ApiData<T> {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Responder)]
|
#[derive(Serialize, Deserialize, JsonSchema, Responder)]
|
||||||
#[response(status = 500)]
|
#[response(status = 500)]
|
||||||
struct ApiError {
|
pub struct ApiError {
|
||||||
pub error: String,
|
pub error: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,6 +137,13 @@ async fn v1_patch_account(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|c| CountryCode::for_alpha3(c).ok())
|
.and_then(|c| CountryCode::for_alpha3(c).ok())
|
||||||
.map(|c| c.alpha3().to_string());
|
.map(|c| c.alpha3().to_string());
|
||||||
|
user.billing_name = req.name.clone();
|
||||||
|
user.billing_address_1 = req.address_1.clone();
|
||||||
|
user.billing_address_2 = req.address_2.clone();
|
||||||
|
user.billing_city = req.city.clone();
|
||||||
|
user.billing_state = req.state.clone();
|
||||||
|
user.billing_postcode = req.postcode.clone();
|
||||||
|
user.billing_tax_id = req.tax_id.clone();
|
||||||
|
|
||||||
db.update_user(&user).await?;
|
db.update_user(&user).await?;
|
||||||
ApiData::ok(())
|
ApiData::ok(())
|
||||||
@ -138,12 +160,7 @@ async fn v1_get_account(
|
|||||||
let uid = db.upsert_user(&pubkey).await?;
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
let user = db.get_user(uid).await?;
|
let user = db.get_user(uid).await?;
|
||||||
|
|
||||||
ApiData::ok(AccountPatchRequest {
|
ApiData::ok(user.into())
|
||||||
email: user.email,
|
|
||||||
contact_nip17: user.contact_nip17,
|
|
||||||
contact_email: user.contact_email,
|
|
||||||
country_code: user.country_code,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn vm_to_status(
|
async fn vm_to_status(
|
||||||
@ -356,9 +373,18 @@ async fn v1_list_vm_templates(
|
|||||||
let max_memory = templates.iter().map(|t| t.memory).max().unwrap_or(GB * 2);
|
let max_memory = templates.iter().map(|t| t.memory).max().unwrap_or(GB * 2);
|
||||||
let max_disk = templates
|
let max_disk = templates
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.disk_size)
|
.map(|t| (t.disk_type, t.disk_interface, t.disk_size))
|
||||||
.max()
|
.fold(HashMap::new(), |mut acc, v| {
|
||||||
.unwrap_or(GB * 5);
|
let k = (v.0.into(), v.1.into());
|
||||||
|
if let Some(mut x) = acc.get_mut(&k) {
|
||||||
|
if *x < v.2 {
|
||||||
|
*x = v.2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
acc.insert(k, v.2);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
});
|
||||||
Some(
|
Some(
|
||||||
custom_templates
|
custom_templates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -370,7 +396,7 @@ async fn v1_list_vm_templates(
|
|||||||
region,
|
region,
|
||||||
max_cpu,
|
max_cpu,
|
||||||
max_memory,
|
max_memory,
|
||||||
max_disk,
|
&max_disk,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
})
|
})
|
||||||
@ -528,6 +554,65 @@ async fn v1_renew_vm(
|
|||||||
ApiData::ok(rsp.into())
|
ApiData::ok(rsp.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extend a VM by LNURL payment
|
||||||
|
#[get("/api/v1/vm/<id>/renew-lnurlp?<amount>")]
|
||||||
|
async fn v1_renew_vm_lnurlp(
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
provisioner: &State<Arc<LNVpsProvisioner>>,
|
||||||
|
id: u64,
|
||||||
|
amount: u64,
|
||||||
|
) -> Result<Json<LnURLPayInvoice>, &'static str> {
|
||||||
|
let vm = db.get_vm(id).await.map_err(|_e| "VM not found")?;
|
||||||
|
if vm.deleted {
|
||||||
|
return Err("VM not found");
|
||||||
|
}
|
||||||
|
if amount < 1000 {
|
||||||
|
return Err("Amount must be greater than 1000");
|
||||||
|
}
|
||||||
|
|
||||||
|
let rsp = provisioner
|
||||||
|
.renew_amount(
|
||||||
|
id,
|
||||||
|
CurrencyAmount::millisats(amount),
|
||||||
|
PaymentMethod::Lightning,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Error generating invoice")?;
|
||||||
|
|
||||||
|
// external_data is pr for lightning payment method
|
||||||
|
Ok(Json(LnURLPayInvoice::new(rsp.external_data)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// LNURL ad-hoc extend vm
|
||||||
|
#[get("/.well-known/lnurlp/<id>")]
|
||||||
|
async fn v1_lnurlp(
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
settings: &State<Settings>,
|
||||||
|
id: u64,
|
||||||
|
) -> Result<Json<PayResponse>, &'static str> {
|
||||||
|
let vm = db.get_vm(id).await.map_err(|_e| "VM not found")?;
|
||||||
|
if vm.deleted {
|
||||||
|
return Err("VM not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta = vec![vec!["text/plain".to_string(), format!("Extend VM {}", id)]];
|
||||||
|
let rsp = PayResponse {
|
||||||
|
callback: Url::parse(&settings.public_url)
|
||||||
|
.map_err(|_| "Invalid public url")?
|
||||||
|
.join(&format!("/api/v1/vm/{}/renew-lnurlp", id))
|
||||||
|
.map_err(|_| "Could not get callback url")?
|
||||||
|
.to_string(),
|
||||||
|
max_sendable: 1_000_000_000,
|
||||||
|
min_sendable: 1_000, // TODO: calc min by using 1s extend time
|
||||||
|
tag: Tag::PayRequest,
|
||||||
|
metadata: serde_json::to_string(&meta).map_err(|_e| "Failed to serialize metadata")?,
|
||||||
|
comment_allowed: None,
|
||||||
|
allows_nostr: None,
|
||||||
|
nostr_pubkey: None,
|
||||||
|
};
|
||||||
|
Ok(Json(rsp))
|
||||||
|
}
|
||||||
|
|
||||||
/// Start a VM
|
/// Start a VM
|
||||||
#[openapi(tag = "VM")]
|
#[openapi(tag = "VM")]
|
||||||
#[patch("/api/v1/vm/<id>/start")]
|
#[patch("/api/v1/vm/<id>/start")]
|
||||||
@ -800,3 +885,115 @@ async fn v1_get_payment(
|
|||||||
|
|
||||||
ApiData::ok(payment.into())
|
ApiData::ok(payment.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print payment invoice
|
||||||
|
#[get("/api/v1/payment/<id>/invoice?<auth>")]
|
||||||
|
async fn v1_get_payment_invoice(
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
id: &str,
|
||||||
|
auth: &str,
|
||||||
|
) -> Result<(ContentType, Vec<u8>), &'static str> {
|
||||||
|
let auth = Nip98Auth::from_base64(auth).map_err(|e| "Missing or invalid auth param")?;
|
||||||
|
if auth
|
||||||
|
.check(&format!("/api/v1/payment/{id}/invoice"), "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 id = if let Ok(i) = hex::decode(id) {
|
||||||
|
i
|
||||||
|
} else {
|
||||||
|
return Err("Invalid payment id");
|
||||||
|
};
|
||||||
|
|
||||||
|
let payment = db
|
||||||
|
.get_vm_payment(&id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Payment not found")?;
|
||||||
|
let vm = db.get_vm(payment.vm_id).await.map_err(|_| "VM not found")?;
|
||||||
|
if vm.user_id != uid {
|
||||||
|
return Err("VM does not belong to you");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !payment.is_paid {
|
||||||
|
return Err("Payment is not paid, can't generate invoice");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct PaymentInfo {
|
||||||
|
year: i32,
|
||||||
|
current_date: DateTime<Utc>,
|
||||||
|
vm: ApiVmStatus,
|
||||||
|
payment: ApiVmPayment,
|
||||||
|
user: AccountPatchRequest,
|
||||||
|
npub: String,
|
||||||
|
total: u64,
|
||||||
|
company: Option<ApiCompany>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let host = db
|
||||||
|
.get_host(vm.host_id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Host not found")?;
|
||||||
|
let region = db
|
||||||
|
.get_host_region(host.region_id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Region not found")?;
|
||||||
|
let company = if let Some(c) = region.company_id {
|
||||||
|
Some(db.get_company(c).await.map_err(|_| "Company not found")?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let user = db.get_user(uid).await.map_err(|_| "User not found")?;
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let template =
|
||||||
|
mustache::compile_path("lnvps_api/invoice.html").map_err(|_| "Invalid template")?;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let template = mustache::compile_str(include_str!("../../invoice.html"))
|
||||||
|
.map_err(|_| "Invalid template")?;
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let mut html = Cursor::new(Vec::new());
|
||||||
|
template
|
||||||
|
.render(
|
||||||
|
&mut html,
|
||||||
|
&PaymentInfo {
|
||||||
|
year: now.year(),
|
||||||
|
current_date: now,
|
||||||
|
vm: vm_to_status(db, vm, None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Failed to get VM state")?,
|
||||||
|
total: payment.amount + payment.tax,
|
||||||
|
payment: payment.into(),
|
||||||
|
npub: nostr::PublicKey::from_slice(&user.pubkey)
|
||||||
|
.map_err(|_| "Invalid pubkey")?
|
||||||
|
.to_bech32()
|
||||||
|
.unwrap(),
|
||||||
|
user: user.into(),
|
||||||
|
company: company.map(|c| c.into()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| "Failed to generate invoice")?;
|
||||||
|
Ok((ContentType::HTML, html.into_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List payment history of a VM
|
||||||
|
#[openapi(tag = "VM")]
|
||||||
|
#[get("/api/v1/vm/<id>/payments")]
|
||||||
|
async fn v1_payment_history(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
id: u64,
|
||||||
|
) -> ApiResult<Vec<ApiVmPayment>> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
let vm = db.get_vm(id).await?;
|
||||||
|
if vm.user_id != uid {
|
||||||
|
return ApiData::err("VM does not belong to you");
|
||||||
|
}
|
||||||
|
|
||||||
|
let payments = db.list_vm_payment(id).await?;
|
||||||
|
ApiData::ok(payments.into_iter().map(|i| i.into()).collect())
|
||||||
|
}
|
@ -1,16 +1,16 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use config::{Config, File};
|
use config::{Config, File};
|
||||||
use lnvps::api;
|
use lnvps_api::api;
|
||||||
use lnvps::cors::CORS;
|
use lnvps_api::data_migration::run_data_migrations;
|
||||||
use lnvps::data_migration::run_data_migrations;
|
use lnvps_api::dvm::start_dvms;
|
||||||
use lnvps::dvm::start_dvms;
|
use lnvps_api::exchange::{DefaultRateCache, ExchangeRateService};
|
||||||
use lnvps::exchange::{DefaultRateCache, ExchangeRateService};
|
use lnvps_api::lightning::get_node;
|
||||||
use lnvps::lightning::get_node;
|
use lnvps_api::payments::listen_all_payments;
|
||||||
use lnvps::payments::listen_all_payments;
|
use lnvps_api::settings::Settings;
|
||||||
use lnvps::settings::Settings;
|
use lnvps_api::status::VmStateCache;
|
||||||
use lnvps::status::VmStateCache;
|
use lnvps_api::worker::{WorkJob, Worker};
|
||||||
use lnvps::worker::{WorkJob, Worker};
|
use lnvps_common::CORS;
|
||||||
use lnvps_db::{LNVpsDb, LNVpsDbMysql};
|
use lnvps_db::{LNVpsDb, LNVpsDbMysql};
|
||||||
use log::error;
|
use log::error;
|
||||||
use nostr::Keys;
|
use nostr::Keys;
|
||||||
@ -168,7 +168,7 @@ async fn main() -> Result<(), Error> {
|
|||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
error!("{}", e);
|
error!("{:?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
@ -1,12 +1,13 @@
|
|||||||
use crate::data_migration::DataMigration;
|
use crate::data_migration::DataMigration;
|
||||||
use crate::provisioner::{LNVpsProvisioner, NetworkProvisioner};
|
use crate::provisioner::{LNVpsProvisioner, NetworkProvisioner};
|
||||||
|
use chrono::Utc;
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
|
use log::info;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use log::info;
|
|
||||||
|
|
||||||
pub struct Ip6InitDataMigration {
|
pub struct Ip6InitDataMigration {
|
||||||
db: Arc<dyn LNVpsDb>,
|
db: Arc<dyn LNVpsDb>,
|
||||||
@ -27,6 +28,9 @@ impl DataMigration for Ip6InitDataMigration {
|
|||||||
let net = NetworkProvisioner::new(db.clone());
|
let net = NetworkProvisioner::new(db.clone());
|
||||||
let vms = db.list_vms().await?;
|
let vms = db.list_vms().await?;
|
||||||
for vm in vms {
|
for vm in vms {
|
||||||
|
if vm.expires < Utc::now() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let host = db.get_host(vm.host_id).await?;
|
let host = db.get_host(vm.host_id).await?;
|
||||||
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
||||||
// if no ipv6 address is picked already pick one
|
// if no ipv6 address is picked already pick one
|
||||||
@ -34,8 +38,7 @@ impl DataMigration for Ip6InitDataMigration {
|
|||||||
IpNetwork::from_str(&i.ip)
|
IpNetwork::from_str(&i.ip)
|
||||||
.map(|i| i.is_ipv4())
|
.map(|i| i.is_ipv4())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
}) {
|
||||||
{
|
|
||||||
let ips_pick = net.pick_ip_for_region(host.region_id).await?;
|
let ips_pick = net.pick_ip_for_region(host.region_id).await?;
|
||||||
if let Some(mut v6) = ips_pick.ip6 {
|
if let Some(mut v6) = ips_pick.ip6 {
|
||||||
info!("Assigning ip {} to vm {}", v6.ip, vm.id);
|
info!("Assigning ip {} to vm {}", v6.ip, vm.id);
|
@ -1,4 +1,6 @@
|
|||||||
use crate::data_migration::dns::DnsDataMigration;
|
use crate::data_migration::dns::DnsDataMigration;
|
||||||
|
use crate::data_migration::ip6_init::Ip6InitDataMigration;
|
||||||
|
use crate::provisioner::LNVpsProvisioner;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
@ -6,8 +8,6 @@ use log::{error, info};
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::data_migration::ip6_init::Ip6InitDataMigration;
|
|
||||||
use crate::provisioner::LNVpsProvisioner;
|
|
||||||
|
|
||||||
mod dns;
|
mod dns;
|
||||||
mod ip6_init;
|
mod ip6_init;
|
||||||
@ -17,9 +17,16 @@ pub trait DataMigration: Send + Sync {
|
|||||||
fn migrate(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send>>;
|
fn migrate(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_data_migrations(db: Arc<dyn LNVpsDb>, lnvps: Arc<LNVpsProvisioner>, settings: &Settings) -> Result<()> {
|
pub async fn run_data_migrations(
|
||||||
|
db: Arc<dyn LNVpsDb>,
|
||||||
|
lnvps: Arc<LNVpsProvisioner>,
|
||||||
|
settings: &Settings,
|
||||||
|
) -> Result<()> {
|
||||||
let mut migrations: Vec<Box<dyn DataMigration>> = vec![];
|
let mut migrations: Vec<Box<dyn DataMigration>> = vec![];
|
||||||
migrations.push(Box::new(Ip6InitDataMigration::new(db.clone(), lnvps.clone())));
|
migrations.push(Box::new(Ip6InitDataMigration::new(
|
||||||
|
db.clone(),
|
||||||
|
lnvps.clone(),
|
||||||
|
)));
|
||||||
|
|
||||||
if let Some(d) = DnsDataMigration::new(db.clone(), settings) {
|
if let Some(d) = DnsDataMigration::new(db.clone(), settings) {
|
||||||
migrations.push(Box::new(d));
|
migrations.push(Box::new(d));
|
@ -6,9 +6,9 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
#[cfg(feature = "cloudflare")]
|
#[cfg(feature = "cloudflare")]
|
||||||
mod cloudflare;
|
mod cloudflare;
|
||||||
|
use crate::provisioner::NetworkProvisioner;
|
||||||
#[cfg(feature = "cloudflare")]
|
#[cfg(feature = "cloudflare")]
|
||||||
pub use cloudflare::*;
|
pub use cloudflare::*;
|
||||||
use crate::provisioner::NetworkProvisioner;
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait DnsServer: Send + Sync {
|
pub trait DnsServer: Send + Sync {
|
@ -1,6 +1,6 @@
|
|||||||
use anyhow::{anyhow, ensure, Result};
|
use anyhow::{anyhow, ensure, Result};
|
||||||
use lnvps_db::async_trait;
|
use lnvps_db::async_trait;
|
||||||
use log::info;
|
use log::{info, trace};
|
||||||
use rocket::serde::Deserialize;
|
use rocket::serde::Deserialize;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@ -15,6 +15,11 @@ pub enum Currency {
|
|||||||
EUR,
|
EUR,
|
||||||
BTC,
|
BTC,
|
||||||
USD,
|
USD,
|
||||||
|
GBP,
|
||||||
|
CAD,
|
||||||
|
CHF,
|
||||||
|
AUD,
|
||||||
|
JPY,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Currency {
|
impl Display for Currency {
|
||||||
@ -23,6 +28,11 @@ impl Display for Currency {
|
|||||||
Currency::EUR => write!(f, "EUR"),
|
Currency::EUR => write!(f, "EUR"),
|
||||||
Currency::BTC => write!(f, "BTC"),
|
Currency::BTC => write!(f, "BTC"),
|
||||||
Currency::USD => write!(f, "USD"),
|
Currency::USD => write!(f, "USD"),
|
||||||
|
Currency::GBP => write!(f, "GBP"),
|
||||||
|
Currency::CAD => write!(f, "CAD"),
|
||||||
|
Currency::CHF => write!(f, "CHF"),
|
||||||
|
Currency::AUD => write!(f, "AUD"),
|
||||||
|
Currency::JPY => write!(f, "JPY"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,6 +45,11 @@ impl FromStr for Currency {
|
|||||||
"eur" => Ok(Currency::EUR),
|
"eur" => Ok(Currency::EUR),
|
||||||
"usd" => Ok(Currency::USD),
|
"usd" => Ok(Currency::USD),
|
||||||
"btc" => Ok(Currency::BTC),
|
"btc" => Ok(Currency::BTC),
|
||||||
|
"gbp" => Ok(Currency::GBP),
|
||||||
|
"cad" => Ok(Currency::CAD),
|
||||||
|
"chf" => Ok(Currency::CHF),
|
||||||
|
"aud" => Ok(Currency::AUD),
|
||||||
|
"jpy" => Ok(Currency::JPY),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,21 +75,25 @@ impl Display for Ticker {
|
|||||||
pub struct TickerRate(pub Ticker, pub f32);
|
pub struct TickerRate(pub Ticker, pub f32);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct CurrencyAmount(pub Currency, u64);
|
pub struct CurrencyAmount(Currency, u64);
|
||||||
|
|
||||||
impl CurrencyAmount {
|
impl CurrencyAmount {
|
||||||
const MILLI_SATS: f64 = 1.0e11;
|
const MILLI_SATS: f64 = 1.0e11;
|
||||||
|
|
||||||
|
pub fn millisats(amount: u64) -> Self {
|
||||||
|
CurrencyAmount(Currency::BTC, amount)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_u64(currency: Currency, amount: u64) -> Self {
|
pub fn from_u64(currency: Currency, amount: u64) -> Self {
|
||||||
CurrencyAmount(currency, amount)
|
CurrencyAmount(currency, amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_f32(currency: Currency, amount: f32) -> Self {
|
pub fn from_f32(currency: Currency, amount: f32) -> Self {
|
||||||
CurrencyAmount(
|
CurrencyAmount(
|
||||||
currency,
|
currency,
|
||||||
match currency {
|
match currency {
|
||||||
Currency::EUR => (amount * 100.0) as u64, // cents
|
|
||||||
Currency::BTC => (amount as f64 * Self::MILLI_SATS) as u64, // milli-sats
|
Currency::BTC => (amount as f64 * Self::MILLI_SATS) as u64, // milli-sats
|
||||||
Currency::USD => (amount * 100.0) as u64, // cents
|
_ => (amount * 100.0) as u64, // cents
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -85,11 +104,14 @@ impl CurrencyAmount {
|
|||||||
|
|
||||||
pub fn value_f32(&self) -> f32 {
|
pub fn value_f32(&self) -> f32 {
|
||||||
match self.0 {
|
match self.0 {
|
||||||
Currency::EUR => self.1 as f32 / 100.0,
|
|
||||||
Currency::BTC => (self.1 as f64 / Self::MILLI_SATS) as f32,
|
Currency::BTC => (self.1 as f64 / Self::MILLI_SATS) as f32,
|
||||||
Currency::USD => self.1 as f32 / 100.0,
|
_ => self.1 as f32 / 100.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn currency(&self) -> Currency {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TickerRate {
|
impl TickerRate {
|
||||||
@ -167,13 +189,28 @@ impl ExchangeRateService for DefaultRateCache {
|
|||||||
if let Some(eur) = rates.eur {
|
if let Some(eur) = rates.eur {
|
||||||
ret.push(TickerRate(Ticker(Currency::BTC, Currency::EUR), eur));
|
ret.push(TickerRate(Ticker(Currency::BTC, Currency::EUR), eur));
|
||||||
}
|
}
|
||||||
|
if let Some(gbp) = rates.gbp {
|
||||||
|
ret.push(TickerRate(Ticker(Currency::BTC, Currency::GBP), gbp));
|
||||||
|
}
|
||||||
|
if let Some(cad) = rates.cad {
|
||||||
|
ret.push(TickerRate(Ticker(Currency::BTC, Currency::CAD), cad));
|
||||||
|
}
|
||||||
|
if let Some(chf) = rates.chf {
|
||||||
|
ret.push(TickerRate(Ticker(Currency::BTC, Currency::CHF), chf));
|
||||||
|
}
|
||||||
|
if let Some(aud) = rates.aud {
|
||||||
|
ret.push(TickerRate(Ticker(Currency::BTC, Currency::AUD), aud));
|
||||||
|
}
|
||||||
|
if let Some(jpy) = rates.jpy {
|
||||||
|
ret.push(TickerRate(Ticker(Currency::BTC, Currency::JPY), jpy));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_rate(&self, ticker: Ticker, amount: f32) {
|
async fn set_rate(&self, ticker: Ticker, amount: f32) {
|
||||||
let mut cache = self.cache.write().await;
|
let mut cache = self.cache.write().await;
|
||||||
info!("{}: {}", &ticker, amount);
|
trace!("{}: {}", &ticker, amount);
|
||||||
cache.insert(ticker, amount);
|
cache.insert(ticker, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,6 +231,16 @@ struct MempoolRates {
|
|||||||
pub usd: Option<f32>,
|
pub usd: Option<f32>,
|
||||||
#[serde(rename = "EUR")]
|
#[serde(rename = "EUR")]
|
||||||
pub eur: Option<f32>,
|
pub eur: Option<f32>,
|
||||||
|
#[serde(rename = "CAD")]
|
||||||
|
pub cad: Option<f32>,
|
||||||
|
#[serde(rename = "GBP")]
|
||||||
|
pub gbp: Option<f32>,
|
||||||
|
#[serde(rename = "CHF")]
|
||||||
|
pub chf: Option<f32>,
|
||||||
|
#[serde(rename = "AUD")]
|
||||||
|
pub aud: Option<f32>,
|
||||||
|
#[serde(rename = "JPY")]
|
||||||
|
pub jpy: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
@ -6,7 +6,7 @@ use anyhow::{bail, Result};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use nostr::Url;
|
use nostr::Url;
|
||||||
use reqwest::header::AUTHORIZATION;
|
use reqwest::header::AUTHORIZATION;
|
||||||
use reqwest::{Client, Method, RequestBuilder};
|
use reqwest::{Method, RequestBuilder};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@ -89,11 +89,10 @@ impl RevolutApi {
|
|||||||
.post(
|
.post(
|
||||||
"/api/orders",
|
"/api/orders",
|
||||||
CreateOrderRequest {
|
CreateOrderRequest {
|
||||||
currency: amount.0.to_string(),
|
currency: amount.currency().to_string(),
|
||||||
amount: match amount.0 {
|
amount: match amount.currency() {
|
||||||
Currency::BTC => bail!("Bitcoin amount not allowed for fiat payments"),
|
Currency::BTC => bail!("Bitcoin amount not allowed for fiat payments"),
|
||||||
Currency::EUR => amount.value(),
|
_ => amount.value(),
|
||||||
Currency::USD => amount.value(),
|
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
},
|
},
|
657
lnvps_api/src/host/libvirt.rs
Normal file
657
lnvps_api/src/host/libvirt.rs
Normal file
@ -0,0 +1,657 @@
|
|||||||
|
use crate::host::{
|
||||||
|
FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient, VmHostDiskInfo,
|
||||||
|
VmHostInfo,
|
||||||
|
};
|
||||||
|
use crate::settings::QemuConfig;
|
||||||
|
use crate::status::{VmRunningState, VmState};
|
||||||
|
use crate::KB;
|
||||||
|
use anyhow::{bail, ensure, Context, Result};
|
||||||
|
use chrono::Utc;
|
||||||
|
use lnvps_db::{async_trait, LNVpsDb, Vm, VmOsImage};
|
||||||
|
use log::info;
|
||||||
|
use rand::random;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use virt::connect::Connect;
|
||||||
|
use virt::domain::Domain;
|
||||||
|
use virt::sys::{
|
||||||
|
virDomainCreate, VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE, VIR_DOMAIN_START_VALIDATE,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LibVirtHost {
|
||||||
|
connection: Connect,
|
||||||
|
qemu: QemuConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LibVirtHost {
|
||||||
|
pub fn new(url: &str, qemu: QemuConfig) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
connection: Connect::open(Some(url))?,
|
||||||
|
qemu,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_disk_image(&self, vm: &Vm, image: &VmOsImage) -> Result<()> {
|
||||||
|
// https://libvirt.org/html/libvirt-libvirt-storage.html#virStorageVolUpload
|
||||||
|
// https://libvirt.org/html/libvirt-libvirt-storage.html#virStorageVolResize
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_domain_xml(&self, cfg: &FullVmInfo) -> Result<DomainXML> {
|
||||||
|
let storage = self
|
||||||
|
.connection
|
||||||
|
.list_all_storage_pools(VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE)?;
|
||||||
|
|
||||||
|
// check the storage disk exists, we don't need anything else from it for now
|
||||||
|
let _storage_disk = if let Some(d) = storage
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.get_name().map(|n| n == cfg.disk.name).unwrap_or(false))
|
||||||
|
{
|
||||||
|
d
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"Disk \"{}\" not found on host! Available pools: {}",
|
||||||
|
cfg.disk.name,
|
||||||
|
storage
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| s.get_name().ok())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let resources = cfg.resources()?;
|
||||||
|
let mut devices = vec![];
|
||||||
|
// primary disk
|
||||||
|
devices.push(DomainDevice::Disk(Disk {
|
||||||
|
kind: DiskType::File,
|
||||||
|
device: DiskDevice::Disk,
|
||||||
|
source: DiskSource {
|
||||||
|
file: Some(format!("{}:vm-{}-disk0", cfg.disk.name, cfg.vm.id)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
target: DiskTarget {
|
||||||
|
dev: "vda".to_string(),
|
||||||
|
bus: Some(DiskBus::VirtIO),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
devices.push(DomainDevice::Interface(NetworkInterface {
|
||||||
|
kind: NetworkKind::Bridge,
|
||||||
|
mac: Some(NetworkMac {
|
||||||
|
address: cfg.vm.mac_address.clone(),
|
||||||
|
}),
|
||||||
|
source: Some(NetworkSource {
|
||||||
|
bridge: Some(self.qemu.bridge.clone()),
|
||||||
|
}),
|
||||||
|
target: None,
|
||||||
|
vlan: cfg.host.vlan_id.map(|v| NetworkVlan {
|
||||||
|
tags: vec![NetworkVlanTag { id: v as u32 }],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
Ok(DomainXML {
|
||||||
|
kind: DomainType::KVM,
|
||||||
|
id: Some(cfg.vm.id),
|
||||||
|
name: Some(format!("VM{}", cfg.vm.id)),
|
||||||
|
uuid: None,
|
||||||
|
title: None,
|
||||||
|
description: None,
|
||||||
|
os: DomainOs {
|
||||||
|
kind: DomainOsType {
|
||||||
|
kind: DomainOsTypeKind::Hvm,
|
||||||
|
arch: Some(DomainOsArch::from_str(&self.qemu.arch)?),
|
||||||
|
machine: Some(DomainOsMachine::from_str(&self.qemu.machine)?),
|
||||||
|
},
|
||||||
|
firmware: Some(DomainOsFirmware::EFI),
|
||||||
|
loader: Some(DomainOsLoader {
|
||||||
|
read_only: None,
|
||||||
|
kind: None,
|
||||||
|
secure: Some(true),
|
||||||
|
stateless: None,
|
||||||
|
format: None,
|
||||||
|
}),
|
||||||
|
boot: DomainOsBoot {
|
||||||
|
dev: DomainOsBootDev::HardDrive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vcpu: resources.cpu,
|
||||||
|
memory: resources.memory,
|
||||||
|
devices: DomainDevices { contents: devices },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl VmHostClient for LibVirtHost {
|
||||||
|
async fn get_info(&self) -> Result<VmHostInfo> {
|
||||||
|
let info = self.connection.get_node_info()?;
|
||||||
|
let storage = self
|
||||||
|
.connection
|
||||||
|
.list_all_storage_pools(VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE)?;
|
||||||
|
Ok(VmHostInfo {
|
||||||
|
cpu: info.cpus as u16,
|
||||||
|
memory: info.memory * KB,
|
||||||
|
disks: storage
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| {
|
||||||
|
let info = p.get_info().ok()?;
|
||||||
|
Some(VmHostDiskInfo {
|
||||||
|
name: p.get_name().context("storage pool name is missing").ok()?,
|
||||||
|
size: info.capacity,
|
||||||
|
used: info.allocation,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn download_os_image(&self, image: &VmOsImage) -> Result<()> {
|
||||||
|
// TODO: download ISO images to host (somehow, ssh?)
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_mac(&self, _vm: &Vm) -> Result<String> {
|
||||||
|
Ok(format!(
|
||||||
|
"52:54:00:{}:{}:{}",
|
||||||
|
hex::encode([random::<u8>()]),
|
||||||
|
hex::encode([random::<u8>()]),
|
||||||
|
hex::encode([random::<u8>()])
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_vm(&self, vm: &Vm) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop_vm(&self, vm: &Vm) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reset_vm(&self, vm: &Vm) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_vm(&self, cfg: &FullVmInfo) -> Result<()> {
|
||||||
|
let domain = self.create_domain_xml(cfg)?;
|
||||||
|
let xml = quick_xml::se::to_string(&domain)?;
|
||||||
|
let domain = Domain::create_xml(&self.connection, &xml, VIR_DOMAIN_START_VALIDATE)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_vm(&self, vm: &Vm) -> Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reinstall_vm(&self, cfg: &FullVmInfo) -> Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState> {
|
||||||
|
Ok(VmState {
|
||||||
|
timestamp: Utc::now().timestamp() as u64,
|
||||||
|
state: VmRunningState::Stopped,
|
||||||
|
cpu_usage: 0.0,
|
||||||
|
mem_usage: 0.0,
|
||||||
|
uptime: 0,
|
||||||
|
net_in: 0,
|
||||||
|
net_out: 0,
|
||||||
|
disk_write: 0,
|
||||||
|
disk_read: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn configure_vm(&self, vm: &FullVmInfo) -> Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_time_series_data(
|
||||||
|
&self,
|
||||||
|
vm: &Vm,
|
||||||
|
series: TimeSeries,
|
||||||
|
) -> Result<Vec<TimeSeriesData>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_terminal(&self, vm: &Vm) -> Result<TerminalStream> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "domain")]
|
||||||
|
struct DomainXML {
|
||||||
|
#[serde(rename = "@type")]
|
||||||
|
pub kind: DomainType,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@id")]
|
||||||
|
pub id: Option<u64>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub uuid: Option<Uuid>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub os: DomainOs,
|
||||||
|
pub vcpu: u16,
|
||||||
|
pub memory: u64,
|
||||||
|
pub devices: DomainDevices,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "devices")]
|
||||||
|
struct DomainDevices {
|
||||||
|
#[serde(rename = "$value")]
|
||||||
|
pub contents: Vec<DomainDevice>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DomainType {
|
||||||
|
#[default]
|
||||||
|
KVM,
|
||||||
|
XEN,
|
||||||
|
HVF,
|
||||||
|
QEMU,
|
||||||
|
LXC,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "os")]
|
||||||
|
struct DomainOs {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub kind: DomainOsType,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@firmware")]
|
||||||
|
pub firmware: Option<DomainOsFirmware>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub loader: Option<DomainOsLoader>,
|
||||||
|
pub boot: DomainOsBoot,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DomainOsFirmware {
|
||||||
|
#[default]
|
||||||
|
EFI,
|
||||||
|
BIOS,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
struct DomainOsType {
|
||||||
|
#[serde(rename = "$text")]
|
||||||
|
pub kind: DomainOsTypeKind,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@arch")]
|
||||||
|
pub arch: Option<DomainOsArch>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@machine")]
|
||||||
|
pub machine: Option<DomainOsMachine>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DomainOsTypeKind {
|
||||||
|
#[default]
|
||||||
|
Hvm,
|
||||||
|
Xen,
|
||||||
|
Linux,
|
||||||
|
XenPvh,
|
||||||
|
Exe,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DomainOsMachine {
|
||||||
|
#[default]
|
||||||
|
Q35,
|
||||||
|
PC,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for DomainOsMachine {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"q35" => Ok(DomainOsMachine::Q35),
|
||||||
|
"pc" => Ok(DomainOsMachine::PC),
|
||||||
|
v => bail!("Unknown machine type {}", v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DomainOsArch {
|
||||||
|
#[default]
|
||||||
|
X86_64,
|
||||||
|
I686,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for DomainOsArch {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"x86_64" => Ok(Self::X86_64),
|
||||||
|
"i686" => Ok(Self::I686),
|
||||||
|
v => bail!("unsupported arch {}", v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "loader")]
|
||||||
|
struct DomainOsLoader {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@readonly")]
|
||||||
|
pub read_only: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@type")]
|
||||||
|
pub kind: Option<DomainOsLoaderType>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@secure")]
|
||||||
|
pub secure: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@stateless")]
|
||||||
|
pub stateless: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@format")]
|
||||||
|
pub format: Option<DomainOsLoaderFormat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DomainOsLoaderType {
|
||||||
|
#[default]
|
||||||
|
ROM,
|
||||||
|
PFlash,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DomainOsLoaderFormat {
|
||||||
|
Raw,
|
||||||
|
#[default]
|
||||||
|
QCow2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
struct DomainOsBoot {
|
||||||
|
#[serde(rename = "@dev")]
|
||||||
|
pub dev: DomainOsBootDev,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DomainOsBootDev {
|
||||||
|
#[serde(rename = "fd")]
|
||||||
|
Floppy,
|
||||||
|
#[serde(rename = "hd")]
|
||||||
|
#[default]
|
||||||
|
HardDrive,
|
||||||
|
CdRom,
|
||||||
|
Network,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "vcpu")]
|
||||||
|
struct DomainVCPU {
|
||||||
|
#[serde(rename = "$text")]
|
||||||
|
pub count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
enum DomainDevice {
|
||||||
|
#[serde(rename = "disk")]
|
||||||
|
Disk(Disk),
|
||||||
|
#[serde(rename = "interface")]
|
||||||
|
Interface(NetworkInterface),
|
||||||
|
#[serde(other)]
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "interface")]
|
||||||
|
struct NetworkInterface {
|
||||||
|
#[serde(rename = "@type")]
|
||||||
|
pub kind: NetworkKind,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mac: Option<NetworkMac>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub source: Option<NetworkSource>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub target: Option<NetworkTarget>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub vlan: Option<NetworkVlan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "vlan")]
|
||||||
|
struct NetworkVlan {
|
||||||
|
#[serde(rename = "tag")]
|
||||||
|
pub tags: Vec<NetworkVlanTag>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "tag")]
|
||||||
|
struct NetworkVlanTag {
|
||||||
|
#[serde(rename = "@id")]
|
||||||
|
pub id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum NetworkKind {
|
||||||
|
Network,
|
||||||
|
#[default]
|
||||||
|
Bridge,
|
||||||
|
User,
|
||||||
|
Ethernet,
|
||||||
|
Direct,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename = "mac")]
|
||||||
|
struct NetworkMac {
|
||||||
|
#[serde(rename = "@address")]
|
||||||
|
pub address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename = "source")]
|
||||||
|
struct NetworkSource {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@bridge")]
|
||||||
|
pub bridge: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename = "target")]
|
||||||
|
struct NetworkTarget {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@dev")]
|
||||||
|
pub dev: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename = "disk")]
|
||||||
|
struct Disk {
|
||||||
|
#[serde(rename = "@type")]
|
||||||
|
pub kind: DiskType,
|
||||||
|
#[serde(rename = "@device")]
|
||||||
|
pub device: DiskDevice,
|
||||||
|
pub source: DiskSource,
|
||||||
|
pub target: DiskTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DiskType {
|
||||||
|
#[default]
|
||||||
|
File,
|
||||||
|
Block,
|
||||||
|
Dir,
|
||||||
|
Network,
|
||||||
|
Volume,
|
||||||
|
Nvme,
|
||||||
|
VHostUser,
|
||||||
|
VHostVdpa,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DiskDevice {
|
||||||
|
Floppy,
|
||||||
|
#[default]
|
||||||
|
Disk,
|
||||||
|
CdRom,
|
||||||
|
Lun,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "source")]
|
||||||
|
struct DiskSource {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@file")]
|
||||||
|
pub file: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@dir")]
|
||||||
|
pub dir: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename = "target")]
|
||||||
|
struct DiskTarget {
|
||||||
|
/// Device name (hint)
|
||||||
|
#[serde(rename = "@dev")]
|
||||||
|
pub dev: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "@bus")]
|
||||||
|
pub bus: Option<DiskBus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum DiskBus {
|
||||||
|
#[default]
|
||||||
|
IDE,
|
||||||
|
SCSI,
|
||||||
|
VirtIO,
|
||||||
|
XEN,
|
||||||
|
USB,
|
||||||
|
SATA,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::host::tests::mock_full_vm;
|
||||||
|
|
||||||
|
fn cfg() -> FullVmInfo {
|
||||||
|
let mut cfg = mock_full_vm();
|
||||||
|
// adjust mock data for libvirt test driver
|
||||||
|
cfg.disk.name = "default-pool".to_string();
|
||||||
|
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xml_os() -> Result<()> {
|
||||||
|
let tag = "<os firmware=\"efi\"><type>hvm</type><boot dev=\"hd\"/></os>";
|
||||||
|
|
||||||
|
let test = DomainOs {
|
||||||
|
kind: DomainOsType {
|
||||||
|
kind: DomainOsTypeKind::Hvm,
|
||||||
|
arch: None,
|
||||||
|
machine: None,
|
||||||
|
},
|
||||||
|
firmware: Some(DomainOsFirmware::EFI),
|
||||||
|
loader: None,
|
||||||
|
boot: DomainOsBoot {
|
||||||
|
dev: DomainOsBootDev::HardDrive,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let xml = quick_xml::se::to_string(&test)?;
|
||||||
|
assert_eq!(tag, xml);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_xml_disk() -> Result<()> {
|
||||||
|
let tag = "<disk type=\"file\" device=\"disk\"><source file=\"/var/lib/libvirt/images/disk.qcow2\"/><target dev=\"vda\" bus=\"virtio\"/></disk>";
|
||||||
|
|
||||||
|
let test = Disk {
|
||||||
|
kind: DiskType::File,
|
||||||
|
device: DiskDevice::Disk,
|
||||||
|
source: DiskSource {
|
||||||
|
file: Some("/var/lib/libvirt/images/disk.qcow2".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
target: DiskTarget {
|
||||||
|
dev: "vda".to_string(),
|
||||||
|
bus: Some(DiskBus::VirtIO),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let xml = quick_xml::se::to_string(&test)?;
|
||||||
|
assert_eq!(tag, xml);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_config_to_domain() -> Result<()> {
|
||||||
|
let cfg = cfg();
|
||||||
|
let template = cfg.template.clone().unwrap();
|
||||||
|
|
||||||
|
let q_cfg = QemuConfig {
|
||||||
|
machine: "q35".to_string(),
|
||||||
|
os_type: "l26".to_string(),
|
||||||
|
bridge: "vmbr0".to_string(),
|
||||||
|
cpu: "kvm64".to_string(),
|
||||||
|
kvm: true,
|
||||||
|
arch: "x86_64".to_string(),
|
||||||
|
};
|
||||||
|
let host = LibVirtHost::new("test:///default", q_cfg)?;
|
||||||
|
let xml = host.create_domain_xml(&cfg)?;
|
||||||
|
|
||||||
|
let res = cfg.resources()?;
|
||||||
|
assert_eq!(xml.vcpu, res.cpu);
|
||||||
|
assert_eq!(xml.memory, res.memory);
|
||||||
|
|
||||||
|
let xml = quick_xml::se::to_string(&xml)?;
|
||||||
|
println!("{}", xml);
|
||||||
|
|
||||||
|
let output = r#"<domain type="kvm" id="1"><name>VM1</name><os firmware="efi"><type arch="x86_64" machine="q35">hvm</type><loader secure="true"/><boot dev="hd"/></os><vcpu>2</vcpu><memory>2147483648</memory><devices><disk type="file" device="disk"><source file="default-pool:vm-1-disk0"/><target dev="vda" bus="virtio"/></disk><interface type="bridge"><mac address="ff:ff:ff:ff:ff:fe"/><source bridge="vmbr0"/><vlan><tag id="100"/></vlan></interface></devices></domain>"#;
|
||||||
|
assert_eq!(xml, output);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ignore]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn text_vm_lifecycle() -> Result<()> {
|
||||||
|
let cfg = cfg();
|
||||||
|
let template = cfg.template.clone().unwrap();
|
||||||
|
|
||||||
|
let q_cfg = QemuConfig {
|
||||||
|
machine: "q35".to_string(),
|
||||||
|
os_type: "l26".to_string(),
|
||||||
|
bridge: "vmbr0".to_string(),
|
||||||
|
cpu: "kvm64".to_string(),
|
||||||
|
kvm: true,
|
||||||
|
arch: "x86_64".to_string(),
|
||||||
|
};
|
||||||
|
let host = LibVirtHost::new("test:///default", q_cfg)?;
|
||||||
|
println!("{:?}", host.get_info().await?);
|
||||||
|
host.create_vm(&cfg).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
373
lnvps_api/src/host/mod.rs
Normal file
373
lnvps_api/src/host/mod.rs
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
use crate::settings::ProvisionerConfig;
|
||||||
|
use crate::status::VmState;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use futures::future::join_all;
|
||||||
|
use lnvps_db::{
|
||||||
|
async_trait, IpRange, LNVpsDb, UserSshKey, Vm, VmCustomTemplate, VmHost, VmHostDisk,
|
||||||
|
VmHostKind, VmIpAssignment, VmOsImage, VmTemplate,
|
||||||
|
};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
|
||||||
|
#[cfg(feature = "libvirt")]
|
||||||
|
mod libvirt;
|
||||||
|
#[cfg(feature = "proxmox")]
|
||||||
|
mod proxmox;
|
||||||
|
|
||||||
|
pub struct TerminalStream {
|
||||||
|
pub rx: Receiver<Vec<u8>>,
|
||||||
|
pub tx: Sender<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic type for creating VM's
|
||||||
|
#[async_trait]
|
||||||
|
pub trait VmHostClient: Send + Sync {
|
||||||
|
async fn get_info(&self) -> Result<VmHostInfo>;
|
||||||
|
|
||||||
|
/// Download OS image to the host
|
||||||
|
async fn download_os_image(&self, image: &VmOsImage) -> Result<()>;
|
||||||
|
|
||||||
|
/// Create a random MAC address for the NIC
|
||||||
|
async fn generate_mac(&self, vm: &Vm) -> Result<String>;
|
||||||
|
|
||||||
|
/// Start a VM
|
||||||
|
async fn start_vm(&self, vm: &Vm) -> Result<()>;
|
||||||
|
|
||||||
|
/// Stop a VM
|
||||||
|
async fn stop_vm(&self, vm: &Vm) -> Result<()>;
|
||||||
|
|
||||||
|
/// Reset VM (Hard)
|
||||||
|
async fn reset_vm(&self, vm: &Vm) -> Result<()>;
|
||||||
|
|
||||||
|
/// Spawn a VM
|
||||||
|
async fn create_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
||||||
|
|
||||||
|
/// Delete a VM
|
||||||
|
async fn delete_vm(&self, vm: &Vm) -> Result<()>;
|
||||||
|
|
||||||
|
/// Re-install a vm OS
|
||||||
|
async fn reinstall_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get the running status of a VM
|
||||||
|
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState>;
|
||||||
|
|
||||||
|
/// Apply vm configuration (patch)
|
||||||
|
async fn configure_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get resource usage data
|
||||||
|
async fn get_time_series_data(
|
||||||
|
&self,
|
||||||
|
vm: &Vm,
|
||||||
|
series: TimeSeries,
|
||||||
|
) -> Result<Vec<TimeSeriesData>>;
|
||||||
|
|
||||||
|
/// Connect to terminal serial port
|
||||||
|
async fn connect_terminal(&self, vm: &Vm) -> Result<TerminalStream>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result<Arc<dyn VmHostClient>> {
|
||||||
|
#[cfg(test)]
|
||||||
|
return Ok(Arc::new(crate::mocks::MockVmHost::new()));
|
||||||
|
|
||||||
|
Ok(match host.kind.clone() {
|
||||||
|
#[cfg(feature = "proxmox")]
|
||||||
|
VmHostKind::Proxmox if cfg.proxmox.is_some() => {
|
||||||
|
let cfg = cfg.proxmox.clone().unwrap();
|
||||||
|
Arc::new(proxmox::ProxmoxClient::new(
|
||||||
|
host.ip.parse()?,
|
||||||
|
&host.name,
|
||||||
|
&host.api_token,
|
||||||
|
cfg.mac_prefix,
|
||||||
|
cfg.qemu,
|
||||||
|
cfg.ssh,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "libvirt")]
|
||||||
|
VmHostKind::LibVirt if cfg.libvirt.is_some() => {
|
||||||
|
let cfg = cfg.libvirt.clone().unwrap();
|
||||||
|
Arc::new(libvirt::LibVirtHost::new(&host.ip, cfg.qemu)?)
|
||||||
|
}
|
||||||
|
_ => bail!("Unknown host config: {}", host.kind),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All VM info necessary to provision a VM and its associated resources
|
||||||
|
pub struct FullVmInfo {
|
||||||
|
/// Instance to create
|
||||||
|
pub vm: Vm,
|
||||||
|
/// Host where the VM will be spawned
|
||||||
|
pub host: VmHost,
|
||||||
|
/// Disk where this VM will be saved on the host
|
||||||
|
pub disk: VmHostDisk,
|
||||||
|
/// VM template resources
|
||||||
|
pub template: Option<VmTemplate>,
|
||||||
|
/// VM custom template resources
|
||||||
|
pub custom_template: Option<VmCustomTemplate>,
|
||||||
|
/// The OS image used to create the VM
|
||||||
|
pub image: VmOsImage,
|
||||||
|
/// List of IP resources assigned to this VM
|
||||||
|
pub ips: Vec<VmIpAssignment>,
|
||||||
|
/// Ranges associated with [ips]
|
||||||
|
pub ranges: Vec<IpRange>,
|
||||||
|
/// SSH key to access the VM
|
||||||
|
pub ssh_key: UserSshKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FullVmInfo {
|
||||||
|
pub async fn load(vm_id: u64, db: Arc<dyn LNVpsDb>) -> Result<Self> {
|
||||||
|
let vm = db.get_vm(vm_id).await?;
|
||||||
|
let host = db.get_host(vm.host_id).await?;
|
||||||
|
let image = db.get_os_image(vm.image_id).await?;
|
||||||
|
let disk = db.get_host_disk(vm.disk_id).await?;
|
||||||
|
let ssh_key = db.get_user_ssh_key(vm.ssh_key_id).await?;
|
||||||
|
let ips = db.list_vm_ip_assignments(vm_id).await?;
|
||||||
|
|
||||||
|
let ip_range_ids: HashSet<u64> = ips.iter().map(|i| i.ip_range_id).collect();
|
||||||
|
let ip_ranges: Vec<_> = ip_range_ids.iter().map(|i| db.get_ip_range(*i)).collect();
|
||||||
|
let ranges: Vec<IpRange> = join_all(ip_ranges)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let template = if let Some(t) = vm.template_id {
|
||||||
|
Some(db.get_vm_template(t).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let custom_template = if let Some(t) = vm.custom_template_id {
|
||||||
|
Some(db.get_custom_vm_template(t).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
// create VM
|
||||||
|
Ok(FullVmInfo {
|
||||||
|
vm,
|
||||||
|
host,
|
||||||
|
template,
|
||||||
|
custom_template,
|
||||||
|
image,
|
||||||
|
ips,
|
||||||
|
disk,
|
||||||
|
ranges,
|
||||||
|
ssh_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CPU cores
|
||||||
|
pub fn resources(&self) -> Result<VmResources> {
|
||||||
|
if let Some(t) = &self.template {
|
||||||
|
Ok(VmResources {
|
||||||
|
cpu: t.cpu,
|
||||||
|
memory: t.memory,
|
||||||
|
disk_size: t.disk_size,
|
||||||
|
})
|
||||||
|
} else if let Some(t) = &self.custom_template {
|
||||||
|
Ok(VmResources {
|
||||||
|
cpu: t.cpu,
|
||||||
|
memory: t.memory,
|
||||||
|
disk_size: t.disk_size,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
bail!("Invalid VM config, no template");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct VmResources {
|
||||||
|
pub cpu: u16,
|
||||||
|
pub memory: u64,
|
||||||
|
pub disk_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct TimeSeriesData {
|
||||||
|
pub timestamp: u64,
|
||||||
|
pub cpu: f32,
|
||||||
|
pub memory: f32,
|
||||||
|
pub memory_size: u64,
|
||||||
|
pub net_in: f32,
|
||||||
|
pub net_out: f32,
|
||||||
|
pub disk_write: f32,
|
||||||
|
pub disk_read: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TimeSeries {
|
||||||
|
Hourly,
|
||||||
|
Daily,
|
||||||
|
Weekly,
|
||||||
|
Monthly,
|
||||||
|
Yearly,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VmHostInfo {
|
||||||
|
pub cpu: u16,
|
||||||
|
pub memory: u64,
|
||||||
|
pub disks: Vec<VmHostDiskInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VmHostDiskInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub used: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::host::FullVmInfo;
|
||||||
|
use crate::{GB, TB};
|
||||||
|
use chrono::Utc;
|
||||||
|
use lnvps_db::{
|
||||||
|
DiskInterface, DiskType, IpRange, IpRangeAllocationMode, OsDistribution, UserSshKey, Vm,
|
||||||
|
VmHost, VmHostDisk, VmIpAssignment, VmOsImage, VmTemplate,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn mock_full_vm() -> FullVmInfo {
|
||||||
|
let template = VmTemplate {
|
||||||
|
id: 1,
|
||||||
|
name: "example".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
created: Default::default(),
|
||||||
|
expires: None,
|
||||||
|
cpu: 2,
|
||||||
|
memory: 2 * GB,
|
||||||
|
disk_size: 100 * GB,
|
||||||
|
disk_type: DiskType::SSD,
|
||||||
|
disk_interface: DiskInterface::PCIe,
|
||||||
|
cost_plan_id: 1,
|
||||||
|
region_id: 1,
|
||||||
|
};
|
||||||
|
FullVmInfo {
|
||||||
|
vm: Vm {
|
||||||
|
id: 1,
|
||||||
|
host_id: 1,
|
||||||
|
user_id: 1,
|
||||||
|
image_id: 1,
|
||||||
|
template_id: Some(template.id),
|
||||||
|
custom_template_id: None,
|
||||||
|
ssh_key_id: 1,
|
||||||
|
created: Default::default(),
|
||||||
|
expires: Default::default(),
|
||||||
|
disk_id: 1,
|
||||||
|
mac_address: "ff:ff:ff:ff:ff:fe".to_string(),
|
||||||
|
deleted: false,
|
||||||
|
ref_code: None,
|
||||||
|
},
|
||||||
|
host: VmHost {
|
||||||
|
id: 1,
|
||||||
|
kind: Default::default(),
|
||||||
|
region_id: 1,
|
||||||
|
name: "mock".to_string(),
|
||||||
|
ip: "https://localhost:8006".to_string(),
|
||||||
|
cpu: 20,
|
||||||
|
memory: 128 * GB,
|
||||||
|
enabled: true,
|
||||||
|
api_token: "mock".to_string(),
|
||||||
|
load_cpu: 1.0,
|
||||||
|
load_memory: 1.0,
|
||||||
|
load_disk: 1.0,
|
||||||
|
vlan_id: Some(100),
|
||||||
|
},
|
||||||
|
disk: VmHostDisk {
|
||||||
|
id: 1,
|
||||||
|
host_id: 1,
|
||||||
|
name: "ssd".to_string(),
|
||||||
|
size: TB * 20,
|
||||||
|
kind: DiskType::SSD,
|
||||||
|
interface: DiskInterface::PCIe,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
template: Some(template.clone()),
|
||||||
|
custom_template: None,
|
||||||
|
image: VmOsImage {
|
||||||
|
id: 1,
|
||||||
|
distribution: OsDistribution::Ubuntu,
|
||||||
|
flavour: "Server".to_string(),
|
||||||
|
version: "24.04.03".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
release_date: Utc::now(),
|
||||||
|
url: "http://localhost.com/ubuntu_server_24.04.img".to_string(),
|
||||||
|
default_username: None,
|
||||||
|
},
|
||||||
|
ips: vec![
|
||||||
|
VmIpAssignment {
|
||||||
|
id: 1,
|
||||||
|
vm_id: 1,
|
||||||
|
ip_range_id: 1,
|
||||||
|
ip: "192.168.1.2".to_string(),
|
||||||
|
deleted: false,
|
||||||
|
arp_ref: None,
|
||||||
|
dns_forward: None,
|
||||||
|
dns_forward_ref: None,
|
||||||
|
dns_reverse: None,
|
||||||
|
dns_reverse_ref: None,
|
||||||
|
},
|
||||||
|
VmIpAssignment {
|
||||||
|
id: 2,
|
||||||
|
vm_id: 1,
|
||||||
|
ip_range_id: 2,
|
||||||
|
ip: "192.168.2.2".to_string(),
|
||||||
|
deleted: false,
|
||||||
|
arp_ref: None,
|
||||||
|
dns_forward: None,
|
||||||
|
dns_forward_ref: None,
|
||||||
|
dns_reverse: None,
|
||||||
|
dns_reverse_ref: None,
|
||||||
|
},
|
||||||
|
VmIpAssignment {
|
||||||
|
id: 3,
|
||||||
|
vm_id: 1,
|
||||||
|
ip_range_id: 3,
|
||||||
|
ip: "fd00::ff:ff:ff:ff:ff".to_string(),
|
||||||
|
deleted: false,
|
||||||
|
arp_ref: None,
|
||||||
|
dns_forward: None,
|
||||||
|
dns_forward_ref: None,
|
||||||
|
dns_reverse: None,
|
||||||
|
dns_reverse_ref: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ranges: vec![
|
||||||
|
IpRange {
|
||||||
|
id: 1,
|
||||||
|
cidr: "192.168.1.0/24".to_string(),
|
||||||
|
gateway: "192.168.1.1/16".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
region_id: 1,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
IpRange {
|
||||||
|
id: 2,
|
||||||
|
cidr: "192.168.2.0/24".to_string(),
|
||||||
|
gateway: "10.10.10.10".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
region_id: 2,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
IpRange {
|
||||||
|
id: 3,
|
||||||
|
cidr: "fd00::/64".to_string(),
|
||||||
|
gateway: "fd00::1".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
region_id: 1,
|
||||||
|
allocation_mode: IpRangeAllocationMode::SlaacEui64,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ssh_key: UserSshKey {
|
||||||
|
id: 1,
|
||||||
|
name: "test".to_string(),
|
||||||
|
user_id: 1,
|
||||||
|
created: Default::default(),
|
||||||
|
key_data: "ssh-ed25519 AAA=".to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ use crate::json_api::JsonApi;
|
|||||||
use crate::settings::{QemuConfig, SshConfig};
|
use crate::settings::{QemuConfig, SshConfig};
|
||||||
use crate::ssh_client::SshClient;
|
use crate::ssh_client::SshClient;
|
||||||
use crate::status::{VmRunningState, VmState};
|
use crate::status::{VmRunningState, VmState};
|
||||||
use anyhow::{anyhow, bail, ensure, Result};
|
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
@ -500,14 +500,25 @@ impl VmHostClient for ProxmoxClient {
|
|||||||
if let Some(n) = nodes.iter().find(|n| n.name == self.node) {
|
if let Some(n) = nodes.iter().find(|n| n.name == self.node) {
|
||||||
let storages = self.list_storage(&n.name).await?;
|
let storages = self.list_storage(&n.name).await?;
|
||||||
let info = VmHostInfo {
|
let info = VmHostInfo {
|
||||||
cpu: n.max_cpu.unwrap_or(0),
|
cpu: n.max_cpu
|
||||||
memory: n.max_mem.unwrap_or(0),
|
.context("Missing cpu count, please make sure you have Sys.Audit permission")?,
|
||||||
|
memory: n.max_mem
|
||||||
|
.context("Missing memory size, please make sure you have Sys.Audit permission")?,
|
||||||
disks: storages
|
disks: storages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| VmHostDiskInfo {
|
.filter_map(|s| {
|
||||||
|
let size = s.total
|
||||||
|
.context("Missing disk size, please make sure you have Datastore.Audit permission")
|
||||||
|
.ok()?;
|
||||||
|
let used = s.used
|
||||||
|
.context("Missing used disk, please make sure you have Datastore.Audit permission")
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
Some(VmHostDiskInfo {
|
||||||
name: s.storage,
|
name: s.storage,
|
||||||
size: s.total.unwrap_or(0),
|
size,
|
||||||
used: s.used.unwrap_or(0),
|
used,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
@ -600,6 +611,32 @@ impl VmHostClient for ProxmoxClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_vm(&self, vm: &Vm) -> Result<()> {
|
||||||
|
let vm_id: ProxmoxVmId = vm.id.into();
|
||||||
|
|
||||||
|
// NOT IMPLEMENTED
|
||||||
|
//let t = self.delete_vm(&self.node, vm_id).await?;
|
||||||
|
//self.wait_for_task(&t).await?;
|
||||||
|
|
||||||
|
if let Some(ssh) = &self.ssh {
|
||||||
|
let mut ses = SshClient::new()?;
|
||||||
|
ses.connect(
|
||||||
|
(self.api.base().host().unwrap().to_string(), 22),
|
||||||
|
&ssh.user,
|
||||||
|
&ssh.key,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let cmd = format!("/usr/sbin/qm destroy {}", vm_id,);
|
||||||
|
let (code, rsp) = ses.execute(cmd.as_str()).await?;
|
||||||
|
info!("{}", rsp);
|
||||||
|
if code != 0 {
|
||||||
|
bail!("Failed to destroy vm, exit-code {}, {}", code, rsp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn reinstall_vm(&self, req: &FullVmInfo) -> Result<()> {
|
async fn reinstall_vm(&self, req: &FullVmInfo) -> Result<()> {
|
||||||
let vm_id = req.vm.id.into();
|
let vm_id = req.vm.id.into();
|
||||||
|
|
||||||
@ -1115,152 +1152,13 @@ impl From<RrdDataPoint> for TimeSeriesData {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{GB, MB, TB};
|
use crate::host::tests::mock_full_vm;
|
||||||
use lnvps_db::{
|
use crate::MB;
|
||||||
DiskInterface, IpRange, IpRangeAllocationMode, OsDistribution, UserSshKey, VmHost,
|
|
||||||
VmHostDisk, VmIpAssignment, VmTemplate,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config() -> Result<()> {
|
fn test_config() -> Result<()> {
|
||||||
let template = VmTemplate {
|
let cfg = mock_full_vm();
|
||||||
id: 1,
|
let template = cfg.template.clone().unwrap();
|
||||||
name: "example".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
created: Default::default(),
|
|
||||||
expires: None,
|
|
||||||
cpu: 2,
|
|
||||||
memory: 2 * GB,
|
|
||||||
disk_size: 100 * GB,
|
|
||||||
disk_type: DiskType::SSD,
|
|
||||||
disk_interface: DiskInterface::PCIe,
|
|
||||||
cost_plan_id: 1,
|
|
||||||
region_id: 1,
|
|
||||||
};
|
|
||||||
let cfg = FullVmInfo {
|
|
||||||
vm: Vm {
|
|
||||||
id: 1,
|
|
||||||
host_id: 1,
|
|
||||||
user_id: 1,
|
|
||||||
image_id: 1,
|
|
||||||
template_id: Some(template.id),
|
|
||||||
custom_template_id: None,
|
|
||||||
ssh_key_id: 1,
|
|
||||||
created: Default::default(),
|
|
||||||
expires: Default::default(),
|
|
||||||
disk_id: 1,
|
|
||||||
mac_address: "ff:ff:ff:ff:ff:fe".to_string(),
|
|
||||||
deleted: false,
|
|
||||||
ref_code: None,
|
|
||||||
},
|
|
||||||
host: VmHost {
|
|
||||||
id: 1,
|
|
||||||
kind: Default::default(),
|
|
||||||
region_id: 1,
|
|
||||||
name: "mock".to_string(),
|
|
||||||
ip: "https://localhost:8006".to_string(),
|
|
||||||
cpu: 20,
|
|
||||||
memory: 128 * GB,
|
|
||||||
enabled: true,
|
|
||||||
api_token: "mock".to_string(),
|
|
||||||
load_cpu: 1.0,
|
|
||||||
load_memory: 1.0,
|
|
||||||
load_disk: 1.0,
|
|
||||||
vlan_id: Some(100),
|
|
||||||
},
|
|
||||||
disk: VmHostDisk {
|
|
||||||
id: 1,
|
|
||||||
host_id: 1,
|
|
||||||
name: "ssd".to_string(),
|
|
||||||
size: TB * 20,
|
|
||||||
kind: DiskType::SSD,
|
|
||||||
interface: DiskInterface::PCIe,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
template: Some(template.clone()),
|
|
||||||
custom_template: None,
|
|
||||||
image: VmOsImage {
|
|
||||||
id: 1,
|
|
||||||
distribution: OsDistribution::Ubuntu,
|
|
||||||
flavour: "Server".to_string(),
|
|
||||||
version: "24.04.03".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
release_date: Utc::now(),
|
|
||||||
url: "http://localhost.com/ubuntu_server_24.04.img".to_string(),
|
|
||||||
},
|
|
||||||
ips: vec![
|
|
||||||
VmIpAssignment {
|
|
||||||
id: 1,
|
|
||||||
vm_id: 1,
|
|
||||||
ip_range_id: 1,
|
|
||||||
ip: "192.168.1.2".to_string(),
|
|
||||||
deleted: false,
|
|
||||||
arp_ref: None,
|
|
||||||
dns_forward: None,
|
|
||||||
dns_forward_ref: None,
|
|
||||||
dns_reverse: None,
|
|
||||||
dns_reverse_ref: None,
|
|
||||||
},
|
|
||||||
VmIpAssignment {
|
|
||||||
id: 2,
|
|
||||||
vm_id: 1,
|
|
||||||
ip_range_id: 2,
|
|
||||||
ip: "192.168.2.2".to_string(),
|
|
||||||
deleted: false,
|
|
||||||
arp_ref: None,
|
|
||||||
dns_forward: None,
|
|
||||||
dns_forward_ref: None,
|
|
||||||
dns_reverse: None,
|
|
||||||
dns_reverse_ref: None,
|
|
||||||
},
|
|
||||||
VmIpAssignment {
|
|
||||||
id: 3,
|
|
||||||
vm_id: 1,
|
|
||||||
ip_range_id: 3,
|
|
||||||
ip: "fd00::ff:ff:ff:ff:ff".to_string(),
|
|
||||||
deleted: false,
|
|
||||||
arp_ref: None,
|
|
||||||
dns_forward: None,
|
|
||||||
dns_forward_ref: None,
|
|
||||||
dns_reverse: None,
|
|
||||||
dns_reverse_ref: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
ranges: vec![
|
|
||||||
IpRange {
|
|
||||||
id: 1,
|
|
||||||
cidr: "192.168.1.0/24".to_string(),
|
|
||||||
gateway: "192.168.1.1/16".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
region_id: 1,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
IpRange {
|
|
||||||
id: 2,
|
|
||||||
cidr: "192.168.2.0/24".to_string(),
|
|
||||||
gateway: "10.10.10.10".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
region_id: 2,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
IpRange {
|
|
||||||
id: 3,
|
|
||||||
cidr: "fd00::/64".to_string(),
|
|
||||||
gateway: "fd00::1".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
region_id: 1,
|
|
||||||
allocation_mode: IpRangeAllocationMode::SlaacEui64,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
],
|
|
||||||
ssh_key: UserSshKey {
|
|
||||||
id: 1,
|
|
||||||
name: "test".to_string(),
|
|
||||||
user_id: 1,
|
|
||||||
created: Default::default(),
|
|
||||||
key_data: "ssh-ed25519 AAA=".to_string(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let q_cfg = QemuConfig {
|
let q_cfg = QemuConfig {
|
||||||
machine: "q35".to_string(),
|
machine: "q35".to_string(),
|
||||||
@ -1268,6 +1166,7 @@ mod tests {
|
|||||||
bridge: "vmbr1".to_string(),
|
bridge: "vmbr1".to_string(),
|
||||||
cpu: "kvm64".to_string(),
|
cpu: "kvm64".to_string(),
|
||||||
kvm: true,
|
kvm: true,
|
||||||
|
arch: "x86_64".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let p = ProxmoxClient::new(
|
let p = ProxmoxClient::new(
|
@ -1,5 +1,4 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod cors;
|
|
||||||
pub mod data_migration;
|
pub mod data_migration;
|
||||||
pub mod dns;
|
pub mod dns;
|
||||||
pub mod exchange;
|
pub mod exchange;
|
@ -1,14 +1,16 @@
|
|||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
use crate::dns::{BasicRecord, DnsServer, RecordType};
|
use crate::dns::{BasicRecord, DnsServer, RecordType};
|
||||||
use crate::exchange::{ExchangeRateService, Ticker, TickerRate};
|
use crate::exchange::{ExchangeRateService, Ticker, TickerRate};
|
||||||
use crate::host::{FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient, VmHostInfo};
|
use crate::host::{
|
||||||
|
FullVmInfo, TerminalStream, TimeSeries, TimeSeriesData, VmHostClient, VmHostInfo,
|
||||||
|
};
|
||||||
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
||||||
use crate::router::{ArpEntry, Router};
|
use crate::router::{ArpEntry, Router};
|
||||||
use crate::status::{VmRunningState, VmState};
|
use crate::status::{VmRunningState, VmState};
|
||||||
use anyhow::{anyhow, bail, ensure, Context};
|
use anyhow::{anyhow, bail, ensure, Context};
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream;
|
use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream;
|
||||||
use lnvps_db::{async_trait, AccessPolicy, DiskInterface, DiskType, IpRange, IpRangeAllocationMode, LNVpsDb, OsDistribution, User, UserSshKey, Vm, VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate};
|
use lnvps_db::{async_trait, AccessPolicy, Company, DiskInterface, DiskType, IpRange, IpRangeAllocationMode, LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, OsDistribution, User, UserSshKey, Vm, VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@ -101,6 +103,7 @@ impl Default for MockDb {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: "Mock".to_string(),
|
name: "Mock".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
company_id: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let mut ip_ranges = HashMap::new();
|
let mut ip_ranges = HashMap::new();
|
||||||
@ -174,6 +177,7 @@ impl Default for MockDb {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
release_date: Utc::now(),
|
release_date: Utc::now(),
|
||||||
url: "https://example.com/debian_12.img".to_string(),
|
url: "https://example.com/debian_12.img".to_string(),
|
||||||
|
default_username: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Self {
|
Self {
|
||||||
@ -198,6 +202,57 @@ impl Default for MockDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl LNVPSNostrDb for MockDb {
|
||||||
|
async fn get_handle(&self, handle_id: u64) -> anyhow::Result<NostrDomainHandle> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_handle_by_name(
|
||||||
|
&self,
|
||||||
|
domain_id: u64,
|
||||||
|
handle: &str,
|
||||||
|
) -> anyhow::Result<NostrDomainHandle> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_handle(&self, handle: &NostrDomainHandle) -> anyhow::Result<u64> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_handle(&self, handle: &NostrDomainHandle) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_handle(&self, handle_id: u64) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_handles(&self, domain_id: u64) -> anyhow::Result<Vec<NostrDomainHandle>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_domain(&self, id: u64) -> anyhow::Result<NostrDomain> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_domain_by_name(&self, name: &str) -> anyhow::Result<NostrDomain> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_domains(&self, owner_id: u64) -> anyhow::Result<Vec<NostrDomain>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_domain(&self, domain: &NostrDomain) -> anyhow::Result<u64> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_domain(&self, domain_id: u64) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl LNVpsDb for MockDb {
|
impl LNVpsDb for MockDb {
|
||||||
async fn migrate(&self) -> anyhow::Result<()> {
|
async fn migrate(&self) -> anyhow::Result<()> {
|
||||||
@ -216,10 +271,8 @@ impl LNVpsDb for MockDb {
|
|||||||
id: max + 1,
|
id: max + 1,
|
||||||
pubkey: pubkey.to_vec(),
|
pubkey: pubkey.to_vec(),
|
||||||
created: Utc::now(),
|
created: Utc::now(),
|
||||||
email: None,
|
|
||||||
contact_nip17: false,
|
|
||||||
contact_email: false,
|
|
||||||
country_code: Some("USA".to_string()),
|
country_code: Some("USA".to_string()),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(max + 1)
|
Ok(max + 1)
|
||||||
@ -475,6 +528,7 @@ impl LNVpsDb for MockDb {
|
|||||||
let mut vms = self.vms.lock().await;
|
let mut vms = self.vms.lock().await;
|
||||||
if let Some(v) = vms.get_mut(&vm.id) {
|
if let Some(v) = vms.get_mut(&vm.id) {
|
||||||
v.ssh_key_id = vm.ssh_key_id;
|
v.ssh_key_id = vm.ssh_key_id;
|
||||||
|
v.mac_address = vm.mac_address.clone();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -637,6 +691,10 @@ impl LNVpsDb for MockDb {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.context("no access policy")?)
|
.context("no access policy")?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_company(&self, company_id: u64) -> anyhow::Result<Company> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -826,6 +884,12 @@ impl VmHostClient for MockVmHost {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_vm(&self, vm: &Vm) -> anyhow::Result<()> {
|
||||||
|
let mut vms = self.vms.lock().await;
|
||||||
|
vms.remove(&vm.id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn reinstall_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> {
|
async fn reinstall_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
@ -896,7 +960,10 @@ impl DnsServer for MockDnsServer {
|
|||||||
zones.get_mut(zone_id).unwrap()
|
zones.get_mut(zone_id).unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
if table.values().any(|v| v.name == record.name && v.kind == record.kind.to_string()) {
|
if table
|
||||||
|
.values()
|
||||||
|
.any(|v| v.name == record.name && v.kind == record.kind.to_string())
|
||||||
|
{
|
||||||
bail!("Duplicate record with name {}", record.name);
|
bail!("Duplicate record with name {}", record.name);
|
||||||
}
|
}
|
||||||
|
|
@ -304,9 +304,6 @@ pub struct IPRangeCapacity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IPRangeCapacity {
|
impl IPRangeCapacity {
|
||||||
// first/last/gw
|
|
||||||
const RESERVED: u128 = 3;
|
|
||||||
|
|
||||||
/// Total number of IPs free
|
/// Total number of IPs free
|
||||||
pub fn available_capacity(&self) -> u128 {
|
pub fn available_capacity(&self) -> u128 {
|
||||||
let net: IpNetwork = self.range.cidr.parse().unwrap();
|
let net: IpNetwork = self.range.cidr.parse().unwrap();
|
||||||
@ -315,7 +312,11 @@ impl IPRangeCapacity {
|
|||||||
NetworkSize::V4(s) => (s as u128).saturating_sub(self.usage),
|
NetworkSize::V4(s) => (s as u128).saturating_sub(self.usage),
|
||||||
NetworkSize::V6(s) => s.saturating_sub(self.usage),
|
NetworkSize::V6(s) => s.saturating_sub(self.usage),
|
||||||
}
|
}
|
||||||
.saturating_sub(Self::RESERVED)
|
.saturating_sub(if self.range.use_full_range {
|
||||||
|
1 // gw
|
||||||
|
} else {
|
||||||
|
3 // first/last/gw
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,22 +286,6 @@ impl LNVpsProvisioner {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assign_available_v4_to_vm(
|
|
||||||
&self,
|
|
||||||
vm: &Vm,
|
|
||||||
v4: &AvailableIp,
|
|
||||||
) -> Result<VmIpAssignment> {
|
|
||||||
let mut assignment = VmIpAssignment {
|
|
||||||
vm_id: vm.id,
|
|
||||||
ip_range_id: v4.range_id,
|
|
||||||
ip: v4.ip.ip().to_string(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
self.save_ip_assignment(&mut assignment).await?;
|
|
||||||
Ok(assignment)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn assign_available_v6_to_vm(
|
pub async fn assign_available_v6_to_vm(
|
||||||
&self,
|
&self,
|
||||||
vm: &Vm,
|
vm: &Vm,
|
||||||
@ -342,7 +326,12 @@ impl LNVpsProvisioner {
|
|||||||
let mut assignments = vec![];
|
let mut assignments = vec![];
|
||||||
match ip.ip4 {
|
match ip.ip4 {
|
||||||
Some(v4) => {
|
Some(v4) => {
|
||||||
let mut assignment = self.assign_available_v4_to_vm(&vm, &v4).await?;
|
let mut assignment = VmIpAssignment {
|
||||||
|
vm_id: vm.id,
|
||||||
|
ip_range_id: v4.range_id,
|
||||||
|
ip: v4.ip.ip().to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
//generate mac address from ip assignment
|
//generate mac address from ip assignment
|
||||||
let mac = self.get_mac_for_assignment(&host, &vm, &assignment).await?;
|
let mac = self.get_mac_for_assignment(&host, &vm, &assignment).await?;
|
||||||
@ -350,6 +339,7 @@ impl LNVpsProvisioner {
|
|||||||
assignment.arp_ref = mac.id; // store ref if we got one
|
assignment.arp_ref = mac.id; // store ref if we got one
|
||||||
self.db.update_vm(&vm).await?;
|
self.db.update_vm(&vm).await?;
|
||||||
|
|
||||||
|
self.save_ip_assignment(&mut assignment).await?;
|
||||||
assignments.push(assignment);
|
assignments.push(assignment);
|
||||||
}
|
}
|
||||||
/// TODO: add expected number of IPS per templates
|
/// TODO: add expected number of IPS per templates
|
||||||
@ -469,6 +459,7 @@ impl LNVpsProvisioner {
|
|||||||
// insert custom templates
|
// insert custom templates
|
||||||
let template_id = self.db.insert_custom_vm_template(&template).await?;
|
let template_id = self.db.insert_custom_vm_template(&template).await?;
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
let mut new_vm = Vm {
|
let mut new_vm = Vm {
|
||||||
id: 0,
|
id: 0,
|
||||||
host_id: host.host.id,
|
host_id: host.host.id,
|
||||||
@ -477,8 +468,8 @@ impl LNVpsProvisioner {
|
|||||||
template_id: None,
|
template_id: None,
|
||||||
custom_template_id: Some(template_id),
|
custom_template_id: Some(template_id),
|
||||||
ssh_key_id: ssh_key.id,
|
ssh_key_id: ssh_key.id,
|
||||||
created: Utc::now(),
|
created: now,
|
||||||
expires: Utc::now(),
|
expires: now,
|
||||||
disk_id: pick_disk.disk.id,
|
disk_id: pick_disk.disk.id,
|
||||||
mac_address: "ff:ff:ff:ff:ff:ff".to_string(),
|
mac_address: "ff:ff:ff:ff:ff:ff".to_string(),
|
||||||
deleted: false,
|
deleted: false,
|
||||||
@ -493,8 +484,23 @@ impl LNVpsProvisioner {
|
|||||||
/// Create a renewal payment
|
/// Create a renewal payment
|
||||||
pub async fn renew(&self, vm_id: u64, method: PaymentMethod) -> Result<VmPayment> {
|
pub async fn renew(&self, vm_id: u64, method: PaymentMethod) -> Result<VmPayment> {
|
||||||
let pe = PricingEngine::new(self.db.clone(), self.rates.clone(), self.tax_rates.clone());
|
let pe = PricingEngine::new(self.db.clone(), self.rates.clone(), self.tax_rates.clone());
|
||||||
|
|
||||||
let price = pe.get_vm_cost(vm_id, method).await?;
|
let price = pe.get_vm_cost(vm_id, method).await?;
|
||||||
|
self.price_to_payment(vm_id, method, price).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renew a VM using a specific amount
|
||||||
|
pub async fn renew_amount(&self, vm_id: u64, amount: CurrencyAmount, method: PaymentMethod) -> Result<VmPayment> {
|
||||||
|
let pe = PricingEngine::new(self.db.clone(), self.rates.clone(), self.tax_rates.clone());
|
||||||
|
let price = pe.get_cost_by_amount(vm_id, amount, method).await?;
|
||||||
|
self.price_to_payment(vm_id, method, price).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn price_to_payment(
|
||||||
|
&self,
|
||||||
|
vm_id: u64,
|
||||||
|
method: PaymentMethod,
|
||||||
|
price: CostResult,
|
||||||
|
) -> Result<VmPayment> {
|
||||||
match price {
|
match price {
|
||||||
CostResult::Existing(p) => Ok(p),
|
CostResult::Existing(p) => Ok(p),
|
||||||
CostResult::New {
|
CostResult::New {
|
||||||
@ -603,8 +609,13 @@ impl LNVpsProvisioner {
|
|||||||
|
|
||||||
/// Delete a VM and its associated resources
|
/// Delete a VM and its associated resources
|
||||||
pub async fn delete_vm(&self, vm_id: u64) -> Result<()> {
|
pub async fn delete_vm(&self, vm_id: u64) -> Result<()> {
|
||||||
// host client currently doesn't support delete (proxmox)
|
let vm = self.db.get_vm(vm_id).await?;
|
||||||
// VM should already be stopped by [Worker]
|
let host = self.db.get_host(vm.host_id).await?;
|
||||||
|
|
||||||
|
let client = get_host_client(&host, &self.provisioner_config)?;
|
||||||
|
if let Err(e) = client.delete_vm(&vm).await {
|
||||||
|
warn!("Failed to delete VM: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
self.delete_ip_assignments(vm_id).await?;
|
self.delete_ip_assignments(vm_id).await?;
|
||||||
self.db.delete_vm(vm_id).await?;
|
self.db.delete_vm(vm_id).await?;
|
||||||
@ -723,6 +734,7 @@ mod tests {
|
|||||||
// spawn vm
|
// spawn vm
|
||||||
provisioner.spawn_vm(vm.id).await?;
|
provisioner.spawn_vm(vm.id).await?;
|
||||||
|
|
||||||
|
let vm = db.get_vm(vm.id).await?;
|
||||||
// check resources
|
// check resources
|
||||||
let router = MockRouter::new();
|
let router = MockRouter::new();
|
||||||
let arp = router.list_arp_entry().await?;
|
let arp = router.list_arp_entry().await?;
|
||||||
@ -771,7 +783,12 @@ mod tests {
|
|||||||
assert_eq!(zones.get("mock-v6-rev-zone-id").unwrap().len(), 1);
|
assert_eq!(zones.get("mock-v6-rev-zone-id").unwrap().len(), 1);
|
||||||
assert_eq!(zones.get("mock-forward-zone-id").unwrap().len(), 2);
|
assert_eq!(zones.get("mock-forward-zone-id").unwrap().len(), 2);
|
||||||
|
|
||||||
let v6 = zones.get("mock-v6-rev-zone-id").unwrap().iter().next().unwrap();
|
let v6 = zones
|
||||||
|
.get("mock-v6-rev-zone-id")
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
assert_eq!(v6.1.kind, "PTR");
|
assert_eq!(v6.1.kind, "PTR");
|
||||||
assert!(v6.1.name.ends_with("0.0.d.f.ip6.arpa"));
|
assert!(v6.1.name.ends_with("0.0.d.f.ip6.arpa"));
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
use crate::exchange::{Currency, CurrencyAmount, ExchangeRateService, Ticker, TickerRate};
|
use crate::exchange::{Currency, CurrencyAmount, ExchangeRateService, Ticker, TickerRate};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use chrono::{DateTime, Days, Months, TimeDelta, Utc};
|
use chrono::{DateTime, Days, Months, TimeDelta, Utc};
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use isocountry::CountryCode;
|
use isocountry::CountryCode;
|
||||||
@ -34,6 +34,49 @@ impl PricingEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get amount of time a certain currency amount will extend a vm in seconds
|
||||||
|
pub async fn get_cost_by_amount(
|
||||||
|
&self,
|
||||||
|
vm_id: u64,
|
||||||
|
input: CurrencyAmount,
|
||||||
|
method: PaymentMethod,
|
||||||
|
) -> Result<CostResult> {
|
||||||
|
let vm = self.db.get_vm(vm_id).await?;
|
||||||
|
|
||||||
|
let cost = if vm.template_id.is_some() {
|
||||||
|
self.get_template_vm_cost(&vm, method).await?
|
||||||
|
} else {
|
||||||
|
self.get_custom_vm_cost(&vm, method).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
match cost {
|
||||||
|
CostResult::Existing(_) => bail!("Invalid response"),
|
||||||
|
CostResult::New {
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
rate,
|
||||||
|
time_value,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
ensure!(currency == input.currency(), "Invalid currency");
|
||||||
|
|
||||||
|
// scale cost
|
||||||
|
let scale = input.value() as f64 / amount as f64;
|
||||||
|
let new_time = (time_value as f64 * scale).floor() as u64;
|
||||||
|
ensure!(new_time > 0, "Extend time is less than 1 second");
|
||||||
|
|
||||||
|
Ok(CostResult::New {
|
||||||
|
amount: input.value(),
|
||||||
|
currency,
|
||||||
|
time_value: new_time,
|
||||||
|
new_expiry: vm.expires.add(TimeDelta::seconds(new_time as i64)),
|
||||||
|
rate,
|
||||||
|
tax: self.get_tax_for_user(vm.user_id, input.value()).await?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get VM cost (for renewal)
|
/// Get VM cost (for renewal)
|
||||||
pub async fn get_vm_cost(&self, vm_id: u64, method: PaymentMethod) -> Result<CostResult> {
|
pub async fn get_vm_cost(&self, vm_id: u64, method: PaymentMethod) -> Result<CostResult> {
|
||||||
let vm = self.db.get_vm(vm_id).await?;
|
let vm = self.db.get_vm(vm_id).await?;
|
||||||
@ -160,13 +203,13 @@ impl PricingEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_msats_amount(&self, amount: CurrencyAmount) -> Result<(u64, f32)> {
|
async fn get_msats_amount(&self, amount: CurrencyAmount) -> Result<(u64, f32)> {
|
||||||
let rate = self.get_ticker(amount.0).await?;
|
let rate = self.get_ticker(amount.currency()).await?;
|
||||||
let cost_btc = amount.value_f32() / rate.1;
|
let cost_btc = amount.value_f32() / rate.1;
|
||||||
let cost_msats = (cost_btc as f64 * crate::BTC_SATS) as u64 * 1000;
|
let cost_msats = (cost_btc as f64 * crate::BTC_SATS) as u64 * 1000;
|
||||||
Ok((cost_msats, rate.1))
|
Ok((cost_msats, rate.1))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_template_expire(vm: &Vm, cost_plan: &VmCostPlan) -> u64 {
|
pub fn next_template_expire(vm: &Vm, cost_plan: &VmCostPlan) -> u64 {
|
||||||
let next_expire = match cost_plan.interval_type {
|
let next_expire = match cost_plan.interval_type {
|
||||||
VmCostPlanIntervalType::Day => vm.expires.add(Days::new(cost_plan.interval_amount)),
|
VmCostPlanIntervalType::Day => vm.expires.add(Days::new(cost_plan.interval_amount)),
|
||||||
VmCostPlanIntervalType::Month => vm
|
VmCostPlanIntervalType::Month => vm
|
||||||
@ -209,7 +252,7 @@ impl PricingEngine {
|
|||||||
list_price: CurrencyAmount,
|
list_price: CurrencyAmount,
|
||||||
method: PaymentMethod,
|
method: PaymentMethod,
|
||||||
) -> Result<(Currency, u64, f32)> {
|
) -> Result<(Currency, u64, f32)> {
|
||||||
Ok(match (list_price.0, method) {
|
Ok(match (list_price.currency(), method) {
|
||||||
(c, PaymentMethod::Lightning) if c != Currency::BTC => {
|
(c, PaymentMethod::Lightning) if c != Currency::BTC => {
|
||||||
let new_price = self.get_msats_amount(list_price).await?;
|
let new_price = self.get_msats_amount(list_price).await?;
|
||||||
(Currency::BTC, new_price.0, new_price.1)
|
(Currency::BTC, new_price.0, new_price.1)
|
||||||
@ -351,11 +394,8 @@ mod tests {
|
|||||||
User {
|
User {
|
||||||
id: 1,
|
id: 1,
|
||||||
pubkey: vec![],
|
pubkey: vec![],
|
||||||
created: Default::default(),
|
|
||||||
email: None,
|
|
||||||
contact_nip17: false,
|
|
||||||
contact_email: false,
|
|
||||||
country_code: Some("USA".to_string()),
|
country_code: Some("USA".to_string()),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
u.insert(
|
u.insert(
|
||||||
@ -363,11 +403,8 @@ mod tests {
|
|||||||
User {
|
User {
|
||||||
id: 2,
|
id: 2,
|
||||||
pubkey: vec![],
|
pubkey: vec![],
|
||||||
created: Default::default(),
|
|
||||||
email: None,
|
|
||||||
contact_nip17: false,
|
|
||||||
contact_email: false,
|
|
||||||
country_code: Some("IRL".to_string()),
|
country_code: Some("IRL".to_string()),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -400,6 +437,25 @@ mod tests {
|
|||||||
_ => bail!("??"),
|
_ => bail!("??"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// from amount
|
||||||
|
let price = pe
|
||||||
|
.get_cost_by_amount(1, CurrencyAmount::millisats(1000), PaymentMethod::Lightning)
|
||||||
|
.await?;
|
||||||
|
// full month price in msats
|
||||||
|
let mo_price = (plan.amount / MOCK_RATE * 1.0e11) as u64;
|
||||||
|
let time_scale = 1000f64 / mo_price as f64;
|
||||||
|
let vm = db.get_vm(1).await?;
|
||||||
|
let next_expire = PricingEngine::next_template_expire(&vm, &plan);
|
||||||
|
match price {
|
||||||
|
CostResult::New { amount, time_value, tax, .. } => {
|
||||||
|
let expect_time = (next_expire as f64 * time_scale) as u64;
|
||||||
|
assert_eq!(expect_time, time_value);
|
||||||
|
assert_eq!(0, tax);
|
||||||
|
assert_eq!(amount, 1000);
|
||||||
|
}
|
||||||
|
_ => bail!("??"),
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
use crate::json_api::JsonApi;
|
use crate::json_api::JsonApi;
|
||||||
use crate::router::{ArpEntry, Router};
|
use crate::router::{ArpEntry, Router};
|
||||||
use anyhow::{ensure, Result};
|
use anyhow::{ensure, Context, Result};
|
||||||
use base64::engine::general_purpose::STANDARD;
|
use base64::engine::general_purpose::STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
@ -33,14 +33,14 @@ impl Router for MikrotikRouter {
|
|||||||
|
|
||||||
async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>> {
|
async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>> {
|
||||||
let rsp: Vec<MikrotikArpEntry> = self.api.req(Method::GET, "/rest/ip/arp", ()).await?;
|
let rsp: Vec<MikrotikArpEntry> = self.api.req(Method::GET, "/rest/ip/arp", ()).await?;
|
||||||
Ok(rsp.into_iter().map(|e| e.into()).collect())
|
Ok(rsp.into_iter().filter_map(|e| e.try_into().ok()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_arp_entry(&self, entry: &ArpEntry) -> Result<ArpEntry> {
|
async fn add_arp_entry(&self, entry: &ArpEntry) -> Result<ArpEntry> {
|
||||||
let req: MikrotikArpEntry = entry.clone().into();
|
let req: MikrotikArpEntry = entry.clone().into();
|
||||||
let rsp: MikrotikArpEntry = self.api.req(Method::PUT, "/rest/ip/arp", req).await?;
|
let rsp: MikrotikArpEntry = self.api.req(Method::PUT, "/rest/ip/arp", req).await?;
|
||||||
debug!("{:?}", rsp);
|
debug!("{:?}", rsp);
|
||||||
Ok(rsp.into())
|
Ok(rsp.try_into()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
|
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
|
||||||
@ -64,7 +64,7 @@ impl Router for MikrotikRouter {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
debug!("{:?}", rsp);
|
debug!("{:?}", rsp);
|
||||||
Ok(rsp.into())
|
Ok(rsp.try_into()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,15 +82,17 @@ struct MikrotikArpEntry {
|
|||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MikrotikArpEntry> for ArpEntry {
|
impl TryFrom<MikrotikArpEntry> for ArpEntry {
|
||||||
fn from(val: MikrotikArpEntry) -> Self {
|
type Error = anyhow::Error;
|
||||||
ArpEntry {
|
|
||||||
id: val.id,
|
fn try_from(value: MikrotikArpEntry) -> std::result::Result<Self, Self::Error> {
|
||||||
address: val.address,
|
Ok(ArpEntry {
|
||||||
mac_address: val.mac_address.unwrap(),
|
id: value.id,
|
||||||
interface: Some(val.interface),
|
address: value.address,
|
||||||
comment: val.comment,
|
mac_address: value.mac_address.context("Mac address is empty")?,
|
||||||
}
|
interface: Some(value.interface),
|
||||||
|
comment: value.comment,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{ensure, Result};
|
||||||
use lnvps_db::{Vm, VmIpAssignment};
|
use lnvps_db::{Vm, VmIpAssignment};
|
||||||
use rocket::async_trait;
|
use rocket::async_trait;
|
||||||
|
|
||||||
@ -30,6 +30,10 @@ pub struct ArpEntry {
|
|||||||
|
|
||||||
impl ArpEntry {
|
impl ArpEntry {
|
||||||
pub fn new(vm: &Vm, ip: &VmIpAssignment, interface: Option<String>) -> Result<Self> {
|
pub fn new(vm: &Vm, ip: &VmIpAssignment, interface: Option<String>) -> Result<Self> {
|
||||||
|
ensure!(
|
||||||
|
vm.mac_address != "ff:ff:ff:ff:ff:ff",
|
||||||
|
"MAC address is invalid because its blank"
|
||||||
|
);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: ip.arp_ref.clone(),
|
id: ip.arp_ref.clone(),
|
||||||
address: ip.ip.clone(),
|
address: ip.ip.clone(),
|
||||||
@ -47,4 +51,3 @@ mod ovh;
|
|||||||
#[cfg(feature = "mikrotik")]
|
#[cfg(feature = "mikrotik")]
|
||||||
pub use mikrotik::MikrotikRouter;
|
pub use mikrotik::MikrotikRouter;
|
||||||
pub use ovh::OvhDedicatedServerVMacRouter;
|
pub use ovh::OvhDedicatedServerVMacRouter;
|
||||||
|
|
@ -351,5 +351,5 @@ enum OvhTaskFunction {
|
|||||||
MoveVirtualMac,
|
MoveVirtualMac,
|
||||||
VirtualMacAdd,
|
VirtualMacAdd,
|
||||||
VirtualMacDelete,
|
VirtualMacDelete,
|
||||||
RemoveVirtualMac
|
RemoveVirtualMac,
|
||||||
}
|
}
|
@ -50,6 +50,9 @@ pub struct Settings {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
/// Tax rates to change per country as a percent of the amount
|
/// Tax rates to change per country as a percent of the amount
|
||||||
pub tax_rate: HashMap<CountryCode, f32>,
|
pub tax_rate: HashMap<CountryCode, f32>,
|
||||||
|
|
||||||
|
/// public host of lnvps_nostr service
|
||||||
|
pub nostr_address_host: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
@ -104,16 +107,27 @@ pub struct SmtpConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum ProvisionerConfig {
|
pub struct ProvisionerConfig {
|
||||||
|
pub proxmox: Option<ProxmoxConfig>,
|
||||||
|
pub libvirt: Option<LibVirtConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
Proxmox {
|
pub struct ProxmoxConfig {
|
||||||
/// Generic VM configuration
|
/// Generic VM configuration
|
||||||
qemu: QemuConfig,
|
pub qemu: QemuConfig,
|
||||||
/// SSH config for issuing commands via CLI
|
/// SSH config for issuing commands via CLI
|
||||||
ssh: Option<SshConfig>,
|
pub ssh: Option<SshConfig>,
|
||||||
/// MAC address prefix for NIC (eg. bc:24:11)
|
/// MAC address prefix for NIC (eg. bc:24:11)
|
||||||
mac_prefix: Option<String>,
|
pub mac_prefix: Option<String>,
|
||||||
},
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct LibVirtConfig {
|
||||||
|
/// Generic VM configuration
|
||||||
|
pub qemu: QemuConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
@ -137,6 +151,8 @@ pub struct QemuConfig {
|
|||||||
pub cpu: String,
|
pub cpu: String,
|
||||||
/// Enable virtualization inside VM
|
/// Enable virtualization inside VM
|
||||||
pub kvm: bool,
|
pub kvm: bool,
|
||||||
|
/// CPU architecture
|
||||||
|
pub arch: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
@ -198,16 +214,20 @@ pub fn mock_settings() -> Settings {
|
|||||||
macaroon: Default::default(),
|
macaroon: Default::default(),
|
||||||
},
|
},
|
||||||
read_only: false,
|
read_only: false,
|
||||||
provisioner: ProvisionerConfig::Proxmox {
|
provisioner: ProvisionerConfig {
|
||||||
|
proxmox: Some(ProxmoxConfig {
|
||||||
qemu: QemuConfig {
|
qemu: QemuConfig {
|
||||||
machine: "q35".to_string(),
|
machine: "q35".to_string(),
|
||||||
os_type: "l26".to_string(),
|
os_type: "l26".to_string(),
|
||||||
bridge: "vmbr1".to_string(),
|
bridge: "vmbr1".to_string(),
|
||||||
cpu: "kvm64".to_string(),
|
cpu: "kvm64".to_string(),
|
||||||
kvm: false,
|
kvm: false,
|
||||||
|
arch: "x86_64".to_string(),
|
||||||
},
|
},
|
||||||
ssh: None,
|
ssh: None,
|
||||||
mac_prefix: Some("ff:ff:ff".to_string()),
|
mac_prefix: Some("ff:ff:ff".to_string()),
|
||||||
|
}),
|
||||||
|
libvirt: None,
|
||||||
},
|
},
|
||||||
delete_after: 0,
|
delete_after: 0,
|
||||||
smtp: None,
|
smtp: None,
|
||||||
@ -220,5 +240,6 @@ pub fn mock_settings() -> Settings {
|
|||||||
nostr: None,
|
nostr: None,
|
||||||
revolut: None,
|
revolut: None,
|
||||||
tax_rate: HashMap::from([(CountryCode::IRL, 23.0), (CountryCode::USA, 1.0)]),
|
tax_rate: HashMap::from([(CountryCode::IRL, 23.0), (CountryCode::USA, 1.0)]),
|
||||||
|
nostr_address_host: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,13 +2,13 @@ use crate::host::get_host_client;
|
|||||||
use crate::provisioner::LNVpsProvisioner;
|
use crate::provisioner::LNVpsProvisioner;
|
||||||
use crate::settings::{ProvisionerConfig, Settings, SmtpConfig};
|
use crate::settings::{ProvisionerConfig, Settings, SmtpConfig};
|
||||||
use crate::status::{VmRunningState, VmState, VmStateCache};
|
use crate::status::{VmRunningState, VmState, VmStateCache};
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Result};
|
||||||
use chrono::{DateTime, Datelike, Days, Utc};
|
use chrono::{DateTime, Datelike, Days, Utc};
|
||||||
use lettre::message::{MessageBuilder, MultiPart};
|
use lettre::message::{MessageBuilder, MultiPart};
|
||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
use lettre::AsyncTransport;
|
use lettre::AsyncTransport;
|
||||||
use lettre::{AsyncSmtpTransport, Tokio1Executor};
|
use lettre::{AsyncSmtpTransport, Tokio1Executor};
|
||||||
use lnvps_db::{LNVpsDb, Vm};
|
use lnvps_db::{LNVpsDb, Vm, VmHost};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use nostr::{EventBuilder, PublicKey, ToBech32};
|
use nostr::{EventBuilder, PublicKey, ToBech32};
|
||||||
use nostr_sdk::Client;
|
use nostr_sdk::Client;
|
||||||
@ -196,7 +196,8 @@ impl Worker {
|
|||||||
self.check_vm(vm).await?;
|
self.check_vm(vm).await?;
|
||||||
|
|
||||||
// delete vm if not paid (in new state)
|
// delete vm if not paid (in new state)
|
||||||
if vm.expires < Utc::now().sub(Days::new(1)) {
|
if vm.created == vm.expires && !vm.deleted && vm.expires < Utc::now().sub(Days::new(1))
|
||||||
|
{
|
||||||
info!("Deleting unpaid VM {}", vm.id);
|
info!("Deleting unpaid VM {}", vm.id);
|
||||||
self.provisioner.delete_vm(vm.id).await?;
|
self.provisioner.delete_vm(vm.id).await?;
|
||||||
}
|
}
|
||||||
@ -280,18 +281,10 @@ impl Worker {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_job(&mut self, job: &WorkJob) -> Result<()> {
|
async fn patch_host(&self, host: &mut VmHost) -> Result<()> {
|
||||||
match job {
|
|
||||||
WorkJob::PatchHosts => {
|
|
||||||
let mut hosts = self.db.list_hosts().await?;
|
|
||||||
for mut host in &mut hosts {
|
|
||||||
info!("Patching host {}", host.name);
|
|
||||||
let client = match get_host_client(host, &self.settings.provisioner_config) {
|
let client = match get_host_client(host, &self.settings.provisioner_config) {
|
||||||
Ok(h) => h,
|
Ok(h) => h,
|
||||||
Err(e) => {
|
Err(e) => bail!("Failed to get host client: {} {}", host.name, e),
|
||||||
warn!("Failed to get host client: {} {}", host.name, e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let info = client.get_info().await?;
|
let info = client.get_info().await?;
|
||||||
let needs_update = info.cpu != host.cpu || info.memory != host.memory;
|
let needs_update = info.cpu != host.cpu || info.memory != host.memory;
|
||||||
@ -320,6 +313,18 @@ impl Worker {
|
|||||||
warn!("Un-mapped host disk {}", disk.name);
|
warn!("Un-mapped host disk {}", disk.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_job(&mut self, job: &WorkJob) -> Result<()> {
|
||||||
|
match job {
|
||||||
|
WorkJob::PatchHosts => {
|
||||||
|
let mut hosts = self.db.list_hosts().await?;
|
||||||
|
for mut host in &mut hosts {
|
||||||
|
info!("Patching host {}", host.name);
|
||||||
|
if let Err(e) = self.patch_host(&mut host).await {
|
||||||
|
error!("Failed to patch host {}: {}", host.name, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkJob::CheckVm { vm_id } => {
|
WorkJob::CheckVm { vm_id } => {
|
7
lnvps_common/Cargo.toml
Normal file
7
lnvps_common/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "lnvps_common"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rocket.workspace = true
|
2
lnvps_common/src/lib.rs
Normal file
2
lnvps_common/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod cors;
|
||||||
|
pub use cors::*;
|
@ -6,9 +6,10 @@ edition = "2021"
|
|||||||
[features]
|
[features]
|
||||||
default = ["mysql"]
|
default = ["mysql"]
|
||||||
mysql = ["sqlx/mysql"]
|
mysql = ["sqlx/mysql"]
|
||||||
|
nostr-domain = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.83"
|
anyhow.workspace = true
|
||||||
sqlx = { version = "0.8.2", features = ["chrono", "migrate", "runtime-tokio"] }
|
sqlx = { version = "0.8.2", features = ["chrono", "migrate", "runtime-tokio"] }
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
|
4
lnvps_db/migrations/20250328220956_fixes.sql
Normal file
4
lnvps_db/migrations/20250328220956_fixes.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE vm_ip_assignment DROP KEY ix_vm_ip_assignment_ip;
|
||||||
|
alter table vm_os_image
|
||||||
|
add column default_username varchar(50);
|
25
lnvps_db/migrations/20250402115943_nostr_address.sql
Normal file
25
lnvps_db/migrations/20250402115943_nostr_address.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
create table nostr_domain
|
||||||
|
(
|
||||||
|
id integer unsigned not null auto_increment primary key,
|
||||||
|
owner_id integer unsigned not null,
|
||||||
|
name varchar(200) not null,
|
||||||
|
enabled bit(1) not null default 0,
|
||||||
|
created timestamp not null default current_timestamp,
|
||||||
|
relays varchar(1024),
|
||||||
|
|
||||||
|
unique key ix_domain_unique (name),
|
||||||
|
constraint fk_nostr_domain_user foreign key (owner_id) references users (id)
|
||||||
|
);
|
||||||
|
create table nostr_domain_handle
|
||||||
|
(
|
||||||
|
id integer unsigned not null auto_increment primary key,
|
||||||
|
domain_id integer unsigned not null,
|
||||||
|
handle varchar(100) not null,
|
||||||
|
created timestamp not null default current_timestamp,
|
||||||
|
pubkey binary(32) not null,
|
||||||
|
relays varchar(1024),
|
||||||
|
|
||||||
|
unique key ix_domain_handle_unique (domain_id, handle),
|
||||||
|
constraint fk_nostr_domain_handle_domain foreign key (domain_id) references nostr_domain (id) on delete cascade
|
||||||
|
)
|
9
lnvps_db/migrations/20250501132143_billing_info.sql
Normal file
9
lnvps_db/migrations/20250501132143_billing_info.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
alter table users
|
||||||
|
add column billing_name varchar(200),
|
||||||
|
add column billing_address_1 varchar(200),
|
||||||
|
add column billing_address_2 varchar(200),
|
||||||
|
add column billing_city varchar(100),
|
||||||
|
add column billing_state varchar(100),
|
||||||
|
add column billing_postcode varchar(50),
|
||||||
|
add column billing_tax_id varchar(50);
|
19
lnvps_db/migrations/20250501162308_company_info.sql
Normal file
19
lnvps_db/migrations/20250501162308_company_info.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
create table company
|
||||||
|
(
|
||||||
|
id integer unsigned not null auto_increment primary key,
|
||||||
|
created timestamp not null default current_timestamp,
|
||||||
|
name varchar(100) not null,
|
||||||
|
email varchar(100) not null,
|
||||||
|
phone varchar(100),
|
||||||
|
address_1 varchar(200),
|
||||||
|
address_2 varchar(200),
|
||||||
|
city varchar(100),
|
||||||
|
state varchar(100),
|
||||||
|
postcode varchar(50),
|
||||||
|
country_code varchar(3),
|
||||||
|
tax_id varchar(50)
|
||||||
|
);
|
||||||
|
alter table vm_host_region
|
||||||
|
add column company_id integer unsigned,
|
||||||
|
add constraint fk_host_region_company foreign key (company_id) references company (id);
|
@ -10,7 +10,7 @@ pub use mysql::*;
|
|||||||
pub use async_trait::async_trait;
|
pub use async_trait::async_trait;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait LNVpsDb: Sync + Send {
|
pub trait LNVpsDb: LNVPSNostrDb + Send + Sync {
|
||||||
/// Migrate database
|
/// Migrate database
|
||||||
async fn migrate(&self) -> Result<()>;
|
async fn migrate(&self) -> Result<()>;
|
||||||
|
|
||||||
@ -172,4 +172,44 @@ pub trait LNVpsDb: Sync + Send {
|
|||||||
|
|
||||||
/// Get access policy
|
/// Get access policy
|
||||||
async fn get_access_policy(&self, access_policy_id: u64) -> Result<AccessPolicy>;
|
async fn get_access_policy(&self, access_policy_id: u64) -> Result<AccessPolicy>;
|
||||||
|
|
||||||
|
/// Get company
|
||||||
|
async fn get_company(&self, company_id: u64) -> Result<Company>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
#[async_trait]
|
||||||
|
pub trait LNVPSNostrDb: Sync + Send {
|
||||||
|
/// Get single handle for a domain
|
||||||
|
async fn get_handle(&self, handle_id: u64) -> Result<NostrDomainHandle>;
|
||||||
|
|
||||||
|
/// Get single handle for a domain
|
||||||
|
async fn get_handle_by_name(&self, domain_id: u64, handle: &str) -> Result<NostrDomainHandle>;
|
||||||
|
|
||||||
|
/// Insert a new handle
|
||||||
|
async fn insert_handle(&self, handle: &NostrDomainHandle) -> Result<u64>;
|
||||||
|
|
||||||
|
/// Update an existing domain handle
|
||||||
|
async fn update_handle(&self, handle: &NostrDomainHandle) -> Result<()>;
|
||||||
|
|
||||||
|
/// Delete handle entry
|
||||||
|
async fn delete_handle(&self, handle_id: u64) -> Result<()>;
|
||||||
|
|
||||||
|
/// List handles
|
||||||
|
async fn list_handles(&self, domain_id: u64) -> Result<Vec<NostrDomainHandle>>;
|
||||||
|
|
||||||
|
/// Get domain object by id
|
||||||
|
async fn get_domain(&self, id: u64) -> Result<NostrDomain>;
|
||||||
|
|
||||||
|
/// Get domain object by name
|
||||||
|
async fn get_domain_by_name(&self, name: &str) -> Result<NostrDomain>;
|
||||||
|
|
||||||
|
/// List domains owned by a user
|
||||||
|
async fn list_domains(&self, owner_id: u64) -> Result<Vec<NostrDomain>>;
|
||||||
|
|
||||||
|
/// Insert a new domain
|
||||||
|
async fn insert_domain(&self, domain: &NostrDomain) -> Result<u64>;
|
||||||
|
|
||||||
|
/// Delete a domain
|
||||||
|
async fn delete_domain(&self, domain_id: u64) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use std::path::PathBuf;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(FromRow, Clone, Debug)]
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
/// Users who buy VM's
|
/// Users who buy VM's
|
||||||
pub struct User {
|
pub struct User {
|
||||||
/// Unique ID of this user (database generated)
|
/// Unique ID of this user (database generated)
|
||||||
@ -23,6 +23,20 @@ pub struct User {
|
|||||||
pub contact_email: bool,
|
pub contact_email: bool,
|
||||||
/// Users country
|
/// Users country
|
||||||
pub country_code: Option<String>,
|
pub country_code: Option<String>,
|
||||||
|
/// Name to show on invoices
|
||||||
|
pub billing_name: Option<String>,
|
||||||
|
/// Billing address line 1
|
||||||
|
pub billing_address_1: Option<String>,
|
||||||
|
/// Billing address line 2
|
||||||
|
pub billing_address_2: Option<String>,
|
||||||
|
/// Billing city
|
||||||
|
pub billing_city: Option<String>,
|
||||||
|
/// Billing state/county
|
||||||
|
pub billing_state: Option<String>,
|
||||||
|
/// Billing postcode/zip
|
||||||
|
pub billing_postcode: Option<String>,
|
||||||
|
/// Billing tax id
|
||||||
|
pub billing_tax_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Clone, Debug, Default)]
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
@ -57,6 +71,7 @@ pub struct VmHostRegion {
|
|||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
pub company_id: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Clone, Debug, Default)]
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
@ -206,6 +221,7 @@ pub struct VmOsImage {
|
|||||||
pub release_date: DateTime<Utc>,
|
pub release_date: DateTime<Utc>,
|
||||||
/// URL location of cloud image
|
/// URL location of cloud image
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
pub default_username: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VmOsImage {
|
impl VmOsImage {
|
||||||
@ -488,3 +504,40 @@ impl FromStr for PaymentMethod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
|
pub struct NostrDomain {
|
||||||
|
pub id: u64,
|
||||||
|
pub owner_id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub relays: Option<String>,
|
||||||
|
pub handles: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
|
pub struct NostrDomainHandle {
|
||||||
|
pub id: u64,
|
||||||
|
pub domain_id: u64,
|
||||||
|
pub handle: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub pubkey: Vec<u8>,
|
||||||
|
pub relays: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
|
pub struct Company {
|
||||||
|
pub id: u64,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub name: String,
|
||||||
|
pub address_1: Option<String>,
|
||||||
|
pub address_2: Option<String>,
|
||||||
|
pub city: Option<String>,
|
||||||
|
pub state: Option<String>,
|
||||||
|
pub country_code: Option<String>,
|
||||||
|
pub tax_id: Option<String>,
|
||||||
|
pub postcode: Option<String>,
|
||||||
|
pub phone: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
}
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
use crate::{
|
use crate::{AccessPolicy, Company, IpRange, LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, Router, User, UserSshKey, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate};
|
||||||
AccessPolicy, IpRange, LNVpsDb, Router, User, UserSshKey, Vm, VmCostPlan, VmCustomPricing,
|
|
||||||
VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostRegion, VmIpAssignment,
|
|
||||||
VmOsImage, VmPayment, VmTemplate,
|
|
||||||
};
|
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{bail, Error, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{Executor, MySqlPool, Row};
|
use sqlx::{Executor, MySqlPool, Row};
|
||||||
@ -60,12 +56,19 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
|
|
||||||
async fn update_user(&self, user: &User) -> Result<()> {
|
async fn update_user(&self, user: &User) -> Result<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"update users set email=?, contact_nip17=?, contact_email=?, country_code=? where id = ?",
|
"update users set email=?, contact_nip17=?, contact_email=?, country_code=?, billing_name=?, billing_address_1=?, billing_address_2=?, billing_city=?, billing_state=?, billing_postcode=?, billing_tax_id=? where id = ?",
|
||||||
)
|
)
|
||||||
.bind(&user.email)
|
.bind(&user.email)
|
||||||
.bind(user.contact_nip17)
|
.bind(user.contact_nip17)
|
||||||
.bind(user.contact_email)
|
.bind(user.contact_email)
|
||||||
.bind(&user.country_code)
|
.bind(&user.country_code)
|
||||||
|
.bind(&user.billing_name)
|
||||||
|
.bind(&user.billing_address_1)
|
||||||
|
.bind(&user.billing_address_2)
|
||||||
|
.bind(&user.billing_city)
|
||||||
|
.bind(&user.billing_state)
|
||||||
|
.bind(&user.billing_postcode)
|
||||||
|
.bind(&user.billing_tax_id)
|
||||||
.bind(user.id)
|
.bind(user.id)
|
||||||
.execute(&self.db)
|
.execute(&self.db)
|
||||||
.await?;
|
.await?;
|
||||||
@ -555,4 +558,124 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
.await
|
.await
|
||||||
.map_err(Error::new)
|
.map_err(Error::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_company(&self, company_id: u64) -> Result<Company> {
|
||||||
|
sqlx::query_as("select * from company where id=?")
|
||||||
|
.bind(company_id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
#[async_trait]
|
||||||
|
impl LNVPSNostrDb for LNVpsDbMysql {
|
||||||
|
async fn get_handle(&self, handle_id: u64) -> Result<NostrDomainHandle> {
|
||||||
|
sqlx::query_as("select * from nostr_domain_handle where id=?")
|
||||||
|
.bind(handle_id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_handle_by_name(&self, domain_id: u64, handle: &str) -> Result<NostrDomainHandle> {
|
||||||
|
sqlx::query_as("select * from nostr_domain_handle where domain_id=? and handle=?")
|
||||||
|
.bind(domain_id)
|
||||||
|
.bind(handle)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_handle(&self, handle: &NostrDomainHandle) -> Result<u64> {
|
||||||
|
Ok(
|
||||||
|
sqlx::query(
|
||||||
|
"insert into nostr_domain_handle(domain_id,handle,pubkey,relays) values(?,?,?,?) returning id",
|
||||||
|
)
|
||||||
|
.bind(handle.domain_id)
|
||||||
|
.bind(&handle.handle)
|
||||||
|
.bind(&handle.pubkey)
|
||||||
|
.bind(&handle.relays)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)?
|
||||||
|
.try_get(0)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_handle(&self, handle: &NostrDomainHandle) -> Result<()> {
|
||||||
|
sqlx::query("update nostr_domain_handle set handle=?,pubkey=?,relays=? where id=?")
|
||||||
|
.bind(&handle.handle)
|
||||||
|
.bind(&handle.pubkey)
|
||||||
|
.bind(&handle.relays)
|
||||||
|
.bind(handle.id)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_handle(&self, handle_id: u64) -> Result<()> {
|
||||||
|
sqlx::query("delete from nostr_domain_handle where id=?")
|
||||||
|
.bind(handle_id)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_handles(&self, domain_id: u64) -> Result<Vec<NostrDomainHandle>> {
|
||||||
|
sqlx::query_as("select * from nostr_domain_handle where domain_id=?")
|
||||||
|
.bind(domain_id)
|
||||||
|
.fetch_all(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_domain(&self, id: u64) -> Result<NostrDomain> {
|
||||||
|
sqlx::query_as("select *,(select count(1) from nostr_domain_handle where domain_id=nostr_domain.id) handles from nostr_domain where id=?")
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_domain_by_name(&self, name: &str) -> Result<NostrDomain> {
|
||||||
|
sqlx::query_as("select *,(select count(1) from nostr_domain_handle where domain_id=nostr_domain.id) handles from nostr_domain where name=?")
|
||||||
|
.bind(name)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_domains(&self, owner_id: u64) -> Result<Vec<NostrDomain>> {
|
||||||
|
sqlx::query_as("select *,(select count(1) from nostr_domain_handle where domain_id=nostr_domain.id) handles from nostr_domain where owner_id=?")
|
||||||
|
.bind(owner_id)
|
||||||
|
.fetch_all(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_domain(&self, domain: &NostrDomain) -> Result<u64> {
|
||||||
|
Ok(
|
||||||
|
sqlx::query(
|
||||||
|
"insert into nostr_domain(owner_id,name,relays) values(?,?,?) returning id",
|
||||||
|
)
|
||||||
|
.bind(domain.owner_id)
|
||||||
|
.bind(&domain.name)
|
||||||
|
.bind(&domain.relays)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)?
|
||||||
|
.try_get(0)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_domain(&self, domain_id: u64) -> Result<()> {
|
||||||
|
sqlx::query("update nostr_domain set deleted = current_timestamp where id = ?")
|
||||||
|
.bind(domain_id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
7
lnvps_nostr/Cargo.lock
generated
Normal file
7
lnvps_nostr/Cargo.lock
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lnvps_nostr"
|
||||||
|
version = "0.1.0"
|
17
lnvps_nostr/Cargo.toml
Normal file
17
lnvps_nostr/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "lnvps_nostr"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lnvps_db = { path = "../lnvps_db", features = ["nostr-domain"] }
|
||||||
|
lnvps_common = { path = "../lnvps_common" }
|
||||||
|
env_logger.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
config.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
rocket.workspace = true
|
||||||
|
hex.workspace = true
|
3
lnvps_nostr/README.md
Normal file
3
lnvps_nostr/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# LNVPS Nostr Services
|
||||||
|
|
||||||
|
A simple webserver hosting various nostr based services for lnvps.net
|
5
lnvps_nostr/config.yaml
Normal file
5
lnvps_nostr/config.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Connection string to lnvps database
|
||||||
|
db: "mysql://root:root@localhost:3376/lnvps"
|
||||||
|
|
||||||
|
# Listen address for http server
|
||||||
|
listen: "127.0.0.1:8001"
|
47
lnvps_nostr/index.html
Normal file
47
lnvps_nostr/index.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!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: 15px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="header">
|
||||||
|
LNVPS
|
||||||
|
<img height="48" width="48" src="https://lnvps.net/logo.jpg" alt="logo"/>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<p>This domain is using LNVPS' free NIP-05 hosting, get your own at <a href="https://lnvps.net">LNVPS.net</a></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
65
lnvps_nostr/src/main.rs
Normal file
65
lnvps_nostr/src/main.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
mod routes;
|
||||||
|
|
||||||
|
use crate::routes::routes;
|
||||||
|
use anyhow::Result;
|
||||||
|
use config::{Config, File};
|
||||||
|
use lnvps_common::CORS;
|
||||||
|
use lnvps_db::{LNVPSNostrDb, LNVpsDbMysql};
|
||||||
|
use log::error;
|
||||||
|
use rocket::http::Method;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
struct Settings {
|
||||||
|
/// Database connection string
|
||||||
|
db: String,
|
||||||
|
/// Listen address for http server
|
||||||
|
listen: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let settings: Settings = Config::builder()
|
||||||
|
.add_source(File::from(PathBuf::from("config.yaml")))
|
||||||
|
.build()?
|
||||||
|
.try_deserialize()?;
|
||||||
|
|
||||||
|
// Connect database
|
||||||
|
let db = LNVpsDbMysql::new(&settings.db).await?;
|
||||||
|
let db: Arc<dyn LNVPSNostrDb> = Arc::new(db);
|
||||||
|
|
||||||
|
let mut config = rocket::Config::default();
|
||||||
|
let ip: SocketAddr = match &settings.listen {
|
||||||
|
Some(i) => i.parse()?,
|
||||||
|
None => SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 8000),
|
||||||
|
};
|
||||||
|
config.address = ip.ip();
|
||||||
|
config.port = ip.port();
|
||||||
|
|
||||||
|
if let Err(e) = rocket::Rocket::custom(config)
|
||||||
|
.manage(db.clone())
|
||||||
|
.manage(settings.clone())
|
||||||
|
.attach(CORS)
|
||||||
|
.mount("/", routes())
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
vec![rocket::Route::ranked(
|
||||||
|
isize::MAX,
|
||||||
|
Method::Options,
|
||||||
|
"/<catch_all_options_route..>",
|
||||||
|
CORS,
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
.launch()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("{}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
72
lnvps_nostr/src/routes.rs
Normal file
72
lnvps_nostr/src/routes.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
use lnvps_db::LNVPSNostrDb;
|
||||||
|
use log::info;
|
||||||
|
use rocket::request::{FromRequest, Outcome};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket::{Request, Route, State, routes};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use rocket::http::ContentType;
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![get_index, nostr_address]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct NostrJson {
|
||||||
|
pub names: HashMap<String, String>,
|
||||||
|
pub relays: HashMap<String, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HostInfo<'r> {
|
||||||
|
pub host: Option<&'r str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for HostInfo<'r> {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
Outcome::Success(HostInfo {
|
||||||
|
host: request.host().map(|h| h.domain().as_str()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/", format = "html")]
|
||||||
|
fn get_index() -> (ContentType, &'static str) {
|
||||||
|
const HTML: &str = include_str!("../index.html");
|
||||||
|
(ContentType::HTML, HTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/.well-known/nostr.json?<name>")]
|
||||||
|
async fn nostr_address(
|
||||||
|
host: HostInfo<'_>,
|
||||||
|
db: &State<Arc<dyn LNVPSNostrDb>>,
|
||||||
|
name: Option<&str>,
|
||||||
|
) -> Result<Json<NostrJson>, &'static str> {
|
||||||
|
let name = name.unwrap_or("_");
|
||||||
|
let host = host.host.unwrap_or("lnvps.net");
|
||||||
|
info!("Got request for {} on host {}", name, host);
|
||||||
|
let domain = db
|
||||||
|
.get_domain_by_name(host)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Domain not found")?;
|
||||||
|
let handle = db
|
||||||
|
.get_handle_by_name(domain.id, name)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Handle not found")?;
|
||||||
|
|
||||||
|
let pubkey_hex = hex::encode(handle.pubkey);
|
||||||
|
let relays = if let Some(r) = handle.relays {
|
||||||
|
r.split(",").map(|x| x.to_string()).collect()
|
||||||
|
} else if let Some(r) = domain.relays {
|
||||||
|
r.split(",").map(|x| x.to_string()).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
Ok(Json(NostrJson {
|
||||||
|
names: HashMap::from([(name.to_string(), pubkey_hex.clone())]),
|
||||||
|
relays: HashMap::from([(pubkey_hex, relays)]),
|
||||||
|
}))
|
||||||
|
}
|
@ -1,52 +0,0 @@
|
|||||||
use crate::host::{FullVmInfo, TimeSeries, TimeSeriesData, VmHostClient};
|
|
||||||
use crate::status::VmState;
|
|
||||||
use lnvps_db::{async_trait, Vm, VmOsImage};
|
|
||||||
|
|
||||||
pub struct LibVirt {}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl VmHostClient for LibVirt {
|
|
||||||
async fn download_os_image(&self, image: &VmOsImage) -> anyhow::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn generate_mac(&self, vm: &Vm) -> anyhow::Result<String> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_vm(&self, vm: &Vm) -> anyhow::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stop_vm(&self, vm: &Vm) -> anyhow::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reset_vm(&self, vm: &Vm) -> anyhow::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reinstall_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_vm_state(&self, vm: &Vm) -> anyhow::Result<VmState> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn configure_vm(&self, vm: &FullVmInfo) -> anyhow::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_time_series_data(
|
|
||||||
&self,
|
|
||||||
vm: &Vm,
|
|
||||||
series: TimeSeries,
|
|
||||||
) -> anyhow::Result<Vec<TimeSeriesData>> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
220
src/host/mod.rs
220
src/host/mod.rs
@ -1,220 +0,0 @@
|
|||||||
use crate::settings::ProvisionerConfig;
|
|
||||||
use crate::status::VmState;
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use futures::future::join_all;
|
|
||||||
use lnvps_db::{
|
|
||||||
async_trait, IpRange, LNVpsDb, UserSshKey, Vm, VmCustomTemplate, VmHost, VmHostDisk,
|
|
||||||
VmHostKind, VmIpAssignment, VmOsImage, VmTemplate,
|
|
||||||
};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
|
||||||
|
|
||||||
//#[cfg(feature = "libvirt")]
|
|
||||||
//mod libvirt;
|
|
||||||
#[cfg(feature = "proxmox")]
|
|
||||||
mod proxmox;
|
|
||||||
|
|
||||||
pub struct TerminalStream {
|
|
||||||
pub rx: Receiver<Vec<u8>>,
|
|
||||||
pub tx: Sender<Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generic type for creating VM's
|
|
||||||
#[async_trait]
|
|
||||||
pub trait VmHostClient: Send + Sync {
|
|
||||||
async fn get_info(&self) -> Result<VmHostInfo>;
|
|
||||||
|
|
||||||
/// Download OS image to the host
|
|
||||||
async fn download_os_image(&self, image: &VmOsImage) -> Result<()>;
|
|
||||||
|
|
||||||
/// Create a random MAC address for the NIC
|
|
||||||
async fn generate_mac(&self, vm: &Vm) -> Result<String>;
|
|
||||||
|
|
||||||
/// Start a VM
|
|
||||||
async fn start_vm(&self, vm: &Vm) -> Result<()>;
|
|
||||||
|
|
||||||
/// Stop a VM
|
|
||||||
async fn stop_vm(&self, vm: &Vm) -> Result<()>;
|
|
||||||
|
|
||||||
/// Reset VM (Hard)
|
|
||||||
async fn reset_vm(&self, vm: &Vm) -> Result<()>;
|
|
||||||
|
|
||||||
/// Spawn a VM
|
|
||||||
async fn create_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
|
||||||
|
|
||||||
/// Re-install a vm OS
|
|
||||||
async fn reinstall_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
|
||||||
|
|
||||||
/// Get the running status of a VM
|
|
||||||
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState>;
|
|
||||||
|
|
||||||
/// Apply vm configuration (patch)
|
|
||||||
async fn configure_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
|
||||||
|
|
||||||
/// Get resource usage data
|
|
||||||
async fn get_time_series_data(
|
|
||||||
&self,
|
|
||||||
vm: &Vm,
|
|
||||||
series: TimeSeries,
|
|
||||||
) -> Result<Vec<TimeSeriesData>>;
|
|
||||||
|
|
||||||
/// Connect to terminal serial port
|
|
||||||
async fn connect_terminal(&self, vm: &Vm) -> Result<TerminalStream>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result<Arc<dyn VmHostClient>> {
|
|
||||||
#[cfg(test)]
|
|
||||||
{
|
|
||||||
Ok(Arc::new(crate::mocks::MockVmHost::new()))
|
|
||||||
}
|
|
||||||
#[cfg(not(test))]
|
|
||||||
{
|
|
||||||
Ok(match (host.kind.clone(), &cfg) {
|
|
||||||
#[cfg(feature = "proxmox")]
|
|
||||||
(
|
|
||||||
VmHostKind::Proxmox,
|
|
||||||
ProvisionerConfig::Proxmox {
|
|
||||||
qemu,
|
|
||||||
ssh,
|
|
||||||
mac_prefix,
|
|
||||||
},
|
|
||||||
) => Arc::new(proxmox::ProxmoxClient::new(
|
|
||||||
host.ip.parse()?,
|
|
||||||
&host.name,
|
|
||||||
&host.api_token,
|
|
||||||
mac_prefix.clone(),
|
|
||||||
qemu.clone(),
|
|
||||||
ssh.clone(),
|
|
||||||
)),
|
|
||||||
_ => bail!("Unknown host config: {}", host.kind),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All VM info necessary to provision a VM and its associated resources
|
|
||||||
pub struct FullVmInfo {
|
|
||||||
/// Instance to create
|
|
||||||
pub vm: Vm,
|
|
||||||
/// Host where the VM will be spawned
|
|
||||||
pub host: VmHost,
|
|
||||||
/// Disk where this VM will be saved on the host
|
|
||||||
pub disk: VmHostDisk,
|
|
||||||
/// VM template resources
|
|
||||||
pub template: Option<VmTemplate>,
|
|
||||||
/// VM custom template resources
|
|
||||||
pub custom_template: Option<VmCustomTemplate>,
|
|
||||||
/// The OS image used to create the VM
|
|
||||||
pub image: VmOsImage,
|
|
||||||
/// List of IP resources assigned to this VM
|
|
||||||
pub ips: Vec<VmIpAssignment>,
|
|
||||||
/// Ranges associated with [ips]
|
|
||||||
pub ranges: Vec<IpRange>,
|
|
||||||
/// SSH key to access the VM
|
|
||||||
pub ssh_key: UserSshKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FullVmInfo {
|
|
||||||
pub async fn load(vm_id: u64, db: Arc<dyn LNVpsDb>) -> Result<Self> {
|
|
||||||
let vm = db.get_vm(vm_id).await?;
|
|
||||||
let host = db.get_host(vm.host_id).await?;
|
|
||||||
let image = db.get_os_image(vm.image_id).await?;
|
|
||||||
let disk = db.get_host_disk(vm.disk_id).await?;
|
|
||||||
let ssh_key = db.get_user_ssh_key(vm.ssh_key_id).await?;
|
|
||||||
let ips = db.list_vm_ip_assignments(vm_id).await?;
|
|
||||||
|
|
||||||
let ip_range_ids: HashSet<u64> = ips.iter().map(|i| i.ip_range_id).collect();
|
|
||||||
let ip_ranges: Vec<_> = ip_range_ids.iter().map(|i| db.get_ip_range(*i)).collect();
|
|
||||||
let ranges: Vec<IpRange> = join_all(ip_ranges)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let template = if let Some(t) = vm.template_id {
|
|
||||||
Some(db.get_vm_template(t).await?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let custom_template = if let Some(t) = vm.custom_template_id {
|
|
||||||
Some(db.get_custom_vm_template(t).await?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
// create VM
|
|
||||||
Ok(FullVmInfo {
|
|
||||||
vm,
|
|
||||||
host,
|
|
||||||
template,
|
|
||||||
custom_template,
|
|
||||||
image,
|
|
||||||
ips,
|
|
||||||
disk,
|
|
||||||
ranges,
|
|
||||||
ssh_key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CPU cores
|
|
||||||
pub fn resources(&self) -> Result<VmResources> {
|
|
||||||
if let Some(t) = &self.template {
|
|
||||||
Ok(VmResources {
|
|
||||||
cpu: t.cpu,
|
|
||||||
memory: t.memory,
|
|
||||||
disk_size: t.disk_size,
|
|
||||||
})
|
|
||||||
} else if let Some(t) = &self.custom_template {
|
|
||||||
Ok(VmResources {
|
|
||||||
cpu: t.cpu,
|
|
||||||
memory: t.memory,
|
|
||||||
disk_size: t.disk_size,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
bail!("Invalid VM config, no template");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct VmResources {
|
|
||||||
pub cpu: u16,
|
|
||||||
pub memory: u64,
|
|
||||||
pub disk_size: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct TimeSeriesData {
|
|
||||||
pub timestamp: u64,
|
|
||||||
pub cpu: f32,
|
|
||||||
pub memory: f32,
|
|
||||||
pub memory_size: u64,
|
|
||||||
pub net_in: f32,
|
|
||||||
pub net_out: f32,
|
|
||||||
pub disk_write: f32,
|
|
||||||
pub disk_read: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum TimeSeries {
|
|
||||||
Hourly,
|
|
||||||
Daily,
|
|
||||||
Weekly,
|
|
||||||
Monthly,
|
|
||||||
Yearly,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct VmHostInfo {
|
|
||||||
pub cpu: u16,
|
|
||||||
pub memory: u64,
|
|
||||||
pub disks: Vec<VmHostDiskInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct VmHostDiskInfo {
|
|
||||||
pub name: String,
|
|
||||||
pub size: u64,
|
|
||||||
pub used: u64,
|
|
||||||
}
|
|
Reference in New Issue
Block a user