feat(detmir): add rust aw reliability planner
This commit is contained in:
Generated
+11
@@ -212,6 +212,17 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aw-ensure-reliability"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"clap",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aw-health-check"
|
name = "aw-health-check"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ members = [
|
|||||||
"crates/detmir-aw-client",
|
"crates/detmir-aw-client",
|
||||||
"crates/aw-db-health",
|
"crates/aw-db-health",
|
||||||
"crates/aw-db-maintenance",
|
"crates/aw-db-maintenance",
|
||||||
|
"crates/aw-ensure-reliability",
|
||||||
"crates/aw-health-check",
|
"crates/aw-health-check",
|
||||||
"crates/aw-prune-local-state",
|
"crates/aw-prune-local-state",
|
||||||
"crates/check-aw-data",
|
"crates/check-aw-data",
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ scripts with durable standalone Rust modules.
|
|||||||
- `dlp-health-check` - AW server DLP health check replacement.
|
- `dlp-health-check` - AW server DLP health check replacement.
|
||||||
- `aw-db-maintenance` - guarded weekly SQLite maintenance for old allowlisted
|
- `aw-db-maintenance` - guarded weekly SQLite maintenance for old allowlisted
|
||||||
process-level session events, with backup-before-delete.
|
process-level session events, with backup-before-delete.
|
||||||
|
- `aw-ensure-reliability` - safe dry-run/apply planner for AW service
|
||||||
|
reliability repair actions that were previously immediate Bash mutations.
|
||||||
- `check-aw-full` - read-only local AW/RDP full check replacement for the
|
- `check-aw-full` - read-only local AW/RDP full check replacement for the
|
||||||
legacy shell helper.
|
legacy shell helper.
|
||||||
- `dlp-aggregator` - AW server DLP warehouse aggregator replacement.
|
- `dlp-aggregator` - AW server DLP warehouse aggregator replacement.
|
||||||
|
|||||||
@@ -1470,6 +1470,37 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg
|
|||||||
(`3 passed`), `cargo clippy -p prod-rollout --all-targets -- -D
|
(`3 passed`), `cargo clippy -p prod-rollout --all-targets -- -D
|
||||||
warnings`, release build OK, `bash -n scripts/prod_rollout.sh`,
|
warnings`, release build OK, `bash -n scripts/prod_rollout.sh`,
|
||||||
artifact check OK.
|
artifact check OK.
|
||||||
|
53. `[done]` Перенести `aw-server/ensure-reliability.sh` в Rust-first
|
||||||
|
dry-run/apply helper:
|
||||||
|
- добавлен crate `aw-ensure-reliability`;
|
||||||
|
- `aw-server/ensure-reliability.sh` теперь Rust-first wrapper:
|
||||||
|
ищет `AW_ENSURE_RELIABILITY_RUST`,
|
||||||
|
`$CARGO_TARGET_DIR/release/aw-ensure-reliability`,
|
||||||
|
`adk-rust/target/release/aw-ensure-reliability`,
|
||||||
|
`/usr/local/bin/aw-ensure-reliability`;
|
||||||
|
- обычный запуск больше не делает `chown`, `systemctl stop/start`,
|
||||||
|
logrotate write или health timer write; он показывает dry-run plan;
|
||||||
|
- реальный Rust repair требует explicit `--apply`;
|
||||||
|
- старый Bash repair доступен только explicit `--apply-legacy`;
|
||||||
|
- Rust helper сохраняет legacy action set: env check,
|
||||||
|
ownership/mode repair for `/var/lib/activitywatch`,
|
||||||
|
`/var/log/activitywatch`, `/opt/activitywatch`, logrotate install,
|
||||||
|
health-check script/timer/service install, ordered AW service restart and
|
||||||
|
enable;
|
||||||
|
- `deploy_aw_server.yml` optional устанавливает
|
||||||
|
`/usr/local/bin/aw-ensure-reliability` до Influx token checks, если
|
||||||
|
release artifact доступен;
|
||||||
|
- production binary доставлен на AW server, но `--apply` не запускался;
|
||||||
|
- production dry-run: `apply=false`, `ok=true`, `missing=0`,
|
||||||
|
`executed=0`, `steps=25`;
|
||||||
|
- final production gates: AW failed units `0`, DetMir status OK with
|
||||||
|
`service_warnings=0`, `dlp_counts={ok:22,warn:0,fail:0}`,
|
||||||
|
`ok_for_operator=true`;
|
||||||
|
- gates: `cargo fmt --all -- --check`, `cargo test -p
|
||||||
|
aw-ensure-reliability` (`2 passed`), `cargo clippy -p
|
||||||
|
aw-ensure-reliability --all-targets -- -D warnings`, release build OK,
|
||||||
|
`bash -n aw-server/ensure-reliability.sh`, artifact check OK,
|
||||||
|
`ansible-playbook deploy_aw_server.yml --syntax-check` OK.
|
||||||
|
|
||||||
Отложить:
|
Отложить:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "aw-ensure-reliability"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
@@ -0,0 +1,547 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use clap::Parser;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
const LOGROTATE_CONTENT: &str = include_str!("../../../../aw-server/logrotate.conf");
|
||||||
|
const HEALTH_TIMER_CONTENT: &str = r#"[Unit]
|
||||||
|
Description=AW Health Check Timer
|
||||||
|
Requires=aw-health-check.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*:0/5:00
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
"#;
|
||||||
|
const HEALTH_SERVICE_CONTENT: &str = r#"[Unit]
|
||||||
|
Description=AW Health Check
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/aw-health-check
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(about = "Plan or apply AW service reliability hardening")]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(long, default_value = "/etc/activitywatch/aw-server.env")]
|
||||||
|
env_file: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "/var/lib/activitywatch")]
|
||||||
|
data_dir: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "/var/log/activitywatch")]
|
||||||
|
log_dir: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "/opt/activitywatch")]
|
||||||
|
opt_dir: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "/etc/logrotate.d/activitywatch")]
|
||||||
|
logrotate_target: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "/usr/local/bin/aw-health-check")]
|
||||||
|
health_script_target: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "/etc/systemd/system/aw-health-check.timer")]
|
||||||
|
health_timer_target: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "/etc/systemd/system/aw-health-check.service")]
|
||||||
|
health_service_target: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "/opt/activitywatch/aw-rus-ops/health-check.sh")]
|
||||||
|
health_script_source: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
apply: bool,
|
||||||
|
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum StepKind {
|
||||||
|
Check,
|
||||||
|
Chown,
|
||||||
|
Chmod,
|
||||||
|
Copy,
|
||||||
|
Write,
|
||||||
|
Systemd,
|
||||||
|
Sleep,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct Step {
|
||||||
|
order: usize,
|
||||||
|
name: String,
|
||||||
|
kind: StepKind,
|
||||||
|
command: String,
|
||||||
|
mutation: bool,
|
||||||
|
needed: bool,
|
||||||
|
reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct Report {
|
||||||
|
apply: bool,
|
||||||
|
ok: bool,
|
||||||
|
env_file: PathBuf,
|
||||||
|
missing_required: Vec<String>,
|
||||||
|
steps: Vec<Step>,
|
||||||
|
executed: Vec<ExecResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ExecResult {
|
||||||
|
order: usize,
|
||||||
|
name: String,
|
||||||
|
ok: bool,
|
||||||
|
exit_code: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let code = match run() {
|
||||||
|
Ok(code) => code,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{err:#}");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::process::exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<i32> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let mut report = build_report(&cli);
|
||||||
|
if cli.apply {
|
||||||
|
if !report.missing_required.is_empty() {
|
||||||
|
report.ok = false;
|
||||||
|
print_report(&report, cli.json)?;
|
||||||
|
bail!("refusing --apply because required inputs are missing");
|
||||||
|
}
|
||||||
|
apply_steps(&mut report)?;
|
||||||
|
}
|
||||||
|
print_report(&report, cli.json)?;
|
||||||
|
Ok(if report.ok { 0 } else { 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_report(cli: &Cli) -> Report {
|
||||||
|
let mut missing_required = Vec::new();
|
||||||
|
if !cli.env_file.is_file() {
|
||||||
|
missing_required.push(format!("env file missing: {}", cli.env_file.display()));
|
||||||
|
}
|
||||||
|
if !cli.health_script_target.is_file() && !cli.health_script_source.is_file() {
|
||||||
|
missing_required.push(format!(
|
||||||
|
"health script target and source missing: {}, {}",
|
||||||
|
cli.health_script_target.display(),
|
||||||
|
cli.health_script_source.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut steps = Vec::new();
|
||||||
|
push_step(
|
||||||
|
&mut steps,
|
||||||
|
"check-env-file",
|
||||||
|
StepKind::Check,
|
||||||
|
format!("test -f {}", shell_quote(&cli.env_file)),
|
||||||
|
false,
|
||||||
|
!cli.env_file.is_file(),
|
||||||
|
"required before reliability actions".to_string(),
|
||||||
|
);
|
||||||
|
for dir in [&cli.data_dir, &cli.log_dir, &cli.opt_dir] {
|
||||||
|
push_step(
|
||||||
|
&mut steps,
|
||||||
|
format!("chown-{}", dir.display()),
|
||||||
|
StepKind::Chown,
|
||||||
|
format!("chown -R activitywatch:activitywatch {}", shell_quote(dir)),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"preserve legacy ownership repair".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for dir in [&cli.data_dir, &cli.log_dir, &cli.opt_dir] {
|
||||||
|
push_step(
|
||||||
|
&mut steps,
|
||||||
|
format!("chmod-{}", dir.display()),
|
||||||
|
StepKind::Chmod,
|
||||||
|
format!("chmod 755 {}", shell_quote(dir)),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"preserve legacy directory mode repair".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
push_step(
|
||||||
|
&mut steps,
|
||||||
|
"install-logrotate",
|
||||||
|
StepKind::Write,
|
||||||
|
format!("write {}", shell_quote(&cli.logrotate_target)),
|
||||||
|
true,
|
||||||
|
!cli.logrotate_target.is_file(),
|
||||||
|
if cli.logrotate_target.is_file() {
|
||||||
|
"logrotate already configured".to_string()
|
||||||
|
} else {
|
||||||
|
"logrotate target missing".to_string()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
push_step(
|
||||||
|
&mut steps,
|
||||||
|
"install-health-script",
|
||||||
|
StepKind::Copy,
|
||||||
|
format!(
|
||||||
|
"copy {} {}",
|
||||||
|
shell_quote(&cli.health_script_source),
|
||||||
|
shell_quote(&cli.health_script_target)
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
!cli.health_script_target.is_file(),
|
||||||
|
if cli.health_script_target.is_file() {
|
||||||
|
"health script already installed".to_string()
|
||||||
|
} else {
|
||||||
|
"health script target missing".to_string()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for (name, path, content_name) in [
|
||||||
|
(
|
||||||
|
"install-health-timer",
|
||||||
|
&cli.health_timer_target,
|
||||||
|
"aw-health-check.timer",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"install-health-service",
|
||||||
|
&cli.health_service_target,
|
||||||
|
"aw-health-check.service",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
push_step(
|
||||||
|
&mut steps,
|
||||||
|
name,
|
||||||
|
StepKind::Write,
|
||||||
|
format!("write {} ({content_name})", shell_quote(path)),
|
||||||
|
true,
|
||||||
|
!path.is_file(),
|
||||||
|
if path.is_file() {
|
||||||
|
format!("{content_name} already installed")
|
||||||
|
} else {
|
||||||
|
format!("{content_name} target missing")
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (name, command) in [
|
||||||
|
("daemon-reload-before-restart", "systemctl daemon-reload"),
|
||||||
|
(
|
||||||
|
"stop-services",
|
||||||
|
"systemctl stop aw-worktime-api aw-worktime-ui-bridge activitywatch-server || true",
|
||||||
|
),
|
||||||
|
("sleep-after-stop", "sleep 2"),
|
||||||
|
(
|
||||||
|
"start-activitywatch-server",
|
||||||
|
"systemctl start activitywatch-server",
|
||||||
|
),
|
||||||
|
("sleep-after-server-start", "sleep 3"),
|
||||||
|
("start-worktime-api", "systemctl start aw-worktime-api"),
|
||||||
|
("sleep-after-api-start", "sleep 2"),
|
||||||
|
(
|
||||||
|
"start-worktime-ui-bridge",
|
||||||
|
"systemctl start aw-worktime-ui-bridge",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"enable-activitywatch-server",
|
||||||
|
"systemctl enable activitywatch-server",
|
||||||
|
),
|
||||||
|
("enable-worktime-api", "systemctl enable aw-worktime-api"),
|
||||||
|
(
|
||||||
|
"enable-worktime-ui-bridge",
|
||||||
|
"systemctl enable aw-worktime-ui-bridge",
|
||||||
|
),
|
||||||
|
("daemon-reload-health", "systemctl daemon-reload"),
|
||||||
|
(
|
||||||
|
"enable-health-timer",
|
||||||
|
"systemctl enable aw-health-check.timer",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"start-health-timer",
|
||||||
|
"systemctl start aw-health-check.timer",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let kind = if command.starts_with("sleep") {
|
||||||
|
StepKind::Sleep
|
||||||
|
} else {
|
||||||
|
StepKind::Systemd
|
||||||
|
};
|
||||||
|
push_step(
|
||||||
|
&mut steps,
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
command.to_string(),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"preserve legacy reliability action".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Report {
|
||||||
|
apply: cli.apply,
|
||||||
|
ok: missing_required.is_empty(),
|
||||||
|
env_file: cli.env_file.clone(),
|
||||||
|
missing_required,
|
||||||
|
steps,
|
||||||
|
executed: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_step(
|
||||||
|
steps: &mut Vec<Step>,
|
||||||
|
name: impl Into<String>,
|
||||||
|
kind: StepKind,
|
||||||
|
command: String,
|
||||||
|
mutation: bool,
|
||||||
|
needed: bool,
|
||||||
|
reason: String,
|
||||||
|
) {
|
||||||
|
steps.push(Step {
|
||||||
|
order: steps.len() + 1,
|
||||||
|
name: name.into(),
|
||||||
|
kind,
|
||||||
|
command,
|
||||||
|
mutation,
|
||||||
|
needed,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_steps(report: &mut Report) -> Result<()> {
|
||||||
|
let steps = report.steps.clone();
|
||||||
|
for step in steps.iter().filter(|step| step.needed) {
|
||||||
|
let result = match step.name.as_str() {
|
||||||
|
"check-env-file" => ExecResult {
|
||||||
|
order: step.order,
|
||||||
|
name: step.name.clone(),
|
||||||
|
ok: Path::new(&report.env_file).is_file(),
|
||||||
|
exit_code: Some(if Path::new(&report.env_file).is_file() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"install-logrotate" => write_file_result(step, report, LOGROTATE_CONTENT, 0o644)?,
|
||||||
|
"install-health-script" => copy_health_script(step, report)?,
|
||||||
|
"install-health-timer" => write_file_result(step, report, HEALTH_TIMER_CONTENT, 0o644)?,
|
||||||
|
"install-health-service" => {
|
||||||
|
write_file_result(step, report, HEALTH_SERVICE_CONTENT, 0o644)?
|
||||||
|
}
|
||||||
|
_ => run_shell_step(step)?,
|
||||||
|
};
|
||||||
|
let ok = result.ok;
|
||||||
|
report.executed.push(result);
|
||||||
|
if !ok {
|
||||||
|
report.ok = false;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.ok = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_file_result(step: &Step, report: &Report, content: &str, mode: u32) -> Result<ExecResult> {
|
||||||
|
let target = match step.name.as_str() {
|
||||||
|
"install-logrotate" => target_from_command(&step.command)?,
|
||||||
|
"install-health-timer" => target_from_command(&step.command)?,
|
||||||
|
"install-health-service" => target_from_command(&step.command)?,
|
||||||
|
_ => bail!("unsupported write step {}", step.name),
|
||||||
|
};
|
||||||
|
let _ = report;
|
||||||
|
if let Some(parent) = target.parent() {
|
||||||
|
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::write(&target, content).with_context(|| format!("write {}", target.display()))?;
|
||||||
|
set_mode(&target, mode).with_context(|| format!("chmod {:o} {}", mode, target.display()))?;
|
||||||
|
Ok(ExecResult {
|
||||||
|
order: step.order,
|
||||||
|
name: step.name.clone(),
|
||||||
|
ok: true,
|
||||||
|
exit_code: Some(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_health_script(step: &Step, report: &Report) -> Result<ExecResult> {
|
||||||
|
let source = source_from_copy_command(&step.command)?;
|
||||||
|
let target = copy_target_from_command(&step.command)?;
|
||||||
|
let _ = report;
|
||||||
|
if let Some(parent) = target.parent() {
|
||||||
|
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::copy(&source, &target)
|
||||||
|
.with_context(|| format!("copy {} -> {}", source.display(), target.display()))?;
|
||||||
|
set_mode(&target, 0o755).with_context(|| format!("chmod 0755 {}", target.display()))?;
|
||||||
|
Ok(ExecResult {
|
||||||
|
order: step.order,
|
||||||
|
name: step.name.clone(),
|
||||||
|
ok: true,
|
||||||
|
exit_code: Some(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_shell_step(step: &Step) -> Result<ExecResult> {
|
||||||
|
let status = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&step.command)
|
||||||
|
.status()
|
||||||
|
.with_context(|| format!("run {}", step.command))?;
|
||||||
|
Ok(ExecResult {
|
||||||
|
order: step.order,
|
||||||
|
name: step.name.clone(),
|
||||||
|
ok: status.success(),
|
||||||
|
exit_code: status.code(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn target_from_command(command: &str) -> Result<PathBuf> {
|
||||||
|
let raw = command
|
||||||
|
.split_whitespace()
|
||||||
|
.nth(1)
|
||||||
|
.or_else(|| command.split_whitespace().nth(2))
|
||||||
|
.context("parse target from command")?;
|
||||||
|
Ok(PathBuf::from(raw.trim_matches('\'')))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_from_copy_command(command: &str) -> Result<PathBuf> {
|
||||||
|
let raw = command
|
||||||
|
.split_whitespace()
|
||||||
|
.nth(1)
|
||||||
|
.context("parse source from copy command")?;
|
||||||
|
Ok(PathBuf::from(raw.trim_matches('\'')))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_target_from_command(command: &str) -> Result<PathBuf> {
|
||||||
|
let raw = command
|
||||||
|
.split_whitespace()
|
||||||
|
.nth(2)
|
||||||
|
.context("parse target from copy command")?;
|
||||||
|
Ok(PathBuf::from(raw.trim_matches('\'')))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn set_mode(path: &Path, mode: u32) -> Result<()> {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(path)?.permissions();
|
||||||
|
perms.set_mode(mode);
|
||||||
|
fs::set_permissions(path, perms)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn set_mode(_path: &Path, _mode: u32) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_report(report: &Report, json: bool) -> Result<()> {
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(report)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"aw-ensure-reliability: {}",
|
||||||
|
if report.apply { "apply" } else { "dry-run" }
|
||||||
|
);
|
||||||
|
println!("env_file: {}", report.env_file.display());
|
||||||
|
println!("ok: {}", report.ok);
|
||||||
|
if !report.missing_required.is_empty() {
|
||||||
|
println!("missing_required:");
|
||||||
|
for item in &report.missing_required {
|
||||||
|
println!(" - {item}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("planned steps:");
|
||||||
|
for step in &report.steps {
|
||||||
|
let risk = if step.mutation { "MUTATION" } else { "check" };
|
||||||
|
let needed = if step.needed { "needed" } else { "skip" };
|
||||||
|
println!(
|
||||||
|
" {:02}. {:<28} {:<8} {:<6} {}",
|
||||||
|
step.order, step.name, risk, needed, step.command
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if report.executed.is_empty() {
|
||||||
|
println!("No mutation executed. Use --apply for explicit reliability fix.");
|
||||||
|
} else {
|
||||||
|
println!("executed:");
|
||||||
|
for item in &report.executed {
|
||||||
|
println!(
|
||||||
|
" {:02}. {:<28} ok={} exit={:?}",
|
||||||
|
item.order, item.name, item.ok, item.exit_code
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shell_quote(path: &Path) -> String {
|
||||||
|
format!("'{}'", path.display().to_string().replace('\'', "'\\''"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dry_run_marks_legacy_mutations() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let env_file = dir.path().join("aw-server.env");
|
||||||
|
let health = dir.path().join("health-check.sh");
|
||||||
|
fs::write(&env_file, "AW_BASE_URL=http://127.0.0.1:5600\n").unwrap();
|
||||||
|
fs::write(&health, "#!/bin/sh\nexit 0\n").unwrap();
|
||||||
|
let cli = Cli {
|
||||||
|
env_file,
|
||||||
|
data_dir: dir.path().join("data"),
|
||||||
|
log_dir: dir.path().join("log"),
|
||||||
|
opt_dir: dir.path().join("opt"),
|
||||||
|
logrotate_target: dir.path().join("logrotate/activitywatch"),
|
||||||
|
health_script_target: dir.path().join("bin/aw-health-check"),
|
||||||
|
health_timer_target: dir.path().join("systemd/aw-health-check.timer"),
|
||||||
|
health_service_target: dir.path().join("systemd/aw-health-check.service"),
|
||||||
|
health_script_source: health,
|
||||||
|
apply: false,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
let report = build_report(&cli);
|
||||||
|
assert!(report.ok);
|
||||||
|
assert!(report.steps.iter().any(|step| step.name == "stop-services"));
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.any(|step| step.name == "install-logrotate")
|
||||||
|
);
|
||||||
|
assert!(report.steps.iter().any(|step| step.mutation));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_env_blocks_apply() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let health = dir.path().join("health-check.sh");
|
||||||
|
fs::write(&health, "#!/bin/sh\nexit 0\n").unwrap();
|
||||||
|
let cli = Cli {
|
||||||
|
env_file: dir.path().join("missing.env"),
|
||||||
|
data_dir: dir.path().join("data"),
|
||||||
|
log_dir: dir.path().join("log"),
|
||||||
|
opt_dir: dir.path().join("opt"),
|
||||||
|
logrotate_target: dir.path().join("logrotate/activitywatch"),
|
||||||
|
health_script_target: dir.path().join("bin/aw-health-check"),
|
||||||
|
health_timer_target: dir.path().join("systemd/aw-health-check.timer"),
|
||||||
|
health_service_target: dir.path().join("systemd/aw-health-check.service"),
|
||||||
|
health_script_source: health,
|
||||||
|
apply: true,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
let report = build_report(&cli);
|
||||||
|
assert!(!report.ok);
|
||||||
|
assert_eq!(report.missing_required.len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -471,6 +471,22 @@
|
|||||||
mode: "0755"
|
mode: "0755"
|
||||||
when: aw_db_health_rust_binary_early.stat.exists | default(false)
|
when: aw_db_health_rust_binary_early.stat.exists | default(false)
|
||||||
|
|
||||||
|
- name: Проверить локальный Rust aw-ensure-reliability до Influx проверок
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ aw_rust_release_dir }}/aw-ensure-reliability"
|
||||||
|
delegate_to: localhost
|
||||||
|
register: aw_ensure_reliability_rust_binary_early
|
||||||
|
become: false
|
||||||
|
|
||||||
|
- name: Установить Rust aw-ensure-reliability до Influx проверок
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ aw_rust_release_dir }}/aw-ensure-reliability"
|
||||||
|
dest: /usr/local/bin/aw-ensure-reliability
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
when: aw_ensure_reliability_rust_binary_early.stat.exists | default(false)
|
||||||
|
|
||||||
- name: Проверить локальный Rust aw-db-maintenance до Influx проверок
|
- name: Проверить локальный Rust aw-db-maintenance до Influx проверок
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "{{ aw_rust_release_dir }}/aw-db-maintenance"
|
path: "{{ aw_rust_release_dir }}/aw-db-maintenance"
|
||||||
|
|||||||
@@ -5,8 +5,39 @@ set -euo pipefail
|
|||||||
# Fixes common issues and ensures proper configuration
|
# Fixes common issues and ensures proper configuration
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
ENV_FILE="/etc/activitywatch/aw-server.env"
|
ENV_FILE="/etc/activitywatch/aw-server.env"
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--apply-legacy" ]]; then
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
TARGET_ROOT="${CARGO_TARGET_DIR:-$ROOT_DIR/adk-rust/target}"
|
||||||
|
for candidate in \
|
||||||
|
"${AW_ENSURE_RELIABILITY_RUST:-}" \
|
||||||
|
"$TARGET_ROOT/release/aw-ensure-reliability" \
|
||||||
|
"$ROOT_DIR/adk-rust/target/release/aw-ensure-reliability" \
|
||||||
|
"/usr/local/bin/aw-ensure-reliability"; do
|
||||||
|
if [[ -n "$candidate" && -x "$candidate" ]]; then
|
||||||
|
exec "$candidate" --health-script-source "$SCRIPT_DIR/health-check.sh" "$@"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
ensure-reliability.sh now requires the Rust planner for safe default runs.
|
||||||
|
Build it first:
|
||||||
|
cd adk-rust && cargo build --release -p aw-ensure-reliability
|
||||||
|
|
||||||
|
Safe dry-run:
|
||||||
|
aw-server/ensure-reliability.sh --json
|
||||||
|
|
||||||
|
Explicit Rust apply:
|
||||||
|
aw-server/ensure-reliability.sh --apply
|
||||||
|
|
||||||
|
Old Bash apply:
|
||||||
|
aw-server/ensure-reliability.sh --apply-legacy
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||||
}
|
}
|
||||||
@@ -143,4 +174,4 @@ main() {
|
|||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
main "$@"
|
main "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ required_bins=(
|
|||||||
aw-rus-healthd
|
aw-rus-healthd
|
||||||
aw-db-health
|
aw-db-health
|
||||||
aw-db-maintenance
|
aw-db-maintenance
|
||||||
|
aw-ensure-reliability
|
||||||
aw-health-check
|
aw-health-check
|
||||||
aw-prune-local-state
|
aw-prune-local-state
|
||||||
check-aw-data
|
check-aw-data
|
||||||
|
|||||||
Reference in New Issue
Block a user