1#[macro_use]
2extern crate log;
3
4#[macro_use]
5extern crate serde_derive;
6
7extern 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"))] fn 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()) }
187 }
188 else {
189 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 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())) },
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
395fn 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 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}