emu_cli/
config.rs

1use serde::{Deserialize, Serialize};
2use std::{collections::HashMap, io::Write, path::PathBuf};
3
4use anyhow::{anyhow, Result};
5
6const DEFAULT_CPU_TYPE: &str = "host";
7const DEFAULT_CPUS: u32 = 8;
8const DEFAULT_MEMORY: u32 = 16384;
9const DEFAULT_VGA: &str = "virtio";
10const DEFAULT_SSH_PORT: u16 = 2222;
11const DEFAULT_IMAGE_INTERFACE: &str = "virtio";
12
13pub type PortMap = HashMap<String, u16>;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Configuration {
17    pub machine: MachineConfiguration,
18    pub ports: PortMap,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct MachineConfiguration {
23    pub ssh_port: u16,
24    pub memory: u32, // megabytes
25    pub cpus: u32,
26    pub cpu_type: String,
27    pub vga: String,
28    pub image_interface: String,
29}
30
31impl std::fmt::Display for Configuration {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.write_str(&toml::to_string_pretty(self).map_err(|_| std::fmt::Error::default())?)
34    }
35}
36
37impl Default for Configuration {
38    fn default() -> Self {
39        Configuration {
40            machine: MachineConfiguration {
41                ssh_port: DEFAULT_SSH_PORT,
42                memory: DEFAULT_MEMORY,
43                cpus: DEFAULT_CPUS,
44                cpu_type: DEFAULT_CPU_TYPE.to_string(),
45                vga: DEFAULT_VGA.to_string(),
46                image_interface: DEFAULT_IMAGE_INTERFACE.to_string(),
47            },
48            ports: HashMap::new(),
49        }
50    }
51}
52
53impl Configuration {
54    pub fn is_port_conflict(&self, other: &Self) -> bool {
55        for key in self.ports.keys() {
56            for okey in other.ports.keys() {
57                if key == okey {
58                    return true;
59                }
60            }
61        }
62
63        return false;
64    }
65
66    pub fn from_file(filename: PathBuf) -> Self {
67        std::fs::read_to_string(filename).map_or_else(
68            |_| Self::default(),
69            |x| toml::from_str(&x).unwrap_or_default(),
70        )
71    }
72
73    pub fn to_file(&self, filename: PathBuf) -> Result<()> {
74        let mut f = std::fs::File::create(filename)?;
75        f.write_all(self.to_string().as_bytes())?;
76
77        Ok(())
78    }
79
80    pub fn valid(&self) -> Result<()> {
81        if self.machine.memory == 0 {
82            return Err(anyhow!("No memory value set"));
83        }
84
85        if self.machine.cpus == 0 {
86            return Err(anyhow!("No cpus value set"));
87        }
88
89        Ok(())
90    }
91
92    pub fn map_port(&mut self, hostport: u16, guestport: u16) {
93        self.ports.insert(hostport.to_string(), guestport);
94    }
95
96    pub fn unmap_port(&mut self, hostport: u16) {
97        self.ports.remove(&hostport.to_string());
98    }
99
100    pub fn set_machine_value(&mut self, key: &str, value: &str) -> Result<()> {
101        match key {
102            "memory" => {
103                self.machine.memory = value.parse::<u32>()?;
104                Ok(())
105            }
106            "cpus" => {
107                self.machine.cpus = value.parse::<u32>()?;
108                Ok(())
109            }
110            "vga" => {
111                self.machine.vga = value.to_string();
112                Ok(())
113            }
114            "image-interface" => {
115                self.machine.image_interface = value.to_string();
116                Ok(())
117            }
118            "image_interface" => {
119                self.machine.image_interface = value.to_string();
120                Ok(())
121            }
122            "cpu-type" => {
123                self.machine.cpu_type = value.to_string();
124                Ok(())
125            }
126            "cpu_type" => {
127                self.machine.cpu_type = value.to_string();
128                Ok(())
129            }
130            "ssh-port" => {
131                self.machine.ssh_port = value.parse::<u16>()?;
132                Ok(())
133            }
134            "ssh_port" => {
135                self.machine.ssh_port = value.parse::<u16>()?;
136                Ok(())
137            }
138            _ => Err(anyhow!("key does not exist")),
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use anyhow::Result;
147    use std::path::PathBuf;
148    use tempfile::NamedTempFile;
149
150    #[test]
151    fn test_set_machine_value() -> Result<()> {
152        let mut config = Configuration::default();
153        config.set_machine_value("memory", "1024")?;
154        assert_eq!(config.machine.memory, 1024);
155        config.set_machine_value("cpus", "2")?;
156        assert_eq!(config.machine.cpus, 2);
157        config.set_machine_value("vga", "none")?;
158        assert_eq!(config.machine.vga, "none");
159        config.set_machine_value("image-interface", "virtio")?;
160        assert_eq!(config.machine.image_interface, "virtio");
161        config.set_machine_value("cpu-type", "host")?;
162        assert_eq!(config.machine.cpu_type, "host");
163        config.set_machine_value("ssh-port", "2222")?;
164        assert_eq!(config.machine.ssh_port, 2222);
165        Ok(())
166    }
167
168    #[test]
169    fn test_map_unmap_ports() -> Result<()> {
170        let mut config = Configuration::default();
171        config.map_port(2222, 22);
172        assert_eq!(config.ports.get("2222"), Some(22).as_ref());
173        config.unmap_port(2222);
174        assert_eq!(config.ports.get("2222"), None);
175
176        let mut conflict1 = Configuration::default();
177        let mut conflict2 = Configuration::default();
178
179        conflict1.map_port(2222, 22);
180        conflict2.map_port(2222, 22);
181
182        assert!(conflict1.is_port_conflict(&conflict2));
183
184        conflict2.unmap_port(2222);
185        assert!(!conflict1.is_port_conflict(&conflict2));
186
187        Ok(())
188    }
189
190    #[test]
191    fn test_io() -> Result<()> {
192        let tmp = NamedTempFile::new()?;
193        let path = tmp.path().to_path_buf();
194        // failure to read results in a default configuration
195        let config = Configuration::from_file(PathBuf::from("/"));
196        assert_eq!(config, Configuration::default());
197
198        // i/o of default configuration
199        Configuration::default().to_file(path.clone())?;
200        let config = Configuration::from_file(path.clone());
201        assert_eq!(config, Configuration::default());
202
203        let orig = Configuration {
204            machine: MachineConfiguration {
205                ssh_port: 2000,
206                cpu_type: Default::default(),
207                cpus: 4,
208                image_interface: Default::default(),
209                memory: 2048,
210                vga: Default::default(),
211            },
212            ports: Default::default(),
213        };
214
215        orig.to_file(path.clone())?;
216        let new = Configuration::from_file(path);
217        assert_eq!(orig, new);
218
219        Ok(())
220    }
221}