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:
Dinko Zdravac
2024-10-28 16:53:36 +01:00
committed by dynco-nym
parent 40d9321aec
commit cc983963d4
43 changed files with 1185 additions and 166 deletions
+6 -6
View File
@@ -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
View File
@@ -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",
]
+5
View File
@@ -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"
+1
View File
@@ -45,3 +45,4 @@ tracing = [
"opentelemetry",
]
clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ]
models = []
+12
View File
@@ -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)
+14
View File
@@ -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"] }
+1
View File
@@ -0,0 +1 @@
pub mod ns_api;
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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))
}
}
+1
View File
@@ -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
```
+1
View File
@@ -0,0 +1 @@
nym-gateway-probe
+27
View File
@@ -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 }
+49
View File
@@ -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
+109
View File
@@ -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(())
}
+78
View File
@@ -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();
}
+54
View File
@@ -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()
}
}
}
}
+4
View File
@@ -1,2 +1,6 @@
data/
enter_db.sh
nym-gateway-probe
nym-node-status-api
*.sqlite
*.sqlite-journal
+10 -6
View File
@@ -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"] }
+8
View File
@@ -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" ]
+5
View File
@@ -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)
}
+29 -8
View File
@@ -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)
);
+20 -12
View File
@@ -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> {
+2 -3
View File
@@ -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()
}
}
+34
View File
@@ -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)
}
+1 -1
View File
@@ -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(
+4 -1
View File
@@ -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 {
+113 -4
View File
@@ -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()
}
+1 -1
View File
@@ -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,))
)]
+14
View File
@@ -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 {
+12
View File
@@ -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,
}
}
}
+1
View File
@@ -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))
+35 -14
View File
@@ -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(())
}
+15 -6
View File
@@ -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,
)
+40 -23
View File
@@ -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();
+76
View File
@@ -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,
}
+118
View File
@@ -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()
}