]> git.proxmox.com Git - proxmox-backup.git/blame - src/section_config.rs
use const api definitions
[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
e18a6c9e
DM
9use proxmox::tools::try_block;
10
ef2f2efb 11use crate::api_schema::*;
803b95a0 12
e471d699
DM
13pub struct SectionConfigPlugin {
14 type_name: String,
255f378a 15 properties: &'static ObjectSchema,
e471d699 16}
22245422 17
803b95a0
DM
18impl 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
26pub 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 35enum ParseState<'a> {
dd193a61 36 BeforeHeader,
826698d5 37 InsideSection(&'a SectionConfigPlugin, String, Value),
22245422
DM
38}
39
92fb0784
DM
40#[derive(Debug)]
41pub struct SectionConfigData {
b65eaac6 42 pub sections: HashMap<String, (String, Value)>,
a27b905c 43 order: VecDeque<String>,
92fb0784
DM
44}
45
46impl 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
74impl 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(&section_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(&section_type) {
184 if let Err(err) = parse_simple_value(&section_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(&section_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]
302fn 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
330lvmthin: local-lvm
331 thinpool data
332 vgname pve5
333 content rootdir,images
e189c93b
DM
334
335lvmthin: 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}