Skip to main content

manta_shared/common/config/
types.rs

1//! Typed config-file schemas for `cli.toml` and `server.toml`.
2//!
3//! See [`CliConfiguration`] and [`ServerConfiguration`] for the
4//! top-level shapes. The loaders that materialise these from disk
5//! live in the parent module ([`super::get_cli_configuration`],
6//! [`super::get_server_configuration`]).
7
8use std::collections::HashMap;
9
10use crate::common::audit::Auditor;
11
12use manta_backend_dispatcher::types::K8sDetails;
13use serde::{Deserialize, Serialize};
14
15/// Which backend API this site speaks.
16#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
17#[serde(rename_all = "lowercase")]
18pub enum BackendTechnology {
19  /// HPE Cray System Management (CSM) backend.
20  Csm,
21  /// OpenCHAMI backend.
22  Ochami,
23}
24
25impl BackendTechnology {
26  /// Return the lowercase string expected by `StaticBackendDispatcher::new`.
27  pub fn as_str(&self) -> &'static str {
28    match self {
29      Self::Csm => "csm",
30      Self::Ochami => "ochami",
31    }
32  }
33}
34
35#[derive(Serialize, Deserialize, Debug)]
36/// Connection details for a single ALPS site (CSM or OCHAMI instance).
37///
38/// The Vault URL used by handlers requiring vault (sat-file, session,
39/// console, logs) is derived at startup from
40/// `[sites.X.k8s.authentication.vault] base_url`. The vault secret path
41/// is derived from a hard-coded prefix and the site name. Neither is
42/// configured here.
43pub struct Site {
44  /// Which backend implementation this site uses (`csm` or `ochami`).
45  pub backend: BackendTechnology,
46  /// Optional per-site SOCKS5 proxy URL used by every outbound HTTP
47  /// request to this site's backend. `None` means direct connection.
48  pub socks5_proxy: Option<String>,
49  /// Base URL of the backend API (e.g. `https://api.alps.cscs.ch`).
50  pub shasta_base_url: String,
51  /// Optional Kubernetes connection details, required by handlers
52  /// that stream CFS session logs or attach to consoles.
53  pub k8s: Option<K8sDetails>,
54  /// Path (absolute or relative to the config dir) of the backend's
55  /// root CA certificate, used to verify TLS to `shasta_base_url`.
56  pub root_ca_cert_file: String,
57}
58
59/// Top-level configuration for the `manta-cli` binary. Persisted as TOML
60/// under `~/.config/manta/cli.toml`. Carries only the fields the CLI uses
61/// — every backend connection detail (per-site URLs, TLS certs, vault,
62/// k8s, per-site SOCKS proxies) lives in `ServerConfiguration`. The CLI
63/// only knows about the *one* manta-server it talks to.
64#[derive(Serialize, Deserialize, Debug)]
65pub struct CliConfiguration {
66  /// `EnvFilter` directive string for the tracing subscriber
67  /// (e.g. `"info"`, `"manta=debug,hyper=warn"`).
68  pub log: String,
69  /// Path to the local file the CLI appends audit lines to.
70  pub audit_file: String,
71  /// Active site name, sent as the `X-Manta-Site` header on every
72  /// request to manta-server. Overridable per-invocation with `--site`.
73  /// The server validates that the name matches one of its configured
74  /// sites; the CLI does no local validation.
75  pub site: String,
76  /// Default HSM group threaded into commands that accept
77  /// `--hsm-group` when none is supplied on the command line.
78  pub parent_hsm_group: String,
79  /// URL of the manta HTTP server this CLI talks to. Required — the CLI
80  /// no longer calls CSM/OCHAMI backends directly; every operation
81  /// (including auth) is forwarded through `manta-server`.
82  pub manta_server_url: String,
83  /// Optional SOCKS5 proxy used to reach `manta_server_url`. Per-site
84  /// proxying for backend traffic is the server's concern.
85  pub socks5_proxy: Option<String>,
86  /// Optional per-request HTTP timeout, in seconds. When set, the
87  /// reqwest client used to reach `manta_server_url` is built with
88  /// `.timeout(Duration::from_secs(n))`; when `None` (the default),
89  /// reqwest applies no per-request timeout — long-running calls
90  /// (e.g. `POST /power`) hang until the server responds or the
91  /// underlying connection drops. Set this to match the server's
92  /// longest legitimate response time when running through a SOCKS5
93  /// tunnel or proxy that silently drops idle connections.
94  #[serde(default)]
95  pub request_timeout_secs: Option<u64>,
96  /// Optional Kafka audit forwarder. When `None`, the CLI emits no
97  /// audit messages.
98  pub auditor: Option<Auditor>,
99}
100
101/// Server-only settings — TLS, listen address, console behaviour. Lives
102/// under `[server]` in `server.toml`.
103#[derive(Serialize, Deserialize, Debug)]
104pub struct ServerSettings {
105  /// TCP listen address (e.g. "0.0.0.0"). When omitted from config
106  /// **and** no `--listen-address` flag is supplied, the server falls
107  /// back to `"0.0.0.0"`.
108  #[serde(default)]
109  pub listen_address: Option<String>,
110  /// TCP port. When omitted from config **and** no `--port` flag is
111  /// supplied, the effective default depends on whether TLS is
112  /// configured: `8443` if both `cert` and `key` are present (HTTPS),
113  /// otherwise `8080` (plain HTTP). See
114  /// [`ServerSettings::default_port`].
115  #[serde(default)]
116  pub port: Option<u16>,
117  /// Path to the TLS certificate (PEM).
118  pub cert: Option<String>,
119  /// Path to the TLS private key (PEM).
120  pub key: Option<String>,
121  /// How long a node-console WebSocket stays open without activity
122  /// before the server tears it down.
123  pub console_inactivity_timeout_secs: u64,
124  /// Per-source-IP rate limit for the `/api/v1/auth/*` endpoints,
125  /// in requests per minute. `None` disables in-process rate limiting
126  /// (operators are then expected to enforce it at the reverse proxy).
127  pub auth_rate_limit_per_minute: Option<u32>,
128  /// Global request timeout applied to every HTTP route, in seconds.
129  /// When this elapses the server returns `408 REQUEST_TIMEOUT`. All
130  /// long-running work (e.g. power transitions) now runs CLI-side,
131  /// so no endpoint needs more than the default.
132  #[serde(default = "default_request_timeout_secs")]
133  pub request_timeout_secs: u64,
134}
135
136impl ServerSettings {
137  /// Effective default listen address when neither config nor CLI flag
138  /// supplies one: bind on all interfaces.
139  pub const DEFAULT_LISTEN_ADDRESS: &'static str = "0.0.0.0";
140
141  /// Effective default port when neither config nor CLI flag supplies
142  /// one. `8443` for the HTTPS path (cert + key both present), `8080`
143  /// for plain HTTP — the latter is the typical dev / sidecar setup
144  /// where TLS is terminated upstream.
145  pub fn default_port(has_tls: bool) -> u16 {
146    if has_tls { 8443 } else { 8080 }
147  }
148}
149
150/// Default global request timeout — 60s. Matches the historical
151/// hardcoded value.
152fn default_request_timeout_secs() -> u64 {
153  60
154}
155
156/// Top-level configuration for the `manta-server` binary. Persisted as
157/// TOML under `~/.config/manta/server.toml`. Has no notion of an "active"
158/// site — the server hosts every configured site simultaneously and
159/// clients select per-request via the `X-Manta-Site` header.
160#[derive(Serialize, Deserialize, Debug)]
161pub struct ServerConfiguration {
162  /// `EnvFilter` directive for the tracing subscriber.
163  pub log: String,
164  /// Path to the local file the server appends audit lines to.
165  pub audit_file: String,
166  /// Network / TLS / console / rate-limit knobs for the HTTPS server.
167  pub server: ServerSettings,
168  /// Per-site backend connection details, keyed by site name. The
169  /// `X-Manta-Site` header on each request picks which one to route to.
170  pub sites: HashMap<String, Site>,
171  /// Optional Kafka audit forwarder (typically used for `/auth/*`
172  /// attempts). When `None`, the server emits no audit messages.
173  pub auditor: Option<Auditor>,
174}
175
176#[cfg(test)]
177mod tests {
178  use super::*;
179
180  #[test]
181  fn site_deserialize_missing_backend_fails() {
182    let bad_toml = r#"
183      shasta_base_url = "https://api.example.com"
184      root_ca_cert_file = "cert.pem"
185      # missing backend
186    "#;
187    let result = toml::from_str::<Site>(bad_toml);
188    assert!(result.is_err());
189  }
190
191  #[test]
192  fn backend_technology_as_str() {
193    assert_eq!(BackendTechnology::Csm.as_str(), "csm");
194    assert_eq!(BackendTechnology::Ochami.as_str(), "ochami");
195  }
196
197  #[test]
198  fn backend_technology_roundtrip_toml() {
199    // Verify TOML serializes as lowercase "csm" / "ochami"
200    #[derive(Serialize, Deserialize)]
201    struct Wrapper {
202      backend: BackendTechnology,
203    }
204    let w = Wrapper {
205      backend: BackendTechnology::Csm,
206    };
207    let s = toml::to_string(&w).unwrap();
208    assert!(s.contains("\"csm\"") || s.contains("csm"));
209    let parsed: Wrapper = toml::from_str(&s).unwrap();
210    assert_eq!(parsed.backend, BackendTechnology::Csm);
211  }
212
213  fn make_minimal_site() -> Site {
214    Site {
215      backend: BackendTechnology::Csm,
216      socks5_proxy: None,
217      shasta_base_url: "https://api.example.com".to_string(),
218      k8s: None,
219      root_ca_cert_file: "cert.pem".to_string(),
220    }
221  }
222
223  #[test]
224  fn cli_configuration_roundtrip_toml_minimal() {
225    let cfg = CliConfiguration {
226      log: "info".to_string(),
227      audit_file: "/tmp/cli-audit.log".to_string(),
228      site: "alps".to_string(),
229      parent_hsm_group: "nodes_free".to_string(),
230      manta_server_url: "https://manta-server.cscs.ch:8443".to_string(),
231      socks5_proxy: Some("socks5h://127.0.0.1:1080".to_string()),
232      request_timeout_secs: None,
233      auditor: None,
234    };
235    let toml_str = toml::to_string(&cfg).unwrap();
236    let parsed: CliConfiguration = toml::from_str(&toml_str).unwrap();
237    assert_eq!(parsed.site, "alps");
238    assert_eq!(parsed.parent_hsm_group, "nodes_free");
239    assert_eq!(parsed.manta_server_url, "https://manta-server.cscs.ch:8443");
240    assert_eq!(
241      parsed.socks5_proxy.as_deref(),
242      Some("socks5h://127.0.0.1:1080")
243    );
244  }
245
246  #[test]
247  fn cli_configuration_socks5_proxy_optional() {
248    let toml_str = r#"
249      log = "info"
250      audit_file = "/tmp/cli-audit.log"
251      site = "alps"
252      parent_hsm_group = ""
253      manta_server_url = "https://manta-server.cscs.ch:8443"
254    "#;
255    let parsed: CliConfiguration = toml::from_str(toml_str).unwrap();
256    assert!(parsed.socks5_proxy.is_none());
257  }
258
259  #[test]
260  fn cli_configuration_missing_manta_server_url_fails() {
261    let bad_toml = r#"
262      log = "info"
263      audit_file = "/tmp/cli-audit.log"
264      site = "alps"
265      parent_hsm_group = ""
266      # missing manta_server_url
267    "#;
268    let result = toml::from_str::<CliConfiguration>(bad_toml);
269    assert!(result.is_err());
270  }
271
272  #[test]
273  fn server_configuration_roundtrip_toml_minimal() {
274    let mut sites = HashMap::new();
275    sites.insert("alps".to_string(), make_minimal_site());
276    let cfg = ServerConfiguration {
277      log: "info".to_string(),
278      audit_file: "/var/log/manta/server-audit.log".to_string(),
279      server: ServerSettings {
280        listen_address: Some("0.0.0.0".to_string()),
281        port: Some(8443),
282        cert: Some("/etc/manta/tls/server.crt".to_string()),
283        key: Some("/etc/manta/tls/server.key".to_string()),
284        console_inactivity_timeout_secs: 1800,
285        auth_rate_limit_per_minute: Some(60),
286        request_timeout_secs: 60,
287      },
288      sites,
289      auditor: None,
290    };
291    let toml_str = toml::to_string(&cfg).unwrap();
292    let parsed: ServerConfiguration = toml::from_str(&toml_str).unwrap();
293    assert_eq!(parsed.server.port, Some(8443));
294    assert_eq!(parsed.server.listen_address.as_deref(), Some("0.0.0.0"));
295    assert_eq!(parsed.server.console_inactivity_timeout_secs, 1800);
296    assert_eq!(parsed.server.request_timeout_secs, 60);
297    assert_eq!(
298      parsed.server.cert.as_deref(),
299      Some("/etc/manta/tls/server.crt")
300    );
301  }
302
303  /// Default port helper: 8443 when TLS is configured, 8080
304  /// otherwise. Used by `manta-server::main` when no `port` is set
305  /// in config or on the CLI.
306  #[test]
307  fn server_settings_default_port_depends_on_tls() {
308    assert_eq!(ServerSettings::default_port(true), 8443);
309    assert_eq!(ServerSettings::default_port(false), 8080);
310  }
311
312  /// power_timeout_secs is gone — confirm the surrounding
313  /// timeout-related fields still default correctly when the only
314  /// remaining knob is absent.
315  #[test]
316  fn server_settings_request_timeout_secs_defaults_to_60() {
317    let toml_str = r#"
318      listen_address = "0.0.0.0"
319      port = 8443
320      console_inactivity_timeout_secs = 1800
321    "#;
322    let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
323    assert_eq!(parsed.request_timeout_secs, 60);
324  }
325
326  /// `[server]` block with neither `listen_address` nor `port`
327  /// supplied — both fields deserialise as `None`, leaving the
328  /// effective values to be filled in at startup time. Confirms the
329  /// schema-level back-compat for the new defaults.
330  #[test]
331  fn server_settings_listen_address_and_port_default_to_none() {
332    let toml_str = r#"
333      console_inactivity_timeout_secs = 1800
334    "#;
335    let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
336    assert!(parsed.listen_address.is_none());
337    assert!(parsed.port.is_none());
338  }
339
340  /// Existing server.toml files that pre-date the request_timeout
341  /// field must keep working — the field falls back to its default.
342  #[test]
343  fn server_settings_request_timeout_field_defaults_when_omitted() {
344    let toml_str = r#"
345      listen_address = "0.0.0.0"
346      port = 8443
347      console_inactivity_timeout_secs = 1800
348    "#;
349    let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
350    assert_eq!(parsed.request_timeout_secs, 60);
351  }
352
353  #[test]
354  fn server_configuration_deserialize_missing_server_section_fails() {
355    let bad_toml = r#"
356      log = "info"
357      audit_file = "/tmp/server.log"
358      [sites]
359    "#;
360    let result = toml::from_str::<ServerConfiguration>(bad_toml);
361    assert!(result.is_err());
362  }
363
364  #[test]
365  fn server_settings_optional_tls_paths() {
366    // TLS cert/key are optional in the schema — flags can supply them
367    // at runtime when the config omits them.
368    let toml_str = r#"
369      listen_address = "0.0.0.0"
370      port = 8443
371      console_inactivity_timeout_secs = 1800
372    "#;
373    let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
374    assert!(parsed.cert.is_none());
375    assert!(parsed.key.is_none());
376  }
377}