gevulot_rs/
runtime_config.rs

1//! This module contains VM runtime configuration definitions.
2//!
3//! Gevulot Network uses this configuration to setup environment inside VM before launching main
4//! application.
5//!
6//! From VM perspective, this configuration will be mounted to `/mnt/gevulot-rt-config/config.yaml`.
7//! Then VM is responsible to process it in order to execute the main application properly.
8//!
9//! [`follow_config`](RuntimeConfig::follow_config) allows to chain multiple configurations.
10//! It contains path to the next configuration file to process after current one is finished.
11//!
12//! ## Processing
13//!
14//! The configuration SHOULD be processed in the following way:
15//!
16//! - Mount default filesystems (default filesystems are defined by VM itself);
17//! - Setup ISA debug exit port if some (specifying multiple ports is not allowed).
18//! - Mount filesystems in order of specification in [`mounts`](RuntimeConfig::mounts);
19//! - Set environment variables specified in [`env`](RuntimeConfig::env);
20//! - Set working directory to [`working_dir`](RuntimeConfig::working_dir);
21//! - Load kernel modules in order of specification in
22//! [`kernel_modules`](RuntimeConfig::kernel_modules);
23//! - Run boot commands in order of specification in [`bootcmd`](RuntimeConfig::bootcmd).
24//!
25//! If current configuration defines a `command` to run, it should be updated together with its
26//! arguments. If there is a following configuration, it should be loaded and processed in the same
27//! way.
28//!
29//! Finally after processing all configuration files, [`command`](RuntimeConfig::command) with
30//! [`args`](RuntimeConfig::args) should be executed.
31//!
32//! Because loading following configuration file happens after mounting, it may be taken from
33//! mounted directory.
34//!
35//! ## Configuration file
36//!
37//! Runtime configurations are expected to be serialized into and deserialized from YAML files.
38//! Every Gevulot runtime configuration YAML file MUST start with `version` field.
39
40use serde::de::Error;
41use serde::{Deserialize, Serialize};
42
43const MAJOR: u64 = 1;
44const MINOR: u64 = 1;
45const PATCH: u64 = 0;
46
47const SEM_VERSION: semver::Version = semver::Version::new(MAJOR, MINOR, PATCH);
48
49/// Version of runtime configuration.
50pub const VERSION: &str = const_format::concatcp!(MAJOR, ".", MINOR, ".", PATCH);
51
52/// Environment variable definition.
53#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
54pub struct EnvVar {
55    pub key: String,
56    pub value: String,
57}
58
59/// Mount definition.
60#[derive(Clone, Debug, Deserialize, Serialize)]
61pub struct Mount {
62    pub source: String,
63    pub target: String,
64    pub fstype: Option<String>,
65    pub flags: Option<u64>,
66    pub data: Option<String>,
67}
68
69impl Mount {
70    /// Create virtio 9p mount.
71    ///
72    /// This is commonly used for providing inputs and outputs to the program in VM.
73    pub fn virtio9p(source: String, target: String) -> Self {
74        Self {
75            source,
76            target,
77            fstype: Some("9p".to_string()),
78            flags: None,
79            data: Some("trans=virtio,version=9p2000.L".to_string()),
80        }
81    }
82}
83
84/// Debug exit method depending on ISA.
85#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
86#[serde(tag = "arch")]
87pub enum DebugExit {
88    /// Definition of exit code port for x86 QEMU.
89    ///
90    /// Defaults: iobase=0xf4,iosize=0x4
91    #[serde(rename = "x86")]
92    X86 {
93        iobase: u16,
94        /// No reason to set it to something other than 0x4,
95        /// because `success_code` is `u32`.
96        iosize: u16,
97        /// Must be odd number greater than 1.
98        /// 1 will be an error code.
99        #[serde(rename = "success-code")]
100        success_code: u32,
101    },
102}
103
104impl DebugExit {
105    pub const fn default_x86() -> Self {
106        Self::X86 {
107            iobase: 0xf4,
108            iosize: 0x4,
109            success_code: 0x3,
110        }
111    }
112}
113
114fn true_value() -> bool {
115    true
116}
117
118fn deserialize_version<'de, D>(deserializer: D) -> Result<String, D::Error>
119where
120    D: serde::Deserializer<'de>,
121{
122    let mut version = String::deserialize(deserializer)?;
123    // After deserialization, complete the version up to SemVer format: "X.Y.Z"
124    let split = version.split('.').collect::<Vec<_>>();
125    match split.len() {
126        1 => {
127            version.push_str(".0.0");
128        }
129        2 => {
130            version.push_str(".0");
131        }
132        3 => {}
133        _ => {
134            return Err(D::Error::custom(
135                "Gevulot runtime config: invalid version string",
136            ));
137        }
138    }
139    // Now compare versions in terms of SemVer
140    let semversion = semver::Version::parse(&version).map_err(|err| {
141        D::Error::custom(format!(
142            "Gevulot runtime config: failed to parse version: {}",
143            err
144        ))
145    })?;
146    if semversion.major != SEM_VERSION.major || semversion > SEM_VERSION {
147        return Err(D::Error::custom(
148            "Gevulot runtime config: unsupported version",
149        ));
150    }
151    Ok(version)
152}
153
154/// Gevulot VM runtime configuration.
155///
156/// See [module-level documentation](self) for more information.
157#[derive(Clone, Debug, Default, Deserialize, Serialize)]
158#[serde(deny_unknown_fields, rename_all = "kebab-case")]
159pub struct RuntimeConfig {
160    /// Version of the config.
161    #[serde(deserialize_with = "deserialize_version")]
162    pub version: String,
163
164    /// Program to execute.
165    pub command: Option<String>,
166
167    /// Arguments to the command.
168    #[serde(default)]
169    pub args: Vec<String>,
170
171    /// Environment variables.
172    #[serde(default)]
173    pub env: Vec<EnvVar>,
174
175    /// Working directory.
176    pub working_dir: Option<String>,
177
178    /// Mounts.
179    #[serde(default)]
180    pub mounts: Vec<Mount>,
181
182    /// Default filesystems to mount.
183    ///
184    /// These filesystems are defined by VM itself. Typically these are `/proc`, `/sys` etc.
185    ///
186    /// When (de-)serlializing, defaults to `true`.
187    #[serde(default = "true_value")]
188    pub default_mounts: bool,
189
190    /// Kernel modules.
191    #[serde(default)]
192    pub kernel_modules: Vec<String>,
193
194    /// Debug exit (e.g. for QEMU `isa-debug-exit` device).
195    ///
196    /// If none specified, a simple shutdown is expected.
197    pub debug_exit: Option<DebugExit>,
198
199    /// Boot commands.
200    ///
201    /// Arbitrary commands to execute at initialization time.
202    #[serde(default)]
203    pub bootcmd: Vec<Vec<String>>,
204
205    /// Path to another runtime configuration file to process after current one.
206    pub follow_config: Option<String>,
207}
208
209// TODO: Implement strict version check to get proper error messages.
210//       Deserializer needs to ensure that version field goes first (as it is described in docs
211//       above) and decline going further if version is not correct. Otherwise such file:
212//         abracadabra: xxxyyyzzz
213//         version: 123
214//       will report error "unknown field `abracadabra`" instead of version error
215//       (like "no `version` at the beginning").
216
217#[cfg(test)]
218mod tests {
219    use super::{DebugExit, EnvVar, RuntimeConfig};
220
221    #[test]
222    fn test_deserialize_version_ok() {
223        let source = "
224        version: 1
225        command: echo
226        ";
227        let result = serde_yaml::from_str::<RuntimeConfig>(source);
228        result.expect("deserialization should succeed");
229    }
230
231    #[test]
232    fn test_deserialize_version_fail_1() {
233        let source = "
234        version: 0
235        commands: echo
236        ";
237        let result = serde_yaml::from_str::<RuntimeConfig>(source);
238        assert!(result.is_err());
239        let err = result.err().unwrap();
240        assert_eq!(
241            err.to_string(),
242            "Gevulot runtime config: unsupported version at line 2 column 9".to_string()
243        );
244    }
245
246    #[test]
247    fn test_deserialize_version_fail_2() {
248        let source = "
249        abracadabra: 0
250        version: 123
251        ";
252        let result = serde_yaml::from_str::<RuntimeConfig>(source);
253        assert!(result.is_err());
254        // TODO: check error message. Can be done only after completing TODO above.
255    }
256
257    const EXAMPLE_CONFIG: &str = "
258    version: 1
259    working-dir: /
260    command: prover
261    args: [--log, info]
262    env:
263      - key: TMPDIR
264        value: /tmp
265    mounts:
266      - source: input-1
267        target: /input/1
268    default-mounts: true
269    kernel-modules:
270      - nvidia
271    debug-exit:
272      arch: x86
273      iobase: 0xf4
274      iosize: 0x4
275      success-code: 0x3
276    bootcmd:
277      - [echo, booting]
278    follow-config: /my/local/config.yaml
279    ";
280
281    #[test]
282    fn test_deserialization_example_config() {
283        let result = serde_yaml::from_str::<RuntimeConfig>(EXAMPLE_CONFIG)
284            .expect("deserialization should succeed");
285        assert_eq!(
286            &result.command.expect("command should be present"),
287            "prover"
288        );
289        assert_eq!(result.args, vec!["--log".to_string(), "info".to_string()]);
290        assert_eq!(result.env.len(), 1);
291        assert_eq!(
292            result.env[0],
293            EnvVar {
294                key: "TMPDIR".to_string(),
295                value: "/tmp".to_string()
296            }
297        );
298        assert_eq!(
299            &result.working_dir.expect("working dir should be present"),
300            "/"
301        );
302        assert_eq!(result.mounts.len(), 1);
303        assert_eq!(result.mounts[0].source, "input-1".to_string());
304        assert_eq!(result.mounts[0].target, "/input/1".to_string());
305        assert_eq!(result.mounts[0].fstype, None);
306        assert_eq!(result.mounts[0].flags, None);
307        assert_eq!(result.mounts[0].data, None);
308        assert!(result.default_mounts);
309        assert_eq!(result.kernel_modules, vec!["nvidia".to_string()]);
310        assert_eq!(result.debug_exit, Some(DebugExit::default_x86()));
311        assert_eq!(result.bootcmd, vec![vec!["echo", "booting"]]);
312        assert_eq!(
313            &result
314                .follow_config
315                .expect("follow config should be present"),
316            "/my/local/config.yaml"
317        );
318    }
319}