use estate_clock::config::{Config, CLOCK_ENV};
use estate_clock::offer::FedRun;
use estate_clock::report::{Outcome, Report, Trigger};
pub use estate_clock::offer::{last_outcome, outcome_is_right};
#[cfg(feature = "clock-fetch")]
fn sync(config: &Result<Config, String>, phase: Trigger) -> Report {
estate_clock::fetch::sync(config, phase)
}
pub const NO_HTTPS: &str =
"an https:// clock source needs gunnar-server's `clock-fetch` feature (off by default: this build asks peer and SNTP sources only)";
#[cfg(not(feature = "clock-fetch"))]
fn sync(config: &Result<Config, String>, phase: Trigger) -> Report {
let report = match config {
Ok(Config::Sources { sources, peer_key }) => {
let polled = estate_clock::poll::poll_with(sources, peer_key.as_deref(), &|_| Err(NO_HTTPS.to_owned()));
let action = estate_clock::adjust::choose(phase, &polled.verdict.decision);
let applied = estate_clock::adjust::apply(action).map_err(|err| err.to_string());
Report::from_poll(phase, &polled, action, applied)
}
Err(why) => Report::without_sources(phase, Outcome::Misconfigured, why.clone()),
Ok(Config::NotConfigured) => Report::without_sources(phase, Outcome::NotConfigured, format!("no clock source is configured ({CLOCK_ENV})")),
Ok(Config::Off) => Report::without_sources(phase, Outcome::Off, format!("{CLOCK_ENV}=off: this machine was told not to set its own clock")),
};
estate_clock::report::record(report.clone());
report
}
fn owns_the_clock() -> bool {
std::env::var_os(CLOCK_ENV).is_some()
|| estate_clock::config::gunnar_alias(CLOCK_ENV).is_some_and(|alias| std::env::var_os(alias).is_some())
|| (appliance_pid1::net::is_init() && appliance_pid1::net::booted_by_korp_start())
}
fn log(report: &Report) {
let summary = report.summary();
match report.outcome {
Outcome::Stepped | Outcome::Slewed | Outcome::AlreadyRight => {
tracing::info!(
outcome = report.outcome.as_str(),
offset_ms = report.offset_ms,
"clock: {summary}"
)
}
Outcome::NotConfigured | Outcome::Off => {
tracing::info!(outcome = report.outcome.as_str(), "clock: {summary}")
}
Outcome::Misconfigured | Outcome::NoQuorum | Outcome::Disagree | Outcome::ApplyFailed => {
tracing::warn!(
outcome = report.outcome.as_str(),
"clock: {summary}. If this box's clock is wrong, every credential it is handed \
will be refused."
)
}
}
for row in &report.sources {
match (&row.offset_ms, &row.error) {
(_, Some(why)) => tracing::warn!(source = %row.source, %why, "clock source failed"),
(Some(offset_ms), None) => {
tracing::info!(source = %row.source, offset_ms, "clock source answered")
}
(None, None) => {}
}
}
}
async fn attempt(phase: Trigger) -> Report {
let config = crate::feed::clock_config();
match tokio::task::spawn_blocking(move || sync(&config, phase)).await {
Ok(report) => report,
Err(err) => {
let report = Report::without_sources(
phase,
Outcome::ApplyFailed,
format!("the clock sync task did not complete: {err}"),
);
estate_clock::report::record(report.clone());
report
}
}
}
pub async fn synchronise_at_boot() {
if !owns_the_clock() {
let report = Report::without_sources(
Trigger::Boot,
Outcome::Off,
"not the appliance's PID 1, so the system clock is somebody else's to set",
);
tracing::debug!("clock: {}", report.summary());
estate_clock::report::record(report);
return;
}
log(&attempt(Trigger::Boot).await);
}
fn configured() -> bool {
!matches!(crate::feed::clock_config(), Ok(Config::NotConfigured) | Ok(Config::Off))
}
pub fn arm_triggers() {
if !owns_the_clock() || !configured() {
return;
}
estate_clock::trigger::arm(|trigger| {
let config = crate::feed::clock_config();
log(&sync(&config, trigger));
});
}
fn fed_sync() -> Outcome {
let config = crate::feed::clock_config();
let report = sync(&config, Trigger::Timer);
log(&report);
report.outcome
}
static FED: FedRun = FedRun::new(fed_sync, arm_triggers);
pub fn fed_arrived() {
if !owns_the_clock() || !configured() {
return;
}
if !FED.start() {
tracing::warn!("clock: the fed sync could not get a thread of its own; the triggers take over");
}
}
pub fn tunnel_up() {
if !owns_the_clock() || !configured() {
return;
}
FED.tunnel_up(last_outcome());
}
#[cfg(test)]
mod airgap {
use super::*;
use estate_clock::config::Source;
fn trap() -> (std::net::TcpListener, Result<Config, String>) {
let trap = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
trap.set_nonblocking(true).unwrap();
let url = format!("https://{}/", trap.local_addr().unwrap());
(trap, Ok(Config::Sources { sources: vec![Source::Https(url)], peer_key: None }))
}
fn dialled(trap: &std::net::TcpListener) -> bool {
std::thread::sleep(std::time::Duration::from_millis(100));
trap.accept().is_ok()
}
#[cfg(not(feature = "clock-fetch"))]
#[test]
fn a_default_build_refuses_an_https_clock_source_by_name_and_dials_nothing() {
let (trap, config) = trap();
let report = sync(&config, Trigger::Timer);
let said = format!("{report:?}");
assert!(said.contains("clock-fetch"), "the refusal names the feature: {said}");
assert!(!dialled(&trap), "a default build dialled the https clock source");
}
#[cfg(feature = "clock-fetch")]
#[test]
fn a_clock_fetch_build_asks_its_https_clock_source_as_before() {
let (trap, config) = trap();
let report = sync(&config, Trigger::Timer);
assert!(dialled(&trap), "a clock-fetch build did not ask its https source: {report:?}");
}
}