Richard W.M. Jones
2015-Dec-02 22:05 UTC
[Libguestfs] [PATCH 0/3] [FOR COMMENTS ONLY] Rework inspection.
This is something I've been working on: Reworking inspection so it's not a big mess of ad hoc C code, but instead uses a well-defined domain-specific language to describe how we inspect guests. The best introduction to this is the manual page, which I include below (it's also included in patch 2/3). Rich. ---------------------------------------------------------------------- NAME guestfs-inspection - guestfs inspection program SYNOPSIS guestfs-inspection NOTE This man page documents the guestfs inspection program. If you want to read about guestfs inspection then this is the wrong place. See "INSPECTION" in guestfs(3) instead. DESCRIPTION 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 guestfsd(8), when the caller uses the guestfs_inspect_os API (see guestfs-internals(1) and "guestfs_inspect_os" in guestfs(3)). 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 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 "WRITING RULES" below. OPTIONS -? --help Display brief help. -v --verbose Enable verbose messages for debugging. 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 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. 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 "/dev/sda1 is a filesystem". File("/dev/sda1", "/etc/fstab") which means "there exists a file called /etc/fstab on the /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 /dev/sda1 is not a filesystem or there does not exist a file called /etc/fstab on this filesystem". Unknown facts are facts that we don't know if they are true or false yet. Rules Rules are used to generate more facts. A simple rule for generating 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 fs & filename, if fs is a filesystem, and running the C code with parameters fs and filename returns true, then File(fs, filename) is a true fact". In the Prolog-inspired language, a comma (,) is the AND operator. A semicolon (;) is the OR operator. :- is a backwards if-statement (the condition is on the right, the conclusion is on the left). Also notice the dot (.) which must follow each rule. Uppercase identifiers are facts. Lowercase identifiers are variables. All identifiers are case-sensitive. Everything in {{ ... }} 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 (...) for grouping expressions on the right hand side of the :- operator. 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 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 File rule, and we substitute (theoretically) every possible string for fs and 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 /etc/fstab in /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 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 /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. 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 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 fs are found by looking at any 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 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 /etc/fstab and /etc/fedora-release would be tried: Filename("/etc/fstab"). Filename("/etc/fedora-release"). File(fs, filename) :- Filesystem(fs), Filename(filename), {{ // C code }}. C expressions returning boolean Simple C code enclosed in {{ ... }} as shown above should return a true, false or error status only. It returns true by returning any integer ≥ 1. It should return 0 to indicate false, and it should return -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 fs and 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 EIO is thrown. 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 var1, 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 set_product_name (that the compiler generates). The return value from the C code should be 0 if everything was OK, or -1 if there is a error (which stops the whole program). C expressions returning multiple results Finally it is possible for C code to return multiple results. The syntax is: [var1, var2, ...]={{ ... }} where var1, var2, etc. are outputs. Unlike the previous rules, these rules may generate multiple facts from a single string substitution. This is how we populate the initial list of true facts about filesystems: Filesystem(fs) :- [fs]={{ int i; for (i = 0; i < nr_filesystems; ++i) { set_fs (fs[i]); } return 0; }}. In this case, the C code repeatedly calls a function set_fs (that the compiler generates) for each new filesystem discovered. Multiple Filesystem facts can be generated as a result of one application of this rule. The return value from the C code should be 0 if everything was OK, or -1 if there is a error (which stops the whole program). 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: * 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. * A fact must have the same number of arguments ("arity") each time it appears in the source. Debugging You can debug the evaluation of inspection programs by calling guestfs_set_verbose (or setting $LIBGUESTFS_DEBUG=1) before launching the handle. This causes guestfsd(8) to pass the --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 stderr (or sent to the event system if you are using that). You can also print debug messages from C code embedded in {{...}} expressions. These are similarly sent upwards through to libguestfs and will appear on stderr. EXIT STATUS This program returns 0 if successful, or non-zero if there was an error. SEE ALSO guestfsd(8), guestfs-hacking(1), guestfs-internals(1), "INSPECTION" in guestfs(3), http://libguestfs.org/. AUTHOR Richard W.M. Jones http://people.redhat.com/~rjones/ COPYRIGHT Copyright (C) 2009-2015 Red Hat Inc. LICENSE 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. ----------------------------------------------------------------------
Richard W.M. Jones
2015-Dec-02 22:05 UTC
[Libguestfs] [PATCH 1/3] inspection: Add rules compiler to the generator.
--- .gitignore | 4 + README | 4 + bootstrap | 2 + generator/Makefile.am | 39 ++- generator/rules_compiler.ml | 757 +++++++++++++++++++++++++++++++++++++++++++ generator/rules_compiler.mli | 21 ++ generator/rules_parser.mly | 111 +++++++ generator/rules_scanner.mll | 112 +++++++ generator/types.ml | 49 +++ generator/utils.ml | 16 + generator/utils.mli | 6 + m4/guestfs_ocaml.m4 | 2 + 12 files changed, 1121 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 11557b6..288a853 100644 --- a/.gitignore +++ b/.gitignore @@ -221,7 +221,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/README b/README index 2c79c0d..26198fc 100644 --- a/README +++ b/README @@ -88,6 +88,10 @@ The full requirements are described below. | | | | Optional if compiling from tarball. | | | | | To build generated files and OCaml bindings. +--------------+-------------+---+-----------------------------------------+ +| ocamllex | 3.11 |R/O| Required if compiling from git. | +| ocamlyacc | | | Optional if compiling from tarball. | +| | | | To build generated files and OCaml bindings. ++--------------+-------------+---+-----------------------------------------+ | findlib | |R/O| Required if compiling from git. | | | | | Optional if compiling from tarball. | | | | | To build generated files and OCaml bindings. 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/generator/Makefile.am b/generator/Makefile.am index 9177e6f..fe6d35d 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 \ @@ -48,6 +51,13 @@ sources = \ prepopts.mli \ python.ml \ ruby.ml \ + 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 \ @@ -88,6 +98,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 @@ -101,6 +114,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 $@ @@ -111,7 +140,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_scanner.ml rm -f $@ $@-t $(OCAMLFIND) ocamldep -I ../ocaml -I $(abs_srcdir) $^ | \ $(SED) 's/ *$$//' | \ @@ -154,6 +183,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..8e0fc6c --- /dev/null +++ b/generator/rules_compiler.ml @@ -0,0 +1,757 @@ +(* 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. *) + +open Printf + +open Utils +open Types +open Pr +open Docstrings + +module StringSet = Set.Make (String) + +let (//) = Filename.concat + +type env = { + free_vars : string list; + assign_vars : string list; + list_assign_vars : string list; + + (* Name of the C environment struct. *) + env_struct : string; +} + +let rec compile filename () + let rules = parse filename in + type_checking filename rules; + + generate_header ~extra_inputs:[filename] CStyle GPLv2plus; + + pr "\ +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <inttypes.h> +#include <unistd.h> +#include <error.h> + +/* XXX At the moment we have to hard-code any headers needed by + * C code snippets from the input here. We could fix this by + * allowing the source to define a C prologue. + */ +#include <sys/types.h> +#include <sys/stat.h> +#include <errno.h> + +#include \"gl_oset.h\" +#include \"gl_xoset.h\" +#include \"gl_array_oset.h\" + +#include \"inspection.h\" + +#include \"guestfs-internal-all.h\" + +/* 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 and so on. eg: 'rule_0 ()' + * 'struct rule_0_env'. + *) + let rules = mapi (fun i rule -> (i, rule)) rules in + + (* Create the environment struct for each rule. This contains all + * the variables either consumed or set by the function. + *) + let rules + List.map (fun (i, rule) -> + let env = compile_rule_environment filename i rule in + (i, rule, env)) + rules in + + (* Write all C code snippets to functions. *) + iteri ( + fun j (i, rule, env) -> + let rec loop = function + | And (e1, e2) | Or (e1, e2) -> loop e1; loop e2 + | Code code -> compile_code filename i rule env j code + | AssignCode _ -> () +(* + | AssignCode (vs, code) -> + compile_assign_code filename i rule env j vs code + *) + | ListAssignCode (vs, code) -> + compile_list_assign_code filename i rule env j vs code + | Term _ | Not _ | True | False -> () + in + loop rule.body + ) rules; + + (* Compile all rules into functions. *) + let rules + List.map ( + fun (i, rule, env) -> + let rule_fn = sprintf "rule_%d" i in + compile_rule filename rule rule_fn env; + (rule, rule_fn) + ) rules in + + 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, rule_fn) -> + pr " if (verbose)\n"; + pr " printf (\"trying rule %%s\\n\", %S);\n" + (string_of_term rule.head); + pr " %s ();\n" rule_fn; + pr "\n"; + ) rules; + + pr " /* Added a true fact during this iteration? */ + if (nr_true_facts == count_true_facts ()) + break; + } /* for (;;) */ +} + +/* EOF */\n" + +and get_all_strings filename rules + let rec loop = function + | True | False | Code _ | AssignCode _ | ListAssignCode _ -> [] + | 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 i rule + (* The name of the C struct. *) + let env_struct = sprintf "rule_%d_env" i 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, list_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 + | Code _ | AssignCode _ | ListAssignCode _ -> () + in + loop rule.body; + + let assign_vars = Hashtbl.create 13 in + let list_assign_vars = Hashtbl.create 13 in + let rec loop = function + | True | False | Term _ | Not _ | Code _ -> () + | 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 + | ListAssignCode (vs, _) -> + List.iter ( + fun v -> + Hashtbl.remove free_vars v; + if Hashtbl.mem list_assign_vars v || 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 list_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 + let list_assign_vars + Hashtbl.fold (fun k _ ks -> k :: ks) list_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 " /* assigned vars */\n"; + List.iter (pr " char *%s;\n") assign_vars + ); + if list_assign_vars <> [] then ( + pr " /* assigned lists */\n"; + pr " size_t nr_list_assign_vars;\n"; + List.iter (pr " char **%s;\n") list_assign_vars + ); + pr "};\n"; + pr "\n"; + + (* Return the OCaml env. *) + { free_vars = free_vars; + assign_vars = assign_vars; + list_assign_vars = list_assign_vars; + env_struct = env_struct; } + +(* Compile a single rule to C code. *) +and compile_rule filename rule rule_fn 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 | Code _ | AssignCode _ | ListAssignCode _ -> () + | 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 "static void\n"; + pr "%s (void)\n" 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"; + + 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 and list_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; + List.iter (pr " env.%s = NULL;\n") env.list_assign_vars; + if env.list_assign_vars <> [] then pr " env.nr_list_assign_vars = 0;\n"; + + (* We can only do this optimization if assign_vars = list_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 = [] && env.list_assign_vars = [] then ( + pr " {\n"; + pr " /* If the fact already exists, don't bother doing any work. */\n"; + pr " CLEANUP_FREE fact *fact = create_fact (%S" rule.head.term_name; + List.iter (function + | Variable v -> pr ", env.%s" v + | Constant s -> pr ", %S" s) + rule.head.term_args; + pr ", NULL);\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 + | Code code -> + let code_fn = function_of_code code in + pr " %s = %s (&env);\n" result code_fn + | AssignCode _ -> + (* XXX *) + + (* The result of AssignCode is always true (else it would + * have exited earlier). Hence: + *) + pr " %s = 1;\n" result + | ListAssignCode (vs, code) -> + let code_fn = function_of_code code in + pr " %s = %s (&env);\n" result code_fn; + pr " if (%s == -1)\n" result; + pr " error (EXIT_FAILURE, 0, \"code returned error in %%s\",\n"; + pr " \"%s\");\n" (string_of_term rule.head); + (* The result of ListAssignCode is always true (else it would + * have exited 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 " CLEANUP_FREE fact *fact = create_fact (%S" term.term_name; + List.iter ( + function + | Variable v -> pr ", env.%s" v + | Constant s -> pr ", %S" s + ) term.term_args; + pr ", NULL);\n"; + pr " %s = is_fact (true, fact);\n" result; + pr " }\n"; + | Not term -> + pr " {\n"; + pr " CLEANUP_FREE fact *fact = create_fact (%S" term.term_name; + List.iter ( + function + | Variable v -> pr ", env.%s" v + | Constant s -> pr ", %S" s + ) term.term_args; + pr ", NULL);\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 "%sCLEANUP_FREE fact *fact = create_fact (%S" indent rule.head.term_name; + List.iter ( + function + | Variable v -> + if not (List.mem v env.list_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 ", NULL);\n"; + in + pr " if (result > 0) /* true */ {\n"; + if env.list_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_list_assign_vars; ++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.list_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_list_assign_vars; ++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 and list_assign_vars. The free_vars don't + * have to be freed because the iterator loop handles them. + *) + List.iter (pr " free (env.%s);\n") env.assign_vars; + List.iter ( + fun v -> + pr " for (size_t i = 0; i < env.nr_list_assign_vars; ++i)\n"; + pr " free (env.%s[i]);\n" v; + pr " free (env.%s);\n" v + ) env.list_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 (boolean) Code snippet from a rule into a function. *) +and compile_code filename i rule env j code + let code_fn = sprintf "rule_%d_code_%d" i j in + Hashtbl.add code_hash code code_fn; + + pr "static int\n"; + pr "%s (struct %s *env)\n" code_fn env.env_struct; + pr "{\n"; + List.iter (fun v -> pr "#define %s (env->%s)\n" v v) env.free_vars; + (* XXX # lineno *) + pr "%s\n" code; + List.iter (pr "#undef %s\n") env.free_vars; + pr "}\n"; + pr "\n"; + +(* Compile a list assignment code (ListAssignCode) snippet + * into a function. + *) +and compile_list_assign_code filename i rule env j vs code + (* Create a function for setting a row in the result. *) + let set_vars_fn = sprintf "rule_%d_code_%d_set_row" i j in + let set_vars_alias = sprintf "set_%s" (String.concat "_" vs) in + pr "static void\n"; + pr "%s (struct %s *env, ...)\n" set_vars_fn env.env_struct; + pr "{\n"; + pr " va_list args;\n"; + pr " size_t i = env->nr_list_assign_vars;\n"; + pr "\n"; + pr " va_start (args, env);\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 (va_arg (args, char *));\n" v; + pr " if (env->%s[i] == NULL)\n" v; + pr " error (EXIT_FAILURE, errno, \"strdup\");\n"; + ) vs; + pr " va_end (args);\n"; + pr " env->nr_list_assign_vars++;\n"; + pr "}\n"; + pr "\n"; + + (* Create the function itself. *) + let code_fn = sprintf "rule_%d_code_%d" i j in + Hashtbl.add code_hash code code_fn; + + pr "static int\n"; + pr "%s (struct %s *env)\n" code_fn env.env_struct; + pr "{\n"; + List.iter (fun v -> pr "#define %s (env->%s)\n" v v) env.free_vars; + pr "#define %s(v1,...) %s (env, (v1), ##__VA_ARGS__)\n" + set_vars_alias set_vars_fn; + (* XXX # lineno *) + pr "%s\n" code; + List.iter (pr "#undef %s\n") env.free_vars; + pr "#undef %s\n" set_vars_alias; + pr "}\n"; + pr "\n"; + +(* Map of code to function names. *) +and code_hash = Hashtbl.create 13 +and function_of_code code = Hashtbl.find code_hash code + +(* Parse the input. *) +and parse filename + let lexbuf = Lexing.from_channel (open_in filename) in + let rules = ref [] in + (try + while true do + let rule = Rules_parser.rule Rules_scanner.token lexbuf in + (*printf "%s\n" (string_of_rule rule);*) + rules := rule :: !rules + 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 + ); + let rules = List.rev !rules in + rules + +(* Minimal type checking. *) +and type_checking 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 | Code _ | AssignCode _ | ListAssignCode _ -> () + +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..bdf6159 --- /dev/null +++ b/generator/rules_parser.mly @@ -0,0 +1,111 @@ +/* libguestfs -*- text -*- + * 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 + */ + +%{ +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 LSQUARE RSQUARE /* [ ... ] */ +%token <string> CODE /* {{ .. }} containing C code */ +%token DOT /* . */ +%token IMPLIC /* :- */ +%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 rule +%type <Types.rule> rule + +%% + +rule: head DOT + { { head = $1; body = True } } + | head IMPLIC body DOT + { { head = $1; body = $3 } } + ; + +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 + { Code $1 } + | LPAREN result_bindings RPAREN EQUALS CODE + { AssignCode ($2, $5) } + | LSQUARE result_bindings RSQUARE EQUALS CODE + { ListAssignCode ($2, $5) } + | 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..58c959d --- /dev/null +++ b/generator/rules_scanner.mll @@ -0,0 +1,112 @@ +(* libguestfs -*- text -*- + * 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 + *) + +{ +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 } + | '[' { LSQUARE } + | ']' { RSQUARE } + | '=' { EQUALS } + | '.' { DOT } + | ":-" { IMPLIC } + | ',' { COMMA } + | ';' { SEMI } + | '!' { NOT } + | '"' { 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 f2d9750..dea94a7 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 @@ -421,3 +423,50 @@ type call_optargt | CallOInt64 of string * int64 | CallOString of string * string | CallOStringList of string * string list + +(* Used by the rules compiler. *) + +type rule = { head : term; body : expr } +(* 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 *) + | Code of string (* {{ ... }} *) + | AssignCode of string list * string (* (a,b)={{ ... }} *) + | ListAssignCode of string list * string (* [a,b]={{ ... }} *) + +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) + | Code _ -> "{{ // code }}" + | AssignCode (bindings, _) -> + sprintf "(%s)={{ // code }}" (String.concat ", " bindings) + | ListAssignCode (bindings, _) -> + sprintf "[%s]={{ // code }}" (String.concat ", " bindings) diff --git a/generator/utils.ml b/generator/utils.ml index 7d47430..6b4497d 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 e0f30c3..392e9d6 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 b3e9387..e213f80 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
2015-Dec-02 22:05 UTC
[Libguestfs] [PATCH 2/3] inspection: Add inspection directory, rules and supporting code.
--- .gitignore | 4 + Makefile.am | 2 + configure.ac | 1 + docs/guestfs-hacking.pod | 6 + generator/main.ml | 4 + inspection/Makefile.am | 87 ++++++++ inspection/cleanups.c | 38 ++++ inspection/facts.c | 321 +++++++++++++++++++++++++++ inspection/guestfs-inspection.pod | 456 ++++++++++++++++++++++++++++++++++++++ inspection/inspection.c | 104 +++++++++ inspection/inspection.h | 67 ++++++ inspection/inspection.rules | 125 +++++++++++ inspection/mount.c | 33 +++ inspection/utils.c | 50 +++++ po/POTFILES | 5 + src/guestfs.pod | 1 + 16 files changed, 1304 insertions(+) create mode 100644 inspection/Makefile.am create mode 100644 inspection/cleanups.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/mount.c create mode 100644 inspection/utils.c diff --git a/.gitignore b/.gitignore index 288a853..b5e5a77 100644 --- a/.gitignore +++ b/.gitignore @@ -248,6 +248,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 951ee43..0363136 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 @@ -289,6 +290,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/configure.ac b/configure.ac index 5af33ed..893826b 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 9f4b72a..5827375 100644 --- a/docs/guestfs-hacking.pod +++ b/docs/guestfs-hacking.pod @@ -560,6 +560,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. @@ -739,6 +744,7 @@ Create the branch in git: L<guestfs(3)>, 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/main.ml b/generator/main.ml index 35511ce..7473e89 100644 --- a/generator/main.ml +++ b/generator/main.ml @@ -212,6 +212,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..3020ff4 --- /dev/null +++ b/inspection/Makefile.am @@ -0,0 +1,87 @@ +# libguestfs +# Copyright (C) 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 $(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 = \ + cleanups.c \ + facts.c \ + inspection.c \ + inspection.h \ + mount.c \ + rules.c \ + utils.c + +# XXX Why is this necessary? +rules.c: ../generator/stamp-generator + rm -f $< + $(MAKE) -C ../generator + +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)/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/cleanups.c b/inspection/cleanups.c new file mode 100644 index 0000000..73d6e92 --- /dev/null +++ b/inspection/cleanups.c @@ -0,0 +1,38 @@ +/* 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 "inspection.h" + +/* Used by the CLEANUP_* macros. Do not call these directly. */ + +void +cleanup_free (void *ptr) +{ + free (* (void **) ptr); +} + +void +cleanup_free_string_list (void *ptr) +{ + free_strings (* (char ***) ptr); +} diff --git a/inspection/facts.c b/inspection/facts.c new file mode 100644 index 0000000..219bcbd --- /dev/null +++ b/inspection/facts.c @@ -0,0 +1,321 @@ +/* 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 "inspection.h" + +/* True and false facts sets. See guestfs-inspection(8)/WRITING RULES + * to understand why these are used. + */ +static gl_oset_t true_facts = NULL; +static gl_oset_t false_facts = NULL; + +/* Used to store a single true or false fact. The only reason we know + * it's a false fact is because it would be stored in the false_facts + * set. + */ +struct fact { + char *term_name; + size_t nr_term_args; + char *term_arg[0]; +}; + +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_helper (void) __attribute__((constructor)); + +static void +init_facts_helper (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_helper (void) __attribute__((destructor)); + +static void +free_facts_helper (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, 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, ...) +{ + fact *f; + const char *p; + size_t i; + va_list args; + + f = malloc (sizeof (*f)); + if (f == NULL) + error (EXIT_FAILURE, errno, "malloc"); + va_start (args, term_name); + for (i = 0; (p = va_arg (args, const char *)) != NULL; ++i) { + f = realloc (f, sizeof (*f) + (i+1) * sizeof (char *)); + if (f == NULL) + error (EXIT_FAILURE, errno, "realloc"); + f->term_arg[i] = (char *) p; + } + va_end (args); + f->term_name = (char *) term_name; + f->nr_term_args = i; + 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..113f903 --- /dev/null +++ b/inspection/guestfs-inspection.pod @@ -0,0 +1,456 @@ +=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, ...]={{ ... }} + +where C<var1>, C<var2>, etc. are outputs. Unlike the previous rules, +these rules may generate multiple facts from a single string +substitution. + +This is how we populate the initial list of true facts about +filesystems: + + Filesystem(fs) :- + [fs]={{ + int i; + for (i = 0; i < nr_filesystems; ++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. + +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). + +=begin comment + +NOT IMPLEMENTED YET +=head2 C code memoization + +For efficiency, the C code fragments are called once per input, and +the result is memoized. + +This means you don't have to worry if the C code is particularly +efficient. No matter how many times we need to evaluate if +F</etc/fstab> exists on F</dev/sda1>, the C code to do this will only +be called once. But it also means that impure C functions returning +different results for the same input won't work, but you shouldn't do +that. + +=end comment + +=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>. + +=begin comment + +NOT IMPLEMENTED YET +Remember that C memoization can cause C code to run fewer times than expected. + +=end comment + +=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-2015 Red Hat Inc. diff --git a/inspection/inspection.c b/inspection/inspection.c new file mode 100644 index 0000000..ec1758d --- /dev/null +++ b/inspection/inspection.c @@ -0,0 +1,104 @@ +/* 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 "inspection.h" + +int verbose = 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..8addef9 --- /dev/null +++ b/inspection/inspection.h @@ -0,0 +1,67 @@ +/* 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. + */ + +#ifndef GUESTFS_INSPECTION_H +#define GUESTFS_INSPECTION_H + +#include <stdbool.h> + +#include "gl_oset.h" + +extern int verbose; + +typedef struct fact fact; + +/* facts.c */ +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, 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 fact *create_fact (const char *term_name, ...); +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); + +/* mount.c - used by generated code */ +extern int get_mount (const char *fs, const char *filename, char **relative_filename); + +/* cleanups.c - used by the CLEANUP_* macros. */ +extern void cleanup_free (void *ptr); +extern void cleanup_free_string_list (void *ptr); + +#ifdef HAVE_ATTRIBUTE_CLEANUP +#define CLEANUP_FREE __attribute__((cleanup(cleanup_free))) +#define CLEANUP_FREE_STRING_LIST \ + __attribute__((cleanup(cleanup_free_string_list))) +#else +#define CLEANUP_FREE +#define CLEANUP_FREE_STRING_LIST +#endif + +/* utils.c */ +extern size_t count_strings (char *const *argv); +extern void free_strings (char **argv); + +#endif /* GUESTFS_INSPECTION_H */ diff --git a/inspection/inspection.rules b/inspection/inspection.rules new file mode 100644 index 0000000..9b1ed26 --- /dev/null +++ b/inspection/inspection.rules @@ -0,0 +1,125 @@ +/* -*- prolog -*- */ + +Filesystem(fs) :- + [fs]={{ + const char *filesystems[] = { "/dev/sda1", "/dev/sda2", "/dev/sda3" }; + int i; + for (i = 0; i < 3; ++i) { set_fs (filesystems[i]); } + return 0; + }}. + +File(fs, filename) :- + Filesystem(fs), + {{ + int r; + char *relative_filename; + struct stat statbuf; + r = get_mount (fs, filename, &relative_filename); + if (r != 1) return r; + r = stat (relative_filename, &statbuf); + free (relative_filename); + if (r == -1) { + if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP) + return 0; + perror ("stat"); + return -1; + } + return S_ISREG (statbuf.st_mode); + }}. + +Directory(fs, dirname) :- + Filesystem(fs), + {{ + int r; + char *relative_dirname; + struct stat statbuf; + r = get_mount (fs, dirname, &relative_dirname); + if (r != 1) return r; + r = stat (relative_dirname, &statbuf); + free (relative_dirname); + if (r == -1) { + if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP) + return 0; + perror ("stat"); + return -1; + } + return S_ISDIR (statbuf.st_mode); + }}. + +Symlink(fs, filename) :- + Filesystem(fs), + {{ + int r; + char *relative_filename; + struct stat statbuf; + r = get_mount (fs, filename, &relative_filename); + if (r != 1) return r; + r = stat (relative_filename, &statbuf); + free (relative_filename); + if (r == -1) { + if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP) + return 0; + perror ("stat"); + return -1; + } + return S_ISLNK (statbuf.st_mode); + }}. + +Unix_root(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") :- + Unix_root(rootfs), + File(rootfs, "/etc/redhat-release"), + ! File(rootfs, "/etc/fedora-release"). + +/* +ProductName(rootfs, product_name) :- + Unix_root(rootfs), + Distro(rootfs, "RHEL"), + (product_name)={{ + // code to read first line from /etc/redhat-release + }}. + +Version(rootfs, major, minor) :- + Unix_root(rootfs), + Distro(rootfs, "RHEL"), + ProductName(rootfs, product_name), + (major, minor)={{ + // code to parse product_name + }}. + +Distro(rootfs, "Fedora") :- + Unix_root(rootfs), + File(rootfs, "/etc/fedora-release"). + +Version(rootfs, major, minor) :- + Unix_root(rootfs), + Distro(rootfs, "Fedora"), + (major, minor)={{ + // code to parse /etc/fedora-release + }}. + +Distro(rootfs, "Debian") :- + Unix_root(rootfs), + File(rootfs, "/etc/debian_version"). + +Version(rootfs, major, minor) :- + Unix_root(rootfs), + Distro(rootfs, "Debian"), + (major, minor)={{ + // code to parse /etc/debian_version + }}. + +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/mount.c b/inspection/mount.c new file mode 100644 index 0000000..a849ed2 --- /dev/null +++ b/inspection/mount.c @@ -0,0 +1,33 @@ +/* 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 "error.h" + +#include "inspection.h" + +int +get_mount (const char *fs, const char *filename, char **relative_filename) +{ + error (EXIT_FAILURE, 0, "get_mount: not implemented"); + return -1; +} diff --git a/inspection/utils.c b/inspection/utils.c new file mode 100644 index 0000000..616e5cd --- /dev/null +++ b/inspection/utils.c @@ -0,0 +1,50 @@ +/* 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 <inttypes.h> + +#include "inspection.h" + +size_t +count_strings (char *const *argv) +{ + size_t argc; + + for (argc = 0; argv[argc] != NULL; ++argc) + ; + return argc; +} + +void +free_strings (char **argv) +{ + size_t argc; + + if (!argv) + return; + + for (argc = 0; argv[argc] != NULL; ++argc) + free (argv[argc]); + free (argv); +} diff --git a/po/POTFILES b/po/POTFILES index c6c277c..a88d0d0 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -256,6 +256,11 @@ gobject/src/struct-version.c gobject/src/struct-xattr.c gobject/src/struct-xfsinfo.c gobject/src/tristate.c +inspection/cleanups.c +inspection/facts.c +inspection/inspection.c +inspection/mount.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 f9dea92..299f391 100644 --- a/src/guestfs.pod +++ b/src/guestfs.pod @@ -3503,6 +3503,7 @@ L<virt-win-reg(1)>. Other libguestfs topics: 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
2015-Dec-02 22:05 UTC
[Libguestfs] [PATCH 3/3] inspection: Add unit tests of the rules compiler.
--- .gitignore | 2 ++ generator/main.ml | 4 +++- inspection/Makefile.am | 25 +++++++++++++++++++++- inspection/test-harness.c | 54 +++++++++++++++++++++++++++++++++++++++++++++++ inspection/test1.rules | 43 +++++++++++++++++++++++++++++++++++++ po/POTFILES | 2 ++ 6 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 inspection/test-harness.c create mode 100644 inspection/test1.rules diff --git a/.gitignore b/.gitignore index b5e5a77..752dd70 100644 --- a/.gitignore +++ b/.gitignore @@ -252,6 +252,8 @@ Makefile.in /inspection/guestfs-inspection.8 /inspection/rules.c /inspection/stamp-guestfs-inspection.pod +/inspection/test1 +/inspection/test1.c /inspector/actual-*.xml /inspector/stamp-virt-inspector.pod /inspector/test-xmllint.sh diff --git a/generator/main.ml b/generator/main.ml index 7473e89..4e819a4 100644 --- a/generator/main.ml +++ b/generator/main.ml @@ -212,7 +212,9 @@ 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/rules.c" (Rules_compiler.compile "inspection/inspection.rules"); diff --git a/inspection/Makefile.am b/inspection/Makefile.am index 3020ff4..733529e 100644 --- a/inspection/Makefile.am +++ b/inspection/Makefile.am @@ -18,7 +18,8 @@ include $(top_srcdir)/subdir-rules.mk generator_built = \ - rules.c + rules.c \ + test1.c BUILT_SOURCES = \ $(generator_built) @@ -85,3 +86,25 @@ stamp-guestfs-inspection.pod: guestfs-inspection.pod --license GPLv2+ \ $< touch $@ + +# Tests. + +TESTS = test1 +check_PROGRAMS = test1 + +test1_SOURCES = \ + cleanups.c \ + facts.c \ + inspection.h \ + test-harness.c \ + test1.c \ + utils.c + +test1_LDADD = $(guestfs_inspection_LDADD) +test1_CPPFLAGS = $(guestfs_inspection_CPPFLAGS) -DTEST1 +test1_CFLAGS = $(guestfs_inspection_CFLAGS) + +# XXX Why is this necessary? +test1.c: ../generator/stamp-generator + rm -f $< + $(MAKE) -C ../generator diff --git a/inspection/test-harness.c b/inspection/test-harness.c new file mode 100644 index 0000000..350f81f --- /dev/null +++ b/inspection/test-harness.c @@ -0,0 +1,54 @@ +/* guestfs-inspection tests + * 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 <assert.h> + +#include "inspection.h" + +int verbose = 1; + +/* Required by the gnulib 'error' module. */ +const char *program_name = "test"; + +int +main (int argc, char *argv[]) +{ + /* Run the rules. */ + rules (); + + /* Check the true facts are what we expect. */ +#ifdef TEST1 + CLEANUP_FREE fact *f1 = create_fact ("Distro", "/dev/sda1", "Debian", NULL); + assert (is_fact (true, f1)); + + CLEANUP_FREE fact *f2 = create_fact ("Distro", "/dev/sda2", "Fedora", NULL); + assert (is_fact (true, f2)); + + CLEANUP_FREE fact *f3 = create_fact ("Distro", "/dev/sda3", "RHEL", NULL); + assert (is_fact (true, f3)); +#else +#error "unknown test case" +#endif + + exit (EXIT_SUCCESS); +} diff --git a/inspection/test1.rules b/inspection/test1.rules new file mode 100644 index 0000000..5903b46 --- /dev/null +++ b/inspection/test1.rules @@ -0,0 +1,43 @@ +/* -*- prolog -*- */ + +Filesystem("/dev/sda1"). +Filesystem("/dev/sda2"). +Filesystem("/dev/sda3"). + +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"). diff --git a/po/POTFILES b/po/POTFILES index a88d0d0..8719f55 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -260,6 +260,8 @@ inspection/cleanups.c inspection/facts.c inspection/inspection.c inspection/mount.c +inspection/test-harness.c +inspection/test1.c inspection/utils.c inspector/inspector.c java/com_redhat_et_libguestfs_GuestFS.c -- 2.5.0