]>
Commit | Line | Data |
---|---|---|
dd193a61 DM |
1 | use failure::*; |
2 | ||
e471d699 | 3 | use std::collections::HashMap; |
e189c93b | 4 | use std::collections::HashSet; |
a27b905c | 5 | use std::collections::VecDeque; |
e189c93b | 6 | |
e471d699 DM |
7 | use serde_json::{json, Value}; |
8 | ||
e18a6c9e DM |
9 | use proxmox::tools::try_block; |
10 | ||
ef2f2efb | 11 | use crate::api_schema::*; |
803b95a0 | 12 | |
e471d699 DM |
13 | pub struct SectionConfigPlugin { |
14 | type_name: String, | |
255f378a | 15 | properties: &'static ObjectSchema, |
e471d699 | 16 | } |
22245422 | 17 | |
803b95a0 DM |
18 | impl SectionConfigPlugin { |
19 | ||
255f378a | 20 | pub fn new(type_name: String, properties: &'static ObjectSchema) -> Self { |
803b95a0 DM |
21 | Self { type_name, properties } |
22 | } | |
23 | ||
24 | } | |
22245422 | 25 | |
e471d699 DM |
26 | pub struct SectionConfig { |
27 | plugins: HashMap<String, SectionConfigPlugin>, | |
22245422 | 28 | |
255f378a | 29 | id_schema: &'static Schema, |
ee7fc433 DM |
30 | parse_section_header: fn(&str) -> Option<(String, String)>, |
31 | parse_section_content: fn(&str) -> Option<(String, String)>, | |
e189c93b | 32 | format_section_header: fn(type_name: &str, section_id: &str, data: &Value) -> String, |
dd193a61 DM |
33 | } |
34 | ||
b943ed8d | 35 | enum ParseState<'a> { |
dd193a61 | 36 | BeforeHeader, |
826698d5 | 37 | InsideSection(&'a SectionConfigPlugin, String, Value), |
22245422 DM |
38 | } |
39 | ||
92fb0784 DM |
40 | #[derive(Debug)] |
41 | pub struct SectionConfigData { | |
b65eaac6 | 42 | pub sections: HashMap<String, (String, Value)>, |
a27b905c | 43 | order: VecDeque<String>, |
92fb0784 DM |
44 | } |
45 | ||
46 | impl SectionConfigData { | |
47 | ||
48 | pub fn new() -> Self { | |
a27b905c | 49 | Self { sections: HashMap::new(), order: VecDeque::new() } |
92fb0784 DM |
50 | } |
51 | ||
e189c93b DM |
52 | pub fn set_data(&mut self, section_id: &str, type_name: &str, config: Value) { |
53 | // fixme: verify section_id schema here?? | |
54 | self.sections.insert(section_id.to_string(), (type_name.to_string(), config)); | |
92fb0784 DM |
55 | } |
56 | ||
e189c93b DM |
57 | fn record_order(&mut self, section_id: &str) { |
58 | self.order.push_back(section_id.to_string()); | |
92fb0784 | 59 | } |
b65eaac6 DM |
60 | |
61 | pub fn convert_to_array(&self, id_prop: &str) -> Value { | |
62 | let mut list: Vec<Value> = vec![]; | |
63 | ||
64 | for (section_id, (_, data)) in &self.sections { | |
5b34c260 DM |
65 | let mut item = data.clone(); |
66 | item.as_object_mut().unwrap().insert(id_prop.into(), section_id.clone().into()); | |
67 | list.push(item); | |
b65eaac6 DM |
68 | } |
69 | ||
70 | list.into() | |
71 | } | |
92fb0784 DM |
72 | } |
73 | ||
22245422 DM |
74 | impl SectionConfig { |
75 | ||
255f378a | 76 | pub fn new(id_schema: &'static Schema) -> Self { |
e471d699 DM |
77 | Self { |
78 | plugins: HashMap::new(), | |
653b1ca1 | 79 | id_schema, |
e471d699 | 80 | parse_section_header: SectionConfig::default_parse_section_header, |
1fa897cf | 81 | parse_section_content: SectionConfig::default_parse_section_content, |
e189c93b | 82 | format_section_header: SectionConfig::default_format_section_header, |
e471d699 DM |
83 | } |
84 | } | |
85 | ||
803b95a0 DM |
86 | pub fn register_plugin(&mut self, plugin: SectionConfigPlugin) { |
87 | self.plugins.insert(plugin.type_name.clone(), plugin); | |
88 | } | |
89 | ||
bfb1d69a | 90 | pub fn write(&self, _filename: &str, config: &SectionConfigData) -> Result<String, Error> { |
e189c93b | 91 | |
a27b905c | 92 | let mut list = VecDeque::new(); |
e189c93b DM |
93 | |
94 | let mut done = HashSet::new(); | |
95 | ||
ae3a512d DM |
96 | for section_id in &config.order { |
97 | if config.sections.get(section_id) == None { continue }; | |
98 | list.push_back(section_id); | |
99 | done.insert(section_id); | |
e189c93b DM |
100 | } |
101 | ||
ae3a512d DM |
102 | for (section_id, _) in &config.sections { |
103 | if done.contains(section_id) { continue }; | |
104 | list.push_back(section_id); | |
e189c93b DM |
105 | } |
106 | ||
107 | let mut raw = String::new(); | |
108 | ||
ae3a512d DM |
109 | for section_id in list { |
110 | let (type_name, section_config) = config.sections.get(section_id).unwrap(); | |
e189c93b DM |
111 | let plugin = self.plugins.get(type_name).unwrap(); |
112 | ||
ae3a512d DM |
113 | if let Err(err) = parse_simple_value(§ion_id, &self.id_schema) { |
114 | bail!("syntax error in section identifier: {}", err.to_string()); | |
115 | } | |
116 | ||
117 | verify_json_object(section_config, &plugin.properties)?; | |
118 | println!("REAL WRITE {} {} {:?}\n", section_id, type_name, section_config); | |
e189c93b | 119 | |
ae3a512d | 120 | let head = (self.format_section_header)(type_name, section_id, section_config); |
e189c93b DM |
121 | |
122 | if !raw.is_empty() { raw += "\n" } | |
123 | ||
124 | raw += &head; | |
125 | ||
126 | for (key, value) in section_config.as_object().unwrap() { | |
127 | let text = match value { | |
9c1b42d2 DM |
128 | Value::Null => { continue; }, // do nothing (delete) |
129 | Value::Bool(v) => v.to_string(), | |
130 | Value::String(v) => v.to_string(), | |
131 | Value::Number(v) => v.to_string(), | |
e189c93b | 132 | _ => { |
ae3a512d | 133 | bail!("got unsupported type in section '{}' key '{}'", section_id, key); |
e189c93b DM |
134 | }, |
135 | }; | |
136 | raw += "\t"; | |
137 | raw += &key; | |
138 | raw += " "; | |
139 | raw += &text; | |
140 | raw += "\n"; | |
141 | } | |
e189c93b DM |
142 | } |
143 | ||
c6ed6cac | 144 | Ok(raw) |
e189c93b DM |
145 | } |
146 | ||
92fb0784 | 147 | pub fn parse(&self, filename: &str, raw: &str) -> Result<SectionConfigData, Error> { |
dd193a61 | 148 | |
dd193a61 | 149 | let mut state = ParseState::BeforeHeader; |
e471d699 | 150 | |
bdb631da | 151 | let test_required_properties = |value: &Value, schema: &ObjectSchema| -> Result<(), Error> { |
255f378a | 152 | for (name, optional, _prop_schema) in schema.properties { |
379ea0ed DM |
153 | if *optional == false && value[name] == Value::Null { |
154 | return Err(format_err!("property '{}' is missing and it is not optional.", name)); | |
bdb631da DM |
155 | } |
156 | } | |
157 | Ok(()) | |
158 | }; | |
159 | ||
9b50c161 | 160 | let mut line_no = 0; |
ee7fc433 | 161 | |
9b50c161 | 162 | try_block!({ |
ee7fc433 | 163 | |
9b50c161 | 164 | let mut result = SectionConfigData::new(); |
dd193a61 | 165 | |
9b50c161 DM |
166 | let mut create_section = |section_id: &str, type_name: &str, config| { |
167 | result.set_data(section_id, type_name, config); | |
168 | result.record_order(section_id); | |
169 | }; | |
dd193a61 | 170 | |
9b50c161 DM |
171 | try_block!({ |
172 | for line in raw.lines() { | |
173 | line_no += 1; | |
dd193a61 | 174 | |
9b50c161 | 175 | match state { |
dd193a61 | 176 | |
9b50c161 | 177 | ParseState::BeforeHeader => { |
dd193a61 | 178 | |
9b50c161 DM |
179 | if line.trim().is_empty() { continue; } |
180 | ||
181 | if let Some((section_type, section_id)) = (self.parse_section_header)(line) { | |
182 | //println!("OKLINE: type: {} ID: {}", section_type, section_id); | |
183 | if let Some(ref plugin) = self.plugins.get(§ion_type) { | |
184 | if let Err(err) = parse_simple_value(§ion_id, &self.id_schema) { | |
185 | bail!("syntax error in section identifier: {}", err.to_string()); | |
bdb631da | 186 | } |
9b50c161 DM |
187 | state = ParseState::InsideSection(plugin, section_id, json!({})); |
188 | } else { | |
189 | bail!("unknown section type '{}'", section_type); | |
bdb631da | 190 | } |
9b50c161 DM |
191 | } else { |
192 | bail!("syntax error (expected header)"); | |
193 | } | |
194 | } | |
195 | ParseState::InsideSection(plugin, ref mut section_id, ref mut config) => { | |
196 | ||
197 | if line.trim().is_empty() { | |
198 | // finish section | |
199 | test_required_properties(config, &plugin.properties)?; | |
200 | create_section(section_id, &plugin.type_name, config.take()); | |
201 | state = ParseState::BeforeHeader; | |
202 | continue; | |
203 | } | |
204 | if let Some((key, value)) = (self.parse_section_content)(line) { | |
205 | //println!("CONTENT: key: {} value: {}", key, value); | |
206 | ||
255f378a | 207 | if let Some((_optional, prop_schema)) = plugin.properties.lookup(&key) { |
9b50c161 DM |
208 | match parse_simple_value(&value, prop_schema) { |
209 | Ok(value) => { | |
210 | if config[&key] == Value::Null { | |
211 | config[key] = value; | |
212 | } else { | |
213 | bail!("duplicate property '{}'", key); | |
214 | } | |
215 | } | |
216 | Err(err) => { | |
217 | bail!("property '{}': {}", key, err.to_string()); | |
218 | } | |
219 | } | |
220 | } else { | |
221 | bail!("unknown property '{}'", key) | |
bdb631da | 222 | } |
9b50c161 DM |
223 | } else { |
224 | bail!("syntax error (expected section properties)"); | |
bdb631da | 225 | } |
bdb631da | 226 | } |
1fa897cf | 227 | } |
dd193a61 | 228 | } |
dd193a61 | 229 | |
9b50c161 DM |
230 | if let ParseState::InsideSection(plugin, section_id, config) = state { |
231 | // finish section | |
232 | test_required_properties(&config, &plugin.properties)?; | |
233 | create_section(§ion_id, &plugin.type_name, config); | |
234 | } | |
e189c93b | 235 | |
9b50c161 DM |
236 | Ok(()) |
237 | ||
238 | }).map_err(|e| format_err!("line {} - {}", line_no, e))?; | |
239 | ||
240 | Ok(result) | |
dd193a61 | 241 | |
9b50c161 | 242 | }).map_err(|e: Error| format_err!("parsing '{}' failed: {}", filename, e)) |
e471d699 DM |
243 | } |
244 | ||
bfb1d69a | 245 | pub fn default_format_section_header(type_name: &str, section_id: &str, _data: &Value) -> String { |
e189c93b DM |
246 | return format!("{}: {}\n", type_name, section_id); |
247 | } | |
248 | ||
1fa897cf DM |
249 | pub fn default_parse_section_content(line: &str) -> Option<(String, String)> { |
250 | ||
251 | if line.is_empty() { return None; } | |
252 | let first_char = line.chars().next().unwrap(); | |
253 | ||
254 | if !first_char.is_whitespace() { return None } | |
255 | ||
515688d1 | 256 | let mut kv_iter = line.trim_start().splitn(2, |c: char| c.is_whitespace()); |
1fa897cf DM |
257 | |
258 | let key = match kv_iter.next() { | |
259 | Some(v) => v.trim(), | |
260 | None => return None, | |
261 | }; | |
262 | ||
263 | if key.len() == 0 { return None; } | |
264 | ||
265 | let value = match kv_iter.next() { | |
266 | Some(v) => v.trim(), | |
267 | None => return None, | |
268 | }; | |
269 | ||
270 | Some((key.into(), value.into())) | |
271 | } | |
272 | ||
dd193a61 DM |
273 | pub fn default_parse_section_header(line: &str) -> Option<(String, String)> { |
274 | ||
0d97734e | 275 | if line.is_empty() { return None; }; |
dd193a61 DM |
276 | |
277 | let first_char = line.chars().next().unwrap(); | |
278 | ||
279 | if !first_char.is_alphabetic() { return None } | |
280 | ||
281 | let mut head_iter = line.splitn(2, ':'); | |
282 | ||
283 | let section_type = match head_iter.next() { | |
1fa897cf | 284 | Some(v) => v.trim(), |
dd193a61 DM |
285 | None => return None, |
286 | }; | |
e471d699 | 287 | |
dd193a61 DM |
288 | if section_type.len() == 0 { return None; } |
289 | ||
dd193a61 | 290 | let section_id = match head_iter.next() { |
1fa897cf | 291 | Some(v) => v.trim(), |
dd193a61 DM |
292 | None => return None, |
293 | }; | |
294 | ||
dd193a61 | 295 | Some((section_type.into(), section_id.into())) |
e471d699 | 296 | } |
e471d699 DM |
297 | } |
298 | ||
299 | ||
300 | // cargo test test_section_config1 -- --nocapture | |
301 | #[test] | |
302 | fn test_section_config1() { | |
303 | ||
304 | let filename = "storage.cfg"; | |
305 | ||
306 | //let mut file = File::open(filename).expect("file not found"); | |
307 | //let mut contents = String::new(); | |
308 | //file.read_to_string(&mut contents).unwrap(); | |
309 | ||
255f378a DM |
310 | const PROPERTIES: ObjectSchema = ObjectSchema::new( |
311 | "lvmthin properties", | |
312 | &[ | |
313 | ("content", true, &StringSchema::new("Storage content types.").schema()), | |
314 | ("thinpool", false, &StringSchema::new("LVM thin pool name.").schema()), | |
315 | ("vgname", false, &StringSchema::new("LVM volume group name.").schema()), | |
316 | ], | |
803b95a0 | 317 | ); |
e471d699 | 318 | |
255f378a DM |
319 | let plugin = SectionConfigPlugin::new("lvmthin".to_string(), &PROPERTIES); |
320 | ||
321 | const ID_SCHEMA: Schema = StringSchema::new("Storage ID schema.") | |
826698d5 | 322 | .min_length(3) |
255f378a | 323 | .schema(); |
826698d5 | 324 | |
255f378a | 325 | let mut config = SectionConfig::new(&ID_SCHEMA); |
803b95a0 | 326 | config.register_plugin(plugin); |
e471d699 DM |
327 | |
328 | let raw = r" | |
dd193a61 | 329 | |
e471d699 DM |
330 | lvmthin: local-lvm |
331 | thinpool data | |
332 | vgname pve5 | |
333 | content rootdir,images | |
e189c93b DM |
334 | |
335 | lvmthin: local-lvm2 | |
336 | thinpool data | |
337 | vgname pve5 | |
338 | content rootdir,images | |
e471d699 DM |
339 | "; |
340 | ||
b943ed8d DM |
341 | let res = config.parse(filename, &raw); |
342 | println!("RES: {:?}", res); | |
c6ed6cac DM |
343 | let raw = config.write(filename, &res.unwrap()); |
344 | println!("CONFIG:\n{}", raw.unwrap()); | |
345 | ||
e471d699 | 346 | |
22245422 | 347 | } |