proxmox-backup/src/section_config.rs

335 lines
11 KiB
Rust
Raw Normal View History

2018-11-26 17:01:24 +00:00
use failure::*;
2018-11-19 07:07:22 +00:00
use std::fs::File;
use std::io::Read;
use std::collections::HashMap;
2018-11-28 13:09:35 +00:00
use std::collections::HashSet;
use std::collections::LinkedList;
2018-11-19 07:07:22 +00:00
use serde_json::{json, Value};
2018-11-27 07:41:25 +00:00
use std::sync::Arc;
use crate::api::schema::*;
2018-11-19 07:07:22 +00:00
pub struct SectionConfigPlugin {
type_name: String,
2018-11-27 07:41:25 +00:00
properties: ObjectSchema,
2018-11-19 07:07:22 +00:00
}
2018-11-19 05:47:39 +00:00
2018-11-27 07:41:25 +00:00
impl SectionConfigPlugin {
pub fn new(type_name: String, properties: ObjectSchema) -> Self {
Self { type_name, properties }
}
}
2018-11-19 05:47:39 +00:00
2018-11-19 07:07:22 +00:00
pub struct SectionConfig {
plugins: HashMap<String, SectionConfigPlugin>,
2018-11-19 05:47:39 +00:00
id_schema: Arc<Schema>,
2018-11-27 11:54:40 +00:00
parse_section_header: fn(&str) -> Option<(String, String)>,
parse_section_content: fn(&str) -> Option<(String, String)>,
2018-11-28 13:09:35 +00:00
format_section_header: fn(type_name: &str, section_id: &str, data: &Value) -> String,
2018-11-26 17:01:24 +00:00
}
enum ParseState<'a> {
2018-11-26 17:01:24 +00:00
BeforeHeader,
InsideSection(&'a SectionConfigPlugin, String, Value),
2018-11-19 05:47:39 +00:00
}
#[derive(Debug)]
pub struct SectionConfigData {
2018-11-28 13:09:35 +00:00
sections: HashMap<String, (String, Value)>,
order: LinkedList<String>,
}
impl SectionConfigData {
pub fn new() -> Self {
2018-11-28 13:09:35 +00:00
Self { sections: HashMap::new(), order: LinkedList::new() }
}
2018-11-28 13:09:35 +00:00
pub fn set_data(&mut self, section_id: &str, type_name: &str, config: Value) {
// fixme: verify section_id schema here??
self.sections.insert(section_id.to_string(), (type_name.to_string(), config));
}
2018-11-28 13:09:35 +00:00
fn record_order(&mut self, section_id: &str) {
self.order.push_back(section_id.to_string());
}
}
2018-11-19 05:47:39 +00:00
impl SectionConfig {
pub fn new(id_schema: Arc<Schema>) -> Self {
2018-11-19 07:07:22 +00:00
Self {
plugins: HashMap::new(),
id_schema: id_schema,
2018-11-19 07:07:22 +00:00
parse_section_header: SectionConfig::default_parse_section_header,
2018-11-27 08:01:36 +00:00
parse_section_content: SectionConfig::default_parse_section_content,
2018-11-28 13:09:35 +00:00
format_section_header: SectionConfig::default_format_section_header,
2018-11-19 07:07:22 +00:00
}
}
2018-11-27 07:41:25 +00:00
pub fn register_plugin(&mut self, plugin: SectionConfigPlugin) {
self.plugins.insert(plugin.type_name.clone(), plugin);
}
2018-11-28 13:09:35 +00:00
pub fn write(&self, filename: &str, config: &SectionConfigData) -> Result<(), Error> {
let mut list = LinkedList::new();
let mut done = HashSet::new();
for id in &config.order {
if config.sections.get(id) == None { continue };
list.push_back(id);
done.insert(id);
}
for (id, _) in &config.sections {
if done.contains(id) { continue };
list.push_back(id);
}
let mut raw = String::new();
for id in list {
let (type_name, section_config) = config.sections.get(id).unwrap();
let plugin = self.plugins.get(type_name).unwrap();
// fixme: verify json data
println!("REAL WRITE {} {} {:?}\n", id, type_name, section_config);
let head = (self.format_section_header)(type_name, id, section_config);
if !raw.is_empty() { raw += "\n" }
raw += &head;
for (key, value) in section_config.as_object().unwrap() {
let text = match value {
2018-11-29 08:33:27 +00:00
Value::Null => { continue; }, // do nothing (delete)
Value::Bool(v) => v.to_string(),
Value::String(v) => v.to_string(),
Value::Number(v) => v.to_string(),
2018-11-28 13:09:35 +00:00
_ => {
bail!("file {}: got unsupported type in section {} key {}", filename, id, key);
},
};
raw += "\t";
raw += &key;
raw += " ";
raw += &text;
raw += "\n";
}
println!("CONFIG:\n{}", raw);
}
Ok(())
}
pub fn parse(&self, filename: &str, raw: &str) -> Result<SectionConfigData, Error> {
2018-11-26 17:01:24 +00:00
let mut line_no = 0;
let mut state = ParseState::BeforeHeader;
2018-11-19 07:07:22 +00:00
2018-11-27 10:55:21 +00:00
let test_required_properties = |value: &Value, schema: &ObjectSchema| -> Result<(), Error> {
for (name, (optional, _prop_schema)) in &schema.properties {
if *optional == false && value[name] == Value::Null {
return Err(format_err!("property '{}' is missing and it is not optional.", name));
}
}
Ok(())
};
let mut result = SectionConfigData::new();
2018-11-27 11:54:40 +00:00
2018-11-28 13:09:35 +00:00
let mut create_section = |section_id: &str, type_name: &str, config| {
result.set_data(section_id, type_name, config);
result.record_order(section_id);
2018-11-27 11:54:40 +00:00
};
2018-11-19 07:07:22 +00:00
for line in raw.lines() {
2018-11-26 17:01:24 +00:00
line_no += 1;
match state {
ParseState::BeforeHeader => {
if line.trim().is_empty() { continue; }
if let Some((section_type, section_id)) = (self.parse_section_header)(line) {
println!("OKLINE: type: {} ID: {}", section_type, section_id);
if let Some(ref plugin) = self.plugins.get(&section_type) {
if let Err(err) = parse_simple_value(&section_id, &self.id_schema) {
bail!("file '{}' line {} - syntax error in section identifier: {}",
filename, line_no, err.to_string());
}
state = ParseState::InsideSection(plugin, section_id, json!({}));
} else {
bail!("file '{}' line {} - unknown section type '{}'",
filename, line_no, section_type);
}
2018-11-26 17:01:24 +00:00
} else {
bail!("file '{}' line {} - syntax error (expected header)", filename, line_no);
2018-11-26 17:01:24 +00:00
}
}
2018-11-27 13:10:16 +00:00
ParseState::InsideSection(plugin, ref mut section_id, ref mut config) => {
2018-11-26 17:01:24 +00:00
if line.trim().is_empty() {
// finish section
2018-11-27 10:55:21 +00:00
if let Err(err) = test_required_properties(config, &plugin.properties) {
bail!("file '{}' line {} - {}", filename, line_no, err.to_string());
}
2018-11-28 13:09:35 +00:00
create_section(section_id, &plugin.type_name, config.take());
2018-11-26 17:01:24 +00:00
state = ParseState::BeforeHeader;
continue;
}
println!("CONTENT: {}", line);
2018-11-27 08:01:36 +00:00
if let Some((key, value)) = (self.parse_section_content)(line) {
println!("CONTENT: key: {} value: {}", key, value);
2018-11-27 10:55:21 +00:00
if let Some((_optional, prop_schema)) = plugin.properties.properties.get::<str>(&key) {
match parse_simple_value(&value, prop_schema) {
Ok(value) => {
if config[&key] == Value::Null {
config[key] = value;
} else {
bail!("file '{}' line {} - duplicate property '{}'",
filename, line_no, key);
}
}
Err(err) => {
bail!("file '{}' line {} - property '{}': {}",
filename, line_no, key, err.to_string());
}
}
} else {
bail!("file '{}' line {} - unknown property '{}'", filename, line_no, key)
}
2018-11-27 08:01:36 +00:00
} else {
bail!("file '{}' line {} - syntax error (expected section properties)", filename, line_no);
}
2018-11-26 17:01:24 +00:00
}
}
}
2018-11-27 13:10:16 +00:00
if let ParseState::InsideSection(plugin, section_id, config) = state {
2018-11-26 17:01:24 +00:00
// finish section
2018-11-28 13:09:35 +00:00
2018-11-27 13:10:16 +00:00
if let Err(err) = test_required_properties(&config, &plugin.properties) {
2018-11-27 10:55:21 +00:00
bail!("file '{}' line {} - {}", filename, line_no, err.to_string());
}
2018-11-28 13:09:35 +00:00
create_section(&section_id, &plugin.type_name, config);
2018-11-19 07:07:22 +00:00
}
2018-11-26 17:01:24 +00:00
2018-11-27 11:54:40 +00:00
Ok(result)
2018-11-19 07:07:22 +00:00
}
2018-11-28 13:09:35 +00:00
pub fn default_format_section_header(type_name: &str, section_id: &str, data: &Value) -> String {
return format!("{}: {}\n", type_name, section_id);
}
2018-11-27 08:01:36 +00:00
pub fn default_parse_section_content(line: &str) -> Option<(String, String)> {
if line.is_empty() { return None; }
let first_char = line.chars().next().unwrap();
if !first_char.is_whitespace() { return None }
let mut kv_iter = line.trim_left().splitn(2, |c: char| c.is_whitespace());
let key = match kv_iter.next() {
Some(v) => v.trim(),
None => return None,
};
if key.len() == 0 { return None; }
let value = match kv_iter.next() {
Some(v) => v.trim(),
None => return None,
};
Some((key.into(), value.into()))
}
2018-11-26 17:01:24 +00:00
pub fn default_parse_section_header(line: &str) -> Option<(String, String)> {
if line.is_empty() { return None; };
2018-11-26 17:01:24 +00:00
let first_char = line.chars().next().unwrap();
if !first_char.is_alphabetic() { return None }
let mut head_iter = line.splitn(2, ':');
let section_type = match head_iter.next() {
2018-11-27 08:01:36 +00:00
Some(v) => v.trim(),
2018-11-26 17:01:24 +00:00
None => return None,
};
2018-11-19 07:07:22 +00:00
2018-11-26 17:01:24 +00:00
if section_type.len() == 0 { return None; }
// fixme: verify format
let section_id = match head_iter.next() {
2018-11-27 08:01:36 +00:00
Some(v) => v.trim(),
2018-11-26 17:01:24 +00:00
None => return None,
};
Some((section_type.into(), section_id.into()))
2018-11-19 07:07:22 +00:00
}
}
// cargo test test_section_config1 -- --nocapture
#[test]
fn test_section_config1() {
let filename = "storage.cfg";
//let mut file = File::open(filename).expect("file not found");
//let mut contents = String::new();
//file.read_to_string(&mut contents).unwrap();
2018-11-27 07:41:25 +00:00
let plugin = SectionConfigPlugin::new(
"lvmthin".to_string(),
ObjectSchema::new("lvmthin properties")
.required("thinpool", StringSchema::new("LVM thin pool name."))
.required("vgname", StringSchema::new("LVM volume group name."))
2018-11-27 10:55:21 +00:00
.optional("content", StringSchema::new("Storage content types."))
2018-11-27 07:41:25 +00:00
);
2018-11-19 07:07:22 +00:00
let id_schema = StringSchema::new("Storage ID schema.")
.min_length(3)
.into();
let mut config = SectionConfig::new(id_schema);
2018-11-27 07:41:25 +00:00
config.register_plugin(plugin);
2018-11-19 07:07:22 +00:00
let raw = r"
2018-11-26 17:01:24 +00:00
2018-11-19 07:07:22 +00:00
lvmthin: local-lvm
thinpool data
vgname pve5
content rootdir,images
2018-11-28 13:09:35 +00:00
lvmthin: local-lvm2
thinpool data
vgname pve5
content rootdir,images
2018-11-19 07:07:22 +00:00
";
let res = config.parse(filename, &raw);
println!("RES: {:?}", res);
2018-11-28 13:09:35 +00:00
config.write(filename, &res.unwrap());
2018-11-19 07:07:22 +00:00
2018-11-19 05:47:39 +00:00
}