Richard W.M. Jones
2016-Feb-23 12:50 UTC
[Libguestfs] [PATCH v3 0/4] [FOR COMMENTS ONLY] Rework inspection.
Previously posted: https://www.redhat.com/archives/libguestfs/2015-December/msg00038.html Inspection now really succeeds on a small number of simple guests. To test it out: $ ./run guestfish -v -x -a /tmp/centos-6.img ><fs> run ><fs> debug sh "guestfs-inspection --verbose" Rich.
Richard W.M. Jones
2016-Feb-23 12:50 UTC
[Libguestfs] [PATCH v3 1/4] inspection: Add rules compiler to the generator.
This adds a "prolog-inspired" compiler to the generator, which we can use to generate guestfs inspection. The compiler generates C code which should be reasonably efficient. Note this change requires ocamllex and ocamlyacc, but these are normally part of any OCaml distribution, since they are part of the OCaml compiler. We also import some GPL-licensed gnulib code for handling sets, which is OK because we're going to use these in a standalone program (in following commits). --- .gitignore | 4 + bootstrap | 2 + docs/guestfs-building.pod | 4 + generator/Makefile.am | 39 +- generator/rules_compiler.ml | 834 +++++++++++++++++++++++++++++++++++++++++++ generator/rules_compiler.mli | 21 ++ generator/rules_parser.mly | 143 ++++++++ generator/rules_scanner.mll | 113 ++++++ generator/types.ml | 67 ++++ generator/utils.ml | 16 + generator/utils.mli | 6 + m4/guestfs_ocaml.m4 | 2 + 12 files changed, 1249 insertions(+), 2 deletions(-) create mode 100644 generator/rules_compiler.ml create mode 100644 generator/rules_compiler.mli create mode 100644 generator/rules_parser.mly create mode 100644 generator/rules_scanner.mll diff --git a/.gitignore b/.gitignore index 40bebb3..67d8a2e 100644 --- a/.gitignore +++ b/.gitignore @@ -225,7 +225,11 @@ Makefile.in /generator/files-generated.txt /generator/generator /generator/.pod2text.data* +/generator/rules_parser.ml +/generator/rules_parser.mli +/generator/rules_scanner.ml /generator/stamp-generator +/generator/stamp-rules-parser /get-kernel/.depend /get-kernel/stamp-virt-get-kernel.pod /get-kernel/virt-get-kernel diff --git a/bootstrap b/bootstrap index 5df6f0f..f932c0c 100755 --- a/bootstrap +++ b/bootstrap @@ -36,6 +36,7 @@ gnulib_tool=$GNULIB_SRCDIR/gnulib-tool modules=' accept4 +array-oset areadlink areadlinkat arpa_inet @@ -97,6 +98,7 @@ warnings xalloc xalloc-die xgetcwd +xoset xstrtol xstrtoll xvasprintf diff --git a/docs/guestfs-building.pod b/docs/guestfs-building.pod index 0693156..973b6f6 100644 --- a/docs/guestfs-building.pod +++ b/docs/guestfs-building.pod @@ -104,6 +104,10 @@ I<Required>. Part of Perl core. =item OCaml findlib +=item ocamllex + +=item ocamlyacc + I<Required> if compiling from git. Optional (but recommended) if compiling from tarball. diff --git a/generator/Makefile.am b/generator/Makefile.am index ab6e059..c53d3b9 100644 --- a/generator/Makefile.am +++ b/generator/Makefile.am @@ -18,6 +18,9 @@ include $(top_srcdir)/subdir-rules.mk # In alphabetical order. +# +# Note we include ocamllex/ocamlyacc-generated files here, since +# we want to distribute these in the tarball for convenience. sources = \ actions.ml \ actions.mli \ @@ -70,6 +73,13 @@ sources = \ python.mli \ ruby.ml \ ruby.mli \ + rules_compiler.ml \ + rules_compiler.mli \ + rules_parser.ml \ + rules_parser.mli \ + rules_parser.mly \ + rules_scanner.ml \ + rules_scanner.mll \ structs.ml \ structs.mli \ tests_c_api.ml \ @@ -112,6 +122,9 @@ objects = \ bindtests.cmo \ errnostring.cmo \ customize.cmo \ + rules_scanner.cmo \ + rules_parser.cmo \ + rules_compiler.cmo \ main.cmo EXTRA_DIST = $(sources) files-generated.txt @@ -125,6 +138,22 @@ if HAVE_OCAML generator: $(objects) $(OCAMLFIND) ocamlc $(OCAMLCFLAGS) -linkpkg $^ -o $@ +rules_parser.ml rules_parser.mli: stamp-rules-parser + +stamp-rules-parser: rules_parser.mly + rm -f $@ + $(OCAMLYACC) $< + touch $@ + +rules_scanner.ml: rules_scanner.mll + $(OCAMLLEX) $< + +# Apparently because rules_parser.mli and rules_scanner.ml may not +# exist before the Makefile is run, the pattern dependencies below +# don't add these rules automatically, so we have to be explicit. +rules_parser.cmi: rules_parser.mli +rules_scanner.cmi: rules_scanner.ml + # Dependencies. %.cmi: %.mli $(OCAMLFIND) ocamlc $(OCAMLCFLAGS) -c $< -o $@ @@ -135,7 +164,7 @@ generator: $(objects) depend: .depend -.depend: $(wildcard $(abs_srcdir)/*.mli) $(wildcard $(abs_srcdir)/*.ml) +.depend: $(wildcard $(abs_srcdir)/*.mli) $(wildcard $(abs_srcdir)/*.ml) rules_parser.ml rules_parser.mli rules_scanner.ml rm -f $@ $@-t $(OCAMLFIND) ocamldep -I ../ocaml -I $(abs_srcdir) $^ | \ $(SED) 's/ *$$//' | \ @@ -178,6 +207,12 @@ stamp-generator: generator CLEANFILES = $(noinst_DATA) $(noinst_PROGRAM) *.cmi *.cmo *~ -DISTCLEANFILES = .depend .pod2text.data.version.2 +DISTCLEANFILES = \ + .depend \ + .pod2text.data.version.2 \ + rules_parser.ml \ + rules_parser.mli \ + rules_scanner.ml \ + stamp-rules-parser SUFFIXES = .cmo .cmi .cmx .ml .mli .mll .mly diff --git a/generator/rules_compiler.ml b/generator/rules_compiler.ml new file mode 100644 index 0000000..e74f529 --- /dev/null +++ b/generator/rules_compiler.ml @@ -0,0 +1,834 @@ +(* libguestfs + * Copyright (C) 2009-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + *) + +(* This is the compiler that turns inspection rules into C code. *) + +open Printf + +open Utils +open Types +open Pr +open Docstrings + +module StringSet = Set.Make (String) + +let (//) = Filename.concat + +type env = { + free_vars : string list; (* Variables which are free in the rule. *) + assign_vars : string list; (* Variables assigned by C code. *) + env_struct : string; (* Name of the C environment struct. *) +} + +let rec compile filename () + let prologue, rules, epilogue = parse filename in + type_check filename rules; + + generate_header ~inputs:[filename; "generator/rules_compiler.ml"] + CStyle GPLv2plus; + + pr "\ +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <inttypes.h> +#include <unistd.h> +#include <errno.h> +#include <error.h> + +#include \"gl_oset.h\" +#include \"gl_xoset.h\" +#include \"gl_array_oset.h\" + +#include \"cleanups.h\" +#include \"rules.h\" +#include \"guestfs-internal-all.h\" + +"; + + (match prologue with + | Some code -> insert_literal_code filename code + | None -> () + ); + + pr "\ +/* Disable a few warnings, so we can take a few short-cuts with the + * generated code. + */ +#pragma GCC diagnostic ignored \"-Wunused-variable\" +#pragma GCC diagnostic ignored \"-Wunused-macros\" +#pragma GCC diagnostic ignored \"-Wunused-function\" + +static gl_oset_t +new_string_set (void) +{ + /* Note that we don't need a dispose function because all the + * strings added to the set will be \"owned\" by other code, either + * static (all_strings) or owned by facts. + */ + return gl_oset_create_empty (GL_ARRAY_OSET, + (gl_setelement_compar_fn) strcmp, NULL); +} + +static void +add_all_strings (gl_oset_t set) +{ + size_t i; + + for (i = 0; all_strings[i] != NULL; ++i) + gl_oset_add (set, all_strings[i]); +} + +"; + + get_all_strings filename rules; + + (* Give each rule a unique number. The number is used for rule + * function names, environments, code functions and so on. + * eg: 'rule_0 ()', 'struct rule_0_env', 'rule_0_code_0 ()'. + *) + iteri ( + fun i rule -> + rule.rule_fn <- sprintf "rule_%d" i + ) rules; + + (* Create the environment struct for each rule. This contains all + * the variables either consumed or set by the function. + *) + let rules + List.map ( + fun rule -> + let env = compile_rule_environment filename rule in + (rule, env) + ) rules in + + (* Write all C code snippets to functions. *) + List.iter ( + fun (rule, env) -> + let j = ref 0 in + let rec loop = function + | And (e1, e2) | Or (e1, e2) -> loop e1; loop e2 + | BoolCode code -> + code.code_fn <- sprintf "%s_code_%d" rule.rule_fn !j; + incr j; + compile_bool_code filename rule env code + | AssignCode (vs, row_count, code) -> + code.code_fn <- sprintf "%s_code_%d" rule.rule_fn !j; + incr j; + compile_assign_code filename rule env vs row_count code + | Term _ | Not _ | True | False -> () + in + loop rule.body + ) rules; + + (* Compile all rules into functions. *) + List.iter (fun (rule, env) -> compile_rule filename rule env) rules; + + pr "\ +void +rules (void) +{ + clear_true_facts (); + clear_false_facts (); + + /* Loop over all the rules until no more true facts can be added. */ + for (;;) { + size_t nr_true_facts = count_true_facts (); + +"; + + List.iter ( + fun (rule, env) -> + pr " if (verbose)\n"; + pr " printf (\"trying rule %%s\\n\", %S);\n" + (string_of_term rule.head); + pr " %s ();\n" rule.rule_fn; + pr "\n"; + ) rules; + + pr " /* Added a true fact during this iteration? */ + if (nr_true_facts == count_true_facts ()) + break; + } /* for (;;) */ +} + +"; + + (match epilogue with + | Some code -> insert_literal_code filename code + | None -> () + ); + + pr "/* EOF */\n" + +and insert_literal_code filename code + (* XXX This function gets the line number wrong. *) + let lineno = code.code_loc.Lexing.pos_lnum in + pr "#line %d \"%s\"\n" lineno filename; + pr "%s\n" code.code + +and get_all_strings filename rules + let rec loop = function + | True | False | BoolCode _ | AssignCode _ -> [] + | And (e1, e2) | Or (e1, e2) -> loop e1 @ loop e2 + | Term term | Not term -> get_term_strings term + and get_term_strings { term_args = args } + filter_map (function Variable _ -> None | Constant s -> Some s) args + in + let all_strings + List.map (fun rule -> get_term_strings rule.head @ loop rule.body) rules in + let all_strings = List.concat all_strings in + let all_strings = sort_uniq all_strings in + pr "const char *all_strings[] = {\n"; + pr " "; + let col = ref 0 in + List.iter ( + fun s -> + let len = String.length s in + if !col + len + 4 >= 72 then ( + col := 0; + pr "\n " + ); + pr "%S, " s; + col := !col + len + 4; + ) all_strings; + if !col > 0 then pr "\n"; + pr " NULL\n"; + pr "};\n"; + pr "\n" + +(* Work the environment of a single rule. Also write out the + * corresponding struct to the C file. + *) +and compile_rule_environment filename rule + (* The name of the C struct. *) + let env_struct = sprintf "%s_env" rule.rule_fn in + + (* Divide all the variables which appear in the rule into: + * - ones which we have to search for [free_vars], + * - ones which are going to be returned by a C expression within + * the body [assign_vars]. + * We can do this statically. + * These sets are non-overlapping, so we just need to check which + * variables are returned by C expressions, and do an additional + * check that no C expressions are returning the same variable. + *) + (* Get the complete list of vars ... *) + let free_vars = Hashtbl.create 13 in + (* ... from the head *) + List.iter ( + function + | Variable v -> + if Hashtbl.mem free_vars v then ( + eprintf "%s: variable '%s' appears two or more times in a rule\n" + filename v; + exit 1 + ); + Hashtbl.add free_vars v 1 + | Constant _ -> () + ) rule.head.term_args; + (* ... from the body *) + let rec loop = function + | And (e1, e2) | Or (e1, e2) -> loop e1; loop e2 + | Term { term_args = args } | Not { term_args = args } -> + List.iter ( + function + | Variable v -> Hashtbl.replace free_vars v 1 + | Constant _ -> () + ) args + | True | False + | BoolCode _ | AssignCode _ -> () + in + loop rule.body; + + let assign_vars = Hashtbl.create 13 in + let rec loop = function + | True | False | Term _ | Not _ | BoolCode _ -> () + | And (e1, e2) | Or (e1, e2) -> loop e1; loop e2 + | AssignCode (vs, _, _) -> + List.iter ( + fun v -> + Hashtbl.remove free_vars v; + if Hashtbl.mem assign_vars v then ( + eprintf "%s: variable '%s' appears two or more times in a C assignment expression in a rule\n" + filename v; + exit 1 + ); + Hashtbl.add assign_vars v 1 + ) vs + in + loop rule.body; + let free_vars = Hashtbl.fold (fun k _ ks -> k :: ks) free_vars [] in + let assign_vars = Hashtbl.fold (fun k _ ks -> k :: ks) assign_vars [] in + + (* Write out the C struct. *) + pr "/* Environment struct for rule %s */\n" (string_of_term rule.head); + pr "struct %s {\n" env_struct; + if free_vars <> [] then ( + pr " /* free variables */\n"; + List.iter (pr " char *%s;\n") free_vars + ); + if assign_vars <> [] then ( + pr " /* (rows of) variables assigned by C code */\n"; + pr " size_t nr_rows;\n"; + List.iter (pr " char **%s;\n") assign_vars + ); + pr "};\n"; + pr "\n"; + + (* Return the OCaml env. *) + { free_vars = free_vars; + assign_vars = assign_vars; + env_struct = env_struct; } + +(* Compile a single rule to C code. *) +and compile_rule filename rule env + (* For each free variable we need to find the possible values for that + * variable. If they appear within the body in a term like + * 'Foo(var)' then we can just look for matching facts and add + * them (at runtime). If they don't, then we start with the list + * of all strings culled from the source + all strings from all facts. + *) + let free_vars = List.map ( + fun v -> + let fact_lookups = ref [] in + let rec loop = function + | True | False | BoolCode _ | AssignCode _ -> () + | And (e1, e2) | Or (e1, e2) -> loop e1; loop e2 + | Term { term_name = term_name; term_args = args } + | Not { term_name = term_name; term_args = args } -> + (* If this term contains this variable at some position, + * then save that in the list of 'facts'. + *) + iteri ( + fun arg_i -> + function + | Variable v' when v = v' -> + fact_lookups := (term_name, arg_i) :: !fact_lookups + | Variable _ | Constant _ -> () + ) args + in + loop rule.body; + let fact_lookups = sort_uniq !fact_lookups in + + v, fact_lookups + ) env.free_vars in + + pr "/* %s */\n" (string_of_term rule.head); + pr "static void\n"; + pr "%s (void)\n" rule.rule_fn; + pr "{\n"; + pr " struct %s env;\n" env.env_struct; + pr " bool added;\n"; + pr " size_t i;\n"; + List.iter ( + fun (v, _) -> + pr " gl_oset_t search_%s;\n" v; + pr " gl_oset_iterator_t iter_%s;\n" v; + ) free_vars; + pr "\n"; + + (* This is an optimization: If the rule contains no free variables, + * we only need to run it once. This even applies if there are + * assigned variables, because the C code is supposed to be pure, + * ie. produce the same result every time it is called. + *) + if free_vars = [] then ( + pr " /* Because this rule contains no free variables, we only need\n"; + pr " * to evaluate it once. This applies even if the rule runs\n"; + pr " * C code (see 'C code memoization' in guestfs-inspection(8)\n"; + pr " * for an explanation of why this is so).\n"; + pr " */\n"; + pr " static bool called = false;\n"; + pr " if (called)\n"; + pr " return;\n"; + pr " called = true;\n"; + pr "\n"; + ); + + if free_vars <> [] then + pr " /* Build the sets we will use for searching each free variable. */\n"; + List.iter ( + function + | v, [] -> + (* The variable doesn't appear in any expressions, so + * add a note to the source. Maybe emit a compiler warning? XXX + *) + pr " search_%s = new_string_set ();\n" v; + pr " /* Warning: variable '%s' is underspecified, so we will\n" v; + pr " * search over all strings from the source and all facts.\n"; + pr " */\n"; + pr " add_all_strings (search_%s);\n" v; + pr " add_all_fact_strings (search_%s);\n" v; + pr "\n" + | v, fact_lookups -> + pr " search_%s = new_string_set ();\n" v; + List.iter ( + fun (term_name, arg_i) -> + pr " add_strings_from_facts (search_%s, %S, %d);\n" + v term_name arg_i + ) fact_lookups; + pr "\n" + ) free_vars; + + (* Do a cartesian search over all [free_vars], substituting each set of + * variables, and evaluating the body. If it evaluates to true, + * then we will add a new true fact! (Or maybe several if we are + * dealing with a list assignment [()*={{...}}]). If it evaluates + * to false, we add a false fact. It's also possible that we + * cannot evaluate the rule at all, because it contains unknown + * facts, in which case we end up adding NO new facts. + *) + if free_vars <> [] then ( + pr " /* Perform cartesian search over free variables. */\n"; + + List.iter ( + fun (v, _) -> + pr " iter_%s = gl_oset_iterator (search_%s);\n" v v; + pr " while (gl_oset_iterator_next (&iter_%s,\n" v; + pr " (const void **)&env.%s)) {\n" v; + ) free_vars; + + ) else ( + (* If there are no free_vars, then we have to add a dummy loop + * around the next code so that the 'continue' statement can be used. + *) + pr " do {\n"; + ); + + (* Initialize any assign_vars in the env struct. Note that the + * free_vars are initialized by the iterator loops above. + *) + List.iter (pr " env.%s = NULL;\n") env.assign_vars; + if env.assign_vars <> [] then pr " env.nr_rows = 0;\n"; + + (* We can only do this optimization if assign_vars = [], + * because we don't know what the C code (returning those vars) + * may give us yet. XXX Actually we could be looser with this: + * we only need to check that the head term contains no assigned + * variables. + *) + if env.assign_vars = [] then ( + pr " {\n"; + pr " /* If the fact already exists, don't bother doing any work. */\n"; + pr " CREATE_FACT (fact, %S, %d" + rule.head.term_name (List.length rule.head.term_args); + List.iter (function + | Variable v -> pr ", env.%s" v + | Constant s -> pr ", %S" s) + rule.head.term_args; + pr ");\n"; + pr "\n"; + pr " if (is_fact (true, fact) || is_fact (false, fact))\n"; + pr " continue;\n"; + pr " }\n"; + pr "\n"; + ); + + (* Evaluate the expression on the right hand side. *) + let rec eval result = function + | True -> + pr " %s = 1;\n" result + | False -> + pr " %s = 0;\n" result + | BoolCode code -> + pr " %s = %s (&env);\n" result code.code_fn + | AssignCode (_, _, code) -> + pr " %s (&env);\n" code.code_fn; + (* The result of AssignCode is always true (else it would + * have exited in the call above). Hence: + *) + pr " %s = 1;\n" result + | And (e1, e2) -> + let re1 = sprintf "r_%d" (unique ()) in + pr " int %s;\n" re1; + eval re1 e1; + pr " if (%s != 1)\n" re1; + pr " %s = %s;\n" result re1; + pr " else {\n"; + let re2 = sprintf "r_%d" (unique ()) in + pr " int %s;\n" re2; + eval re2 e2; + pr " %s = %s;\n" result re2; + pr " }\n"; + | Or (e1, e2) -> + let re1 = sprintf "r_%d" (unique ()) in + pr " int %s;\n" re1; + eval re1 e1; + pr " if (%s == 1)\n" re1; + pr " %s = %s;\n" result re1; + pr " else {\n"; + let re2 = sprintf "r_%d" (unique ()) in + pr " int %s;\n" re2; + eval re2 e2; + pr " %s = %s;\n" result re2; + pr " }\n"; + | Term term -> + pr " {\n"; + pr " CREATE_FACT (fact, %S, %d" + term.term_name (List.length term.term_args); + List.iter ( + function + | Variable v -> pr ", env.%s" v + | Constant s -> pr ", %S" s + ) term.term_args; + pr ");\n"; + pr " %s = is_fact (true, fact);\n" result; + pr " }\n"; + | Not term -> + pr " {\n"; + pr " CREATE_FACT (fact, %S, %d" + term.term_name (List.length term.term_args); + List.iter ( + function + | Variable v -> pr ", env.%s" v + | Constant s -> pr ", %S" s + ) term.term_args; + pr ");\n"; + pr " %s = is_fact (false, fact);\n" result; + pr " }\n"; + in + pr " /* Evaluate the RHS of the rule with this assignment of variables. */\n"; + pr " int result;\n"; + eval "result" rule.body; + pr " if (result == -1) /* not determined */ continue;\n"; + let make_fact ?i ?(indent = 2) () + let indent = spaces indent in + pr "%sCREATE_FACT (fact, %S, %d" + indent rule.head.term_name (List.length rule.head.term_args); + List.iter ( + function + | Variable v -> + if not (List.mem v env.assign_vars) then + pr ", env.%s" v + else ( + let i = match i with Some i -> i | None -> assert false in + pr ", env.%s[%s]" v i + ) + | Constant s -> pr ", %S" s + ) rule.head.term_args; + pr ");\n"; + in + pr " if (result > 0) /* true */ {\n"; + if env.assign_vars = [] then ( + make_fact ~indent:4 (); + pr " added = add_fact (true, fact);\n"; + pr " if (added && verbose) {\n"; + pr " printf (\"added new fact \");\n"; + pr " print_fact (true, fact, stdout);\n"; + pr " printf (\"\\n\");\n"; + pr " }\n"; + ) else ( + pr " for (i = 0; i < env.nr_rows; ++i) {\n"; + make_fact ~i:"i" ~indent:6 (); + pr " added = add_fact (true, fact);\n"; + pr " if (added && verbose) {\n"; + pr " printf (\"added new fact \");\n"; + pr " print_fact (true, fact, stdout);\n"; + pr " printf (\"\\n\");\n"; + pr " }\n"; + pr " }\n"; + ); + pr " }\n"; + pr " if (result == 0) /* false */ {\n"; + if env.assign_vars = [] then ( + make_fact ~indent:4 (); + pr "\n"; + pr " added = add_fact (false, fact);\n"; + pr " if (added && verbose) {\n"; + pr " printf (\"added new fact \");\n"; + pr " print_fact (false, fact, stdout);\n"; + pr " printf (\"\\n\");\n"; + pr " }\n"; + ) else ( + pr " for (i = 0; i < env.nr_rows; ++i) {\n"; + make_fact ~i:"i" ~indent:6 (); + pr " added = add_fact (false, fact);\n"; + pr " if (added && verbose) {\n"; + pr " printf (\"added new fact \");\n"; + pr " print_fact (false, fact, stdout);\n"; + pr " printf (\"\\n\");\n"; + pr " }\n"; + pr " }\n"; + ); + pr " }\n"; + + (* Free any assign_vars. The free_vars don't have to be freed + * because the iterator loop handles them. + *) + List.iter ( + fun v -> + pr " for (i = 0; i < env.nr_rows; ++i)\n"; + pr " free (env.%s[i]);\n" v; + pr " free (env.%s);\n" v + ) env.assign_vars; + + if free_vars <> [] then ( + List.iter ( + fun (v, _) -> + pr " }\n"; + pr " gl_oset_iterator_free (&iter_%s);\n" v + ) (List.rev free_vars); + ) else ( + pr " } while (0);\n"; + ); + pr "\n"; + + List.iter ( + function + | v, _ -> + pr " gl_oset_free (search_%s);\n" v + ) free_vars; + + pr "}\n"; + pr "\n" + +(* Compile a BoolCode snippet from a rule into a function. *) +and compile_bool_code filename rule env code + (* Create a function which wraps the C code. *) + let code_wrapper_fn = sprintf "%s_wrapper" code.code_fn in + List.iter (fun v -> pr "#define %s (_env->%s)\n" v v) env.free_vars; + pr "static int\n"; + pr "%s (struct %s *_env)\n" code_wrapper_fn env.env_struct; + pr "{\n"; + insert_literal_code filename code; + pr "}\n"; + List.iter (pr "#undef %s\n") env.free_vars; + pr "\n"; + + (* Create the function itself. *) + pr "static int\n"; + pr "%s (struct %s *env)\n" code.code_fn env.env_struct; + pr "{\n"; + pr " int r;\n"; + pr "\n"; + pr " if (verbose)\n"; + pr " printf (\"running C function %%s:%%d\\n\",\n"; + pr " \"%s\", %d);\n" filename code.code_loc.Lexing.pos_lnum; + pr "\n"; + pr " r = %s (env);\n" code_wrapper_fn; + pr "\n"; + pr " /* If the C function returns -1, it causes us to exit at once. */\n"; + pr " if (r == -1)\n"; + pr " error (EXIT_FAILURE, 0,\n"; + pr " \"%%s:%%d: C function failed - see earlier errors\",\n"; + pr " \"%s\", %d);\n" + filename code.code_loc.Lexing.pos_lnum; + pr "\n"; + pr " return r;\n"; + pr "}\n"; + pr "\n"; + +(* Compile assignment code (AssignCode) snippet into a function. *) +and compile_assign_code filename rule env vs row_count code + (* Create a function for setting a row in the result. *) + let set_vars_fn = sprintf "%s_set_row" code.code_fn in + let set_vars_alias = sprintf "set_%s" (String.concat "_" vs) in + pr "static void\n"; + pr "%s (struct %s *_env, %s)\n" + set_vars_fn env.env_struct + (String.concat ", " + (List.map (sprintf "const char *%s") vs)); + pr "{\n"; + pr " size_t _i = _env->nr_rows;\n"; + pr "\n"; + List.iter ( + fun v -> + pr " _env->%s = realloc (_env->%s, (_i+1) * sizeof (char *));\n" v v; + pr " if (_env->%s == NULL)\n" v; + pr " error (EXIT_FAILURE, errno, \"realloc\");\n"; + pr " _env->%s[_i] = strdup (%s);\n" v v; + pr " if (_env->%s[_i] == NULL)\n" v; + pr " error (EXIT_FAILURE, errno, \"strdup\");\n" + ) vs; + pr " _env->nr_rows++;\n"; + pr "}\n"; + pr "\n"; + + (* Create a function which wraps the C code. *) + let code_wrapper_fn = sprintf "%s_wrapper" code.code_fn in + List.iter (fun v -> pr "#define %s (_env->%s)\n" v v) env.free_vars; + pr "#define %s(%s) %s (_env, %s)\n" + set_vars_alias (String.concat ", " vs) + set_vars_fn (String.concat ", " (List.map (fun v -> "("^v^")") vs)); + pr "static int\n"; + pr "%s (struct %s *_env)\n" code_wrapper_fn env.env_struct; + pr "{\n"; + insert_literal_code filename code; + pr "}\n"; + List.iter (pr "#undef %s\n") env.free_vars; + pr "#undef %s\n" set_vars_alias; + pr "\n"; + + (* Create the function itself. *) + pr "static void\n"; + pr "%s (struct %s *env)\n" code.code_fn env.env_struct; + pr "{\n"; + pr " int r;\n"; + pr "\n"; + pr " if (verbose)\n"; + pr " printf (\"running C function %%s:%%d\\n\",\n"; + pr " \"%s\", %d);\n" filename code.code_loc.Lexing.pos_lnum; + pr "\n"; + pr " r = %s (env);\n" code_wrapper_fn; + pr "\n"; + pr " /* If the C function returns -1, it causes us to exit at once. */\n"; + pr " if (r == -1)\n"; + pr " error (EXIT_FAILURE, 0,\n"; + pr " \"%%s:%%d: C function failed - see earlier errors\",\n"; + pr " \"%s\", %d);\n" + filename code.code_loc.Lexing.pos_lnum; + pr "\n"; + pr " /* Check the set_* function was called the expected number\n"; + pr " * of times.\n"; + pr " */\n"; + (match row_count with + | RowsOne -> + pr " if (env->nr_rows != 1)\n" + | RowsZeroOrMore -> + pr " if (0) /* no check necessary for (var)* assignment */\n" + | RowsZeroOrOne -> + pr " if (env->nr_rows > 1)\n" + | RowsOneOrMore -> + pr " if (env->nr_rows < 1)\n" + ); + pr " error (EXIT_FAILURE, 0,\n"; + pr " \"%%s:%%d: C function called %%s incorrect number of times (%%zu)\",\n"; + pr " \"%s\", %d, \"%s\", env->nr_rows);\n" + filename code.code_loc.Lexing.pos_lnum set_vars_alias; + pr "}\n"; + pr "\n"; + +(* Parse the input. *) +and parse filename + let lexbuf = Lexing.from_channel (open_in filename) in + let chunks = ref [] in + (try + while true do + let chunk = Rules_parser.chunk Rules_scanner.token lexbuf in + chunks := chunk :: !chunks + done + with + | End_of_file -> () + | Rules_scanner.Error (msg, _, lineno, charno) -> + eprintf "%s: %d: %d: %s\n" filename lineno charno msg; + exit 1 + | Parsing.Parse_error -> + let p = Lexing.lexeme_start_p lexbuf in + eprintf "%s: %d: %d: syntax error\n" + filename + p.Lexing.pos_lnum + (p.Lexing.pos_cnum - p.Lexing.pos_bol); + exit 1 + ); + + (* Allow only the first and last chunk to be code (optional prologue + * and epilogue). The rest must be rules. + *) + let rev_chunks = !chunks in + + let epilogue, rev_chunks + match rev_chunks with + | CodeChunk epilogue :: chunks -> Some epilogue, chunks + | chunks -> None, chunks in + + let chunks = List.rev rev_chunks in + + let prologue, chunks + match chunks with + | CodeChunk prologue :: chunks -> Some prologue, chunks + | chunks -> None, chunks in + + let rules = List.map ( + function + | RuleChunk rule -> rule + | CodeChunk { code_loc = code_loc } -> + eprintf "%s: %d: syntax error: prologue and epilogue can only appear at the beginning or end of the input file\n" + filename code_loc.Lexing.pos_lnum; + exit 1 + ) chunks in + + prologue, rules, epilogue + +(* Minimal type checking. *) +and type_check filename rules + check_term_rhs filename rules; + check_term_arity filename rules + +(* If a term appears on the right hand side in any expression, then + * the term must also appear on the left hand side of a rule. + *) +and check_term_rhs filename rules + let names = List.map (fun { head = { term_name = name } } -> name) rules in + let names + List.fold_left (fun set x -> StringSet.add x set) StringSet.empty names in + + let errors = ref 0 in + List.iter ( + fun { body = body } -> + visit_terms ( + fun { term_name = name } -> + if not (StringSet.mem name names) then ( + eprintf "%s: '%s' appears in a rule expression, but does not appear on the left hand side of any rule. Maybe there is a typo?\n" + filename name; + incr errors + ) + ) body + ) rules; + if !errors > 0 then exit 1 + +(* Check the arity of terms is the same wherever they appear. *) +and check_term_arity filename rules + let hash = Hashtbl.create (List.length rules) in (* name -> arity *) + + let errors = ref 0 in + + let check_arity { term_name = name; term_args = args } + let arity = List.length args in + try + let expected_arity = Hashtbl.find hash name in + if arity <> expected_arity then ( + eprintf "%s: '%s' has different number of parameters (has %d, expected %d). It must have the same number of parameters throughout the program.\n" + filename name arity expected_arity; + incr errors + ) + with + (* The first time we've seen this term. *) + Not_found -> Hashtbl.add hash name arity + in + + List.iter ( + fun { head = head; body = body } -> + check_arity head; + visit_terms check_arity body + ) rules; + + if !errors > 0 then exit 1 + +and visit_terms f = function + | And (e1, e2) + | Or (e1, e2) -> visit_terms f e1; visit_terms f e2 + | Term t + | Not t -> f t + | True | False | BoolCode _ | AssignCode _ -> () + +and unique + let i = ref 0 in + fun () -> incr i; !i diff --git a/generator/rules_compiler.mli b/generator/rules_compiler.mli new file mode 100644 index 0000000..2bc5274 --- /dev/null +++ b/generator/rules_compiler.mli @@ -0,0 +1,21 @@ +(* libguestfs + * Copyright (C) 2009-2015 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + *) + +(* This is the compiler that turns inspection rules into C code. *) + +val compile : string -> unit -> unit diff --git a/generator/rules_parser.mly b/generator/rules_parser.mly new file mode 100644 index 0000000..8dc595f --- /dev/null +++ b/generator/rules_parser.mly @@ -0,0 +1,143 @@ +/* libguestfs -*- text -*- + * Copyright (C) 2009-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +%{ +open Types +%} + +%token <string> STRING /* string literal */ +%token <string> UID /* uppercase identifier */ +%token <string> LID /* lowercase identifier */ + +%token TRUE /* true (keyword) */ +%token FALSE /* false (keyword) */ + +%token LPAREN RPAREN /* ( ... ) */ +%token <string> CODE /* {{ .. }} containing C code */ +%token STAR /* * (zero or more rows) */ +%token QUESTION /* ? (zero or one row) */ +%token PLUS /* + (one or more rows) */ +%token DOT /* . */ +%token IMPLIC /* :- (implication) */ +%token COMMA /* , (AND operator) */ +%token SEMI /* ; (OR operator) */ +%token NOT /* ! */ +%token EQUALS /* = */ + +/* These operators are arranged from lowest to highest precedence. */ +%left IMPLIC +%left SEMI +%left COMMA +%nonassoc NOT + +%start chunk +%type <Types.chunk> chunk +%type <Types.rule> rule + +%% + +chunk: CODE + { CodeChunk { code = $1; + code_loc = symbol_start_pos (); + code_fn = "" } } + | rule + { RuleChunk $1 } + ; + +rules: /* empty */ + { [] } + | rule rules + { $1 :: $2 } + +rule: head DOT + { { head = $1; body = True; + rule_loc = symbol_start_pos (); rule_fn = "" } } + | head IMPLIC body DOT + { { head = $1; body = $3; + rule_loc = symbol_start_pos (); rule_fn = "" } } + ; + +head: term + { $1 } + ; + +term: UID + { { term_name = $1; term_args = [] } } + | UID LPAREN term_args RPAREN + { { term_name = $1; term_args = $3 } } + ; + +term_args: + term_arg + { [ $1 ] } + | term_arg COMMA term_args + { $1 :: $3 } + ; + +term_arg: + LID + { Variable $1 } + | STRING + { Constant $1 } + ; + +body: expr + { $1 } + ; + +expr: TRUE + { True } + | FALSE + { False } + | term + { Term $1 } + | CODE + { BoolCode { code = $1; + code_loc = symbol_start_pos (); + code_fn = "" } } + | LPAREN result_bindings RPAREN EQUALS CODE + { AssignCode ($2, RowsOne, + { code = $5; + code_loc = symbol_start_pos (); code_fn = "" }) } + | LPAREN result_bindings RPAREN STAR EQUALS CODE + { AssignCode ($2, RowsZeroOrMore, + { code = $6; + code_loc = symbol_start_pos (); code_fn = "" }) } + | LPAREN result_bindings RPAREN QUESTION EQUALS CODE + { AssignCode ($2, RowsZeroOrOne, + { code = $6; + code_loc = symbol_start_pos (); code_fn = "" }) } + | LPAREN result_bindings RPAREN PLUS EQUALS CODE + { AssignCode ($2, RowsOneOrMore, + { code = $6; + code_loc = symbol_start_pos (); code_fn = "" }) } + | NOT term + { Not $2 } + | expr COMMA expr + { And ($1, $3) } + | expr SEMI expr + { Or ($1, $3) } + | LPAREN expr RPAREN + { $2 } + ; + +result_bindings: + LID + { [ $1 ] } + | LID COMMA result_bindings + { $1 :: $3 } diff --git a/generator/rules_scanner.mll b/generator/rules_scanner.mll new file mode 100644 index 0000000..5b32aaf --- /dev/null +++ b/generator/rules_scanner.mll @@ -0,0 +1,113 @@ +(* libguestfs -*- text -*- + * Copyright (C) 2009-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + *) + +{ +open Rules_parser + +let string_of_lexbuf = Lexing.lexeme + +(* Errors raised by the lexer. *) +exception Error of string * string * int * int + +let raise_error lexbuf msg + let p = Lexing.lexeme_start_p lexbuf in + raise (Error (msg, p.Lexing.pos_fname, + p.Lexing.pos_lnum, + p.Lexing.pos_cnum - p.Lexing.pos_bol)) + +(* Store "..." strings. *) +let string_buf = Buffer.create 256 +let reset_string_buffer () = Buffer.clear string_buf +let store_string_char c = Buffer.add_char string_buf c +let get_string_buffer () = Buffer.contents string_buf + +let char_for_backslash = function + | 'n' -> '\010' + | 'r' -> '\013' + | 'b' -> '\008' + | 't' -> '\009' + | c -> c + +(* Store {{ CODE }} sections. *) +let code_buf = Buffer.create 256 +let reset_code_buffer () = Buffer.clear code_buf +let store_code_char c = Buffer.add_char code_buf c +let get_code_buffer () = Buffer.contents code_buf +} + +(* Characters that can appear within an identifier (after the first + * character which is treated specially below). + *) +let id_char = ['a'-'z' 'A'-'Z' '0'-'9' '_'] + +(* Whitespace. *) +let ws = [' ' '\t'] + +(* Backslash escapes within strings. *) +let backslash_escapes = ['\\' '\'' '"' 'n' 't' 'b' 'r'] + +rule token = parse + | "/*" { comment lexbuf; token lexbuf } + | '(' { LPAREN } + | ')' { RPAREN } + | '*' { STAR } + | '?' { QUESTION } + | '+' { PLUS } + | '.' { DOT } + | ":-" { IMPLIC } + | ',' { COMMA } + | ';' { SEMI } + | '!' { NOT } + | '=' { EQUALS } + | '"' { reset_string_buffer (); + string lexbuf; + STRING (get_string_buffer ()) } + | "{{" { reset_code_buffer (); + code lexbuf; + CODE (get_code_buffer ()) } + | "true" { TRUE } + | "false" { FALSE } + | ['A'-'Z'] id_char* { UID (string_of_lexbuf lexbuf) } + | ['a'-'z' '_'] id_char* { LID (string_of_lexbuf lexbuf) } + | '\n' { Lexing.new_line lexbuf; token lexbuf } + | ws { token lexbuf } + | eof { raise End_of_file } + | _ { raise_error lexbuf "unexpected character in input" } + +(* Discard C-style comments. *) +and comment = parse + | "*/" { () } + | eof { raise_error lexbuf "unterminated comment" } + | '\n' { Lexing.new_line lexbuf; comment lexbuf } + | _ { comment lexbuf } + +(* Store "..." strings. *) +and string = parse + | '"' { () } + | eof { raise_error lexbuf "unterminated string" } + | '\n' { raise_error lexbuf "strings cannot contain newline characters" } + | '\\' (backslash_escapes as c) + { store_string_char (char_for_backslash c); string lexbuf } + | _ as c { store_string_char c; string lexbuf } + +(* Store {{ ... }} (CODE) sections containing C code. *) +and code = parse + | "}}" { () } + | eof { raise_error lexbuf "unterminated code section" } + | '\n' as c { Lexing.new_line lexbuf; store_code_char c; code lexbuf } + | _ as c { store_code_char c; code lexbuf } diff --git a/generator/types.ml b/generator/types.ml index b2f8724..4170846 100644 --- a/generator/types.ml +++ b/generator/types.ml @@ -18,6 +18,8 @@ (* Please read generator/README first. *) +open Printf + (* Types used to describe the API. *) type style = ret * args * optargs @@ -426,3 +428,68 @@ type call_optargt | CallOInt64 of string * int64 | CallOString of string * string | CallOStringList of string * string list + +(* Used by the rules compiler. *) + +type chunk = RuleChunk of rule | CodeChunk of code + +and rule = { + head : term; + body : expr; + rule_loc : Lexing.position; + mutable rule_fn : string; +} +(* The type of a parsed rule from the source. *) + +and term = { term_name : string; term_args : term_arg list } + +and term_arg = Variable of string | Constant of string + +and expr + | True (* used for facts *) + | False (* false (keyword) *) + | Term of term + | Not of term (* ! term *) + | And of expr * expr (* expr, expr *) + | Or of expr * expr (* expr; expr *) + | BoolCode of code (* {{ ... }} *) + | AssignCode of string list * row_count * code (* (a,b)={{ ... }} *) + +and code = { + code : string; + code_loc : Lexing.position; + mutable code_fn : string; +} + +and row_count = RowsOne | RowsZeroOrMore | RowsOneOrMore | RowsZeroOrOne + +let rec string_of_rule { head = head; body = body } + sprintf "%s :-\n\t%s." (string_of_term head) (string_of_expr body) + +and string_of_term = function + | { term_name = term_name; term_args = [] } -> + sprintf "%s" term_name + | { term_name = term_name; term_args = args } -> + sprintf "%s(%s)" term_name + (String.concat ", " (List.map string_of_term_arg args)) + +and string_of_term_arg = function + | Variable s -> s + | Constant s -> sprintf "%S" s + +and string_of_expr = function + | True -> "true" + | False -> "true" + | Term term -> string_of_term term + | Not term -> sprintf "!%s" (string_of_term term) + | And (e1, e2) -> sprintf "(%s,%s)" (string_of_expr e1) (string_of_expr e2) + | Or (e1, e2) -> sprintf "(%s;%s)" (string_of_expr e1) (string_of_expr e2) + | BoolCode _ -> "{{ // code }}" + | AssignCode (bindings, RowsOne, _) -> + sprintf "(%s)={{ // code }}" (String.concat ", " bindings) + | AssignCode (bindings, RowsZeroOrMore, _) -> + sprintf "(%s)*={{ // code }}" (String.concat ", " bindings) + | AssignCode (bindings, RowsOneOrMore, _) -> + sprintf "(%s)+={{ // code }}" (String.concat ", " bindings) + | AssignCode (bindings, RowsZeroOrOne, _) -> + sprintf "(%s)?={{ // code }}" (String.concat ", " bindings) diff --git a/generator/utils.ml b/generator/utils.ml index 34edf9d..97fce11 100644 --- a/generator/utils.ml +++ b/generator/utils.ml @@ -231,6 +231,22 @@ let mapi f xs in loop 0 xs +let uniq ?(cmp = Pervasives.compare) xs + let rec loop acc = function + | [] -> acc + | [x] -> x :: acc + | x :: (y :: _ as xs) when cmp x y = 0 -> + loop acc xs + | x :: (y :: _ as xs) -> + loop (x :: acc) xs + in + List.rev (loop [] xs) + +let sort_uniq ?(cmp = Pervasives.compare) xs + let xs = List.sort cmp xs in + let xs = uniq ~cmp xs in + xs + let count_chars c str let count = ref 0 in for i = 0 to String.length str - 1 do diff --git a/generator/utils.mli b/generator/utils.mli index aec1f71..96d0bd5 100644 --- a/generator/utils.mli +++ b/generator/utils.mli @@ -84,6 +84,12 @@ val iteri : (int -> 'a -> unit) -> 'a list -> unit val mapi : (int -> 'a -> 'b) -> 'a list -> 'b list +val uniq : ?cmp:('a -> 'a -> int) -> 'a list -> 'a list + (** Uniquify a list (the list must be sorted first). *) + +val sort_uniq : ?cmp:('a -> 'a -> int) -> 'a list -> 'a list + (** Sort and uniquify a list. *) + val count_chars : char -> string -> int (** Count number of times the character occurs in string. *) diff --git a/m4/guestfs_ocaml.m4 b/m4/guestfs_ocaml.m4 index 346779c..829906b 100644 --- a/m4/guestfs_ocaml.m4 +++ b/m4/guestfs_ocaml.m4 @@ -29,6 +29,8 @@ AS_IF([test "x$enable_ocaml" != "xno"],[ OCAMLFIND AC_PROG_OCAML AC_PROG_FINDLIB + AC_PROG_OCAMLLEX + AC_PROG_OCAMLYACC dnl OCaml >= 3.11 is required. AC_MSG_CHECKING([if OCaml version >= 3.11]) -- 2.5.0
Richard W.M. Jones
2016-Feb-23 12:50 UTC
[Libguestfs] [PATCH v3 2/4] inspection: Add inspection rules and supporting code.
Using the rules compiler added in the previous commit, create an inspection program ("guestfs-inspection"). This will run inside the appliance. It is partly written in the Prolog-inspired language, with C supporting functions. For more details, read the guestfs-inspection(8) man page added in this commit. --- .gitignore | 4 + Makefile.am | 2 + appliance/Makefile.am | 3 +- configure.ac | 1 + docs/guestfs-hacking.pod | 6 + generator/Makefile.am | 2 +- generator/main.ml | 4 + inspection/Makefile.am | 92 ++++++ inspection/detect.c | 152 ++++++++++ inspection/facts.c | 310 ++++++++++++++++++++ inspection/guestfs-inspection.pod | 450 ++++++++++++++++++++++++++++ inspection/inspection.c | 106 +++++++ inspection/inspection.h | 60 ++++ inspection/inspection.rules | 476 ++++++++++++++++++++++++++++++ inspection/match.c | 173 +++++++++++ inspection/mount.c | 597 ++++++++++++++++++++++++++++++++++++++ inspection/rules.h | 69 +++++ inspection/stringsbuf.c | 254 ++++++++++++++++ inspection/stringsbuf.h | 56 ++++ inspection/utils.c | 66 +++++ po/POTFILES | 7 + src/guestfs.pod | 1 + 22 files changed, 2889 insertions(+), 2 deletions(-) create mode 100644 inspection/Makefile.am create mode 100644 inspection/detect.c create mode 100644 inspection/facts.c create mode 100644 inspection/guestfs-inspection.pod create mode 100644 inspection/inspection.c create mode 100644 inspection/inspection.h create mode 100644 inspection/inspection.rules create mode 100644 inspection/match.c create mode 100644 inspection/mount.c create mode 100644 inspection/rules.h create mode 100644 inspection/stringsbuf.c create mode 100644 inspection/stringsbuf.h create mode 100644 inspection/utils.c diff --git a/.gitignore b/.gitignore index 67d8a2e..8da932c 100644 --- a/.gitignore +++ b/.gitignore @@ -252,6 +252,10 @@ Makefile.in /haskell/Guestfs030Config /haskell/Guestfs050LVCreate /haskell/Guestfs.hs +/inspection/guestfs-inspection +/inspection/guestfs-inspection.8 +/inspection/rules.c +/inspection/stamp-guestfs-inspection.pod /inspector/actual-*.xml /inspector/stamp-virt-inspector.pod /inspector/test-xmllint.sh diff --git a/Makefile.am b/Makefile.am index ba99feb..11f5e78 100644 --- a/Makefile.am +++ b/Makefile.am @@ -42,6 +42,7 @@ SUBDIRS += src docs examples po # The daemon and the appliance. if ENABLE_DAEMON +SUBDIRS += inspection SUBDIRS += daemon SUBDIRS += tests/daemon endif @@ -290,6 +291,7 @@ all-local: find $(DIST_SUBDIRS) -name '*.c' -o -name '*.pl' -o -name '*.pm' | \ grep -v -E '^(examples|gnulib|gobject/docs|perl/(blib|examples)|po-docs|tests|test-data)/' | \ grep -v -E '/((guestfs|rc)_protocol\.c)$$' | \ + grep -v -E '^inspection/rules\.c$$' | \ grep -v -E '^python/utils\.c$$' | \ LC_ALL=C sort > po/POTFILES cd $(srcdir); \ diff --git a/appliance/Makefile.am b/appliance/Makefile.am index d8fb15b..eb1f4d3 100644 --- a/appliance/Makefile.am +++ b/appliance/Makefile.am @@ -72,11 +72,12 @@ packagelist: packagelist.in Makefile cmp -s $@ $@-t || mv $@-t $@ rm -f $@-t -supermin.d/daemon.tar.gz: ../daemon/guestfsd guestfs_lvm_conf.aug guestfs_shadow.aug +supermin.d/daemon.tar.gz: ../daemon/guestfsd ../inspection/guestfs-inspection guestfs_lvm_conf.aug guestfs_shadow.aug rm -f $@ $@-t rm -rf tmp-d mkdir -p tmp-d$(DAEMON_SUPERMIN_DIR) tmp-d/etc tmp-d/usr/share/guestfs ln ../daemon/guestfsd tmp-d$(DAEMON_SUPERMIN_DIR)/guestfsd + ln ../inspection/guestfs-inspection tmp-d$(DAEMON_SUPERMIN_DIR)/guestfs-inspection ln $(srcdir)/guestfs_lvm_conf.aug tmp-d/usr/share/guestfs/guestfs_lvm_conf.aug ln $(srcdir)/guestfs_shadow.aug tmp-d/usr/share/guestfs/guestfs_shadow.aug ( cd tmp-d && tar zcf - * ) > $@-t diff --git a/configure.ac b/configure.ac index 29b5092..00fccbb 100644 --- a/configure.ac +++ b/configure.ac @@ -205,6 +205,7 @@ AC_CONFIG_FILES([Makefile golang/Makefile golang/examples/Makefile haskell/Makefile + inspection/Makefile inspector/Makefile java/Makefile java/examples/Makefile diff --git a/docs/guestfs-hacking.pod b/docs/guestfs-hacking.pod index 419a4c2..4cd76bf 100644 --- a/docs/guestfs-hacking.pod +++ b/docs/guestfs-hacking.pod @@ -558,6 +558,11 @@ L<virt-get-kernel(1)> command and documentation. Gnulib is used as a portability library. A copy of gnulib is included under here. +=item F<inspection> + +Inspection. See L<guestfs(3)/INSPECTION> and +L<guestfs-inspection(8)>. + =item F<inspector> L<virt-inspector(1)>, the virtual machine image inspector. @@ -837,6 +842,7 @@ Optionally do a full release of the development branch. L<guestfs(3)>, L<guestfs-building(1)>, L<guestfs-examples(3)>, +L<guestfs-inspection(8)>, L<guestfs-internals(3)>, L<guestfs-performance(1)>, L<guestfs-release-notes(1)>, diff --git a/generator/Makefile.am b/generator/Makefile.am index c53d3b9..210113a 100644 --- a/generator/Makefile.am +++ b/generator/Makefile.am @@ -194,7 +194,7 @@ noinst_DATA = stamp-generator # Git removes empty directories, so in cases where the # generator is creating the sole file in a directory, we # have to create the directory first. -stamp-generator: generator +stamp-generator: generator $(wildcard $(top_srcdir)/inspection/*.rules) mkdir -p $(top_srcdir)/perl/lib/Sys mkdir -p $(top_srcdir)/ruby/ext/guestfs mkdir -p $(top_srcdir)/java/com/redhat/et/libguestfs diff --git a/generator/main.ml b/generator/main.ml index 63a5d25..4137f85 100644 --- a/generator/main.ml +++ b/generator/main.ml @@ -213,6 +213,10 @@ Run it from the top source directory using the command output_to "customize/customize-synopsis.pod" generate_customize_synopsis_pod; output_to "customize/customize-options.pod" generate_customize_options_pod; + (* Run the rules compiler to generate inspection rules. *) + output_to "inspection/rules.c" + (Rules_compiler.compile "inspection/inspection.rules"); + (* Generate the list of files generated -- last. *) printf "generated %d lines of code\n" (get_lines_generated ()); let files = List.sort compare (get_files_generated ()) in diff --git a/inspection/Makefile.am b/inspection/Makefile.am new file mode 100644 index 0000000..5232bc3 --- /dev/null +++ b/inspection/Makefile.am @@ -0,0 +1,92 @@ +# libguestfs +# Copyright (C) 2015-2016 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +include $(top_srcdir)/subdir-rules.mk + +generator_built = \ + rules.c + +BUILT_SOURCES = \ + $(generator_built) + +CLEANFILES = \ + stamp-guestfs-inspection.pod \ + guestfs-inspection.8 + +# Build the inspection program. + +if INSTALL_DAEMON +sbin_PROGRAMS = guestfs-inspection +else +noinst_PROGRAMS = guestfs-inspection +endif + +guestfs_inspection_SOURCES = \ + ../daemon/cleanups.c \ + ../daemon/cleanups.h \ + ../daemon/command.c \ + ../daemon/command.h \ + detect.c \ + facts.c \ + inspection.c \ + inspection.h \ + match.c \ + mount.c \ + rules.c \ + rules.h \ + stringsbuf.c \ + stringsbuf.h \ + utils.c + +guestfs_inspection_LDADD = \ + $(AUGEAS_LIBS) \ + $(HIVEX_LIBS) \ + $(PCRE_LIBS) \ + $(top_builddir)/gnulib/lib/.libs/libgnu.a + +guestfs_inspection_CPPFLAGS = \ + -I$(top_srcdir)/gnulib/lib \ + -I$(top_builddir)/gnulib/lib \ + -I$(top_srcdir)/daemon \ + -I$(top_builddir)/daemon \ + -I$(top_srcdir)/src \ + -I$(top_builddir)/src + +guestfs_inspection_CFLAGS = \ + $(WARN_CFLAGS) $(WERROR_CFLAGS) \ + $(AUGEAS_CFLAGS) \ + $(HIVEX_CFLAGS) \ + $(PCRE_CFLAGS) + +# Manual pages and HTML files for the website. +if INSTALL_DAEMON +man_MANS = guestfs-inspection.8 +else +noinst_MANS = guestfs-inspection.8 +endif +noinst_DATA = $(top_builddir)/website/guestfs-inspection.8.html + +guestfs-inspection.8 $(top_builddir)/website/guestfs-inspection.8.html: stamp-guestfs-inspection.pod + +stamp-guestfs-inspection.pod: guestfs-inspection.pod + $(PODWRAPPER) \ + --section 8 \ + --man guestfs-inspection.8 \ + --html $(top_builddir)/website/guestfs-inspection.8.html \ + --license GPLv2+ \ + $< + touch $@ diff --git a/inspection/detect.c b/inspection/detect.c new file mode 100644 index 0000000..1ebabf0 --- /dev/null +++ b/inspection/detect.c @@ -0,0 +1,152 @@ +/* guestfs-inspection + * Copyright (C) 2009-2015 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <error.h> +#include <errno.h> + +#include "ignore-value.h" + +#include <pcre.h> + +#include "guestfs-internal-all.h" +#include "cleanups.h" +#include "inspection.h" + +int +get_distro_from_os_release (const char *fs, char **distro) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +get_version_from_os_release (const char *fs, char **major, char **minor) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +get_product_name_from_os_release (const char *fs, char **product_name) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +get_distro_from_lsb_release (const char *fs, char **distro) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +get_version_from_lsb_release (const char *fs, char **major, char **minor) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +get_product_name_from_lsb_release (const char *fs, char **product_name) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +get_version_from_oracle_release (const char *fs, char **major, char **minor) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +COMPILE_REGEXP (re_centos_old, "CentOS.*release (\\d+).*Update (\\d+)", 0) +COMPILE_REGEXP (re_centos, "CentOS.*release (\\d+)\\.(\\d+)", 0) +COMPILE_REGEXP (re_centos_no_minor, "CentOS.*release (\\d+)", 0) + +#define CENTOS_RELEASE_FILE "/etc/centos-release" + +int +get_version_from_centos_release (const char *fs, char **major, char **minor) +{ + CLEANUP_FREE char *line = first_line_of_file (fs, CENTOS_RELEASE_FILE); + + if (match2 (line, re_centos_old, major, minor) || + match2 (line, re_centos, major, minor)) + return 0; + + if ((*major = match1 (line, re_centos_no_minor)) != NULL) { + *minor = strdup ("0"); + if (*minor == NULL) + error (EXIT_FAILURE, errno, "strdup"); + return 0; + } + + fprintf (stderr, "%s: cannot parse major/minor version from file", + CENTOS_RELEASE_FILE); + return -1; +} + +int +get_version_from_altlinux_release (const char *fs, char **major, char **minor) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +match_redhat_release_fedora (const char *fs) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +match_redhat_release_rhel (const char *fs) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +match_redhat_release_centos (const char *fs) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +match_redhat_release_scientific_linux (const char *fs) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} + +int +get_version_from_redhat_release (const char *fs, char **major, char **minor) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return -1; +} diff --git a/inspection/facts.c b/inspection/facts.c new file mode 100644 index 0000000..d4ba7d7 --- /dev/null +++ b/inspection/facts.c @@ -0,0 +1,310 @@ +/* guestfs-inspection + * Copyright (C) 2009-2015 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* Handle the true and false facts sets, and other helper functions. */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <stdbool.h> +#include <string.h> +#include <inttypes.h> +#include <assert.h> +#include <error.h> +#include <errno.h> +#include <alloca.h> + +#include "gl_oset.h" +#include "gl_xoset.h" +#include "gl_array_oset.h" + +#include "rules.h" + +/* True and false facts sets. See guestfs-inspection(8)/WRITING RULES + * to understand why these are used. The fact structs themselves are + * identical, we only know that a fact is false or true based on which + * list it appears on. + */ +static gl_oset_t true_facts = NULL; +static gl_oset_t false_facts = NULL; + +static int +compare_facts (const void *vf1, const void *vf2) +{ + size_t i; + int r; + const fact *f1 = vf1; + const fact *f2 = vf2; + + r = strcmp (f1->term_name, f2->term_name); + if (r != 0) return r; + + /* If term names are equal, they are supposed to have the same + * number of arguments. We type-checked that when reading the + * source. However, better check. + */ + assert (f1->nr_term_args == f2->nr_term_args); + + for (i = 0; i < f1->nr_term_args; ++i) { + r = strcmp (f1->term_arg[i], f2->term_arg[i]); + if (r != 0) return r; + } + + return 0; +} + +static void +free_fact (const void *vf) +{ + /* Why is the parameter const? + * See Bruno Haible's explanation here: + * https://www.mail-archive.com/bug-gnulib@gnu.org/msg08619.html + */ + fact *f = (fact *) vf; + size_t i; + + free (f->term_name); + for (i = 0; i < f->nr_term_args; ++i) + free (f->term_arg[i]); + free (f); +} + +static void init_facts (void) __attribute__((constructor)); + +static void +init_facts (void) +{ + true_facts = gl_oset_create_empty (GL_ARRAY_OSET, compare_facts, free_fact); + false_facts = gl_oset_create_empty (GL_ARRAY_OSET, compare_facts, free_fact); +} + +static void free_facts (void) __attribute__((destructor)); + +static void +free_facts (void) +{ + gl_oset_free (true_facts); + gl_oset_free (false_facts); +} + +static void +clear_set (gl_oset_t set) +{ + const void *f; + gl_oset_iterator_t iter = gl_oset_iterator (set); + + while (gl_oset_iterator_next (&iter, &f)) { + gl_oset_remove (set, (void *) f); + } + gl_oset_iterator_free (&iter); +} + +void +clear_true_facts (void) +{ + clear_set (true_facts); +} + +void +clear_false_facts (void) +{ + clear_set (false_facts); +} + +size_t +count_true_facts (void) +{ + return gl_oset_size (true_facts); +} + +/* This is just for debugging facts. */ +void +print_fact (bool is_true, const fact *f, FILE *fp) +{ + size_t i; + + if (!is_true) + fputc ('!', fp); + fputs (f->term_name, fp); + if (f->nr_term_args > 0) + fputc ('(', fp); + for (i = 0; i < f->nr_term_args; ++i) { + fputc ('"', fp); + fputs (f->term_arg[i], fp); + fputc ('"', fp); + if (i+1 != f->nr_term_args) + fputs (", ", fp); + } + if (f->nr_term_args > 0) + fputc (')', fp); +} + +static void +print_set (bool is_true, gl_oset_t set) +{ + const void *vf; + gl_oset_iterator_t iter = gl_oset_iterator (set); + + while (gl_oset_iterator_next (&iter, &vf)) { + fact *f = (fact *) vf; + print_fact (is_true, f, stdout); + printf ("\n"); + } + gl_oset_iterator_free (&iter); +} + +void +print_true_facts (void) +{ + print_set (true, true_facts); +} + +void +print_false_facts (void) +{ + print_set (false, false_facts); +} + +/* Look for every string parameter of every fact we know about, and + * add all those strings to the set. + */ +void +add_all_fact_strings (gl_oset_t set) +{ + const void *vf; + fact *f; + gl_oset_iterator_t iter; + size_t i; + + iter = gl_oset_iterator (true_facts); + while (gl_oset_iterator_next (&iter, &vf)) { + f = (fact *) vf; + for (i = 0; i < f->nr_term_args; ++i) + gl_oset_add (set, f->term_arg[i]); + } + gl_oset_iterator_free (&iter); + + iter = gl_oset_iterator (false_facts); + while (gl_oset_iterator_next (&iter, &vf)) { + f = (fact *) vf; + for (i = 0; i < f->nr_term_args; ++i) + gl_oset_add (set, f->term_arg[i]); + } + gl_oset_iterator_free (&iter); +} + +/* Look for every string parameter in the specific argument position + * of the specific term name (both true and false), and add those + * strings only to the set. + */ +void +add_strings_from_facts (gl_oset_t set, const char *term_name, size_t arg_i) +{ + const void *vf; + fact *f; + gl_oset_iterator_t iter; + + iter = gl_oset_iterator (true_facts); + while (gl_oset_iterator_next (&iter, &vf)) { + f = (fact *) vf; + if (strcmp (f->term_name, term_name) == 0) + gl_oset_add (set, f->term_arg[arg_i]); + } + gl_oset_iterator_free (&iter); + + iter = gl_oset_iterator (false_facts); + while (gl_oset_iterator_next (&iter, &vf)) { + f = (fact *) vf; + if (strcmp (f->term_name, term_name) == 0) + gl_oset_add (set, f->term_arg[arg_i]); + } + gl_oset_iterator_free (&iter); +} + +/* NB: This does not make a deep copy of the strings. However before + * we add the fact to the true_facts or false_facts arrays, we do call + * deep_copy to copy the strings. + */ +fact * +create_fact (const char *term_name, size_t n, ...) +{ + fact *f; + size_t i; + va_list args; + + f = malloc (sizeof (*f) + n * sizeof (char *)); + if (f == NULL) + error (EXIT_FAILURE, errno, "malloc"); + va_start (args, term_name); + for (i = 0; i < n; ++i) { + const char *p = va_arg (args, const char *); + f->term_arg[i] = (char *) p; + } + va_end (args); + f->term_name = (char *) term_name; + f->nr_term_args = n; + return f; /* caller must free only the struct */ +} + +static fact * +deep_copy_fact (const fact *f) +{ + fact *ret; + size_t i; + + ret = malloc (sizeof (*ret) + f->nr_term_args * sizeof (char *)); + if (ret == NULL) + error (EXIT_FAILURE, errno, "malloc"); + + ret->term_name = strdup (f->term_name); + if (ret->term_name == NULL) + error (EXIT_FAILURE, errno, "strdup"); + + ret->nr_term_args = f->nr_term_args; + + for (i = 0; i < f->nr_term_args; ++i) { + ret->term_arg[i] = strdup (f->term_arg[i]); + if (ret->term_arg[i] == NULL) + error (EXIT_FAILURE, errno, "strdup"); + } + + return ret; +} + +bool +is_fact (bool is_true, const fact *f) +{ + return gl_oset_search (is_true ? true_facts : false_facts, f); +} + +bool +add_fact (bool is_true, const fact *f) +{ + fact *f2 = deep_copy_fact (f); + bool ret; + + ret = gl_oset_add (is_true ? true_facts : false_facts, f2); + + /* Didn't add it, so we must free the deep copy. */ + if (!ret) + free_fact (f2); + + return ret; +} diff --git a/inspection/guestfs-inspection.pod b/inspection/guestfs-inspection.pod new file mode 100644 index 0000000..2848308 --- /dev/null +++ b/inspection/guestfs-inspection.pod @@ -0,0 +1,450 @@ +=head1 NAME + +guestfs-inspection - guestfs inspection program + +=head1 SYNOPSIS + + guestfs-inspection + +=head1 NOTE + +This man page documents the guestfs inspection program. If you want +to read about guestfs inspection then this is the wrong place. See +L<guestfs(3)/INSPECTION> instead. + +=head1 DESCRIPTION + +C<guestfs-inspection> is a standalone program that performs inspection +on the local disks, to find out what operating system(s) are +installed. It normally runs inside the libguestfs appliance, started +by L<guestfsd(8)>, when the caller uses the C<guestfs_inspect_os> API +(see L<guestfs-internals(1)> and L<guestfs(3)/guestfs_inspect_os>). +You should never need to run this program by hand. + +The program looks at all disks attached to the appliance, looking for +filesystems that might belong to operating systems. It may mount +these temporarily to examine them for Linux configuration files, +Windows Registries, and so on. It then tries to determine what +operating system(s) are installed on the disks. It is able to detect +many different Linux distributions, Windows, BSD, and others. The +currently mounted root filesystem is ignored, since when running under +libguestfs, that filesystem is part of the libguestfs appliance (this +is the main difference compared to programs like C<facter>). + +Guestfs-inpection is written in C, but most of the C is generated by a +rules compiler from a set of inspection rules written in a more +compact, declarative, Prolog-inspired language. If you want to write +or modify the rules, see L</WRITING RULES> below. + +=head1 OPTIONS + +=over 4 + +=item B<-?> + +=item B<--help> + +Display brief help. + +=item B<-v> + +=item B<--verbose> + +Enable verbose messages for debugging. + +=back + +=head1 WRITING RULES + +Inspection is performed according to a set of rules written in a +compact, declarative, Prolog-inspired language. This section explains +how this language works, so you can write your own rules to detect +other operating systems. + +The rules can be found starting in C<inspection/inspection.rules> (in +the libguestfs sources). The rules are compiled down to C and linked +into the guestfs-inspection program, together with a bit of extra C +code to provide runtime support. + +=head2 Facts + +Facts are what we try to determine about the operating system(s) we +are inspecting. They look like this: + + Filesystem("/dev/sda1") + +which means "F</dev/sda1> is a filesystem". + + File("/dev/sda1", "/etc/fstab") + +which means "there exists a file called F</etc/fstab> on the +F</dev/sda1> filesystem". + +Facts come in three flavours: true facts, false facts, and unknown +facts. False facts are written like this: + + ! File("/dev/sda1", "/etc/fstab") + +which means "either F</dev/sda1> is not a filesystem or there does not +exist a file called F</etc/fstab> on this filesystem". + +Unknown facts are facts that we don't know if they are true or false +yet. + +=head2 Rules + +Rules are used to generate more facts. A simple rule for generating +C<File> facts might look like this: + + File(fs, filename) :- + Filesystem(fs), + {{ + // some C code to mount 'fs' and check for 'filename' + }}. + +You can read this as: "For all C<fs> & C<filename>, if C<fs> is a +filesystem, and running the C code with parameters C<fs> and +C<filename> returns true, then C<File(fs, filename)> is a true fact". + +In the Prolog-inspired language, a comma (C<,>) is the AND operator. +A semicolon (C<;>) is the OR operator. C<:-> is a backwards +if-statement (the condition is on the right, the conclusion is on the +left). Also notice the dot (C<.>) which must follow each rule. + +Uppercase identifiers are facts. Lowercase identifiers are variables. +All identifiers are case-sensitive. + +Everything in C<{{ ... }}> is embedded C code. In this case the C +code returns a true/false/error indication, but embedded C code can +also do more complicated things and return strings and lists as we'll +see later. + +You can use parentheses C<(...)> for grouping expressions on the right +hand side of the C<:-> operator. + +=head2 Program evaluation + +Let's take a simple set of rules which you might use to detect a +Fedora root filesystem: + + File(fs, filename) :- + Filesystem(fs), + {{ + // some C code to mount 'fs' and check for 'filename' + }}. + + Fedora(rootfs) :- + Filesystem(rootfs), + File(rootfs, "/etc/fedora-release"). + +When evaluating this program, there are two sets of facts, the true +facts and the false facts. Let's start with the false facts set being +empty, and let's seed the true facts set with some C<Filesystem> +facts: + + true_facts = { Filesystem("/dev/sda1"), Filesystem("/dev/sda3") } + false_facts = { } // empty set + +Unknown facts are facts which don't appear in either set. + +Evaluating the program works like this: We consider each rule in turn, +and see if we can find new true or false facts from it. These new +facts are added to the true or false facts sets. After looking at +each rule in the program, as long as at least one new fact was added +to the true facts set, we go back to the start of the rules and repeat +over. We do this until we can no longer add any new true facts, and +then we're done. + +In the case of this program, we start with the C<File> rule, and we +substitute (theoretically) every possible string for C<fs> and +C<filename>. + +For example, this substitution: + + File("/dev/sda1", "/etc/fedora-release") :- + Filesystem("/dev/sda1"), + {{ // checks for file and returns false }}. + +turns out to be false (because the C code doesn't find F</etc/fstab> +in F</dev/sda1>), so that yields a new false fact: + + ! File("/dev/sda1", "/etc/fedora-release") + +But this substitution turns out to be true: + + File("/dev/sda3", "/etc/fedora-release") :- + Filesystem("/dev/sda3"), + {{ // checks for file and returns true }}. + +so that yields a new true fact: + + File("/dev/sda3", "/etc/fedora-release") + +In theory every possible string is tried, eg C<File("ardvark", "foo123654")>. +That would take literally forever to run, but luckily the rules +compiler is smarter. + +Looking now at the second rule, we try this substitution: + + Fedora("/dev/sda3") :- + Filesystem("/dev/sda3"), + File("/dev/sda3", "/etc/fedora-release"). + +which yields another new true fact: + + Fedora("/dev/sda3") + +Because we added several new true facts to the set, we go back and +repeat the whole process. But after trying all the rules for a second +time, no more true facts can be added, so now we're done. + +At the end, the set of true facts is: + + true_facts = { Filesystem("/dev/sda1"), Filesystem("/dev/sda3"), + File("/dev/sda3", "/etc/fedora-release"), + Fedora("/dev/sda3") } + +We don't care about the false facts -- they are discarded at the end +of the program. + +The summary of inspection is that F</dev/sda3> contains a Fedora root +filesystem. + +Of course real inspection is much more complicated than this, but the +same program evaluation order is followed. + +=head2 Some caveats with the language + +It's easy to look at an expression like: + + Fedora(rootfs) :- + Filesystem(rootfs), + File(rootfs, "/etc/fedora-release"). /* line 3 */ + +and think that line 3 is "calling" the "File function". This is +B<not> what is happening! Rules are not functions. Rules are +considered in isolation. Rules don't "call" other rules. Instead +when trying to find possible values that can be substituted into a +rule, we only look at the rule and the current sets of true and false +facts. + +When searching for values to subsitute, in theory the compiler would +have to look at every possible string. In practice of course it can't +and doesn't do that. Instead it looks at the current sets of true and +false facts to find strings to substitute. In the following rule: + + File(fs, filename) :- + Filesystem(fs), + {{ // C code }}. + +suitable choices for C<fs> are found by looking at any C<Filesystem> +facts in either the true or false sets. + +In some cases, this doesn't work, as in the example above where we +have no clues for the C<filename> variable. In that case the compiler +tries every string literal from every rule in the program. This can +be inefficient, but by modifying the rule slightly you can avoid this. +In the following program, only the strings F</etc/fstab> and +F</etc/fedora-release> would be tried: + + Filename("/etc/fstab"). + Filename("/etc/fedora-release"). + File(fs, filename) :- + Filesystem(fs), + Filename(filename), + {{ // C code }}. + +=head2 C expressions returning boolean + +Simple C code enclosed in C<{{ ... }}> as shown above should return a +true, false or error status only. It returns true by returning any +integer E<ge> 1. It should return C<0> to indicate false, and it +should return C<-1> to indicate an error (which stops the program and +causes inspection to fail with a user-visible error). + +Here is an example of a simple C expression returning a boolean: + + File(fs, filename) :- + Filesystem(fs), + {{ + int r; + char *relative_filename; + r = get_mount (fs, filename, &relative_filename); + if (r != 1) return r; + r = access (relative_filename, F_OK); + free (relative_filename); + if (r == -1) { + if (errno == ENOENT || errno == ENOTDIR) + return 0; + perror ("access"); + return -1; + } + return 1; + }}. + +Notice that C<fs> and C<filename> are passed into the C code as local +variables. + +You can see that dealing with errors is a bit involved, because we +want to fail hard if some error like C<EIO> is thrown. + +=head2 C expressions returning strings + +C expressions can also return strings or tuples of strings. This is +useful where you want to parse the content of external files. + +The syntax for this sort of C expression is: + + (var1, var2, ...)={{ ... }} + +where C<var1>, C<var2>, etc. are outputs from the C code. + +In the following example, a lot of error checking has been omitted +for clarity: + + ProductName(fs, product_name) :- + Unix_root(fs), + Distro(fs, "RHEL"), + (product_name)={{ + int r; + char *line = NULL; + size_t n; + char *relative_filename; + r = get_mount (fs, "/etc/redhat-release", &relative_filename); + FILE *fp = fopen (relative_filename, "r"); + free (relative_filename); + getline (&line, &n, fp); + fclose (fp); + set_product_name (line); + free (line); + return 0; + }}. + +The C code calls a function C<set_product_name> (that the compiler +generates). + +The return value from the C code should be C<0> if everything was OK, +or C<-1> if there is a error (which stops the whole program). + +=head2 C expressions returning multiple results + +Finally it is possible for C code to return multiple results. + +The syntax is: + + (var1, var2, ...)*={{ ... }} + + (var1, var2, ...)?={{ ... }} + + (var1, var2, ...)+={{ ... }} + +where C<var1>, C<var2>, etc. are outputs. Unlike the previous rules, +these rules may generate zero or multiple facts from a single string +substitution. + +For example, here is how we could populate a list of C<Filesystem> +facts: + + Filesystem(fs) :- + (fs)*={{ + int i; + extern char **fs; + for (i = 0; fs[i] != NULL; ++i) { + set_fs (fs[i]); + } + return 0; + }}. + +In this case, the C code repeatedly calls a function C<set_fs> (that +the compiler generates) for each new filesystem discovered. Multiple +C<Filesystem> facts can be generated as a result of one application of +this rule. + +As with regular expressions, C<*> means we expect zero or more rows of +results, C<?> means zero or one, and C<+> means one or more. The +generated target code checks that you call the C<set_*> function the +correct number of times. + +The return value from the C code should be C<0> if everything was OK, +or C<-1> if there is a error (which stops the whole program). + +=head2 Prologue and epilogue + +If you want to insert arbitrary chunks of C code into the output, for +example to C<#include> headers, then you can insert literal code +between C<{{ ... }}> at the top and bottom of the file (ie. before +all rules and after all rules). You cannot insert these chunks +between rules. + +=head2 C code memoization + +Although currently B<only partially implemented>, in future we will +implement memoization of C code. This means that for every input, the +C code will be called only once, making it less important that C code +is efficient: no matter how many times we need to evaluate if +F</etc/fstab> exists on F</dev/sda1>, the C code would only be called +once. + +This means that impure C functions returning different results for the +same input won't work in future, so don't do that. + +=head2 Type checking + +The current language treats every value as a string. Every expression +is a boolean. One possible future enhancement is to handle other +types. There is still some minimal type checking applied: + +=over 4 + +=item * + +A fact name which appears on a right hand side of any rule must also +appear on the left hand side of a rule. This is mainly for catching +typos. + +=item * + +A fact must have the same number of arguments ("arity") each time it +appears in the source. + +=back + +=head2 Debugging + +You can debug the evaluation of inspection programs by calling +C<guestfs_set_verbose> (or setting C<$LIBGUESTFS_DEBUG=1>) before +launching the handle. + +This causes L<guestfsd(8)> to pass the I<--verbose> parameter to this +inspection program, which in turn causes the inspection program to +print information about what rules it is trying and what true/false +facts it has found. These are passed back to libguestfs and printed +on C<stderr> (or sent to the event system if you are using that). + +You can also print debug messages from C code embedded in C<{{...}}> +expressions. These are similarly sent upwards through to libguestfs +and will appear on C<stderr>. + +Remember that C memoization [to be implemented in a future version] +can cause C code to run fewer times than expected. + +=head1 EXIT STATUS + +This program returns 0 if successful, or non-zero if there was an +error. + +=head1 SEE ALSO + +L<guestfsd(8)>, +L<guestfs-hacking(1)>, +L<guestfs-internals(1)>, +L<guestfs(3)/INSPECTION>, +L<http://libguestfs.org/>. + +=head1 AUTHOR + +Richard W.M. Jones L<http://people.redhat.com/~rjones/> + +=head1 COPYRIGHT + +Copyright (C) 2009-2016 Red Hat Inc. diff --git a/inspection/inspection.c b/inspection/inspection.c new file mode 100644 index 0000000..d84b9fe --- /dev/null +++ b/inspection/inspection.c @@ -0,0 +1,106 @@ +/* guestfs-inspection + * Copyright (C) 2009-2015 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <inttypes.h> +#include <getopt.h> +#include <unistd.h> + +#include "rules.h" + +int verbose = 0; +const char *sysroot = NULL; +const size_t sysroot_len = 0; + +/* Required by the gnulib 'error' module. */ +const char *program_name = "guestfs-inspection"; + +static void __attribute__((noreturn)) +usage (int status) +{ + if (status != EXIT_SUCCESS) + fprintf (stderr, "Try `%s --help' for more information.\n", + program_name); + else { + printf ("%s: guestfs inspection\n" + "Copyright (C) 2009-2015 Red Hat Inc.\n" + "Usage:\n" + "Options:\n" + " --help Display brief help\n" + " -v|--verbose Verbose messages\n" + " -V|--version Display version and exit\n" + "For more information, see the manpage %s(8).\n", + program_name, program_name); + } + exit (status); +} + +int +main (int argc, char *argv[]) +{ + enum { HELP_OPTION = CHAR_MAX + 1 }; + + static const char *options = "vV"; + static const struct option long_options[] = { + { "help", 0, 0, HELP_OPTION }, + { "verbose", 0, 0, 'v' }, + { "version", 0, 0, 'V' }, + { 0, 0, 0, 0 } + }; + int c; + int option_index; + + for (;;) { + c = getopt_long (argc, argv, options, long_options, &option_index); + if (c == -1) break; + + switch (c) { + case 0: /* options which are long only */ + fprintf (stderr, "%s: unknown long option: %s (%d)\n", + program_name, + long_options[option_index].name, option_index); + exit (EXIT_FAILURE); + + case 'v': + verbose++; + break; + + case 'V': + printf ("%s %s\n", program_name, PACKAGE_VERSION_FULL); + exit (EXIT_SUCCESS); + + case HELP_OPTION: + usage (EXIT_SUCCESS); + + default: + usage (EXIT_FAILURE); + } + } + + /* Run the rules. */ + rules (); + + /* Print the true facts. XXX Output XXX */ + print_true_facts (); + + exit (EXIT_SUCCESS); +} diff --git a/inspection/inspection.h b/inspection/inspection.h new file mode 100644 index 0000000..f3359b1 --- /dev/null +++ b/inspection/inspection.h @@ -0,0 +1,60 @@ +/* guestfs-inspection + * Copyright (C) 2009-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef GUESTFS_INSPECTION_H +#define GUESTFS_INSPECTION_H + +#include <pcre.h> + +/* detect.c - used by rules */ +extern int get_distro_from_os_release (const char *fs, char **distro); +extern int get_version_from_os_release (const char *fs, char **major, char **minor); +extern int get_product_name_from_os_release (const char *fs, char **product_name); +extern int get_distro_from_lsb_release (const char *fs, char **distro); +extern int get_version_from_lsb_release (const char *fs, char **major, char **minor); +extern int get_product_name_from_lsb_release (const char *fs, char **product_name); +extern int get_version_from_oracle_release (const char *fs, char **major, char **minor); +extern int get_version_from_centos_release (const char *fs, char **major, char **minor); +extern int get_version_from_altlinux_release (const char *fs, char **major, char **minor); +extern int match_redhat_release_fedora (const char *fs); +extern int match_redhat_release_rhel (const char *fs); +extern int match_redhat_release_centos (const char *fs); +extern int match_redhat_release_scientific_linux (const char *fs); +extern int get_version_from_redhat_release (const char *fs, char **major, char **minor); + +/* match.c */ +extern char *match1 (const char *str, const pcre *re); +extern int match2 (const char *str, const pcre *re, char **ret1, char **ret2); + +/* mount.c - used by rules */ +extern char **get_all_block_devices (void); +extern char **get_all_partitions (void); +extern char **get_all_mddevs (void); +extern char **get_all_lvs (void); +extern char **get_all_ldmvols (void); +extern char **get_all_ldmparts (void); +extern char **get_all_btrfs_subvolumes (const char *fs); +extern char *get_vfs_type (const char *fs); +extern int get_partition_mbr_id (const char *fs); +extern int is_mountable (const char *fs); +extern int get_mount (const char *fs, const char *filename, char **relative_filename); + +/* utils.c */ +extern char *first_line_of_file (const char *fs, const char *filename); + +#endif /* GUESTFS_INSPECTION_H */ diff --git a/inspection/inspection.rules b/inspection/inspection.rules new file mode 100644 index 0000000..0bdad19 --- /dev/null +++ b/inspection/inspection.rules @@ -0,0 +1,476 @@ +/* Libguestfs inspection rules -*- prolog -*- + * Copyright (C) 2009-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* To understand what's going on here, it's recommended that you read + * guestfs-inspection(8) (inspection/guestfs-inspection.pod) first. + */ + +{{ +#include "inspection.h" +#include <sys/types.h> +#include <sys/stat.h> +#include <errno.h> +}} + +/* Whole block devices. */ +BlockDevice(dev) :- + (dev)*={{ + CLEANUP_FREE_STRING_LIST char **devs = get_all_block_devices (); + if (devs == NULL) return -1; + for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]); + return 0; + }}. + +/* Partitions. */ +Partition(dev) :- + (dev)*={{ + CLEANUP_FREE_STRING_LIST char **devs = get_all_partitions (); + if (devs == NULL) return -1; + /* Ignore partitions with type byte 0x42 (RHBZ#887520). */ + for (size_t i = 0; devs[i] != NULL; ++i) { + int r = get_partition_mbr_id (devs[i]); + if (r == -1) return -1; + if (r != 0x42) set_dev (devs[i]); + } + return 0; + }}. + +/* LVM2 logical volumes. */ +LV(dev) :- + (dev)*={{ + CLEANUP_FREE_STRING_LIST char **devs = get_all_lvs (); + if (devs == NULL) return -1; + for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]); + return 0; + }}. + +/* /dev/md* devices. */ +MDDev(dev) :- + (dev)*={{ + CLEANUP_FREE_STRING_LIST char **devs = get_all_mddevs (); + if (devs == NULL) return -1; + for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]); + return 0; + }}. + +/* Windows LDM voumes. */ +LDMVol(dev) :- + (dev)*={{ + CLEANUP_FREE_STRING_LIST char **devs = get_all_ldmvols (); + if (devs == NULL) return -1; + for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]); + return 0; + }}. + +/* Windows LDM partitions. */ +LDMPart(dev) :- + (dev)*={{ + CLEANUP_FREE_STRING_LIST char **devs = get_all_ldmparts (); + if (devs == NULL) return -1; + for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]); + return 0; + }}. + +/* Device(dev) is just a group name for block devices, partitions etc. */ +Device(dev) :- + BlockDevice(dev); Partition(dev); LV(dev); + MDDev(dev); LDMVol(dev); LDMPart(dev). + +/* Map a filesystem to its VFS type (from blkid). */ +VFSType(fs, vfs_type) :- + Device(fs), + (vfs_type)?={{ + CLEANUP_FREE char *vfs_type = get_vfs_type (fs); + if (vfs_type != NULL) set_vfs_type (vfs_type); + return 0; + }}. + +/* A device contains a mountable filesystem (not swap, empty, etc). */ +Mountable(fs) :- + Device(fs), + {{ return is_mountable (fs); }}. + +/* Where a filesystem is btrfs and mountable, get the subvolumes. */ +BtrfsSubvolume(subvol) :- + Device(fs), + Mountable(fs), + VFSType(fs, "btrfs"), + (subvol)*={{ + size_t i; + CLEANUP_FREE_STRING_LIST char **paths = get_all_btrfs_subvolumes (fs); + if (paths == NULL) return -1; + for (i = 0; paths[i] != NULL; ++i) { + CLEANUP_FREE char *subvol; + if (asprintf (&subvol, "btrfsvol:%s/%s", fs, paths[i]) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + set_subvol (subvol); + } + return 0; + }}. +VFSType(subvol, "btrfs") :- BtrfsSubvolume(subvol). + +/* Ignore all *_member types. In libblkid these are returned + * for things which are members of some RAID or LVM set, most + * importantly "LVM2_member" which is a PV. Also ignore + * crypto_LUKS (LUKS encrypted partition). + */ +ContainerDevice(dev) :- + VFSType(dev, vfs_type), + {{ return STRSUFFIX (vfs_type, "_member"); }}. +ContainerDevice(dev) :- + VFSType(dev, "crypto_LUKS"). + +/* Ignore all swap devices. */ +SwapDevice(dev) :- VFSType(dev, "swap"). + +/* This rule generates one Filesystem(fs) fact per mountable + * filesystem found in the appliance. A filesystem could be + * a device, partition, LV, btrfs subvolume, etc. + */ +Filesystem(fs) :- + !ContainerDevice(fs), + !SwapDevice(fs), + (Device(fs), Mountable(fs); BtrfsSubvolume(fs)). + +/* File(fs, filename) is true if filename is a regular file in fs. + * It also follows symlinks. + */ +File(fs, filename) :- + Filesystem(fs), + {{ + int r; + CLEANUP_FREE char *relative_filename = NULL; + struct stat statbuf; + + if (filename[0] != '/') return 0; + if (get_mount (fs, filename, &relative_filename) == -1) + return -1; + r = stat (relative_filename, &statbuf); + if (r == -1) { + if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP) + return 0; + perror (relative_filename); + return -1; + } + return S_ISREG (statbuf.st_mode); + }}. + +/* Directory(fs, dirname) is true if dirname is a directory in fs. + * It also follows symlinks. + */ +Directory(fs, dirname) :- + Filesystem(fs), + {{ + int r; + CLEANUP_FREE char *relative_dirname = NULL; + struct stat statbuf; + + if (dirname[0] != '/') return 0; + if (get_mount (fs, dirname, &relative_dirname) == -1) + return -1; + r = stat (relative_dirname, &statbuf); + if (r == -1) { + if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP) + return 0; + perror (relative_dirname); + return -1; + } + return S_ISDIR (statbuf.st_mode); + }}. + +/* Symlink(fs, filename) is true if filename is a symlink. */ +Symlink(fs, filename) :- + Filesystem(fs), + {{ + int r; + CLEANUP_FREE char *relative_filename = NULL; + struct stat statbuf; + + if (filename[0] != '/') return 0; + if (get_mount (fs, filename, &relative_filename) == -1) + return -1; + r = stat (relative_filename, &statbuf); + if (r == -1) { + if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP) + return 0; + perror (relative_filename); + return -1; + } + return S_ISLNK (statbuf.st_mode); + }}. + +/* grub or grub2 /boot */ +GrubBoot(fs) :- + Filesystem(fs), + File(fs, "/grub/menu.lst"), + File(fs, "/grub/grub.conf"), + File(fs, "/grub2/grub.cfg"). + +/* FreeBSD root. */ +FreeBSDRoot(fs) :- + Filesystem(fs), + Directory(fs, "/bin"), + Directory(fs, "/etc"), + File(fs, "/etc/freebsd-update.conf"), + File(fs, "/etc/fstab"). + +/* NetBSD root. */ +NetBSDRoot(fs) :- + Filesystem(fs), + Directory(fs, "/bin"), + Directory(fs, "/etc"), + File(fs, "/netbsd"), + File(fs, "/etc/fstab"), + File(fs, "/etc/release"). + +/* OpenBSD root. */ +OpenBSDRoot(fs) :- + Filesystem(fs), + Directory(fs, "/bin"), + Directory(fs, "/etc"), + File(fs, "/bsd"), + File(fs, "/etc/fstab"), + File(fs, "/etc/motd"). + +/* Hurd root. */ +HurdRoot(fs) :- + Filesystem(fs), + File(fs, "/hurd/console"), + File(fs, "/hurd/hello"), + File(fs, "/hurd/null"). + +/* Minix root. */ +MinixRoot(fs) :- + Filesystem(fs), + File(fs, "/service/vm"), + File(fs, "/etc/fstab"), + File(fs, "/etc/version"). + +/* Linux root (any distro). */ +LinuxRoot(fs) :- + Filesystem(fs), + Directory(fs, "/etc"), + File(fs, "/etc/fstab"), + (Directory(fs, "/bin"); Symlink(fs, "/bin")), + !FreeBSDRoot(fs), + !NetBSDRoot(fs), + !OpenBSDRoot(fs), + !HurdRoot(fs), + !MinixRoot(fs). + +/* Is it Linux using /etc/os-release? (Linux systemd). */ +LinuxRootWithOSRelease(fs) :- + LinuxRoot(fs), + File(fs, "/etc/os-release"). + +Distro(fs, distro) :- + LinuxRootWithOSRelease(fs), + (distro)?={{ + int r; + CLEANUP_FREE char *distro = NULL; + if ((r = get_distro_from_os_release (fs, &distro)) <= 0) + return r; + set_distro (distro); + return 0; + }}. + +Version(fs, major, minor) :- + LinuxRootWithOSRelease(fs), + (major, minor)?={{ + int r; + CLEANUP_FREE char *major = NULL, *minor = NULL; + if ((r = get_version_from_os_release (fs, &major, &minor)) <= 0) + return r; + set_major_minor (major, minor); + return 0; + }}. + +ProductName(fs, product_name) :- + LinuxRootWithOSRelease(fs), + (product_name)?={{ + int r; + CLEANUP_FREE char *product_name = NULL; + if ((r = get_product_name_from_os_release (fs, &product_name)) <= 0) + return r; + set_product_name (product_name); + return 0; + }}. + +/* Is it Linux using /etc/lsb-release? (Linux Standards Base). */ +LinuxRootWithLSBRelease(fs) :- + LinuxRoot(fs), + File(fs, "/etc/lsb-release"), + !LinuxRootWithOSRelease(fs). /* prefer /etc/os-release */ + +Distro(fs, distro) :- + LinuxRootWithLSBRelease(fs), + (distro)?={{ + int r; + CLEANUP_FREE char *distro = NULL; + if ((r = get_distro_from_lsb_release (fs, &distro)) <= 0) + return r; + set_distro (distro); + return 0; + }}. + +Version(fs, major, minor) :- + LinuxRootWithLSBRelease(fs), + (major, minor)?={{ + int r; + CLEANUP_FREE char *major = NULL, *minor = NULL; + if ((r = get_version_from_lsb_release (fs, &major, &minor)) <= 0) + return r; + set_major_minor (major, minor); + return 0; + }}. + +ProductName(fs, product_name) :- + LinuxRootWithLSBRelease(fs), + (product_name)?={{ + int r; + CLEANUP_FREE char *product_name = NULL; + if ((r = get_product_name_from_lsb_release (fs, &product_name)) <= 0) + return r; + set_product_name (product_name); + return 0; + }}. + +/* Fall back on /etc/*-release files to determine the distro, version and + * product name. + */ +LinuxRootWithReleaseFile(fs, "/etc/oracle-release") :- + LinuxRoot(fs), + !LinuxRootWithOSRelease(fs), !LinuxRootWithLSBRelease(fs), + File(fs, "/etc/oracle-release"). +Distro(fs, "oracle-linux") :- + LinuxRootWithReleaseFile(fs, "/etc/oracle-release"). +Version(fs, major, minor) :- + LinuxRootWithReleaseFile(fs, "/etc/oracle-release"), + (major, minor)={{ + CLEANUP_FREE char *major = NULL, *minor = NULL; + if (get_version_from_oracle_release (fs, &major, &minor) == -1) + return -1; + set_major_minor (major, minor); + return 0; + }}. + +LinuxRootWithReleaseFile(fs, "/etc/centos-release") :- + LinuxRoot(fs), + !LinuxRootWithOSRelease(fs), !LinuxRootWithLSBRelease(fs), + File(fs, "/etc/centos-release"). +Distro(fs, "centos") :- + LinuxRootWithReleaseFile(fs, "/etc/centos-release"). +Version(fs, major, minor) :- + LinuxRootWithReleaseFile(fs, "/etc/centos-release"), + (major, minor)={{ + CLEANUP_FREE char *major = NULL, *minor = NULL; + if (get_version_from_centos_release (fs, &major, &minor) == -1) + return -1; + set_major_minor (major, minor); + return 0; + }}. + +LinuxRootWithReleaseFile(fs, "/etc/altlinux-release") :- + LinuxRoot(fs), + !LinuxRootWithOSRelease(fs), !LinuxRootWithLSBRelease(fs), + File(fs, "/etc/altlinux-release"). +Distro(fs, "altlinux") :- + LinuxRootWithReleaseFile(fs, "/etc/altlinux-release"). +Version(fs, major, minor) :- + LinuxRootWithReleaseFile(fs, "/etc/altlinux-release"), + (major, minor)={{ + CLEANUP_FREE char *major = NULL, *minor = NULL; + if (get_version_from_altlinux_release (fs, &major, &minor) == -1) + return -1; + set_major_minor (major, minor); + return 0; + }}. + +/* Handle the fallback case of /etc/redhat-release. */ +LinuxRootWithReleaseFile(fs, "/etc/redhat-release") :- + LinuxRoot(fs), + !LinuxRootWithOSRelease(fs), !LinuxRootWithLSBRelease(fs), + !LinuxRootWithReleaseFile(fs, "/etc/oracle-release"), + !LinuxRootWithReleaseFile(fs, "/etc/centos-release"), + !LinuxRootWithReleaseFile(fs, "/etc/altlinux-release"), + File(fs, "/etc/redhat-release"). +Distro(fs, "fedora") :- + LinuxRootWithReleaseFile(fs, "/etc/redhat-release"), + {{ return match_redhat_release_fedora (fs); }}. +Distro(fs, "rhel") :- + LinuxRootWithReleaseFile(fs, "/etc/redhat-release"), + {{ return match_redhat_release_rhel (fs); }}. +Distro(fs, "centos") :- + LinuxRootWithReleaseFile(fs, "/etc/redhat-release"), + {{ return match_redhat_release_centos (fs); }}. +Distro(fs, "scientificlinux") :- + LinuxRootWithReleaseFile(fs, "/etc/redhat-release"), + {{ return match_redhat_release_scientific_linux (fs); }}. +Distro(fs, "redhat-based") :- + LinuxRootWithReleaseFile(fs, "/etc/redhat-release"), + !Distro(fs, "fedora"), + !Distro(fs, "rhel"), + !Distro(fs, "centos"), + !Distro(fs, "scientificlinux"). + +Version(fs, major, minor) :- + LinuxRootWithReleaseFile(fs, "/etc/redhat-release"), + (major, minor)={{ + CLEANUP_FREE char *major = NULL, *minor = NULL; + if (get_version_from_redhat_release (fs, &major, &minor) == -1) + return -1; + set_major_minor (major, minor); + return 0; + }}. + +/* Get the product name from a generic Linux release file. */ +ProductName(fs, product_name) :- + LinuxRootWithReleaseFile(fs, release_file), + (product_name)={{ + CLEANUP_FREE char *product_name = first_line_of_file (fs, release_file); + set_product_name (product_name); + return 0; + }}. + +/* XXX debian, arch-linux etc release files */ + +/* XXX Linux architecture, fstab, hostname */ + + + +/* XXX CoreOS etc. */ + + + + + + + + +/* +Has_fstab(rootfs) :- + Unix_root(fs), + File(rootfs, "/etc/fstab"). +Mount(rootfs, dev, mountpoint) :- + Has_fstab(rootfs), + (dev, mountpoint)*={{ + // code to return (dev, mountpoint) pairs from /etc/fstab + }}. +*/ diff --git a/inspection/match.c b/inspection/match.c new file mode 100644 index 0000000..fe12b12 --- /dev/null +++ b/inspection/match.c @@ -0,0 +1,173 @@ +/* libguestfs + * Copyright (C) 2010-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <error.h> +#include <errno.h> + +#include <pcre.h> + +#include "inspection.h" + +static char * +safe_strndup (const char *str, size_t len) +{ + char *ret = strndup (str, len); + if (ret == NULL) + error (EXIT_FAILURE, errno, "strndup"); + return ret; +} + +#if 0 +/* Match a regular expression which contains no captures. Returns + * true if it matches or false if it doesn't. + */ +int +match (const char *str, const pcre *re) +{ + size_t len = strlen (str); + int vec[30], r; + + r = pcre_exec (re, NULL, str, len, 0, 0, vec, sizeof vec / sizeof vec[0]); + if (r == PCRE_ERROR_NOMATCH) + return 0; + + return 1; +} +#endif + +/* Match a regular expression which contains exactly one capture. If + * the string matches, return the capture, otherwise return NULL. The + * caller must free the result. + */ +char * +match1 (const char *str, const pcre *re) +{ + size_t len = strlen (str); + int vec[30], r; + + r = pcre_exec (re, NULL, str, len, 0, 0, vec, sizeof vec / sizeof vec[0]); + if (r == PCRE_ERROR_NOMATCH) + return NULL; + + return r == 2 ? safe_strndup (&str[vec[2]], vec[3]-vec[2]) : NULL; +} + +/* Match a regular expression which contains exactly two captures. */ +int +match2 (const char *str, const pcre *re, char **ret1, char **ret2) +{ + size_t len = strlen (str); + int vec[30], r; + + r = pcre_exec (re, NULL, str, len, 0, 0, vec, 30); + if (r == PCRE_ERROR_NOMATCH) + return 0; + + *ret1 = NULL; + *ret2 = NULL; + + if (r > 1) *ret1 = safe_strndup (&str[vec[2]], vec[3]-vec[2]); + if (r > 2) *ret2 = safe_strndup (&str[vec[4]], vec[5]-vec[4]); + + return 1; +} + +#if 0 +/* Match a regular expression which contains exactly three captures. */ +int +match3 (const char *str, const pcre *re, + char **ret1, char **ret2, char **ret3) +{ + size_t len = strlen (str); + int vec[30], r; + + r = pcre_exec (re, NULL, str, len, 0, 0, vec, 30); + if (r == PCRE_ERROR_NOMATCH) + return 0; + + *ret1 = NULL; + *ret2 = NULL; + *ret3 = NULL; + + if (r > 1) *ret1 = safe_strndup (&str[vec[2]], vec[3]-vec[2]); + if (r > 2) *ret2 = safe_strndup (&str[vec[4]], vec[5]-vec[4]); + if (r > 3) *ret3 = safe_strndup (&str[vec[6]], vec[7]-vec[6]); + + return 1; +} + +/* Match a regular expression which contains exactly four captures. */ +int +match4 (const char *str, const pcre *re, + char **ret1, char **ret2, char **ret3, char **ret4) +{ + size_t len = strlen (str); + int vec[30], r; + + r = pcre_exec (re, NULL, str, len, 0, 0, vec, 30); + if (r == PCRE_ERROR_NOMATCH) + return 0; + + *ret1 = NULL; + *ret2 = NULL; + *ret3 = NULL; + *ret4 = NULL; + + if (r > 1) *ret1 = safe_strndup (&str[vec[2]], vec[3]-vec[2]); + if (r > 2) *ret2 = safe_strndup (&str[vec[4]], vec[5]-vec[4]); + if (r > 3) *ret3 = safe_strndup (&str[vec[6]], vec[7]-vec[6]); + if (r > 4) *ret4 = safe_strndup (&str[vec[8]], vec[9]-vec[8]); + + return 1; +} + +/* Match a regular expression which contains exactly six captures. */ +int +match6 (const char *str, const pcre *re, + char **ret1, char **ret2, char **ret3, char **ret4, + char **ret5, char **ret6) +{ + size_t len = strlen (str); + int vec[30], r; + + r = pcre_exec (re, NULL, str, len, 0, 0, vec, 30); + if (r == PCRE_ERROR_NOMATCH) + return 0; + + *ret1 = NULL; + *ret2 = NULL; + *ret3 = NULL; + *ret4 = NULL; + *ret5 = NULL; + *ret6 = NULL; + + if (r > 1) *ret1 = safe_strndup (&str[vec[2]], vec[3]-vec[2]); + if (r > 2) *ret2 = safe_strndup (&str[vec[4]], vec[5]-vec[4]); + if (r > 3) *ret3 = safe_strndup (&str[vec[6]], vec[7]-vec[6]); + if (r > 4) *ret4 = safe_strndup (&str[vec[8]], vec[9]-vec[8]); + if (r > 5) *ret5 = safe_strndup (&str[vec[10]], vec[11]-vec[10]); + if (r > 6) *ret6 = safe_strndup (&str[vec[12]], vec[13]-vec[12]); + + return 1; +} +#endif diff --git a/inspection/mount.c b/inspection/mount.c new file mode 100644 index 0000000..e10dbdd --- /dev/null +++ b/inspection/mount.c @@ -0,0 +1,597 @@ +/* guestfs-inspection + * Copyright (C) 2009-2015 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> +#include <unistd.h> +#include <fcntl.h> +#include <dirent.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/mount.h> +#include <errno.h> +#include <error.h> + +#include "c-ctype.h" + +#include "guestfs-internal-all.h" + +#include "inspection.h" +#include "cleanups.h" +#include "command.h" +#include "stringsbuf.h" + +/* If root device is an ext2 filesystem, this is the major and minor. + * This is so we can ignore this device from the point of view of the + * user, eg. in guestfs_list_devices and many other places. + */ +static dev_t root_device = 0; + +/* A temporary directory where we place all the mountpoints. */ +static char mountpoints[] = "/tmp/mp.XXXXXX"; + +static void init_mount (void) __attribute__((constructor)); + +static void +init_mount (void) +{ + struct stat statbuf; + + if (stat ("/", &statbuf) == 0) + root_device = statbuf.st_dev; + + if (mkdtemp (mountpoints) == NULL) + perror ("mkdtemp"); +} + +static void free_mount (void) __attribute__((destructor)); + +static void +free_mount (void) +{ + DIR *dir; + struct dirent *d; + + /* Unmount all mountpoints in the temporary directory, then + * delete those directories and the parent. + */ + dir = opendir (mountpoints); + if (!dir) { + perror (mountpoints); + return; + } + + for (;;) { + errno = 0; + d = readdir (dir); + if (!d) break; + + if (d->d_name[0] != '.') { + CLEANUP_FREE char *mp; + + if (asprintf (&mp, "%s/%s", mountpoints, d->d_name) == -1) { + perror ("asprintf"); + continue; + } + + if (umount2 (mp, MNT_DETACH) == -1) /* lazy umount */ + perror (mp); + + if (rmdir (mp) == -1) + perror (mp); + } + } + + /* Check readdir didn't fail */ + if (errno != 0) { + perror ("readdir"); + return; + } + + /* Close the directory handle */ + if (closedir (dir) == -1) { + perror ("closedir"); + return; + } + + if (rmdir (mountpoints) == -1) + perror (mountpoints); +} + +/* Return true iff device is the root device (and therefore should be + * ignored from the point of view of user calls). + */ +static int +is_root_device_stat (struct stat *statbuf) +{ + if (statbuf->st_rdev == root_device) return 1; + return 0; +} + +static int +is_root_device (const char *device) +{ + struct stat statbuf; + + if (stat (device, &statbuf) == -1) { + perror (device); + return 0; + } + + return is_root_device_stat (&statbuf); +} + +typedef int (*block_dev_func_t) (const char *dev, struct stringsbuf *r); + +/* Execute a given function for each discovered block device */ +static char ** +foreach_block_device (block_dev_func_t func) +{ + DECLARE_STRINGSBUF (r); + DIR *dir; + struct dirent *d; + char dev_path[256]; + int fd; + bool err = false; + + dir = opendir ("/sys/block"); + if (!dir) { + perror ("opendir: /sys/block"); + return NULL; + } + + for (;;) { + errno = 0; + d = readdir (dir); + if (!d) break; + + if (STREQLEN (d->d_name, "sd", 2) || + STREQLEN (d->d_name, "hd", 2) || + STREQLEN (d->d_name, "ubd", 3) || + STREQLEN (d->d_name, "vd", 2) || + STREQLEN (d->d_name, "sr", 2)) { + snprintf (dev_path, sizeof dev_path, "/dev/%s", d->d_name); + + /* Ignore the root device. */ + if (is_root_device (dev_path)) + continue; + + /* RHBZ#514505: Some versions of qemu <= 0.10 add a + * CD-ROM device even though we didn't request it. Try to + * detect this by seeing if the device contains media. + */ + fd = open (dev_path, O_RDONLY|O_CLOEXEC); + if (fd == -1) { + perror (dev_path); + continue; + } + close (fd); + + /* Call the map function for this device */ + if ((*func)(d->d_name, &r) != 0) { + err = true; + break; + } + } + } + + /* Check readdir didn't fail */ + if (errno != 0) { + perror ("readdir: /sys/block"); + free_stringslen (r.argv, r.size); + closedir (dir); + return NULL; + } + + /* Close the directory handle */ + if (closedir (dir) == -1) { + perror ("closedir: /sys/block"); + free_stringslen (r.argv, r.size); + return NULL; + } + + /* Free the result list on error */ + if (err) { + free_stringslen (r.argv, r.size); + return NULL; + } + + /* Sort the devices. */ + if (r.size > 0) + sort_device_names (r.argv, r.size); + + /* NULL terminate the list */ + end_stringsbuf (&r); + + return r.argv; +} + +/* Add a device to the list of devices */ +static int +add_device (const char *device, struct stringsbuf *r) +{ + char dev_path[256]; + snprintf (dev_path, sizeof dev_path, "/dev/%s", device); + + add_string (r, dev_path); + + return 0; +} + +char ** +get_all_block_devices (void) +{ + return foreach_block_device (add_device); +} + +static int +add_partitions (const char *device, struct stringsbuf *r) +{ + char devdir[256]; + + /* Open the device's directory under /sys/block */ + snprintf (devdir, sizeof devdir, "/sys/block/%s", device); + + DIR *dir = opendir (devdir); + if (!dir) { + perror (devdir); + free_stringslen (r->argv, r->size); + return -1; + } + + /* Look in /sys/block/<device>/ for entries starting with <device> + * e.g. /sys/block/sda/sda1 + */ + errno = 0; + struct dirent *d; + while ((d = readdir (dir)) != NULL) { + if (STREQLEN (d->d_name, device, strlen (device))) { + char part[256]; + snprintf (part, sizeof part, "/dev/%s", d->d_name); + + add_string (r, part); + } + } + + /* Check if readdir failed */ + if (0 != errno) { + perror (devdir); + free_stringslen (r->argv, r->size); + closedir (dir); + return -1; + } + + /* Close the directory handle */ + if (closedir (dir) == -1) { + perror (device); + free_stringslen (r->argv, r->size); + return -1; + } + + return 0; +} + +char ** +get_all_partitions (void) +{ + return foreach_block_device (add_partitions); +} + +char ** +get_all_mddevs (void) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return NULL; +} + +char ** +get_all_lvs (void) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return NULL; +} + +char ** +get_all_ldmvols (void) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return NULL; +} + +char ** +get_all_ldmparts (void) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return NULL; +} + +char ** +get_all_btrfs_subvolumes (const char *fs) +{ + error (EXIT_FAILURE, 0, "%s: not implemented", __func__); + return NULL; +} + +static char * +get_blkid_tag (const char *device, const char *tag) +{ + char *out; + CLEANUP_FREE char *err = NULL; + int r; + size_t len; + + r = commandr (&out, &err, + "blkid", + /* Adding -c option kills all caching, even on RHEL 5. */ + "-c", "/dev/null", + "-o", "value", "-s", tag, device, NULL); + if (r != 0 && r != 2) { + if (r >= 0) + fprintf (stderr, "%s: %s (blkid returned %d)\n", device, err, r); + else + fprintf (stderr, "%s: %s\n", device, err); + free (out); + return NULL; + } + + if (r == 2) { /* means UUID etc not found */ + free (out); + out = strdup (""); + if (out == NULL) + perror ("strdup"); + return out; + } + + /* Trim trailing \n if present. */ + len = strlen (out); + if (len > 0 && out[len-1] == '\n') + out[len-1] = '\0'; + + return out; /* caller frees */ +} + +char * +get_vfs_type (const char *fs) +{ + return get_blkid_tag (fs, "TYPE"); +} + +/* Test if sfdisk is recent enough to have --part-type, to be used instead + * of --print-id and --change-id. + */ +static int +test_sfdisk_has_part_type (void) +{ + static int tested = -1; + int r; + CLEANUP_FREE char *out = NULL, *err = NULL; + + if (tested != -1) + return tested; + + r = command (&out, &err, "sfdisk", "--help", NULL); + if (r == -1) { + fprintf (stderr, "%s: %s\n", "sfdisk --help", err); + return -1; + } + + tested = strstr (out, "--part-type") != NULL; + return tested; +} + +static char * +part_to_dev (const char *part) +{ + int err = 1; + size_t n = strlen (part); + char *r; + + while (n >= 1 && c_isdigit (part[n-1])) { + err = 0; + n--; + } + + if (err) { + fprintf (stderr, "device name is not a partition\n"); + return NULL; + } + + r = strndup (part, n); + if (r == NULL) { + perror ("strdup"); + return NULL; + } + + return r; +} + +static int +part_to_partnum (const char *part) +{ + int err = 1; + size_t n = strlen (part); + int r; + + while (n >= 1 && c_isdigit (part[n-1])) { + err = 0; + n--; + } + + if (err) { + fprintf (stderr, "device name is not a partition\n"); + return -1; + } + + if (sscanf (&part[n], "%d", &r) != 1) { + fprintf (stderr, "could not parse number\n"); + return -1; + } + + return r; +} + +int +get_partition_mbr_id (const char *fs) +{ + CLEANUP_FREE char *device; + int partnum; + char partnum_str[16]; + const char *param + test_sfdisk_has_part_type () ? "--part-type" : "--print-id"; + CLEANUP_FREE char *out = NULL, *err = NULL; + int r; + unsigned id; + + /* Get the block device and partition number from the filesystem + * string. + */ + device = part_to_dev (fs); + if (device == NULL) + return -1; + partnum = part_to_partnum (fs); + if (partnum == -1) + return -1; + snprintf (partnum_str, sizeof partnum_str, "%d", partnum); + + r = command (&out, &err, "sfdisk", param, device, partnum_str, NULL); + if (r == -1) { + fprintf (stderr, "sfdisk %s: %s\n", param, err); + return -1; + } + + /* It's printed in hex ... */ + if (sscanf (out, "%x", &id) != 1) { + fprintf (stderr, "sfdisk --print-id: cannot parse output: %s\n", out); + return -1; + } + + return id; +} + +/* When mounting filesystems, we place them in temporary directories + * under 'mountpoints'. We name the temporary directory after the + * device name, but since device names contain '/' characters, we have + * to mangle the name. + */ +static char * +get_mount_name (const char *fs) +{ + char *ret; + size_t i; + + if (asprintf (&ret, "%s/%s", mountpoints, fs) == -1) { + perror ("asprintf"); + return NULL; + } + + for (i = strlen (mountpoints) + 1; i < strlen (ret); ++i) { + if (ret[i] == '/') + ret[i] = '_'; + } + + return ret; /* caller frees */ +} + +int +is_mountable (const char *fs) +{ + CLEANUP_FREE char *mp = NULL; + struct stat statbuf; + int r; + CLEANUP_FREE char *err = NULL; + + mp = get_mount_name (fs); + if (mp == NULL) + return -1; + + if (stat (mp, &statbuf) == 0 && S_ISDIR (statbuf.st_mode)) + return 1; /* mountable, and mounted already */ + + /* Try to create the mountpoint. */ + if (mkdir (mp, 0700) == -1) { + perror (mp); + return -1; + } + + /* Try to mount the filesystem. */ + r = command (NULL, &err, + "mount", "-o", "ro", fs, mp, NULL); + if (r == -1) { + fprintf (stderr, "mount: %s: %s\n", fs, err); + + /* Now hack things for the *BSDs. */ + /* FreeBSD fs is a variant of ufs called ufs2 ... */ + free (err); err = NULL; + r = command (NULL, &err, + "mount", "-o", "ro,ufstype=ufs2", fs, mp, NULL); + if (r == -1) { + fprintf (stderr, "mount [ufs2]: %s: %s\n", fs, err); + + /* while NetBSD and OpenBSD use another variant labeled 44bsd */ + free (err); err = NULL; + r = command (NULL, &err, + "mount", "-o", "ro,ufstype=44bsd", fs, mp, NULL); + if (r == -1) { + fprintf (stderr, "mount [44bsd]: %s: %s\n", fs, err); + + /* Mount failed, so remove the mountpoint. */ + rmdir (mp); + return 0; + } + } + } + + /* Mount succeeded. */ + return 1; +} + +int +get_mount (const char *fs, const char *filename, char **relative_filename) +{ + CLEANUP_FREE char *mp = NULL; + int r; + + if (filename[0] != '/') { + fprintf (stderr, "get_mount: filename is not an absolute path: %s\n", + filename); + return -1; + } + + r = is_mountable (fs); + if (r == -1) + return -1; + if (r == 0) { + fprintf (stderr, "get_mount: called on non-mountable filesystem: %s\n", + fs); + return -1; + } + + mp = get_mount_name (fs); + + /* Construct the filename relative to the mountpoint. */ + if (asprintf (relative_filename, "%s%s", mp, filename) == -1) { + perror ("asprintf"); + return -1; + } + + return 0; +} diff --git a/inspection/rules.h b/inspection/rules.h new file mode 100644 index 0000000..cf5b646 --- /dev/null +++ b/inspection/rules.h @@ -0,0 +1,69 @@ +/* guestfs-inspection + * Copyright (C) 2009-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef GUESTFS_RULES_H +#define GUESTFS_RULES_H + +#include <stdbool.h> + +#include "gl_oset.h" + +extern int verbose; + +/* facts.c */ +struct fact { + char *term_name; + size_t nr_term_args; + char *term_arg[]; +}; +typedef struct fact fact; + +/* Create a fact on the heap. This doesn't copy the strings, but they + * are deep copied when we call add_fact. + */ +extern fact *create_fact (const char *term_name, size_t n, ...); + +/* Create a fact on the stack and set 'var' to be a const pointer to + * it. This doesn't copy the strings, but they are deep copied when + * we call add_fact. Because of stupidity in C99, this is way more + * complex than it needs to be. + */ +#define CREATE_FACT(var,name,n,...) \ + struct { \ + const char *term_name; \ + size_t nr_term_args; \ + const char *term_arg[n]; \ + } var##_tmp_fact = { (name), (n), { __VA_ARGS__ } }; \ + const fact *var = (struct fact *) &var##_tmp_fact + +extern void clear_true_facts (void); +extern void clear_false_facts (void); +extern size_t count_true_facts (void); +extern void print_fact (bool is_true, const fact *f, FILE *fp); +extern void print_true_facts (void); +extern void print_false_facts (void); +extern void add_all_fact_strings (gl_oset_t set); +extern void add_strings_from_facts (gl_oset_t set, const char *term_name, size_t arg_i); +extern bool is_fact (bool is_true, const fact *); +extern bool add_fact (bool is_true, const fact *); + +/* rules.c - generated code */ +extern const char *all_strings[]; +extern void rules (void); + +#endif /* GUESTFS_RULES_H */ diff --git a/inspection/stringsbuf.c b/inspection/stringsbuf.c new file mode 100644 index 0000000..338134e --- /dev/null +++ b/inspection/stringsbuf.c @@ -0,0 +1,254 @@ +/* libguestfs - the guestfsd daemon + * Copyright (C) 2009-2015 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <error.h> +#include <assert.h> + +#include "guestfs-internal-all.h" + +#include "stringsbuf.h" + +void +add_string_nodup (struct stringsbuf *sb, char *str) +{ + char **new_argv; + + if (sb->size >= sb->alloc) { + sb->alloc += 64; + new_argv = realloc (sb->argv, sb->alloc * sizeof (char *)); + if (new_argv == NULL) + error (EXIT_FAILURE, errno, "realloc"); + sb->argv = new_argv; + } + + sb->argv[sb->size] = str; + sb->size++; +} + +void +add_string (struct stringsbuf *sb, const char *str) +{ + char *new_str = NULL; + + if (str) { + new_str = strdup (str); + if (new_str == NULL) + error (EXIT_FAILURE, errno, "strdup"); + } + + add_string_nodup (sb, new_str); +} + +void +add_sprintf (struct stringsbuf *sb, const char *fs, ...) +{ + va_list args; + char *str; + int r; + + va_start (args, fs); + r = vasprintf (&str, fs, args); + va_end (args); + if (r == -1) + error (EXIT_FAILURE, errno, "vasprintf"); + + add_string_nodup (sb, str); +} + +void +end_stringsbuf (struct stringsbuf *sb) +{ + add_string_nodup (sb, NULL); +} + +void +free_stringsbuf (struct stringsbuf *sb) +{ + if (sb->argv != NULL) + free_stringslen (sb->argv, sb->size); +} + +size_t +count_strings (char *const *argv) +{ + size_t argc; + + for (argc = 0; argv[argc] != NULL; ++argc) + ; + return argc; +} + +static int +compare (const void *vp1, const void *vp2) +{ + char * const *p1 = (char * const *) vp1; + char * const *p2 = (char * const *) vp2; + return strcmp (*p1, *p2); +} + +void +sort_strings (char **argv, size_t len) +{ + qsort (argv, len, sizeof (char *), compare); +} + +void +free_strings (char **argv) +{ + size_t argc; + + if (!argv) + return; + + for (argc = 0; argv[argc] != NULL; ++argc) + free (argv[argc]); + free (argv); +} + +void +free_stringslen (char **argv, size_t len) +{ + size_t i; + + if (!argv) + return; + + for (i = 0; i < len; ++i) + free (argv[i]); + free (argv); +} + +/* Compare device names (including partition numbers if present). + * https://rwmj.wordpress.com/2011/01/09/how-are-linux-drives-named-beyond-drive-26-devsdz/ + */ +int +compare_device_names (const char *a, const char *b) +{ + size_t alen, blen; + int r; + int a_partnum, b_partnum; + + /* Skip /dev/ prefix if present. */ + if (STRPREFIX (a, "/dev/")) + a += 5; + if (STRPREFIX (b, "/dev/")) + b += 5; + + /* Skip sd/hd/ubd/vd. */ + alen = strcspn (a, "d"); + blen = strcspn (b, "d"); + assert (alen > 0 && alen <= 2); + assert (blen > 0 && blen <= 2); + a += alen + 1; + b += blen + 1; + + /* Get device name part, that is, just 'a', 'ab' etc. */ + alen = strcspn (a, "0123456789"); + blen = strcspn (b, "0123456789"); + + /* If device name part is longer, it is always greater, eg. + * "/dev/sdz" < "/dev/sdaa". + */ + if (alen != blen) + return alen - blen; + + /* Device name parts are the same length, so do a regular compare. */ + r = strncmp (a, b, alen); + if (r != 0) + return r; + + /* Compare partitions numbers. */ + a += alen; + b += alen; + + /* If no partition numbers, bail -- the devices are the same. This + * can happen in one peculiar case: where you have a mix of devices + * with different interfaces (eg. /dev/sda and /dev/vda). + * (RHBZ#858128). + */ + if (!*a && !*b) + return 0; + + r = sscanf (a, "%d", &a_partnum); + assert (r == 1); + r = sscanf (b, "%d", &b_partnum); + assert (r == 1); + + return a_partnum - b_partnum; +} + +static int +compare_device_names_vp (const void *vp1, const void *vp2) +{ + char * const *p1 = (char * const *) vp1; + char * const *p2 = (char * const *) vp2; + return compare_device_names (*p1, *p2); +} + +void +sort_device_names (char **argv, size_t len) +{ + qsort (argv, len, sizeof (char *), compare_device_names_vp); +} + +char * +concat_strings (char *const *argv) +{ + return join_strings ("", argv); +} + +char * +join_strings (const char *separator, char *const *argv) +{ + size_t i, len, seplen, rlen; + char *r; + + seplen = strlen (separator); + + len = 0; + for (i = 0; argv[i] != NULL; ++i) { + if (i > 0) + len += seplen; + len += strlen (argv[i]); + } + len++; /* for final \0 */ + + r = malloc (len); + if (r == NULL) + error (EXIT_FAILURE, errno, "malloc"); + + rlen = 0; + for (i = 0; argv[i] != NULL; ++i) { + if (i > 0) { + memcpy (&r[rlen], separator, seplen); + rlen += seplen; + } + len = strlen (argv[i]); + memcpy (&r[rlen], argv[i], len); + rlen += len; + } + r[rlen] = '\0'; + + return r; +} diff --git a/inspection/stringsbuf.h b/inspection/stringsbuf.h new file mode 100644 index 0000000..4a26ffb --- /dev/null +++ b/inspection/stringsbuf.h @@ -0,0 +1,56 @@ +/* libguestfs - the guestfsd daemon + * Copyright (C) 2009-2015 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef GUESTFS_INSPECTION_STRINGSBUF_H +#define GUESTFS_INSPECTION_STRINGSBUF_H + +/* Growable strings buffer. */ +struct stringsbuf { + char **argv; + size_t size; + size_t alloc; +}; +#define DECLARE_STRINGSBUF(v) \ + struct stringsbuf (v) = { .argv = NULL, .size = 0, .alloc = 0 } + +/* Append a string to the strings buffer. + * + * add_string_nodup: don't copy the string. + * add_string: copy the string. + * end_stringsbuf: NULL-terminate the buffer. + */ +extern void add_string_nodup (struct stringsbuf *sb, char *str); +extern void add_string (struct stringsbuf *sb, const char *str); +extern void add_sprintf (struct stringsbuf *sb, const char *fs, ...) + __attribute__((format (printf,2,3))); +extern void end_stringsbuf (struct stringsbuf *sb); +extern void free_stringsbuf (struct stringsbuf *sb); + +extern size_t count_strings (char *const *argv); +extern void sort_strings (char **argv, size_t len); +extern void free_strings (char **argv); +extern void free_stringslen (char **argv, size_t len); + +extern void sort_device_names (char **argv, size_t len); +extern int compare_device_names (const char *a, const char *b); + +/* Concatenate strings, optionally with a separator string between each. */ +extern char *concat_strings (char *const *argv); +extern char *join_strings (const char *separator, char *const *argv); + +#endif /* GUESTFS_INSPECTION_STRINGSBUF_H */ diff --git a/inspection/utils.c b/inspection/utils.c new file mode 100644 index 0000000..9e44ab3 --- /dev/null +++ b/inspection/utils.c @@ -0,0 +1,66 @@ +/* libguestfs + * Copyright (C) 2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <error.h> +#include <errno.h> + +#include <pcre.h> + +#include "cleanups.h" +#include "inspection.h" + +/* Get the first line of the file, without any trailing newline + * character. The caller must free the returned string. + * + * If the file is completely empty or begins with '\n' this returns an + * empty string. + * + * This function never returns NULL. If the file does not exist or + * there is some other error, it exits with an error. + */ +char * +first_line_of_file (const char *fs, const char *filename) +{ + CLEANUP_FREE char *relative_filename = NULL; + FILE *fp; + ssize_t r; + size_t n = 0; + char *line = NULL; + + if (get_mount (fs, filename, &relative_filename) == -1) + error (EXIT_FAILURE, 0, "%s: failed to get mountpoint: %s %s", + __func__, fs, filename); + + fp = fopen (relative_filename, "r"); + if (fp == NULL) + error (EXIT_FAILURE, errno, "%s: open: %s", __func__, relative_filename); + r = getline (&line, &n, fp); + if (r == -1) + error (EXIT_FAILURE, errno, "%s: getline: %s", __func__, relative_filename); + fclose (fp); + + if (r > 0 && line[r-1] == '\n') + line[r-1] = '\0'; + + return line; /* caller frees */ +} diff --git a/po/POTFILES b/po/POTFILES index 2a1e313..c89ca14 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -259,6 +259,13 @@ gobject/src/struct-version.c gobject/src/struct-xattr.c gobject/src/struct-xfsinfo.c gobject/src/tristate.c +inspection/detect.c +inspection/facts.c +inspection/inspection.c +inspection/match.c +inspection/mount.c +inspection/stringsbuf.c +inspection/utils.c inspector/inspector.c java/com_redhat_et_libguestfs_GuestFS.c lua/lua-guestfs.c diff --git a/src/guestfs.pod b/src/guestfs.pod index 2a199c0..4fb58e6 100644 --- a/src/guestfs.pod +++ b/src/guestfs.pod @@ -3545,6 +3545,7 @@ Other libguestfs topics: L<guestfs-building(1)>, L<guestfs-faq(1)>, L<guestfs-hacking(1)>, +L<guestfs-inspection(8)>, L<guestfs-internals(1)>, L<guestfs-performance(1)>, L<guestfs-release-notes(1)>, -- 2.5.0
Richard W.M. Jones
2016-Feb-23 12:50 UTC
[Libguestfs] [PATCH v3 3/4] inspection: Add unit tests of the rules compiler.
These are unit tests of basic features of the rules compiler, such as evaluation of expressions and embedding C code. --- .gitignore | 4 ++ generator/main.ml | 6 ++- inspection/Makefile.am | 43 ++++++++++++++++- inspection/test-harness.c | 53 +++++++++++++++++++++ inspection/test1.rules | 80 ++++++++++++++++++++++++++++++++ inspection/test2.rules | 116 ++++++++++++++++++++++++++++++++++++++++++++++ po/POTFILES | 3 ++ 7 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 inspection/test-harness.c create mode 100644 inspection/test1.rules create mode 100644 inspection/test2.rules diff --git a/.gitignore b/.gitignore index 8da932c..8c7d784 100644 --- a/.gitignore +++ b/.gitignore @@ -256,6 +256,10 @@ Makefile.in /inspection/guestfs-inspection.8 /inspection/rules.c /inspection/stamp-guestfs-inspection.pod +/inspection/test1 +/inspection/test1.c +/inspection/test2 +/inspection/test2.c /inspector/actual-*.xml /inspector/stamp-virt-inspector.pod /inspector/test-xmllint.sh diff --git a/generator/main.ml b/generator/main.ml index 4137f85..3ad12a2 100644 --- a/generator/main.ml +++ b/generator/main.ml @@ -213,7 +213,11 @@ Run it from the top source directory using the command output_to "customize/customize-synopsis.pod" generate_customize_synopsis_pod; output_to "customize/customize-options.pod" generate_customize_options_pod; - (* Run the rules compiler to generate inspection rules. *) + (* Run the rules compiler to generate test cases and inspection rules. *) + output_to "inspection/test1.c" + (Rules_compiler.compile "inspection/test1.rules"); + output_to "inspection/test2.c" + (Rules_compiler.compile "inspection/test2.rules"); output_to "inspection/rules.c" (Rules_compiler.compile "inspection/inspection.rules"); diff --git a/inspection/Makefile.am b/inspection/Makefile.am index 5232bc3..30dad82 100644 --- a/inspection/Makefile.am +++ b/inspection/Makefile.am @@ -18,7 +18,9 @@ include $(top_srcdir)/subdir-rules.mk generator_built = \ - rules.c + rules.c \ + test1.c \ + test2.c BUILT_SOURCES = \ $(generator_built) @@ -90,3 +92,42 @@ stamp-guestfs-inspection.pod: guestfs-inspection.pod --license GPLv2+ \ $< touch $@ + +# Tests. + +LOG_COMPILER = $(VG) +TESTS_ENVIRONMENT = $(top_builddir)/run --test + +TESTS = test1 test2 +check_PROGRAMS = $(TESTS) + +test1_SOURCES = \ + ../daemon/cleanups.c \ + ../daemon/cleanups.h \ + facts.c \ + rules.h \ + stringsbuf.c \ + stringsbuf.h \ + test-harness.c \ + test1.c + +test1_LDADD = $(guestfs_inspection_LDADD) +test1_CPPFLAGS = $(guestfs_inspection_CPPFLAGS) +test1_CFLAGS = $(guestfs_inspection_CFLAGS) + +test2_SOURCES = \ + ../daemon/cleanups.c \ + ../daemon/cleanups.h \ + facts.c \ + rules.h \ + stringsbuf.c \ + stringsbuf.h \ + test-harness.c \ + test2.c + +test2_LDADD = $(guestfs_inspection_LDADD) +test2_CPPFLAGS = $(guestfs_inspection_CPPFLAGS) +test2_CFLAGS = $(guestfs_inspection_CFLAGS) + +check-valgrind: + $(MAKE) VG="@VG@" check diff --git a/inspection/test-harness.c b/inspection/test-harness.c new file mode 100644 index 0000000..a4622b6 --- /dev/null +++ b/inspection/test-harness.c @@ -0,0 +1,53 @@ +/* guestfs-inspection tests + * Copyright (C) 2015-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <errno.h> + +#include "cleanups.h" +#include "rules.h" + +int verbose = 1; + +int +main (int argc, char *argv[]) +{ + /* Run the rules. */ + rules (); + + /* For debugging. */ + print_true_facts (); + + /* The tests should contain a TestOK rule which verifies that all + * true and false facts are what we expect. So we just need to + * check that TestOK is true and we're done. + */ + CLEANUP_FREE fact *f = create_fact ("TestOK", NULL); + if (!is_fact (true, f)) { + fprintf (stderr, "%s: test failed, see debugging information above\n", + argv[0]); + exit (EXIT_FAILURE); + } + + exit (EXIT_SUCCESS); +} diff --git a/inspection/test1.rules b/inspection/test1.rules new file mode 100644 index 0000000..9157fc3 --- /dev/null +++ b/inspection/test1.rules @@ -0,0 +1,80 @@ +/* Libguestfs inspection rules unit test -*- prolog -*- + * Copyright (C) 2015-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +Filesystem(fs) :- + (fs)*={{ + const char *fses[] = { "/dev/sda1", "/dev/sda2", "/dev/sda3", NULL }; + size_t i; + for (i = 0; fses[i] != NULL; ++i) + set_fs (fses[i]); + return 0; + }}. + +File("/dev/sda1", "/etc/fstab"). +File("/dev/sda1", "/etc/debian_version"). +File("/dev/sda2", "/etc/fstab"). +File("/dev/sda2", "/etc/redhat-release"). +File("/dev/sda2", "/etc/fedora-release"). +File("/dev/sda3", "/etc/fstab"). +File("/dev/sda3", "/etc/fedora-release") :- false. +File("/dev/sda3", "/etc/redhat-release"). + +Symlink("/dev/sda1", "/bin"). + +Directory("/dev/sda1", "/lib"). +Directory("/dev/sda1", "/etc"). +Directory("/dev/sda2", "/bin"). +Directory("/dev/sda2", "/etc"). +Directory("/dev/sda2", "/lib"). +Directory("/dev/sda3", "/bin"). +Directory("/dev/sda3", "/etc"). +Directory("/dev/sda3", "/lib"). + +UnixRoot(fs) :- + Filesystem(fs), + (Directory(fs, "/bin"); Symlink(fs, "/bin")), + (File(fs, "/etc/fstab"); Symlink(fs, "/etc/fstab")), + (Directory(fs, "/lib"); Symlink(fs, "/lib")). + +Distro(rootfs, "RHEL") :- + UnixRoot(rootfs), + File(rootfs, "/etc/redhat-release"), + ! File(rootfs, "/etc/fedora-release"). + +Distro(rootfs, "Fedora") :- + UnixRoot(rootfs), + File(rootfs, "/etc/fedora-release"). + +Distro(rootfs, "Debian") :- + UnixRoot(rootfs), + File(rootfs, "/etc/debian_version"). + +ProductName(rootfs, "Fedora", product_name) :- + Filesystem(rootfs), + Distro(rootfs, "Fedora"), + (product_name)={{ + set_product_name ("Fedora release 23 (Twenty Three)"); + return 0; + }}. + +/* This rule is checked by the test harness. */ +TestOK :- + Distro("/dev/sda1", "Debian"), + Distro("/dev/sda2", "Fedora"), + Distro("/dev/sda3", "RHEL"), + ProductName("/dev/sda2", "Fedora", "Fedora release 23 (Twenty Three)"). diff --git a/inspection/test2.rules b/inspection/test2.rules new file mode 100644 index 0000000..8cf994a --- /dev/null +++ b/inspection/test2.rules @@ -0,0 +1,116 @@ +/* Libguestfs inspection rules unit test -*- prolog -*- + * Copyright (C) 2015-2016 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* Test false, true expressions. */ +TestTrue1. +TestTrue2 :- true. +TestFalse :- false. + +/* Basic boolean expressions. */ +TestAnd1 :- true, true. +TestAnd2 :- true, false. +TestAnd3 :- false, false. +TestAnd4 :- false, true. +TestOr1 :- true; true. +TestOr2 :- true; false. +TestOr3 :- false; false. +TestOr4 :- false; true. +TestNot1 :- !TestTrue1. +TestNot2 :- !TestFalse. + +/* Precedence of AND and OR operators. */ +TestPrec :- true; true, false. /* should be parsed as true; (true, false) */ + +/* Parentheses. */ +TestParen1 :- (true, true); false. +TestParen2 :- false; (true, true). +TestParen3 :- (true; false), true. +TestParen4 :- true, (true; false). + +/* Cartesian product. */ +TestCartesian(x,y) :- true. + +/* Boolean code. */ +TestCTrue :- {{ return 1; }}. +TestCFalse :- {{ return 0; }}. + +/* Assignment code. */ +TestCAssign1(x) :- (x)={{ set_x ("123"); return 0; }}. +TestCAssign2(x,y) :- (x,y)={{ set_x_y ("123", "456"); return 0; }}. +TestCAssign3(x,y,z) :- (x,y,z)={{ set_x_y_z ("123", "456", "789"); return 0; }}. + +/* Assignment code for lists. */ +TestCListAssign1(x) :- + (x)*={{ /* no assignments */ return 0; }}. +TestCListAssign2(x) :- + (x)*={{ set_x ("123"); return 0; }}. +TestCListAssign3(x) :- + (x)*={{ set_x ("123"); set_x ("456"); return 0; }}. +TestCListAssign4(x,y) :- + (x,y)*={{ /* no assignments */ return 0; }}. +TestCListAssign5(x,y) :- + (x,y)*={{ set_x_y ("123", "456"); return 0; }}. +TestCListAssign6(x,y) :- + (x,y)*={{ set_x_y ("123", "456"); set_x_y ("456", "789"); return 0; }}. + +/* These assignments only differ in a small amount of checking code. */ +TestCListAssign7(x,y) :- + (x,y)?={{ /* no assignments */ return 0; }}. +TestCListAssign8(x,y) :- + (x,y)?={{ set_x_y ("123", "456"); return 0; }}. +TestCListAssign9(x,y) :- + (x,y)+={{ set_x_y ("123", "456"); return 0; }}. +TestCListAssign10(x,y) :- + (x,y)+={{ set_x_y ("123", "456"); set_x_y ("456", "789"); return 0; }}. + +/* Check no clashes in generated code with commonly used variables. */ +TestCAssignClash(i,j,fact,verbose,args,env) :- + (i,j,fact,verbose,args,env)={{ + set_i_j_fact_verbose_args_env ("1", "2", "3", "4", "5", "6"); + return 0; + }}. + +/* This rule is checked by the test harness. */ +TestOK :- + TestTrue1, TestTrue2, !TestFalse, + TestAnd1, !TestAnd2, !TestAnd3, !TestAnd4, + TestOr1, TestOr2, !TestOr3, TestOr4, + !TestNot1, TestNot2, + TestPrec, + TestParen1, TestParen2, TestParen3, TestParen4, + TestCartesian("123", "123"), + TestCartesian("123", "456"), + TestCartesian("123", "789"), + TestCartesian("456", "123"), /* etc etc */ + TestCTrue, !TestCFalse, + TestCAssign1("123"), TestCAssign2("123", "456"), + TestCAssign3("123", "456", "789"), + /* no assignments to TestCListAssign1 */ + TestCListAssign2("123"), + TestCListAssign3("123"), + TestCListAssign3("456"), + /* no assignments to TestCListAssign4 */ + TestCListAssign5("123", "456"), + TestCListAssign6("123", "456"), + TestCListAssign6("456", "789"), + /* no assignments to TestCListAssign7 */ + TestCListAssign8("123", "456"), + TestCListAssign9("123", "456"), + TestCListAssign10("123", "456"), + TestCListAssign10("456", "789"), + TestCAssignClash("1", "2", "3", "4", "5", "6"). diff --git a/po/POTFILES b/po/POTFILES index c89ca14..c3cf4bf 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -265,6 +265,9 @@ inspection/inspection.c inspection/match.c inspection/mount.c inspection/stringsbuf.c +inspection/test-harness.c +inspection/test1.c +inspection/test2.c inspection/utils.c inspector/inspector.c java/com_redhat_et_libguestfs_GuestFS.c -- 2.5.0
Richard W.M. Jones
2016-Feb-23 12:50 UTC
[Libguestfs] [PATCH v3 4/4] DISABLE NON-PARTITION GUESTS TEMPORARILY
--- inspection/inspection.rules | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/inspection/inspection.rules b/inspection/inspection.rules index 0bdad19..cc03209 100644 --- a/inspection/inspection.rules +++ b/inspection/inspection.rules @@ -51,40 +51,48 @@ Partition(dev) :- }}. /* LVM2 logical volumes. */ +/* LV(dev) :- (dev)*={{ CLEANUP_FREE_STRING_LIST char **devs = get_all_lvs (); if (devs == NULL) return -1; for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]); return 0; - }}. + }}. */ +LV(dev) :- false. /* /dev/md* devices. */ +/* MDDev(dev) :- (dev)*={{ CLEANUP_FREE_STRING_LIST char **devs = get_all_mddevs (); if (devs == NULL) return -1; for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]); return 0; - }}. + }}. */ +MDDev(dev) :- false. /* Windows LDM voumes. */ +/* LDMVol(dev) :- (dev)*={{ CLEANUP_FREE_STRING_LIST char **devs = get_all_ldmvols (); if (devs == NULL) return -1; for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]); return 0; - }}. + }}. */ +LDMVol(dev) :- false. /* Windows LDM partitions. */ +/* LDMPart(dev) :- (dev)*={{ CLEANUP_FREE_STRING_LIST char **devs = get_all_ldmparts (); if (devs == NULL) return -1; for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]); return 0; - }}. + }}. */ +LDMPart(dev) :- false. /* Device(dev) is just a group name for block devices, partitions etc. */ Device(dev) :- @@ -106,6 +114,7 @@ Mountable(fs) :- {{ return is_mountable (fs); }}. /* Where a filesystem is btrfs and mountable, get the subvolumes. */ +/* BtrfsSubvolume(subvol) :- Device(fs), Mountable(fs), @@ -124,7 +133,8 @@ BtrfsSubvolume(subvol) :- } return 0; }}. -VFSType(subvol, "btrfs") :- BtrfsSubvolume(subvol). +VFSType(subvol, "btrfs") :- BtrfsSubvolume(subvol). */ +BtrfsSubvolume(subvol) :- false. /* Ignore all *_member types. In libblkid these are returned * for things which are members of some RAID or LVM set, most -- 2.5.0