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