Skip to main content

isoloom_spec/model/
virtiofs.rs

1//! A shared volume as a folder of the Proxmox server (`volumes.<name>.virtiofs`): each VM that
2//! mounts the volume gets the folder through virtiofs instead of over NFS from its owner.
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7/// The most virtiofs folders one Proxmox VM takes (the bpg provider's limit on `virtiofs`).
8pub const MAX_VIRTIOFS: usize = 8;
9
10/// On Proxmox, the volume as a folder of the server, shared into each VM that mounts it
11/// (virtiofs, Proxmox VE 8.4 or later) instead of over NFS from its owner. Opt-in, because the
12/// folder lives on the server: Isoloom creates a directory mapping for it unless `mapping` names
13/// one made on the cluster, and the folder itself must exist on the node. Linux VMs only; every
14/// other target shares the volume as it always does.
15#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
16#[serde(deny_unknown_fields)]
17pub struct Virtiofs {
18    /// An existing directory mapping of the cluster, by its id (Datacenter, Resource Mappings,
19    /// Directory mappings). By default Isoloom creates one, `iso<slot>-<volume>`, for the folder
20    /// `<virtiofs_root>/<environment>/<volume>` on the node.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub mapping: Option<String>,
23    /// How the guests cache the folder: `auto` (the default: metadata kept a short while, so
24    /// another VM's changes show within a second), `always`, `metadata` or `never` (every access
25    /// goes to the server: slowest, always current).
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub cache: Option<VirtiofsCache>,
28    /// Reads and writes that bypass the guest's page cache when a program asks for it
29    /// (`O_DIRECT`), for databases that manage their own cache. Off by default.
30    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
31    pub direct_io: bool,
32    /// Pass extended attributes through (SELinux labels, file capabilities). Off by default.
33    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
34    pub xattr: bool,
35    /// Pass POSIX ACLs through (`setfacl`), which implies `xattr`. Off by default.
36    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
37    pub acl: bool,
38}
39
40/// How a VM caches a virtiofs folder (see [`Virtiofs::cache`]).
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42#[serde(rename_all = "lowercase")]
43pub enum VirtiofsCache {
44    /// Metadata and lookups kept a short while, or until they change: Proxmox's default.
45    #[default]
46    Auto,
47    /// Everything kept: fastest, but another VM's changes may not show.
48    Always,
49    /// Metadata kept, data read each time.
50    Metadata,
51    /// Nothing kept: every access goes to the server.
52    Never,
53}
54
55impl VirtiofsCache {
56    /// The value Proxmox takes.
57    #[must_use]
58    pub fn id(self) -> &'static str {
59        match self {
60            VirtiofsCache::Auto => "auto",
61            VirtiofsCache::Always => "always",
62            VirtiofsCache::Metadata => "metadata",
63            VirtiofsCache::Never => "never",
64        }
65    }
66}
67
68impl Virtiofs {
69    /// Whether extended attributes pass through: asked for, or implied by ACLs.
70    #[must_use]
71    pub fn exposes_xattr(&self) -> bool {
72        self.xattr || self.acl
73    }
74}
75
76/// Whether `id` can name a Proxmox directory mapping: a letter, then letters, digits, `-` and
77/// `_` (Proxmox's configuration ids), at most 64.
78#[must_use]
79pub fn mapping_id(id: &str) -> bool {
80    id.len() <= 64 && id.starts_with(|c: char| c.is_ascii_alphabetic()) && id.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_'))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn a_cache_goes_by_the_name_proxmox_takes() -> Result<(), serde_yaml_ng::Error> {
89        for cache in [VirtiofsCache::Auto, VirtiofsCache::Always, VirtiofsCache::Metadata, VirtiofsCache::Never] {
90            assert_eq!(serde_yaml_ng::from_str::<VirtiofsCache>(cache.id())?, cache);
91        }
92        Ok(())
93    }
94
95    #[test]
96    fn extended_attributes_pass_when_asked_for_or_acls_need_them() -> Result<(), serde_yaml_ng::Error> {
97        let mut read = Vec::new();
98        for yaml in ["{}", "{ xattr: true }", "{ acl: true }"] {
99            read.push(serde_yaml_ng::from_str::<Virtiofs>(yaml)?.exposes_xattr());
100        }
101        assert_eq!(read, [false, true, true]);
102        Ok(())
103    }
104
105    #[test]
106    fn a_mapping_id_is_a_letter_then_up_to_63_letters_digits_dashes_or_underscores() {
107        let longest = format!("a{}", "b".repeat(63));
108        let ids = ["share-1_a", longest.as_str(), &format!("{longest}c"), "1share", "a b", "a.b"];
109        assert_eq!(ids.map(mapping_id), [true, true, false, false, false, false]);
110    }
111}