Merge with develop to resolve update conflicts and preserve building both the support tool and this branch (both modified build scripts on the same lines)

This commit is contained in:
Herbert Wolverson 2024-06-15 09:43:11 -05:00
commit c9f9c51e7e
36 changed files with 1709 additions and 13 deletions

View File

@ -22,7 +22,7 @@ ETC_DIR=$DPKG_DIR/etc
MOTD_DIR=$DPKG_DIR/etc/update-motd.d
LQOS_FILES="graphInfluxDB.py influxDBdashboardTemplate.json integrationCommon.py integrationRestHttp.py integrationSplynx.py integrationUISP.py integrationSonar.py ispConfig.example.py LibreQoS.py lqos.example lqTools.py mikrotikFindIPv6.py network.example.json pythonCheck.py README.md scheduler.py ShapedDevices.example.csv"
LQOS_BIN_FILES="lqos_scheduler.service.example lqosd.service.example lqos_node_manager.service.example"
RUSTPROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_setup lqos_map_perf uisp_integration"
RUSTPROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_setup lqos_map_perf uisp_integration lqos_support_tool"
####################################################
# Clean any previous dist build

View File

@ -52,7 +52,7 @@ rustup update
# Start building
echo "Please wait while the system is compiled. Service will not be interrupted during this stage."
PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_map_perf uisp_integration"
PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_map_perf uisp_integration lqos_support_tool"
mkdir -p bin/static
pushd rust > /dev/null
#cargo clean

58
src/rust/Cargo.lock generated
View File

@ -131,9 +131,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.83"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "async-compression"
@ -437,6 +437,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.9.1"
@ -513,9 +519,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.4"
version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
dependencies = [
"clap_builder",
"clap_derive",
@ -523,9 +529,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.2"
version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
dependencies = [
"anstream",
"anstyle",
@ -535,9 +541,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.4"
version = "4.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -1675,9 +1681,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.154"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libloading"
@ -1820,6 +1826,7 @@ dependencies = [
"jemallocator",
"lqos_bus",
"lqos_config",
"lqos_support_tool",
"lqos_utils",
"nix 0.28.0",
"once_cell",
@ -1880,6 +1887,23 @@ dependencies = [
"uuid",
]
[[package]]
name = "lqos_support_tool"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"colored",
"libc",
"log",
"lqos_config",
"miniz_oxide",
"nix 0.29.0",
"serde",
"serde_cbor",
"serde_json",
]
[[package]]
name = "lqos_sys"
version = "0.1.0"
@ -2185,7 +2209,19 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.5.0",
"cfg-if",
"cfg_aliases",
"cfg_aliases 0.1.1",
"libc",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.5.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
]

View File

@ -32,4 +32,5 @@ members = [
"lqos_map_perf", # A CLI tool for testing eBPF map performance
"uisp", # REST support for the UISP API
"uisp_integration", # UISP Integration in Rust
"lqos_support_tool", # A Helper tool to make it easier to request/receive support
]

View File

@ -22,6 +22,7 @@ once_cell = "1"
dns-lookup = "1"
dashmap = "5"
reqwest = { version = "0.11.20", features = ["json"] }
lqos_support_tool = { path = "../lqos_support_tool" }
# Support JemAlloc on supported platforms
[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies]

View File

@ -13,6 +13,7 @@ mod network_tree;
mod queue_info;
mod toasts;
mod flow_monitor;
mod support;
// Use JemAllocator only on supported platforms
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
@ -45,6 +46,7 @@ fn rocket() -> _ {
static_pages::unknown_devices_page,
static_pages::circuit_queue,
static_pages::pretty_map_graph,
static_pages::help_page,
config_control::config_page,
network_tree::tree_page,
static_pages::ip_dump,
@ -124,6 +126,10 @@ fn rocket() -> _ {
flow_monitor::flows_lat_lon,
flow_monitor::flows_ether_protocol,
flow_monitor::flows_ip_protocol,
// Suport System
support::run_sanity_check,
support::gather_support_data,
support::submit_support_data,
],
);

View File

@ -83,6 +83,14 @@ pub async fn pretty_map_graph<'a>(
NoCache::new(NamedFile::open("static/showoff.html").await.ok())
}
// Help me obi-wan, you're our only hope
#[get("/help")]
pub async fn help_page<'a>(
_auth: AuthGuard,
) -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/help.html").await.ok())
}
#[get("/vendor/bootstrap.min.css")]
pub async fn bootsrap_css<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/bootstrap.min.css").await.ok())

View File

@ -0,0 +1,62 @@
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use rocket::fs::NamedFile;
use rocket::serde::Deserialize;
use rocket::serde::json::Json;
use lqos_config::load_config;
use lqos_support_tool::{run_sanity_checks, SanityChecks};
use crate::auth_guard::AuthGuard;
#[get("/api/sanity")]
pub async fn run_sanity_check(
_auth: AuthGuard,
) -> Json<SanityChecks> {
let mut status = run_sanity_checks().unwrap();
status.results.sort_by(|a,b| a.success.cmp(&b.success));
Json(status)
}
#[derive(Deserialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct SupportMetadata {
name: String,
comment: String,
}
#[post("/api/gatherSupport", data="<info>")]
pub async fn gather_support_data(
info: Json<SupportMetadata>
) -> NamedFile {
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let filename = format!("/tmp/libreqos_{}.support", timestamp);
let path = Path::new(&filename);
let lts_key = if let Ok(cfg) = load_config() {
cfg.long_term_stats.license_key.unwrap_or("None".to_string())
} else {
"None".to_string()
};
if let Ok(dump) = lqos_support_tool::gather_all_support_info(&info.name, &info.comment, &lts_key) {
std::fs::write(&path, dump.serialize_and_compress().unwrap()).unwrap();
}
NamedFile::open(path).await.unwrap()
}
#[post("/api/submitSupport", data="<info>")]
pub async fn submit_support_data(
info: Json<SupportMetadata>
) -> String {
let lts_key = if let Ok(cfg) = load_config() {
cfg.long_term_stats.license_key.unwrap_or("None".to_string())
} else {
"None".to_string()
};
if let Ok(dump) = lqos_support_tool::gather_all_support_info(&info.name, &info.comment, &lts_key) {
lqos_support_tool::submit_to_network(dump);
"Your support submission has been sent".to_string()
} else {
"Something went wrong".to_string()
}
}

View File

@ -50,6 +50,9 @@
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i>
Reload LibreQoS</a>

View File

@ -0,0 +1,322 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span
id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li>
</ul>
</div>
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item" id="statsLink"></li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
LibreQoS</a>
</li>
</ul>
</div>
</nav>
<div id="container" class="pad4">
<div class="row">
<div class="col-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-question-circle"></i> Help & Support</h5>
<p class="alert-warning">
<i class="fa fa-info-circle"></i>
Priority support is given to Long-Term Stats subscribers and donors. Other support is
best effort, with volunteers trying to help as best we can.
</p>
<p>
<a href="https://github.com/sponsors/LibreQoE/" class="btn btn-primary">
<i class="fa fa-money"></i> Support LibreQoS Today
</a>
</p>
</div>
</div>
</div>
<div class="col-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-book"></i> Documentation</h5>
<p>The documentation is a great place to start!</p>
<a href="https://libreqos.readthedocs.io/en/latest/" class="btn btn-primary"><i class="fa fa-book"></i> Read The LibreQoS Documentation</a>
</div>
</div>
</div>
<div class="col-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-user-circle"></i> Chat</h5>
<p>
Connect with other LibreQoS users, and the LibreQoS team on our Zulip Chat System. <br />
<a class="btn btn-primary" href="https://chat.libreqos.io/"><i class="fa fa-user-circle"></i> Zulip chat system</a>.
</p>
</div>
</div>
</div>
</div>
<div class="row" style="margin-top: 10px;">
<div class="col-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-wrench"></i> Tools</h5>
<p style="border: 1px solid #eee">
<strong>Sanity check</strong> reads your configuration and looks for common issues. This should be
your first step when troubleshooting the system.<br />
<button id="btnSanity" class="btn btn-primary" onclick="sanity()"><i class="fa fa-info"></i> Sanity Check Your Installation</button>
</p>
<p style="border: 1px solid #eee">
<strong>Download</strong> support data to a local file. You can send this to LibreQoS via the
chat or email (as part of an ongoing support discussion).<br />
<button id="btnDownload" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#downloadDump"><i class="fa fa-download"></i> Download Support Dump</button>
</p>
<p style="border: 1px solid #eee">
<strong>Submit</strong> support data directly to LibreQoS. Please contact us when you do this,
with detailed information about the problems you are experiencing. Repeatedly hitting this
button will get you slower - or no - service!<br />
<button id="btnSubmit" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#submitDump"><i class="fa fa-send"></i> Send Support Dump</button>
</p>
</div>
</div>
</div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<div class="modal" tabindex="-1" id="sanityModal">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Configuration Check Results</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="overflow: auto;">
<p id="configCheck"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal" tabindex="-1" id="downloadDump">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Information To Add to the Support Dump</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="overflow: auto;">
<div class="row">
<div class="col">
<label for="gatherName" class="form-label">Your Name</label>
<input type="text" id="gatherName" class="form-control" />
</div>
<div class="col">
<label for="gatherEmail" class="form-label">Your Email Address</label>
<input type="text" id="gatherEmail" class="form-control" />
</div>
</div>
<div class="row">
<div class="col">
<label for="gatherComments" class="form-label">Comments</label>
<input type="text" id="gatherComments" class="form-control" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="gather()"><i class="fa fa-download"></i> Generate and Download</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal" tabindex="-1" id="submitDump">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Information To Add to the Support Dump</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="overflow: auto;">
<p class="alert-warning"><i class="fa fa-warning"></i> Your support dump will contain unredated data about your customers, and lots
of information about your server. By submitting, you are acknowledging that LibreQoS bear no liability for this data.
LibreQoS will take all reasonable measures to protect your data, and will not share it.</p>
<div class="row">
<div class="col">
<label for="submitName" class="form-label">Your Name</label>
<input type="text" id="submitName" class="form-control" />
</div>
<div class="col">
<label for="submitEmail" class="form-label">Your Email Address</label>
<input type="text" id="submitEmail" class="form-control" />
</div>
</div>
<div class="row">
<div class="col">
<label for="submitComments" class="form-label">Comments</label>
<input type="text" id="submitComments" class="form-control" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="submit()"><i class="fa fa-send"></i> Submit Support Data to LibreQoS</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
function start() {
setTitle();
colorReloadButton();
updateHostCounts();
}
function gather() {
let details = {
name: $("#gatherName").val() + ", " + $("#gatherEmail").val(),
comment: $("#gatherComments").val()
};
console.log(details);
if (details.name === ", " || details.name.indexOf("@")<1) {
alert("Please enter a name and email address. If you share this, it'll be handy to know who sent it!");
return;
}
$.ajax({
type: "POST",
url: "/api/gatherSupport",
data: JSON.stringify(details),
xhrFields: {
responseType: 'blob' // to avoid binary data being mangled on charset conversion
},
success: function(blob, result, xhr) {
var filename = "libreqos.support";
// use HTML5 a[download] attribute to specify filename
var downloadUrl = URL.createObjectURL(blob);
var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location.href = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
}
})
}
function submit() {
let details = {
name: $("#submitName").val() + ", " + $("#submitEmail").val(),
comment: $("#submitComments").val()
};
console.log(details);
if (details.name === ", " || details.name.indexOf("@")<1) {
alert("Please enter a name and email address. If you share this, it'll be handy to know who sent it!");
return;
}
$.ajax({
type: "POST",
url: "/api/submitSupport",
data: JSON.stringify(details),
success: function(result) {
console.log(result);
alert(result);
}
})
}
function sanity() {
$.get("/api/sanity", (data) => {
console.log(data);
let html = "<table class='table'><thead><th>Check</th><th>Success?</th><th>Comment</th></thead><tbody>";
for (let i=0; i<data.results.length; i++) {
let row = data.results[i];
html += "<tr>";
html += "<td>" + row.name + "</td>";
if (row.success) {
html += "<td style='color: green'><i class='fa fa-check'></i>";
} else {
html += "<td style='color: red'><i class='fa fa-warning'></i>";
}
html += "<td>" + row.comments + "</td>";
html += "</tr>";
}
html += "</tbody>";
$("#configCheck").html(html);
// Show the modal
const myModal = new bootstrap.Modal(document.getElementById('sanityModal'), { focus: true });
myModal.show();
})
}
$(document).ready(start);
</script>
</body>
</html>

View File

@ -41,6 +41,9 @@
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
</li>

View File

@ -48,6 +48,9 @@
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
LibreQoS</a>

View File

@ -41,6 +41,9 @@
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
</li>

View File

@ -48,6 +48,9 @@
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
LibreQoS</a>

View File

@ -40,6 +40,9 @@
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
</li>

View File

@ -0,0 +1,17 @@
[package]
name = "lqos_support_tool"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.7", features = ["derive"] }
colored = "2.1.0"
lqos_config = { path = "../lqos_config" }
serde = { version = "1.0", features = ["derive"] }
serde_cbor = "0" # For RFC8949/7409 format C binary objects
miniz_oxide = "0.7.1"
log = "0.4.21" # For compression
libc = "0.2.155"
nix = "0.29.0"
serde_json = "1.0.117"

View File

@ -0,0 +1,13 @@
use colored::Colorize;
pub fn success(s: &str) {
println!("{} - {s}", "OK".bright_green());
}
pub fn warn(s: &str) {
println!("{} - {s}", "WARN".bright_yellow());
}
pub fn error(s: &str) {
println!("{} - {s}", "ERROR".bright_red());
}

View File

@ -0,0 +1,36 @@
//! Provides a support library for the support tool system.
mod support_info;
pub mod console;
mod sanity_checks;
use std::io::Write;
use std::net::TcpStream;
pub use support_info::gather_all_support_info;
pub use support_info::SupportDump;
pub use sanity_checks::run_sanity_checks;
use crate::console::{error, success};
pub use sanity_checks::SanityChecks;
const REMOTE_SYSTEM: &str = "stats.libreqos.io:9200";
pub fn submit_to_network(dump: SupportDump) {
// Build the payload
let serialized = dump.serialize_and_compress().unwrap();
let length = serialized.len() as u64;
let mut bytes = Vec::new();
bytes.extend(1212u32.to_be_bytes());
bytes.extend(length.to_be_bytes());
bytes.extend(&serialized);
bytes.extend(1212u32.to_be_bytes());
// Do the actual connection
let stream = TcpStream::connect(REMOTE_SYSTEM);
if stream.is_err() {
error(&format!("Unable to connect to {REMOTE_SYSTEM}"));
println!("{stream:?}");
return;
}
let mut stream = stream.unwrap();
stream.write_all(&bytes).unwrap();
success("Submitted to LibreQoS for Analysis. Thank you.");
}

View File

@ -0,0 +1,189 @@
//! Provides the start of a text-mode support tool for LibreQoS. It will double as
//! a library (see `lib.rs`) to provide similar functionality from the GUI.
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use clap::{Parser, Subcommand};
use colored::Colorize;
use lqos_config::load_config;
use lqos_support_tool::{gather_all_support_info, run_sanity_checks, SupportDump};
#[derive(Parser)]
#[command(version = "1.0", about = "LibreQoS Support Tool", long_about = None, arg_required_else_help = true)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
/// Sanity Checks your Configuration against your hardware
Sanity,
/// Gather Support Info and Save it to /tmp
Gather,
/// Gather Support Info and Send it to the LibreQoS Team. Note that LTS users and donors get priority, we don't guarantee that we'll help anyone else. Please make sure you've tried Zulip first ( https://chat.libreqos.io/ )
Submit,
/// Summarize the contents of a support dump
Summarize {
/// The filename to read
filename: String
},
/// Expand all submitted data from a support dump into a given directory
Expand {
/// The filename to read from
filename: String,
/// The target directory
target: String,
}
}
fn read_line() -> String {
use std::io::{stdin,stdout,Write};
let mut s = String::new();
stdin().read_line(&mut s).expect("Did not enter a correct string");
s.trim().to_string()
}
fn get_lts_key() -> String {
if let Ok(cfg) = load_config() {
if let Some(key) = cfg.long_term_stats.license_key {
return key.clone();
}
}
println!();
println!("{}", "No LTS Key Found!".bright_red());
println!("We prioritize helping Long-Term Stats Subscribers and Donors.");
println!("Please enter your LTS key (from your email), or ENTER for none:");
return read_line();
}
fn get_name() -> String {
let mut candidate = String::new();
while candidate.is_empty() {
println!("Please enter your name, email address and Zulip handle in a single line (ENTER when done).");
candidate = read_line();
}
candidate
}
fn get_comments() -> String {
println!("Anything you'd like to tell us about? (Comments)");
read_line()
}
fn gather_dump() {
let name = get_name();
let lts_key = get_lts_key();
let comments = get_comments();
let dump = gather_all_support_info(&name, &comments, &lts_key).unwrap();
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let filename = format!("/tmp/libreqos_{}.support", timestamp);
let path = Path::new(&filename);
std::fs::write(&path, dump.serialize_and_compress().unwrap()).unwrap();
lqos_support_tool::console::success(&format!("Dump written to {}", filename));
}
fn summarize(filename: &str) {
let path = Path::new(filename);
if !path.exists() {
println!("Dump not found at {filename}");
} else {
let bytes = std::fs::read(&path).unwrap();
if let Ok(decoded) = SupportDump::from_bytes(&bytes) {
println!("Sent by: {}", decoded.sender);
println!("Comments: {}", decoded.comment);
println!("LTS Key: {}", decoded.lts_key);
println!("{:50} {:10} {}", "Sanity Check", "Success?", "Comment");
for entry in decoded.sanity_checks.results.iter() {
println!("{:50} {:10} {}", entry.name, entry.success, entry.comments);
}
println!();
println!("{:40} {:10} : {:40?}", "ENTRY", "SIZE", "FILENAME");
for entry in decoded.entries.iter() {
println!("{:40} {:10} : {:40?}", entry.name, entry.contents.len(), entry.filename);
}
} else {
println!("Dump did not decode");
}
}
}
fn sanity_checks() {
if let Err(e) = run_sanity_checks() {
println!("Sanity Check Failed: {e:?}");
}
}
fn expand(filename: &str, target: &str) {
// Check inputs
let in_path = Path::new(filename);
if !in_path.exists() {
println!("{} not found", filename);
return;
}
let out_path = Path::new(target);
if !out_path.exists() {
println!("{} not found", target);
return;
}
if !out_path.is_dir() {
println!("{} is not a directory", target);
return;
}
// Load the data
let bytes = std::fs::read(&in_path).unwrap();
if let Ok(decoded) = SupportDump::from_bytes(&bytes) {
// Save the header
let header = format!("From: {}\nComment: {}\nLTS Key: {}\n", decoded.sender, decoded.comment, decoded.lts_key);
let header_path = out_path.join("header.txt");
std::fs::write(header_path, header.as_bytes()).unwrap();
// Save the sanity check results
let mut sanity = String::new();
for check in decoded.sanity_checks.results.iter() {
sanity += &format!("{} - Success? {}: {}\n", check.name, check.success, check.comments);
}
let sanity_path = out_path.join("sanity_checks.txt");
std::fs::write(sanity_path, sanity.as_bytes()).unwrap();
// Save the files
for (idx, dump) in decoded.entries.iter().enumerate() {
let trimmed = dump.name.trim().replace(' ', "").to_lowercase().replace('(', "").replace(')', "");
let dump_path = out_path.join(&format!("{idx:0>2}_{}", trimmed));
std::fs::write(dump_path, dump.contents.as_bytes()).unwrap();
}
}
println!("Expanded data written to {}", target);
}
fn submit() {
// Get header
let name = get_name();
let lts_key = get_lts_key();
let comments = get_comments();
// Get the data
let dump = gather_all_support_info(&name, &comments, &lts_key).unwrap();
// Send it
lqos_support_tool::submit_to_network(dump);
}
fn main() {
let cli = Cli::parse();
match cli.command {
Some(Commands::Sanity) => sanity_checks(),
Some(Commands::Gather) => gather_dump(),
Some(Commands::Summarize { filename }) => summarize(&filename),
Some(Commands::Expand { filename, target }) => expand(&filename, &target),
Some(Commands::Submit) => submit(),
_ => {}
}
}

View File

@ -0,0 +1,57 @@
mod config_sane;
mod interfaces;
mod queues;
mod bridge;
mod net_json;
mod shaped_devices;
use serde::{Deserialize, Serialize};
use crate::console::{error, success};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct SanityChecks {
pub results: Vec<SanityCheck>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct SanityCheck {
pub name: String,
pub success: bool,
pub comments: String,
}
pub fn run_sanity_checks() -> anyhow::Result<SanityChecks> {
println!("Running Sanity Checks");
let mut results = Vec::new();
// Run the checks
config_sane::config_exists(&mut results);
config_sane::can_load_config(&mut results);
interfaces::interfaces_exist(&mut results);
queues::sanity_check_queues(&mut results);
bridge::check_interface_status(&mut results);
bridge::check_bridge(&mut results);
net_json::check_net_json_exists(&mut results);
net_json::can_we_load_net_json(&mut results);
net_json::can_we_parse_net_json(&mut results);
shaped_devices::shaped_devices_exists(&mut results);
shaped_devices::can_we_read_shaped_devices(&mut results);
shaped_devices::parent_check(&mut results);
// Did any fail?
let mut any_errors = false;
for s in results.iter() {
if s.success {
success(&format!("{} {}", s.name, s.comments));
} else {
error(&format!("{}: {}", s.name, s.comments));
any_errors = true;
}
}
if any_errors {
error("ERRORS FOUND DURING SANITY CHECK");
}
Ok(SanityChecks { results })
}

View File

@ -0,0 +1,124 @@
use std::process::Command;
use lqos_config::load_config;
use crate::sanity_checks::SanityCheck;
pub fn check_bridge(results: &mut Vec<SanityCheck>) {
if let Ok(cfg) = load_config() {
if let Ok(interfaces) = get_interfaces_from_ip_link() {
// On a stick mode is bridge-free
if cfg.on_a_stick_mode() {
results.push(SanityCheck{
name: format!("Single Interface Mode: Bridges Ignored"),
success: true,
comments: "".to_string(),
});
return;
}
// Is the XDP bridge enabled?
if let Some(bridge) = &cfg.bridge {
if bridge.use_xdp_bridge {
for bridge_if in interfaces
.iter()
.filter(|bridge_if| bridge_if.link_type == "ether" && bridge_if.operational_state == "UP")
{
// We found a bridge. Check member interfaces to check that it does NOT include any XDP
// bridge members.
let in_bridge: Vec<&IpLinkInterface> = interfaces
.iter()
.filter(|member_if| {
if let Some(master) = &member_if.master {
master == &bridge_if.name
} else {
false
}
})
.filter(|member_if| member_if.name == cfg.internet_interface() || member_if.name == cfg.isp_interface())
.collect();
if in_bridge.len() == 2 {
results.push(SanityCheck{
name: format!("Linux Bridge AND XDP Bridge At Once ({})", bridge_if.name),
success: false,
comments: format!("Bridge ({}) contains both the internet and ISP interfaces, and you have the xdp_bridge enabled. This is not supported.", bridge_if.name),
});
} else {
results.push(SanityCheck{
name: format!("Bridge Membership Check ({})", bridge_if.name),
success: true,
comments: "".to_string(),
});
}
}
}
}
}
}
}
pub fn check_interface_status(results: &mut Vec<SanityCheck>) {
if let Ok(cfg) = load_config() {
if let Ok(interfaces) = get_interfaces_from_ip_link() {
if let Some(stick) = &cfg.single_interface {
if let Some(iface) = interfaces.iter().find(|i| i.name == stick.interface) {
results.push(SanityCheck{
name: format!("Interface {} in state {}", iface.name, iface.operational_state),
success: true,
comments: "".to_string(),
});
}
} else if let Some(bridge) = &cfg.bridge {
if let Some(iface) = interfaces.iter().find(|i| i.name == bridge.to_internet) {
results.push(SanityCheck{
name: format!("Interface {} in state {}", iface.name, iface.operational_state),
success: true,
comments: "".to_string(),
});
}
if let Some(iface) = interfaces.iter().find(|i| i.name == bridge.to_network) {
results.push(SanityCheck{
name: format!("Interface {} in state {}", iface.name, iface.operational_state),
success: true,
comments: "".to_string(),
});
}
}
}
}
}
#[derive(Debug)]
struct IpLinkInterface {
pub name: String,
pub index: u32,
pub operational_state: String,
pub link_type: String,
pub master: Option<String>,
}
fn get_interfaces_from_ip_link() -> anyhow::Result<Vec<IpLinkInterface>> {
let output = Command::new("/sbin/ip")
.args(["-j", "link"])
.output()?;
let output = String::from_utf8(output.stdout)?;
let output_json = serde_json::from_str::<serde_json::Value>(&output)?;
let mut interfaces = Vec::new();
for interface in output_json.as_array().unwrap() {
let name = interface["ifname"].as_str().unwrap().to_string();
let index = interface["ifindex"].as_u64().unwrap() as u32;
let operstate = interface["operstate"].as_str().unwrap().to_string();
let link_type = interface["link_type"].as_str().unwrap().to_string();
let master = interface["master"].as_str().map(|s| s.to_string());
interfaces.push(IpLinkInterface {
name,
index,
operational_state: operstate,
link_type,
master,
});
}
Ok(interfaces)
}

View File

@ -0,0 +1,34 @@
use std::path::Path;
use lqos_config::load_config;
use crate::sanity_checks::SanityCheck;
pub fn config_exists(results: &mut Vec<SanityCheck>) {
let path = Path::new("/etc/lqos.conf");
let mut result = SanityCheck {
name: "Config File Exists".to_string(),
..Default::default()
};
if path.exists() {
result.success = true;
} else {
result.success = false;
result.comments = "/etc/lqos.conf could not be opened".to_string();
}
results.push(result);
}
pub fn can_load_config(results: &mut Vec<SanityCheck>) {
let mut result = SanityCheck {
name: "Config File Can Be Loaded".to_string(),
..Default::default()
};
let cfg = load_config();
if cfg.is_ok() {
result.success = true;
} else {
result.success = false;
result.comments = "Configuration file could not be loaded".to_string();
}
results.push(result);
}

View File

@ -0,0 +1,63 @@
use std::ffi::CString;
use anyhow::Error;
use lqos_config::load_config;
use crate::sanity_checks::SanityCheck;
fn interface_name_to_index(interface_name: &str) -> anyhow::Result<u32> {
use nix::libc::if_nametoindex;
let if_name = CString::new(interface_name)?;
let index = unsafe { if_nametoindex(if_name.as_ptr()) };
if index == 0 {
Err(Error::msg(format!("Unknown interface: {interface_name}")))
} else {
Ok(index)
}
}
pub fn interfaces_exist(results: &mut Vec<SanityCheck>) {
if let Ok(cfg) = load_config() {
if cfg.on_a_stick_mode() {
if interface_name_to_index(&cfg.internet_interface()).is_ok() {
results.push(SanityCheck {
name: "Single Interface Exists".to_string(),
success: true,
comments: "".to_string(),
});
} else {
results.push(SanityCheck {
name: "Single Interface Exists".to_string(),
success: false,
comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.internet_interface()),
});
}
} else {
if interface_name_to_index(&cfg.internet_interface()).is_ok() {
results.push(SanityCheck {
name: "Internet Interface Exists".to_string(),
success: true,
comments: "".to_string(),
});
} else {
results.push(SanityCheck {
name: "Internet Interface Exists".to_string(),
success: false,
comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.internet_interface()),
});
}
if interface_name_to_index(&cfg.isp_interface()).is_ok() {
results.push(SanityCheck {
name: "ISP Facing Interface Exists".to_string(),
success: true,
comments: "".to_string(),
});
} else {
results.push(SanityCheck {
name: "ISP Facing Interface Exists".to_string(),
success: false,
comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.isp_interface()),
});
}
}
}
}

View File

@ -0,0 +1,68 @@
use std::path::Path;
use serde_json::Value;
use lqos_config::load_config;
use crate::sanity_checks::SanityCheck;
pub fn check_net_json_exists(results: &mut Vec<SanityCheck>) {
if let Ok(cfg) = load_config() {
let path = Path::new(&cfg.lqos_directory).join("network.json");
if path.exists() {
results.push(SanityCheck{
name: "network.json exists".to_string(),
success: true,
comments: "".to_string(),
});
} else {
results.push(SanityCheck{
name: "network.json exists".to_string(),
success: false,
comments: format!("File not found at {:?}", path),
});
}
}
}
pub fn can_we_load_net_json(results: &mut Vec<SanityCheck>) {
if let Ok(cfg) = load_config() {
let path = Path::new(&cfg.lqos_directory).join("network.json");
if path.exists() {
if let Ok(str) = std::fs::read_to_string(path) {
match serde_json::from_str::<Value>(&str) {
Ok(json) => {
results.push(SanityCheck{
name: "network.json is parseable JSON".to_string(),
success: true,
comments: "".to_string(),
});
}
Err(e) => {
results.push(SanityCheck{
name: "network.json is parseable JSON".to_string(),
success: false,
comments: format!("{e:?}"),
});
}
}
}
}
}
}
pub fn can_we_parse_net_json(results: &mut Vec<SanityCheck>) {
match lqos_config::NetworkJson::load() {
Ok(json) => {
results.push(SanityCheck{
name: "network.json is valid JSON".to_string(),
success: true,
comments: "".to_string(),
});
}
Err(e) => {
results.push(SanityCheck{
name: "network.json is valid JSON".to_string(),
success: false,
comments: format!("{e:?}"),
});
}
}
}

View File

@ -0,0 +1,82 @@
use std::path::Path;
use lqos_config::load_config;
use crate::sanity_checks::SanityCheck;
fn check_queues(interface: &str) -> (i32, i32) {
let path = format!("/sys/class/net/{interface}/queues/");
let sys_path = Path::new(&path);
if !sys_path.exists() {
return (0,0);
}
let mut counts = (0, 0);
let paths = std::fs::read_dir(sys_path).unwrap();
for path in paths {
if let Ok(path) = &path {
if path.path().is_dir() {
if let Some(filename) = path.path().file_name() {
if let Some(filename) = filename.to_str() {
if filename.starts_with("rx-") {
counts.0 += 1;
} else if filename.starts_with("tx-") {
counts.1 += 1;
}
}
}
}
}
}
counts
}
pub fn sanity_check_queues(results: &mut Vec<SanityCheck>) {
if let Ok(cfg) = load_config() {
if cfg.on_a_stick_mode() {
let counts = check_queues(&cfg.internet_interface());
if counts.0 > 1 && counts.1 > 1 {
results.push(SanityCheck{
name: "Queue Check (Internet Interface)".to_string(),
success: true,
comments: "".to_string(),
});
} else {
results.push(SanityCheck{
name: "Queue Check (Internet Interface)".to_string(),
success: false,
comments: format!("{} does not provide multiple RX and TX queues", cfg.internet_interface()),
});
}
} else {
let counts = check_queues(&cfg.internet_interface());
if counts.0 > 1 && counts.1 > 1 {
results.push(SanityCheck{
name: "Queue Check (Internet Interface)".to_string(),
success: true,
comments: "".to_string(),
});
} else {
results.push(SanityCheck{
name: "Queue Check (Internet Interface)".to_string(),
success: false,
comments: format!("{} does not provide multiple RX and TX queues", cfg.internet_interface()),
});
}
let counts = check_queues(&cfg.isp_interface());
if counts.0 > 1 && counts.1 > 1 {
results.push(SanityCheck{
name: "Queue Check (ISP Facing Interface)".to_string(),
success: true,
comments: "".to_string(),
});
} else {
results.push(SanityCheck{
name: "Queue Check (ISP Facing Interface)".to_string(),
success: false,
comments: format!("{} does not provide multiple RX and TX queues", cfg.isp_interface()),
});
}
}
}
}

View File

@ -0,0 +1,66 @@
use std::path::Path;
use lqos_config::load_config;
use crate::sanity_checks::SanityCheck;
pub fn shaped_devices_exists(results: &mut Vec<SanityCheck>) {
if let Ok(cfg) = load_config() {
let path = Path::new(&cfg.lqos_directory).join("ShapedDevices.csv");
if path.exists() {
results.push(SanityCheck{
name: "ShapedDevices.csv exists".to_string(),
success: true,
comments: "".to_string(),
});
} else {
results.push(SanityCheck{
name: "ShapedDevices.csv exists".to_string(),
success: false,
comments: format!("File not found at {:?}", path),
});
}
}
}
pub fn can_we_read_shaped_devices(results: &mut Vec<SanityCheck>) {
match lqos_config::ConfigShapedDevices::load() {
Ok(sd) => {
results.push(SanityCheck{
name: "ShapedDevices.csv Loads?".to_string(),
success: true,
comments: format!("{} Devices Found", sd.devices.len()),
});
}
Err(e) => {
results.push(SanityCheck{
name: "ShapedDevices.csv Loads?".to_string(),
success: false,
comments: format!("{e:?}"),
});
}
}
}
pub fn parent_check(results: &mut Vec<SanityCheck>) {
if let Ok(net_json) = lqos_config::NetworkJson::load() {
if net_json.nodes.len() < 2 {
results.push(SanityCheck{
name: "Flat Network - Skipping Parent Check".to_string(),
success: true,
comments: String::new(),
});
return;
}
if let Ok(shaped_devices) = lqos_config::ConfigShapedDevices::load() {
for sd in shaped_devices.devices.iter() {
if !net_json.nodes.iter().any(|n| n.name == sd.parent_node) {
results.push(SanityCheck{
name: "Shaped Device Invalid Parent".to_string(),
success: false,
comments: format!("Device {}/{} is parented to {} - which does not exist", sd.device_name, sd.device_id, sd.parent_node),
});
}
}
}
}
}

View File

@ -0,0 +1,128 @@
use colored::Colorize;
use serde::{Deserialize, Serialize};
use crate::console::error;
use crate::sanity_checks::{run_sanity_checks, SanityChecks};
mod ip_link;
mod systemctl_services;
mod systemctl_service_single;
mod lqos_config;
mod task_journal;
mod service_config;
mod ip_addr;
mod kernel_info;
mod distro_name;
pub trait SupportInfo {
fn get_string(&self) -> String;
fn get_name(&self) -> String;
fn get_filename(&self) -> Option<String>;
fn gather(&mut self) -> anyhow::Result<()>;
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct SupportDump {
pub sender: String,
pub comment: String,
pub lts_key: String,
pub sanity_checks: SanityChecks,
pub entries: Vec<DumpEntry>
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct DumpEntry {
pub name: String,
pub filename: Option<String>,
pub contents: String,
}
impl SupportDump {
pub fn serialize_and_compress(&self) -> anyhow::Result<Vec<u8>> {
let cbor_bytes = serde_cbor::to_vec(self)?;
let compressed_bytes = miniz_oxide::deflate::compress_to_vec(&cbor_bytes, 10);
Ok(compressed_bytes)
}
pub fn from_bytes(raw_bytes: &[u8]) -> anyhow::Result<Self> {
let decompressed_bytes = miniz_oxide::inflate::decompress_to_vec(raw_bytes).unwrap();
let deserialized = serde_cbor::from_slice(&decompressed_bytes)?;
Ok(deserialized)
}
}
pub fn gather_all_support_info(sender: &str, comments: &str, lts_key: &str) -> anyhow::Result<SupportDump> {
let sanity_checks = run_sanity_checks()?;
let mut data_targets: Vec<Box<dyn SupportInfo>> = vec![
lqos_config::LqosConfig::boxed(),
ip_link::IpLink::boxed(),
ip_addr::IpAddr::boxed(),
kernel_info::KernelInfo::boxed(),
distro_name::DistroName::boxed(),
systemctl_services::SystemCtlServices::boxed(),
systemctl_service_single::SystemCtlService::boxed("lqosd"),
systemctl_service_single::SystemCtlService::boxed("lqos_node_manager"),
systemctl_service_single::SystemCtlService::boxed("lqos_scheduler"),
task_journal::TaskJournal::boxed("lqosd"),
task_journal::TaskJournal::boxed("lqos_node_manager"),
task_journal::TaskJournal::boxed("lqos_scheduler"),
service_config::ServiceConfig::boxed("ShapedDevices.csv"),
service_config::ServiceConfig::boxed("network.json"),
];
for target in data_targets.iter_mut() {
println!("{} : {}",
"TASK-GATHER".cyan(),
target.get_name().yellow()
);
if let Err(e) = target.gather() {
error(&e.to_string());
}
}
let mut dump = SupportDump {
sender: sender.to_string(),
comment: comments.to_string(),
lts_key: lts_key.to_string(),
sanity_checks,
entries: Vec::new(),
};
for target in data_targets.iter() {
let entry = DumpEntry {
name: target.get_name(),
filename: target.get_filename(),
contents: target.get_string(),
};
dump.entries.push(entry);
}
//println!("{dump:#?}");
Ok(dump)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip() {
let original = SupportDump {
entries: vec![
DumpEntry {
name: "Test".to_string(),
filename: None,
contents: "BLAH".to_string(),
}
],
..Default::default()
};
let bytes = original.serialize_and_compress().unwrap();
let restored = SupportDump::from_bytes(&bytes).unwrap();
assert_eq!(original.entries.len(), restored.entries.len());
assert_eq!(original.entries.len(), 1);
assert_eq!(original.entries[0].name, "Test");
assert!(original.entries[0].filename.is_none());
assert_eq!(original.entries[0].contents, "BLAH");
}
}

View File

@ -0,0 +1,38 @@
use std::process::Command;
use crate::console::success;
use crate::support_info::SupportInfo;
#[derive(Debug, Default)]
pub struct DistroName {
output: String,
}
impl SupportInfo for DistroName {
fn get_string(&self) -> String {
self.output.to_string()
}
fn get_name(&self) -> String {
"LSB Distro Info".to_string()
}
fn get_filename(&self) -> Option<String> {
None
}
fn gather(&mut self) -> anyhow::Result<()> {
let output = Command::new("/bin/lsb_release")
.arg("-a")
.output()?;
let out_str = String::from_utf8_lossy(output.stdout.as_slice());
self.output = out_str.to_string();
success("Gathered distro info");
Ok(())
}
}
impl DistroName {
pub fn boxed() -> Box<Self> {
Box::new(Self::default())
}
}

View File

@ -0,0 +1,38 @@
use std::process::Command;
use crate::console::success;
use crate::support_info::SupportInfo;
#[derive(Debug, Default)]
pub struct IpAddr {
output: String,
}
impl SupportInfo for IpAddr {
fn get_string(&self) -> String {
self.output.to_string()
}
fn get_name(&self) -> String {
"IP Address Information".to_string()
}
fn get_filename(&self) -> Option<String> {
None
}
fn gather(&mut self) -> anyhow::Result<()> {
let output = Command::new("/sbin/ip")
.arg("addr")
.output()?;
let out_str = String::from_utf8_lossy(output.stdout.as_slice());
self.output = out_str.to_string();
success("Gathered `ip addr` data");
Ok(())
}
}
impl IpAddr {
pub fn boxed() -> Box<Self> {
Box::new(Self::default())
}
}

View File

@ -0,0 +1,38 @@
use std::process::Command;
use crate::console::success;
use crate::support_info::SupportInfo;
#[derive(Debug, Default)]
pub struct IpLink {
output: String,
}
impl SupportInfo for IpLink {
fn get_string(&self) -> String {
self.output.to_string()
}
fn get_name(&self) -> String {
"IP Link Information".to_string()
}
fn get_filename(&self) -> Option<String> {
None
}
fn gather(&mut self) -> anyhow::Result<()> {
let output = Command::new("/sbin/ip")
.arg("link")
.output()?;
let out_str = String::from_utf8_lossy(output.stdout.as_slice());
self.output = out_str.to_string();
success("Gathered `ip link` data");
Ok(())
}
}
impl IpLink {
pub fn boxed() -> Box<Self> {
Box::new(Self::default())
}
}

View File

@ -0,0 +1,38 @@
use std::process::Command;
use crate::console::success;
use crate::support_info::SupportInfo;
#[derive(Debug, Default)]
pub struct KernelInfo {
output: String,
}
impl SupportInfo for KernelInfo {
fn get_string(&self) -> String {
self.output.to_string()
}
fn get_name(&self) -> String {
"Uname Kernel Info".to_string()
}
fn get_filename(&self) -> Option<String> {
None
}
fn gather(&mut self) -> anyhow::Result<()> {
let output = Command::new("/bin/uname")
.arg("-a")
.output()?;
let out_str = String::from_utf8_lossy(output.stdout.as_slice());
self.output = out_str.to_string();
success("Gathered kernel info");
Ok(())
}
}
impl KernelInfo {
pub fn boxed() -> Box<Self> {
Box::new(Self::default())
}
}

View File

@ -0,0 +1,38 @@
use std::path::Path;
use crate::console::success;
use crate::support_info::SupportInfo;
#[derive(Default)]
pub struct LqosConfig {
output: String,
}
impl SupportInfo for LqosConfig {
fn get_string(&self) -> String {
self.output.to_string()
}
fn get_name(&self) -> String {
"LibreQoS Config File".to_string()
}
fn get_filename(&self) -> Option<String> {
Some("/etc/lqos.conf".to_string())
}
fn gather(&mut self) -> anyhow::Result<()> {
let path = Path::new("/etc/lqos.conf");
if !path.exists() {
anyhow::bail!("/etc/lqos.conf could not be opened");
}
self.output = std::fs::read_to_string(path)?;
success("Gathered /etc/lqos.conf");
Ok(())
}
}
impl LqosConfig {
pub fn boxed() -> Box<Self> {
Box::new(Self::default())
}
}

View File

@ -0,0 +1,50 @@
use std::path::Path;
use lqos_config::load_config;
use crate::console::{error, success};
use crate::support_info::SupportInfo;
#[derive(Default)]
pub struct ServiceConfig {
target: String,
output: String,
}
impl SupportInfo for ServiceConfig {
fn get_string(&self) -> String {
self.output.to_string()
}
fn get_name(&self) -> String {
format!("Config File: {}", self.target)
}
fn get_filename(&self) -> Option<String> {
let cfg = load_config();
if let Ok(cfg) = cfg {
Some(format!("{}{}", cfg.lqos_directory, self.target))
} else {
error("Unable to read configuration!");
None
}
}
fn gather(&mut self) -> anyhow::Result<()> {
let cfg = load_config()?;
let path = Path::new(&cfg.lqos_directory).join(&self.target);
if !path.exists() {
anyhow::bail!("Could not read from {:?}", path);
}
self.output = std::fs::read_to_string(path)?;
success(&format!("Gathered {}", self.target));
Ok(())
}
}
impl ServiceConfig {
pub fn boxed<S: ToString>(target: S) -> Box<Self> {
Box::new(Self {
target: target.to_string(),
..Default::default()
})
}
}

View File

@ -0,0 +1,43 @@
use std::process::Command;
use crate::console::success;
use crate::support_info::SupportInfo;
#[derive(Default)]
pub struct SystemCtlService {
target: String,
output: String,
}
impl SupportInfo for SystemCtlService {
fn get_string(&self) -> String {
self.output.to_string()
}
fn get_name(&self) -> String {
format!("SystemCtl Status ({})", self.target)
}
fn get_filename(&self) -> Option<String> {
None
}
fn gather(&mut self) -> anyhow::Result<()> {
let out = Command::new("/bin/systemctl")
.args(&["--no-pager", "status", &self.target])
.output()?;
self.output = String::from_utf8_lossy(&out.stdout).to_string();
success(&format!("Gathered systemctl status for {}", self.target));
Ok(())
}
}
impl SystemCtlService {
pub fn boxed<S: ToString>(target: S) -> Box<Self> {
Box::new(Self {
target: target.to_string(),
..Default::default()
})
}
}

View File

@ -0,0 +1,39 @@
use std::process::Command;
use crate::console::success;
use crate::support_info::SupportInfo;
#[derive(Default)]
pub struct SystemCtlServices {
output: String,
}
impl SupportInfo for SystemCtlServices {
fn get_string(&self) -> String {
self.output.to_string()
}
fn get_name(&self) -> String {
"SystemCtl Status".to_string()
}
fn get_filename(&self) -> Option<String> {
None
}
fn gather(&mut self) -> anyhow::Result<()> {
let out = Command::new("/bin/systemctl")
.args(&["--no-pager", "status"])
.output()?;
self.output = String::from_utf8_lossy(&out.stdout).to_string();
success("Gathered global `systemctl status`");
Ok(())
}
}
impl SystemCtlServices {
pub fn boxed() -> Box<Self> {
Box::new(Self::default())
}
}

View File

@ -0,0 +1,43 @@
use std::process::Command;
use crate::console::success;
use crate::support_info::SupportInfo;
#[derive(Default)]
pub struct TaskJournal {
target: String,
output: String,
}
impl SupportInfo for TaskJournal {
fn get_string(&self) -> String {
self.output.to_string()
}
fn get_name(&self) -> String {
format!("Journal ({})", self.target)
}
fn get_filename(&self) -> Option<String> {
None
}
fn gather(&mut self) -> anyhow::Result<()> {
let out = Command::new("/bin/journalctl")
.args(&["--no-pager", "-u", &self.target])
.output()?;
self.output = String::from_utf8_lossy(&out.stdout).to_string();
success(&format!("Gathered journalctl status for {}", self.target));
Ok(())
}
}
impl TaskJournal {
pub fn boxed<S: ToString>(target: S) -> Box<Self> {
Box::new(Self {
target: target.to_string(),
..Default::default()
})
}
}