Feature: Ergänzt info/debug-Logging im gesamten Programm
Bisher wurde logger-ctdra nur für warn() bei Fehlerfällen genutzt, wodurch z. B. ein manuelles 'mount --all' im Terminal minutenlang keine Ausgabe zeigte, obwohl im Hintergrund Erreichbarkeitsprüfungen (Ping/curl/mac2ip, bis zu 15s) und Mount-Vorgänge liefen. Jetzt loggen Reconcile-Entscheidungen, Mount-/Unmount-Aufrufe, MAC-Auflösung, Lock-Erwerb, Config-/Credential-/ Crypto-Operationen sowie Cron-/CLI-Lifecycle-Ereignisse durchgängig auf info- bzw. debug-Niveau (ohne Passwörter/Zugangsdaten zu protokollieren). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -149,6 +149,7 @@ async fn remove(id: &str) -> anyhow::Result<()> {
|
|||||||
creds.delete(id, None).await?;
|
creds.delete(id, None).await?;
|
||||||
smart_mount::mount::cleanup_credentials(&pair, &cfg.settings);
|
smart_mount::mount::cleanup_credentials(&pair, &cfg.settings);
|
||||||
|
|
||||||
|
logger_ctdra::info("drive", &format!("drive pair '{id}' removed"));
|
||||||
println!("Drive pair '{id}' removed.");
|
println!("Drive pair '{id}' removed.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -241,6 +242,7 @@ async fn add(args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger_ctdra::info("drive", &format!("drive pair '{id}' created"));
|
||||||
println!("Drive pair '{id}' created.");
|
println!("Drive pair '{id}' created.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -373,6 +375,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger_ctdra::info("drive", &format!("drive pair '{id}' updated"));
|
||||||
println!("Drive pair '{id}' updated.");
|
println!("Drive pair '{id}' updated.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,14 @@ use clap_complete::Shell;
|
|||||||
/// `sudo` nicht gefunden).
|
/// `sudo` nicht gefunden).
|
||||||
pub(crate) fn require_root(context: &str) -> anyhow::Result<()> {
|
pub(crate) fn require_root(context: &str) -> anyhow::Result<()> {
|
||||||
if !sudo_ctdra::is_run_as_root() {
|
if !sudo_ctdra::is_run_as_root() {
|
||||||
|
logger_ctdra::info(
|
||||||
|
"cli",
|
||||||
|
&format!("'{context}' requires root - re-executing via sudo"),
|
||||||
|
);
|
||||||
let err = sudo_ctdra::run_as_root();
|
let err = sudo_ctdra::run_as_root();
|
||||||
anyhow::bail!("'{context}' requires root privileges: could not re-exec via sudo: {err}");
|
anyhow::bail!("'{context}' requires root privileges: could not re-exec via sudo: {err}");
|
||||||
}
|
}
|
||||||
|
logger_ctdra::debug("cli", &format!("'{context}': already running as root"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +93,27 @@ pub enum Commands {
|
|||||||
Completions { shell: Shell },
|
Completions { shell: Shell },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Kurzname eines Subcommands fürs Logging (siehe [`dispatch`]) - kein `Debug`-Derive auf
|
||||||
|
/// `Commands` nötig, das würde auch die (teils sensiblen) Argument-Felder mit abdrucken.
|
||||||
|
fn command_label(cmd: &Commands) -> &'static str {
|
||||||
|
match cmd {
|
||||||
|
Commands::Drive { .. } => "drive",
|
||||||
|
Commands::Mount { .. } => "mount",
|
||||||
|
Commands::Unmount { .. } => "unmount",
|
||||||
|
Commands::Status { .. } => "status",
|
||||||
|
Commands::Watch => "watch",
|
||||||
|
Commands::Service { .. } => "service",
|
||||||
|
Commands::Doctor { .. } => "doctor",
|
||||||
|
Commands::Completions { .. } => "completions",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Führt das per `Cli` geparste Subcommand aus.
|
/// Führt das per `Cli` geparste Subcommand aus.
|
||||||
pub async fn dispatch(cli: Cli) -> anyhow::Result<()> {
|
pub async fn dispatch(cli: Cli) -> anyhow::Result<()> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"cli",
|
||||||
|
&format!("dispatching '{}'", command_label(&cli.command)),
|
||||||
|
);
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Drive { action } => drive::run(*action).await,
|
Commands::Drive { action } => drive::run(*action).await,
|
||||||
Commands::Mount { name, all } => mount_cmd::run_mount(name, all).await,
|
Commands::Mount { name, all } => mount_cmd::run_mount(name, all).await,
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ pub async fn run_mount(name: Option<String>, all: bool) -> anyhow::Result<()> {
|
|||||||
println!("No matching drive pairs found.");
|
println!("No matching drive pairs found.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info(
|
||||||
|
"mount",
|
||||||
|
&format!("mount: processing {} drive pair(s)", pairs.len()),
|
||||||
|
);
|
||||||
|
|
||||||
let creds = CredentialStore::open().await?;
|
let creds = CredentialStore::open().await?;
|
||||||
for pair in &pairs {
|
for pair in &pairs {
|
||||||
@@ -43,6 +47,10 @@ pub async fn run_unmount(name: Option<String>, all: bool) -> anyhow::Result<()>
|
|||||||
println!("No matching drive pairs found.");
|
println!("No matching drive pairs found.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info(
|
||||||
|
"mount",
|
||||||
|
&format!("unmount: processing {} drive pair(s)", pairs.len()),
|
||||||
|
);
|
||||||
|
|
||||||
for pair in &pairs {
|
for pair in &pairs {
|
||||||
match reconcile::unmount_pair(pair, &cfg.settings).await {
|
match reconcile::unmount_pair(pair, &cfg.settings).await {
|
||||||
|
|||||||
+8
-1
@@ -31,6 +31,10 @@ fn install() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
match systemd::install_cron(interval)? {
|
match systemd::install_cron(interval)? {
|
||||||
systemd::CronInstallOutcome::SystemFile(path) => {
|
systemd::CronInstallOutcome::SystemFile(path) => {
|
||||||
|
logger_ctdra::info(
|
||||||
|
"service",
|
||||||
|
&format!("cron entry written: {}", path.display()),
|
||||||
|
);
|
||||||
println!("Cron entry written: {}", path.display());
|
println!("Cron entry written: {}", path.display());
|
||||||
}
|
}
|
||||||
systemd::CronInstallOutcome::Unavailable => {
|
systemd::CronInstallOutcome::Unavailable => {
|
||||||
@@ -45,7 +49,10 @@ fn install() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
fn uninstall() -> anyhow::Result<()> {
|
fn uninstall() -> anyhow::Result<()> {
|
||||||
match systemd::uninstall_cron()? {
|
match systemd::uninstall_cron()? {
|
||||||
systemd::CronUninstallOutcome::Removed => println!("Cron entry removed."),
|
systemd::CronUninstallOutcome::Removed => {
|
||||||
|
logger_ctdra::info("service", "cron entry removed");
|
||||||
|
println!("Cron entry removed.");
|
||||||
|
}
|
||||||
systemd::CronUninstallOutcome::NotPresent => {
|
systemd::CronUninstallOutcome::NotPresent => {
|
||||||
println!("Nothing to remove - no cron entry was installed.")
|
println!("Nothing to remove - no cron entry was installed.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ pub async fn run() -> anyhow::Result<()> {
|
|||||||
had_failure = true;
|
had_failure = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info(
|
||||||
|
"watch",
|
||||||
|
&format!(
|
||||||
|
"watch pass done: {} pair(s), {}",
|
||||||
|
outcomes.len(),
|
||||||
|
if had_failure {
|
||||||
|
"with failures"
|
||||||
|
} else {
|
||||||
|
"no failures"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if had_failure {
|
if had_failure {
|
||||||
anyhow::bail!("at least one drive pair could not be reconciled");
|
anyhow::bail!("at least one drive pair could not be reconciled");
|
||||||
|
|||||||
+3
-1
@@ -19,5 +19,7 @@ pub use schema::{
|
|||||||
pub fn init() {
|
pub fn init() {
|
||||||
config_ctdra::set_config_name("config");
|
config_ctdra::set_config_name("config");
|
||||||
let program = config_ctdra::get_program_name();
|
let program = config_ctdra::get_program_name();
|
||||||
config_ctdra::set_custom_dir(std::path::PathBuf::from("/etc").join(program));
|
let dir = std::path::PathBuf::from("/etc").join(program);
|
||||||
|
logger_ctdra::debug("config", &format!("config directory: {}", dir.display()));
|
||||||
|
config_ctdra::set_custom_dir(dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ pub fn load() -> Result<AppConfig> {
|
|||||||
|
|
||||||
/// Fügt ein neues Laufwerkspaar hinzu (load-mutate-store in einem atomaren Schritt).
|
/// Fügt ein neues Laufwerkspaar hinzu (load-mutate-store in einem atomaren Schritt).
|
||||||
pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
|
pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"config",
|
||||||
|
&format!("writing new pair '{}' to config", pair.id),
|
||||||
|
);
|
||||||
Ok(config_ctdra::modify::<AppConfig, _>(|cfg| {
|
Ok(config_ctdra::modify::<AppConfig, _>(|cfg| {
|
||||||
cfg.pairs.push(pair.clone());
|
cfg.pairs.push(pair.clone());
|
||||||
})?)
|
})?)
|
||||||
@@ -18,6 +22,7 @@ pub fn add_pair(pair: DrivePair) -> Result<AppConfig> {
|
|||||||
/// Ersetzt ein bestehendes Laufwerkspaar (Vergleich über `id`).
|
/// Ersetzt ein bestehendes Laufwerkspaar (Vergleich über `id`).
|
||||||
pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
|
pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
|
||||||
let id = pair.id.clone();
|
let id = pair.id.clone();
|
||||||
|
logger_ctdra::debug("config", &format!("writing updated pair '{id}' to config"));
|
||||||
let updated = config_ctdra::modify::<AppConfig, _>(move |cfg| {
|
let updated = config_ctdra::modify::<AppConfig, _>(move |cfg| {
|
||||||
if let Some(existing) = cfg.pairs.iter_mut().find(|p| p.id == pair.id) {
|
if let Some(existing) = cfg.pairs.iter_mut().find(|p| p.id == pair.id) {
|
||||||
*existing = pair.clone();
|
*existing = pair.clone();
|
||||||
@@ -31,6 +36,7 @@ pub fn update_pair(pair: DrivePair) -> Result<AppConfig> {
|
|||||||
|
|
||||||
/// Entfernt ein Laufwerkspaar per ID.
|
/// Entfernt ein Laufwerkspaar per ID.
|
||||||
pub fn remove_pair(id: &str) -> Result<AppConfig> {
|
pub fn remove_pair(id: &str) -> Result<AppConfig> {
|
||||||
|
logger_ctdra::debug("config", &format!("removing pair '{id}' from config"));
|
||||||
// Die "vorher"-Länge wird INNERHALB desselben `modify`-Aufrufs (auf der bereits frisch
|
// Die "vorher"-Länge wird INNERHALB desselben `modify`-Aufrufs (auf der bereits frisch
|
||||||
// geladenen `cfg`) ermittelt statt über einen separaten, vorgelagerten `load()`-Aufruf:
|
// geladenen `cfg`) ermittelt statt über einen separaten, vorgelagerten `load()`-Aufruf:
|
||||||
// zwischen zwei getrennten Aufrufen könnte ein nebenläufiger Schreiber die Paarliste
|
// zwischen zwei getrennten Aufrufen könnte ein nebenläufiger Schreiber die Paarliste
|
||||||
|
|||||||
@@ -32,8 +32,16 @@ mod file_key {
|
|||||||
|
|
||||||
pub fn load_or_create(path: &Path) -> Result<[u8; 32]> {
|
pub fn load_or_create(path: &Path) -> Result<[u8; 32]> {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"crypto",
|
||||||
|
&format!("loading master key from '{}'", path.display()),
|
||||||
|
);
|
||||||
return read(path);
|
return read(path);
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info(
|
||||||
|
"crypto",
|
||||||
|
&format!("creating new master key at '{}'", path.display()),
|
||||||
|
);
|
||||||
create(path)
|
create(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ pub const NONCE_LEN: usize = 12;
|
|||||||
/// aes-gcms Re-Export-Kette nicht automatisch aktiviert wird). `getrandom::fill` ist
|
/// aes-gcms Re-Export-Kette nicht automatisch aktiviert wird). `getrandom::fill` ist
|
||||||
/// unabhängig davon stabil und genau für diesen Zweck gedacht.
|
/// unabhängig davon stabil und genau für diesen Zweck gedacht.
|
||||||
pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
|
pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||||
|
logger_ctdra::debug("crypto", "encrypting credential data");
|
||||||
let cipher = Aes256Gcm::new(&Key::<Aes256Gcm>::from(*key));
|
let cipher = Aes256Gcm::new(&Key::<Aes256Gcm>::from(*key));
|
||||||
|
|
||||||
let mut nonce_bytes = [0u8; NONCE_LEN];
|
let mut nonce_bytes = [0u8; NONCE_LEN];
|
||||||
@@ -44,6 +45,7 @@ pub fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
|
|||||||
|
|
||||||
/// Entschlüsselt einen zuvor mit [`encrypt`] erzeugten Ciphertext.
|
/// Entschlüsselt einen zuvor mit [`encrypt`] erzeugten Ciphertext.
|
||||||
pub fn decrypt(ciphertext: &[u8], nonce: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
|
pub fn decrypt(ciphertext: &[u8], nonce: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
|
||||||
|
logger_ctdra::debug("crypto", "decrypting credential data");
|
||||||
if nonce.len() != NONCE_LEN {
|
if nonce.len() != NONCE_LEN {
|
||||||
return Err(Error::Crypto(format!(
|
return Err(Error::Crypto(format!(
|
||||||
"invalid nonce length: expected {NONCE_LEN}, got {}",
|
"invalid nonce length: expected {NONCE_LEN}, got {}",
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ impl CredentialStore {
|
|||||||
domain: Option<&str>,
|
domain: Option<&str>,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"db",
|
||||||
|
&format!(
|
||||||
|
"storing credential for pair '{pair_id}' ({})",
|
||||||
|
side.as_str()
|
||||||
|
),
|
||||||
|
);
|
||||||
let key = resolve_master_key()?;
|
let key = resolve_master_key()?;
|
||||||
let (ciphertext, nonce) = crypto::encrypt(password.as_bytes(), &key)?;
|
let (ciphertext, nonce) = crypto::encrypt(password.as_bytes(), &key)?;
|
||||||
let now = now_unix();
|
let now = now_unix();
|
||||||
@@ -85,6 +92,13 @@ impl CredentialStore {
|
|||||||
|
|
||||||
/// Liest und entschlüsselt die Zugangsdaten für `pair_id`/`side`, falls vorhanden.
|
/// Liest und entschlüsselt die Zugangsdaten für `pair_id`/`side`, falls vorhanden.
|
||||||
pub async fn get(&self, pair_id: &str, side: Side) -> Result<Option<Credential>> {
|
pub async fn get(&self, pair_id: &str, side: Side) -> Result<Option<Credential>> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"db",
|
||||||
|
&format!(
|
||||||
|
"reading credential for pair '{pair_id}' ({})",
|
||||||
|
side.as_str()
|
||||||
|
),
|
||||||
|
);
|
||||||
let mut rows = self
|
let mut rows = self
|
||||||
.conn
|
.conn
|
||||||
.query(
|
.query(
|
||||||
@@ -117,6 +131,13 @@ impl CredentialStore {
|
|||||||
|
|
||||||
/// Löscht Zugangsdaten. `side = None` löscht beide Seiten (z. B. beim Entfernen eines Paars).
|
/// Löscht Zugangsdaten. `side = None` löscht beide Seiten (z. B. beim Entfernen eines Paars).
|
||||||
pub async fn delete(&self, pair_id: &str, side: Option<Side>) -> Result<()> {
|
pub async fn delete(&self, pair_id: &str, side: Option<Side>) -> Result<()> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"db",
|
||||||
|
&format!(
|
||||||
|
"deleting credential(s) for pair '{pair_id}' ({})",
|
||||||
|
side.map(|s| s.as_str()).unwrap_or("both sides")
|
||||||
|
),
|
||||||
|
);
|
||||||
match side {
|
match side {
|
||||||
Some(side) => {
|
Some(side) => {
|
||||||
self.conn
|
self.conn
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub fn resolve_db_path() -> PathBuf {
|
|||||||
/// Öffnet (und initialisiert bei Bedarf) die lokale Datenbank am aufgelösten Pfad.
|
/// Öffnet (und initialisiert bei Bedarf) die lokale Datenbank am aufgelösten Pfad.
|
||||||
pub async fn open() -> Result<turso::Connection> {
|
pub async fn open() -> Result<turso::Connection> {
|
||||||
let path = resolve_db_path();
|
let path = resolve_db_path();
|
||||||
|
logger_ctdra::debug("db", &format!("opening database at '{}'", path.display()));
|
||||||
if let Some(dir) = path.parent() {
|
if let Some(dir) = path.parent() {
|
||||||
tokio::fs::create_dir_all(dir)
|
tokio::fs::create_dir_all(dir)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ async fn main() -> ExitCode {
|
|||||||
Err(_) => "info".to_string(),
|
Err(_) => "info".to_string(),
|
||||||
};
|
};
|
||||||
logger_ctdra::set_log_level(parse_log_level(&log_level));
|
logger_ctdra::set_log_level(parse_log_level(&log_level));
|
||||||
|
logger_ctdra::info(
|
||||||
|
"main",
|
||||||
|
&format!(
|
||||||
|
"smart-mount {} starting (log level: {log_level})",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let cli = cli::Cli::parse();
|
let cli = cli::Cli::parse();
|
||||||
match cli::dispatch(cli).await {
|
match cli::dispatch(cli).await {
|
||||||
|
|||||||
+13
-1
@@ -23,9 +23,16 @@ use crate::error::{Error, Result};
|
|||||||
/// Datei-Deskriptor), bis der Guard gedroppt wird - Schließen des Deskriptors gibt die Sperre
|
/// Datei-Deskriptor), bis der Guard gedroppt wird - Schließen des Deskriptors gibt die Sperre
|
||||||
/// implizit frei, ein explizites `unlock()` ist dafür nicht nötig.
|
/// implizit frei, ein explizites `unlock()` ist dafür nicht nötig.
|
||||||
pub struct PairLock {
|
pub struct PairLock {
|
||||||
|
pair_id: String,
|
||||||
_file: File,
|
_file: File,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for PairLock {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
logger_ctdra::debug("lock", &format!("pair '{}': lock released", self.pair_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Verzeichnis für die Sperrdateien: `/run/smart-mount/locks` im Root-/System-Kontext (root ist
|
/// Verzeichnis für die Sperrdateien: `/run/smart-mount/locks` im Root-/System-Kontext (root ist
|
||||||
/// dort ohnehin die einzige Partei, die Paare in diesem Kontext mountet), sonst
|
/// dort ohnehin die einzige Partei, die Paare in diesem Kontext mountet), sonst
|
||||||
/// `$XDG_RUNTIME_DIR` (per-Nutzer, von systemd `0700`-geschützt angelegt) mit Fallback auf das
|
/// `$XDG_RUNTIME_DIR` (per-Nutzer, von systemd `0700`-geschützt angelegt) mit Fallback auf das
|
||||||
@@ -59,8 +66,13 @@ fn acquire_blocking(pair_id: &str) -> Result<PairLock> {
|
|||||||
// Blockiert, bis die Sperre frei wird - `flock(2)` kennt keinen Async-Mechanismus, daher
|
// Blockiert, bis die Sperre frei wird - `flock(2)` kennt keinen Async-Mechanismus, daher
|
||||||
// läuft dieser gesamte Aufruf über `spawn_blocking` (siehe [`acquire`]) auf einem
|
// läuft dieser gesamte Aufruf über `spawn_blocking` (siehe [`acquire`]) auf einem
|
||||||
// Blocking-Thread statt einem Tokio-Worker-Thread.
|
// Blocking-Thread statt einem Tokio-Worker-Thread.
|
||||||
|
logger_ctdra::debug("lock", &format!("pair '{pair_id}': waiting for lock"));
|
||||||
file.lock().map_err(|e| Error::io(&path, e))?;
|
file.lock().map_err(|e| Error::io(&path, e))?;
|
||||||
Ok(PairLock { _file: file })
|
logger_ctdra::debug("lock", &format!("pair '{pair_id}': lock acquired"));
|
||||||
|
Ok(PairLock {
|
||||||
|
pair_id: pair_id.to_string(),
|
||||||
|
_file: file,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sperrt ein Laufwerkspaar prozessübergreifend für die Dauer des zurückgegebenen Guards.
|
/// Sperrt ein Laufwerkspaar prozessübergreifend für die Dauer des zurückgegebenen Guards.
|
||||||
|
|||||||
+25
-9
@@ -93,6 +93,17 @@ fn run_tolerating_already_done_with_timeout(
|
|||||||
tolerate_timeout: bool,
|
tolerate_timeout: bool,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"mount",
|
||||||
|
&format!(
|
||||||
|
"{context}: running '{} {}' (timeout {timeout_secs}s)",
|
||||||
|
cmd.get_program().to_string_lossy(),
|
||||||
|
cmd.get_args()
|
||||||
|
.map(|a| a.to_string_lossy().into_owned())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
),
|
||||||
|
);
|
||||||
let output =
|
let output =
|
||||||
run_with_timeout(cmd, timeout_secs).map_err(|e| crate::error::Error::MountFailed {
|
run_with_timeout(cmd, timeout_secs).map_err(|e| crate::error::Error::MountFailed {
|
||||||
context: context.to_string(),
|
context: context.to_string(),
|
||||||
@@ -100,6 +111,7 @@ fn run_tolerating_already_done_with_timeout(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
|
logger_ctdra::debug("mount", &format!("{context}: succeeded"));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,16 +185,20 @@ fn cleanup_side_credentials(
|
|||||||
match kind {
|
match kind {
|
||||||
MountKind::Smb => {
|
MountKind::Smb => {
|
||||||
let path = smb::credentials_path(&pair.id, side);
|
let path = smb::credentials_path(&pair.id, side);
|
||||||
if path.exists()
|
if path.exists() {
|
||||||
&& let Err(e) = std::fs::remove_file(&path)
|
match std::fs::remove_file(&path) {
|
||||||
{
|
Ok(()) => logger_ctdra::debug(
|
||||||
logger_ctdra::warn(
|
"mount",
|
||||||
"mount",
|
&format!("deleted credentials file '{}'", path.display()),
|
||||||
&format!(
|
|
||||||
"Could not delete credentials file '{}': {e}",
|
|
||||||
path.display()
|
|
||||||
),
|
),
|
||||||
);
|
Err(e) => logger_ctdra::warn(
|
||||||
|
"mount",
|
||||||
|
&format!(
|
||||||
|
"Could not delete credentials file '{}': {e}",
|
||||||
|
path.display()
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MountKind::WebDav => {
|
MountKind::WebDav => {
|
||||||
|
|||||||
+6
-1
@@ -29,7 +29,12 @@ impl MountBackend for SmbBackend {
|
|||||||
|
|
||||||
fn prepare(&self, target: &MountTarget, cred: Option<&Credential>) -> Result<()> {
|
fn prepare(&self, target: &MountTarget, cred: Option<&Credential>) -> Result<()> {
|
||||||
if let Some(cred) = cred {
|
if let Some(cred) = cred {
|
||||||
write_credentials_file(&credentials_path(&target.pair_id, target.side), cred)?;
|
let path = credentials_path(&target.pair_id, target.side);
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"mount",
|
||||||
|
&format!("cifs: writing credentials file '{}'", path.display()),
|
||||||
|
);
|
||||||
|
write_credentials_file(&path, cred)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,6 +301,15 @@ pub fn activate_symlink(pair: &DrivePair, side: Side) -> Result<()> {
|
|||||||
let _ = std::fs::remove_file(&tmp_path);
|
let _ = std::fs::remove_file(&tmp_path);
|
||||||
std::os::unix::fs::symlink(&target, &tmp_path).map_err(|e| Error::io(&tmp_path, e))?;
|
std::os::unix::fs::symlink(&target, &tmp_path).map_err(|e| Error::io(&tmp_path, e))?;
|
||||||
std::fs::rename(&tmp_path, link_path).map_err(|e| Error::io(link_path, e))?;
|
std::fs::rename(&tmp_path, link_path).map_err(|e| Error::io(link_path, e))?;
|
||||||
|
logger_ctdra::info(
|
||||||
|
"mount",
|
||||||
|
&format!(
|
||||||
|
"pair '{}': activated '{}' -> '{}'",
|
||||||
|
pair.id,
|
||||||
|
link_path.display(),
|
||||||
|
target.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ impl MountBackend for WebDavBackend {
|
|||||||
"davfs2 credential has an empty username or password".to_string(),
|
"davfs2 credential has an empty username or password".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"mount",
|
||||||
|
&format!("davfs2: writing secrets entry for '{}'", target.source),
|
||||||
|
);
|
||||||
write_secrets_entry(
|
write_secrets_entry(
|
||||||
&davfs2_secrets_path(),
|
&davfs2_secrets_path(),
|
||||||
&target.source,
|
&target.source,
|
||||||
|
|||||||
+19
-1
@@ -38,6 +38,12 @@ enum Mac2IpOutput {
|
|||||||
/// `binary` ist der konfigurierte Binary-Name/-Pfad (`GlobalSettings::mac2ip_binary`,
|
/// `binary` ist der konfigurierte Binary-Name/-Pfad (`GlobalSettings::mac2ip_binary`,
|
||||||
/// standardmäßig `"mac2ip"`, per PATH aufgelöst).
|
/// standardmäßig `"mac2ip"`, per PATH aufgelöst).
|
||||||
pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
|
pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
|
||||||
|
logger_ctdra::info(
|
||||||
|
"network",
|
||||||
|
&format!(
|
||||||
|
"resolving MAC '{mac}' via '{binary}' (may take up to {MAC2IP_TIMEOUT_SECS}s, e.g. for an nmap scan)"
|
||||||
|
),
|
||||||
|
);
|
||||||
let output = Command::new("timeout")
|
let output = Command::new("timeout")
|
||||||
.arg(MAC2IP_TIMEOUT_SECS.to_string())
|
.arg(MAC2IP_TIMEOUT_SECS.to_string())
|
||||||
.arg(binary)
|
.arg(binary)
|
||||||
@@ -49,13 +55,25 @@ pub fn resolve(mac: &str, binary: &str) -> Result<Ipv4Addr> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if output.status.code() == Some(124) {
|
if output.status.code() == Some(124) {
|
||||||
|
logger_ctdra::warn(
|
||||||
|
"network",
|
||||||
|
&format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s for MAC '{mac}'"),
|
||||||
|
);
|
||||||
return Err(Error::Mac2Ip {
|
return Err(Error::Mac2Ip {
|
||||||
mac: mac.to_string(),
|
mac: mac.to_string(),
|
||||||
reason: format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s (timed out)"),
|
reason: format!("'{binary}' did not respond within {MAC2IP_TIMEOUT_SECS}s (timed out)"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_output(&output.stdout, mac)
|
let result = parse_output(&output.stdout, mac);
|
||||||
|
match &result {
|
||||||
|
Ok(ip) => logger_ctdra::info("network", &format!("MAC '{mac}' resolved to {ip}")),
|
||||||
|
Err(e) => logger_ctdra::debug(
|
||||||
|
"network",
|
||||||
|
&format!("MAC '{mac}' could not be resolved: {e}"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prüft, ob das konfigurierte `mac2ip`-Binary über `PATH` auffindbar ist.
|
/// Prüft, ob das konfigurierte `mac2ip`-Binary über `PATH` auffindbar ist.
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ use std::process::{Command, Stdio};
|
|||||||
/// (z. B. ein kaputter Reverse-Proxy) sonst bei jedem `watch`-Durchlauf einen vollen,
|
/// (z. B. ein kaputter Reverse-Proxy) sonst bei jedem `watch`-Durchlauf einen vollen,
|
||||||
/// letztlich erfolglosen Umschaltversuch (inkl. `MOUNT_TIMEOUT_SECS`-Wartezeit) auslösen würde.
|
/// letztlich erfolglosen Umschaltversuch (inkl. `MOUNT_TIMEOUT_SECS`-Wartezeit) auslösen würde.
|
||||||
pub fn is_reachable(addr: &str) -> bool {
|
pub fn is_reachable(addr: &str) -> bool {
|
||||||
|
let reachable = is_reachable_inner(addr);
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"network",
|
||||||
|
&format!("reachability check for '{addr}': {reachable}"),
|
||||||
|
);
|
||||||
|
reachable
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_reachable_inner(addr: &str) -> bool {
|
||||||
if addr.starts_with("http://") || addr.starts_with("https://") {
|
if addr.starts_with("http://") || addr.starts_with("https://") {
|
||||||
Command::new("curl")
|
Command::new("curl")
|
||||||
.args([
|
.args([
|
||||||
|
|||||||
+84
-12
@@ -50,6 +50,13 @@ pub struct ReconcileOutcome {
|
|||||||
/// Risiko für einen Effizienzgewinn, den die Reachability-Parallelisierung bereits liefert.
|
/// Risiko für einen Effizienzgewinn, den die Reachability-Parallelisierung bereits liefert.
|
||||||
pub async fn watch_once(cfg: &AppConfig, creds: &CredentialStore) -> Vec<ReconcileOutcome> {
|
pub async fn watch_once(cfg: &AppConfig, creds: &CredentialStore) -> Vec<ReconcileOutcome> {
|
||||||
let enabled: Vec<&DrivePair> = cfg.pairs.iter().filter(|p| p.enabled).collect();
|
let enabled: Vec<&DrivePair> = cfg.pairs.iter().filter(|p| p.enabled).collect();
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"watch: checking reachability for {} enabled pair(s)",
|
||||||
|
enabled.len()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let mut checks = Vec::with_capacity(enabled.len());
|
let mut checks = Vec::with_capacity(enabled.len());
|
||||||
for pair in &enabled {
|
for pair in &enabled {
|
||||||
@@ -123,16 +130,28 @@ async fn reconcile_pair_checked(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(action) => ReconcileOutcome {
|
Ok(action) => {
|
||||||
pair_id,
|
logger_ctdra::info(
|
||||||
pair_name,
|
"reconcile",
|
||||||
action,
|
&format!("pair '{pair_name}' ({pair_id}): {action:?}"),
|
||||||
},
|
);
|
||||||
Err(e) => ReconcileOutcome {
|
ReconcileOutcome {
|
||||||
pair_id,
|
pair_id,
|
||||||
pair_name,
|
pair_name,
|
||||||
action: Action::Failed(e.to_string()),
|
action,
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logger_ctdra::error(
|
||||||
|
"reconcile",
|
||||||
|
&format!("pair '{pair_name}' ({pair_id}): reconcile failed: {e}"),
|
||||||
|
);
|
||||||
|
ReconcileOutcome {
|
||||||
|
pair_id,
|
||||||
|
pair_name,
|
||||||
|
action: Action::Failed(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +166,13 @@ async fn reconcile_pair_inner(
|
|||||||
let _guard = lock::acquire(&pair.id).await?;
|
let _guard = lock::acquire(&pair.id).await?;
|
||||||
|
|
||||||
let active = target::active_side(pair);
|
let active = target::active_side(pair);
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}': local_reachable={local_reachable} cloud_reachable={cloud_reachable} active={:?}",
|
||||||
|
pair.id, active
|
||||||
|
),
|
||||||
|
);
|
||||||
cleanup_orphaned_mounts(pair, settings, active).await;
|
cleanup_orphaned_mounts(pair, settings, active).await;
|
||||||
|
|
||||||
if local_reachable {
|
if local_reachable {
|
||||||
@@ -182,7 +208,13 @@ async fn reconcile_pair_inner(
|
|||||||
fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> (bool, Option<Ipv4Addr>) {
|
fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> (bool, Option<Ipv4Addr>) {
|
||||||
match address::resolve_ip(&local.address, settings) {
|
match address::resolve_ip(&local.address, settings) {
|
||||||
Ok(ip) => (network::is_reachable(&ip.to_string()), Some(ip)),
|
Ok(ip) => (network::is_reachable(&ip.to_string()), Some(ip)),
|
||||||
Err(_) => (false, None),
|
Err(e) => {
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!("local address could not be resolved: {e}"),
|
||||||
|
);
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +283,15 @@ async fn switch_to(
|
|||||||
old_active: Option<Side>,
|
old_active: Option<Side>,
|
||||||
cached_local_ip: Option<Ipv4Addr>,
|
cached_local_ip: Option<Ipv4Addr>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
logger_ctdra::info(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}': switching to '{}' (previously active: {:?})",
|
||||||
|
pair.id,
|
||||||
|
new_side.as_str(),
|
||||||
|
old_active
|
||||||
|
),
|
||||||
|
);
|
||||||
mount_side(pair, settings, new_side, creds, cached_local_ip).await?;
|
mount_side(pair, settings, new_side, creds, cached_local_ip).await?;
|
||||||
|
|
||||||
// Falls das Aktivieren des Symlinks fehlschlägt, muss die gerade gemountete `new_side`
|
// Falls das Aktivieren des Symlinks fehlschlägt, muss die gerade gemountete `new_side`
|
||||||
@@ -280,6 +321,16 @@ async fn mount_side(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mount_target =
|
let mount_target =
|
||||||
target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?;
|
target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?;
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}' ({}): resolved source '{}' -> '{}'",
|
||||||
|
pair.id,
|
||||||
|
side.as_str(),
|
||||||
|
mount_target.source,
|
||||||
|
mount_target.mount_point.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
std::fs::create_dir_all(&mount_target.mount_point)
|
std::fs::create_dir_all(&mount_target.mount_point)
|
||||||
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
|
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
|
||||||
// Backing-Verzeichnis auf `owner_user` chownen (0700) - relevant vor allem für NFS, wo es
|
// Backing-Verzeichnis auf `owner_user` chownen (0700) - relevant vor allem für NFS, wo es
|
||||||
@@ -293,6 +344,15 @@ async fn mount_side(
|
|||||||
backend.check_available()?;
|
backend.check_available()?;
|
||||||
let cred = creds.get(&pair.id, side).await?;
|
let cred = creds.get(&pair.id, side).await?;
|
||||||
backend.prepare(&mount_target, cred.as_ref())?;
|
backend.prepare(&mount_target, cred.as_ref())?;
|
||||||
|
logger_ctdra::info(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}' ({}): mounting via {} ...",
|
||||||
|
pair.id,
|
||||||
|
side.as_str(),
|
||||||
|
backend.name()
|
||||||
|
),
|
||||||
|
);
|
||||||
backend.mount(&mount_target)
|
backend.mount(&mount_target)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +361,7 @@ async fn mount_side(
|
|||||||
/// ein leeres, ausgehängtes Backing-Verzeichnis) - der nächste `mount`/`watch`-Lauf räumt das
|
/// ein leeres, ausgehängtes Backing-Verzeichnis) - der nächste `mount`/`watch`-Lauf räumt das
|
||||||
/// beim erneuten Aktivieren automatisch auf.
|
/// beim erneuten Aktivieren automatisch auf.
|
||||||
pub async fn unmount_pair(pair: &DrivePair, settings: &GlobalSettings) -> Result<Action> {
|
pub async fn unmount_pair(pair: &DrivePair, settings: &GlobalSettings) -> Result<Action> {
|
||||||
|
logger_ctdra::info("reconcile", &format!("pair '{}': unmounting", pair.id));
|
||||||
let _guard = lock::acquire(&pair.id).await?;
|
let _guard = lock::acquire(&pair.id).await?;
|
||||||
|
|
||||||
// Beide Seiten werden unabhängig voneinander versucht - ein Fehler (auch ein
|
// Beide Seiten werden unabhängig voneinander versucht - ein Fehler (auch ein
|
||||||
@@ -363,7 +424,18 @@ async fn unmount_side(pair: &DrivePair, settings: &GlobalSettings, side: Side) -
|
|||||||
owner_user: pair.owner_user.clone(),
|
owner_user: pair.owner_user.clone(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mount::backend_for(target::side_kind(pair, side)).unmount(&mount_target)
|
let backend = mount::backend_for(target::side_kind(pair, side));
|
||||||
|
logger_ctdra::debug(
|
||||||
|
"reconcile",
|
||||||
|
&format!(
|
||||||
|
"pair '{}' ({}): unmounting via {} ({})",
|
||||||
|
pair.id,
|
||||||
|
side.as_str(),
|
||||||
|
backend.name(),
|
||||||
|
mount_target.mount_point.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
backend.unmount(&mount_target)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ pub fn install_cron(watch_interval_secs: u64) -> Result<CronInstallOutcome> {
|
|||||||
std::fs::set_permissions(CRON_D_PATH, std::fs::Permissions::from_mode(0o644))
|
std::fs::set_permissions(CRON_D_PATH, std::fs::Permissions::from_mode(0o644))
|
||||||
.map_err(|e| Error::io(CRON_D_PATH, e))?;
|
.map_err(|e| Error::io(CRON_D_PATH, e))?;
|
||||||
}
|
}
|
||||||
|
logger_ctdra::info("systemd", &format!("cron entry written to '{CRON_D_PATH}'"));
|
||||||
Ok(CronInstallOutcome::SystemFile(PathBuf::from(CRON_D_PATH)))
|
Ok(CronInstallOutcome::SystemFile(PathBuf::from(CRON_D_PATH)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +97,10 @@ pub fn uninstall_cron() -> Result<CronUninstallOutcome> {
|
|||||||
return Ok(CronUninstallOutcome::NotPresent);
|
return Ok(CronUninstallOutcome::NotPresent);
|
||||||
}
|
}
|
||||||
std::fs::remove_file(path).map_err(|e| Error::io(CRON_D_PATH, e))?;
|
std::fs::remove_file(path).map_err(|e| Error::io(CRON_D_PATH, e))?;
|
||||||
|
logger_ctdra::info(
|
||||||
|
"systemd",
|
||||||
|
&format!("cron entry removed from '{CRON_D_PATH}'"),
|
||||||
|
);
|
||||||
Ok(CronUninstallOutcome::Removed)
|
Ok(CronUninstallOutcome::Removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user