]> git.proxmox.com Git - proxmox-backup.git/blame - src/section_config.rs
src/bin/proxmox-backup-client.rs: use async fn
[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
cad540e9 9use proxmox::api::schema::*;
e18a6c9e
DM
10use proxmox::tools::try_block;
11
e471d699
DM
12pub struct SectionConfigPlugin {
13 type_name: String,
255f378a 14 properties: &'static ObjectSchema,
e471d699 15}
22245422 16
803b95a0
DM
17impl SectionConfigPlugin {
18
255f378a 19 pub fn new(type_name: String, properties: &'static ObjectSchema) -> Self {
803b95a0
DM
20 Self { type_name, properties }
21 }
22
23}
22245422 24
e471d699
DM
25pub struct SectionConfig {
26 plugins: HashMap<String, SectionConfigPlugin>,
22245422 27
255f378a 28 id_schema: &'static 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
255f378a 75 pub fn new(id_schema: &'static Schema) -> Self {
e471d699
DM
76 Self {
77 plugins: HashMap::new(),
653b1ca1 78 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 147
dd193a61 148 let mut state = ParseState::BeforeHeader;
e471d699 149
bdb631da 150 let test_required_properties = |value: &Value, schema: &ObjectSchema| -> Result<(), Error> {
255f378a 151 for (name, optional, _prop_schema) in schema.properties {
379ea0ed
DM
152 if *optional == false && value[name] == Value::Null {
153 return Err(format_err!("property '{}' is missing and it is not optional.", name));
bdb631da
DM
154 }
155 }
156 Ok(())
157 };
158
9b50c161 159 let mut line_no = 0;
ee7fc433 160
9b50c161 161 try_block!({
ee7fc433 162
9b50c161 163 let mut result = SectionConfigData::new();
dd193a61 164
9b50c161
DM
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 };
dd193a61 169
9b50c161
DM
170 try_block!({
171 for line in raw.lines() {
172 line_no += 1;
dd193a61 173
9b50c161 174 match state {
dd193a61 175
9b50c161 176 ParseState::BeforeHeader => {
dd193a61 177
9b50c161
DM
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());
bdb631da 185 }
9b50c161
DM
186 state = ParseState::InsideSection(plugin, section_id, json!({}));
187 } else {
188 bail!("unknown section type '{}'", section_type);
bdb631da 189 }
9b50c161
DM
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
255f378a 206 if let Some((_optional, prop_schema)) = plugin.properties.lookup(&key) {
9b50c161
DM
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)
bdb631da 221 }
9b50c161
DM
222 } else {
223 bail!("syntax error (expected section properties)");
bdb631da 224 }
bdb631da 225 }
1fa897cf 226 }
dd193a61 227 }
dd193a61 228
9b50c161
DM
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 }
e189c93b 234
9b50c161
DM
235 Ok(())
236
237 }).map_err(|e| format_err!("line {} - {}", line_no, e))?;
238
239 Ok(result)
dd193a61 240
9b50c161 241 }).map_err(|e: Error| format_err!("parsing '{}' failed: {}", filename, e))
e471d699
DM
242 }
243
bfb1d69a 244 pub fn default_format_section_header(type_name: &str, section_id: &str, _data: &Value) -> String {
e189c93b
DM
245 return format!("{}: {}\n", type_name, section_id);
246 }
247
1fa897cf
DM
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
515688d1 255 let mut kv_iter = line.trim_start().splitn(2, |c: char| c.is_whitespace());
1fa897cf
DM
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
dd193a61
DM
272 pub fn default_parse_section_header(line: &str) -> Option<(String, String)> {
273
0d97734e 274 if line.is_empty() { return None; };
dd193a61
DM
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() {
1fa897cf 283 Some(v) => v.trim(),
dd193a61
DM
284 None => return None,
285 };
e471d699 286
dd193a61
DM
287 if section_type.len() == 0 { return None; }
288
dd193a61 289 let section_id = match head_iter.next() {
1fa897cf 290 Some(v) => v.trim(),
dd193a61
DM
291 None => return None,
292 };
293
dd193a61 294 Some((section_type.into(), section_id.into()))
e471d699 295 }
e471d699
DM
296}
297
298
299// cargo test test_section_config1 -- --nocapture
300#[test]
301fn 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
255f378a
DM
309 const PROPERTIES: ObjectSchema = ObjectSchema::new(
310 "lvmthin properties",
311 &[
312 ("content", true, &StringSchema::new("Storage content types.").schema()),
313 ("thinpool", false, &StringSchema::new("LVM thin pool name.").schema()),
314 ("vgname", false, &StringSchema::new("LVM volume group name.").schema()),
315 ],
803b95a0 316 );
e471d699 317
255f378a
DM
318 let plugin = SectionConfigPlugin::new("lvmthin".to_string(), &PROPERTIES);
319
320 const ID_SCHEMA: Schema = StringSchema::new("Storage ID schema.")
826698d5 321 .min_length(3)
255f378a 322 .schema();
826698d5 323
255f378a 324 let mut config = SectionConfig::new(&ID_SCHEMA);
803b95a0 325 config.register_plugin(plugin);
e471d699
DM
326
327 let raw = r"
dd193a61 328
e471d699
DM
329lvmthin: local-lvm
330 thinpool data
331 vgname pve5
332 content rootdir,images
e189c93b
DM
333
334lvmthin: local-lvm2
335 thinpool data
336 vgname pve5
337 content rootdir,images
e471d699
DM
338";
339
b943ed8d
DM
340 let res = config.parse(filename, &raw);
341 println!("RES: {:?}", res);
c6ed6cac
DM
342 let raw = config.write(filename, &res.unwrap());
343 println!("CONFIG:\n{}", raw.unwrap());
344
e471d699 345
22245422 346}