manta_shared/common/config/
mod.rs1use std::{
28 fs::{self, File},
29 io::{Read, Write},
30 path::PathBuf,
31};
32
33use crate::common::error::MantaError as Error;
34use config::Config;
35use directories::ProjectDirs;
36use toml_edit::DocumentMut;
37
38fn get_project_dirs() -> Result<ProjectDirs, Error> {
44 ProjectDirs::from(
45 "local", "cscs", "manta", )
49 .ok_or_else(|| {
50 Error::MissingField(
51 "Could not determine project directories \
52 (home directory may not be set)"
53 .to_string(),
54 )
55 })
56}
57
58pub fn get_default_config_path() -> Result<PathBuf, Error> {
68 Ok(PathBuf::from(get_project_dirs()?.config_dir()))
69}
70
71fn default_config_file(filename: &str) -> Result<PathBuf, Error> {
73 let mut path = get_default_config_path()?;
74 path.push(filename);
75 Ok(path)
76}
77
78pub fn get_default_manta_config_file_path() -> Result<PathBuf, Error> {
84 default_config_file("config.toml")
85}
86
87pub fn get_default_manta_cli_config_file_path() -> Result<PathBuf, Error> {
90 default_config_file("cli.toml")
91}
92
93pub fn get_default_manta_server_config_file_path() -> Result<PathBuf, Error> {
96 default_config_file("server.toml")
97}
98
99pub fn get_default_cache_path() -> Result<PathBuf, Error> {
102 Ok(PathBuf::from(get_project_dirs()?.cache_dir()))
103}
104
105pub fn read_config_toml() -> Result<(PathBuf, DocumentMut), Error> {
120 let path = get_cli_config_file_path()?;
121
122 tracing::debug!(
123 "Reading manta CLI configuration from {}",
124 path.to_string_lossy()
125 );
126
127 let content = fs::read_to_string(&path)?;
128
129 let doc = content.parse::<DocumentMut>()?;
130
131 Ok((path, doc))
132}
133
134pub fn write_config_toml(
147 path: &std::path::Path,
148 doc: &DocumentMut,
149) -> Result<(), Error> {
150 let mut file = std::fs::OpenOptions::new()
151 .write(true)
152 .truncate(true)
153 .open(path)?;
154
155 file.write_all(doc.to_string().as_bytes())?;
156 file.flush()?;
157
158 Ok(())
159}
160
161pub fn get_csm_root_cert_content(file_path: &str) -> Result<Vec<u8>, Error> {
175 let mut buf = Vec::new();
176 let root_cert_file_rslt = File::open(file_path);
177
178 let file_rslt = if root_cert_file_rslt.is_err() {
179 let mut config_path = get_default_config_path()?;
180 config_path.push(file_path);
181 File::open(config_path)
182 } else {
183 root_cert_file_rslt
184 };
185
186 match file_rslt {
187 Ok(mut file) => {
188 file.read_to_end(&mut buf)?;
189 Ok(buf)
190 }
191 Err(_) => Err(Error::NotFound(
192 "CA public root file could not be found".to_string(),
193 )),
194 }
195}
196
197pub fn get_cli_config_file_path() -> Result<PathBuf, Error> {
222 if let Ok(env_path) = std::env::var("MANTA_CLI_CONFIG") {
223 Ok(PathBuf::from(env_path))
224 } else {
225 get_default_manta_cli_config_file_path()
226 }
227}
228
229pub fn get_server_config_file_path() -> Result<PathBuf, Error> {
252 if let Ok(env_path) = std::env::var("MANTA_SERVER_CONFIG") {
253 Ok(PathBuf::from(env_path))
254 } else {
255 get_default_manta_server_config_file_path()
256 }
257}
258
259const CLI_CONFIG_SAMPLE: &str = r#"log = "info"
261# Optional: the active site (`X-Manta-Site` header). Omit it and pass
262# `--site <name>` per invocation instead; the server validates the name.
263site = "<site_name>"
264manta_server_url = "https://manta-server.example.com:8443"
265
266# Timeout knobs. Values shown are the built-in defaults — delete a
267# line to fall back to the default, or change the value to override.
268#
269# Per-request HTTP timeout reaching `manta_server_url` (seconds).
270# Default 300 caps REST calls. Streams (SSE log tail, WS console)
271# are unlimited when this is absent; setting it caps streams too.
272request_timeout_secs = 300
273#
274# `manta power on/off/reset`: poll interval (seconds) and max
275# attempts before giving up. 300 × 3 s = 15 min total wait.
276power_poll_interval_secs = 3
277power_max_poll_attempts = 300
278#
279# `manta apply sat-file`: poll interval, overall hard cap, and
280# cap on consecutive "session not yet visible" responses (seconds).
281sat_file_poll_interval_secs = 10
282sat_file_poll_budget_secs = 14400 # 4 hours
283sat_file_not_visible_budget_secs = 300 # 5 minutes
284
285[sites.<site_name>]
286backend = "csm" # or "ochami"
287shasta_base_url = "https://api.example.com"
288root_ca_cert_file = "alps_root_cert.pem"
289"#;
290
291const CLI_CONFIG_MIGRATION: &str = "\
293Migration from ~/.config/manta/config.toml:
294 copy these fields verbatim: log, site, auditor, sites
295 add CLI-only (now required): manta_server_url = \"https://...\"
296 (CLI talks only to the manta server)
297 drop (no longer recognised): sites.<X>.manta_server_url, audit_file
298 do not copy (server-only fields): the [server] section belongs in
299 server.toml, not cli.toml";
300
301const SERVER_CONFIG_SAMPLE: &str = r#"log = "info"
303
304[server]
305listen_address = "0.0.0.0"
306port = 8443
307cert = "/path/to/server.crt"
308key = "/path/to/server.key"
309console_inactivity_timeout_secs = 1800
310auth_rate_limit_per_minute = 60 # per source IP for /auth/*; omit to disable
311# Values shown for the two timeout knobs are the built-in defaults —
312# delete a line to fall back to the default, or change to override.
313request_timeout_secs = 300 # global per-route timeout; returns 408 on expiry
314shutdown_grace_period_secs = 30 # drain window after SIGTERM / Ctrl+C; matches k8s terminationGracePeriodSeconds
315# allow_http = false # opt in to plain-HTTP listen when no cert/key is set
316 # (e.g. TLS terminated upstream). Default fail-closed.
317# Filesystem root for POST /migrate/{backup,restore}. Required for those
318# endpoints to work — the server will reject migrate requests with 400
319# while this is unset. Must be an absolute path to an existing directory.
320# migrate_backup_root = "/var/lib/manta/migrate"
321
322# [auditor.kafka] # optional: enable Kafka audit emission
323# brokers = ["kafka.example.com:9092"]
324# topic = "manta-audit"
325# message_timeout_ms = 5000 # librdkafka per-message delivery deadline; default 5000
326# delivery_wait_secs = 0 # how long produce_message blocks; 0 = fire-and-forget (default)
327
328[sites.<site_name>]
329backend = "csm"
330shasta_base_url = "https://api.example.com"
331root_ca_cert_file = "/path/to/alps_root_cert.pem"
332"#;
333
334const SERVER_CONFIG_MIGRATION: &str = "\
336Migration from ~/.config/manta/config.toml:
337 copy these fields verbatim: log, auditor, sites
338 add new [server] section: listen_address, port, cert, key,
339 console_inactivity_timeout_secs
340 drop (CLI-only): site, hsm_group, manta_server_url
341 drop (no longer recognised): sites.<X>.manta_server_url, audit_file";
342
343fn missing_config_message(
344 binary: &str,
345 expected_path: &std::path::Path,
346 sample: &str,
347 migration: &str,
348) -> String {
349 let legacy_exists = get_default_manta_config_file_path()
350 .map(|p| p.exists())
351 .unwrap_or(false);
352 let mut msg = format!(
353 "{binary} configuration file '{}' not found.\n\nMinimal example:\n\n{sample}",
354 expected_path.to_string_lossy()
355 );
356 if legacy_exists {
357 msg.push('\n');
358 msg.push_str(migration);
359 }
360 msg
361}
362
363fn load_config(
369 label: &str,
370 path: PathBuf,
371 sample: &str,
372 migration: &str,
373) -> Result<Config, Error> {
374 if !path.exists() {
375 return Err(Error::NotFound(missing_config_message(
376 label, &path, sample, migration,
377 )));
378 }
379 let path_str = path.to_str().ok_or_else(|| {
380 Error::MissingField(format!(
381 "{label} configuration file path contains invalid UTF-8"
382 ))
383 })?;
384 ::config::Config::builder()
385 .add_source(::config::File::new(path_str, ::config::FileFormat::Toml))
386 .add_source(
387 ::config::Environment::with_prefix("MANTA")
388 .try_parsing(true)
389 .prefix_separator("_"),
390 )
391 .build()
392 .map_err(Error::ConfigError)
393}
394
395pub fn get_cli_configuration() -> Result<Config, Error> {
417 load_config(
418 "CLI",
419 get_cli_config_file_path()?,
420 CLI_CONFIG_SAMPLE,
421 CLI_CONFIG_MIGRATION,
422 )
423}
424
425pub fn get_server_configuration() -> Result<Config, Error> {
446 load_config(
447 "Server",
448 get_server_config_file_path()?,
449 SERVER_CONFIG_SAMPLE,
450 SERVER_CONFIG_MIGRATION,
451 )
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use std::io::Write;
458 use std::sync::Mutex;
459 use tempfile::NamedTempFile;
460
461 static ENV_LOCK: Mutex<()> = Mutex::new(());
465
466 struct EnvGuard(&'static str);
470 impl EnvGuard {
471 fn set(key: &'static str, value: &str) -> Self {
472 unsafe { std::env::set_var(key, value) };
474 Self(key)
475 }
476 }
477 impl Drop for EnvGuard {
478 fn drop(&mut self) {
479 unsafe { std::env::remove_var(self.0) };
480 }
481 }
482
483 fn write_tmp_toml(content: &str) -> NamedTempFile {
484 let mut f = NamedTempFile::new().expect("tempfile");
485 f.write_all(content.as_bytes()).expect("write tempfile");
486 f
487 }
488
489 #[test]
490 fn default_cli_config_path_ends_with_cli_toml() {
491 let path = get_default_manta_cli_config_file_path().unwrap();
492 assert_eq!(path.file_name().unwrap(), "cli.toml");
493 }
494
495 #[test]
496 fn default_server_config_path_ends_with_server_toml() {
497 let path = get_default_manta_server_config_file_path().unwrap();
498 assert_eq!(path.file_name().unwrap(), "server.toml");
499 }
500
501 #[test]
502 fn default_legacy_config_path_ends_with_config_toml() {
503 let path = get_default_manta_config_file_path().unwrap();
504 assert_eq!(path.file_name().unwrap(), "config.toml");
505 }
506
507 #[test]
508 fn cli_and_server_default_paths_share_parent() {
509 let cli = get_default_manta_cli_config_file_path().unwrap();
510 let server = get_default_manta_server_config_file_path().unwrap();
511 assert_eq!(cli.parent(), server.parent());
512 }
513
514 #[test]
515 fn cli_config_file_path_honors_env_var() {
516 let _g = ENV_LOCK.lock().unwrap();
517 let _e = EnvGuard::set("MANTA_CLI_CONFIG", "/tmp/custom-cli.toml");
518 let path = get_cli_config_file_path().unwrap();
519 assert_eq!(path, PathBuf::from("/tmp/custom-cli.toml"));
520 }
521
522 #[test]
523 fn server_config_file_path_honors_env_var() {
524 let _g = ENV_LOCK.lock().unwrap();
525 let _e = EnvGuard::set("MANTA_SERVER_CONFIG", "/tmp/custom-server.toml");
526 let path = get_server_config_file_path().unwrap();
527 assert_eq!(path, PathBuf::from("/tmp/custom-server.toml"));
528 }
529
530 #[test]
531 fn cli_configuration_with_missing_file_returns_notfound() {
532 let _g = ENV_LOCK.lock().unwrap();
533 let _e = EnvGuard::set(
534 "MANTA_CLI_CONFIG",
535 "/nonexistent-dir/definitely-not-here.toml",
536 );
537 let err = get_cli_configuration().unwrap_err();
538 match err {
539 Error::NotFound(msg) => {
540 assert!(
541 msg.contains("CLI configuration file"),
542 "expected helpful NotFound message, got: {msg}"
543 );
544 assert!(
545 msg.contains("Minimal example"),
546 "expected sample TOML in message"
547 );
548 }
549 other => panic!("expected NotFound, got {other:?}"),
550 }
551 }
552
553 #[test]
554 fn cli_configuration_with_malformed_toml_returns_config_error() {
555 let _g = ENV_LOCK.lock().unwrap();
556 let bad = write_tmp_toml("this is = not [valid toml");
557 let _e = EnvGuard::set("MANTA_CLI_CONFIG", bad.path().to_str().unwrap());
558 let err = get_cli_configuration().unwrap_err();
559 assert!(
560 matches!(err, Error::ConfigError(_)),
561 "expected ConfigError variant, got {err:?}"
562 );
563 }
564
565 #[test]
566 fn cli_configuration_loads_valid_toml_and_env_var_overrides_file() {
567 let _g = ENV_LOCK.lock().unwrap();
568 let good = write_tmp_toml(
569 r#"log = "info"
570site = "alps"
571manta_server_url = "https://example:8443"
572"#,
573 );
574 let _path =
575 EnvGuard::set("MANTA_CLI_CONFIG", good.path().to_str().unwrap());
576
577 let cfg = get_cli_configuration().unwrap();
578 assert_eq!(cfg.get_string("log").unwrap(), "info");
579 assert_eq!(cfg.get_string("site").unwrap(), "alps");
580 drop(cfg);
581
582 let _override = EnvGuard::set("MANTA_LOG", "trace");
585 let cfg = get_cli_configuration().unwrap();
586 assert_eq!(
587 cfg.get_string("log").unwrap(),
588 "trace",
589 "env var should override file value"
590 );
591 }
592
593 #[test]
594 fn server_configuration_with_missing_file_returns_notfound() {
595 let _g = ENV_LOCK.lock().unwrap();
596 let _e = EnvGuard::set(
597 "MANTA_SERVER_CONFIG",
598 "/nonexistent-dir/missing-server.toml",
599 );
600 let err = get_server_configuration().unwrap_err();
601 match err {
602 Error::NotFound(msg) => {
603 assert!(
604 msg.contains("Server configuration file"),
605 "expected helpful NotFound message, got: {msg}"
606 );
607 }
608 other => panic!("expected NotFound, got {other:?}"),
609 }
610 }
611
612 #[test]
613 fn server_configuration_loads_valid_toml() {
614 let _g = ENV_LOCK.lock().unwrap();
615 let good = write_tmp_toml(
616 r#"log = "info"
617
618[server]
619listen_address = "0.0.0.0"
620port = 8443
621cert = "/etc/manta/cert.pem"
622key = "/etc/manta/key.pem"
623"#,
624 );
625 let _e =
626 EnvGuard::set("MANTA_SERVER_CONFIG", good.path().to_str().unwrap());
627 let cfg = get_server_configuration().unwrap();
628 assert_eq!(cfg.get_string("server.listen_address").unwrap(), "0.0.0.0");
629 assert_eq!(cfg.get_int("server.port").unwrap(), 8443);
630 }
631}