manta_server/config.rs
1//! Typed schema for `server.toml`.
2//!
3//! The untyped `config::Config` is loaded by
4//! [`manta_shared::common::config::get_server_configuration`]; this
5//! module owns the typed deserialisation target.
6//!
7//! The shape on disk is:
8//!
9//! ```toml
10//! log = "info"
11//!
12//! [server]
13//! listen_address = "0.0.0.0"
14//! port = 8443
15//! cert = "/etc/manta/tls/server.crt"
16//! key = "/etc/manta/tls/server.key"
17//! console_inactivity_timeout_secs = 1800
18//! auth_rate_limit_per_minute = 60
19//!
20//! [sites.alps]
21//! backend = "csm"
22//! shasta_base_url = "https://api.alps.cscs.ch"
23//! root_ca_cert_file = "/etc/manta/alps-ca.pem"
24//! ```
25//!
26//! [`ServerConfiguration`] is the top-level type; [`ServerSettings`]
27//! and [`Site`] are its nested sections. There is no notion of an
28//! "active" site — the server hosts every entry in `[sites.*]`
29//! simultaneously and clients pick one per request via the
30//! `X-Manta-Site` header.
31
32use std::collections::HashMap;
33
34use crate::server::common::audit::Auditor;
35use manta_backend_dispatcher::types::K8sDetails;
36use serde::{Deserialize, Serialize};
37
38/// Which backend API this site speaks.
39#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
40#[serde(rename_all = "lowercase")]
41pub enum BackendTechnology {
42 /// HPE Cray System Management (CSM) backend.
43 Csm,
44 /// OpenCHAMI backend.
45 Ochami,
46}
47
48impl BackendTechnology {
49 /// Return the lowercase string expected by `StaticBackendDispatcher::new`.
50 pub fn as_str(&self) -> &'static str {
51 match self {
52 Self::Csm => "csm",
53 Self::Ochami => "ochami",
54 }
55 }
56}
57
58#[derive(Serialize, Deserialize, Debug)]
59/// Connection details for a single ALPS site (CSM or OCHAMI instance).
60///
61/// The Vault URL used by handlers requiring vault (sat-file, session,
62/// console, logs) is derived at startup from
63/// `[sites.X.k8s.authentication.vault] base_url`. The vault secret path
64/// is derived from a hard-coded prefix and the site name. Neither is
65/// configured here.
66pub struct Site {
67 /// Which backend implementation this site uses (`csm` or `ochami`).
68 pub backend: BackendTechnology,
69 /// Optional per-site SOCKS5 proxy URL used by every outbound HTTP
70 /// request to this site's backend. `None` means direct connection.
71 pub socks5_proxy: Option<String>,
72 /// Base URL of the backend API (e.g. `https://api.alps.cscs.ch`).
73 pub shasta_base_url: String,
74 /// Optional Kubernetes connection details, required by handlers
75 /// that stream CFS session logs or attach to consoles.
76 pub k8s: Option<K8sDetails>,
77 /// Path (absolute or relative to the config dir) of the backend's
78 /// root CA certificate, used to verify TLS to `shasta_base_url`.
79 pub root_ca_cert_file: String,
80}
81
82/// Server-only settings — TLS, listen address, console behaviour. Lives
83/// under `[server]` in `server.toml`.
84#[derive(Serialize, Deserialize, Debug)]
85pub struct ServerSettings {
86 /// TCP listen address (e.g. "0.0.0.0"). When omitted from config
87 /// **and** no `--listen-address` flag is supplied, the server falls
88 /// back to `"0.0.0.0"`.
89 #[serde(default)]
90 pub listen_address: Option<String>,
91 /// TCP port. When omitted from config **and** no `--port` flag is
92 /// supplied, the effective default depends on whether TLS is
93 /// configured: `8443` if both `cert` and `key` are present (HTTPS),
94 /// otherwise `8080` (plain HTTP). See
95 /// [`ServerSettings::default_port`].
96 #[serde(default)]
97 pub port: Option<u16>,
98 /// Path to the TLS certificate (PEM).
99 pub cert: Option<String>,
100 /// Path to the TLS private key (PEM).
101 pub key: Option<String>,
102 /// How long a node-console WebSocket stays open without activity
103 /// before the server tears it down.
104 pub console_inactivity_timeout_secs: u64,
105 /// Per-source-IP rate limit for the `/api/v1/auth/*` endpoints,
106 /// in requests per minute. `None` disables in-process rate limiting
107 /// (operators are then expected to enforce it at the reverse proxy).
108 pub auth_rate_limit_per_minute: Option<u32>,
109 /// Global request timeout applied to every HTTP route, in seconds.
110 /// When this elapses the server returns `408 REQUEST_TIMEOUT`. All
111 /// long-running work (e.g. power transitions) now runs CLI-side,
112 /// so no endpoint needs more than the default.
113 #[serde(default = "default_request_timeout_secs")]
114 pub request_timeout_secs: u64,
115 /// Grace period (seconds) `axum_server` waits for in-flight
116 /// requests to finish after SIGTERM / Ctrl+C before force-aborting.
117 /// Matches the standard k8s `terminationGracePeriodSeconds` default
118 /// (30 s); pods that hit this without finishing get SIGKILL'd by
119 /// the kubelet.
120 #[serde(default = "default_shutdown_grace_period_secs")]
121 pub shutdown_grace_period_secs: u64,
122 /// Filesystem root that confines `POST /migrate/{backup,restore}`
123 /// file access. When set, every `destination` / `bos_file` /
124 /// `cfs_file` / `hsm_file` / `ims_file` / `image_dir` path in the
125 /// request is canonicalised and rejected unless it resolves under
126 /// this directory. When unset (default), the migrate endpoints
127 /// return `BadRequest` even for admin callers — the operator must
128 /// explicitly opt in to server-side filesystem writes.
129 #[serde(default)]
130 pub migrate_backup_root: Option<String>,
131 /// Opt in to plain-HTTP listen mode. Default `false`: when neither
132 /// `cert` nor `key` is configured the server refuses to start, so
133 /// bearer tokens can't accidentally land on the wire in cleartext.
134 /// Set to `true` only when TLS terminates upstream (reverse proxy
135 /// or sidecar); otherwise leave it off and configure both `cert`
136 /// and `key`.
137 #[serde(default)]
138 pub allow_http: bool,
139}
140
141impl ServerSettings {
142 /// Effective default listen address when neither config nor CLI flag
143 /// supplies one: bind on all interfaces.
144 pub const DEFAULT_LISTEN_ADDRESS: &'static str = "0.0.0.0";
145
146 /// Effective default port when neither config nor CLI flag supplies
147 /// one. `8443` for the HTTPS path (cert + key both present), `8080`
148 /// for plain HTTP — the latter is the typical dev / sidecar setup
149 /// where TLS is terminated upstream.
150 pub fn default_port(has_tls: bool) -> u16 {
151 if has_tls { 8443 } else { 8080 }
152 }
153}
154
155/// Default global request timeout — 600s (10 min). Bumped from 300s
156/// after `GET /cache/configuration` (and other cross-resource fan-out
157/// endpoints that hit CFS + BSS + IMS concurrently) started 408'ing
158/// against busy sites where any single upstream fetch can stall on
159/// the CSM Envoy reset window. 10 min still bounds truly hung
160/// requests while letting heavy-but-healthy ones through. Override
161/// via `request_timeout_secs` in `server.toml` if your deployment
162/// needs a different ceiling.
163fn default_shutdown_grace_period_secs() -> u64 {
164 30
165}
166
167fn default_request_timeout_secs() -> u64 {
168 600
169}
170
171/// Top-level configuration for the `manta-server` binary.
172///
173/// Persisted as TOML under `~/.config/manta/server.toml` and loaded
174/// once at startup. Has no notion of an "active" site — the server
175/// hosts every entry in [`Self::sites`] simultaneously and clients
176/// select per-request via the `X-Manta-Site` header.
177///
178/// See the module-level docs for the on-disk layout.
179#[derive(Serialize, Deserialize, Debug)]
180pub struct ServerConfiguration {
181 /// `EnvFilter` directive for the tracing subscriber.
182 pub log: String,
183 /// Network / TLS / console / rate-limit knobs for the HTTPS server.
184 pub server: ServerSettings,
185 /// Per-site backend connection details, keyed by site name. The
186 /// `X-Manta-Site` header on each request picks which one to route to.
187 pub sites: HashMap<String, Site>,
188 /// Optional Kafka audit forwarder (typically used for `/auth/*`
189 /// attempts). When `None`, the server emits no audit messages.
190 pub auditor: Option<Auditor>,
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn site_deserialize_missing_backend_fails() {
199 let bad_toml = r#"
200 shasta_base_url = "https://api.example.com"
201 root_ca_cert_file = "cert.pem"
202 # missing backend
203 "#;
204 let result = toml::from_str::<Site>(bad_toml);
205 assert!(result.is_err());
206 }
207
208 #[test]
209 fn backend_technology_as_str() {
210 assert_eq!(BackendTechnology::Csm.as_str(), "csm");
211 assert_eq!(BackendTechnology::Ochami.as_str(), "ochami");
212 }
213
214 #[test]
215 fn backend_technology_roundtrip_toml() {
216 // Verify TOML serializes as lowercase "csm" / "ochami"
217 #[derive(Serialize, Deserialize)]
218 struct Wrapper {
219 backend: BackendTechnology,
220 }
221 let w = Wrapper {
222 backend: BackendTechnology::Csm,
223 };
224 let s = toml::to_string(&w).unwrap();
225 assert!(s.contains("\"csm\"") || s.contains("csm"));
226 let parsed: Wrapper = toml::from_str(&s).unwrap();
227 assert_eq!(parsed.backend, BackendTechnology::Csm);
228 }
229
230 fn make_minimal_site() -> Site {
231 Site {
232 backend: BackendTechnology::Csm,
233 socks5_proxy: None,
234 shasta_base_url: "https://api.example.com".to_string(),
235 k8s: None,
236 root_ca_cert_file: "cert.pem".to_string(),
237 }
238 }
239
240 #[test]
241 fn server_configuration_roundtrip_toml_minimal() {
242 let mut sites = HashMap::new();
243 sites.insert("alps".to_string(), make_minimal_site());
244 let cfg = ServerConfiguration {
245 log: "info".to_string(),
246 server: ServerSettings {
247 listen_address: Some("0.0.0.0".to_string()),
248 port: Some(8443),
249 cert: Some("/etc/manta/tls/server.crt".to_string()),
250 key: Some("/etc/manta/tls/server.key".to_string()),
251 console_inactivity_timeout_secs: 1800,
252 auth_rate_limit_per_minute: Some(60),
253 request_timeout_secs: 300,
254 shutdown_grace_period_secs: 30,
255 migrate_backup_root: None,
256 allow_http: false,
257 },
258 sites,
259 auditor: None,
260 };
261 let toml_str = toml::to_string(&cfg).unwrap();
262 let parsed: ServerConfiguration = toml::from_str(&toml_str).unwrap();
263 assert_eq!(parsed.server.port, Some(8443));
264 assert_eq!(parsed.server.listen_address.as_deref(), Some("0.0.0.0"));
265 assert_eq!(parsed.server.console_inactivity_timeout_secs, 1800);
266 assert_eq!(parsed.server.request_timeout_secs, 300);
267 assert_eq!(
268 parsed.server.cert.as_deref(),
269 Some("/etc/manta/tls/server.crt")
270 );
271 }
272
273 /// Default port helper: 8443 when TLS is configured, 8080
274 /// otherwise. Used by `manta-server::main` when no `port` is set
275 /// in config or on the CLI.
276 #[test]
277 fn server_settings_default_port_depends_on_tls() {
278 assert_eq!(ServerSettings::default_port(true), 8443);
279 assert_eq!(ServerSettings::default_port(false), 8080);
280 }
281
282 /// power_timeout_secs is gone — confirm the surrounding
283 /// timeout-related fields still default correctly when the only
284 /// remaining knob is absent.
285 #[test]
286 fn server_settings_request_timeout_secs_defaults_to_600() {
287 let toml_str = r#"
288 listen_address = "0.0.0.0"
289 port = 8443
290 console_inactivity_timeout_secs = 1800
291 "#;
292 let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
293 assert_eq!(parsed.request_timeout_secs, 600);
294 }
295
296 /// `[server]` block with neither `listen_address` nor `port`
297 /// supplied — both fields deserialise as `None`, leaving the
298 /// effective values to be filled in at startup time. Confirms the
299 /// schema-level back-compat for the new defaults.
300 #[test]
301 fn server_settings_listen_address_and_port_default_to_none() {
302 let toml_str = r#"
303 console_inactivity_timeout_secs = 1800
304 "#;
305 let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
306 assert!(parsed.listen_address.is_none());
307 assert!(parsed.port.is_none());
308 }
309
310 /// Existing server.toml files that pre-date the request_timeout
311 /// field must keep working — the field falls back to its default.
312 #[test]
313 fn server_settings_request_timeout_field_defaults_when_omitted() {
314 let toml_str = r#"
315 listen_address = "0.0.0.0"
316 port = 8443
317 console_inactivity_timeout_secs = 1800
318 "#;
319 let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
320 assert_eq!(parsed.request_timeout_secs, 600);
321 }
322
323 #[test]
324 fn server_configuration_deserialize_missing_server_section_fails() {
325 let bad_toml = r#"
326 log = "info"
327 [sites]
328 "#;
329 let result = toml::from_str::<ServerConfiguration>(bad_toml);
330 assert!(result.is_err());
331 }
332
333 #[test]
334 fn server_settings_optional_tls_paths() {
335 // TLS cert/key are optional in the schema — flags can supply them
336 // at runtime when the config omits them.
337 let toml_str = r#"
338 listen_address = "0.0.0.0"
339 port = 8443
340 console_inactivity_timeout_secs = 1800
341 "#;
342 let parsed: ServerSettings = toml::from_str(toml_str).unwrap();
343 assert!(parsed.cert.is_none());
344 assert!(parsed.key.is_none());
345 }
346}