zap_model/
task.rs

1use log::*;
2use pest::error::Error as PestError;
3use pest::error::ErrorVariant;
4use pest::iterators::Pairs;
5use pest::Parser;
6use std::collections::HashMap;
7use std::fs::File;
8use std::io::Read;
9use std::path::PathBuf;
10
11#[derive(Parser)]
12#[grammar = "task.pest"]
13struct TaskParser;
14
15/**
16 * A Script represents something that can be executed oa a remote host.
17 *
18 * These come in two variants:
19 *   - Inline string of shell commands to run
20 *   - A script or binary file to transfer and execute
21 */
22#[derive(Clone, Debug)]
23pub struct Script {
24    /**
25     * Inline scripts will have parameters rendered into then with handlebars syntax
26     */
27    pub inline: Option<String>,
28    /**
29     * File scripts will be executed with the parameters passed as command line
30     * arguments, e.g. the "msg" parameter would be passed as:
31     *      ./file --msg=value
32     */
33    pub file: Option<PathBuf>,
34}
35
36impl Script {
37    fn new() -> Self {
38        Self {
39            inline: None,
40            file: None,
41        }
42    }
43
44    pub fn has_file(&self) -> bool {
45        self.file.is_some()
46    }
47
48    /**
49     * Return the script's contents as bytes
50     *
51     * This is useful for transferring the script to another host for execution
52     *
53     * If the `file` member is defined, that will be preferred, even if `inline` is also defined
54     */
55    pub fn as_bytes(&self, parameters: Option<&HashMap<String, String>>) -> Option<Vec<u8>> {
56        use handlebars::Handlebars;
57
58        if self.inline.is_some() && self.file.is_some() {
59            warn!("Both inline and file structs are defined for this script, only file will be used!\n({})",
60            self.inline.as_ref().unwrap());
61        }
62
63        if let Some(path) = &self.file {
64            match File::open(path) {
65                Ok(mut file) => {
66                    let mut buf = vec![];
67
68                    if let Ok(count) = file.read_to_end(&mut buf) {
69                        debug!("Read {} bytes of {}", count, path.display());
70                        return Some(buf);
71                    } else {
72                        error!("Failed to read the file {}", path.display());
73                    }
74                }
75                Err(err) => {
76                    error!("Failed to open the file at {},  {:?}", path.display(), err);
77                }
78            }
79        }
80
81        if let Some(inline) = &self.inline {
82            // Early exit if there are no parameters to render
83            if parameters.is_none() {
84                return Some(inline.as_bytes().to_vec());
85            }
86
87            let parameters = parameters.unwrap();
88
89            let mut hb = Handlebars::new();
90            hb.register_escape_fn(handlebars::no_escape);
91            match hb.render_template(inline, &parameters) {
92                Ok(rendered) => {
93                    return Some(rendered.as_bytes().to_vec());
94                }
95                Err(err) => {
96                    error!("Failed to render command ({:?}): {}", err, inline);
97                    return Some(inline.as_bytes().to_vec());
98                }
99            }
100        }
101
102        None
103    }
104}
105
106#[derive(Clone, Debug)]
107pub struct Task {
108    pub name: String,
109    pub script: Script,
110}
111
112impl Task {
113    pub fn new(name: &str) -> Self {
114        Task {
115            name: name.to_string(),
116            script: Script::new(),
117        }
118    }
119
120    fn parse(parser: &mut Pairs<Rule>) -> Result<Self, PestError<Rule>> {
121        let mut task: Option<Self> = None;
122        let mut inline = None;
123        let mut file = None;
124
125        while let Some(parsed) = parser.next() {
126            match parsed.as_rule() {
127                Rule::identifier => {
128                    task = Some(Task::new(parsed.as_str()));
129                }
130                Rule::script => {
131                    for pair in parsed.into_inner() {
132                        match pair.as_rule() {
133                            Rule::script_inline => {
134                                inline = Some(parse_str(&mut pair.into_inner())?);
135                            }
136                            Rule::script_file => {
137                                let path = parse_str(&mut pair.into_inner())?;
138                                file = Some(PathBuf::from(path));
139                            }
140                            _ => {}
141                        }
142                    }
143                }
144                _ => {}
145            }
146        }
147
148        if let Some(mut task) = task {
149            task.script.inline = inline;
150            task.script.file = file;
151
152            return Ok(task);
153        } else {
154            return Err(PestError::new_from_pos(
155                ErrorVariant::CustomError {
156                    message: "Could not find a valid task definition".to_string(),
157                },
158                /* TODO: Find a better thing to report */
159                pest::Position::from_start(""),
160            ));
161        }
162    }
163
164    pub fn from_str(buf: &str) -> Result<Self, PestError<Rule>> {
165        let mut parser = TaskParser::parse(Rule::taskfile, buf)?;
166        while let Some(parsed) = parser.next() {
167            match parsed.as_rule() {
168                Rule::task => {
169                    return Task::parse(&mut parsed.into_inner());
170                }
171                _ => {}
172            }
173        }
174
175        Err(PestError::new_from_pos(
176            ErrorVariant::CustomError {
177                message: "Could not find a valid task definition".to_string(),
178            },
179            pest::Position::from_start(buf),
180        ))
181    }
182
183    pub fn from_path(path: &PathBuf) -> Result<Self, PestError<Rule>> {
184        match File::open(path) {
185            Ok(mut file) => {
186                let mut contents = String::new();
187
188                if let Err(e) = file.read_to_string(&mut contents) {
189                    return Err(PestError::new_from_pos(
190                        ErrorVariant::CustomError {
191                            message: format!("{}", e),
192                        },
193                        pest::Position::from_start(""),
194                    ));
195                } else {
196                    return Self::from_str(&contents);
197                }
198            }
199            Err(e) => {
200                return Err(PestError::new_from_pos(
201                    ErrorVariant::CustomError {
202                        message: format!("{}", e),
203                    },
204                    pest::Position::from_start(""),
205                ));
206            }
207        }
208    }
209}
210
211/**
212 * Parser utility function to fish out the _actual_ string value for something
213 * that is looking like a string Rule
214 */
215fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
216    while let Some(parsed) = parser.next() {
217        match parsed.as_rule() {
218            Rule::string => {
219                return parse_str(&mut parsed.into_inner());
220            }
221            Rule::triple_quoted => {
222                return parse_str(&mut parsed.into_inner());
223            }
224            Rule::single_quoted => {
225                return parse_str(&mut parsed.into_inner());
226            }
227            Rule::inner_single_str => {
228                return Ok(parsed.as_str().to_string());
229            }
230            Rule::inner_triple_str => {
231                return Ok(parsed.as_str().to_string());
232            }
233            _ => {}
234        }
235    }
236    return Err(PestError::new_from_pos(
237        ErrorVariant::CustomError {
238            message: "Could not parse out a string value".to_string(),
239        },
240        /* TODO: Find a better thing to report */
241        pest::Position::from_start(""),
242    ));
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    #[test]
249    fn parse_task_with_comments() {
250        let buf = r#"
251        /*
252         * This is a simple one
253         */
254        task Hey {
255                script {
256                    inline = 'echo "hi"'
257                }
258            }"#;
259        let _task = TaskParser::parse(Rule::taskfile, buf)
260            .unwrap()
261            .next()
262            .unwrap();
263    }
264
265    #[test]
266    fn parse_simple_script_task() {
267        let buf = r#"task Install {
268                parameters {
269                    package {
270                        required = true
271                        help = 'Name of package to be installed'
272                        type = string
273                    }
274                }
275                script {
276                    inline = 'zypper in -y {{package}}'
277                }
278            }"#;
279        let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
280    }
281
282    #[test]
283    fn parse_no_parameters() {
284        let buf = r#"task PrintEnv {
285                script {
286                    inline = 'env'
287                }
288            }"#;
289        let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
290    }
291
292    #[test]
293    fn parse_task_fn() {
294        let buf = r#"task PrintEnv {
295                script {
296                    inline = 'env'
297                }
298            }"#;
299        let task = Task::from_str(buf).expect("Failed to parse the task");
300        assert_eq!(task.name, "PrintEnv");
301
302        let script = task.script;
303
304        assert_eq!(script.as_bytes(None).unwrap(), "env".as_bytes());
305    }
306
307    #[test]
308    fn parse_task_fn_with_triple_quotes() {
309        let buf = r#"task PrintEnv {
310                script {
311                    inline = '''env'''
312                }
313            }"#;
314        let task = Task::from_str(buf).expect("Failed to parse the task");
315        assert_eq!(task.name, "PrintEnv");
316
317        let script = task.script;
318        assert_eq!(script.as_bytes(None).unwrap(), "env".as_bytes());
319    }
320}