Fully functional network scores (#5048)
* Compile & copy wg probe * Node status agent WIP * Enable debug logging * Agent submits results - add clap to agent - agent runs network probe - /submit endpoint on NS API * Build clients with timeouts * Update logging and dev scripts * Replace /blaclisted endpoint * Testruns fully functional - task that queues testruns periodically - testruns read/write in DB * Probe scores fully working - testruns are assigned on API - submit updates testruns correctly on NS API side - agent registers with API - agent submits results correctly * Clippy fixes * PR feedback * Clippy again * PR feedback * Run clippy earlier in CI * Make refresh delay configurable in server & agent
This commit is contained in:
@@ -57,6 +57,12 @@ jobs:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --all-targets -- -D warnings
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@@ -82,9 +88,3 @@ jobs:
|
||||
with:
|
||||
command: test
|
||||
args: --workspace -- --ignored
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --all-targets -- -D warnings
|
||||
|
||||
Generated
+112
-73
@@ -466,6 +466,7 @@ checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core 0.4.5",
|
||||
"axum-macros",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
@@ -553,6 +554,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-macros"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-test"
|
||||
version = "16.2.0"
|
||||
@@ -4524,20 +4536,20 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-api-requests"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmrs 0.17.0-pre",
|
||||
"cosmwasm-std",
|
||||
"ecdsa",
|
||||
"getset",
|
||||
"nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-credentials-interface 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-serde-helpers 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-credentials-interface 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-serde-helpers 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"schemars",
|
||||
"serde",
|
||||
"sha2 0.10.8",
|
||||
@@ -4665,7 +4677,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-bin-common"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"const-str",
|
||||
"log",
|
||||
@@ -4989,11 +5001,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-coconut-bandwidth-contract-common"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5012,15 +5024,22 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-coconut-dkg-common"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw4",
|
||||
"nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-common-models"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5050,7 +5069,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-compact-ecash"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bls12_381",
|
||||
@@ -5060,8 +5079,8 @@ dependencies = [
|
||||
"ff",
|
||||
"group",
|
||||
"itertools 0.12.1",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"rand",
|
||||
"serde",
|
||||
"sha2 0.9.9",
|
||||
@@ -5085,12 +5104,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-config"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"handlebars",
|
||||
"log",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"serde",
|
||||
"toml 0.8.14",
|
||||
"url",
|
||||
@@ -5114,7 +5133,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-contracts-common"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -5249,12 +5268,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-credentials-interface"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"bls12_381",
|
||||
"nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"rand",
|
||||
"serde",
|
||||
"strum 0.26.3",
|
||||
@@ -5293,12 +5312,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"ed25519-dalek",
|
||||
"nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-sphinx-types 0.2.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-sphinx-types 0.2.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"subtle-encoding",
|
||||
@@ -5372,14 +5391,14 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-ecash-contract-common"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-utils",
|
||||
"nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@@ -5403,7 +5422,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-ecash-time"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
@@ -5431,7 +5450,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-exit-policy"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5663,7 +5682,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-group-contract-common"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cw-controllers",
|
||||
@@ -5691,11 +5710,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-http-api-client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"http 1.1.0",
|
||||
"nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"reqwest 0.12.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5874,7 +5893,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -5882,7 +5901,7 @@ dependencies = [
|
||||
"cw-controllers",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
@@ -5984,7 +6003,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -6012,7 +6031,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-network-defaults"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"log",
|
||||
@@ -6207,18 +6226,18 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-node-requests"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"celes",
|
||||
"humantime 2.1.0",
|
||||
"humantime-serde",
|
||||
"nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-exit-policy 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-wireguard-types 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-exit-policy 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-wireguard-types 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6227,6 +6246,22 @@ dependencies = [
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-agent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.5.20",
|
||||
"nym-bin-common 0.6.0",
|
||||
"nym-common-models",
|
||||
"reqwest 0.12.4",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "0.1.0"
|
||||
@@ -6240,16 +6275,20 @@ dependencies = [
|
||||
"futures-util",
|
||||
"moka",
|
||||
"nym-bin-common 0.6.0",
|
||||
"nym-common-models",
|
||||
"nym-explorer-client",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-task",
|
||||
"nym-validator-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-validator-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"regex",
|
||||
"reqwest 0.12.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_path",
|
||||
"sqlx",
|
||||
"strum 0.26.3",
|
||||
"strum_macros 0.26.4",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -6365,7 +6404,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"pem",
|
||||
]
|
||||
@@ -6438,7 +6477,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-serde-helpers"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
@@ -6753,7 +6792,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-sphinx-types"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"sphinx-packet",
|
||||
"thiserror",
|
||||
@@ -6917,7 +6956,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-validator-client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6935,20 +6974,19 @@ dependencies = [
|
||||
"flate2",
|
||||
"futures",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"nym-api-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-coconut-bandwidth-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-coconut-dkg-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-config 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-ecash-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-group-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-vesting-contract-common 0.7.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-api-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-coconut-bandwidth-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-coconut-dkg-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-config 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-ecash-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-group-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-vesting-contract-common 0.7.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"prost 0.12.6",
|
||||
"reqwest 0.12.4",
|
||||
"serde",
|
||||
@@ -6958,6 +6996,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"wasmtimer",
|
||||
"zeroize",
|
||||
@@ -7017,12 +7056,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -7089,12 +7128,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-wireguard-types"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1"
|
||||
source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"log",
|
||||
"nym-config 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)",
|
||||
"nym-config 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"x25519-dalek",
|
||||
@@ -8001,9 +8040,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.1.0"
|
||||
version = "11.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d"
|
||||
checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
]
|
||||
|
||||
@@ -61,6 +61,7 @@ members = [
|
||||
"common/ip-packet-requests",
|
||||
"common/ledger",
|
||||
"common/mixnode-common",
|
||||
"common/models",
|
||||
"common/network-defaults",
|
||||
"common/node-tester-utils",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
@@ -121,6 +122,7 @@ members = [
|
||||
"nym-node/nym-node-http-api",
|
||||
"nym-node/nym-node-requests",
|
||||
"nym-node-status-api",
|
||||
"nym-node-status-agent",
|
||||
"nym-outfox",
|
||||
"nym-validator-rewarder",
|
||||
"tools/echo-server",
|
||||
@@ -148,12 +150,14 @@ members = [
|
||||
default-members = [
|
||||
"clients/native",
|
||||
"clients/socks5",
|
||||
"common/models",
|
||||
"explorer-api",
|
||||
"gateway",
|
||||
"mixnode",
|
||||
"nym-api",
|
||||
"nym-data-observatory",
|
||||
"nym-node",
|
||||
"nym-node-status-api",
|
||||
"nym-validator-rewarder",
|
||||
"nym-node-status-api",
|
||||
"service-providers/authenticator",
|
||||
@@ -314,6 +318,7 @@ si-scale = "0.2.3"
|
||||
sphinx-packet = "0.1.1"
|
||||
sqlx = "0.7.4"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
subtle-encoding = "0.5"
|
||||
syn = "1"
|
||||
sysinfo = "0.30.13"
|
||||
|
||||
@@ -45,3 +45,4 @@ tracing = [
|
||||
"opentelemetry",
|
||||
]
|
||||
clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ]
|
||||
models = []
|
||||
|
||||
@@ -531,6 +531,18 @@ where
|
||||
}
|
||||
|
||||
if res.status().is_success() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let text = res.text().await.inspect_err(|err| {
|
||||
tracing::error!("Couldn't even get response text: {err}");
|
||||
})?;
|
||||
tracing::trace!("Result:\n{:#?}", text);
|
||||
|
||||
serde_json::from_str(&text)
|
||||
.map_err(|err| HttpClientError::GenericRequestFailure(err.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
Ok(res.json().await?)
|
||||
} else if res.status() == StatusCode::NOT_FOUND {
|
||||
Err(HttpClientError::NotFound)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "nym-common-models"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -0,0 +1 @@
|
||||
pub mod ns_api;
|
||||
@@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TestrunAssignment {
|
||||
/// has nothing to do with GW identity key. This is PK from `gateways` table
|
||||
pub testrun_id: i64,
|
||||
pub gateway_pk_id: i64,
|
||||
}
|
||||
+2
-2
@@ -19,5 +19,5 @@ MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftq
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9
|
||||
|
||||
EXPLORER_API=https://canary-explorer.performance.nymte.ch/api
|
||||
NYXD="https://canary-validator.performance.nymte.ch"
|
||||
NYM_API="https://canary-api.performance.nymte.ch/api"
|
||||
NYXD=https://canary-validator.performance.nymte.ch
|
||||
NYM_API=https://canary-api.performance.nymte.ch/api
|
||||
|
||||
+2
-2
@@ -19,5 +19,5 @@ VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8
|
||||
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
|
||||
|
||||
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api
|
||||
NYXD="https://qa-validator.qa.nymte.ch"
|
||||
NYM_API="https://qa-nym-api.qa.nymte.ch/api"
|
||||
NYXD=https://qa-validator.qa.nymte.ch
|
||||
NYM_API=https://qa-nym-api.qa.nymte.ch/api
|
||||
|
||||
+3
-3
@@ -20,6 +20,6 @@ ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jl
|
||||
|
||||
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
|
||||
EXPLORER_API=https://sandbox-explorer.nymtech.net/api
|
||||
NYXD="https://rpc.sandbox.nymtech.net"
|
||||
NYXD_WS="wss://rpc.sandbox.nymtech.net/websocket"
|
||||
NYM_API="https://sandbox-nym-api1.nymtech.net/api"
|
||||
NYXD=https://rpc.sandbox.nymtech.net
|
||||
NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket
|
||||
NYM_API=https://sandbox-nym-api1.nymtech.net/api
|
||||
|
||||
@@ -83,7 +83,9 @@ impl ExplorerClient {
|
||||
} else if response.status() == StatusCode::NOT_FOUND {
|
||||
Err(ExplorerApiError::NotFound)
|
||||
} else {
|
||||
Err(ExplorerApiError::RequestFailure(response.text().await?))
|
||||
let status = response.status();
|
||||
let err_msg = format!("{}: {}", response.text().await?, status);
|
||||
Err(ExplorerApiError::RequestFailure(err_msg))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ warning: no queries found; do you have the `offline` feature enabled
|
||||
### Possible solutions
|
||||
|
||||
- does your `sqlx-cli` version match `sqlx` version from `Cargo.toml`?
|
||||
+ `cargo install -f sqlx-cli --version <specific version>`
|
||||
```
|
||||
cargo install sqlx-cli --version <exact semver version as sqlx> --force
|
||||
```
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
nym-gateway-probe
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-agent"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true}
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
nym-bin-common = { path = "../common/bin-common", features = ["models"]}
|
||||
nym-common-models = { path = "../common/models" }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde_json = { workspace = true }
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
export RUST_LOG=${RUST_LOG:-debug}
|
||||
|
||||
crate_root=$(dirname $(realpath "$0"))
|
||||
gateway_probe_src=$(dirname $(dirname "$crate_root"))/nym-vpn-client/nym-vpn-core
|
||||
echo "gateway_probe_src=$gateway_probe_src"
|
||||
echo "crate_root=$crate_root"
|
||||
|
||||
export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe"
|
||||
|
||||
# build & copy over GW probe
|
||||
function copy_gw_probe() {
|
||||
pushd $gateway_probe_src
|
||||
cargo build --release --package nym-gateway-probe
|
||||
cp target/release/nym-gateway-probe "$crate_root"
|
||||
$crate_root/nym-gateway-probe --version
|
||||
popd
|
||||
}
|
||||
|
||||
function build_agent() {
|
||||
cargo build --package nym-node-status-agent --release
|
||||
}
|
||||
|
||||
function swarm() {
|
||||
local workers=$1
|
||||
echo "Running $workers in parallel"
|
||||
|
||||
build_agent
|
||||
|
||||
for ((i=1; i<=$workers; i++)); do
|
||||
../target/release/nym-node-status-agent run-probe &
|
||||
done
|
||||
|
||||
wait
|
||||
|
||||
echo "All agents completed"
|
||||
}
|
||||
|
||||
export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1"
|
||||
export NODE_STATUS_AGENT_SERVER_PORT="8000"
|
||||
|
||||
copy_gw_probe
|
||||
|
||||
swarm 30
|
||||
|
||||
# cargo run -- run-probe
|
||||
@@ -0,0 +1,109 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_common_models::ns_api::TestrunAssignment;
|
||||
use std::sync::OnceLock;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::probe::GwProbe;
|
||||
|
||||
// Helper for passing LONG_VERSION to clap
|
||||
fn pretty_build_info_static() -> &'static str {
|
||||
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
|
||||
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
|
||||
pub(crate) struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Command,
|
||||
#[arg(short, long, env = "NODE_STATUS_AGENT_SERVER_ADDRESS")]
|
||||
pub(crate) server_address: String,
|
||||
|
||||
#[arg(short = 'p', long, env = "NODE_STATUS_AGENT_SERVER_PORT")]
|
||||
pub(crate) server_port: u16,
|
||||
// TODO dz accept keypair for identification / auth
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub(crate) enum Command {
|
||||
RunProbe {
|
||||
/// path of binary to run
|
||||
#[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")]
|
||||
probe_path: String,
|
||||
#[arg(short, long, env = "NODE_STATUS_AGENT_GATEWAY_ID")]
|
||||
gateway_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub(crate) async fn execute(&self) -> anyhow::Result<()> {
|
||||
match &self.command {
|
||||
Command::RunProbe {
|
||||
probe_path,
|
||||
gateway_id,
|
||||
} => self.run_probe(probe_path, gateway_id).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_probe(&self, probe_path: &str, gateway_id: &Option<String>) -> anyhow::Result<()> {
|
||||
let server_address = format!("{}:{}", &self.server_address, self.server_port);
|
||||
|
||||
let probe = GwProbe::new(probe_path.to_string());
|
||||
|
||||
let version = probe.version().await;
|
||||
tracing::info!("Probe version:\n{}", version);
|
||||
|
||||
let testrun = request_testrun(&server_address).await?;
|
||||
|
||||
let log = probe.run_and_get_log(gateway_id);
|
||||
|
||||
submit_results(&server_address, testrun.testrun_id, log).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const URL_BASE: &str = "internal/testruns";
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
async fn request_testrun(server_addr: &str) -> anyhow::Result<TestrunAssignment> {
|
||||
let target_url = format!("{}/{}", server_addr, URL_BASE);
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.get(target_url)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|response| response.error_for_status())?;
|
||||
res.json()
|
||||
.await
|
||||
.map(|testrun| {
|
||||
tracing::info!("Received testrun assignment: {:?}", testrun);
|
||||
testrun
|
||||
})
|
||||
.map_err(|err| {
|
||||
tracing::error!("err");
|
||||
err.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(probe_outcome))]
|
||||
async fn submit_results(
|
||||
server_addr: &str,
|
||||
testrun_id: i64,
|
||||
probe_outcome: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let target_url = format!("{}/{}/{}", server_addr, URL_BASE, testrun_id);
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.post(target_url)
|
||||
.body(probe_outcome)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|response| response.error_for_status())?;
|
||||
|
||||
tracing::debug!("Submitted results: {})", res.status());
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::cli::Args;
|
||||
use clap::Parser;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{filter::Directive, EnvFilter};
|
||||
|
||||
mod cli;
|
||||
mod probe;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
setup_tracing();
|
||||
let args = Args::parse();
|
||||
|
||||
let server_addr = format!("{}:{}", args.server_address, args.server_port);
|
||||
test_ns_api_conn(&server_addr).await?;
|
||||
|
||||
args.execute().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_ns_api_conn(server_addr: &str) -> anyhow::Result<()> {
|
||||
reqwest::get(server_addr)
|
||||
.await
|
||||
.map(|res| {
|
||||
tracing::info!(
|
||||
"Testing connection to NS API at {server_addr}: {}",
|
||||
res.status()
|
||||
);
|
||||
})
|
||||
.map_err(|err| anyhow::anyhow!("Couldn't connect to server on {}: {}", server_addr, err))
|
||||
}
|
||||
|
||||
pub(crate) fn setup_tracing() {
|
||||
fn directive_checked(directive: impl Into<String>) -> Directive {
|
||||
directive
|
||||
.into()
|
||||
.parse()
|
||||
.expect("Failed to parse log directive")
|
||||
}
|
||||
|
||||
let log_builder = tracing_subscriber::fmt()
|
||||
// Use a more compact, abbreviated log format
|
||||
.compact()
|
||||
// Display source code file paths
|
||||
.with_file(true)
|
||||
// Display source code line numbers
|
||||
.with_line_number(true)
|
||||
.with_thread_ids(true)
|
||||
// Don't display the event's target (module path)
|
||||
.with_target(false);
|
||||
|
||||
let mut filter = EnvFilter::builder()
|
||||
// if RUST_LOG isn't set, set default level
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
// these crates are more granularly filtered
|
||||
let filter_crates = [
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"hyper",
|
||||
"sqlx",
|
||||
"h2",
|
||||
"tendermint_rpc",
|
||||
"tower_http",
|
||||
"axum",
|
||||
];
|
||||
for crate_name in filter_crates {
|
||||
filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name)));
|
||||
}
|
||||
|
||||
filter = filter.add_directive(directive_checked("nym_bin_common=debug"));
|
||||
filter = filter.add_directive(directive_checked("nym_explorer_client=debug"));
|
||||
filter = filter.add_directive(directive_checked("nym_network_defaults=debug"));
|
||||
filter = filter.add_directive(directive_checked("nym_validator_client=debug"));
|
||||
|
||||
log_builder.with_env_filter(filter).init();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use tracing::error;
|
||||
|
||||
pub(crate) struct GwProbe {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl GwProbe {
|
||||
pub(crate) fn new(probe_path: String) -> Self {
|
||||
Self { path: probe_path }
|
||||
}
|
||||
|
||||
pub(crate) async fn version(&self) -> String {
|
||||
let mut command = tokio::process::Command::new(&self.path);
|
||||
command.stdout(std::process::Stdio::piped());
|
||||
command.arg("--version");
|
||||
|
||||
match command.spawn() {
|
||||
Ok(child) => {
|
||||
if let Ok(output) = child.wait_with_output().await {
|
||||
return String::from_utf8(output.stdout)
|
||||
.unwrap_or("Unable to get log from test run".to_string());
|
||||
}
|
||||
"Unable to get probe version".to_string()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get probe version: {}", e);
|
||||
"Failed to get probe version".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_and_get_log(&self, gateway_key: &Option<String>) -> String {
|
||||
let mut command = std::process::Command::new(&self.path);
|
||||
command.stdout(std::process::Stdio::piped());
|
||||
|
||||
if let Some(gateway_id) = gateway_key {
|
||||
command.arg("--gateway").arg(gateway_id);
|
||||
}
|
||||
|
||||
match command.spawn() {
|
||||
Ok(child) => {
|
||||
if let Ok(output) = child.wait_with_output() {
|
||||
return String::from_utf8(output.stdout)
|
||||
.unwrap_or("Unable to get log from test run".to_string());
|
||||
}
|
||||
"Unable to get log from test run".to_string()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to spawn test: {}", e);
|
||||
"Failed to spawn test run task".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,6 @@
|
||||
data/
|
||||
enter_db.sh
|
||||
nym-gateway-probe
|
||||
nym-node-status-api
|
||||
*.sqlite
|
||||
*.sqlite-journal
|
||||
|
||||
@@ -14,27 +14,31 @@ rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true, features = ["tokio"] }
|
||||
axum = { workspace = true, features = ["tokio", "macros"] }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["cargo", "derive", "env", "string"] }
|
||||
cosmwasm-std = { workspace = true }
|
||||
envy = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
moka = { workspace = true, features = ["future"] }
|
||||
nym-bin-common = { path = "../common/bin-common" }
|
||||
nym-bin-common = { path = "../common/bin-common", features = ["models"]}
|
||||
nym-common-models = { path = "../common/models" }
|
||||
nym-explorer-client = { path = "../explorer-api/explorer-client" }
|
||||
# TODO dz: ref before Nym API client changes. Update to latest develop once new Nym API is live
|
||||
nym-network-defaults = { git = "https://github.com/nymtech/nym", rev = "f86e08866" }
|
||||
nym-validator-client = { git = "https://github.com/nymtech/nym", rev = "f86e08866" }
|
||||
# TODO dz: before Nym API client breaking changes. Update to latest develop once new Nym API is live
|
||||
nym-network-defaults = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" }
|
||||
nym-validator-client = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" }
|
||||
# nym-network-defaults = { path = "../common/network-defaults" }
|
||||
# nym-validator-client = { path = "../common/client-libs/validator-client" }
|
||||
nym-task = { path = "../common/task" }
|
||||
nym-node-requests = { git = "https://github.com/nymtech/nym", rev = "f86e08866" }
|
||||
nym-node-requests = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" }
|
||||
# nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] }
|
||||
regex = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_json_path = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
|
||||
WORKDIR /nym
|
||||
|
||||
COPY nym-node-status-api/nym-node-status-api ./
|
||||
ENTRYPOINT [ "/nym/nym-node-status-api" ]
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
use std::fs::Permissions;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite";
|
||||
@@ -41,6 +43,9 @@ async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Resu
|
||||
let mut file = File::create("enter_db.sh").await?;
|
||||
let _ = file.write(b"#!/bin/bash\n").await?;
|
||||
file.write_all(format!("sqlite3 {}/{}", out_dir, db_filename).as_bytes())
|
||||
.await?;
|
||||
|
||||
file.set_permissions(Permissions::from_mode(0o755))
|
||||
.await
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,34 @@ export RUST_LOG=${RUST_LOG:-debug}
|
||||
|
||||
export NYM_API_CLIENT_TIMEOUT=60
|
||||
export EXPLORER_CLIENT_TIMEOUT=60
|
||||
#export NYXD=https://rpc.nymtech.net
|
||||
#export NYM_API=https://validator.nymtech.net/api/
|
||||
#export EXPLORER_API=https://explorer.nymtech.net/api/
|
||||
#export NETWORK_NAME=mainnet
|
||||
|
||||
#cargo run --package nym-node-status-api --release -- --connection-url "sqlite://node-status-api.sqlite?mode=rwc"
|
||||
export ENVIRONMENT="mainnet.env"
|
||||
|
||||
cd ..
|
||||
docker build -t node-status-api -f nym-node-status-api/Dockerfile .
|
||||
docker run --env-file envs/mainnet.env -e NYM_NODE_STATUS_API_CONNECTION_URL="sqlite://node-status-api.sqlite?mode=rwc" node-status-api
|
||||
function run_bare() {
|
||||
# export necessary env vars
|
||||
set -a
|
||||
source ../envs/$ENVIRONMENT
|
||||
set +a
|
||||
export RUST_LOG=debug
|
||||
|
||||
# --conection-url is provided in build.rs
|
||||
cargo run --package nym-node-status-api
|
||||
}
|
||||
|
||||
function run_docker() {
|
||||
cargo build --package nym-node-status-api --release
|
||||
cp ../target/release/nym-node-status-api .
|
||||
|
||||
cd ..
|
||||
docker build -t node-status-api -f nym-node-status-api/Dockerfile.dev .
|
||||
docker run --env-file envs/${ENVIRONMENT} \
|
||||
-e EXPLORER_CLIENT_TIMEOUT=$EXPLORER_CLIENT_TIMEOUT \
|
||||
-e NYM_API_CLIENT_TIMEOUT=$NYM_API_CLIENT_TIMEOUT \
|
||||
-e DATABASE_URL="sqlite://node-status-api.sqlite?mode=rwc" \
|
||||
-e RUST_LOG=${RUST_LOG} node-status-api
|
||||
|
||||
}
|
||||
|
||||
run_bare
|
||||
|
||||
# run_docker
|
||||
|
||||
@@ -98,3 +98,15 @@ CREATE TABLE
|
||||
FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id),
|
||||
UNIQUE (mix_id, date_utc) -- This constraint automatically creates an index
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE testruns
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
gateway_id INTEGER,
|
||||
status INTEGER NOT NULL, -- 0=pending, 1=in-progress, 2=complete
|
||||
timestamp_utc INTEGER NOT NULL,
|
||||
ip_address VARCHAR NOT NULL,
|
||||
log VARCHAR NOT NULL,
|
||||
FOREIGN KEY (gateway_id) REFERENCES gateways (id)
|
||||
);
|
||||
|
||||
@@ -41,26 +41,34 @@ pub(crate) struct Cli {
|
||||
pub(crate) nyxd_addr: Url,
|
||||
|
||||
/// Nym api client timeout.
|
||||
#[clap(
|
||||
long,
|
||||
default_value = "15",
|
||||
env = "NYM_NODE_STATUS_API_NYM_API_CLIENT_TIMEOUT"
|
||||
)]
|
||||
#[clap(long, default_value = "15", env = "NYM_API_CLIENT_TIMEOUT")]
|
||||
#[arg(value_parser = parse_duration)]
|
||||
pub(crate) nym_api_client_timeout: Duration,
|
||||
|
||||
/// Explorer api client timeout.
|
||||
#[clap(
|
||||
long,
|
||||
default_value = "15",
|
||||
env = "NYM_NODE_STATUS_API_EXPLORER_CLIENT_TIMEOUT"
|
||||
)]
|
||||
#[clap(long, default_value = "15", env = "EXPLORER_CLIENT_TIMEOUT")]
|
||||
#[arg(value_parser = parse_duration)]
|
||||
pub(crate) explorer_client_timeout: Duration,
|
||||
|
||||
/// Connection url for the database.
|
||||
#[clap(long, env = "NYM_NODE_STATUS_API_CONNECTION_URL")]
|
||||
pub(crate) connection_url: String,
|
||||
#[clap(long, env = "DATABASE_URL")]
|
||||
pub(crate) database_url: String,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
default_value = "600",
|
||||
env = "NODE_STATUS_API_MONITOR_REFRESH_INTERVAL"
|
||||
)]
|
||||
#[arg(value_parser = parse_duration)]
|
||||
pub(crate) monitor_refresh_interval: Duration,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
default_value = "600",
|
||||
env = "NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL"
|
||||
)]
|
||||
#[arg(value_parser = parse_duration)]
|
||||
pub(crate) testruns_refresh_interval: Duration,
|
||||
}
|
||||
|
||||
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod queries;
|
||||
@@ -30,7 +29,7 @@ impl Storage {
|
||||
}
|
||||
|
||||
/// Cloning pool is cheap, it's the same underlying set of connections
|
||||
pub async fn pool_owned(&self) -> DbPool {
|
||||
pub fn pool_owned(&self) -> DbPool {
|
||||
self.pool.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
};
|
||||
use nym_node_requests::api::v1::node::models::NodeDescription;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::{EnumString, FromRepr};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) struct GatewayRecord {
|
||||
@@ -298,3 +299,36 @@ pub(crate) mod gateway {
|
||||
pub(crate) last_updated_utc: String,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestRunDto {
|
||||
pub id: i64,
|
||||
pub gateway_id: i64,
|
||||
pub status: i64,
|
||||
pub timestamp_utc: i64,
|
||||
pub ip_address: String,
|
||||
pub log: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, strum_macros::Display, EnumString, FromRepr, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub(crate) enum TestRunStatus {
|
||||
Complete = 2,
|
||||
InProgress = 1,
|
||||
Pending = 0,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GatewayIdentityDto {
|
||||
pub gateway_identity_key: String,
|
||||
pub bonded: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GatewayInfoDto {
|
||||
pub id: i64,
|
||||
pub gateway_identity_key: String,
|
||||
pub self_described: Option<String>,
|
||||
pub explorer_pretty_bond: Option<String>,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ mod gateways;
|
||||
mod misc;
|
||||
mod mixnodes;
|
||||
mod summary;
|
||||
pub(crate) mod testruns;
|
||||
|
||||
pub(crate) use gateways::{
|
||||
ensure_gateways_still_bonded, get_all_gateways, insert_gateways,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
use crate::http::models::TestrunAssignment;
|
||||
use crate::{
|
||||
db::models::{TestRunDto, TestRunStatus},
|
||||
testruns::now_utc,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use sqlx::{pool::PoolConnection, Sqlite};
|
||||
|
||||
pub(crate) async fn get_testrun_by_id(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
testrun_id: i64,
|
||||
) -> anyhow::Result<TestRunDto> {
|
||||
sqlx::query_as!(
|
||||
TestRunDto,
|
||||
r#"SELECT
|
||||
id as "id!",
|
||||
gateway_id as "gateway_id!",
|
||||
status as "status!",
|
||||
timestamp_utc as "timestamp_utc!",
|
||||
ip_address as "ip_address!",
|
||||
log as "log!"
|
||||
FROM testruns
|
||||
WHERE id = ?
|
||||
ORDER BY timestamp_utc"#,
|
||||
testrun_id
|
||||
)
|
||||
.fetch_one(conn.as_mut())
|
||||
.await
|
||||
.context(format!("Couldn't retrieve testrun {testrun_id}"))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_oldest_testrun_and_make_it_pending(
|
||||
// TODO dz accept mut reference, repeat in all similar functions
|
||||
conn: PoolConnection<Sqlite>,
|
||||
) -> anyhow::Result<Option<TestrunAssignment>> {
|
||||
let mut conn = conn;
|
||||
let assignment = sqlx::query_as!(
|
||||
TestrunAssignment,
|
||||
r#"UPDATE testruns
|
||||
SET status = ?
|
||||
WHERE rowid =
|
||||
(
|
||||
SELECT rowid
|
||||
FROM testruns
|
||||
WHERE status = ?
|
||||
ORDER BY timestamp_utc asc
|
||||
LIMIT 1
|
||||
)
|
||||
RETURNING
|
||||
id as "testrun_id!",
|
||||
gateway_id as "gateway_pk_id!"
|
||||
"#,
|
||||
TestRunStatus::InProgress as i64,
|
||||
TestRunStatus::Pending as i64,
|
||||
)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(assignment)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_testrun_status(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
testrun_id: i64,
|
||||
status: TestRunStatus,
|
||||
) -> anyhow::Result<()> {
|
||||
let status = status as u32;
|
||||
sqlx::query!(
|
||||
"UPDATE testruns SET status = ? WHERE id = ?",
|
||||
status,
|
||||
testrun_id
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_gateway_last_probe_log(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
gateway_pk: i64,
|
||||
log: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE gateways SET last_probe_log = ? WHERE id = ?",
|
||||
log,
|
||||
gateway_pk
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await
|
||||
.map(drop)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_gateway_last_probe_result(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
gateway_pk: i64,
|
||||
result: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE gateways SET last_probe_result = ? WHERE id = ?",
|
||||
result,
|
||||
gateway_pk
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await
|
||||
.map(drop)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_gateway_score(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
gateway_pk: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
let now = now_utc().timestamp();
|
||||
sqlx::query!(
|
||||
"UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?",
|
||||
now,
|
||||
now,
|
||||
gateway_pk
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await
|
||||
.map(drop)
|
||||
.map_err(From::from)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", axum::routing::get(gateways))
|
||||
.route("/skinny", axum::routing::get(gateways_skinny))
|
||||
.route("/skinny/:identity_key", axum::routing::get(get_gateway))
|
||||
.route("/:identity_key", axum::routing::get(get_gateway))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
|
||||
@@ -35,7 +35,10 @@ impl RouterBuilder {
|
||||
.nest("/mixnodes", mixnodes::routes())
|
||||
.nest("/services", services::routes())
|
||||
.nest("/summary", summary::routes()),
|
||||
// .nest("/testruns", testruns::_routes()),
|
||||
)
|
||||
.nest(
|
||||
"/internal",
|
||||
Router::new().nest("/testruns", testruns::routes()),
|
||||
);
|
||||
|
||||
Self {
|
||||
|
||||
@@ -1,7 +1,116 @@
|
||||
use axum::Router;
|
||||
use axum::Json;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
Router,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::http::state::AppState;
|
||||
use crate::db::models::TestRunStatus;
|
||||
use crate::db::queries;
|
||||
use crate::{
|
||||
db,
|
||||
http::{
|
||||
error::{HttpError, HttpResult},
|
||||
models::TestrunAssignment,
|
||||
state::AppState,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) fn _routes() -> Router<AppState> {
|
||||
unimplemented!()
|
||||
// TODO dz consider adding endpoint to trigger testrun scan for a given gateway_id
|
||||
// like in H< src/http/testruns.rs
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", axum::routing::get(request_testrun))
|
||||
.route("/:testrun_id", axum::routing::post(submit_testrun))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn request_testrun(State(state): State<AppState>) -> HttpResult<Json<TestrunAssignment>> {
|
||||
// TODO dz log agent's key
|
||||
// TODO dz log agent's network probe version
|
||||
tracing::debug!("Agent X requested testrun");
|
||||
|
||||
let db = state.db_pool();
|
||||
let conn = db
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
|
||||
return match db::queries::testruns::get_oldest_testrun_and_make_it_pending(conn).await {
|
||||
Ok(res) => {
|
||||
if let Some(testrun) = res {
|
||||
// TODO dz consider adding a column to testruns table with agent's public key
|
||||
tracing::debug!(
|
||||
"🏃 Assigned testrun row_id {} to agent X",
|
||||
&testrun.testrun_id
|
||||
);
|
||||
Ok(Json(testrun))
|
||||
} else {
|
||||
Err(HttpError::not_found("No testruns available"))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(HttpError::internal_with_logging(err)),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO dz accept testrun_id as query parameter
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn submit_testrun(
|
||||
Path(testrun_id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
body: String,
|
||||
) -> HttpResult<StatusCode> {
|
||||
tracing::debug!(
|
||||
"Agent submitted testrun {}. Total length: {}",
|
||||
testrun_id,
|
||||
body.len(),
|
||||
);
|
||||
// TODO dz store testrun results
|
||||
|
||||
let db = state.db_pool();
|
||||
let mut conn = db
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
|
||||
let testrun = queries::testruns::get_testrun_by_id(&mut conn, testrun_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e}");
|
||||
HttpError::not_found(testrun_id)
|
||||
})?;
|
||||
// TODO dz this should be part of a single transaction: commit after everything is done
|
||||
queries::testruns::update_testrun_status(&mut conn, testrun_id, TestRunStatus::Complete)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
queries::testruns::update_gateway_last_probe_log(&mut conn, testrun.gateway_id, &body)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
let result = get_result_from_log(&body);
|
||||
queries::testruns::update_gateway_last_probe_result(&mut conn, testrun.gateway_id, &result)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
queries::testruns::update_gateway_score(&mut conn, testrun.gateway_id)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
// TODO dz log gw identity key
|
||||
|
||||
tracing::info!(
|
||||
"✅ Testrun row_id {} for gateway {} complete",
|
||||
testrun.id,
|
||||
testrun.gateway_id
|
||||
);
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
fn get_result_from_log(log: &str) -> String {
|
||||
let re = regex::Regex::new(r"\n\{\s").unwrap();
|
||||
let result: Vec<_> = re.splitn(log, 2).collect();
|
||||
if result.len() == 2 {
|
||||
let res = format!("{} {}", "{", result[1]).to_string();
|
||||
return res;
|
||||
}
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use utoipauto::utoipauto;
|
||||
#[utoipauto(paths = "./nym-node-status-api/src")]
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(title = "Nym API"),
|
||||
info(title = "Node Status API"),
|
||||
tags(),
|
||||
components(schemas(nym_node_requests::api::v1::node::models::NodeDescription,))
|
||||
)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
pub(crate) type HttpResult<T> = Result<T, HttpError>;
|
||||
|
||||
pub(crate) struct HttpError {
|
||||
@@ -13,12 +15,24 @@ impl HttpError {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn internal_with_logging(msg: impl Display) -> Self {
|
||||
tracing::error!("{}", msg.to_string());
|
||||
Self::internal()
|
||||
}
|
||||
|
||||
pub(crate) fn internal() -> Self {
|
||||
Self {
|
||||
message: serde_json::json!({"message": "Internal server error"}).to_string(),
|
||||
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn not_found(msg: impl Display) -> Self {
|
||||
Self {
|
||||
message: serde_json::json!({"message": msg.to_string()}).to_string(),
|
||||
status: axum::http::StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::response::IntoResponse for HttpError {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::db::models::TestRunDto;
|
||||
use nym_node_requests::api::v1::node::models::NodeDescription;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) use nym_common_models::ns_api::TestrunAssignment;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Gateway {
|
||||
pub gateway_identity_key: String,
|
||||
@@ -72,3 +75,12 @@ pub(crate) struct SummaryHistory {
|
||||
pub value_json: serde_json::Value,
|
||||
pub timestamp_utc: String,
|
||||
}
|
||||
|
||||
impl From<TestRunDto> for TestrunAssignment {
|
||||
fn from(value: TestRunDto) -> Self {
|
||||
Self {
|
||||
gateway_pk_id: value.gateway_id,
|
||||
testrun_id: value.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ pub(crate) async fn start_http_api(
|
||||
|
||||
// TODO dz do we need this to be configurable?
|
||||
let bind_addr = format!("0.0.0.0:{}", http_port);
|
||||
tracing::info!("Binding server to {bind_addr}");
|
||||
let server = router.build_server(bind_addr).await?;
|
||||
|
||||
Ok(start_server(server))
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{filter::Directive, EnvFilter};
|
||||
|
||||
pub(crate) fn setup_tracing_logger() {
|
||||
fn directive_checked(directive: impl Into<String>) -> Directive {
|
||||
directive
|
||||
.into()
|
||||
.parse()
|
||||
.expect("Failed to parse log directive")
|
||||
// TODO dz you can get the tracing-subscriber via basic-tracing feature on nym-bin-common
|
||||
pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> {
|
||||
fn directive_checked(directive: impl Into<String>) -> anyhow::Result<Directive> {
|
||||
directive.into().parse().map_err(From::from)
|
||||
}
|
||||
|
||||
let log_builder = tracing_subscriber::fmt()
|
||||
@@ -22,10 +20,11 @@ pub(crate) fn setup_tracing_logger() {
|
||||
|
||||
let mut filter = EnvFilter::builder()
|
||||
// if RUST_LOG isn't set, set default level
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.with_default_directive(LevelFilter::DEBUG.into())
|
||||
.from_env_lossy();
|
||||
|
||||
// these crates are more granularly filtered
|
||||
let filter_crates = [
|
||||
let warn_crates = [
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"hyper",
|
||||
@@ -35,14 +34,36 @@ pub(crate) fn setup_tracing_logger() {
|
||||
"tower_http",
|
||||
"axum",
|
||||
];
|
||||
for crate_name in filter_crates {
|
||||
filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name)));
|
||||
for crate_name in warn_crates {
|
||||
filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?);
|
||||
}
|
||||
|
||||
filter = filter.add_directive(directive_checked("nym_bin_common=debug"));
|
||||
filter = filter.add_directive(directive_checked("nym_explorer_client=debug"));
|
||||
filter = filter.add_directive(directive_checked("nym_network_defaults=debug"));
|
||||
filter = filter.add_directive(directive_checked("nym_validator_client=debug"));
|
||||
let log_level_hint = filter.max_level_hint();
|
||||
|
||||
// debug or higher granularity (e.g. trace)
|
||||
let debug_or_higher = std::cmp::max(
|
||||
log_level_hint.unwrap_or(LevelFilter::DEBUG),
|
||||
LevelFilter::DEBUG,
|
||||
);
|
||||
filter = filter.add_directive(directive_checked(format!(
|
||||
"nym_bin_common={}",
|
||||
debug_or_higher
|
||||
))?);
|
||||
filter = filter.add_directive(directive_checked(format!(
|
||||
"nym_explorer_client={}",
|
||||
debug_or_higher
|
||||
))?);
|
||||
filter = filter.add_directive(directive_checked(format!(
|
||||
"nym_network_defaults={}",
|
||||
debug_or_higher
|
||||
))?);
|
||||
filter = filter.add_directive(directive_checked(format!(
|
||||
"nym_validator_client={}",
|
||||
debug_or_higher
|
||||
))?);
|
||||
|
||||
log_builder.with_env_filter(filter).init();
|
||||
tracing::info!("Log level: {:?}", log_level_hint);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,26 +6,35 @@ mod db;
|
||||
mod http;
|
||||
mod logging;
|
||||
mod monitor;
|
||||
mod testruns;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
logging::setup_tracing_logger();
|
||||
logging::setup_tracing_logger()?;
|
||||
|
||||
let args = cli::Cli::parse();
|
||||
|
||||
let connection_url = args.connection_url.clone();
|
||||
let connection_url = args.database_url.clone();
|
||||
tracing::debug!("Using config:\n{:#?}", args);
|
||||
|
||||
let storage = db::Storage::init(connection_url).await?;
|
||||
let db_pool = storage.pool_owned().await;
|
||||
let db_pool = storage.pool_owned();
|
||||
let args_clone = args.clone();
|
||||
tokio::spawn(async move {
|
||||
monitor::spawn_in_background(db_pool, args_clone).await;
|
||||
monitor::spawn_in_background(
|
||||
db_pool,
|
||||
args_clone.explorer_client_timeout,
|
||||
args_clone.nym_api_client_timeout,
|
||||
&args_clone.nyxd_addr,
|
||||
args_clone.monitor_refresh_interval,
|
||||
)
|
||||
.await;
|
||||
tracing::info!("Started monitor task");
|
||||
});
|
||||
tracing::info!("Started monitor task");
|
||||
testruns::spawn(storage.pool_owned(), args.testruns_refresh_interval).await;
|
||||
|
||||
let shutdown_handles = http::server::start_http_api(
|
||||
storage.pool_owned().await,
|
||||
storage.pool_owned(),
|
||||
args.http_port,
|
||||
args.nym_http_cache_ttl,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::cli::Cli;
|
||||
use crate::db::models::{
|
||||
gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT,
|
||||
GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT,
|
||||
@@ -19,34 +18,50 @@ use nym_validator_client::NymApiClient;
|
||||
use reqwest::Url;
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::Duration;
|
||||
use tracing::instrument;
|
||||
|
||||
const REFRESH_DELAY: Duration = Duration::from_secs(60 * 5);
|
||||
const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(15);
|
||||
// TODO dz should be configurable
|
||||
const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60);
|
||||
|
||||
static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw";
|
||||
|
||||
// TODO dz: query many NYM APIs:
|
||||
// multiple instances running directory cache, ask sachin
|
||||
pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Cli) -> JoinHandle<()> {
|
||||
#[instrument(level = "debug", name = "data_monitor", skip_all)]
|
||||
pub(crate) async fn spawn_in_background(
|
||||
db_pool: DbPool,
|
||||
explorer_client_timeout: Duration,
|
||||
nym_api_client_timeout: Duration,
|
||||
nyxd_addr: &Url,
|
||||
refresh_interval: Duration,
|
||||
) {
|
||||
let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env();
|
||||
|
||||
loop {
|
||||
tracing::info!("Refreshing node info...");
|
||||
|
||||
if let Err(e) = run(&db_pool, &network_defaults, &config).await {
|
||||
if let Err(e) = run(
|
||||
&db_pool,
|
||||
&network_defaults,
|
||||
explorer_client_timeout,
|
||||
nym_api_client_timeout,
|
||||
nyxd_addr,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Monitor run failed: {e}, retrying in {}s...",
|
||||
FAILURE_RETRY_DELAY.as_secs()
|
||||
);
|
||||
// TODO dz implement some sort of backoff
|
||||
tokio::time::sleep(FAILURE_RETRY_DELAY).await;
|
||||
} else {
|
||||
tracing::info!(
|
||||
"Info successfully collected, sleeping for {}s...",
|
||||
REFRESH_DELAY.as_secs()
|
||||
refresh_interval.as_secs()
|
||||
);
|
||||
tokio::time::sleep(REFRESH_DELAY).await;
|
||||
tokio::time::sleep(refresh_interval).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +69,9 @@ pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Cli) -> JoinHan
|
||||
async fn run(
|
||||
pool: &DbPool,
|
||||
network_details: &NymNetworkDetails,
|
||||
config: &Cli,
|
||||
explorer_client_timeout: Duration,
|
||||
nym_api_client_timeout: Duration,
|
||||
nyxd_addr: &Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let default_api_url = network_details
|
||||
.endpoints
|
||||
@@ -68,19 +85,17 @@ async fn run(
|
||||
.expect("rust sdk mainnet default explorer url not parseable")
|
||||
});
|
||||
|
||||
// TODO dz replace explorer api with ipinfo.io
|
||||
let default_explorer_url =
|
||||
default_explorer_url.expect("explorer url missing in network config");
|
||||
let explorer_client =
|
||||
ExplorerClient::new_with_timeout(default_explorer_url, config.explorer_client_timeout)?;
|
||||
ExplorerClient::new_with_timeout(default_explorer_url, explorer_client_timeout)?;
|
||||
let explorer_gateways = explorer_client
|
||||
.get_gateways()
|
||||
.await
|
||||
.log_error("get_gateways")?;
|
||||
tracing::debug!("6");
|
||||
|
||||
let api_client =
|
||||
// TODO dz introduce timeout ?
|
||||
NymApiClient::new(default_api_url);
|
||||
let api_client = NymApiClient::new_with_timeout(default_api_url, nym_api_client_timeout);
|
||||
let gateways = api_client
|
||||
.get_cached_described_gateways()
|
||||
.await
|
||||
@@ -97,14 +112,16 @@ async fn run(
|
||||
.log_error("get_cached_mixnodes")?;
|
||||
tracing::debug!("Fetched {} mixnodes", mixnodes.len());
|
||||
|
||||
// TODO dz can we calculate blacklisted GWs from their performance?
|
||||
// where do we get their performance?
|
||||
let gateways_blacklisted = api_client
|
||||
.nym_api
|
||||
.get_gateways_blacklisted()
|
||||
.await
|
||||
.map(|vec| vec.into_iter().collect::<HashSet<_>>())
|
||||
.log_error("get_gateways_blacklisted")?;
|
||||
let gateways_blacklisted = skimmed_gateways
|
||||
.iter()
|
||||
.filter_map(|gw| {
|
||||
if gw.performance.round_to_integer() <= 50 {
|
||||
Some(gw.ed25519_identity_pubkey.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// Cached mixnodes don't include blacklisted nodes
|
||||
// We need that to calculate the total locked tokens later
|
||||
@@ -124,7 +141,7 @@ async fn run(
|
||||
.await
|
||||
.log_error("get_active_mixnodes")?;
|
||||
let delegation_program_members =
|
||||
get_delegation_program_details(network_details, &config.nyxd_addr).await?;
|
||||
get_delegation_program_details(network_details, nyxd_addr).await?;
|
||||
|
||||
// keep stats for later
|
||||
let count_bonded_mixnodes = mixnodes.len();
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
use crate::db::models::GatewayIdentityDto;
|
||||
use crate::db::DbPool;
|
||||
use futures_util::TryStreamExt;
|
||||
use std::time::Duration;
|
||||
use tracing::instrument;
|
||||
|
||||
pub(crate) mod models;
|
||||
mod queue;
|
||||
pub(crate) use queue::now_utc;
|
||||
|
||||
pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tracing::info!("Spawning testruns...");
|
||||
|
||||
if let Err(e) = run(&pool).await {
|
||||
tracing::error!("Cron job failed: {}", e);
|
||||
}
|
||||
tracing::debug!("Sleeping for {}s...", refresh_interval.as_secs());
|
||||
tokio::time::sleep(refresh_interval).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO dz make number of max agents configurable
|
||||
|
||||
// TODO dz periodically clean up stale pending testruns
|
||||
#[instrument(level = "debug", name = "testrun_queue", skip_all)]
|
||||
async fn run(pool: &DbPool) -> anyhow::Result<()> {
|
||||
if pool.is_closed() {
|
||||
tracing::debug!("DB pool closed, returning early");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut conn = pool.acquire().await?;
|
||||
|
||||
let gateways = sqlx::query_as!(
|
||||
GatewayIdentityDto,
|
||||
r#"SELECT
|
||||
gateway_identity_key as "gateway_identity_key!",
|
||||
bonded as "bonded: bool"
|
||||
FROM gateways
|
||||
ORDER BY last_testrun_utc"#,
|
||||
)
|
||||
.fetch(&mut *conn)
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
// TODO dz this filtering could be done in SQL
|
||||
let gateways: Vec<GatewayIdentityDto> = gateways.into_iter().filter(|g| g.bonded).collect();
|
||||
|
||||
tracing::debug!("Trying to queue {} testruns", gateways.len());
|
||||
let mut testruns_created = 0;
|
||||
for gateway in gateways {
|
||||
if let Err(e) = queue::try_queue_testrun(
|
||||
&mut conn,
|
||||
gateway.gateway_identity_key.clone(),
|
||||
// TODO dz read from config
|
||||
"127.0.0.1".to_string(),
|
||||
)
|
||||
.await
|
||||
// TODO dz measure how many were actually inserted and how many were skipped
|
||||
{
|
||||
tracing::debug!(
|
||||
"Skipping test for identity {} with error {}",
|
||||
&gateway.gateway_identity_key,
|
||||
e
|
||||
);
|
||||
} else {
|
||||
testruns_created += 1;
|
||||
}
|
||||
}
|
||||
tracing::debug!("{} testruns queued in total", testruns_created);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GatewayIdentityDto {
|
||||
pub gateway_identity_key: String,
|
||||
pub bonded: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct TestRun {
|
||||
pub id: u32,
|
||||
pub identity_key: String,
|
||||
pub status: String,
|
||||
pub log: String,
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunStatus};
|
||||
use crate::testruns::models::TestRun;
|
||||
use anyhow::anyhow;
|
||||
use chrono::DateTime;
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::Sqlite;
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub(crate) async fn try_queue_testrun(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
identity_key: String,
|
||||
ip_address: String,
|
||||
) -> anyhow::Result<TestRun> {
|
||||
let timestamp = now_utc().timestamp();
|
||||
let timestamp_pretty = now_utc_as_rfc3339();
|
||||
|
||||
let items = sqlx::query_as!(
|
||||
GatewayInfoDto,
|
||||
r#"SELECT
|
||||
id as "id!",
|
||||
gateway_identity_key as "gateway_identity_key!",
|
||||
self_described as "self_described?",
|
||||
explorer_pretty_bond as "explorer_pretty_bond?"
|
||||
FROM gateways
|
||||
WHERE gateway_identity_key = ?
|
||||
ORDER BY gateway_identity_key
|
||||
LIMIT 1"#,
|
||||
identity_key,
|
||||
)
|
||||
// TODO dz shoudl call .fetch_one
|
||||
// TODO dz replace this in other queries as well
|
||||
.fetch(conn.as_mut())
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
let gateway = items
|
||||
.iter()
|
||||
.find(|g| g.gateway_identity_key == identity_key);
|
||||
|
||||
// TODO dz if let Some() = gateway.first() ...
|
||||
if gateway.is_none() {
|
||||
return Err(anyhow!("Unknown gateway {identity_key}"));
|
||||
}
|
||||
let gateway_id = gateway.unwrap().id;
|
||||
|
||||
//
|
||||
// check if there is already a test run for this gateway
|
||||
//
|
||||
let items = sqlx::query_as!(
|
||||
TestRunDto,
|
||||
r#"SELECT
|
||||
id as "id!",
|
||||
gateway_id as "gateway_id!",
|
||||
status as "status!",
|
||||
timestamp_utc as "timestamp_utc!",
|
||||
ip_address as "ip_address!",
|
||||
log as "log!"
|
||||
FROM testruns
|
||||
WHERE gateway_id = ? AND status != 2
|
||||
ORDER BY id DESC
|
||||
LIMIT 1"#,
|
||||
gateway_id,
|
||||
)
|
||||
.fetch(conn.as_mut())
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
if !items.is_empty() {
|
||||
let testrun = items.first().unwrap();
|
||||
return Ok(TestRun {
|
||||
id: testrun.id as u32,
|
||||
identity_key,
|
||||
status: format!(
|
||||
"{}",
|
||||
TestRunStatus::from_repr(testrun.status as u8).unwrap()
|
||||
),
|
||||
log: testrun.log.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// save test run
|
||||
//
|
||||
let status = TestRunStatus::Pending as u32;
|
||||
let log = format!(
|
||||
"Test for {identity_key} requested at {} UTC\n\n",
|
||||
timestamp_pretty
|
||||
);
|
||||
|
||||
let id = sqlx::query!(
|
||||
"INSERT INTO testruns (gateway_id, status, ip_address, timestamp_utc, log) VALUES (?, ?, ?, ?, ?)",
|
||||
gateway_id,
|
||||
status,
|
||||
ip_address,
|
||||
timestamp,
|
||||
log,
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await?
|
||||
.last_insert_rowid();
|
||||
|
||||
Ok(TestRun {
|
||||
id: id as u32,
|
||||
identity_key,
|
||||
status: format!("{}", TestRunStatus::Pending),
|
||||
log,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO dz do we need these?
|
||||
pub fn now_utc() -> DateTime<chrono::Utc> {
|
||||
SystemTime::now().into()
|
||||
}
|
||||
|
||||
pub fn now_utc_as_rfc3339() -> String {
|
||||
now_utc().to_rfc3339()
|
||||
}
|
||||
Reference in New Issue
Block a user