From 63774cb8c4c6fcedbca903109c5c62c6bed1851b Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Tue, 2 Jun 2026 18:23:44 +0300 Subject: [PATCH] feat(detmir): add rust linux install planners --- adk-rust/Cargo.lock | 11 + adk-rust/Cargo.toml | 1 + adk-rust/README.md | 2 + adk-rust/RUNBOOK.md | 22 ++ adk-rust/crates/aw-linux-install/Cargo.toml | 16 + adk-rust/crates/aw-linux-install/src/main.rs | 290 ++++++++++++++++++ .../check_detmir_rust_release_artifacts.sh | 1 + scripts/install_aw_console_ssh_logger.sh | 33 ++ scripts/install_aw_linux_client.sh | 33 ++ scripts/install_aw_linux_remote_worker.sh | 38 ++- .../install_aw_linux_web_category_logger.sh | 33 ++ scripts/install_aw_pve_webadmin_logger.sh | 33 ++ 12 files changed, 511 insertions(+), 2 deletions(-) create mode 100644 adk-rust/crates/aw-linux-install/Cargo.toml create mode 100644 adk-rust/crates/aw-linux-install/src/main.rs diff --git a/adk-rust/Cargo.lock b/adk-rust/Cargo.lock index 6e129da..b91ab22 100644 --- a/adk-rust/Cargo.lock +++ b/adk-rust/Cargo.lock @@ -233,6 +233,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "aw-linux-install" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "aw-prune-local-state" version = "0.1.0" diff --git a/adk-rust/Cargo.toml b/adk-rust/Cargo.toml index d933a4a..f861ee9 100644 --- a/adk-rust/Cargo.toml +++ b/adk-rust/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/aw-db-maintenance", "crates/aw-ensure-reliability", "crates/aw-health-check", + "crates/aw-linux-install", "crates/aw-prune-local-state", "crates/check-aw-data", "crates/check-aw-full", diff --git a/adk-rust/README.md b/adk-rust/README.md index dfa902a..4bfdadb 100644 --- a/adk-rust/README.md +++ b/adk-rust/README.md @@ -25,6 +25,8 @@ scripts with durable standalone Rust modules. 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. +- `aw-linux-install` - safe dry-run/apply planner for Linux ActivityWatch + install wrappers; legacy shell install is explicit. - `check-aw-full` - read-only local AW/RDP full check replacement for the legacy shell helper. - `dlp-aggregator` - AW server DLP warehouse aggregator replacement. diff --git a/adk-rust/RUNBOOK.md b/adk-rust/RUNBOOK.md index 40130ec..e783205 100644 --- a/adk-rust/RUNBOOK.md +++ b/adk-rust/RUNBOOK.md @@ -1501,6 +1501,28 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg 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. +54. `[done]` Перевести Linux install scripts в Rust-first safe planner слой: + - добавлен crate `aw-linux-install`; + - wrappers теперь Rust-first и dry-run по умолчанию: + `scripts/install_aw_linux_client.sh`, + `scripts/install_aw_linux_remote_worker.sh`, + `scripts/install_aw_linux_web_category_logger.sh`, + `scripts/install_aw_console_ssh_logger.sh`, + `scripts/install_aw_pve_webadmin_logger.sh`; + - real install требует explicit `--apply`; + - old shell install требует explicit `--apply-legacy`; + - `aw-linux-install --apply` запускает соответствующий legacy script с + `--apply-legacy`, сохраняя embedded Python collectors и shell install + contract; + - `remote_worker` legacy path исправлен: вложенные client/console/web + installers вызываются с `--apply-legacy`, чтобы real legacy install не + превратился в dry-run после оборачивания; + - PVE webadmin logger не запускался и Proxmox platform не менялся; это + только safe planner wrapper; + - gates: `cargo fmt --all -- --check`, `cargo test -p aw-linux-install` + (`2 passed`), `cargo clippy -p aw-linux-install --all-targets -- -D + warnings`, release build OK, `sh -n` для всех пяти wrappers OK, + dry-run JSON для всех пяти wrappers OK, artifact check OK. Отложить: diff --git a/adk-rust/crates/aw-linux-install/Cargo.toml b/adk-rust/crates/aw-linux-install/Cargo.toml new file mode 100644 index 0000000..e967b47 --- /dev/null +++ b/adk-rust/crates/aw-linux-install/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "aw-linux-install" +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 diff --git a/adk-rust/crates/aw-linux-install/src/main.rs b/adk-rust/crates/aw-linux-install/src/main.rs new file mode 100644 index 0000000..d225c69 --- /dev/null +++ b/adk-rust/crates/aw-linux-install/src/main.rs @@ -0,0 +1,290 @@ +use std::path::PathBuf; +use std::process::Command; + +use anyhow::{Context, Result, bail}; +use clap::{Parser, ValueEnum}; +use serde::Serialize; + +#[derive(Debug, Clone, Copy, ValueEnum, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +enum InstallKind { + Client, + RemoteWorker, + ConsoleSsh, + WebCategory, + PveWebadmin, +} + +#[derive(Debug, Parser)] +#[command(about = "Safe planner/apply wrapper for AW Linux install scripts")] +struct Cli { + #[arg(long, value_enum)] + kind: InstallKind, + + #[arg(long)] + legacy_script: PathBuf, + + #[arg(long, default_value = "10.10.10.13")] + server_host: String, + + #[arg(long, default_value = "5600")] + server_port: String, + + #[arg(long, default_value = "5")] + poll_interval: String, + + #[arg(long, default_value = "0.13.2")] + version: String, + + #[arg(long)] + install_base: Option, + + #[arg(long, default_value_t = false)] + force: bool, + + #[arg(long, default_value_t = false)] + apply: bool, + + #[arg(long, default_value_t = false)] + json: bool, +} + +#[derive(Debug, Serialize)] +struct Plan { + apply: bool, + kind: InstallKind, + legacy_script: PathBuf, + required_files: Vec, + steps: Vec, + missing_count: usize, +} + +#[derive(Debug, Serialize)] +struct Requirement { + name: String, + ok: bool, + detail: String, +} + +#[derive(Debug, Serialize)] +struct Step { + order: usize, + name: String, + mutation: bool, + command: Vec, + summary: String, +} + +fn main() { + let code = match run() { + Ok(code) => code, + Err(err) => { + eprintln!("{err:#}"); + 1 + } + }; + std::process::exit(code); +} + +fn run() -> Result { + let cli = Cli::parse(); + let plan = build_plan(&cli); + if !cli.apply { + print_plan(&plan, cli.json)?; + return Ok(if plan.missing_count == 0 { 0 } else { 2 }); + } + if plan.missing_count > 0 { + print_plan(&plan, cli.json)?; + bail!("refusing --apply because required files are missing"); + } + let Some(step) = plan.steps.first() else { + bail!("empty install plan"); + }; + let Some(program) = step.command.first() else { + bail!("empty legacy command"); + }; + let status = Command::new(program) + .args(&step.command[1..]) + .status() + .with_context(|| format!("run {}", shell_join(&step.command)))?; + Ok(status.code().unwrap_or(1)) +} + +fn build_plan(cli: &Cli) -> Plan { + let required_files = vec![Requirement { + name: "legacy_script".to_string(), + ok: cli.legacy_script.is_file(), + detail: cli.legacy_script.display().to_string(), + }]; + let command = legacy_command(cli); + let steps = vec![Step { + order: 1, + name: format!("{:?}", cli.kind).to_lowercase(), + mutation: true, + summary: summary(cli.kind).to_string(), + command, + }]; + let missing_count = required_files.iter().filter(|item| !item.ok).count(); + Plan { + apply: cli.apply, + kind: cli.kind, + legacy_script: cli.legacy_script.clone(), + required_files, + steps, + missing_count, + } +} + +fn legacy_command(cli: &Cli) -> Vec { + let mut command = vec![ + "sh".to_string(), + cli.legacy_script.display().to_string(), + "--apply-legacy".to_string(), + "--server-host".to_string(), + cli.server_host.clone(), + "--server-port".to_string(), + cli.server_port.clone(), + ]; + match cli.kind { + InstallKind::Client => { + command.extend(["--version".to_string(), cli.version.clone()]); + if let Some(path) = &cli.install_base { + command.extend(["--install-base".to_string(), path.display().to_string()]); + } + if cli.force { + command.push("--force".to_string()); + } + } + InstallKind::RemoteWorker => { + command.extend([ + "--poll-interval".to_string(), + cli.poll_interval.clone(), + "--version".to_string(), + cli.version.clone(), + ]); + } + InstallKind::ConsoleSsh | InstallKind::WebCategory | InstallKind::PveWebadmin => { + command.extend(["--poll-interval".to_string(), cli.poll_interval.clone()]); + } + } + command +} + +fn summary(kind: InstallKind) -> &'static str { + match kind { + InstallKind::Client => { + "Install ActivityWatch Linux GUI watcher bundle and remote server config" + } + InstallKind::RemoteWorker => { + "Install Linux client, console/SSH logger, and web category logger" + } + InstallKind::ConsoleSsh => "Install console command and SSH session logger", + InstallKind::WebCategory => "Install Linux web category logger", + InstallKind::PveWebadmin => "Install Proxmox webadmin logger service", + } +} + +fn print_plan(plan: &Plan, json: bool) -> Result<()> { + if json { + println!("{}", serde_json::to_string_pretty(plan)?); + return Ok(()); + } + println!( + "aw-linux-install: {}", + if plan.apply { "apply" } else { "dry-run" } + ); + println!("kind: {:?}", plan.kind); + println!("legacy_script: {}", plan.legacy_script.display()); + println!("missing_inputs: {}", plan.missing_count); + println!("required files:"); + for item in &plan.required_files { + println!( + " [{}] {} - {}", + if item.ok { "OK" } else { "MISS" }, + item.name, + item.detail + ); + } + println!("planned steps:"); + for step in &plan.steps { + println!( + " {:02}. MUTATION {} :: {}", + step.order, + step.summary, + shell_join(&step.command) + ); + } + if !plan.apply { + println!("No install executed. Use --apply for explicit legacy install execution."); + } + Ok(()) +} + +fn shell_join(command: &[String]) -> String { + command + .iter() + .map(|part| { + if part + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || "-_./:=,".contains(ch)) + { + part.clone() + } else { + format!("'{}'", part.replace('\'', "'\\''")) + } + }) + .collect::>() + .join(" ") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn client_plan_preserves_version_and_force() { + let dir = tempfile::tempdir().unwrap(); + let script = dir.path().join("install.sh"); + std::fs::write(&script, "#!/bin/sh\n").unwrap(); + let cli = Cli { + kind: InstallKind::Client, + legacy_script: script, + server_host: "10.10.10.13".to_string(), + server_port: "5600".to_string(), + poll_interval: "5".to_string(), + version: "0.13.2".to_string(), + install_base: Some(PathBuf::from("/tmp/aw")), + force: true, + apply: false, + json: false, + }; + let plan = build_plan(&cli); + let cmd = &plan.steps[0].command; + assert!(cmd.contains(&"--apply-legacy".to_string())); + assert!(cmd.contains(&"--install-base".to_string())); + assert!(cmd.contains(&"--force".to_string())); + } + + #[test] + fn remote_worker_plan_includes_poll_and_version() { + let dir = tempfile::tempdir().unwrap(); + let script = dir.path().join("install.sh"); + std::fs::write(&script, "#!/bin/sh\n").unwrap(); + let cli = Cli { + kind: InstallKind::RemoteWorker, + legacy_script: script, + server_host: "host".to_string(), + server_port: "5600".to_string(), + poll_interval: "9".to_string(), + version: "0.13.3".to_string(), + install_base: None, + force: false, + apply: false, + json: false, + }; + let plan = build_plan(&cli); + let joined = shell_join(&plan.steps[0].command); + assert!(joined.contains("--poll-interval 9")); + assert!(joined.contains("--version 0.13.3")); + } +} diff --git a/scripts/check_detmir_rust_release_artifacts.sh b/scripts/check_detmir_rust_release_artifacts.sh index d3f506a..cb414df 100644 --- a/scripts/check_detmir_rust_release_artifacts.sh +++ b/scripts/check_detmir_rust_release_artifacts.sh @@ -19,6 +19,7 @@ required_bins=( aw-db-maintenance aw-ensure-reliability aw-health-check + aw-linux-install aw-prune-local-state check-aw-data check-aw-full diff --git a/scripts/install_aw_console_ssh_logger.sh b/scripts/install_aw_console_ssh_logger.sh index a811b47..c12bafb 100755 --- a/scripts/install_aw_console_ssh_logger.sh +++ b/scripts/install_aw_console_ssh_logger.sh @@ -11,6 +11,39 @@ LOG_DIR="${HOME}/.local/state/aw-console-ssh-logger/logs" SYSTEMD_USER_DIR="${HOME}/.config/systemd/user" BASHRC="${HOME}/.bashrc" BASH_PROFILE="${HOME}/.bash_profile" +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)" +SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")" + +if [ "${1:-}" = "--apply-legacy" ]; then + shift +else + TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}" + for candidate in \ + "${AW_LINUX_INSTALL_RUST:-}" \ + "${TARGET_ROOT}/release/aw-linux-install" \ + "${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \ + "/usr/local/bin/aw-linux-install"; do + if [ -n "$candidate" ] && [ -x "$candidate" ]; then + exec "$candidate" --kind console-ssh --legacy-script "$SELF_PATH" "$@" + fi + done + cat >&2 <<'EOF' +install_aw_console_ssh_logger.sh now requires the Rust planner for safe default runs. +Build it first: + cd adk-rust && cargo build --release -p aw-linux-install + +Safe dry-run: + scripts/install_aw_console_ssh_logger.sh --json + +Explicit install: + scripts/install_aw_console_ssh_logger.sh --apply + +Old shell install: + scripts/install_aw_console_ssh_logger.sh --apply-legacy +EOF + exit 2 +fi usage() { cat <<'EOF' diff --git a/scripts/install_aw_linux_client.sh b/scripts/install_aw_linux_client.sh index 8be886f..19223d9 100755 --- a/scripts/install_aw_linux_client.sh +++ b/scripts/install_aw_linux_client.sh @@ -9,6 +9,39 @@ BIN_DIR="${HOME}/.local/bin" AUTOSTART_DIR="${HOME}/.config/autostart" CONFIG_ROOT="${XDG_CONFIG_HOME:-$HOME/.config}/activitywatch" FORCE="0" +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)" +SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")" + +if [ "${1:-}" = "--apply-legacy" ]; then + shift +else + TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}" + for candidate in \ + "${AW_LINUX_INSTALL_RUST:-}" \ + "${TARGET_ROOT}/release/aw-linux-install" \ + "${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \ + "/usr/local/bin/aw-linux-install"; do + if [ -n "$candidate" ] && [ -x "$candidate" ]; then + exec "$candidate" --kind client --legacy-script "$SELF_PATH" "$@" + fi + done + cat >&2 <<'EOF' +install_aw_linux_client.sh now requires the Rust planner for safe default runs. +Build it first: + cd adk-rust && cargo build --release -p aw-linux-install + +Safe dry-run: + scripts/install_aw_linux_client.sh --json + +Explicit install: + scripts/install_aw_linux_client.sh --apply + +Old shell install: + scripts/install_aw_linux_client.sh --apply-legacy +EOF + exit 2 +fi usage() { cat <<'EOF' diff --git a/scripts/install_aw_linux_remote_worker.sh b/scripts/install_aw_linux_remote_worker.sh index 3cebd20..d3865a5 100644 --- a/scripts/install_aw_linux_remote_worker.sh +++ b/scripts/install_aw_linux_remote_worker.sh @@ -5,6 +5,39 @@ SERVER_HOST="10.10.10.13" SERVER_PORT="5600" POLL_INTERVAL="5" AW_VERSION="0.13.2" +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)" +SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")" + +if [ "${1:-}" = "--apply-legacy" ]; then + shift +else + TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}" + for candidate in \ + "${AW_LINUX_INSTALL_RUST:-}" \ + "${TARGET_ROOT}/release/aw-linux-install" \ + "${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \ + "/usr/local/bin/aw-linux-install"; do + if [ -n "$candidate" ] && [ -x "$candidate" ]; then + exec "$candidate" --kind remote-worker --legacy-script "$SELF_PATH" "$@" + fi + done + cat >&2 <<'EOF' +install_aw_linux_remote_worker.sh now requires the Rust planner for safe default runs. +Build it first: + cd adk-rust && cargo build --release -p aw-linux-install + +Safe dry-run: + scripts/install_aw_linux_remote_worker.sh --json + +Explicit install: + scripts/install_aw_linux_remote_worker.sh --apply + +Old shell install: + scripts/install_aw_linux_remote_worker.sh --apply-legacy +EOF + exit 2 +fi usage() { cat <<'EOF' @@ -49,19 +82,20 @@ while [ "$#" -gt 0 ]; do esac done -SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) - sh "${SCRIPT_DIR}/install_aw_linux_client.sh" \ + --apply-legacy \ --server-host "${SERVER_HOST}" \ --server-port "${SERVER_PORT}" \ --version "${AW_VERSION}" sh "${SCRIPT_DIR}/install_aw_console_ssh_logger.sh" \ + --apply-legacy \ --server-host "${SERVER_HOST}" \ --server-port "${SERVER_PORT}" \ --poll-interval "${POLL_INTERVAL}" sh "${SCRIPT_DIR}/install_aw_linux_web_category_logger.sh" \ + --apply-legacy \ --server-host "${SERVER_HOST}" \ --server-port "${SERVER_PORT}" \ --poll-interval "${POLL_INTERVAL}" diff --git a/scripts/install_aw_linux_web_category_logger.sh b/scripts/install_aw_linux_web_category_logger.sh index 4666df7..b58d641 100644 --- a/scripts/install_aw_linux_web_category_logger.sh +++ b/scripts/install_aw_linux_web_category_logger.sh @@ -10,6 +10,39 @@ STATE_DIR="${HOME}/.local/state/aw-linux-web-category" LOG_DIR="${HOME}/.local/state/aw-linux-web-category/logs" CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/aw-linux-web-category" SYSTEMD_USER_DIR="${HOME}/.config/systemd/user" +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)" +SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")" + +if [ "${1:-}" = "--apply-legacy" ]; then + shift +else + TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}" + for candidate in \ + "${AW_LINUX_INSTALL_RUST:-}" \ + "${TARGET_ROOT}/release/aw-linux-install" \ + "${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \ + "/usr/local/bin/aw-linux-install"; do + if [ -n "$candidate" ] && [ -x "$candidate" ]; then + exec "$candidate" --kind web-category --legacy-script "$SELF_PATH" "$@" + fi + done + cat >&2 <<'EOF' +install_aw_linux_web_category_logger.sh now requires the Rust planner for safe default runs. +Build it first: + cd adk-rust && cargo build --release -p aw-linux-install + +Safe dry-run: + scripts/install_aw_linux_web_category_logger.sh --json + +Explicit install: + scripts/install_aw_linux_web_category_logger.sh --apply + +Old shell install: + scripts/install_aw_linux_web_category_logger.sh --apply-legacy +EOF + exit 2 +fi usage() { cat <<'EOF' diff --git a/scripts/install_aw_pve_webadmin_logger.sh b/scripts/install_aw_pve_webadmin_logger.sh index 98b664d..6fcc196 100755 --- a/scripts/install_aw_pve_webadmin_logger.sh +++ b/scripts/install_aw_pve_webadmin_logger.sh @@ -9,6 +9,39 @@ STATE_DIR="/var/lib/aw-pve-webadmin-logger" LOG_DIR="/var/log/aw-pve-webadmin-logger" CONFIG_PATH="/etc/aw-pve-webadmin-logger/config.json" SERVICE_PATH="/etc/systemd/system/aw-pve-webadmin-logger.service" +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)" +SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")" + +if [ "${1:-}" = "--apply-legacy" ]; then + shift +else + TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}" + for candidate in \ + "${AW_LINUX_INSTALL_RUST:-}" \ + "${TARGET_ROOT}/release/aw-linux-install" \ + "${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \ + "/usr/local/bin/aw-linux-install"; do + if [ -n "$candidate" ] && [ -x "$candidate" ]; then + exec "$candidate" --kind pve-webadmin --legacy-script "$SELF_PATH" "$@" + fi + done + cat >&2 <<'EOF' +install_aw_pve_webadmin_logger.sh now requires the Rust planner for safe default runs. +Build it first: + cd adk-rust && cargo build --release -p aw-linux-install + +Safe dry-run: + scripts/install_aw_pve_webadmin_logger.sh --json + +Explicit install: + scripts/install_aw_pve_webadmin_logger.sh --apply + +Old shell install: + scripts/install_aw_pve_webadmin_logger.sh --apply-legacy +EOF + exit 2 +fi usage() { cat <<'EOF'