]> git.proxmox.com Git - proxmox-backup.git/blame - src/section_config.rs
backup-client: add bash completion for datastore names
[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
DM
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
92fb0784 161 let mut result = SectionConfigData::new();
ee7fc433 162
e189c93b
DM
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);
ee7fc433
DM
166 };
167
e471d699 168 for line in raw.lines() {
dd193a61
DM
169 line_no += 1;
170
dd193a61
DM
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) {
91643d90 178 //println!("OKLINE: type: {} ID: {}", section_type, section_id);
b943ed8d 179 if let Some(ref plugin) = self.plugins.get(&section_type) {
826698d5
DM
180 if let Err(err) = parse_simple_value(&section_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!({}));
b943ed8d
DM
185 } else {
186 bail!("file '{}' line {} - unknown section type '{}'",
187 filename, line_no, section_type);
188 }
dd193a61 189 } else {
b943ed8d 190 bail!("file '{}' line {} - syntax error (expected header)", filename, line_no);
dd193a61
DM
191 }
192 }
e3a22170 193 ParseState::InsideSection(plugin, ref mut section_id, ref mut config) => {
dd193a61
DM
194
195 if line.trim().is_empty() {
196 // finish section
bdb631da
DM
197 if let Err(err) = test_required_properties(config, &plugin.properties) {
198 bail!("file '{}' line {} - {}", filename, line_no, err.to_string());
199 }
e189c93b 200 create_section(section_id, &plugin.type_name, config.take());
dd193a61
DM
201 state = ParseState::BeforeHeader;
202 continue;
203 }
1fa897cf 204 if let Some((key, value)) = (self.parse_section_content)(line) {
91643d90 205 //println!("CONTENT: key: {} value: {}", key, value);
bdb631da
DM
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 }
1fa897cf
DM
225 } else {
226 bail!("file '{}' line {} - syntax error (expected section properties)", filename, line_no);
227 }
dd193a61
DM
228 }
229 }
230 }
231
e3a22170 232 if let ParseState::InsideSection(plugin, section_id, config) = state {
dd193a61 233 // finish section
e189c93b 234
e3a22170 235 if let Err(err) = test_required_properties(&config, &plugin.properties) {
bdb631da
DM
236 bail!("file '{}' line {} - {}", filename, line_no, err.to_string());
237 }
e189c93b 238 create_section(&section_id, &plugin.type_name, config);
e471d699 239 }
dd193a61 240
ee7fc433 241 Ok(result)
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
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
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
803b95a0
DM
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."))
bdb631da 314 .optional("content", StringSchema::new("Storage content types."))
803b95a0 315 );
e471d699 316
826698d5
DM
317 let id_schema = StringSchema::new("Storage ID schema.")
318 .min_length(3)
319 .into();
320
321 let mut config = SectionConfig::new(id_schema);
803b95a0 322 config.register_plugin(plugin);
e471d699
DM
323
324 let raw = r"
dd193a61 325
e471d699
DM
326lvmthin: local-lvm
327 thinpool data
328 vgname pve5
329 content rootdir,images
e189c93b
DM
330
331lvmthin: local-lvm2
332 thinpool data
333 vgname pve5
334 content rootdir,images
e471d699
DM
335";
336
b943ed8d
DM
337 let res = config.parse(filename, &raw);
338 println!("RES: {:?}", res);
c6ed6cac
DM
339 let raw = config.write(filename, &res.unwrap());
340 println!("CONFIG:\n{}", raw.unwrap());
341
e471d699 342
22245422 343}