]> git.proxmox.com Git - proxmox-backup.git/blob - src/section_config.rs
8f5529770ffc2a733022c7bbb38eca49aa238202
[proxmox-backup.git] / src / section_config.rs
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(&section_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 state = ParseState::BeforeHeader;
149
150 let test_required_properties = |value: &Value, schema: &ObjectSchema| -> Result<(), Error> {
151 for (name, (optional, _prop_schema)) in &schema.properties {
152 if *optional == false && value[name] == Value::Null {
153 return Err(format_err!("property '{}' is missing and it is not optional.", name));
154 }
155 }
156 Ok(())
157 };
158
159 let mut line_no = 0;
160
161 try_block!({
162
163 let mut result = SectionConfigData::new();
164
165 let mut create_section = |section_id: &str, type_name: &str, config| {
166 result.set_data(section_id, type_name, config);
167 result.record_order(section_id);
168 };
169
170 try_block!({
171 for line in raw.lines() {
172 line_no += 1;
173
174 match state {
175
176 ParseState::BeforeHeader => {
177
178 if line.trim().is_empty() { continue; }
179
180 if let Some((section_type, section_id)) = (self.parse_section_header)(line) {
181 //println!("OKLINE: type: {} ID: {}", section_type, section_id);
182 if let Some(ref plugin) = self.plugins.get(&section_type) {
183 if let Err(err) = parse_simple_value(&section_id, &self.id_schema) {
184 bail!("syntax error in section identifier: {}", err.to_string());
185 }
186 state = ParseState::InsideSection(plugin, section_id, json!({}));
187 } else {
188 bail!("unknown section type '{}'", section_type);
189 }
190 } else {
191 bail!("syntax error (expected header)");
192 }
193 }
194 ParseState::InsideSection(plugin, ref mut section_id, ref mut config) => {
195
196 if line.trim().is_empty() {
197 // finish section
198 test_required_properties(config, &plugin.properties)?;
199 create_section(section_id, &plugin.type_name, config.take());
200 state = ParseState::BeforeHeader;
201 continue;
202 }
203 if let Some((key, value)) = (self.parse_section_content)(line) {
204 //println!("CONTENT: key: {} value: {}", key, value);
205
206 if let Some((_optional, prop_schema)) = plugin.properties.properties.get::<str>(&key) {
207 match parse_simple_value(&value, prop_schema) {
208 Ok(value) => {
209 if config[&key] == Value::Null {
210 config[key] = value;
211 } else {
212 bail!("duplicate property '{}'", key);
213 }
214 }
215 Err(err) => {
216 bail!("property '{}': {}", key, err.to_string());
217 }
218 }
219 } else {
220 bail!("unknown property '{}'", key)
221 }
222 } else {
223 bail!("syntax error (expected section properties)");
224 }
225 }
226 }
227 }
228
229 if let ParseState::InsideSection(plugin, section_id, config) = state {
230 // finish section
231 test_required_properties(&config, &plugin.properties)?;
232 create_section(&section_id, &plugin.type_name, config);
233 }
234
235 Ok(())
236
237 }).map_err(|e| format_err!("line {} - {}", line_no, e))?;
238
239 Ok(result)
240
241 }).map_err(|e: Error| format_err!("parsing '{}' failed: {}", filename, e))
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 }