Документация и runtime: полный production-fix по fix6 без legacy

This commit is contained in:
2026-05-10 22:58:59 +05:00
parent 83c4e3ba05
commit 819cc9c308
12 changed files with 106 additions and 267 deletions
+14 -3
View File
@@ -130,13 +130,18 @@ impl Config {
fn check_host_tools(&self) -> anyhow::Result<()> {
let mut required = vec!["tc", "ip", "wg", "docker", "modprobe"];
if !crate::netns::nsenter_available() {
eprintln!("nsenter is not available, will use docker exec fallback");
} else {
required.push("nsenter");
anyhow::bail!("required host tool not found in PATH: nsenter");
}
required.push("nsenter");
if self.anti_torrent.enabled {
required.push("nft");
}
if self.reboot.enabled {
required.push("systemctl");
}
if self.swap.enabled {
required.extend(["fallocate", "chmod", "mkswap", "swapon", "sysctl"]);
}
for tool in required {
if !tool_exists(tool) {
@@ -199,6 +204,12 @@ fn tool_exists(tool: &str) -> bool {
"modprobe" => &[&["--version"]],
"nsenter" => &[&["--help"]],
"nft" => &[&["--version"]],
"systemctl" => &[&["--version"], &["--help"]],
"fallocate" => &[&["--help"]],
"chmod" => &[&["--help"]],
"mkswap" => &[&["--help"]],
"swapon" => &[&["--help"]],
"sysctl" => &[&["--version"], &["--help"]],
_ => &[&["--version"], &["--help"], &["-V"]],
};
-116
View File
@@ -1,116 +0,0 @@
use chrono::{Duration, NaiveDateTime, NaiveTime, Timelike, Utc};
use rand::Rng;
use std::{process::Command, thread, time::Duration as StdDuration};
use crate::config::Config;
pub fn maybe_schedule_reboot(cfg: &Config) -> anyhow::Result<()> {
if !cfg.reboot.enabled {
return Ok(());
}
let planned = next_reboot_time(&cfg.reboot.window_start_utc, &cfg.reboot.window_end_utc)?;
println!("reboot planned at UTC {}", planned);
Ok(())
}
pub fn reboot_scheduler_loop(cfg: Config) -> anyhow::Result<()> {
if !cfg.reboot.enabled {
return Ok(());
}
loop {
let planned = next_reboot_time(&cfg.reboot.window_start_utc, &cfg.reboot.window_end_utc)?;
println!("reboot scheduler: next reboot at UTC {}", planned);
loop {
let now = Utc::now().naive_utc();
if now >= planned {
break;
}
thread::sleep(StdDuration::from_secs(30));
}
let out = Command::new("systemctl")
.args(["--no-block", "reboot"])
.output()?;
if out.status.success() {
println!("reboot request accepted by systemctl");
return Ok(());
}
eprintln!(
"reboot request failed: {}",
String::from_utf8_lossy(&out.stderr)
);
thread::sleep(StdDuration::from_secs(60));
}
}
fn next_reboot_time(start_hhmm: &str, end_hhmm: &str) -> anyhow::Result<NaiveDateTime> {
let start = parse_hhmm(start_hhmm)?;
let end = parse_hhmm(end_hhmm)?;
let now = Utc::now().naive_utc();
let today = now.date();
let mut base_day = today;
let now_t = NaiveTime::from_hms_opt(now.hour(), now.minute(), now.second())
.ok_or_else(|| anyhow::anyhow!("bad current time"))?;
if now_t >= start {
base_day = today
.succ_opt()
.ok_or_else(|| anyhow::anyhow!("date overflow"))?;
}
let span = if end > start {
end.signed_duration_since(start)
} else {
Duration::hours(24) - start.signed_duration_since(end)
};
let secs = rand::thread_rng().gen_range(0..span.num_seconds().max(1));
let base_dt = base_day
.and_hms_opt(start.hour(), start.minute(), 0)
.ok_or_else(|| anyhow::anyhow!("invalid base datetime"))?;
Ok(base_dt + Duration::seconds(secs))
}
fn parse_hhmm(v: &str) -> anyhow::Result<NaiveTime> {
let mut s = v.split(':');
let h = s
.next()
.ok_or_else(|| anyhow::anyhow!("bad time"))?
.parse()?;
let m = s
.next()
.ok_or_else(|| anyhow::anyhow!("bad time"))?
.parse()?;
if h > 23 || m > 59 {
anyhow::bail!("bad time")
}
NaiveTime::from_hms_opt(h, m, 0).ok_or_else(|| anyhow::anyhow!("bad time"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_time_rejects_invalid() {
assert!(parse_hhmm("24:00").is_err());
assert!(parse_hhmm("10:60").is_err());
assert!(parse_hhmm("ab:cd").is_err());
}
#[test]
fn cross_midnight_window_has_expected_span() {
let start = parse_hhmm("23:00").expect("start");
let end = parse_hhmm("02:00").expect("end");
let span = if end > start {
end.signed_duration_since(start)
} else {
Duration::hours(24) - start.signed_duration_since(end)
};
assert_eq!(span.num_hours(), 3);
}
}
+13 -3
View File
@@ -183,6 +183,13 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
}
}
println!("Swap:");
println!(" enabled: {}", cfg.swap.enabled);
println!(" path: {}", cfg.swap.path);
println!(" size_mb: {}", cfg.swap.size_mb);
println!(" priority: {}", cfg.swap.priority);
println!(" swappiness: {}", cfg.swap.swappiness);
let ct_count = Command::new("sysctl")
.args(["-n", "net.netfilter.nf_conntrack_count"])
.output()
@@ -220,7 +227,7 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
}
}
print_reboot_section();
print_reboot_section(cfg);
Ok(())
}
@@ -301,12 +308,15 @@ fn print_degraded_sections(cfg: &Config) -> anyhow::Result<()> {
}
}
print_reboot_section();
print_reboot_section(cfg);
Ok(())
}
fn print_reboot_section() {
fn print_reboot_section(cfg: &Config) {
println!("Reboot scheduler:");
println!(" enabled: {}", cfg.reboot.enabled);
println!(" window_start_utc: {}", cfg.reboot.window_start_utc);
println!(" window_end_utc: {}", cfg.reboot.window_end_utc);
let svc = Command::new("systemctl")
.args(["is-active", "awg2-reboot-scheduler"])
.output();
+33 -12
View File
@@ -180,7 +180,7 @@ pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficS
let actual_map_ingress = inspect_ingress.map;
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
let expected_map = expected_peer_class_map(&peers, cfg.traffic.class_start_id);
let expected_map = expected_peer_class_map(&peers, cfg.traffic.class_start_id)?;
let expected_limit = normalize_tc_rate(&cfg.traffic.peer_limit);
let mut out = Vec::with_capacity(peers.len());
@@ -384,7 +384,7 @@ fn inspect_and_diff(
});
}
let expected_map = expected_peer_class_map(peers, class_start_id);
let expected_map = expected_peer_class_map(peers, class_start_id)?;
if actual_map_egress != expected_map {
return Ok(TrafficDrift {
has_drift: true,
@@ -657,12 +657,20 @@ fn extract_flowid_class_id(line: &str) -> Option<u16> {
class_tail.parse::<u16>().ok()
}
fn expected_peer_class_map(peers: &[wg::Peer], class_start_id: u16) -> BTreeMap<String, u16> {
fn expected_peer_class_map(
peers: &[wg::Peer],
class_start_id: u16,
) -> anyhow::Result<BTreeMap<String, u16>> {
let mut map = BTreeMap::new();
for (idx, p) in peers.iter().enumerate() {
map.insert(p.ipv4.to_string(), class_start_id + idx as u16);
let idx_u16 = u16::try_from(idx)
.map_err(|_| anyhow::anyhow!("too many peers for class id allocation"))?;
let class_id = class_start_id
.checked_add(idx_u16)
.ok_or_else(|| anyhow::anyhow!("class id overflow: too many peers"))?;
map.insert(p.ipv4.to_string(), class_id);
}
map
Ok(map)
}
fn extract_u32_match_hex(line: &str) -> Option<String> {
@@ -773,7 +781,13 @@ fn apply_full_topology(
)?;
for (idx, p) in peers.iter().enumerate() {
let class_id = cfg.traffic.class_start_id + idx as u16;
let idx_u16 = u16::try_from(idx)
.map_err(|_| anyhow::anyhow!("too many peers for class id allocation"))?;
let class_id = cfg
.traffic
.class_start_id
.checked_add(idx_u16)
.ok_or_else(|| anyhow::anyhow!("class id overflow: too many peers"))?;
let ip = p.ipv4.to_string();
let class_egress = format!("1:{class_id}");
let class_ingress = format!("2:{class_id}");
@@ -912,8 +926,12 @@ fn run_allow_exists(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Resul
match transport.run(args) {
Ok(_) => Ok(()),
Err(e) => {
let msg = e.to_string();
if msg.contains("File exists") || msg.contains("EEXIST") {
let msg = e.to_string().to_ascii_lowercase();
if msg.contains("file exists")
|| msg.contains("eexist")
|| msg.contains("object already exists")
|| msg.contains("exclusive flag on")
{
return Ok(());
}
Err(e)
@@ -925,11 +943,14 @@ fn run_allow_notfound(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Res
match transport.run(args) {
Ok(_) => Ok(()),
Err(e) => {
let msg = e.to_string();
if msg.contains("No such file")
|| msg.contains("No such")
let msg = e.to_string().to_ascii_lowercase();
if msg.contains("no such file")
|| msg.contains("no such")
|| msg.contains("not found")
|| msg.contains("Cannot find")
|| msg.contains("cannot find")
|| msg.contains("cannot find device")
|| msg.contains("no such device")
|| msg.contains("does not exist")
{
return Ok(());
}