Skip to content

Commit ad9b091

Browse files
committed
feat(zimaos): auto-register meshtier instance on startup
1 parent d7e73c1 commit ad9b091

5 files changed

Lines changed: 217 additions & 15 deletions

File tree

easytier-web/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod db;
2525
mod gateway;
2626
mod migrator;
2727
mod restful;
28+
mod system_startup;
2829

2930
#[cfg(feature = "embed")]
3031
mod web;
@@ -272,6 +273,8 @@ async fn main() {
272273
.await
273274
.unwrap();
274275

276+
let _system_startup_task = system_startup::start(mgr.clone());
277+
275278
gateway::register_route_to_gateway_if_needed(cli.api_server_addr, cli.api_server_port).await;
276279

277280
#[cfg(feature = "embed")]

easytier-web/src/restful/meshtier.rs

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,20 +127,7 @@ async fn do_connect(client_mgr: &ClientManager) -> Result<MeshResponse, HttpHand
127127
.wait_ready_info()
128128
.await
129129
.map_err(internal_error)?;
130-
let instance_id = mesh_response_instance_id(&zt_info)
131-
.map_err(|e| internal_error(format!("invalid zerotier id: {e}")))?;
132-
133-
// Keep meshtier as a single instance: always remove old meshtier instances before create.
134-
clear_meshtier_networks(client_mgr, runtime.identity).await?;
135-
136-
let cfg = default_mesh_network_config(instance_id, &zt_info.id, &zt_info.id);
137-
client_mgr
138-
.handle_run_network_instance(runtime.identity, cfg, true)
139-
.await
140-
.map_err(convert_remote_error)?;
141-
142-
// Keep startup checks for EasyTier instance, but return ZT-compatible payload for API callers.
143-
wait_easytier_online(client_mgr, runtime.identity, instance_id).await?;
130+
register_meshtier_instance(client_mgr, runtime.identity, &zt_info).await?;
144131

145132
match runtime.zerotier.get_info().await {
146133
Ok(info) => Ok(info),
@@ -217,6 +204,25 @@ async fn wait_easytier_online(
217204
.map_err(|_| internal_error("wait easytier online timeout"))?
218205
}
219206

207+
async fn register_meshtier_instance(
208+
client_mgr: &ClientManager,
209+
identity: SessionIdentity,
210+
zt_info: &MeshResponse,
211+
) -> Result<MeshResponse, HttpHandleError> {
212+
let instance_id = mesh_response_instance_id(zt_info)
213+
.map_err(|e| internal_error(format!("invalid zerotier id: {e}")))?;
214+
215+
clear_meshtier_networks(client_mgr, identity).await?;
216+
217+
let cfg = default_mesh_network_config(instance_id, &zt_info.id, &zt_info.id);
218+
client_mgr
219+
.handle_run_network_instance(identity, cfg, true)
220+
.await
221+
.map_err(convert_remote_error)?;
222+
223+
wait_easytier_online(client_mgr, identity, instance_id).await
224+
}
225+
220226
async fn clear_meshtier_networks(
221227
client_mgr: &ClientManager,
222228
identity: SessionIdentity,
@@ -268,6 +274,33 @@ fn add_running_meshtier_network_ids(
268274
}
269275
}
270276

277+
async fn has_meshtier_instance(
278+
client_mgr: &ClientManager,
279+
identity: SessionIdentity,
280+
) -> Result<bool, HttpHandleError> {
281+
let mut network_ids = BTreeSet::new();
282+
283+
if let Ok(info) = client_mgr.handle_collect_network_info(identity, None).await {
284+
add_running_meshtier_network_ids(&mut network_ids, info);
285+
}
286+
287+
let saved_networks: Vec<user_running_network_configs::Model> = client_mgr
288+
.get_storage()
289+
.list_network_configs(identity, ListNetworkProps::All)
290+
.await
291+
.map_err(convert_db_error)?;
292+
293+
for network in saved_networks {
294+
if let Ok(inst_id) = uuid::Uuid::parse_str(network.get_network_inst_id()) {
295+
if is_meshtier_instance(&inst_id) {
296+
network_ids.insert(inst_id);
297+
}
298+
}
299+
}
300+
301+
Ok(!network_ids.is_empty())
302+
}
303+
271304
fn is_meshtier_instance(inst_id: &uuid::Uuid) -> bool {
272305
inst_id.as_bytes()[..8] == MESH_TIER_PREFIX
273306
}
@@ -475,6 +508,74 @@ async fn pick_admin_identity(
475508
Ok((admin.user_id, admin.machine_id))
476509
}
477510

511+
pub(crate) async fn ensure_meshtier_instance_for_startup(
512+
client_mgr: &ClientManager,
513+
) -> anyhow::Result<bool> {
514+
let runtime = match MeshRuntime::from_request(client_mgr).await {
515+
Ok(runtime) => runtime,
516+
Err((StatusCode::SERVICE_UNAVAILABLE, Json(err))) => {
517+
tracing::debug!(
518+
"startup meshtier reconcile waiting for core session: {}",
519+
err.message
520+
);
521+
return Ok(false);
522+
}
523+
Err((status, Json(err))) => {
524+
return Err(anyhow::anyhow!(
525+
"startup meshtier reconcile failed to build runtime, status {}: {}",
526+
status,
527+
err.message
528+
));
529+
}
530+
};
531+
532+
let zt_info = runtime
533+
.zerotier
534+
.get_info()
535+
.await
536+
.map_err(|e| anyhow::anyhow!("failed to get zerotier info: {e}"))?;
537+
538+
if zt_info.status != MeshStatus::Online || !mesh_response_is_ready(&zt_info) {
539+
tracing::debug!(
540+
id = %zt_info.id,
541+
ip = ?zt_info.ip,
542+
status = ?zt_info.status,
543+
"startup meshtier reconcile waiting for zerotier readiness"
544+
);
545+
return Ok(false);
546+
}
547+
548+
match has_meshtier_instance(client_mgr, runtime.identity).await {
549+
Ok(true) => {
550+
tracing::info!("startup meshtier reconcile found existing meshtier instance");
551+
return Ok(true);
552+
}
553+
Ok(false) => {}
554+
Err((status, Json(err))) => {
555+
return Err(anyhow::anyhow!(
556+
"failed to inspect meshtier instance state, status {}: {}",
557+
status,
558+
err.message
559+
));
560+
}
561+
}
562+
563+
register_meshtier_instance(client_mgr, runtime.identity, &zt_info)
564+
.await
565+
.map_err(|(status, Json(err))| {
566+
anyhow::anyhow!(
567+
"startup meshtier reconcile failed to register instance, status {}: {}",
568+
status,
569+
err.message
570+
)
571+
})?;
572+
tracing::info!(
573+
zerotier_id = %zt_info.id,
574+
"startup meshtier reconcile registered meshtier instance"
575+
);
576+
Ok(true)
577+
}
578+
478579
impl ZeroTierClient {
479580
fn new(base_url: Url) -> Self {
480581
Self {

easytier-web/src/restful/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod auth;
22
pub(crate) mod captcha;
3-
mod meshtier;
3+
pub(crate) mod meshtier;
44
mod network;
55
mod users;
66

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use std::time::{Duration, Instant};
2+
3+
use easytier::common::scoped_task::ScopedTask;
4+
5+
use crate::client_manager::ClientManager;
6+
7+
const AUTO_REGISTER_MESHTIER_ON_START_ENV: &str =
8+
"ZIMAOS_EASYTIER_WEB_AUTO_REGISTER_MESHTIER_ON_START";
9+
const AUTO_REGISTER_MESHTIER_WAIT_SECS_ENV: &str =
10+
"ZIMAOS_EASYTIER_WEB_AUTO_REGISTER_MESHTIER_WAIT_SECS";
11+
const AUTO_REGISTER_MESHTIER_RETRY_SECS_ENV: &str =
12+
"ZIMAOS_EASYTIER_WEB_AUTO_REGISTER_MESHTIER_RETRY_SECS";
13+
14+
const DEFAULT_AUTO_REGISTER_MESHTIER_WAIT_SECS: u64 = 300;
15+
const DEFAULT_AUTO_REGISTER_MESHTIER_RETRY_SECS: u64 = 3;
16+
17+
fn parse_env_bool(name: &str, default: bool) -> bool {
18+
let Ok(raw) = std::env::var(name) else {
19+
return default;
20+
};
21+
match raw.trim().to_ascii_lowercase().as_str() {
22+
"1" | "true" | "yes" | "on" => true,
23+
"0" | "false" | "no" | "off" => false,
24+
_ => default,
25+
}
26+
}
27+
28+
fn parse_env_u64(name: &str, default: u64) -> u64 {
29+
let Ok(raw) = std::env::var(name) else {
30+
return default;
31+
};
32+
raw.trim().parse::<u64>().unwrap_or(default)
33+
}
34+
35+
pub fn start_auto_register_task(client_mgr: std::sync::Arc<ClientManager>) -> ScopedTask<()> {
36+
ScopedTask::from(tokio::spawn(async move {
37+
if !parse_env_bool(AUTO_REGISTER_MESHTIER_ON_START_ENV, true) {
38+
tracing::info!(
39+
"{}=false, skip startup meshtier auto registration",
40+
AUTO_REGISTER_MESHTIER_ON_START_ENV
41+
);
42+
return;
43+
}
44+
45+
let wait_secs = parse_env_u64(
46+
AUTO_REGISTER_MESHTIER_WAIT_SECS_ENV,
47+
DEFAULT_AUTO_REGISTER_MESHTIER_WAIT_SECS,
48+
);
49+
let retry_secs = parse_env_u64(
50+
AUTO_REGISTER_MESHTIER_RETRY_SECS_ENV,
51+
DEFAULT_AUTO_REGISTER_MESHTIER_RETRY_SECS,
52+
)
53+
.max(1);
54+
let deadline = Instant::now() + Duration::from_secs(wait_secs);
55+
56+
loop {
57+
match crate::restful::meshtier::ensure_meshtier_instance_for_startup(
58+
client_mgr.as_ref(),
59+
)
60+
.await
61+
{
62+
Ok(true) => {
63+
tracing::info!("startup meshtier auto registration finished");
64+
return;
65+
}
66+
Ok(false) => {
67+
tracing::debug!(
68+
"startup meshtier auto registration skipped, prerequisites not ready yet"
69+
);
70+
}
71+
Err(err) => {
72+
tracing::warn!("startup meshtier auto registration failed: {err}");
73+
}
74+
}
75+
76+
if Instant::now() >= deadline {
77+
tracing::warn!(
78+
"startup meshtier auto registration timed out after {} seconds",
79+
wait_secs
80+
);
81+
return;
82+
}
83+
84+
tokio::time::sleep(Duration::from_secs(retry_secs)).await;
85+
}
86+
}))
87+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
mod meshtier;
2+
3+
use std::sync::Arc;
4+
5+
use easytier::common::scoped_task::ScopedTask;
6+
7+
use crate::client_manager::ClientManager;
8+
9+
pub fn start(client_mgr: Arc<ClientManager>) -> ScopedTask<()> {
10+
meshtier::start_auto_register_task(client_mgr)
11+
}

0 commit comments

Comments
 (0)