playbook_api/
lib.rs

1#[macro_use]
2extern crate log;
3
4#[macro_use]
5extern crate serde_derive;
6
7// #[macro_use]
8extern crate itertools;
9
10extern crate yaml_rust;
11extern crate ymlctx;
12extern crate colored;
13extern crate regex;
14extern crate nix;
15extern crate impersonate;
16extern crate serde_json;
17extern crate uuid;
18extern crate libc;
19
20#[cfg(feature = "lang_python")]
21extern crate pyo3;
22
23#[cfg(feature = "as_switch")]
24extern crate handlebars;
25
26pub use ymlctx::context::{Context, CtxObj};
27pub mod lang;
28pub mod builtins;
29pub mod systems;
30
31use std::str;
32use std::path::Path;
33use std::fs::File;
34use std::io::prelude::*;
35use std::io::{BufReader, Write};
36use std::collections::HashMap;
37use std::result::Result;
38use std::collections::HashSet;
39use yaml_rust::YamlLoader;
40use colored::*;
41use regex::Regex;
42use builtins::{TransientContext, ExitCode};
43use systems::Infrastructure;
44
45#[derive(Debug, Clone, PartialEq)]
46pub enum TaskErrorSource {
47    NixError(nix::Error),
48    ExitCode(i32),
49    Signal(nix::sys::signal::Signal),
50    Internal,
51    ExternalAPIError
52}
53
54#[derive(Debug, Clone, PartialEq)]
55pub struct TaskError {
56    msg: String,
57    src: TaskErrorSource
58}
59
60impl std::fmt::Display for TaskError {
61    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
62        write!(f, "{}", &self.msg)
63    }
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct Closure {
68    #[serde(rename = "c")]
69    container: u8,
70    #[serde(rename = "p")]
71    step_ptr: usize,
72    #[serde(rename = "s")]
73    pub ctx_states: Context,
74}
75
76#[test]
77fn test_closure_deserialize00() {
78    let closure_str = r#"{"c":1,"p":0,"s":{"data":{}}}"#;
79    assert_eq!(serde_json::from_str::<Closure>(closure_str).unwrap(), Closure {
80        container: 1,
81        step_ptr: 0,
82        ctx_states: Context::new()
83    });
84}
85
86#[test]
87fn test_closure_deserialize01() {
88    let closure_str = r#"{"c":1,"p":0,"s":{"data":{"playbook":{"Str":"tests/test1/say_hi.yml"}}}}"#;
89    assert_eq!(serde_json::from_str::<Closure>(closure_str).unwrap(), Closure {
90        container: 1,
91        step_ptr: 0,
92        ctx_states: Context::new().set("playbook", CtxObj::Str(String::from("tests/test1/say_hi.yml")))
93    });
94}
95
96#[test]
97fn test_closure_deserialize02() {
98    let closure_str = r#"{"c":1,"p":1,"s":{"data":{"playbook":{"Str":"tests/test1/test_sys_vars.yml"},"message":{"Str":"Salut!"}}}}"#;
99    assert_eq!(serde_json::from_str::<Closure>(closure_str).unwrap(), Closure {
100        container: 1,
101        step_ptr: 1,
102        ctx_states: Context::new()
103            .set("playbook", CtxObj::Str(String::from("tests/test1/test_sys_vars.yml")))
104            .set("message", CtxObj::Str(String::from("Salut!")))
105    });
106}
107
108pub fn copy_user_info(facts: &mut HashMap<String, String>, user: &str) {
109    if let Some(output) = std::process::Command::new("getent").args(&["passwd", &user]).output().ok() {
110        let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
111        let fields: Vec<&str> = stdout.split(":").collect();
112        facts.insert(String::from("uid"), String::from(fields[2]));
113        facts.insert(String::from("gid"), String::from(fields[3]));
114        facts.insert(String::from("full_name"), String::from(fields[4]));
115        facts.insert(String::from("home_dir"), String::from(fields[5]));
116    }
117}
118
119fn read_contents<P: AsRef<Path>>(fname: P) -> Result<String, std::io::Error> {
120    let mut contents = String::new();
121    let mut file = File::open(fname)?;
122    file.read_to_string(&mut contents)?;
123    return Ok(contents);
124}
125
126pub fn format_cmd<I>(cmd: I) -> String
127  where I: IntoIterator<Item = String>
128{
129    cmd.into_iter().map(|s| { if s.contains(" ") { format!("\"{}\"", s) } else { s.to_owned() } }).collect::<Vec<String>>().join(" ")
130}
131
132type TaskSpawner = fn(src: Context, ctx_step: Context) -> Result<(), TaskError>;
133
134#[cfg(not(feature = "sandbox"))] // protect the host by removing the entrance to all user codes!
135fn invoke(src: Context, ctx_step: Context) -> Result<Context, ExitCode> {
136    let ref action: String = ctx_step.unpack("action").unwrap();
137    let ref src_path_str: String = src.unpack("src").unwrap();
138    if !cfg!(feature = "ci_only") {
139        eprintln!("{}", "== Context ======================".cyan());
140        eprintln!("# ctx({}@{}) =\n{}", action.cyan(), src_path_str.dimmed(), ctx_step);
141        eprintln!("{}", "== EOF ==========================".cyan());
142        match std::io::stderr().flush() {
143            Ok(_) => {},
144            Err(_) => {}
145        }
146    }
147    let src_path = Path::new(src_path_str);
148    if let Some(ext_os) = src_path.extension() {
149        let ext = ext_os.to_str().unwrap();
150        #[allow(unused_variables)]
151        let wrapper = |whichever: TaskSpawner| -> Result<(), Option<String>> {
152            let last_words;
153            #[cfg(not(feature = "ci_only"))]
154            println!("{}", "== Output =======================".blue());
155            last_words = if let Err(e) = whichever(src, ctx_step) {
156                match e.src {
157                    TaskErrorSource::NixError(_) | TaskErrorSource::ExitCode(_) | TaskErrorSource::Signal(_) => {
158                        Err(Some(format!("{}", e)))
159                    },
160                    TaskErrorSource::Internal => Err(None),
161                    TaskErrorSource::ExternalAPIError => unreachable!()
162                }
163            }
164            else { Ok(()) };
165            #[cfg(not(feature = "ci_only"))]
166            println!("{}", "== EOF ==========================".blue());
167            match std::io::stdout().flush() {
168                Ok(_) => {},
169                Err(_) => {}
170            }
171            return last_words;
172        };
173        let ret: Result<(), Option<String>> = match ext {
174            #[cfg(feature = "lang_python")]
175            "py" => wrapper(lang::python::invoke),
176            _ => Err(Some(format!("It is not clear how to run {}.", src_path_str)))
177        };
178        if let Err(last_words) = ret {
179            if let Some(msg) = last_words {
180                error!("{}", msg);
181            }
182            Err(ExitCode::ErrTask)
183        }
184        else {
185            Ok(Context::new()) // TODO pass return value back as a context
186        }
187    }
188    else {
189        // TODO C-style FFI invocation
190        unimplemented!();
191    }
192}
193
194fn symbols<P: AsRef<Path>>(src: P) -> Result<HashSet<String>, std::io::Error> {
195    let mut ret = HashSet::new();
196    let file = File::open(src)?;
197    let re = Regex::new(r"^#\[playbook\((\w+)\)\]").unwrap();
198    for line in BufReader::new(file).lines() {
199        let ref line = line?;
200        if let Some(caps) = re.captures(line){
201            ret.insert(caps.get(1).unwrap().as_str().to_owned());
202        }
203    }
204    Ok(ret)
205}
206
207fn resolve<'step>(ctx_step: &'step Context, whitelist: &Vec<Context>) -> (Option<&'step str>, Option<Context>) {
208    let key_action;
209    if let Some(k) = ctx_step.get("action") { key_action = k; }
210    else { return (None, None); }
211    if let CtxObj::Str(action) = key_action {
212        let action: &'step str = action;
213        for ctx_source in whitelist {
214            if let Some(CtxObj::Str(src)) = ctx_source.get("src") {
215                let ref playbook: String = ctx_step.unpack("playbook").unwrap();
216                let playbook_dir;
217                if let Some(parent) = Path::new(playbook).parent() {
218                    playbook_dir = parent;
219                }
220                else {
221                    playbook_dir = Path::new(".");
222                }
223                let ref src_path = playbook_dir.join(src);
224                let src_path_str = src_path.to_str().unwrap();
225                debug!("Searching \"{}\" for `{}`.", src_path_str, action);
226                if let Ok(src_synbols) = symbols(src_path) {
227                    if src_synbols.contains(action) {
228                        debug!("Action `{}` has been found.", action);
229                        return(Some(action), Some(ctx_source.set("src", CtxObj::Str(src_path_str.to_owned()))));
230                    }
231                }
232                else {
233                    warn!("IO Error: {}", src_path_str);
234                }
235            }
236        }
237        (Some(action), None)
238    }
239    else {
240        (None, None)
241    }
242}
243
244fn try_as_builtin(ctx_step: &Context, closure: &Closure) -> TransientContext {
245    match builtins::resolve(&ctx_step) {
246        (Some(action), Some(sys_func)) => {
247            let ctx_sys = ctx_step.overlay(&closure.ctx_states).hide("whitelist");
248            info!("{}: {}", "Built-in".magenta(), action);
249            if !cfg!(feature = "ci_only") {
250                eprintln!("{}", "== Context ======================".cyan());
251                eprintln!("# ctx({}) =\n{}", action.cyan(), ctx_sys);
252                eprintln!("{}", "== EOF ==========================".cyan());
253            }
254            sys_func(ctx_sys)
255        },
256        (Some(action), None) => {
257            error!("Action not recognized: {}", action);
258            TransientContext::Diverging(ExitCode::ErrYML)
259        },
260        (None, _) => {
261            error!("Syntax Error: Key `whitelist` should be a list of mappings.");
262            TransientContext::Diverging(ExitCode::ErrYML)
263        }
264    }
265}
266
267fn run_step(ctx_step: Context, closure: Closure) -> TransientContext {
268    if let Some(whitelist) = ctx_step.list_contexts("whitelist") {
269        match resolve(&ctx_step, &whitelist) {
270            (_, Some(ctx_source)) => {
271                let show_step = |for_real: bool| {
272                    let step_header = format!("Step {}", closure.step_ptr+1).cyan();
273                    if let Some(CtxObj::Str(step_name)) = ctx_step.get("name") {
274                        info!("{}: {}", if for_real { step_header } else { step_header.dimmed() }, step_name);
275                    }
276                    else {
277                        info!("{}", if for_real { step_header } else { step_header.dimmed() });
278                    }
279                };
280                if closure.container == 1 {
281                    #[cfg(feature = "sandbox")] unreachable!();
282                    #[cfg(not(feature = "sandbox"))]
283                    {
284                        show_step(true);
285                        TransientContext::from(invoke(ctx_source, ctx_step.hide("whitelist")))
286                    }
287                }
288                else {
289                    if let Some(ctx_docker) = ctx_step.subcontext("docker") {
290                        show_step(false);
291                        if let Some(CtxObj::Str(image_name)) = ctx_docker.get("image") {
292                            info!("Entering Docker: {}", image_name.purple());
293                            let mut closure1 = closure.clone();
294                            closure1.container = 1;
295                            // Register any reassignment of "playbook" to the ctx_states to prolong its lifetime
296                            if let Some(ctx_docker_vars) = ctx_docker.subcontext("vars") {
297                                closure1.ctx_states = closure1.ctx_states.set_opt("playbook", ctx_docker_vars.get_clone("playbook"));
298                            }
299                            let mut resume_params = vec! [
300                                String::from("--arg-resume"),
301                                match serde_json::to_string(&closure1) {
302                                    Ok(s) => s,
303                                    Err(_) => {
304                                        error!("Failed to serialize states.");
305                                        return TransientContext::Diverging(ExitCode::ErrApp)
306                                    }
307                                },
308                                ctx_step.unpack("playbook").unwrap()
309                            ];
310                            let verbose_unpack = ctx_step.unpack("verbose-fern");
311                            if let Ok(verbose) = verbose_unpack {
312                                if verbose > 0 {
313                                    resume_params.push(format!("-{}", "v".repeat(verbose)));
314                                }
315                            }
316                            let infrastructure_str = if let Some(CtxObj::Str(s)) = ctx_step.get("as-switch") { s } else { "docker" };
317                            info!("Selected infrastructure: {}", infrastructure_str);
318                            if let Some(infrastructure) = systems::abstract_infrastructures(&infrastructure_str) {
319                                match infrastructure.start(ctx_docker.set_opt("playbook-from", ctx_step.get_clone("playbook")), resume_params) {
320                                    Ok(_docker_cmd) => {
321                                        TransientContext::from(Ok(Context::new())) // TODO pass return value back as a context
322                                    },
323                                    Err(e) => {
324                                        match e.src {
325                                            TaskErrorSource::NixError(_) | TaskErrorSource::ExitCode(_) | TaskErrorSource::Signal(_) => {
326                                                error!("{}: {}", "Container has crashed".red().bold(), e);
327                                            },
328                                            TaskErrorSource::Internal => {
329                                                error!("{}: {}", "InternalError".red().bold(), e);
330                                            },
331                                            TaskErrorSource::ExternalAPIError => {
332                                                error!("{}: {}", "ExternalAPIError".red().bold(), e);
333                                            }
334                                        }
335                                        TransientContext::Diverging(ExitCode::ErrTask)
336                                    }
337                                }
338                            }
339                            else {
340                                error!("Undefined infrastructure.");
341                                TransientContext::Diverging(ExitCode::ErrApp)
342                            }
343                        }
344                        else {
345                            error!("Syntax Error: Cannot parse the name of the image.");
346                            TransientContext::Diverging(ExitCode::ErrYML)
347                        }
348                    }
349                    else {
350                        #[cfg(feature = "sandbox")] unreachable!();
351                        #[cfg(not(feature = "sandbox"))]
352                        {
353                            show_step(true);
354                            TransientContext::from(invoke(ctx_source, ctx_step.hide("whitelist")))
355                        }
356                    }
357                }
358            },
359            (Some(_action), None) => {
360                try_as_builtin(&ctx_step, &closure)
361            },
362            (None, None) => {
363                error!("Syntax Error: Key `action` must be a string.");
364                TransientContext::Diverging(ExitCode::ErrYML)
365            }
366        }
367    }
368    else {
369        try_as_builtin(&ctx_step, &closure)
370    }    
371}
372
373fn deduce_context(ctx_step_raw: &Context, ctx_global: &Context, ctx_args: &Context, closure: &Closure) -> Context {
374    let ctx_partial = ctx_global.overlay(ctx_step_raw).overlay(ctx_args).overlay(&closure.ctx_states);
375    debug!("ctx({}) =\n{}", "partial".dimmed(), ctx_partial);
376    if let Some(CtxObj::Str(_)) = ctx_partial.get("arg-resume") {
377        if let Some(ctx_docker_vars) = ctx_partial.subcontext("docker").unwrap().subcontext("vars") {
378            ctx_partial.overlay(&ctx_docker_vars).hide("docker")
379        }
380        else { ctx_partial.hide("docker") }
381    }
382    else { ctx_partial }
383}
384
385fn get_steps(raw: Context) -> Result<(Vec<Context>, Context), ExitCode> {
386    let ctx_global = raw.hide("steps");
387    if let Some(steps) = raw.list_contexts("steps") {
388        Ok((steps, ctx_global))
389    }
390    else {
391        Err(ExitCode::ErrYML)
392    }
393}
394
395/// Correctly exit from a sys_fork action
396fn maybe_exit(exit_code: ExitCode, ctx_states: &Context) -> ExitCode {
397    if let Some(CtxObj::Bool(noreturn)) = ctx_states.get("_exit") {
398        if *noreturn {
399            unsafe { libc::_exit(0); }
400        }
401    }
402    exit_code
403}
404
405pub fn run_playbook(raw: Context, ctx_args: Context) -> Result<(), ExitCode> {
406    let mut ctx_states = Box::new(Context::new());
407    let (steps, ctx_global) = match get_steps(raw) {
408        Ok(v) => v,
409        Err(e) => {
410            error!("Syntax Error: Key `steps` is not an array.");
411            return Err(e);
412        }
413    };
414    if let Some(CtxObj::Str(closure_str)) = ctx_args.get("arg-resume") {
415        // ^^ Then we must be in a docker container because main() has guaranteed that.
416        match serde_json::from_str::<Closure>(closure_str) {
417            Ok(closure) => {
418                let ctx_step = deduce_context(&steps[closure.step_ptr], &ctx_global, &ctx_args, &closure);
419                match run_step(ctx_step, closure) {
420                    TransientContext::Stateful(_) | TransientContext::Stateless(_) => Ok(()),
421                    TransientContext::Diverging(exit_code) => match exit_code {
422                        ExitCode::Success => Ok(()),
423                        _ => Err(exit_code)
424                    }
425                }
426            }
427            Err(_e) => {
428                error!("Syntax Error: Cannot parse the `--arg-resume` flag. {}", closure_str.underline());
429                #[cfg(feature = "ci_only")]
430                eprintln!("{}", _e);
431                Err(ExitCode::ErrApp)
432            }
433        }
434    }
435    else {
436        for (i, ctx_step_raw) in steps.iter().enumerate() {
437            let closure = Closure { container: 0, step_ptr: i, ctx_states: ctx_states.as_ref().clone() };
438            let ctx_step = deduce_context(ctx_step_raw, &ctx_global, &ctx_args, &closure);
439            match run_step(ctx_step, closure) {
440                TransientContext::Stateless(_) => { }
441                TransientContext::Stateful(ctx_pipe) => {
442                    ctx_states = Box::new(ctx_states.overlay(&ctx_pipe));
443                }
444                TransientContext::Diverging(exit_code) => match maybe_exit(exit_code, &ctx_states) {
445                    ExitCode::Success => { return Ok(()); }
446                    exit_code @ _ => { return Err(exit_code); }
447                }
448            }
449        }
450        maybe_exit(ExitCode::Success, &ctx_states);
451        Ok(())
452    }
453}
454
455pub fn load_yaml<P: AsRef<Path>>(playbook: P) -> Result<Context, ExitCode> {
456    let fname = playbook.as_ref();
457    let contents = match read_contents(fname) {
458        Ok(v) => v,
459        Err(e) => {
460            error!("IO Error (while loading the playbook {:?}): {}", playbook.as_ref(), e);
461            return Err(ExitCode::ErrSys);
462        }
463    };
464    match YamlLoader::load_from_str(&contents) {
465        Ok(yml_global) => {
466            Ok(Context::from(yml_global[0].to_owned()))
467        },
468        Err(e) => {
469            error!("{}: {}", e, "Some YAML parsing error has occurred.");
470            Err(ExitCode::ErrYML)
471        }
472    }
473}