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