diff --git a/src/rust/lqos_config/src/etc.rs b/src/rust/lqos_config/src/etc.rs index 1cfbf91d..4b66b47b 100644 --- a/src/rust/lqos_config/src/etc.rs +++ b/src/rust/lqos_config/src/etc.rs @@ -211,6 +211,53 @@ impl EtcLqos { } } +pub fn enable_long_term_stats(license_key: String) { + if let Ok(raw) = std::fs::read_to_string("/etc/lqos.conf") { + let document = raw.parse::(); + match document { + Err(e) => { + error!("Unable to parse TOML from /etc/lqos.conf"); + error!("Full error: {:?}", e); + return; + } + Ok(mut config_doc) => { + let cfg = toml_edit::de::from_document::(config_doc.clone()); + match cfg { + Ok(mut cfg) => { + // Now we enable LTS if its not present + if let Ok(isp_config) = crate::LibreQoSConfig::load() { + if cfg.long_term_stats.is_none() { + cfg.long_term_stats = Some(LongTermStats { + gather_stats: true, + collation_period_seconds: 60, + license_key: Some(license_key), + uisp_reporting_interval_seconds: if isp_config.automatic_import_uisp { + Some(300) + } else { + None + } + }); + config_doc["long_term_stats"] = value(toml_edit::ser::to_string(&cfg.long_term_stats.unwrap()).unwrap()); + let new_cfg = config_doc.to_string(); + if let Err(e) = fs::write(Path::new("/etc/lqos.conf"), new_cfg) { + log::error!("Unable to write to /etc/lqos.conf"); + log::error!("{e:?}"); + return; + } + } + } + } + Err(e) => { + error!("Unable to parse TOML from /etc/lqos.conf"); + error!("Full error: {:?}", e); + return; + } + } + } + } + } +} + fn check_config(cfg_doc: &mut Document, cfg: &mut EtcLqos) { use sha2::digest::Update; use sha2::Digest; diff --git a/src/rust/lqos_config/src/lib.rs b/src/rust/lqos_config/src/lib.rs index 65cced4e..6f05cecb 100644 --- a/src/rust/lqos_config/src/lib.rs +++ b/src/rust/lqos_config/src/lib.rs @@ -14,7 +14,7 @@ mod program_control; mod shaped_devices; pub use authentication::{UserRole, WebUsers}; -pub use etc::{BridgeConfig, BridgeInterface, BridgeVlan, EtcLqos, Tunables}; +pub use etc::{BridgeConfig, BridgeInterface, BridgeVlan, EtcLqos, Tunables, enable_long_term_stats}; pub use libre_qos_config::LibreQoSConfig; pub use network_json::{NetworkJson, NetworkJsonNode, NetworkJsonTransport}; pub use program_control::load_libreqos; diff --git a/src/rust/lts_client/src/collector/collection_manager.rs b/src/rust/lts_client/src/collector/collection_manager.rs index b98228d4..2383d80e 100644 --- a/src/rust/lts_client/src/collector/collection_manager.rs +++ b/src/rust/lts_client/src/collector/collection_manager.rs @@ -35,10 +35,16 @@ pub async fn start_long_term_stats() -> Sender { } async fn collation_scheduler(tx: Sender) { + log::info!("Starting collation scheduler"); loop { let collation_period = get_collation_period(); - tx.send(StatsUpdateMessage::CollationTime).await.unwrap(); + log::info!("Collation period: {}s", collation_period.as_secs()); + if tx.send(StatsUpdateMessage::CollationTime).await.is_err() { + log::warn!("Unable to send collation time message"); + } + log::info!("Sent collation time message. Sleeping."); tokio::time::sleep(collation_period).await; + log::info!("Collation scheduler woke up."); } } @@ -107,11 +113,20 @@ fn get_uisp_collation_period() -> Option { } async fn uisp_collection_manager(control_tx: Sender) { - if let Some(period) = get_uisp_collation_period() { - log::info!("Starting UISP poller with period {:?}", period); - loop { - control_tx.send(StatsUpdateMessage::UispCollationTime).await.unwrap(); - tokio::time::sleep(period).await; + // Outer loop: If UISP is disabled, check hourly to see if it + // was enabled. If it is enabled, start the inner loop. + loop { + // Inner loop - if there's a collation period set for UISP, + // poll it. + if let Some(period) = get_uisp_collation_period() { + log::info!("Starting UISP poller with period {:?}", period); + loop { + control_tx.send(StatsUpdateMessage::UispCollationTime).await.unwrap(); + tokio::time::sleep(period).await; + } + } else { + // Sleep for one hour - then we'll check again + tokio::time::sleep(Duration::from_secs(3600)).await; } } } \ No newline at end of file diff --git a/src/rust/lts_client/src/submission_queue/comm_channel/mod.rs b/src/rust/lts_client/src/submission_queue/comm_channel/mod.rs index 02576282..b16a2e52 100644 --- a/src/rust/lts_client/src/submission_queue/comm_channel/mod.rs +++ b/src/rust/lts_client/src/submission_queue/comm_channel/mod.rs @@ -62,6 +62,7 @@ async fn connect_if_permitted() -> Result { QueueError::NoLocalLicenseKey })?; if !usage_cfg.gather_stats { + log::warn!("Gathering long-term stats is disabled."); return Err(QueueError::StatsDisabled); } let license_key = usage_cfg.license_key.ok_or_else(|| { diff --git a/src/rust/lts_client/src/submission_queue/licensing.rs b/src/rust/lts_client/src/submission_queue/licensing.rs index 4be124d0..225dec35 100644 --- a/src/rust/lts_client/src/submission_queue/licensing.rs +++ b/src/rust/lts_client/src/submission_queue/licensing.rs @@ -1,4 +1,4 @@ -use crate::transport_data::{ask_license_server, LicenseReply}; +use crate::transport_data::{ask_license_server, LicenseReply, ask_license_server_for_new_account}; use lqos_config::EtcLqos; use lqos_utils::unix_time::unix_now; use once_cell::sync::Lazy; @@ -44,7 +44,11 @@ pub(crate) async fn get_license_status() -> LicenseState { const MISERLY_NO_KEY: &str = "IDontSupportDevelopersAndShouldFeelBad"; async fn check_license(unix_time: u64) -> LicenseState { + log::info!("Checking LTS stats license"); if let Ok(cfg) = EtcLqos::load() { + // The config file is good. Is LTS enabled? + // If it isn't, we need to try very gently to see if a pending + // request has been submitted. if let Some(cfg) = cfg.long_term_stats { if let Some(key) = cfg.license_key { if key == MISERLY_NO_KEY { @@ -81,7 +85,29 @@ async fn check_license(unix_time: u64) -> LicenseState { } } } + } else { + // LTS is unconfigured - but not explicitly disabled. + // So we need to check if we have a pending request. + // If a license key has been assigned, then we'll setup + // LTS. If it hasn't, we'll just return Unknown. + if let Some(node_id) = &cfg.node_id { + if let Ok(result) = ask_license_server_for_new_account(node_id.to_string()).await { + if let LicenseReply::NewActivation { license_key } = result { + // We have a new license! + let _ = lqos_config::enable_long_term_stats(license_key); + // Note that we're not doing anything beyond this - the next cycle + // will pick up on there actually being a license + } else { + log::info!("No pending LTS license found"); + } + } + } else { + // There's no node ID either - we can't talk to this + log::warn!("No NodeID is configured. No online services are possible."); + } } + } else { + log::error!("Unable to load lqosd configuration. Not going to try."); } LicenseState::Unknown } diff --git a/src/rust/lts_client/src/transport_data/license_types.rs b/src/rust/lts_client/src/transport_data/license_types.rs index 76a312bc..c773634c 100644 --- a/src/rust/lts_client/src/transport_data/license_types.rs +++ b/src/rust/lts_client/src/transport_data/license_types.rs @@ -24,6 +24,11 @@ pub enum LicenseRequest { /// The sodium-style public key of the requesting shaper node public_key: PublicKey, }, + /// Check to see if this node has been newly approved + PendingLicenseRequest { + /// The local node id + node_id: String, + } } /// License server responses for a key @@ -43,6 +48,11 @@ pub enum LicenseReply { /// The server's public key public_key: PublicKey, }, + /// New Activation + NewActivation { + /// The license key to apply + license_key: String, + } } /// Errors that can occur when checking licenses diff --git a/src/rust/lts_client/src/transport_data/license_utils.rs b/src/rust/lts_client/src/transport_data/license_utils.rs index 2d118449..f8fdc0dd 100644 --- a/src/rust/lts_client/src/transport_data/license_utils.rs +++ b/src/rust/lts_client/src/transport_data/license_utils.rs @@ -17,7 +17,27 @@ fn build_license_request(key: String) -> Result, LicenseCheckError> { let mut result = Vec::new(); let payload = serde_cbor::to_vec(&LicenseRequest::LicenseCheck { key }); if let Err(e) = payload { - log::warn!("Unable to serialize statistics. Not sending them."); + log::warn!("Unable to serialize license request. Not sending them."); + log::warn!("{e:?}"); + return Err(LicenseCheckError::SerializeFail); + } + let payload = payload.unwrap(); + + // Store the version as network order + result.extend(1u16.to_be_bytes()); + // Store the payload size as network order + result.extend((payload.len() as u64).to_be_bytes()); + // Store the payload itself + result.extend(payload); + + Ok(result) +} + +fn build_activation_query(node_id: String) -> Result, LicenseCheckError> { + let mut result = Vec::new(); + let payload = serde_cbor::to_vec(&LicenseRequest::PendingLicenseRequest { node_id } ); + if let Err(e) = payload { + log::warn!("Unable to serialize license request. Not sending them."); log::warn!("{e:?}"); return Err(LicenseCheckError::SerializeFail); } @@ -107,6 +127,47 @@ pub async fn ask_license_server(key: String) -> Result Result +{ + if let Ok(buffer) = build_activation_query(node_id) { + let stream = TcpStream::connect(LICENSE_SERVER).await; + if let Err(e) = &stream { + if e.kind() == std::io::ErrorKind::NotFound { + log::error!("Unable to access {LICENSE_SERVER}. Check that lqosd is running and you have appropriate permissions."); + return Err(LicenseCheckError::SendFail); + } + } + let stream = stream; + match stream { + Ok(mut stream) => { + let ret = stream.write(&buffer).await; + if ret.is_err() { + log::error!("Unable to write to {LICENSE_SERVER} stream."); + log::error!("{:?}", ret); + return Err(LicenseCheckError::SendFail); + } + let mut buf = Vec::with_capacity(10240); + let ret = stream.read_to_end(&mut buf).await; + if ret.is_err() { + log::error!("Unable to read from {LICENSE_SERVER} stream."); + log::error!("{:?}", ret); + return Err(LicenseCheckError::SendFail); + } + + decode_response(&buf) + } + Err(e) => { + log::warn!("TCP stream failed to connect: {:?}", e); + Err(LicenseCheckError::ReceiveFail) + } + } + } else { + Err(LicenseCheckError::SerializeFail) + } +} + /// Ask the license server for the public key pub async fn exchange_keys_with_license_server( node_id: String,