northstar_runtime/runtime/
config.rs

1use std::{
2    collections::HashMap,
3    os::unix::prelude::{MetadataExt, PermissionsExt},
4    path::{Path, PathBuf},
5    time,
6};
7
8use anyhow::{bail, Context};
9use bytesize::ByteSize;
10use nix::{sys::stat, unistd};
11use serde::{de::Error as SerdeError, Deserialize, Deserializer};
12use url::Url;
13
14use crate::{
15    common::non_nul_string::NonNulString, npk::manifest::console::Permissions,
16    runtime::repository::RepositoryId,
17};
18
19/// Runtime configuration
20#[derive(Clone, Debug, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct Config {
23    /// Directory with unpacked containers.
24    pub run_dir: PathBuf,
25    /// Directory where rw data of container shall be stored
26    pub data_dir: PathBuf,
27    /// Directory for sockets
28    pub socket_dir: PathBuf,
29    /// Top level cgroup name
30    pub cgroup: NonNulString,
31    /// Event loop buffer size
32    #[serde(default = "default_event_buffer_size")]
33    pub event_buffer_size: usize,
34    /// Notification buffer size
35    #[serde(default = "default_notification_buffer_size")]
36    pub notification_buffer_size: usize,
37    /// Console configuration.
38    #[serde(default)]
39    pub console: Console,
40    /// Loop device timeout
41    #[serde(with = "humantime_serde", default = "default_loop_device_timeout")]
42    pub loop_device_timeout: time::Duration,
43    /// Repositories
44    #[serde(default)]
45    pub repositories: HashMap<RepositoryId, Repository>,
46    /// Debugging options
47    pub debug: Option<Debug>,
48}
49
50/// Globally accessible console.
51#[derive(Clone, Debug, Deserialize)]
52pub struct ConsoleGlobal {
53    /// Bind globally accesible console to this address.
54    #[serde(deserialize_with = "console_url")]
55    pub bind: Url,
56    /// Permissions
57    pub permissions: Permissions,
58    /// Console options
59    pub options: Option<ConsoleOptions>,
60}
61
62/// Console Quality of Service
63#[derive(Clone, Debug, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct ConsoleOptions {
66    /// Token validity duration.
67    #[serde(with = "humantime_serde", default = "default_token_validity")]
68    pub token_validity: time::Duration,
69    /// Limits the number of requests processed per second.
70    #[serde(default = "default_max_requests_per_second")]
71    pub max_requests_per_sec: usize,
72    /// Maximum request size in bytes
73    #[serde(deserialize_with = "bytesize", default = "default_max_request_size")]
74    pub max_request_size: u64,
75    /// Maximum npk size in bytes.
76    #[serde(
77        deserialize_with = "bytesize",
78        default = "default_max_npk_install_size"
79    )]
80    pub max_npk_install_size: u64,
81    /// NPK stream timeout in seconds.
82    #[serde(with = "humantime_serde", default = "default_npk_stream_timeout")]
83    pub npk_stream_timeout: time::Duration,
84}
85
86impl Default for ConsoleOptions {
87    fn default() -> Self {
88        Self {
89            token_validity: default_token_validity(),
90            max_requests_per_sec: default_max_requests_per_second(),
91            max_request_size: default_max_request_size(),
92            max_npk_install_size: default_max_npk_install_size(),
93            npk_stream_timeout: default_npk_stream_timeout(),
94        }
95    }
96}
97
98/// Console Quality of Service
99#[derive(Clone, Default, Debug, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct Console {
102    /// Globally accessible console.
103    pub global: Option<ConsoleGlobal>,
104    /// Options for console connections with containers.
105    pub options: Option<ConsoleOptions>,
106}
107
108/// Repository type
109#[derive(Clone, Debug, Deserialize)]
110pub enum RepositoryType {
111    /// Directory based
112    #[serde(rename = "fs")]
113    Fs {
114        /// Path to the repository
115        dir: PathBuf,
116    },
117    /// Memory based
118    #[serde(rename = "mem")]
119    Memory,
120}
121
122/// Repository configuration
123#[derive(Clone, Debug, Deserialize)]
124#[serde(deny_unknown_fields)]
125pub struct Repository {
126    /// Repository type: fs or mem.
127    pub r#type: RepositoryType,
128    /// Optional key for this repository.
129    pub key: Option<PathBuf>,
130    /// Mount the containers from this repository on runtime start. Default: false.
131    #[serde(default)]
132    pub mount_on_start: bool,
133    /// Maximum number of containers that can be stored in this repository.
134    pub capacity_num: Option<u32>,
135    /// Maximum total size of all containers in this repository.
136    #[serde(default, deserialize_with = "bytesize")]
137    pub capacity_size: Option<u64>,
138}
139
140/// Container debug settings
141#[derive(Clone, Debug, Deserialize)]
142#[serde(deny_unknown_fields)]
143pub struct Debug {
144    /// Commands to run before the container is started.
145    //  <CONTAINER> is replaced with the container name.
146    //  <PID> is replaced with the container init pid.
147    #[serde(default)]
148    pub commands: Vec<String>,
149}
150
151impl Config {
152    /// Validate the configuration
153    pub(crate) fn check(&self) -> anyhow::Result<()> {
154        check_rw_directory(&self.run_dir).context("checking run_dir")?;
155        check_rw_directory(&self.data_dir).context("checking data_dir")?;
156        check_rw_directory(&self.socket_dir).context("checking socket_dir")?;
157        Ok(())
158    }
159}
160
161/// Checks that the directory exists and that it is readable and writeable
162fn check_rw_directory(path: &Path) -> anyhow::Result<()> {
163    if !path.exists() {
164        bail!("{} does not exist", path.display());
165    } else if !is_rw(path) {
166        bail!("{} is not read and/or writeable", path.display());
167    } else {
168        Ok(())
169    }
170}
171
172/// Return true if path is read and writeable
173fn is_rw(path: &Path) -> bool {
174    match std::fs::metadata(path) {
175        Ok(stat) => {
176            let same_uid = stat.uid() == unistd::getuid().as_raw();
177            let same_gid = stat.gid() == unistd::getgid().as_raw();
178            let mode = stat::Mode::from_bits_truncate(stat.permissions().mode());
179
180            let is_readable = (same_uid && mode.contains(stat::Mode::S_IRUSR))
181                || (same_gid && mode.contains(stat::Mode::S_IRGRP))
182                || mode.contains(stat::Mode::S_IROTH);
183            let is_writable = (same_uid && mode.contains(stat::Mode::S_IWUSR))
184                || (same_gid && mode.contains(stat::Mode::S_IWGRP))
185                || mode.contains(stat::Mode::S_IWOTH);
186
187            is_readable && is_writable
188        }
189        Err(_) => false,
190    }
191}
192
193/// Validate the console url schemes are all "tcp" or "unix"
194fn console_url<'de, D>(deserializer: D) -> Result<Url, D::Error>
195where
196    D: Deserializer<'de>,
197{
198    let url = Url::deserialize(deserializer)?;
199    if url.scheme() != "tcp" && url.scheme() != "unix" {
200        Err(D::Error::custom("console scheme must be tcp or unix"))
201    } else {
202        Ok(url)
203    }
204}
205
206/// Parse human readable byte sizes.
207fn bytesize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
208where
209    D: Deserializer<'de>,
210    T: From<u64>,
211{
212    String::deserialize(deserializer)
213        .and_then(|s| s.parse::<ByteSize>().map_err(D::Error::custom))
214        .map(|s| s.as_u64().into())
215}
216
217/// Default loop device timeout.
218const fn default_loop_device_timeout() -> time::Duration {
219    time::Duration::from_secs(10)
220}
221
222/// Default event buffer size.
223const fn default_event_buffer_size() -> usize {
224    256
225}
226
227/// Default notification buffer size.
228const fn default_notification_buffer_size() -> usize {
229    128
230}
231
232/// Default token validity time.
233const fn default_token_validity() -> time::Duration {
234    time::Duration::from_secs(60)
235}
236
237/// Default maximum requests per second.
238const fn default_max_requests_per_second() -> usize {
239    1000
240}
241
242/// Default maximum NPK size.
243const fn default_max_npk_install_size() -> u64 {
244    256 * 1024 * 1024
245}
246/// Default timeout between two npks stream chunks.
247const fn default_npk_stream_timeout() -> time::Duration {
248    time::Duration::from_secs(10)
249}
250
251/// Default maximum length per request in bytes.
252const fn default_max_request_size() -> u64 {
253    1024 * 1024
254}
255
256#[test]
257#[allow(clippy::unwrap_used)]
258fn validate_console_url() {
259    let config = r#"
260data_dir = "target/northstar/data"
261run_dir = "target/northstar/run"
262socket_dir = "target/northstar/sockets"
263cgroup = "northstar"
264[console.global]
265bind = "tcp://localhost:4200"
266permissions = "full"
267"#;
268
269    toml::from_str::<Config>(config).unwrap();
270
271    // Invalid url
272    let config = r#"
273data_dir = "target/northstar/data"
274run_dir = "target/northstar/run"
275socket_dir = "target/northstar/sockets"
276cgroup = "northstar"
277[console.global]
278bind = "http://localhost:4200"
279permissions = "full"
280"#;
281
282    assert!(toml::from_str::<Config>(config).is_err());
283}
284
285#[test]
286fn repository_size() {
287    let config = r#"
288data_dir = "target/northstar/data"
289run_dir = "target/northstar/run"
290socket_dir = "target/northstar/sockets"
291cgroup = "northstar"
292
293[repositories.memory]
294type = "mem"
295key = "examples/northstar.pub"
296capacity_num = 10
297capacity_size = "100MB"
298"#;
299    let config = toml::from_str::<Config>(config).expect("failed to parse config");
300    let memory = config
301        .repositories
302        .get("memory")
303        .expect("failed to find memory repository");
304    assert_eq!(memory.key, Some("examples/northstar.pub".into()));
305    assert_eq!(memory.capacity_num, Some(10));
306    assert_eq!(memory.capacity_size, Some(100000000));
307}