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, 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 let config = Configuration::from_file(PathBuf::from("/"));
196 assert_eq!(config, Configuration::default());
197
198 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}