Update the LTS system to periodically ask if we have a license yet, allowing for automatic configuration of LTS if and when they sign up. This is a WIP - not recommended to merge yet.

This commit is contained in:
Herbert Wolverson 2023-10-06 16:37:28 +00:00
parent 5d5bfcb26d
commit 916645ab93
7 changed files with 169 additions and 9 deletions

View File

@ -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::<Document>();
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::<EtcLqos>(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;

View File

@ -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;

View File

@ -35,10 +35,16 @@ pub async fn start_long_term_stats() -> Sender<StatsUpdateMessage> {
}
async fn collation_scheduler(tx: Sender<StatsUpdateMessage>) {
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<Duration> {
}
async fn uisp_collection_manager(control_tx: Sender<StatsUpdateMessage>) {
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;
}
}
}

View File

@ -62,6 +62,7 @@ async fn connect_if_permitted() -> Result<TcpStream, QueueError> {
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(|| {

View File

@ -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
}

View File

@ -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

View File

@ -17,7 +17,27 @@ fn build_license_request(key: String) -> Result<Vec<u8>, 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<Vec<u8>, 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<LicenseReply, LicenseChec
}
}
pub async fn ask_license_server_for_new_account(
node_id: String,
) -> Result<LicenseReply, LicenseCheckError>
{
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,