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