Skip to content

Commit a491ffd

Browse files
davidv1992rnijveld
authored andcommitted
[Management] Implement assigning regions to servers on registation.
1 parent 13edca6 commit a491ffd

File tree

10 files changed

+158
-10
lines changed

10 files changed

+158
-10
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ COPY --from=builder /build/artifacts/nts-pool-management /usr/local/bin/nts-pool
4343
COPY --from=builder /build/artifacts/nts-pool-monitor /usr/local/bin/nts-pool-monitor
4444
COPY --from=builder /build/nts-pool-management/assets /opt/nts-pool-management/assets
4545

46+
# Temporarily copy test geodb, until we actually implement proper loading of the geolocation database.
47+
COPY --from=builder /build/nts-pool-ke/testdata/GeoLite2-Country-Test.mmdb /opt/nts-pool-management/GeoLite2-Country-Test.mmdb
48+
4649
# Set a default assets directory
4750
ENV NTSPOOL_ASSETS_DIR=/opt/nts-pool-management/assets
4851

compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ services:
7979
DATABASE_URL: "postgres://nts-pool@psql:5432/nts-pool"
8080
NTSPOOL_DATABASE_RUN_MIGRATIONS: "false"
8181
NTSPOOL_ASSETS_DIR: "/app/nts-pool-management/assets"
82+
NTSPOOL_GEOLOCATION_DB: "/app/nts-pool-ke/testdata/GeoLite2-Country-Test.mmdb"
8283
NTSPOOL_JWT_SECRET: "a-string-secret-at-least-256-bits-long"
8384
NTSPOOL_COOKIE_SECRET: "another-string-secret-at-least-256-bits-long"
8485
NTSPOOL_SMTP_URL: "smtp://mailcrab:1025"

helm-charts/nts-pool/templates/management.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,5 @@ spec:
101101
value: "1"
102102
- name: RESTART_BUMP
103103
value: "{{ default 1 .Values.management.restartBump}}"
104+
- name: NTSPOOL_GEOLOCATION_DB
105+
value: "/opt/nts-pool-management/GeoLite2-Country-Test.mmdb" # Temporary, until proper loading of geolocation database is implemented

nts-pool-management/.sqlx/query-8f1990990f9c7bc98787dfdc41df5ede2cc53928bdf0b2521091dc38f0c75afe.json renamed to nts-pool-management/.sqlx/query-41819cf933c55bdc06b40379f6499a5eae6fd24d3a9715970fc7923f6b4cae00.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nts-pool-management/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ askama = { workspace = true }
1515
axum = { workspace = true, features = ["macros"] }
1616
axum-extra = { workspace = true, features = ["cookie", "cookie-private", "cookie-key-expansion", "typed-header"] }
1717
headers.workspace = true
18+
maxminddb.workspace = true
19+
notify.workspace = true
1820
nts-pool-shared = { workspace = true, features = ["sqlx"] }
21+
phf.workspace = true
1922
serde = { workspace = true, features = ["derive"] }
2023
serde_json = { workspace = true }
2124
serde_qs = { workspace = true }

nts-pool-management/src/common/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub struct AppConfig {
88
// Base website configuration
99
pub base_url: BaseUrl,
1010
pub assets_path: String,
11+
pub geolocation_db: std::path::PathBuf,
1112
// Database config
1213
pub database_url: String,
1314
pub database_run_migrations: RunDatabaseMigrations,
@@ -42,6 +43,9 @@ impl AppConfig {
4243
.wrap_err("NTSPOOL_BASE_URL not set in environment")?,
4344
);
4445
let assets_path = std::env::var("NTSPOOL_ASSETS_DIR").unwrap_or("./assets".into());
46+
let geolocation_db = std::env::var("NTSPOOL_GEOLOCATION_DB")
47+
.wrap_err("NTSPOOL_GEOLOCATION_DB not set in environment")?
48+
.into();
4549

4650
// Database configuration
4751
let database_url = std::env::var("NTSPOOL_DATABASE_URL")
@@ -108,6 +112,7 @@ impl AppConfig {
108112
Ok(Self {
109113
base_url,
110114
assets_path,
115+
geolocation_db,
111116
database_url,
112117
database_run_migrations,
113118
jwt_secret,
@@ -136,6 +141,7 @@ impl Default for AppConfig {
136141
cookie_secret: "UNSAFE_SECRET".into(),
137142
mail_from_address: "noreply@example.com".into(),
138143
mail_smtp_url: "smtp://localhost:25".into(),
144+
geolocation_db: "../nts-pool-ke/testdata/GeoLite2-Country-Test.mmdb".into(),
139145
assets_path: "./assets".into(),
140146
monitor_result_batchsize: 4,
141147
monitor_result_batchtime: Duration::from_secs(60),

nts-pool-management/src/main.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
use std::str::FromStr;
1+
use std::{
2+
str::FromStr,
3+
sync::{Arc, RwLock},
4+
};
25

36
use axum::{extract::FromRef, middleware};
47
use axum_extra::extract::cookie;
8+
use eyre::Context;
9+
use notify::Watcher;
510
use sqlx::PgPool;
611
use tracing::info;
712

813
use crate::{
914
common::config::RunDatabaseMigrations,
15+
common::error::AppError,
1016
config::AppConfig,
1117
email::{MailTransport, Mailer},
1218
};
@@ -29,6 +35,7 @@ pub struct AppState {
2935
private_cookie_key: cookie::Key,
3036
mailer: Mailer,
3137
config: AppConfig,
38+
geodb: Arc<RwLock<Arc<maxminddb::Reader<Vec<u8>>>>>,
3239
}
3340

3441
pub trait DbConnLike<'a>:
@@ -77,6 +84,61 @@ pub async fn pool_conn(
7784
}
7885
}
7986

87+
async fn load_geodb(
88+
geodb_path: &std::path::Path,
89+
) -> Result<Arc<maxminddb::Reader<Vec<u8>>>, AppError> {
90+
let geodb_raw = tokio::fs::read(geodb_path)
91+
.await
92+
.wrap_err("Could not load geolocation database from disk")?;
93+
94+
Ok(Arc::new(
95+
maxminddb::Reader::from_source(geodb_raw).wrap_err("Invalid geolocation database")?,
96+
))
97+
}
98+
99+
async fn manage_geodb(
100+
geodb_path: impl AsRef<std::path::Path> + Send + 'static,
101+
) -> Result<Arc<RwLock<Arc<maxminddb::Reader<Vec<u8>>>>>, AppError> {
102+
let (change_sender, mut change_receiver) = tokio::sync::mpsc::unbounded_channel::<()>();
103+
// Use a poll watcher here as INotify can be unreliable in many ways and I don't want to deal with that.
104+
let mut watcher = notify::poll::PollWatcher::new(
105+
move |event: notify::Result<notify::Event>| {
106+
if event.is_ok() {
107+
let _ = change_sender.send(());
108+
}
109+
},
110+
notify::Config::default()
111+
.with_poll_interval(std::time::Duration::from_secs(60))
112+
.with_compare_contents(true),
113+
)
114+
.wrap_err("Could not setup watcher for changes in geolocation database")?;
115+
116+
watcher
117+
.watch(geodb_path.as_ref(), notify::RecursiveMode::NonRecursive)
118+
.wrap_err("Could not watch geolocation database for changes")?;
119+
120+
let geodb = Arc::new(RwLock::new(load_geodb(geodb_path.as_ref()).await?));
121+
let geodb_cloned = geodb.clone();
122+
123+
tokio::spawn(async move {
124+
// keep the watcher alive
125+
let _w = watcher;
126+
loop {
127+
change_receiver.recv().await;
128+
match load_geodb(geodb_path.as_ref()).await {
129+
Ok(new_geodb) => {
130+
*geodb.write().unwrap() = new_geodb;
131+
}
132+
Err(e) => {
133+
tracing::error!("Could not refresh geolocation database: {e}");
134+
}
135+
}
136+
}
137+
});
138+
139+
Ok(geodb_cloned)
140+
}
141+
80142
#[tokio::main]
81143
async fn main() {
82144
let config = AppConfig::from_env().expect("Failed to load configuration");
@@ -122,6 +184,10 @@ async fn main() {
122184

123185
let serve_dir_service = tower_http::services::ServeDir::new(&config.assets_path);
124186

187+
let geodb = manage_geodb(config.geolocation_db.clone())
188+
.await
189+
.expect("Unable to initialize geolocation database");
190+
125191
// construct the application state
126192
let state = AppState {
127193
db,
@@ -130,6 +196,7 @@ async fn main() {
130196
private_cookie_key,
131197
mailer,
132198
config,
199+
geodb,
133200
};
134201

135202
// setup routes

nts-pool-management/src/models/time_source.rs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
use std::collections::HashSet;
2+
13
use eyre::Context;
4+
use phf::phf_map;
25
use serde::Deserialize;
36

47
use crate::{
@@ -10,6 +13,16 @@ use crate::{
1013
},
1114
};
1215

16+
static CONTINENTS: phf::Map<&'static str, &'static str> = phf_map! {
17+
"AF" => "AFRICA",
18+
"AN" => "ANTARCTICA",
19+
"AS" => "ASIA",
20+
"EU" => "EUROPE",
21+
"NA" => "NORTH-AMERICA",
22+
"OC" => "OCEANIA",
23+
"SA" => "SOUTH_AMERICA",
24+
};
25+
1326
uuid!(TimeSourceId);
1427

1528
#[derive(Debug, Clone, Deserialize, sqlx::FromRow)]
@@ -74,19 +87,66 @@ impl TryFrom<NewTimeSourceForm> for NewTimeSource {
7487
}
7588
}
7689

90+
pub async fn infer_regions(
91+
hostname: impl AsRef<str>,
92+
geodb: &maxminddb::Reader<impl AsRef<[u8]>>,
93+
) -> Vec<String> {
94+
// Note: port doesn't matter but is needed for the lookup_host interface
95+
let addresses = match tokio::net::lookup_host((hostname.as_ref(), 4460)).await {
96+
Ok(addresses) => addresses,
97+
Err(e) => {
98+
if e.raw_os_error().is_some() {
99+
// Definitely an issue
100+
tracing::error!("Could not resolve hostname of time source: {e}");
101+
}
102+
return vec![];
103+
}
104+
};
105+
106+
let mut result = HashSet::new();
107+
for addr in addresses {
108+
let Some(lookup) = (match geodb.lookup::<maxminddb::geoip2::Country>(addr.ip()) {
109+
Ok(lookup) => lookup,
110+
Err(e) => {
111+
tracing::error!("Failure during geoip lookup: {e}");
112+
None
113+
}
114+
}) else {
115+
continue;
116+
};
117+
118+
if let Some(continent) = lookup
119+
.continent
120+
.and_then(|v| v.code)
121+
.and_then(|c| CONTINENTS.get(c))
122+
{
123+
result.insert((*continent).to_owned());
124+
}
125+
if let Some(country) = lookup.country.and_then(|v| v.iso_code) {
126+
result.insert(country.to_owned());
127+
}
128+
}
129+
130+
result.into_iter().collect()
131+
}
132+
77133
pub async fn create(
78134
conn: impl DbConnLike<'_>,
79135
owner: UserId,
80136
new_time_source: NewTimeSource,
137+
geodb: &maxminddb::Reader<impl AsRef<[u8]>>,
81138
) -> Result<(), sqlx::Error> {
139+
let regions = infer_regions(&new_time_source.hostname, geodb).await;
140+
82141
sqlx::query!(
83142
r#"
84-
INSERT INTO time_sources (owner, hostname, port)
85-
VALUES ($1, $2, $3)
143+
INSERT INTO time_sources (owner, hostname, port, countries)
144+
VALUES ($1, $2, $3, $4)
86145
"#,
87146
owner as _,
88147
new_time_source.hostname,
89148
new_time_source.port as _,
149+
regions.as_slice(),
90150
)
91151
.execute(conn)
92152
.await?;

nts-pool-management/src/routes/management.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ pub async fn create_time_source(
5151
flash: FlashMessageService,
5252
Form(new_time_source): Form<NewTimeSourceForm>,
5353
) -> Result<impl IntoResponse, AppError> {
54-
let flash = match time_source::create(&state.db, user.id, new_time_source.try_into()?).await {
55-
Ok(_) => flash.success("Time source added successfully".to_string()),
56-
Err(_) => flash.error("Could not add time source".to_string()),
57-
};
54+
let geodb = state.geodb.read().unwrap().clone();
55+
let flash =
56+
match time_source::create(&state.db, user.id, new_time_source.try_into()?, &geodb).await {
57+
Ok(_) => flash.success("Time source added successfully".to_string()),
58+
Err(_) => flash.error("Could not add time source".to_string()),
59+
};
5860

5961
Ok((flash, Redirect::to(TIME_SOURCES_ENDPOINT)))
6062
}

0 commit comments

Comments
 (0)