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: also produce the audit trail to Kafka
323# brokers = ["kafka.example.com:9092"]
324# topic = "manta-audit"
325# message_timeout_ms = 5000 # how long a queued event retries delivery; default 5000
326# [auditor.kafka.properties] # optional librdkafka passthrough (TLS/SASL settings)
327# "security.protocol" = "SASL_SSL"
328
329[sites.<site_name>]
330backend = "csm"
331shasta_base_url = "https://api.example.com"
332root_ca_cert_file = "/path/to/alps_root_cert.pem"
333"#;
334
335const SERVER_CONFIG_MIGRATION: &str = "\
337Migration from ~/.config/manta/config.toml:
338 copy these fields verbatim: log, auditor, sites
339 add new [server] section: listen_address, port, cert, key,
340 console_inactivity_timeout_secs
341 drop (CLI-only): site, hsm_group, manta_server_url
342 drop (no longer recognised): sites.<X>.manta_server_url, audit_file";
343
344fn missing_config_message(
345 binary: &str,
346 expected_path: &std::path::Path,
347 sample: &str,
348 migration: &str,
349) -> String {
350 let legacy_exists = get_default_manta_config_file_path()
351 .map(|p| p.exists())
352 .unwrap_or(false);
353 let mut msg = format!(
354 "{binary} configuration file '{}' not found.\n\nMinimal example:\n\n{sample}",
355 expected_path.to_string_lossy()
356 );
357 if legacy_exists {
358 msg.push('\n');
359 msg.push_str(migration);
360 }
361 msg
362}
363
364fn load_config(
370 label: &str,
371 path: PathBuf,
372 sample: &str,
373 migration: &str,
374) -> Result<Config, Error> {
375 if !path.exists() {
376 return Err(Error::NotFound(missing_config_message(
377 label, &path, sample, migration,
378 )));
379 }
380 let path_str = path.to_str().ok_or_else(|| {
381 Error::MissingField(format!(
382 "{label} configuration file path contains invalid UTF-8"
383 ))
384 })?;
385 ::config::Config::builder()
386 .add_source(::config::File::new(path_str, ::config::FileFormat::Toml))
387 .add_source(
388 ::config::Environment::with_prefix("MANTA")
389 .try_parsing(true)
390 .prefix_separator("_"),
391 )
392 .build()
393 .map_err(Error::ConfigError)
394}
395
396pub fn get_cli_configuration() -> Result<Config, Error> {
418 load_config(
419 "CLI",
420 get_cli_config_file_path()?,
421 CLI_CONFIG_SAMPLE,
422 CLI_CONFIG_MIGRATION,
423 )
424}
425
426pub fn get_server_configuration() -> Result<Config, Error> {
447 load_config(
448 "Server",
449 get_server_config_file_path()?,
450 SERVER_CONFIG_SAMPLE,
451 SERVER_CONFIG_MIGRATION,
452 )
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use std::io::Write;
459 use std::sync::Mutex;
460 use tempfile::NamedTempFile;
461
462 static ENV_LOCK: Mutex<()> = Mutex::new(());
466
467 struct EnvGuard(&'static str);
471 impl EnvGuard {
472 fn set(key: &'static str, value: &str) -> Self {
473 unsafe { std::env::set_var(key, value) };
475 Self(key)
476 }
477 }
478 impl Drop for EnvGuard {
479 fn drop(&mut self) {
480 unsafe { std::env::remove_var(self.0) };
481 }
482 }
483
484 fn write_tmp_toml(content: &str) -> NamedTempFile {
485 let mut f = NamedTempFile::new().expect("tempfile");
486 f.write_all(content.as_bytes()).expect("write tempfile");
487 f
488 }
489
490 #[test]
491 fn default_cli_config_path_ends_with_cli_toml() {
492 let path = get_default_manta_cli_config_file_path().unwrap();
493 assert_eq!(path.file_name().unwrap(), "cli.toml");
494 }
495
496 #[test]
497 fn default_server_config_path_ends_with_server_toml() {
498 let path = get_default_manta_server_config_file_path().unwrap();
499 assert_eq!(path.file_name().unwrap(), "server.toml");
500 }
501
502 #[test]
503 fn default_legacy_config_path_ends_with_config_toml() {
504 let path = get_default_manta_config_file_path().unwrap();
505 assert_eq!(path.file_name().unwrap(), "config.toml");
506 }
507
508 #[test]
509 fn cli_and_server_default_paths_share_parent() {
510 let cli = get_default_manta_cli_config_file_path().unwrap();
511 let server = get_default_manta_server_config_file_path().unwrap();
512 assert_eq!(cli.parent(), server.parent());
513 }
514
515 #[test]
516 fn cli_config_file_path_honors_env_var() {
517 let _g = ENV_LOCK.lock().unwrap();
518 let _e = EnvGuard::set("MANTA_CLI_CONFIG", "/tmp/custom-cli.toml");
519 let path = get_cli_config_file_path().unwrap();
520 assert_eq!(path, PathBuf::from("/tmp/custom-cli.toml"));
521 }
522
523 #[test]
524 fn server_config_file_path_honors_env_var() {
525 let _g = ENV_LOCK.lock().unwrap();
526 let _e = EnvGuard::set("MANTA_SERVER_CONFIG", "/tmp/custom-server.toml");
527 let path = get_server_config_file_path().unwrap();
528 assert_eq!(path, PathBuf::from("/tmp/custom-server.toml"));
529 }
530
531 #[test]
532 fn cli_configuration_with_missing_file_returns_notfound() {
533 let _g = ENV_LOCK.lock().unwrap();
534 let _e = EnvGuard::set(
535 "MANTA_CLI_CONFIG",
536 "/nonexistent-dir/definitely-not-here.toml",
537 );
538 let err = get_cli_configuration().unwrap_err();
539 match err {
540 Error::NotFound(msg) => {
541 assert!(
542 msg.contains("CLI configuration file"),
543 "expected helpful NotFound message, got: {msg}"
544 );
545 assert!(
546 msg.contains("Minimal example"),
547 "expected sample TOML in message"
548 );
549 }
550 other => panic!("expected NotFound, got {other:?}"),
551 }
552 }
553
554 #[test]
555 fn cli_configuration_with_malformed_toml_returns_config_error() {
556 let _g = ENV_LOCK.lock().unwrap();
557 let bad = write_tmp_toml("this is = not [valid toml");
558 let _e = EnvGuard::set("MANTA_CLI_CONFIG", bad.path().to_str().unwrap());
559 let err = get_cli_configuration().unwrap_err();
560 assert!(
561 matches!(err, Error::ConfigError(_)),
562 "expected ConfigError variant, got {err:?}"
563 );
564 }
565
566 #[test]
567 fn cli_configuration_loads_valid_toml_and_env_var_overrides_file() {
568 let _g = ENV_LOCK.lock().unwrap();
569 let good = write_tmp_toml(
570 r#"log = "info"
571site = "alps"
572manta_server_url = "https://example:8443"
573"#,
574 );
575 let _path =
576 EnvGuard::set("MANTA_CLI_CONFIG", good.path().to_str().unwrap());
577
578 let cfg = get_cli_configuration().unwrap();
579 assert_eq!(cfg.get_string("log").unwrap(), "info");
580 assert_eq!(cfg.get_string("site").unwrap(), "alps");
581 drop(cfg);
582
583 let _override = EnvGuard::set("MANTA_LOG", "trace");
586 let cfg = get_cli_configuration().unwrap();
587 assert_eq!(
588 cfg.get_string("log").unwrap(),
589 "trace",
590 "env var should override file value"
591 );
592 }
593
594 #[test]
595 fn server_configuration_with_missing_file_returns_notfound() {
596 let _g = ENV_LOCK.lock().unwrap();
597 let _e = EnvGuard::set(
598 "MANTA_SERVER_CONFIG",
599 "/nonexistent-dir/missing-server.toml",
600 );
601 let err = get_server_configuration().unwrap_err();
602 match err {
603 Error::NotFound(msg) => {
604 assert!(
605 msg.contains("Server configuration file"),
606 "expected helpful NotFound message, got: {msg}"
607 );
608 }
609 other => panic!("expected NotFound, got {other:?}"),
610 }
611 }
612
613 #[test]
614 fn server_configuration_loads_valid_toml() {
615 let _g = ENV_LOCK.lock().unwrap();
616 let good = write_tmp_toml(
617 r#"log = "info"
618
619[server]
620listen_address = "0.0.0.0"
621port = 8443
622cert = "/etc/manta/cert.pem"
623key = "/etc/manta/key.pem"
624"#,
625 );
626 let _e =
627 EnvGuard::set("MANTA_SERVER_CONFIG", good.path().to_str().unwrap());
628 let cfg = get_server_configuration().unwrap();
629 assert_eq!(cfg.get_string("server.listen_address").unwrap(), "0.0.0.0");
630 assert_eq!(cfg.get_int("server.port").unwrap(), 8443);
631 }
632}