This commit is contained in:
Mateusz Gruszczyński committed 2026-10-06 15:31:59 +02:00
1 parent f07429c267
commit 5d2aaeabe6
11 files changed
+302 -14

No files matched your search

+1
View File
@@ -13,6 +13,7 @@
- Refresh dynamic ACL editor translations after the language bundle loads or the user changes language, preventing raw keys such as `aclProfileNew` or `aclProfilePreviewBlocksAll` from appearing in the UI.
- Accept single IPv4/IPv6 host addresses in ACL rules without requiring users to type `/32` or `/128`; CIDR input remains supported.
- Document `migrate_npm.py` sibling-module requirements and correct the container migration example.
- Detect Linux runtime type in Process diagnostics and make LXC uptime/load container-aware while leaving VM/host and other environments on the existing statistics path.
- Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.5.
## 0.26.5
+1
View File
@@ -17,6 +17,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost
- Clear the ACME certificate-creation form after the issuance job finishes successfully, so successful certificates do not leave stale values while failed jobs keep the non-secret form values available for correction.
- Compose explicit IP allows and **Allow only** countries as alternatives inside one ACL policy scope. Explicit IP denies and blocked countries still win, and profile/vhost/location scopes remain cumulative. This makes `default=deny` support private CIDRs **or** selected countries as intended.
- Document the sibling `migrate_npm_core.py` and `migrate_npm_api.py` modules required by `scripts/migrate_npm.py`, and fix the container example to mount the complete `scripts` directory.
- Detect the Linux runtime environment in Process diagnostics. LXC reports `Linux / LXC` and overrides system uptime with PID 1 age plus load average from the container-visible `/proc/loadavg`; VM/host/Docker and other Linux environments keep the existing host statistics path.
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.5 requires no data migration.
0.26.6 retains the 0.26.5 interface-scaling and listener-navigation fixes below.
+10 -10
View File
@@ -2,11 +2,11 @@
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
e73f72bb57de20471e00ff480c6d253fda2332fae5c7cac8b03e9d41c46107c2 AGENTS.md
0626ac8645ed0de397efbe49a9cdfd50503a99a152d54075b05d03a90e947b50 CHANGELOG.md
772c0029a0fffae09543aeeefa8f2eb515d335e2999c91b569b828bfc6dd3326 CHANGELOG.md
c4a563a2ede2b8296b5b6b6da21bd0ca47b5763c7eab3cefd3194e8db09c89cb Cargo.lock
4fdd0088ff59114a81e3ab45f4d1a3cbb5e6f36f86177bd2af7e3ddc5ec08d5c Cargo.toml
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
aa46c791f1e23c25e3c7c5eb57bdbfd2548f29dbf3af41c81eed69f51be7dd61 README.md
a08f6efc32fa4bd581f75aa35d9d7a01bdd3e9ddbe9fcf1c92ce8a8e5a28167f README.md
df48dd2780d331a320af6f22a7da07a5b0d35415a254bfd83647d1bad7fdc782 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
@@ -72,7 +72,7 @@ a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
f8baa8bdc8a67bf4e4a6e6d233435f5a43075a09bf08b08b597fcef1853ce5c5 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
f5fcba03d7d7e7f6b94300881dcd7f8ae38866f85795600dbbeb51c1f38e6ec0 docs/TEST_REPORT.md
57ab62ba11a468b34bbed5bd6604e0a416d030354062596da119f47eedecfbcd docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
@@ -105,7 +105,7 @@ bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_A
57edc3d5ca6c268b2b6b2e8027e81fb543c0dab86e8f69c4ee5776ea8df901c7 docs/V0.26.3_IMPLEMENTATION.md
da5ec87b9482f2fc506c4334d9365f3bfaec89254a14e5585dd6460326b20e10 docs/V0.26.4_IMPLEMENTATION.md
0ddf5215c40c8366844350bc6462d8013dba747b6f3e8fbf477985af215e5e8d docs/V0.26.5_IMPLEMENTATION.md
0a2e969a6f35922841fac5c4b5aa08f2080fca2afe3b39a452f6598d54517e3c docs/V0.26.6_IMPLEMENTATION.md
dd9b5dcdbd53a04fd32458caaf03b56aa335f0153b1a0e332fdf230fec12680e docs/V0.26.6_IMPLEMENTATION.md
ce259052c023e90a7bfb6e39fe82e52b845a8b2a051bfa29883d29e87a9301a2 docs/VHOSTS.md
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
@@ -146,7 +146,7 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
fac9f7a615a95378236bd664ea276792c171d9bf3a3b11f32789e692e0f93c79 scripts/check_source.py
af078d88007da9a72e8470e9205a2f360b1e665a519114f9a5ed1951cf948d45 scripts/check_source.py
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
b3488a5ea97a54a8bc6dd766e76ffb3f41aa9011a6731042bffc473d2d5f84fd scripts/deploy_core.py
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
@@ -161,7 +161,7 @@ d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_a
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
6fd9faad0842dae9ccde41eea0c5368a234b6aba396b73620cd5eeee83d6786b scripts/preflight.py
b992d0e0939e37ad8bce6d8cdf51233bd068983853b970ebb177a2690e1def63 scripts/release_smoke.py
167369ca944994a71323e07ef097a4f1d7d93a6a6aa42beb86ced05fb882d5aa scripts/release_smoke.py
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
@@ -216,7 +216,7 @@ f869e4da5de149fc04ce1aab658befeaf3208c862f149ca976235d15832c3cdb src/admin/test
446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js
9eccd075880950e9100c7a8cca936d92f0c7106ce5140515deb3cc57f03c2bf0 src/admin/web/app/core.js
6f15cbcfa47adf98b5da962811fbbc6731880bf99f128b372b6f9f41391eeed1 src/admin/web/app/preferences.js
d7f6675277c927f4eae1b83c7aa91b315429e7034dfe61c489c2f737a9c19f07 src/admin/web/app/runtime.js
5891cbb5bfe7219ecb85d6f8d1794be1c3e4593fd3caa0668b53bd4121d083ce src/admin/web/app/runtime.js
31e716be0ea45b84a56fc656b3d7529853d3d0a56698592a16375ff689b90956 src/admin/web/app/settings.js
642d013e2e18dc8a8eccc7a2fe93d6570521dc9a0644beac7af4bbe8c5cea1ee src/admin/web/cache-panel.js
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
@@ -421,9 +421,9 @@ b8b3150abe1c6f61d3f593e169795aae397b578607710ea6fd887b90f37b872e src/settings/t
46d9f63ce13a634e00f38d36ca3bdb217f2e37f04d75a4ef118e7dac9896a489 src/stream/model.rs
9c59bdb856106c0a74d1f8c156b0dd7289a170322ea556bad19bb8de3bf13d69 src/stream/runtime.rs
71c8850c9f80ec2c2eaa1ab3559670ba6b10be4fa5ad088e94047238d2d0c67b src/stream/tests.rs
ecb12075f5fe78bba9efa1ae89080f7f0ab8dab6c1daa2ac7fd00493f0a3841f src/system_stats/linux.rs
9c29121f17057cbcb3f3ea0a24fe90ad5e6cb80242830290ea55dc66265d8796 src/system_stats/linux/tests.rs
02701f4a90285dfc32c100f80e421846fbe29f5be935c4268ce3bba42e51e4bd src/system_stats/mod.rs
3070d78a7b88c2e47ed59960fa35e7d220a4cd7d78e48f6c6d2bcc5ff77f9620 src/system_stats/linux.rs
d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs
751d59582fb92c84e0c0cc7c0855c9d0a545805c9ba3be61826e50fb96c297ed src/system_stats/mod.rs
050aa831b1012c4aaade63863f433f01bfacf5d3f95787e57975572116a30ab2 src/system_stats/tests.rs
7467d83066bb6986d8c9103597f7a7617ac677579ed46d573c573ff6ca57bbd9 src/tinyauth.rs
0e1d87554db55e724f30c41ec4a5c80bdb8fc9c8cba8700ac509864d4abf8871 src/tinyauth/tests.rs
+3 -1
View File
@@ -4,12 +4,14 @@
RustProxy 0.26.6 is a compatibility-preserving access-policy and administration workflow patch on top of 0.26.5. SQLite remains schema 8 and the saved proxy configuration format is unchanged.
The release keeps backend diagnostics column/sort state synchronized with account-scoped administrator preferences, clears the certificate-creation form only after an ACME issue job reaches `ready`, documents all Python modules required by the NPM migrator, and composes explicit IP/country exceptions inside one ACL policy scope while retaining deny precedence and cumulative profile/vhost/location policies. The ACL profile UI now exposes guided block-by-default and allow-by-default modes with a live OR-policy preview, plus custom mode for mixed rules. Plain host IPs are accepted in addition to CIDRs.
The release keeps backend diagnostics column/sort state synchronized with account-scoped administrator preferences, clears the certificate-creation form only after an ACME issue job reaches `ready`, documents all Python modules required by the NPM migrator, and composes explicit IP/country exceptions inside one ACL policy scope while retaining deny precedence and cumulative profile/vhost/location policies. The ACL profile UI now exposes guided block-by-default and allow-by-default modes with a live OR-policy preview, plus custom mode for mixed rules. Plain host IPs are accepted in addition to CIDRs. Linux Process diagnostics also classify the runtime environment and, only for detected LXC, replace host-oriented uptime/load collection with PID 1 age and container-visible `/proc/loadavg`.
## Source-level regression coverage
Rust-independent contracts require the shared preference application path to reapply backend table state, the ACME UI to defer form reset until the matching issue job succeeds, the NPM migration documentation to ship and mount all three helper modules, frontend/location routing to use the composed IP/country ACL evaluator, and the ACL profile editor to retain its guided modes, preview and IP normalization path. Rust unit coverage in `src/acl/tests.rs` exercises allow-country OR allow-IP, block-country OR deny-IP, explicit deny precedence and plain IPv4/IPv6 host rules, but those Rust tests require a compiler-enabled release host.
Source contracts additionally require the Linux environment classifier, LXC-only uptime/load override, PID 1 start-time parser and Process UI `Linux / <environment>` composition. Rust unit coverage includes LXC-vs-VM precedence, VM/host classification, LXCFS proc detection, `/proc/loadavg` parsing and PID 1 stat parsing.
Existing source, security, memory, Web Server, documentation, packaging, upgrade, SQLite migration and administration JavaScript contracts remain enabled.
A compiler follow-up also removes a redundant route-level `country_resolved = true` write reported as `unused_assignments`; the write occurred after the flag's final read and therefore does not change GeoIP or ACL behavior.
+6
View File
@@ -30,6 +30,12 @@ The reusable ACL editor exposes this as two guided modes without changing the sa
A live policy preview shows the effective default and OR branches before save. The editor also accepts a single IPv4/IPv6 address and normalizes it to a host rule; the backend accepts the same address form directly, so API-authored profiles do not require `/32` or `/128` either.
## LXC runtime diagnostics
Linux diagnostics classify the runtime as LXC, Docker, Podman, systemd-nspawn, WSL, VM or Host for the Process platform label. Classification uses namespace/container metadata already exposed by procfs/sysfs/systemd and does not shell out. LXC detection takes precedence over DMI virtualization so an LXC guest running on a virtualized host is still shown as `Linux / LXC`.
Only LXC changes metric sourcing. `system_uptime_seconds` is replaced with the age of PID 1 calculated from `/proc/1/stat` and `CLOCK_BOOTTIME`, avoiding the host uptime returned by ordinary Linux system APIs. The 1/5/15 load values are read from the container-visible `/proc/loadavg`, allowing LXCFS-backed installations to expose their virtualized container load instead of the host load. Other environments retain the existing `systemstat` uptime/load path.
## Compatibility
- SQLite schema: 8, unchanged.
+7
View File
@@ -1215,6 +1215,13 @@ for marker_0241 in [
assert marker_0241 in system_stats_0241, marker_0241
for marker_0241 in ["/proc/stat", "/proc/meminfo", "RssAnon", "VmData", "VmSwap", "pub(crate) fn augment"]:
assert marker_0241 in linux_stats_0241, marker_0241
for marker_0266_lxc in [
"LinuxEnvironment::Lxc", "process_start_ticks", "CLOCK_BOOTTIME", "/proc/1/stat",
"/proc/loadavg", "lxc_load_average", "view.environment = Some(environment.label())",
"if environment == LinuxEnvironment::Lxc",
]:
assert marker_0266_lxc in linux_stats_0241, marker_0266_lxc
assert 'const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");' in app_js
for forbidden in ["sysctl", "netstat", "Command::new"]:
assert forbidden not in system_stats_0241, f"shared statistics adapter must not shell/parse per platform: {forbidden}"
for marker_0241 in ["installed_data_dir_candidates", '"/var/lib/rustproxy"', '"/var/db/rustproxy"', "resolve_installed_data_dir"]:
+7
View File
@@ -552,6 +552,13 @@ def source_contract_smoke() -> None:
assert forbidden not in system_stats, forbidden
for marker in ["/proc/stat", "/proc/meminfo", "RssAnon", "VmData", "VmSwap", "pub(crate) fn augment"]:
assert marker in linux_stats, marker
for marker in [
"LinuxEnvironment::Lxc", "process_start_ticks", "CLOCK_BOOTTIME", "/proc/1/stat",
"/proc/loadavg", "lxc_load_average", "view.environment = Some(environment.label())",
"if environment == LinuxEnvironment::Lxc",
]:
assert marker in linux_stats, marker
assert 'const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");' in app_js
for marker in ["installed_data_dir_candidates", "resolve_installed_data_dir", '"/var/lib/rustproxy"', '"/var/db/rustproxy"']:
assert marker in main, marker
for marker in ['payload["runtime"]', '"statistics_source"', '"data_directory"', '"database_path"']:
+2 -1
View File
@@ -297,7 +297,8 @@ function renderDiagnostics(status, appendSample = true) {
const coldLimit = Number(c.cold_limit_bytes), coldPeak = Number.isFinite(coldLimit) && coldLimit > 0 ? Math.max(coldLimit, ...cacheColdStorageSamples) : null;
$("cache-cold-sparkline").setAttribute("points", sparklinePoints(cacheColdStorageSamples, 300, 60, 5, coldPeak));
$("cache-cold-chart-value").textContent = coldEnabled ? occupancyText(c.cold_bytes, c.cold_limit_bytes) : t("notEnabled");
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", d.platform || "");
const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", processPlatform);
setOptionalMetric("process-cpu", d.process_cpu_percent != null, `${fmt(d.process_cpu_percent, 1)}%`);
const fds = knownPair(d.open_fds, d.max_fds, value => fmt(value), t("unlimited"));
setOptionalMetric("process-fds", fds != null, fds);
+208 -2
View File
@@ -1,6 +1,31 @@
use super::View;
use std::fs;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LinuxEnvironment {
Lxc,
Docker,
Podman,
SystemdNspawn,
Wsl,
Vm,
Host,
}
impl LinuxEnvironment {
fn label(self) -> &'static str {
match self {
Self::Lxc => "LXC",
Self::Docker => "Docker",
Self::Podman => "Podman",
Self::SystemdNspawn => "systemd-nspawn",
Self::Wsl => "WSL",
Self::Vm => "VM",
Self::Host => "Host",
}
}
}
fn key_value(text: &str, name: &str) -> Option<u64> {
text.lines().find_map(|line| {
let (key, rest) = line.split_once(':')?;
@@ -10,7 +35,180 @@ fn key_value(text: &str, name: &str) -> Option<u64> {
})
}
fn read_text(path: &str) -> String {
fs::read(path)
.ok()
.map(|bytes| String::from_utf8_lossy(&bytes).replace('\0', "\n"))
.unwrap_or_default()
}
fn lxcfs_proc_override(mountinfo: &str) -> bool {
mountinfo.lines().any(|line| {
line.contains("lxcfs")
&& (line.contains(" /proc/uptime ") || line.contains(" /proc/loadavg "))
})
}
fn classify_environment(
systemd_container: &str,
pid1_environment: &str,
cgroup: &str,
mountinfo: &str,
dmi: &str,
hypervisor: &str,
osrelease: &str,
dockerenv: bool,
containerenv: bool,
) -> LinuxEnvironment {
let systemd = systemd_container.trim().to_ascii_lowercase();
let pid1 = pid1_environment.to_ascii_lowercase();
let cgroup = cgroup.to_ascii_lowercase();
let dmi = dmi.to_ascii_lowercase();
let hypervisor = hypervisor.trim().to_ascii_lowercase();
let osrelease = osrelease.to_ascii_lowercase();
if systemd.starts_with("lxc")
|| pid1.lines().any(|line| line.trim() == "container=lxc")
|| cgroup.contains("lxc.payload")
|| cgroup.contains("/lxc/")
|| cgroup.contains("machine-lxc\\x2d")
|| lxcfs_proc_override(mountinfo)
{
return LinuxEnvironment::Lxc;
}
if dockerenv
|| systemd == "docker"
|| pid1.lines().any(|line| line.trim() == "container=docker")
|| cgroup.contains("/docker/")
|| cgroup.contains("docker-")
{
return LinuxEnvironment::Docker;
}
if containerenv
|| systemd == "podman"
|| pid1.lines().any(|line| line.trim() == "container=podman")
|| cgroup.contains("libpod")
{
return LinuxEnvironment::Podman;
}
if systemd == "systemd-nspawn" || cgroup.contains("machine.slice/machine-") {
return LinuxEnvironment::SystemdNspawn;
}
if osrelease.contains("microsoft") || osrelease.contains("wsl") {
return LinuxEnvironment::Wsl;
}
if !hypervisor.is_empty()
|| [
"kvm",
"qemu",
"vmware",
"virtualbox",
"virtual machine",
"xen",
"bochs",
"bhyve",
"parallels",
"amazon ec2",
"google compute engine",
"openstack",
"rhev",
"nutanix",
]
.iter()
.any(|marker| dmi.contains(marker))
{
return LinuxEnvironment::Vm;
}
LinuxEnvironment::Host
}
fn detect_environment() -> LinuxEnvironment {
let systemd_container = read_text("/run/systemd/container");
let pid1_environment = read_text("/proc/1/environ");
let cgroup = format!(
"{}\n{}",
read_text("/proc/1/cgroup"),
read_text("/proc/self/cgroup")
);
let mountinfo = read_text("/proc/self/mountinfo");
let dmi = [
"/sys/class/dmi/id/product_name",
"/sys/class/dmi/id/sys_vendor",
"/sys/class/dmi/id/board_vendor",
"/sys/class/dmi/id/bios_vendor",
]
.into_iter()
.map(read_text)
.collect::<Vec<_>>()
.join("\n");
let hypervisor = read_text("/sys/hypervisor/type");
let osrelease = read_text("/proc/sys/kernel/osrelease");
classify_environment(
&systemd_container,
&pid1_environment,
&cgroup,
&mountinfo,
&dmi,
&hypervisor,
&osrelease,
std::path::Path::new("/.dockerenv").exists(),
std::path::Path::new("/run/.containerenv").exists(),
)
}
fn parse_load_average(text: &str) -> Option<[f64; 3]> {
let mut values = text.split_whitespace().take(3).map(str::parse::<f64>);
let load = [
values.next()?.ok()?,
values.next()?.ok()?,
values.next()?.ok()?,
];
load.iter()
.all(|value| value.is_finite() && *value >= 0.0)
.then_some(load)
}
fn process_start_ticks(stat: &str) -> Option<u64> {
let end_of_name = stat.rfind(')')?;
stat.get(end_of_name + 1..)?
.split_whitespace()
.nth(19)?
.parse()
.ok()
}
fn boottime_seconds() -> Option<f64> {
let mut value = std::mem::MaybeUninit::<libc::timespec>::uninit();
let rc = unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, value.as_mut_ptr()) };
if rc != 0 {
return None;
}
let value = unsafe { value.assume_init() };
Some(value.tv_sec as f64 + value.tv_nsec as f64 / 1_000_000_000.0)
}
fn clock_ticks_per_second() -> Option<f64> {
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
(ticks > 0).then_some(ticks as f64)
}
fn lxc_uptime_seconds() -> Option<u64> {
let stat = fs::read_to_string("/proc/1/stat").ok()?;
let started = process_start_ticks(&stat)? as f64 / clock_ticks_per_second()?;
let age = boottime_seconds()? - started;
(age.is_finite() && age >= 0.0).then_some(age.floor() as u64)
}
fn lxc_load_average() -> Option<[f64; 3]> {
fs::read_to_string("/proc/loadavg")
.ok()
.and_then(|text| parse_load_average(&text))
}
pub(crate) fn augment(view: &mut View) {
let environment = detect_environment();
view.environment = Some(environment.label());
if let Ok(stat) = fs::read_to_string("/proc/stat") {
view.logical_cpus = Some(
stat.lines()
@@ -23,8 +221,16 @@ pub(crate) fn augment(view: &mut View) {
.filter(|count| *count > 0);
}
// Linux-specific augmentation preserves fields and units that existed before
// the cross-platform library adapter. Generic CPU/load/network/process
// metrics are deliberately collected by the shared API in mod.rs.
// the cross-platform library adapter. LXC additionally overrides uptime/load
// with namespace-visible data so the dashboard does not report the host values.
if environment == LinuxEnvironment::Lxc {
if let Some(uptime) = lxc_uptime_seconds() {
view.system_uptime_seconds = Some(uptime);
}
if let Some(load) = lxc_load_average() {
view.load_average = Some(load);
}
}
if let Ok(mem) = fs::read_to_string("/proc/meminfo") {
view.memory_total_bytes = key_value(&mem, "MemTotal").map(|n| n.saturating_mul(1024));
view.memory_used_bytes = view
+56
View File
@@ -12,4 +12,60 @@ mod tests {
);
assert_eq!(key_value("", "VmRSS"), None);
}
#[test]
fn parses_load_average_from_proc() {
assert_eq!(
parse_load_average("0.25 1.50 2.75 1/100 123\n"),
Some([0.25, 1.5, 2.75])
);
assert_eq!(parse_load_average("bad data"), None);
}
#[test]
fn parses_pid_one_start_time_with_spaces_in_name() {
let prefix = "1 (init process) S";
let middle = (4..22).map(|value| value.to_string()).collect::<Vec<_>>();
let stat = format!("{prefix} {} 4242 0 0", middle.join(" "));
assert_eq!(process_start_ticks(&stat), Some(4242));
}
#[test]
fn lxc_detection_has_priority_over_vm_metadata() {
assert_eq!(
classify_environment(
"lxc",
"container=lxc\n",
"0::/",
"",
"QEMU",
"",
"6.8.0",
false,
false,
),
LinuxEnvironment::Lxc
);
}
#[test]
fn detects_vm_and_plain_host_without_changing_metrics_policy() {
assert_eq!(
classify_environment("", "", "", "", "QEMU Standard PC", "", "", false, false),
LinuxEnvironment::Vm
);
assert_eq!(
classify_environment("", "", "", "", "Dell PowerEdge", "", "", false, false),
LinuxEnvironment::Host
);
}
#[test]
fn lxcfs_proc_mount_identifies_lxc_environment() {
let mountinfo = "100 99 0:55 / /proc/loadavg rw - fuse.lxcfs lxcfs rw\n";
assert_eq!(
classify_environment("", "", "", mountinfo, "", "", "", false, false),
LinuxEnvironment::Lxc
);
}
}
+1
View File
@@ -21,6 +21,7 @@ pub struct NetworkInterfaceView {
pub struct View {
pub source: &'static str,
pub platform: &'static str,
pub environment: Option<&'static str>,
pub sampled_at_ms: u64,
pub cpu_percent: Option<f64>,
pub process_cpu_percent: Option<f64>,