Rest of the data for /gateways

This commit is contained in:
dynco-nym
2025-06-04 09:29:56 +02:00
parent ea08345025
commit f31e6f2470
6 changed files with 92 additions and 92 deletions
@@ -1,5 +1,3 @@
use std::str::FromStr;
use crate::{
http::{self, models::SummaryHistory},
utils::{decimal_to_i64, unix_timestamp_to_utc_rfc3339, NumericalCheckedCast},
@@ -14,6 +12,7 @@ use nym_validator_client::{
};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::str::FromStr;
use strum_macros::{EnumString, FromRepr};
use time::{Date, OffsetDateTime};
use utoipa::ToSchema;
@@ -36,6 +36,8 @@ pub(crate) async fn get_all_nym_nodes(pool: &DbPool) -> anyhow::Result<Vec<NymNo
bond_info as "bond_info: serde_json::Value"
FROM
nym_nodes
ORDER BY
node_id
"#,
)
.fetch(&mut *conn)
@@ -46,20 +46,19 @@ async fn dvpn_gateways(
state: State<AppState>,
) -> HttpResult<Json<Vec<DVpnGateway>>> {
let min_node_version: String = min_node_version.unwrap_or_else(|| String::from("1.6.2"));
let min_node_version = semver::Version::parse(&min_node_version)
let _min_node_version = semver::Version::parse(&min_node_version)
.map_err(|_| HttpError::invalid_input("Min version must be valid semver"))?;
let db = state.db_pool();
let res = state.cache().get_dvpn_gateway_list(db).await;
Ok(Json(res))
Ok(Json(
state.cache().get_dvpn_gateway_list(state.db_pool()).await,
))
}
#[allow(dead_code)] // clippy doesn't detect usage in utoipa macros
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Path)]
struct TwoLetterCountryCodeParam {
#[param(minimum = 2, maximum = 2)]
#[param(min_length = 2, max_length = 2)]
two_letter_country_code: String,
}
@@ -6,6 +6,7 @@ use nym_validator_client::{
models::{
AuthenticatorDetails, BinaryBuildInformationOwned, IpPacketRouterDetails, NymNodeData,
},
nym_api::SkimmedNode,
nym_nodes::{BasicEntryInformation, NodeRole},
};
use serde::{Deserialize, Serialize};
@@ -143,19 +144,9 @@ pub mod wg_outcome_versions {
}
}
impl TryFrom<&Gateway> for DVpnGateway {
type Error = anyhow::Error;
fn try_from(value: &Gateway) -> Result<Self, Self::Error> {
Self::try_from(value.to_owned())
}
}
impl TryFrom<Gateway> for DVpnGateway {
type Error = anyhow::Error;
fn try_from(value: Gateway) -> Result<Self, Self::Error> {
let location = value
impl DVpnGateway {
pub(crate) fn new(gateway: Gateway, skimmed_node: &SkimmedNode) -> anyhow::Result<Self> {
let location = gateway
.explorer_pretty_bond
.ok_or_else(|| anyhow::anyhow!("Missing explorer_pretty_bond"))
.and_then(|value| {
@@ -163,50 +154,18 @@ impl TryFrom<Gateway> for DVpnGateway {
})
.map(|bond| bond.location)?;
let self_described = value
let self_described = gateway
.self_described
.ok_or_else(|| anyhow::anyhow!("Missing self_described"))
.and_then(|value| serde_json::from_value::<NymNodeData>(value).map_err(From::from))?;
let last_probe_result = value
let last_probe_result = gateway
.last_probe_result
.and_then(|value| serde_json::from_value::<LastProbeResult>(value).ok());
// TODO: polyfill `last_probe`, see below from VPN API
/*
const last_testrun_utc = item.last_testrun_utc;
const last_probe: DirectoryGatewayProbe | undefined =
last_testrun_utc && item.last_probe_result?.outcome
? {
last_updated_utc: last_testrun_utc,
outcome: item.last_probe_result.outcome,
}
: undefined;
//
// reshape test probe
//
if (
last_probe?.outcome?.wg &&
isDirectoryGatewayProbeOutcome_WG_V2(last_probe?.outcome?.wg) &&
last_probe?.outcome?.wg?.can_handshake === undefined
) {
last_probe.outcome.wg = {
...last_probe.outcome.wg,
can_handshake: last_probe.outcome.wg.can_handshake_v4,
can_resolve_dns: last_probe.outcome.wg.can_resolve_dns_v4,
ping_hosts_performance:
last_probe.outcome.wg.ping_hosts_performance_v4,
ping_ips_performance: last_probe.outcome.wg.ping_ips_performance_v4,
};
}
*/
Ok(Self {
identity_key: value.gateway_identity_key,
name: value.description.moniker,
identity_key: gateway.gateway_identity_key,
name: gateway.description.moniker,
ip_packet_router: self_described.ip_packet_router,
authenticator: self_described.authenticator,
location: Location {
@@ -215,12 +174,15 @@ impl TryFrom<Gateway> for DVpnGateway {
two_letter_iso_country_code: location.two_letter_iso_country_code,
},
last_probe: last_probe_result.map(|res| res.outcome),
// TODO
ip_addresses: vec![], // value.ip_addresses (SkimmedNode),
mix_port: 0, // value.mix_port (SkimmedNode),
role: NodeRole::EntryGateway, // value.role (SkimmedNode),
entry: None, // (SkimmedNode)
performance: value.performance, // (SkimmedNode)
ip_addresses: skimmed_node
.ip_addresses
.iter()
.map(|ip| ip.to_string())
.collect(),
mix_port: skimmed_node.mix_port,
role: skimmed_node.role.clone(),
entry: skimmed_node.entry.clone(),
performance: gateway.performance,
build_information: self_described.build_information,
})
}
@@ -7,7 +7,7 @@ use nym_mixnet_contract_common::NodeId;
use nym_validator_client::nym_api::SkimmedNode;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::RwLock;
use tracing::instrument;
use tracing::{error, instrument, warn};
use crate::{
db::{queries, DbPool},
@@ -208,47 +208,84 @@ impl HttpCache {
None => {
tracing::trace!("No gateways (dVPN) in cache, refreshing from DB...");
let mut failed_to_convert_gw_count = 0;
let gateways: Vec<DVpnGateway> = self
.get_gateway_list(db)
let gateways = self.get_gateway_list(db).await;
let started_with = gateways.len();
let Ok(skimmed_nodes) = crate::db::queries::get_described_bonded_nym_nodes(db)
.await
.map(|records| {
records
.into_iter()
.filter_map(|dto| SkimmedNode::try_from(dto).ok())
.map(|skimmed_node| {
(
skimmed_node.ed25519_identity_pubkey.to_base58_string(),
skimmed_node,
)
})
.collect::<HashMap<_, _>>()
})
.inspect_err(|err| {
// this would fail only in case of internal error: log and return gracefully
error!("Failed to get nym_nodes from DB: {err}");
})
else {
return Vec::new();
};
let res_gws = gateways
.into_iter()
.filter(|gw| gw.bonded)
.filter_map(|d| {
d.try_into()
.inspect_err(|_| failed_to_convert_gw_count += 1)
.ok()
.filter_map(|gw| match skimmed_nodes.get(&gw.gateway_identity_key) {
Some(skimmed_node) => Some((gw, skimmed_node)),
None => {
warn!(
"No SkimmedNode data found for GW, identity_key={}",
gw.gateway_identity_key
);
None
}
})
.filter(|g: &DVpnGateway| {
.filter_map(
|(gw, skimmed_node)| match DVpnGateway::new(gw, skimmed_node) {
Ok(gw) => Some(gw),
Err(err) => {
error!(
"Failed to convert GW node_id={} to dVPN form: {}",
skimmed_node.node_id, err
);
None
}
},
)
.filter(|gw| {
// gateways must have a country
g.location.two_letter_iso_country_code.len() == 2
if gw.location.two_letter_iso_country_code.len() == 2 {
true
} else {
warn!(
"Invalid country code: {}",
gw.location.two_letter_iso_country_code
);
false
}
})
// sort by country, then by identity key
.sorted_by_key(|item| {
// for some reason, closure on this owned iterator takes
// reference, but returns owned element, which means
// we have to clone despite having access to owned data
(
item.identity_key.clone(),
item.location.two_letter_iso_country_code.clone(),
item.identity_key.clone(),
)
})
.collect();
.collect::<Vec<_>>();
if failed_to_convert_gw_count > 0 {
tracing::error!(
"Failed to convert {} DB gateways to dVPN form",
failed_to_convert_gw_count
);
}
if gateways.is_empty() {
tracing::warn!("Database contains 0 gateways");
if res_gws.is_empty() && started_with > 0 {
tracing::warn!("Started with {}, got 0 gateways", started_with);
} else {
self.upsert_dvpn_gateway_list(gateways.clone()).await;
self.upsert_dvpn_gateway_list(res_gws.clone()).await;
}
gateways
res_gws
}
}
}
@@ -151,7 +151,8 @@ impl Monitor {
.await
.log_error("get_all_basic_nodes")?;
tracing::info!("🟣 get_all_basic_nodes: {}", nym_nodes.len());
let nym_node_count = nym_nodes.len();
tracing::info!("🟣 get_all_basic_nodes: {}", nym_node_count);
let nym_node_records =
self.prepare_nym_node_data(nym_nodes.clone(), &bonded_nym_nodes, &described_nodes);
@@ -252,7 +253,7 @@ impl Monitor {
//
let nodes_summary = vec![
(NYMNODE_COUNT, nym_nodes.len()),
(NYMNODE_COUNT, nym_node_count),
(ASSIGNED_MIXING_COUNT, assigned_mixing_count),
(MIXNODES_LEGACY_COUNT, count_legacy_mixnodes),
(NYMNODES_DESCRIBED_COUNT, described_nodes.len()),
@@ -268,7 +269,7 @@ impl Monitor {
let last_updated = now_utc();
let last_updated_utc = last_updated.unix_timestamp().to_string();
let network_summary = NetworkSummary {
total_nodes: nym_nodes.len().cast_checked()?,
total_nodes: nym_node_count.cast_checked()?,
mixnodes: mixnode::MixnodeSummary {
bonded: mixnode::MixingNodesSummary {
count: assigned_mixing_count.cast_checked()?,