guestfs -> GuestFS
[libguestfs.git] / src / generator.ml
index c8809e4..84ee90f 100755 (executable)
  * 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 script generates a large amount of code and documentation for
+ * all the daemon actions.
  *
- * This script generates a large amount of code and documentation for
- * all the daemon actions.  To add a new action there are only two
- * files you need to change, this one to describe the interface, and
+ * To add a new action there are only two files you need to change,
+ * this one to describe the interface (see the big table below), and
  * daemon/<somefile>.c to write the implementation.
+ *
+ * After editing this file, run it (./src/generator.ml) to regenerate
+ * all the output files.
+ *
+ * IMPORTANT: This script should NOT print any warnings.  If it prints
+ * warnings, you should treat them as errors.
+ * [Need to add -warn-error to ocaml command line]
  *)
 
 #load "unix.cma";;
@@ -28,11 +38,11 @@ open Printf
 
 type style = ret * args
 and ret =
-    (* "Err" as a return value means an int used as a simple error
+    (* "RErr" as a return value means an int used as a simple error
      * indication, ie. 0 or -1.
      *)
-  | Err
-    (* "Int" as a return value means an int which is -1 for error
+  | RErr
+    (* "RInt" as a return value means an int which is -1 for error
      * or any value >= 0 on success.
      *)
   | RInt of string
@@ -55,24 +65,114 @@ and ret =
   | RPVList of string
   | RVGList of string
   | RLVList of string
-and args =
-    (* 0 arguments, 1 argument, etc. The guestfs_h param is implicit. *)
-  | P0
-  | P1 of argt
-  | P2 of argt * argt
-  | P3 of argt * argt * argt
+and args = argt list   (* Function parameters, guestfs handle is implicit. *)
+
+    (* Note in future we should allow a "variable args" parameter as
+     * the final parameter, to allow commands like
+     *   chmod mode file [file(s)...]
+     * This is not implemented yet, but many commands (such as chmod)
+     * are currently defined with the argument order keeping this future
+     * possibility in mind.
+     *)
 and argt =
   | String of string   (* const char *name, cannot be NULL *)
   | OptString of string        (* const char *name, may be NULL *)
+  | StringList of string(* list of strings (each string cannot be NULL) *)
   | Bool of string     (* boolean *)
   | Int of string      (* int (smallish ints, signed, <= 31 bits) *)
 
 type flags =
   | ProtocolLimitWarning  (* display warning about protocol size limits *)
+  | DangerWillRobinson   (* flags particularly dangerous commands *)
   | FishAlias of string          (* provide an alias for this cmd in guestfish *)
   | FishAction of string  (* call this function in guestfish *)
   | NotInFish            (* do not export via guestfish *)
 
+let protocol_limit_warning =
+  "Because of the message protocol, there is a transfer limit 
+of somewhere between 2MB and 4MB.  To transfer large files you should use
+FTP."
+
+let danger_will_robinson =
+  "B<This command is dangerous.  Without careful use you
+can easily destroy all your data>."
+
+(* You can supply zero or as many tests as you want per API call.
+ *
+ * Note that the test environment has 3 block devices, of size 500MB,
+ * 50MB and 10MB (respectively /dev/sda, /dev/sdb, /dev/sdc).
+ * Note for partitioning purposes, the 500MB device has 63 cylinders.
+ *
+ * To be able to run the tests in a reasonable amount of time,
+ * the virtual machine and block devices are reused between tests.
+ * So don't try testing kill_subprocess :-x
+ *
+ * Between each test we umount-all and lvm-remove-all.
+ *
+ * Don't assume anything about the previous contents of the block
+ * devices.  Use 'Init*' to create some initial scenarios.
+ *)
+type tests = (test_init * test) list
+and test =
+    (* Run the command sequence and just expect nothing to fail. *)
+  | TestRun of seq
+    (* Run the command sequence and expect the output of the final
+     * command to be the string.
+     *)
+  | TestOutput of seq * string
+    (* Run the command sequence and expect the output of the final
+     * command to be the list of strings.
+     *)
+  | TestOutputList of seq * string list
+    (* Run the command sequence and expect the output of the final
+     * command to be the integer.
+     *)
+  | TestOutputInt of seq * int
+    (* Run the command sequence and expect the output of the final
+     * command to be a true value (!= 0 or != NULL).
+     *)
+  | TestOutputTrue of seq
+    (* Run the command sequence and expect the output of the final
+     * command to be a false value (== 0 or == NULL, but not an error).
+     *)
+  | TestOutputFalse of seq
+    (* Run the command sequence and expect the output of the final
+     * command to be a list of the given length (but don't care about
+     * content).
+     *)
+  | TestOutputLength of seq * int
+    (* Run the command sequence and expect the final command (only)
+     * to fail.
+     *)
+  | TestLastFail of seq
+
+(* Some initial scenarios for testing. *)
+and test_init =
+    (* Do nothing, block devices could contain random stuff including
+     * LVM PVs, and some filesystems might be mounted.  This is usually
+     * a bad idea.
+     *)
+  | InitNone
+    (* Block devices are empty and no filesystems are mounted. *)
+  | InitEmpty
+    (* /dev/sda contains a single partition /dev/sda1, which is formatted
+     * as ext2, empty [except for lost+found] and mounted on /.
+     * /dev/sdb and /dev/sdc may have random content.
+     * No LVM.
+     *)
+  | InitBasicFS
+    (* /dev/sda:
+     *   /dev/sda1 (is a PV):
+     *     /dev/VG/LV (size 8MB):
+     *       formatted as ext2, empty [except for lost+found], mounted on /
+     * /dev/sdb and /dev/sdc may have random content.
+     *)
+  | InitBasicFSonLVM
+
+(* Sequence of commands for testing. *)
+and seq = cmd list
+and cmd = string list
+
 (* Note about long descriptions: When referring to another
  * action, use the format C<guestfs_other> (ie. the full name of
  * the C function).  This will be replaced as appropriate in other
@@ -82,7 +182,8 @@ type flags =
  *)
 
 let non_daemon_functions = [
-  ("launch", (Err, P0), -1, [FishAlias "run"; FishAction "launch"],
+  ("launch", (RErr, []), -1, [FishAlias "run"; FishAction "launch"],
+   [],
    "launch the qemu subprocess",
    "\
 Internally libguestfs is implemented by running a virtual machine
@@ -91,7 +192,8 @@ using L<qemu(1)>.
 You should call this after configuring the handle
 (eg. adding drives) but before performing any actions.");
 
-  ("wait_ready", (Err, P0), -1, [NotInFish],
+  ("wait_ready", (RErr, []), -1, [NotInFish],
+   [],
    "wait until the qemu subprocess launches",
    "\
 Internally libguestfs is implemented by running a virtual machine
@@ -100,12 +202,14 @@ using L<qemu(1)>.
 You should call this after C<guestfs_launch> to wait for the launch
 to complete.");
 
-  ("kill_subprocess", (Err, P0), -1, [],
+  ("kill_subprocess", (RErr, []), -1, [],
+   [],
    "kill the qemu subprocess",
    "\
 This kills the qemu subprocess.  You should never need to call this.");
 
-  ("add_drive", (Err, P1 (String "filename")), -1, [FishAlias "add"],
+  ("add_drive", (RErr, [String "filename"]), -1, [FishAlias "add"],
+   [],
    "add an image to examine or modify",
    "\
 This function adds a virtual machine disk image C<filename> to the
@@ -121,14 +225,16 @@ image).
 
 This is equivalent to the qemu parameter C<-drive file=filename>.");
 
-  ("add_cdrom", (Err, P1 (String "filename")), -1, [FishAlias "cdrom"],
+  ("add_cdrom", (RErr, [String "filename"]), -1, [FishAlias "cdrom"],
+   [],
    "add a CD-ROM disk image to examine",
    "\
 This function adds a virtual CD-ROM disk image to the guest.
 
 This is equivalent to the qemu parameter C<-cdrom filename>.");
 
-  ("config", (Err, P2 (String "qemuparam", OptString "qemuvalue")), -1, [],
+  ("config", (RErr, [String "qemuparam"; OptString "qemuvalue"]), -1, [],
+   [],
    "add qemu parameters",
    "\
 This can be used to add arbitrary qemu command line parameters
@@ -140,7 +246,8 @@ The first character of C<param> string must be a C<-> (dash).
 
 C<value> can be NULL.");
 
-  ("set_path", (Err, P1 (String "path")), -1, [FishAlias "path"],
+  ("set_path", (RErr, [String "path"]), -1, [FishAlias "path"],
+   [],
    "set the search path",
    "\
 Set the path that libguestfs searches for kernel and initrd.img.
@@ -153,7 +260,8 @@ must make sure it remains valid for the lifetime of the handle.
 
 Setting C<path> to C<NULL> restores the default path.");
 
-  ("get_path", (RConstString "path", P0), -1, [],
+  ("get_path", (RConstString "path", []), -1, [],
+   [],
    "get the search path",
    "\
 Return the current search path.
@@ -161,19 +269,22 @@ Return the current search path.
 This is always non-NULL.  If it wasn't set already, then this will
 return the default path.");
 
-  ("set_autosync", (Err, P1 (Bool "autosync")), -1, [FishAlias "autosync"],
+  ("set_autosync", (RErr, [Bool "autosync"]), -1, [FishAlias "autosync"],
+   [],
    "set autosync mode",
    "\
 If C<autosync> is true, this enables autosync.  Libguestfs will make a
 best effort attempt to run C<guestfs_sync> when the handle is closed
 (also if the program exits without closing handles).");
 
-  ("get_autosync", (RBool "autosync", P0), -1, [],
+  ("get_autosync", (RBool "autosync", []), -1, [],
+   [],
    "get autosync mode",
    "\
 Get the autosync flag.");
 
-  ("set_verbose", (Err, P1 (Bool "verbose")), -1, [FishAlias "verbose"],
+  ("set_verbose", (RErr, [Bool "verbose"]), -1, [FishAlias "verbose"],
+   [],
    "set verbose mode",
    "\
 If C<verbose> is true, this turns on verbose messages (to C<stderr>).
@@ -181,14 +292,21 @@ If C<verbose> is true, this turns on verbose messages (to C<stderr>).
 Verbose messages are disabled unless the environment variable
 C<LIBGUESTFS_DEBUG> is defined and set to C<1>.");
 
-  ("get_verbose", (RBool "verbose", P0), -1, [],
+  ("get_verbose", (RBool "verbose", []), -1, [],
+   [],
    "get verbose mode",
    "\
 This returns the verbose messages flag.")
 ]
 
 let daemon_functions = [
-  ("mount", (Err, P2 (String "device", String "mountpoint")), 1, [],
+  ("mount", (RErr, [String "device"; String "mountpoint"]), 1, [],
+   [InitEmpty, TestOutput (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["write_file"; "/new"; "new file contents"; "0"];
+       ["cat"; "/new"]], "new file contents")],
    "mount a guest disk at a position in the filesystem",
    "\
 Mount a guest disk at a position in the filesystem.  Block devices
@@ -208,7 +326,8 @@ on the underlying device.
 The filesystem options C<sync> and C<noatime> are set with this
 call, in order to improve reliability.");
 
-  ("sync", (Err, P0), 2, [],
+  ("sync", (RErr, []), 2, [],
+   [ InitEmpty, TestRun [["sync"]]],
    "sync disks, writes are flushed through to the disk image",
    "\
 This syncs the disk, so that any writes are flushed through to the
@@ -217,14 +336,20 @@ underlying disk image.
 You should always call this if you have modified a disk image, before
 closing the handle.");
 
-  ("touch", (Err, P1 (String "path")), 3, [],
+  ("touch", (RErr, [String "path"]), 3, [],
+   [InitBasicFS, TestOutputTrue (
+      [["touch"; "/new"];
+       ["exists"; "/new"]])],
    "update file timestamps or create a new file",
    "\
 Touch acts like the L<touch(1)> command.  It can be used to
 update the timestamps on a file, or, if the file does not exist,
 to create a new zero-length file.");
 
-  ("cat", (RString "content", P1 (String "path")), 4, [ProtocolLimitWarning],
+  ("cat", (RString "content", [String "path"]), 4, [ProtocolLimitWarning],
+   [InitBasicFS, TestOutput (
+      [["write_file"; "/new"; "new file contents"; "0"];
+       ["cat"; "/new"]], "new file contents")],
    "list the contents of a file",
    "\
 Return the contents of the file named C<path>.
@@ -234,7 +359,10 @@ Note that this function cannot correctly handle binary files
 as end of string).  For those you need to use the C<guestfs_read_file>
 function which has a more complex interface.");
 
-  ("ll", (RString "listing", P1 (String "directory")), 5, [],
+  ("ll", (RString "listing", [String "directory"]), 5, [],
+   [], (* XXX Tricky to test because it depends on the exact format
+       * of the 'ls -l' command, which changes between F10 and F11.
+       *)
    "list the files in a directory (long format)",
    "\
 List the files in C<directory> (relative to the root directory,
@@ -243,7 +371,12 @@ there is no cwd) in the format of 'ls -la'.
 This command is mostly useful for interactive sessions.  It
 is I<not> intended that you try to parse the output string.");
 
-  ("ls", (RStringList "listing", P1 (String "directory")), 6, [],
+  ("ls", (RStringList "listing", [String "directory"]), 6, [],
+   [InitBasicFS, TestOutputList (
+      [["touch"; "/new"];
+       ["touch"; "/newer"];
+       ["touch"; "/newest"];
+       ["ls"; "/"]], ["lost+found"; "new"; "newer"; "newest"])],
    "list the files in a directory",
    "\
 List the files in C<directory> (relative to the root directory,
@@ -253,14 +386,21 @@ hidden files are shown.
 This command is mostly useful for interactive sessions.  Programs
 should probably use C<guestfs_readdir> instead.");
 
-  ("list_devices", (RStringList "devices", P0), 7, [],
+  ("list_devices", (RStringList "devices", []), 7, [],
+   [InitEmpty, TestOutputList (
+      [["list_devices"]], ["/dev/sda"; "/dev/sdb"; "/dev/sdc"])],
    "list the block devices",
    "\
 List all the block devices.
 
 The full block device names are returned, eg. C</dev/sda>");
 
-  ("list_partitions", (RStringList "partitions", P0), 8, [],
+  ("list_partitions", (RStringList "partitions", []), 8, [],
+   [InitBasicFS, TestOutputList (
+      [["list_partitions"]], ["/dev/sda1"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["list_partitions"]], ["/dev/sda1"; "/dev/sda2"; "/dev/sda3"])],
    "list the partitions",
    "\
 List all the partitions detected on all block devices.
@@ -270,7 +410,15 @@ The full partition device names are returned, eg. C</dev/sda1>
 This does not return logical volumes.  For that you will need to
 call C<guestfs_lvs>.");
 
-  ("pvs", (RStringList "physvols", P0), 9, [],
+  ("pvs", (RStringList "physvols", []), 9, [],
+   [InitBasicFSonLVM, TestOutputList (
+      [["pvs"]], ["/dev/sda1"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["pvs"]], ["/dev/sda1"; "/dev/sda2"; "/dev/sda3"])],
    "list the LVM physical volumes (PVs)",
    "\
 List all the physical volumes detected.  This is the equivalent
@@ -281,7 +429,17 @@ PVs (eg. C</dev/sda2>).
 
 See also C<guestfs_pvs_full>.");
 
-  ("vgs", (RStringList "volgroups", P0), 10, [],
+  ("vgs", (RStringList "volgroups", []), 10, [],
+   [InitBasicFSonLVM, TestOutputList (
+      [["vgs"]], ["VG"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["vgcreate"; "VG1"; "/dev/sda1 /dev/sda2"];
+       ["vgcreate"; "VG2"; "/dev/sda3"];
+       ["vgs"]], ["VG1"; "VG2"])],
    "list the LVM volume groups (VGs)",
    "\
 List all the volumes groups detected.  This is the equivalent
@@ -292,7 +450,20 @@ detected (eg. C<VolGroup00>).
 
 See also C<guestfs_vgs_full>.");
 
-  ("lvs", (RStringList "logvols", P0), 11, [],
+  ("lvs", (RStringList "logvols", []), 11, [],
+   [InitBasicFSonLVM, TestOutputList (
+      [["lvs"]], ["/dev/VG/LV"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["vgcreate"; "VG1"; "/dev/sda1 /dev/sda2"];
+       ["vgcreate"; "VG2"; "/dev/sda3"];
+       ["lvcreate"; "LV1"; "VG1"; "50"];
+       ["lvcreate"; "LV2"; "VG1"; "50"];
+       ["lvcreate"; "LV3"; "VG2"; "50"];
+       ["lvs"]], ["/dev/VG1/LV1"; "/dev/VG1/LV2"; "/dev/VG2/LV3"])],
    "list the LVM logical volumes (LVs)",
    "\
 List all the logical volumes detected.  This is the equivalent
@@ -303,25 +474,37 @@ This returns a list of the logical volume device names
 
 See also C<guestfs_lvs_full>.");
 
-  ("pvs_full", (RPVList "physvols", P0), 12, [],
+  ("pvs_full", (RPVList "physvols", []), 12, [],
+   [InitBasicFSonLVM, TestOutputLength (
+      [["pvs"]], 1)],
    "list the LVM physical volumes (PVs)",
    "\
 List all the physical volumes detected.  This is the equivalent
 of the L<pvs(8)> command.  The \"full\" version includes all fields.");
 
-  ("vgs_full", (RVGList "volgroups", P0), 13, [],
+  ("vgs_full", (RVGList "volgroups", []), 13, [],
+   [InitBasicFSonLVM, TestOutputLength (
+      [["pvs"]], 1)],
    "list the LVM volume groups (VGs)",
    "\
 List all the volumes groups detected.  This is the equivalent
 of the L<vgs(8)> command.  The \"full\" version includes all fields.");
 
-  ("lvs_full", (RLVList "logvols", P0), 14, [],
+  ("lvs_full", (RLVList "logvols", []), 14, [],
+   [InitBasicFSonLVM, TestOutputLength (
+      [["pvs"]], 1)],
    "list the LVM logical volumes (LVs)",
    "\
 List all the logical volumes detected.  This is the equivalent
 of the L<lvs(8)> command.  The \"full\" version includes all fields.");
 
-  ("read_lines", (RStringList "lines", P1 (String "path")), 15, [],
+  ("read_lines", (RStringList "lines", [String "path"]), 15, [],
+   [InitBasicFS, TestOutputList (
+      [["write_file"; "/new"; "line1\r\nline2\nline3"; "0"];
+       ["read_lines"; "/new"]], ["line1"; "line2"; "line3"]);
+    InitBasicFS, TestOutputList (
+      [["write_file"; "/new"; ""; "0"];
+       ["read_lines"; "/new"]], [])],
    "read file as lines",
    "\
 Return the contents of the file named C<path>.
@@ -334,7 +517,8 @@ Note that this function cannot correctly handle binary files
 as end of line).  For those you need to use the C<guestfs_read_file>
 function which has a more complex interface.");
 
-  ("aug_init", (Err, P2 (String "root", Int "flags")), 16, [],
+  ("aug_init", (RErr, [String "root"; Int "flags"]), 16, [],
+   [], (* XXX Augeas code needs tests. *)
    "create a new Augeas handle",
    "\
 Create a new Augeas handle for editing configuration files.
@@ -384,7 +568,8 @@ To close the handle, you can call C<guestfs_aug_close>.
 
 To find out more about Augeas, see L<http://augeas.net/>.");
 
-  ("aug_close", (Err, P0), 26, [],
+  ("aug_close", (RErr, []), 26, [],
+   [], (* XXX Augeas code needs tests. *)
    "close the current Augeas handle",
    "\
 Close the current Augeas handle and free up any resources
@@ -392,7 +577,8 @@ used by it.  After calling this, you have to call
 C<guestfs_aug_init> again before you can use any other
 Augeas functions.");
 
-  ("aug_defvar", (RInt "nrnodes", P2 (String "name", OptString "expr")), 17, [],
+  ("aug_defvar", (RInt "nrnodes", [String "name"; OptString "expr"]), 17, [],
+   [], (* XXX Augeas code needs tests. *)
    "define an Augeas variable",
    "\
 Defines an Augeas variable C<name> whose value is the result
@@ -402,7 +588,8 @@ undefined.
 On success this returns the number of nodes in C<expr>, or
 C<0> if C<expr> evaluates to something which is not a nodeset.");
 
-  ("aug_defnode", (RIntBool ("nrnodes", "created"), P3 (String "name", String "expr", String "val")), 18, [],
+  ("aug_defnode", (RIntBool ("nrnodes", "created"), [String "name"; String "expr"; String "val"]), 18, [],
+   [], (* XXX Augeas code needs tests. *)
    "define an Augeas node",
    "\
 Defines a variable C<name> whose value is the result of
@@ -416,18 +603,21 @@ On success this returns a pair containing the
 number of nodes in the nodeset, and a boolean flag
 if a node was created.");
 
-  ("aug_get", (RString "val", P1 (String "path")), 19, [],
+  ("aug_get", (RString "val", [String "path"]), 19, [],
+   [], (* XXX Augeas code needs tests. *)
    "look up the value of an Augeas path",
    "\
 Look up the value associated with C<path>.  If C<path>
 matches exactly one node, the C<value> is returned.");
 
-  ("aug_set", (Err, P2 (String "path", String "val")), 20, [],
+  ("aug_set", (RErr, [String "path"; String "val"]), 20, [],
+   [], (* XXX Augeas code needs tests. *)
    "set Augeas path to value",
    "\
 Set the value associated with C<path> to C<value>.");
 
-  ("aug_insert", (Err, P3 (String "path", String "label", Bool "before")), 21, [],
+  ("aug_insert", (RErr, [String "path"; String "label"; Bool "before"]), 21, [],
+   [], (* XXX Augeas code needs tests. *)
    "insert a sibling Augeas node",
    "\
 Create a new sibling C<label> for C<path>, inserting it into
@@ -438,27 +628,31 @@ C<path> must match exactly one existing node in the tree, and
 C<label> must be a label, ie. not contain C</>, C<*> or end
 with a bracketed index C<[N]>.");
 
-  ("aug_rm", (RInt "nrnodes", P1 (String "path")), 22, [],
+  ("aug_rm", (RInt "nrnodes", [String "path"]), 22, [],
+   [], (* XXX Augeas code needs tests. *)
    "remove an Augeas path",
    "\
 Remove C<path> and all of its children.
 
 On success this returns the number of entries which were removed.");
 
-  ("aug_mv", (Err, P2 (String "src", String "dest")), 23, [],
+  ("aug_mv", (RErr, [String "src"; String "dest"]), 23, [],
+   [], (* XXX Augeas code needs tests. *)
    "move Augeas node",
    "\
 Move the node C<src> to C<dest>.  C<src> must match exactly
 one node.  C<dest> is overwritten if it exists.");
 
-  ("aug_match", (RStringList "matches", P1 (String "path")), 24, [],
+  ("aug_match", (RStringList "matches", [String "path"]), 24, [],
+   [], (* XXX Augeas code needs tests. *)
    "return Augeas nodes which match path",
    "\
 Returns a list of paths which match the path expression C<path>.
 The returned paths are sufficiently qualified so that they match
 exactly one node in the current tree.");
 
-  ("aug_save", (Err, P0), 25, [],
+  ("aug_save", (RErr, []), 25, [],
+   [], (* XXX Augeas code needs tests. *)
    "write all pending Augeas changes to disk",
    "\
 This writes all pending changes to disk.
@@ -466,7 +660,8 @@ This writes all pending changes to disk.
 The flags which were passed to C<guestfs_aug_init> affect exactly
 how files are saved.");
 
-  ("aug_load", (Err, P0), 27, [],
+  ("aug_load", (RErr, []), 27, [],
+   [], (* XXX Augeas code needs tests. *)
    "load files into the tree",
    "\
 Load files into the tree.
@@ -474,11 +669,287 @@ Load files into the tree.
 See C<aug_load> in the Augeas documentation for the full gory
 details.");
 
-  ("aug_ls", (RStringList "matches", P1 (String "path")), 28, [],
+  ("aug_ls", (RStringList "matches", [String "path"]), 28, [],
+   [], (* XXX Augeas code needs tests. *)
    "list Augeas nodes under a path",
    "\
 This is just a shortcut for listing C<guestfs_aug_match>
-C<path/*> and sorting the files into alphabetical order.");
+C<path/*> and sorting the resulting nodes into alphabetical order.");
+
+  ("rm", (RErr, [String "path"]), 29, [],
+   [InitBasicFS, TestRun
+      [["touch"; "/new"];
+       ["rm"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["rm"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["mkdir"; "/new"];
+       ["rm"; "/new"]]],
+   "remove a file",
+   "\
+Remove the single file C<path>.");
+
+  ("rmdir", (RErr, [String "path"]), 30, [],
+   [InitBasicFS, TestRun
+      [["mkdir"; "/new"];
+       ["rmdir"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["rmdir"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["touch"; "/new"];
+       ["rmdir"; "/new"]]],
+   "remove a directory",
+   "\
+Remove the single directory C<path>.");
+
+  ("rm_rf", (RErr, [String "path"]), 31, [],
+   [InitBasicFS, TestOutputFalse
+      [["mkdir"; "/new"];
+       ["mkdir"; "/new/foo"];
+       ["touch"; "/new/foo/bar"];
+       ["rm_rf"; "/new"];
+       ["exists"; "/new"]]],
+   "remove a file or directory recursively",
+   "\
+Remove the file or directory C<path>, recursively removing the
+contents if its a directory.  This is like the C<rm -rf> shell
+command.");
+
+  ("mkdir", (RErr, [String "path"]), 32, [],
+   [InitBasicFS, TestOutputTrue
+      [["mkdir"; "/new"];
+       ["is_dir"; "/new"]];
+    InitBasicFS, TestLastFail
+      [["mkdir"; "/new/foo/bar"]]],
+   "create a directory",
+   "\
+Create a directory named C<path>.");
+
+  ("mkdir_p", (RErr, [String "path"]), 33, [],
+   [InitBasicFS, TestOutputTrue
+      [["mkdir_p"; "/new/foo/bar"];
+       ["is_dir"; "/new/foo/bar"]];
+    InitBasicFS, TestOutputTrue
+      [["mkdir_p"; "/new/foo/bar"];
+       ["is_dir"; "/new/foo"]];
+    InitBasicFS, TestOutputTrue
+      [["mkdir_p"; "/new/foo/bar"];
+       ["is_dir"; "/new"]]],
+   "create a directory and parents",
+   "\
+Create a directory named C<path>, creating any parent directories
+as necessary.  This is like the C<mkdir -p> shell command.");
+
+  ("chmod", (RErr, [Int "mode"; String "path"]), 34, [],
+   [], (* XXX Need stat command to test *)
+   "change file mode",
+   "\
+Change the mode (permissions) of C<path> to C<mode>.  Only
+numeric modes are supported.");
+
+  ("chown", (RErr, [Int "owner"; Int "group"; String "path"]), 35, [],
+   [], (* XXX Need stat command to test *)
+   "change file owner and group",
+   "\
+Change the file owner to C<owner> and group to C<group>.
+
+Only numeric uid and gid are supported.  If you want to use
+names, you will need to locate and parse the password file
+yourself (Augeas support makes this relatively easy).");
+
+  ("exists", (RBool "existsflag", [String "path"]), 36, [],
+   [InitBasicFS, TestOutputTrue (
+      [["touch"; "/new"];
+       ["exists"; "/new"]]);
+    InitBasicFS, TestOutputTrue (
+      [["mkdir"; "/new"];
+       ["exists"; "/new"]])],
+   "test if file or directory exists",
+   "\
+This returns C<true> if and only if there is a file, directory
+(or anything) with the given C<path> name.
+
+See also C<guestfs_is_file>, C<guestfs_is_dir>, C<guestfs_stat>.");
+
+  ("is_file", (RBool "fileflag", [String "path"]), 37, [],
+   [InitBasicFS, TestOutputTrue (
+      [["touch"; "/new"];
+       ["is_file"; "/new"]]);
+    InitBasicFS, TestOutputFalse (
+      [["mkdir"; "/new"];
+       ["is_file"; "/new"]])],
+   "test if file exists",
+   "\
+This returns C<true> if and only if there is a file
+with the given C<path> name.  Note that it returns false for
+other objects like directories.
+
+See also C<guestfs_stat>.");
+
+  ("is_dir", (RBool "dirflag", [String "path"]), 38, [],
+   [InitBasicFS, TestOutputFalse (
+      [["touch"; "/new"];
+       ["is_dir"; "/new"]]);
+    InitBasicFS, TestOutputTrue (
+      [["mkdir"; "/new"];
+       ["is_dir"; "/new"]])],
+   "test if file exists",
+   "\
+This returns C<true> if and only if there is a directory
+with the given C<path> name.  Note that it returns false for
+other objects like files.
+
+See also C<guestfs_stat>.");
+
+  ("pvcreate", (RErr, [String "device"]), 39, [],
+   [InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["pvs"]], ["/dev/sda1"; "/dev/sda2"; "/dev/sda3"])],
+   "create an LVM physical volume",
+   "\
+This creates an LVM physical volume on the named C<device>,
+where C<device> should usually be a partition name such
+as C</dev/sda1>.");
+
+  ("vgcreate", (RErr, [String "volgroup"; StringList "physvols"]), 40, [],
+   [InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["vgcreate"; "VG1"; "/dev/sda1 /dev/sda2"];
+       ["vgcreate"; "VG2"; "/dev/sda3"];
+       ["vgs"]], ["VG1"; "VG2"])],
+   "create an LVM volume group",
+   "\
+This creates an LVM volume group called C<volgroup>
+from the non-empty list of physical volumes C<physvols>.");
+
+  ("lvcreate", (RErr, [String "logvol"; String "volgroup"; Int "mbytes"]), 41, [],
+   [InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["pvcreate"; "/dev/sda1"];
+       ["pvcreate"; "/dev/sda2"];
+       ["pvcreate"; "/dev/sda3"];
+       ["vgcreate"; "VG1"; "/dev/sda1 /dev/sda2"];
+       ["vgcreate"; "VG2"; "/dev/sda3"];
+       ["lvcreate"; "LV1"; "VG1"; "50"];
+       ["lvcreate"; "LV2"; "VG1"; "50"];
+       ["lvcreate"; "LV3"; "VG2"; "50"];
+       ["lvcreate"; "LV4"; "VG2"; "50"];
+       ["lvcreate"; "LV5"; "VG2"; "50"];
+       ["lvs"]],
+      ["/dev/VG1/LV1"; "/dev/VG1/LV2";
+       "/dev/VG2/LV3"; "/dev/VG2/LV4"; "/dev/VG2/LV5"])],
+   "create an LVM volume group",
+   "\
+This creates an LVM volume group called C<logvol>
+on the volume group C<volgroup>, with C<size> megabytes.");
+
+  ("mkfs", (RErr, [String "fstype"; String "device"]), 42, [],
+   [InitEmpty, TestOutput (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["write_file"; "/new"; "new file contents"; "0"];
+       ["cat"; "/new"]], "new file contents")],
+   "make a filesystem",
+   "\
+This creates a filesystem on C<device> (usually a partition
+of LVM logical volume).  The filesystem type is C<fstype>, for
+example C<ext3>.");
+
+  ("sfdisk", (RErr, [String "device";
+                    Int "cyls"; Int "heads"; Int "sectors";
+                    StringList "lines"]), 43, [DangerWillRobinson],
+   [],
+   "create partitions on a block device",
+   "\
+This is a direct interface to the L<sfdisk(8)> program for creating
+partitions on block devices.
+
+C<device> should be a block device, for example C</dev/sda>.
+
+C<cyls>, C<heads> and C<sectors> are the number of cylinders, heads
+and sectors on the device, which are passed directly to sfdisk as
+the I<-C>, I<-H> and I<-S> parameters.  If you pass C<0> for any
+of these, then the corresponding parameter is omitted.  Usually for
+'large' disks, you can just pass C<0> for these, but for small
+(floppy-sized) disks, sfdisk (or rather, the kernel) cannot work
+out the right geometry and you will need to tell it.
+
+C<lines> is a list of lines that we feed to C<sfdisk>.  For more
+information refer to the L<sfdisk(8)> manpage.
+
+To create a single partition occupying the whole disk, you would
+pass C<lines> as a single element list, when the single element being
+the string C<,> (comma).");
+
+  ("write_file", (RErr, [String "path"; String "content"; Int "size"]), 44, [ProtocolLimitWarning],
+   [InitEmpty, TestOutput (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["write_file"; "/new"; "new file contents"; "0"];
+       ["cat"; "/new"]], "new file contents")],
+   "create a file",
+   "\
+This call creates a file called C<path>.  The contents of the
+file is the string C<content> (which can contain any 8 bit data),
+with length C<size>.
+
+As a special case, if C<size> is C<0>
+then the length is calculated using C<strlen> (so in this case
+the content cannot contain embedded ASCII NULs).");
+
+  ("umount", (RErr, [String "pathordevice"]), 45, [FishAlias "unmount"],
+   [InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["mounts"]], ["/dev/sda1"]);
+    InitEmpty, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["umount"; "/"];
+       ["mounts"]], [])],
+   "unmount a filesystem",
+   "\
+This unmounts the given filesystem.  The filesystem may be
+specified either by its mountpoint (path) or the device which
+contains the filesystem.");
+
+  ("mounts", (RStringList "devices", []), 46, [],
+   [InitBasicFS, TestOutputList (
+      [["mounts"]], ["/dev/sda1"])],
+   "show mounted filesystems",
+   "\
+This returns the list of currently mounted filesystems.  It returns
+the list of devices (eg. C</dev/sda1>, C</dev/VG/LV>).
+
+Some internal mounts are not shown.");
+
+  ("umount_all", (RErr, []), 47, [FishAlias "unmount-all"],
+   [InitBasicFS, TestOutputList (
+      [["umount_all"];
+       ["mounts"]], [])],
+   "unmount all filesystems",
+   "\
+This unmounts all mounted filesystems.
+
+Some internal mounts are not unmounted by this call.");
+
+  ("lvm_remove_all", (RErr, []), 48, [DangerWillRobinson],
+   [],
+   "remove all LVM LVs, VGs and PVs",
+   "\
+This command removes all LVM logical volumes, volume groups
+and physical volumes.");
+
 ]
 
 let all_functions = non_daemon_functions @ daemon_functions
@@ -487,7 +958,8 @@ let all_functions = non_daemon_functions @ daemon_functions
  * alphabetically, so this is useful:
  *)
 let all_functions_sorted =
-  List.sort (fun (n1,_,_,_,_,_) (n2,_,_,_,_,_) -> compare n1 n2) all_functions
+  List.sort (fun (n1,_,_,_,_,_,_) (n2,_,_,_,_,_,_) ->
+              compare n1 n2) all_functions
 
 (* Column names and types from LVM PVs/VGs/LVs. *)
 let pv_cols = [
@@ -599,6 +1071,17 @@ let rec replace_str s s1 s2 =
     s' ^ s2 ^ replace_str s'' s1 s2
   )
 
+let rec string_split sep str =
+  let len = String.length str in
+  let seplen = String.length sep in
+  let i = find str sep in
+  if i = -1 then [str]
+  else (
+    let s' = String.sub str 0 i in
+    let s'' = String.sub str (i+seplen) (len-i-seplen) in
+    s' :: string_split sep s''
+  )
+
 let rec find_map f = function
   | [] -> raise Not_found
   | x :: xs ->
@@ -613,59 +1096,101 @@ let iteri f xs =
   in
   loop 0 xs
 
-(* 'pr' prints to the current output file. *)
-let chan = ref stdout
-let pr fs = ksprintf (output_string !chan) fs
+let mapi f xs =
+  let rec loop i = function
+    | [] -> []
+    | x :: xs -> let r = f i x in r :: loop (i+1) xs
+  in
+  loop 0 xs
 
-let iter_args f = function
-  | P0 -> ()
-  | P1 arg1 -> f arg1
-  | P2 (arg1, arg2) -> f arg1; f arg2
-  | P3 (arg1, arg2, arg3) -> f arg1; f arg2; f arg3
+let name_of_argt = function
+  | String n | OptString n | StringList n | Bool n | Int n -> n
+
+(* Check function names etc. for consistency. *)
+let check_functions () =
+  let contains_uppercase str =
+    let len = String.length str in
+    let rec loop i =
+      if i >= len then false
+      else (
+       let c = str.[i] in
+       if c >= 'A' && c <= 'Z' then true
+       else loop (i+1)
+      )
+    in
+    loop 0
+  in
 
-let iteri_args f = function
-  | P0 -> ()
-  | P1 arg1 -> f 0 arg1
-  | P2 (arg1, arg2) -> f 0 arg1; f 1 arg2
-  | P3 (arg1, arg2, arg3) -> f 0 arg1; f 1 arg2; f 2 arg3
+  (* Check function names. *)
+  List.iter (
+    fun (name, _, _, _, _, _, _) ->
+      if String.length name >= 7 && String.sub name 0 7 = "guestfs" then
+       failwithf "function name %s does not need 'guestfs' prefix" name;
+      if contains_uppercase name then
+       failwithf "function name %s should not contain uppercase chars" name;
+      if String.contains name '-' then
+       failwithf "function name %s should not contain '-', use '_' instead."
+         name
+  ) all_functions;
 
-let map_args f = function
-  | P0 -> []
-  | P1 arg1 -> [f arg1]
-  | P2 (arg1, arg2) ->
-      let n1 = f arg1 in let n2 = f arg2 in [n1; n2]
-  | P3 (arg1, arg2, arg3) ->
-      let n1 = f arg1 in let n2 = f arg2 in let n3 = f arg3 in [n1; n2; n3]
+  (* Check function parameter/return names. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      let check_arg_ret_name n =
+       if contains_uppercase n then
+         failwithf "%s param/ret %s should not contain uppercase chars"
+           name n;
+       if String.contains n '-' || String.contains n '_' then
+         failwithf "%s param/ret %s should not contain '-' or '_'"
+           name n;
+       if n = "value" then
+         failwithf "%s has a param/ret called 'value', which causes conflicts in the OCaml bindings, use something like 'val' or a more descriptive name" n
+      in
 
-let nr_args = function | P0 -> 0 | P1 _ -> 1 | P2 _ -> 2 | P3 _ -> 3
+      (match fst style with
+       | RErr -> ()
+       | RInt n | RBool n | RConstString n | RString n
+       | RStringList n | RPVList n | RVGList n | RLVList n ->
+          check_arg_ret_name n
+       | RIntBool (n,m) ->
+          check_arg_ret_name n;
+          check_arg_ret_name m
+      );
+      List.iter (fun arg -> check_arg_ret_name (name_of_argt arg)) (snd style)
+  ) all_functions;
 
-let name_of_argt = function String n | OptString n | Bool n | Int n -> n
+  (* Check short descriptions. *)
+  List.iter (
+    fun (name, _, _, _, _, shortdesc, _) ->
+      if shortdesc.[0] <> Char.lowercase shortdesc.[0] then
+       failwithf "short description of %s should begin with lowercase." name;
+      let c = shortdesc.[String.length shortdesc-1] in
+      if c = '\n' || c = '.' then
+       failwithf "short description of %s should not end with . or \\n." name
+  ) all_functions;
 
-(* Check function names etc. for consistency. *)
-let check_functions () =
+  (* Check long dscriptions. *)
   List.iter (
-    fun (name, _, _, _, _, longdesc) ->
-      if String.contains name '-' then
-       failwithf "function name '%s' should not contain '-', use '_' instead."
-         name;
+    fun (name, _, _, _, _, _, longdesc) ->
       if longdesc.[String.length longdesc-1] = '\n' then
        failwithf "long description of %s should not end with \\n." name
   ) all_functions;
 
+  (* Check proc_nrs. *)
   List.iter (
-    fun (name, _, proc_nr, _, _, _) ->
+    fun (name, _, proc_nr, _, _, _, _) ->
       if proc_nr <= 0 then
        failwithf "daemon function %s should have proc_nr > 0" name
   ) daemon_functions;
 
   List.iter (
-    fun (name, _, proc_nr, _, _, _) ->
+    fun (name, _, proc_nr, _, _, _, _) ->
       if proc_nr <> -1 then
        failwithf "non-daemon function %s should have proc_nr -1" name
   ) non_daemon_functions;
 
   let proc_nrs =
-    List.map (fun (name, _, proc_nr, _, _, _) -> name, proc_nr)
+    List.map (fun (name, _, proc_nr, _, _, _, _) -> name, proc_nr)
       daemon_functions in
   let proc_nrs =
     List.sort (fun (_,nr1) (_,nr2) -> compare nr1 nr2) proc_nrs in
@@ -675,16 +1200,20 @@ let check_functions () =
     | (name1,nr1) :: ((name2,nr2) :: _ as rest) when nr1 < nr2 ->
        loop rest
     | (name1,nr1) :: (name2,nr2) :: _ ->
-       failwithf "'%s' and '%s' have conflicting procedure numbers (%d, %d)"
+       failwithf "%s and %s have conflicting procedure numbers (%d, %d)"
          name1 name2 nr1 nr2
   in
   loop proc_nrs
 
+(* 'pr' prints to the current output file. *)
+let chan = ref stdout
+let pr fs = ksprintf (output_string !chan) fs
+
+(* Generate a header block in a number of standard styles. *)
 type comment_style = CStyle | HashStyle | OCamlStyle
 type license = GPLv2 | LGPLv2
 
-(* Generate a header block in a number of standard styles. *)
-let rec generate_header comment license =
+let generate_header comment license =
   let c = match comment with
     | CStyle ->     pr "/* "; " *"
     | HashStyle ->  pr "# ";  "#"
@@ -733,10 +1262,12 @@ let rec generate_header comment license =
   );
   pr "\n"
 
+(* Start of main code generation functions below this line. *)
+
 (* Generate the pod documentation for the C API. *)
-and generate_actions_pod () =
+let rec generate_actions_pod () =
   List.iter (
-    fun (shortname, style, _, flags, _, longdesc) ->
+    fun (shortname, style, _, flags, _, _, longdesc) ->
       let name = "guestfs_" ^ shortname in
       pr "=head2 %s\n\n" name;
       pr " ";
@@ -744,7 +1275,7 @@ and generate_actions_pod () =
       pr "\n\n";
       pr "%s\n\n" longdesc;
       (match fst style with
-       | Err ->
+       | RErr ->
           pr "This function returns 0 on success or -1 on error.\n\n"
        | RInt _ ->
           pr "On error this function returns -1.\n\n"
@@ -762,21 +1293,21 @@ I<The caller must free the returned string after use>.\n\n"
 I<The caller must free the strings and the array after use>.\n\n"
        | RIntBool _ ->
           pr "This function returns a C<struct guestfs_int_bool *>.
-I<The caller must call C<guestfs_free_int_bool> after use.>.\n\n"
+I<The caller must call C<guestfs_free_int_bool> after use>.\n\n"
        | RPVList _ ->
           pr "This function returns a C<struct guestfs_lvm_pv_list *>.
-I<The caller must call C<guestfs_free_lvm_pv_list> after use.>.\n\n"
+I<The caller must call C<guestfs_free_lvm_pv_list> after use>.\n\n"
        | RVGList _ ->
           pr "This function returns a C<struct guestfs_lvm_vg_list *>.
-I<The caller must call C<guestfs_free_lvm_vg_list> after use.>.\n\n"
+I<The caller must call C<guestfs_free_lvm_vg_list> after use>.\n\n"
        | RLVList _ ->
           pr "This function returns a C<struct guestfs_lvm_lv_list *>.
-I<The caller must call C<guestfs_free_lvm_lv_list> after use.>.\n\n"
+I<The caller must call C<guestfs_free_lvm_lv_list> after use>.\n\n"
       );
       if List.mem ProtocolLimitWarning flags then
-       pr "Because of the message protocol, there is a transfer limit 
-of somewhere between 2MB and 4MB.  To transfer large files you should use
-FTP.\n\n";
+       pr "%s\n\n" protocol_limit_warning;
+      if List.mem DangerWillRobinson flags then
+       pr "%s\n\n" danger_will_robinson;
   ) all_functions_sorted
 
 and generate_structs_pod () =
@@ -810,8 +1341,9 @@ and generate_structs_pod () =
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
 
 (* Generate the protocol (XDR) file, 'guestfs_protocol.x' and
- * indirectly 'guestfs_protocol.h' and 'guestfs_protocol.c'.  We
- * have to use an underscore instead of a dash because otherwise
+ * indirectly 'guestfs_protocol.h' and 'guestfs_protocol.c'.
+ *
+ * We have to use an underscore instead of a dash because otherwise
  * rpcgen generates incorrect code.
  *
  * This header is NOT exported to clients, but see also generate_structs_h.
@@ -842,24 +1374,25 @@ and generate_xdr () =
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
   List.iter (
-    fun(shortname, style, _, _, _, _) ->
+    fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
 
       (match snd style with
-       | P0 -> ()
+       | [] -> ()
        | args ->
           pr "struct %s_args {\n" name;
-          iter_args (
+          List.iter (
             function
             | String n -> pr "  string %s<>;\n" n
             | OptString n -> pr "  str *%s;\n" n
+            | StringList n -> pr "  str %s<>;\n" n
             | Bool n -> pr "  bool %s;\n" n
             | Int n -> pr "  int %s;\n" n
           ) args;
           pr "};\n\n"
       );
       (match fst style with
-       | Err -> ()
+       | RErr -> ()
        | RInt n ->
           pr "struct %s_ret {\n" name;
           pr "  int %s;\n" n;
@@ -901,7 +1434,7 @@ and generate_xdr () =
   (* Table of procedure numbers. *)
   pr "enum guestfs_procedure {\n";
   List.iter (
-    fun (shortname, _, proc_nr, _, _, _) ->
+    fun (shortname, _, proc_nr, _, _, _, _) ->
       pr "  GUESTFS_PROC_%s = %d,\n" (String.uppercase shortname) proc_nr
   ) daemon_functions;
   pr "  GUESTFS_PROC_dummy\n"; (* so we don't have a "hanging comma" *)
@@ -999,7 +1532,7 @@ and generate_structs_h () =
 and generate_actions_h () =
   generate_header CStyle LGPLv2;
   List.iter (
-    fun (shortname, style, _, _, _, _) ->
+    fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
       generate_prototype ~single_line:true ~newline:true ~handle:"handle"
        name style
@@ -1011,7 +1544,7 @@ and generate_client_actions () =
 
   (* Client-side stubs for each function. *)
   List.iter (
-    fun (shortname, style, _, _, _, _) ->
+    fun (shortname, style, _, _, _, _, _) ->
       let name = "guestfs_" ^ shortname in
 
       (* Generate the return value struct. *)
@@ -1020,7 +1553,7 @@ and generate_client_actions () =
       pr "  struct guestfs_message_header hdr;\n";
       pr "  struct guestfs_message_error err;\n";
       (match fst style with
-       | Err -> ()
+       | RErr -> ()
        | RConstString _ ->
           failwithf "RConstString cannot be returned from a daemon function"
        | RInt _
@@ -1049,7 +1582,7 @@ and generate_client_actions () =
       pr "  }\n";
 
       (match fst style with
-       | Err -> ()
+       | RErr -> ()
        | RConstString _ ->
           failwithf "RConstString cannot be returned from a daemon function"
        | RInt _
@@ -1073,7 +1606,7 @@ and generate_client_actions () =
 
       let error_code =
        match fst style with
-       | Err | RInt _ | RBool _ -> "-1"
+       | RErr | RInt _ | RBool _ -> "-1"
        | RConstString _ ->
            failwithf "RConstString cannot be returned from a daemon function"
        | RString _ | RStringList _ | RIntBool _
@@ -1083,7 +1616,7 @@ and generate_client_actions () =
       pr "{\n";
 
       (match snd style with
-       | P0 -> ()
+       | [] -> ()
        | _ -> pr "  struct %s_args args;\n" name
       );
 
@@ -1101,16 +1634,19 @@ and generate_client_actions () =
       pr "\n";
 
       (match snd style with
-       | P0 ->
+       | [] ->
           pr "  serial = dispatch (g, GUESTFS_PROC_%s, NULL, NULL);\n"
             (String.uppercase shortname)
        | args ->
-          iter_args (
+          List.iter (
             function
             | String n ->
                 pr "  args.%s = (char *) %s;\n" n n
             | OptString n ->
                 pr "  args.%s = %s ? (char **) &%s : NULL;\n" n n n
+            | StringList n ->
+                pr "  args.%s.%s_val = (char **) %s;\n" n n n;
+                pr "  for (args.%s.%s_len = 0; %s[args.%s.%s_len]; args.%s.%s_len++) ;\n" n n n n n n n;
             | Bool n ->
                 pr "  args.%s = %s;\n" n n
             | Int n ->
@@ -1149,7 +1685,7 @@ and generate_client_actions () =
       pr "\n";
 
       (match fst style with
-       | Err -> pr "  return 0;\n"
+       | RErr -> pr "  return 0;\n"
        | RInt n
        | RBool n -> pr "  return rv.ret.%s;\n" n
        | RConstString _ ->
@@ -1189,7 +1725,7 @@ and generate_daemon_actions_h () =
   pr "\n";
 
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
        generate_prototype
          ~single_line:true ~newline:true ~in_daemon:true ~prefix:"do_"
          name style;
@@ -1215,13 +1751,13 @@ and generate_daemon_actions () =
   pr "\n";
 
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
       (* Generate server-side stubs. *)
       pr "static void %s_stub (XDR *xdr_in)\n" name;
       pr "{\n";
       let error_code =
        match fst style with
-       | Err | RInt _ -> pr "  int r;\n"; "-1"
+       | RErr | RInt _ -> pr "  int r;\n"; "-1"
        | RBool _ -> pr "  int r;\n"; "-1"
        | RConstString _ ->
            failwithf "RConstString cannot be returned from a daemon function"
@@ -1233,13 +1769,14 @@ and generate_daemon_actions () =
        | RLVList _ -> pr "  guestfs_lvm_int_lv_list *r;\n"; "NULL" in
 
       (match snd style with
-       | P0 -> ()
+       | [] -> ()
        | args ->
           pr "  struct guestfs_%s_args args;\n" name;
-          iter_args (
+          List.iter (
             function
             | String n
             | OptString n -> pr "  const char *%s;\n" n
+            | StringList n -> pr "  char **%s;\n" n
             | Bool n -> pr "  int %s;\n" n
             | Int n -> pr "  int %s;\n" n
           ) args
@@ -1247,7 +1784,7 @@ and generate_daemon_actions () =
       pr "\n";
 
       (match snd style with
-       | P0 -> ()
+       | [] -> ()
        | args ->
           pr "  memset (&args, 0, sizeof args);\n";
           pr "\n";
@@ -1255,10 +1792,14 @@ and generate_daemon_actions () =
           pr "    reply_with_error (\"%%s: daemon failed to decode procedure arguments\", \"%s\");\n" name;
           pr "    return;\n";
           pr "  }\n";
-          iter_args (
+          List.iter (
             function
             | String n -> pr "  %s = args.%s;\n" n n
             | OptString n -> pr "  %s = args.%s ? *args.%s : NULL;\n" n n n
+            | StringList n ->
+                pr "  args.%s.%s_val = realloc (args.%s.%s_val, sizeof (char *) * (args.%s.%s_len+1));\n" n n n n n n;
+                pr "  args.%s.%s_val[args.%s.%s_len] = NULL;\n" n n n n;
+                pr "  %s = args.%s.%s_val;\n" n n n
             | Bool n -> pr "  %s = args.%s;\n" n n
             | Int n -> pr "  %s = args.%s;\n" n n
           ) args;
@@ -1270,12 +1811,12 @@ and generate_daemon_actions () =
       pr ";\n";
 
       pr "  if (r == %s)\n" error_code;
-      pr "    /* do_%s has already called reply_with_error, so just return */\n" name;
-      pr "    return;\n";
+      pr "    /* do_%s has already called reply_with_error */\n" name;
+      pr "    goto done;\n";
       pr "\n";
 
       (match fst style with
-       | Err -> pr "  reply (NULL, NULL);\n"
+       | RErr -> pr "  reply (NULL, NULL);\n"
        | RInt n ->
           pr "  struct guestfs_%s_ret ret;\n" name;
           pr "  ret.%s = r;\n" n;
@@ -1317,6 +1858,16 @@ and generate_daemon_actions () =
           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n" name
       );
 
+      (* Free the args. *)
+      (match snd style with
+       | [] ->
+          pr "done: ;\n";
+       | _ ->
+          pr "done:\n";
+          pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_args, (char *) &args);\n"
+            name
+      );
+
       pr "}\n\n";
   ) daemon_functions;
 
@@ -1326,7 +1877,7 @@ and generate_daemon_actions () =
   pr "  switch (proc_nr) {\n";
 
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
        pr "    case GUESTFS_PROC_%s:\n" (String.uppercase name);
        pr "      %s_stub (xdr_in);\n" name;
        pr "      break;\n"
@@ -1503,17 +2054,475 @@ and generate_daemon_actions () =
 
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
 
+(* Generate the tests. *)
+and generate_tests () =
+  generate_header CStyle GPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <fcntl.h>
+
+#include \"guestfs.h\"
+
+static guestfs_h *g;
+static int suppress_error = 0;
+
+static void print_error (guestfs_h *g, void *data, const char *msg)
+{
+  if (!suppress_error)
+    fprintf (stderr, \"%%s\\n\", msg);
+}
+
+static void print_strings (char * const * const argv)
+{
+  int argc;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    printf (\"\\t%%s\\n\", argv[argc]);
+}
+
+";
+
+  let test_names =
+    List.map (
+      fun (name, _, _, _, tests, _, _) ->
+       mapi (generate_one_test name) tests
+    ) all_functions in
+  let test_names = List.concat test_names in
+  let nr_tests = List.length test_names in
+
+  pr "\
+int main (int argc, char *argv[])
+{
+  char c = 0;
+  int failed = 0;
+  const char *srcdir;
+  int fd;
+  char buf[256];
+
+  g = guestfs_create ();
+  if (g == NULL) {
+    printf (\"guestfs_create FAILED\\n\");
+    exit (1);
+  }
+
+  guestfs_set_error_handler (g, print_error, NULL);
+
+  srcdir = getenv (\"srcdir\");
+  if (!srcdir) srcdir = \".\";
+  guestfs_set_path (g, srcdir);
+
+  snprintf (buf, sizeof buf, \"%%s/test1.img\", srcdir);
+  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  if (fd == -1) {
+    perror (buf);
+    exit (1);
+  }
+  if (lseek (fd, %d, SEEK_SET) == -1) {
+    perror (\"lseek\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (write (fd, &c, 1) == -1) {
+    perror (\"write\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (close (fd) == -1) {
+    perror (buf);
+    unlink (buf);
+    exit (1);
+  }
+  if (guestfs_add_drive (g, buf) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
+    exit (1);
+  }
+
+  snprintf (buf, sizeof buf, \"%%s/test2.img\", srcdir);
+  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  if (fd == -1) {
+    perror (buf);
+    exit (1);
+  }
+  if (lseek (fd, %d, SEEK_SET) == -1) {
+    perror (\"lseek\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (write (fd, &c, 1) == -1) {
+    perror (\"write\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (close (fd) == -1) {
+    perror (buf);
+    unlink (buf);
+    exit (1);
+  }
+  if (guestfs_add_drive (g, buf) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
+    exit (1);
+  }
+
+  snprintf (buf, sizeof buf, \"%%s/test3.img\", srcdir);
+  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  if (fd == -1) {
+    perror (buf);
+    exit (1);
+  }
+  if (lseek (fd, %d, SEEK_SET) == -1) {
+    perror (\"lseek\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (write (fd, &c, 1) == -1) {
+    perror (\"write\");
+    close (fd);
+    unlink (buf);
+    exit (1);
+  }
+  if (close (fd) == -1) {
+    perror (buf);
+    unlink (buf);
+    exit (1);
+  }
+  if (guestfs_add_drive (g, buf) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
+    exit (1);
+  }
+
+  if (guestfs_launch (g) == -1) {
+    printf (\"guestfs_launch FAILED\\n\");
+    exit (1);
+  }
+  if (guestfs_wait_ready (g) == -1) {
+    printf (\"guestfs_wait_ready FAILED\\n\");
+    exit (1);
+  }
+
+" (500 * 1024 * 1024) (50 * 1024 * 1024) (10 * 1024 * 1024);
+
+  iteri (
+    fun i test_name ->
+      pr "  printf (\"%3d/%3d %s\\n\");\n" (i+1) nr_tests test_name;
+      pr "  if (%s () == -1) {\n" test_name;
+      pr "    printf (\"%s FAILED\\n\");\n" test_name;
+      pr "    failed++;\n";
+      pr "  }\n";
+  ) test_names;
+  pr "\n";
+
+  pr "  guestfs_close (g);\n";
+  pr "  snprintf (buf, sizeof buf, \"%%s/test1.img\", srcdir);\n";
+  pr "  unlink (buf);\n";
+  pr "  snprintf (buf, sizeof buf, \"%%s/test2.img\", srcdir);\n";
+  pr "  unlink (buf);\n";
+  pr "  snprintf (buf, sizeof buf, \"%%s/test3.img\", srcdir);\n";
+  pr "  unlink (buf);\n";
+  pr "\n";
+
+  pr "  if (failed > 0) {\n";
+  pr "    printf (\"***** %%d / %d tests FAILED *****\\n\", failed);\n"
+    nr_tests;
+  pr "    exit (1);\n";
+  pr "  }\n";
+  pr "\n";
+
+  pr "  exit (0);\n";
+  pr "}\n"
+
+and generate_one_test name i (init, test) =
+  let test_name = sprintf "test_%s_%d" name i in
+
+  pr "static int %s (void)\n" test_name;
+  pr "{\n";
+
+  (match init with
+   | InitNone -> ()
+   | InitEmpty ->
+       pr "  /* InitEmpty for %s (%d) */\n" name i;
+       List.iter (generate_test_command_call test_name)
+        [["umount_all"];
+         ["lvm_remove_all"]]
+   | InitBasicFS ->
+       pr "  /* InitBasicFS for %s (%d): create ext2 on /dev/sda1 */\n" name i;
+       List.iter (generate_test_command_call test_name)
+        [["umount_all"];
+         ["lvm_remove_all"];
+         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["mkfs"; "ext2"; "/dev/sda1"];
+         ["mount"; "/dev/sda1"; "/"]]
+   | InitBasicFSonLVM ->
+       pr "  /* InitBasicFSonLVM for %s (%d): create ext2 on /dev/VG/LV */\n"
+        name i;
+       List.iter (generate_test_command_call test_name)
+        [["umount_all"];
+         ["lvm_remove_all"];
+         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["pvcreate"; "/dev/sda1"];
+         ["vgcreate"; "VG"; "/dev/sda1"];
+         ["lvcreate"; "LV"; "VG"; "8"];
+         ["mkfs"; "ext2"; "/dev/VG/LV"];
+         ["mount"; "/dev/VG/LV"; "/"]]
+  );
+
+  let get_seq_last = function
+    | [] ->
+       failwithf "%s: you cannot use [] (empty list) when expecting a command"
+         test_name
+    | seq ->
+       let seq = List.rev seq in
+       List.rev (List.tl seq), List.hd seq
+  in
+
+  (match test with
+   | TestRun seq ->
+       pr "  /* TestRun for %s (%d) */\n" name i;
+       List.iter (generate_test_command_call test_name) seq
+   | TestOutput (seq, expected) ->
+       pr "  /* TestOutput for %s (%d) */\n" name i;
+       let seq, last = get_seq_last seq in
+       let test () =
+        pr "    if (strcmp (r, \"%s\") != 0) {\n" (c_quote expected);
+        pr "      fprintf (stderr, \"%s: expected \\\"%s\\\" but got \\\"%%s\\\"\\n\", r);\n" test_name (c_quote expected);
+        pr "      return -1;\n";
+        pr "    }\n"
+       in
+       List.iter (generate_test_command_call test_name) seq;
+       generate_test_command_call ~test test_name last
+   | TestOutputList (seq, expected) ->
+       pr "  /* TestOutputList for %s (%d) */\n" name i;
+       let seq, last = get_seq_last seq in
+       let test () =
+        iteri (
+          fun i str ->
+            pr "    if (!r[%d]) {\n" i;
+            pr "      fprintf (stderr, \"%s: short list returned from command\\n\");\n" test_name;
+            pr "      print_strings (r);\n";
+            pr "      return -1;\n";
+            pr "    }\n";
+            pr "    if (strcmp (r[%d], \"%s\") != 0) {\n" i (c_quote str);
+            pr "      fprintf (stderr, \"%s: expected \\\"%s\\\" but got \\\"%%s\\\"\\n\", r[%d]);\n" test_name (c_quote str) i;
+            pr "      return -1;\n";
+            pr "    }\n"
+        ) expected;
+        pr "    if (r[%d] != NULL) {\n" (List.length expected);
+        pr "      fprintf (stderr, \"%s: extra elements returned from command\\n\");\n"
+          test_name;
+        pr "      print_strings (r);\n";
+        pr "      return -1;\n";
+        pr "    }\n"
+       in
+       List.iter (generate_test_command_call test_name) seq;
+       generate_test_command_call ~test test_name last
+   | TestOutputInt (seq, expected) ->
+       pr "  /* TestOutputInt for %s (%d) */\n" name i;
+       let seq, last = get_seq_last seq in
+       let test () =
+        pr "    if (r != %d) {\n" expected;
+        pr "      fprintf (stderr, \"%s: expected %d but got %%d\\n\", r);\n"
+          test_name expected;
+        pr "      return -1;\n";
+        pr "    }\n"
+       in
+       List.iter (generate_test_command_call test_name) seq;
+       generate_test_command_call ~test test_name last
+   | TestOutputTrue seq ->
+       pr "  /* TestOutputTrue for %s (%d) */\n" name i;
+       let seq, last = get_seq_last seq in
+       let test () =
+        pr "    if (!r) {\n";
+        pr "      fprintf (stderr, \"%s: expected true, got false\\n\");\n"
+          test_name;
+        pr "      return -1;\n";
+        pr "    }\n"
+       in
+       List.iter (generate_test_command_call test_name) seq;
+       generate_test_command_call ~test test_name last
+   | TestOutputFalse seq ->
+       pr "  /* TestOutputFalse for %s (%d) */\n" name i;
+       let seq, last = get_seq_last seq in
+       let test () =
+        pr "    if (r) {\n";
+        pr "      fprintf (stderr, \"%s: expected false, got true\\n\");\n"
+          test_name;
+        pr "      return -1;\n";
+        pr "    }\n"
+       in
+       List.iter (generate_test_command_call test_name) seq;
+       generate_test_command_call ~test test_name last
+   | TestOutputLength (seq, expected) ->
+       pr "  /* TestOutputLength for %s (%d) */\n" name i;
+       let seq, last = get_seq_last seq in
+       let test () =
+        pr "    int j;\n";
+        pr "    for (j = 0; j < %d; ++j)\n" expected;
+        pr "      if (r[j] == NULL) {\n";
+        pr "        fprintf (stderr, \"%s: short list returned\\n\");\n"
+          test_name;
+        pr "        print_strings (r);\n";
+        pr "        return -1;\n";
+        pr "      }\n";
+        pr "    if (r[j] != NULL) {\n";
+        pr "      fprintf (stderr, \"%s: long list returned\\n\");\n"
+          test_name;
+        pr "      print_strings (r);\n";
+        pr "      return -1;\n";
+        pr "    }\n"
+       in
+       List.iter (generate_test_command_call test_name) seq;
+       generate_test_command_call ~test test_name last
+   | TestLastFail seq ->
+       pr "  /* TestLastFail for %s (%d) */\n" name i;
+       let seq, last = get_seq_last seq in
+       List.iter (generate_test_command_call test_name) seq;
+       generate_test_command_call test_name ~expect_error:true last
+  );
+
+  pr "  return 0;\n";
+  pr "}\n";
+  pr "\n";
+  test_name
+
+(* Generate the code to run a command, leaving the result in 'r'.
+ * If you expect to get an error then you should set expect_error:true.
+ *)
+and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
+  match cmd with
+  | [] -> assert false
+  | name :: args ->
+      (* Look up the command to find out what args/ret it has. *)
+      let style =
+       try
+         let _, style, _, _, _, _, _ =
+           List.find (fun (n, _, _, _, _, _, _) -> n = name) all_functions in
+         style
+       with Not_found ->
+         failwithf "%s: in test, command %s was not found" test_name name in
+
+      if List.length (snd style) <> List.length args then
+       failwithf "%s: in test, wrong number of args given to %s"
+         test_name name;
+
+      pr "  {\n";
+
+      List.iter (
+       function
+       | String _, _
+       | OptString _, _
+       | Int _, _
+       | Bool _, _ -> ()
+       | StringList n, arg ->
+           pr "    char *%s[] = {\n" n;
+           let strs = string_split " " arg in
+           List.iter (
+             fun str -> pr "      \"%s\",\n" (c_quote str)
+           ) strs;
+           pr "      NULL\n";
+           pr "    };\n";
+      ) (List.combine (snd style) args);
+
+      let error_code =
+       match fst style with
+       | RErr | RInt _ | RBool _ -> pr "    int r;\n"; "-1"
+       | RConstString _ -> pr "    const char *r;\n"; "NULL"
+       | RString _ -> pr "    char *r;\n"; "NULL"
+       | RStringList _ ->
+           pr "    char **r;\n";
+           pr "    int i;\n";
+           "NULL"
+       | RIntBool _ ->
+           pr "    struct guestfs_int_bool *r;\n";
+           "NULL"
+       | RPVList _ ->
+           pr "    struct guestfs_lvm_pv_list *r;\n";
+           "NULL"
+       | RVGList _ ->
+           pr "    struct guestfs_lvm_vg_list *r;\n";
+           "NULL"
+       | RLVList _ ->
+           pr "    struct guestfs_lvm_lv_list *r;\n";
+           "NULL" in
+
+      pr "    suppress_error = %d;\n" (if expect_error then 1 else 0);
+      pr "    r = guestfs_%s (g" name;
+
+      (* Generate the parameters. *)
+      List.iter (
+       function
+       | String _, arg -> pr ", \"%s\"" (c_quote arg)
+       | OptString _, arg ->
+           if arg = "NULL" then pr ", NULL" else pr ", \"%s\"" (c_quote arg)
+       | StringList n, _ ->
+           pr ", %s" n
+       | Int _, arg ->
+           let i =
+             try int_of_string arg
+             with Failure "int_of_string" ->
+               failwithf "%s: expecting an int, but got '%s'" test_name arg in
+           pr ", %d" i
+       | Bool _, arg ->
+           let b = bool_of_string arg in pr ", %d" (if b then 1 else 0)
+      ) (List.combine (snd style) args);
+
+      pr ");\n";
+      if not expect_error then
+       pr "    if (r == %s)\n" error_code
+      else
+       pr "    if (r != %s)\n" error_code;
+      pr "      return -1;\n";
+
+      (* Insert the test code. *)
+      (match test with
+       | None -> ()
+       | Some f -> f ()
+      );
+
+      (match fst style with
+       | RErr | RInt _ | RBool _ | RConstString _ -> ()
+       | RString _ -> pr "    free (r);\n"
+       | RStringList _ ->
+          pr "    for (i = 0; r[i] != NULL; ++i)\n";
+          pr "      free (r[i]);\n";
+          pr "    free (r);\n"
+       | RIntBool _ ->
+          pr "    guestfs_free_int_bool (r);\n"
+       | RPVList _ ->
+          pr "    guestfs_free_lvm_pv_list (r);\n"
+       | RVGList _ ->
+          pr "    guestfs_free_lvm_vg_list (r);\n"
+       | RLVList _ ->
+          pr "    guestfs_free_lvm_lv_list (r);\n"
+      );
+
+      pr "  }\n"
+
+and c_quote str =
+  let str = replace_str str "\r" "\\r" in
+  let str = replace_str str "\n" "\\n" in
+  let str = replace_str str "\t" "\\t" in
+  str
+
 (* Generate a lot of different functions for guestfish. *)
 and generate_fish_cmds () =
   generate_header CStyle GPLv2;
 
   let all_functions =
     List.filter (
-      fun (_, _, _, flags, _, _) -> not (List.mem NotInFish flags)
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
     ) all_functions in
   let all_functions_sorted =
     List.filter (
-      fun (_, _, _, flags, _, _) -> not (List.mem NotInFish flags)
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
     ) all_functions_sorted in
 
   pr "#include <stdio.h>\n";
@@ -1531,7 +2540,7 @@ and generate_fish_cmds () =
   pr "  printf (\"    %%-16s     %%s\\n\", \"Command\", \"Description\");\n";
   pr "  list_builtin_commands ();\n";
   List.iter (
-    fun (name, _, _, flags, shortdesc, _) ->
+    fun (name, _, _, flags, _, shortdesc, _) ->
       let name = replace_char name '_' '-' in
       pr "  printf (\"%%-20s %%s\\n\", \"%s\", \"%s\");\n"
        name shortdesc
@@ -1544,7 +2553,7 @@ and generate_fish_cmds () =
   pr "void display_command (const char *cmd)\n";
   pr "{\n";
   List.iter (
-    fun (name, style, _, flags, shortdesc, longdesc) ->
+    fun (name, style, _, flags, _, shortdesc, longdesc) ->
       let name2 = replace_char name '_' '-' in
       let alias =
        try find_map (function FishAlias n -> Some n | _ -> None) flags
@@ -1552,18 +2561,26 @@ and generate_fish_cmds () =
       let longdesc = replace_str longdesc "C<guestfs_" "C<" in
       let synopsis =
        match snd style with
-       | P0 -> name2
+       | [] -> name2
        | args ->
            sprintf "%s <%s>"
-             name2 (String.concat "> <" (map_args name_of_argt args)) in
+             name2 (String.concat "> <" (List.map name_of_argt args)) in
 
       let warnings =
        if List.mem ProtocolLimitWarning flags then
-         "\n\nBecause of the message protocol, there is a transfer limit 
-of somewhere between 2MB and 4MB.  To transfer large files you should use
-FTP."
+         ("\n\n" ^ protocol_limit_warning)
        else "" in
 
+      (* For DangerWillRobinson commands, we should probably have
+       * guestfish prompt before allowing you to use them (especially
+       * in interactive mode). XXX
+       *)
+      let warnings =
+       warnings ^
+         if List.mem DangerWillRobinson flags then
+           ("\n\n" ^ danger_will_robinson)
+         else "" in
+
       let describe_alias =
        if name <> alias then
          sprintf "\n\nYou can use '%s' as an alias for this command." alias
@@ -1626,11 +2643,11 @@ FTP."
 
   (* run_<action> actions *)
   List.iter (
-    fun (name, style, _, flags, _, _) ->
+    fun (name, style, _, flags, _, _, _) ->
       pr "static int run_%s (const char *cmd, int argc, char *argv[])\n" name;
       pr "{\n";
       (match fst style with
-       | Err
+       | RErr
        | RInt _
        | RBool _ -> pr "  int r;\n"
        | RConstString _ -> pr "  const char *r;\n"
@@ -1641,29 +2658,32 @@ FTP."
        | RVGList _ -> pr "  struct guestfs_lvm_vg_list *r;\n"
        | RLVList _ -> pr "  struct guestfs_lvm_lv_list *r;\n"
       );
-      iter_args (
+      List.iter (
        function
-       | String n -> pr "  const char *%s;\n" n
+       | String n
        | OptString n -> pr "  const char *%s;\n" n
+       | StringList n -> pr "  char **%s;\n" n
        | Bool n -> pr "  int %s;\n" n
        | Int n -> pr "  int %s;\n" n
       ) (snd style);
 
       (* Check and convert parameters. *)
-      let argc_expected = nr_args (snd style) in
+      let argc_expected = List.length (snd style) in
       pr "  if (argc != %d) {\n" argc_expected;
       pr "    fprintf (stderr, \"%%s should have %d parameter(s)\\n\", cmd);\n"
        argc_expected;
       pr "    fprintf (stderr, \"type 'help %%s' for help on %%s\\n\", cmd, cmd);\n";
       pr "    return -1;\n";
       pr "  }\n";
-      iteri_args (
+      iteri (
        fun i ->
          function
          | String name -> pr "  %s = argv[%d];\n" name i
          | OptString name ->
              pr "  %s = strcmp (argv[%d], \"\") != 0 ? argv[%d] : NULL;\n"
                name i i
+         | StringList name ->
+             pr "  %s = parse_string_list (argv[%d]);\n" name i
          | Bool name ->
              pr "  %s = is_true (argv[%d]) ? 1 : 0;\n" name i
          | Int name ->
@@ -1680,7 +2700,7 @@ FTP."
 
       (* Check return value for errors and display command results. *)
       (match fst style with
-       | Err -> pr "  return r;\n"
+       | RErr -> pr "  return r;\n"
        | RInt _ ->
           pr "  if (r == -1) return -1;\n";
           pr "  if (r) printf (\"%%d\\n\", r);\n";
@@ -1733,7 +2753,7 @@ FTP."
   pr "int run_action (const char *cmd, int argc, char *argv[])\n";
   pr "{\n";
   List.iter (
-    fun (name, _, _, flags, _, _) ->
+    fun (name, _, _, flags, _, _, _) ->
       let name2 = replace_char name '_' '-' in
       let alias =
        try find_map (function FishAlias n -> Some n | _ -> None) flags
@@ -1760,11 +2780,11 @@ FTP."
 and generate_fish_actions_pod () =
   let all_functions_sorted =
     List.filter (
-      fun (_, _, _, flags, _, _) -> not (List.mem NotInFish flags)
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
     ) all_functions_sorted in
 
   List.iter (
-    fun (name, style, _, flags, _, longdesc) ->
+    fun (name, style, _, flags, _, _, longdesc) ->
       let longdesc = replace_str longdesc "C<guestfs_" "C<" in
       let name = replace_char name '_' '-' in
       let alias =
@@ -1777,16 +2797,23 @@ and generate_fish_actions_pod () =
       pr "\n";
       pr "\n";
       pr " %s" name;
-      iter_args (
+      List.iter (
        function
        | String n -> pr " %s" n
        | OptString n -> pr " %s" n
+       | StringList n -> pr " %s,..." n
        | Bool _ -> pr " true|false"
        | Int n -> pr " %s" n
       ) (snd style);
       pr "\n";
       pr "\n";
-      pr "%s\n\n" longdesc
+      pr "%s\n\n" longdesc;
+
+      if List.mem ProtocolLimitWarning flags then
+       pr "%s\n\n" protocol_limit_warning;
+
+      if List.mem DangerWillRobinson flags then
+       pr "%s\n\n" danger_will_robinson
   ) all_functions_sorted
 
 (* Generate a C function prototype. *)
@@ -1797,7 +2824,7 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
   if extern then pr "extern ";
   if static then pr "static ";
   (match fst style with
-   | Err -> pr "int "
+   | RErr -> pr "int "
    | RInt _ -> pr "int "
    | RBool _ -> pr "int "
    | RConstString _ -> pr "const char *"
@@ -1817,7 +2844,7 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
        else pr "guestfs_lvm_int_lv_list *"
   );
   pr "%s%s (" prefix name;
-  if handle = None && nr_args (snd style) = 0 then
+  if handle = None && List.length (snd style) = 0 then
     pr "void"
   else (
     let comma = ref false in
@@ -1831,10 +2858,11 @@ and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
       );
       comma := true
     in
-    iter_args (
+    List.iter (
       function
       | String n -> next (); pr "const char *%s" n
       | OptString n -> next (); pr "const char *%s" n
+      | StringList n -> next (); pr "char * const* const %s" n
       | Bool n -> next (); pr "int %s" n
       | Int n -> next (); pr "int %s" n
     ) (snd style);
@@ -1851,14 +2879,15 @@ and generate_call_args ?handle style =
    | None -> ()
    | Some handle -> pr "%s" handle; comma := true
   );
-  iter_args (
+  List.iter (
     fun arg ->
       if !comma then pr ", ";
       comma := true;
       match arg with
-      | String n -> pr "%s" n
-      | OptString n -> pr "%s" n
-      | Bool n -> pr "%s" n
+      | String n
+      | OptString n
+      | StringList n
+      | Bool n
       | Int n -> pr "%s" n
   ) (snd style);
   pr ")"
@@ -1890,7 +2919,7 @@ val close : t -> unit
 
   (* The actions. *)
   List.iter (
-    fun (name, style, _, _, shortdesc, _) ->
+    fun (name, style, _, _, _, shortdesc, _) ->
       generate_ocaml_prototype name style;
       pr "(** %s *)\n" shortdesc;
       pr "\n"
@@ -1915,7 +2944,7 @@ let () =
 
   (* The actions. *)
   List.iter (
-    fun (name, style, _, _, shortdesc, _) ->
+    fun (name, style, _, _, _, shortdesc, _) ->
       generate_ocaml_prototype ~is_external:true name style;
   ) all_functions
 
@@ -2004,19 +3033,24 @@ and generate_ocaml_c () =
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
+      let params =
+       "gv" :: List.map (fun arg -> name_of_argt arg ^ "v") (snd style) in
+
       pr "CAMLprim value\n";
-      pr "ocaml_guestfs_%s (value gv" name;
-      iter_args (
-       fun arg -> pr ", value %sv" (name_of_argt arg)
-      ) (snd style);
+      pr "ocaml_guestfs_%s (value %s" name (List.hd params);
+      List.iter (pr ", value %s") (List.tl params);
       pr ")\n";
       pr "{\n";
-      pr "  CAMLparam%d (gv" (1 + (nr_args (snd style)));
-      iter_args (
-       fun arg -> pr ", %sv" (name_of_argt arg)
-      ) (snd style);
-      pr ");\n";
+
+      (match params with
+       | p1 :: p2 :: p3 :: p4 :: p5 :: rest ->
+          pr "  CAMLparam5 (%s);\n" (String.concat ", " [p1; p2; p3; p4; p5]);
+          pr "  CAMLxparam%d (%s);\n"
+            (List.length rest) (String.concat ", " rest)
+       | ps ->
+          pr "  CAMLparam%d (%s);\n" (List.length ps) (String.concat ", " ps)
+      );
       pr "  CAMLlocal1 (rv);\n";
       pr "\n";
 
@@ -2025,7 +3059,7 @@ and generate_ocaml_c () =
       pr "    caml_failwith (\"%s: used handle after closing it\");\n" name;
       pr "\n";
 
-      iter_args (
+      List.iter (
        function
        | String n ->
            pr "  const char *%s = String_val (%sv);\n" n n
@@ -2033,6 +3067,8 @@ and generate_ocaml_c () =
            pr "  const char *%s =\n" n;
            pr "    %sv != Val_int (0) ? String_val (Field (%sv, 0)) : NULL;\n"
              n n
+       | StringList n ->
+           pr "  char **%s = ocaml_guestfs_strings_val (%sv);\n" n n
        | Bool n ->
            pr "  int %s = Bool_val (%sv);\n" n n
        | Int n ->
@@ -2040,7 +3076,7 @@ and generate_ocaml_c () =
       ) (snd style);
       let error_code =
        match fst style with
-       | Err -> pr "  int r;\n"; "-1"
+       | RErr -> pr "  int r;\n"; "-1"
        | RInt _ -> pr "  int r;\n"; "-1"
        | RBool _ -> pr "  int r;\n"; "-1"
        | RConstString _ -> pr "  const char *r;\n"; "NULL"
@@ -2068,12 +3104,20 @@ and generate_ocaml_c () =
       generate_call_args ~handle:"g" style;
       pr ";\n";
       pr "  caml_leave_blocking_section ();\n";
+
+      List.iter (
+       function
+       | StringList n ->
+           pr "  ocaml_guestfs_free_strings (%s);\n" n;
+       | String _ | OptString _ | Bool _ | Int _ -> ()
+      ) (snd style);
+
       pr "  if (r == %s)\n" error_code;
       pr "    ocaml_guestfs_raise_error (g, \"%s\");\n" name;
       pr "\n";
 
       (match fst style with
-       | Err -> pr "  rv = Val_unit;\n"
+       | RErr -> pr "  rv = Val_unit;\n"
        | RInt _ -> pr "  rv = Val_int (r);\n"
        | RBool _ -> pr "  rv = Val_bool (r);\n"
        | RConstString _ -> pr "  rv = caml_copy_string (r);\n"
@@ -2102,7 +3146,18 @@ and generate_ocaml_c () =
 
       pr "  CAMLreturn (rv);\n";
       pr "}\n";
-      pr "\n"
+      pr "\n";
+
+      if List.length params > 5 then (
+       pr "CAMLprim value\n";
+       pr "ocaml_guestfs_%s_byte (value *argv, int argn)\n" name;
+       pr "{\n";
+       pr "  return ocaml_guestfs_%s (argv[0]" name;
+       iteri (fun i _ -> pr ", argv[%d]" i) (List.tl params);
+       pr ");\n";
+       pr "}\n";
+       pr "\n"
+      )
   ) all_functions
 
 and generate_ocaml_lvm_structure_decls () =
@@ -2124,15 +3179,16 @@ and generate_ocaml_lvm_structure_decls () =
 and generate_ocaml_prototype ?(is_external = false) name style =
   if is_external then pr "external " else pr "val ";
   pr "%s : t -> " name;
-  iter_args (
+  List.iter (
     function
     | String _ -> pr "string -> "
     | OptString _ -> pr "string option -> "
+    | StringList _ -> pr "string array -> "
     | Bool _ -> pr "bool -> "
     | Int _ -> pr "int -> "
   ) (snd style);
   (match fst style with
-   | Err -> pr "unit" (* all errors are turned into exceptions *)
+   | RErr -> pr "unit" (* all errors are turned into exceptions *)
    | RInt _ -> pr "int"
    | RBool _ -> pr "bool"
    | RConstString _ -> pr "string"
@@ -2143,7 +3199,12 @@ and generate_ocaml_prototype ?(is_external = false) name style =
    | RVGList _ -> pr "lvm_vg array"
    | RLVList _ -> pr "lvm_lv array"
   );
-  if is_external then pr " = \"ocaml_guestfs_%s\"" name;
+  if is_external then (
+    pr " = ";
+    if List.length (snd style) + 1 > 5 then
+      pr "\"ocaml_guestfs_%s_byte\" " name;
+    pr "\"ocaml_guestfs_%s\"" name
+  );
   pr "\n"
 
 (* Generate Perl xs code, a sort of crazy variation of C with macros. *)
@@ -2189,20 +3250,32 @@ my_newSVull(unsigned long long val) {
 #endif
 }
 
-/* XXX Not thread-safe, and in general not safe if the caller is
- * issuing multiple requests in parallel (on different guestfs
- * handles).  We should use the guestfs_h handle passed to the
- * error handle to distinguish these cases.
- */
-static char *last_error = NULL;
+/* http://www.perlmonks.org/?node_id=680842 */
+static char **
+XS_unpack_charPtrPtr (SV *arg) {
+  char **ret;
+  AV *av;
+  I32 i;
 
-static void
-error_handler (guestfs_h *g,
-              void *data,
-              const char *msg)
-{
-  if (last_error != NULL) free (last_error);
-  last_error = strdup (msg);
+  if (!arg || !SvOK (arg) || !SvROK (arg) || SvTYPE (SvRV (arg)) != SVt_PVAV) {
+    croak (\"array reference expected\");
+  }
+
+  av = (AV *)SvRV (arg);
+  ret = (char **)malloc (av_len (av) + 1 + 1);
+
+  for (i = 0; i <= av_len (av); i++) {
+    SV **elem = av_fetch (av, i, 0);
+
+    if (!elem || !*elem)
+      croak (\"missing element in list\");
+
+    ret[i] = SvPV_nolen (*elem);
+  }
+
+  ret[i] = NULL;
+
+  return ret;
 }
 
 MODULE = Sys::Guestfs  PACKAGE = Sys::Guestfs
@@ -2213,7 +3286,7 @@ _create ()
       RETVAL = guestfs_create ();
       if (!RETVAL)
         croak (\"could not create guestfs handle\");
-      guestfs_set_error_handler (RETVAL, error_handler, NULL);
+      guestfs_set_error_handler (RETVAL, NULL, NULL);
  OUTPUT:
       RETVAL
 
@@ -2226,9 +3299,9 @@ DESTROY (g)
 ";
 
   List.iter (
-    fun (name, style, _, _, _, _) ->
+    fun (name, style, _, _, _, _, _) ->
       (match fst style with
-       | Err -> pr "void\n"
+       | RErr -> pr "void\n"
        | RInt _ -> pr "SV *\n"
        | RBool _ -> pr "SV *\n"
        | RConstString _ -> pr "SV *\n"
@@ -2243,21 +3316,36 @@ DESTROY (g)
       generate_call_args ~handle:"g" style;
       pr "\n";
       pr "      guestfs_h *g;\n";
-      iter_args (
+      List.iter (
        function
        | String n -> pr "      char *%s;\n" n
        | OptString n -> pr "      char *%s;\n" n
+       | StringList n -> pr "      char **%s;\n" n
        | Bool n -> pr "      int %s;\n" n
        | Int n -> pr "      int %s;\n" n
       ) (snd style);
+
+      let do_cleanups () =
+       List.iter (
+         function
+         | String _
+         | OptString _
+         | Bool _
+         | Int _ -> ()
+         | StringList n -> pr "        free (%s);\n" n
+       ) (snd style)
+      in
+
       (* Code. *)
       (match fst style with
-       | Err ->
+       | RErr ->
           pr " PPCODE:\n";
           pr "      if (guestfs_%s " name;
           generate_call_args ~handle:"g" style;
-          pr " == -1)\n";
-          pr "        croak (\"%s: %%s\", last_error);\n" name
+          pr " == -1) {\n";
+          do_cleanups ();
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      }\n"
        | RInt n
        | RBool n ->
           pr "PREINIT:\n";
@@ -2266,8 +3354,10 @@ DESTROY (g)
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
-          pr "      if (%s == -1)\n" n;
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "      if (%s == -1) {\n" n;
+          do_cleanups ();
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      }\n";
           pr "      RETVAL = newSViv (%s);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
@@ -2278,8 +3368,10 @@ DESTROY (g)
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
-          pr "      if (%s == NULL)\n" n;
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "      if (%s == NULL) {\n" n;
+          do_cleanups ();
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      }\n";
           pr "      RETVAL = newSVpv (%s, 0);\n" n;
           pr " OUTPUT:\n";
           pr "      RETVAL\n"
@@ -2290,8 +3382,10 @@ DESTROY (g)
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
-          pr "      if (%s == NULL)\n" n;
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "      if (%s == NULL) {\n" n;
+          do_cleanups ();
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      }\n";
           pr "      RETVAL = newSVpv (%s, 0);\n" n;
           pr "      free (%s);\n" n;
           pr " OUTPUT:\n";
@@ -2304,8 +3398,10 @@ DESTROY (g)
           pr "      %s = guestfs_%s " n name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
-          pr "      if (%s == NULL)\n" n;
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "      if (%s == NULL) {\n" n;
+          do_cleanups ();
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      }\n";
           pr "      for (n = 0; %s[n] != NULL; ++n) /**/;\n" n;
           pr "      EXTEND (SP, n);\n";
           pr "      for (i = 0; i < n; ++i) {\n";
@@ -2320,8 +3416,10 @@ DESTROY (g)
           pr "      r = guestfs_%s " name;
           generate_call_args ~handle:"g" style;
           pr ";\n";
-          pr "      if (r == NULL)\n";
-          pr "        croak (\"%s: %%s\", last_error);\n" name;
+          pr "      if (r == NULL) {\n";
+          do_cleanups ();
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      }\n";
           pr "      EXTEND (SP, 2);\n";
           pr "      PUSHs (sv_2mortal (newSViv (r->i)));\n";
           pr "      PUSHs (sv_2mortal (newSViv (r->b)));\n";
@@ -2333,6 +3431,9 @@ DESTROY (g)
        | RLVList n ->
           generate_perl_lvm_code "lv" lv_cols name style n;
       );
+
+      do_cleanups ();
+
       pr "\n"
   ) all_functions
 
@@ -2346,7 +3447,7 @@ and generate_perl_lvm_code typ cols name style n =
   generate_call_args ~handle:"g" style;
   pr ";\n";
   pr "      if (%s == NULL)\n" n;
-  pr "        croak (\"%s: %%s\", last_error);\n" name;
+  pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
   pr "      EXTEND (SP, %s->len);\n" n;
   pr "      for (i = 0; i < %s->len; ++i) {\n" n;
   pr "        hv = newHV ();\n";
@@ -2456,16 +3557,16 @@ sub new {
    * they are pulled in from the XS code automatically.
    *)
   List.iter (
-    fun (name, style, _, flags, _, longdesc) ->
+    fun (name, style, _, flags, _, _, longdesc) ->
       let longdesc = replace_str longdesc "C<guestfs_" "C<$h-E<gt>" in
       pr "=item ";
       generate_perl_prototype name style;
       pr "\n\n";
       pr "%s\n\n" longdesc;
       if List.mem ProtocolLimitWarning flags then
-       pr "Because of the message protocol, there is a transfer limit 
-of somewhere between 2MB and 4MB.  To transfer large files you should use
-FTP.\n\n";
+       pr "%s\n\n" protocol_limit_warning;
+      if List.mem DangerWillRobinson flags then
+       pr "%s\n\n" danger_will_robinson
   ) all_functions_sorted;
 
   (* End of file. *)
@@ -2493,7 +3594,7 @@ L<guestfs(3)>, L<guestfish(1)>.
 
 and generate_perl_prototype name style =
   (match fst style with
-   | Err -> ()
+   | RErr -> ()
    | RBool n
    | RInt n
    | RConstString n
@@ -2506,14 +3607,366 @@ and generate_perl_prototype name style =
   );
   pr "$h->%s (" name;
   let comma = ref false in
-  iter_args (
+  List.iter (
     fun arg ->
       if !comma then pr ", ";
       comma := true;
-      pr "%s" (name_of_argt arg)
+      match arg with
+      | String n | OptString n | Bool n | Int n ->
+         pr "$%s" n
+      | StringList n ->
+         pr "\\@%s" n
   ) (snd style);
   pr ");"
 
+(* Generate Python C module. *)
+and generate_python_c () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include <Python.h>
+
+#include \"guestfs.h\"
+
+typedef struct {
+  PyObject_HEAD
+  guestfs_h *g;
+} Pyguestfs_Object;
+
+static guestfs_h *
+get_handle (PyObject *obj)
+{
+  assert (obj);
+  assert (obj != Py_None);
+  return ((Pyguestfs_Object *) obj)->g;
+}
+
+static PyObject *
+put_handle (guestfs_h *g)
+{
+  assert (g);
+  return
+    PyCObject_FromVoidPtrAndDesc ((void *) g, (char *) \"guestfs_h\", NULL);
+}
+
+/* This list should be freed (but not the strings) after use. */
+static const char **
+get_string_list (PyObject *obj)
+{
+  int i, len;
+  const char **r;
+
+  assert (obj);
+
+  if (!PyList_Check (obj)) {
+    PyErr_SetString (PyExc_RuntimeError, \"expecting a list parameter\");
+    return NULL;
+  }
+
+  len = PyList_Size (obj);
+  r = malloc (sizeof (char *) * (len+1));
+  if (r == NULL) {
+    PyErr_SetString (PyExc_RuntimeError, \"get_string_list: out of memory\");
+    return NULL;
+  }
+
+  for (i = 0; i < len; ++i)
+    r[i] = PyString_AsString (PyList_GetItem (obj, i));
+  r[len] = NULL;
+
+  return r;
+}
+
+static PyObject *
+put_string_list (char * const * const argv)
+{
+  PyObject *list;
+  int argc, i;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    ;
+
+  list = PyList_New (argc);
+  for (i = 0; i < argc; ++i)
+    PyList_SetItem (list, i, PyString_FromString (argv[i]));
+
+  return list;
+}
+
+static void
+free_strings (char **argv)
+{
+  int argc;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    free (argv[argc]);
+  free (argv);
+}
+
+static PyObject *
+py_guestfs_create (PyObject *self, PyObject *args)
+{
+  guestfs_h *g;
+
+  g = guestfs_create ();
+  if (g == NULL) {
+    PyErr_SetString (PyExc_RuntimeError,
+                     \"guestfs.create: failed to allocate handle\");
+    return NULL;
+  }
+  guestfs_set_error_handler (g, NULL, NULL);
+  return put_handle (g);
+}
+
+static PyObject *
+py_guestfs_close (PyObject *self, PyObject *args)
+{
+  PyObject *py_g;
+  guestfs_h *g;
+
+  if (!PyArg_ParseTuple (args, (char *) \"O:guestfs_close\", &py_g))
+    return NULL;
+  g = get_handle (py_g);
+
+  guestfs_close (g);
+
+  Py_INCREF (Py_None);
+  return Py_None;
+}
+
+";
+
+  (* LVM structures, turned into Python dictionaries. *)
+  List.iter (
+    fun (typ, cols) ->
+      pr "static PyObject *\n";
+      pr "put_lvm_%s (struct guestfs_lvm_%s *%s)\n" typ typ typ;
+      pr "{\n";
+      pr "  PyObject *dict;\n";
+      pr "\n";
+      pr "  dict = PyDict_New ();\n";
+      List.iter (
+       function
+       | name, `String ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyString_FromString (%s->%s));\n"
+             typ name
+       | name, `UUID ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyString_FromStringAndSize (%s->%s, 32));\n"
+             typ name
+       | name, `Bytes ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromUnsignedLongLong (%s->%s));\n"
+             typ name
+       | name, `Int ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromLongLong (%s->%s));\n"
+             typ name
+       | name, `OptPercent ->
+           pr "  if (%s->%s >= 0)\n" typ name;
+           pr "    PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                          PyFloat_FromDouble ((double) %s->%s));\n"
+             typ name;
+           pr "  else {\n";
+           pr "    Py_INCREF (Py_None);\n";
+           pr "    PyDict_SetItemString (dict, \"%s\", Py_None);" name;
+           pr "  }\n"
+      ) cols;
+      pr "  return dict;\n";
+      pr "};\n";
+      pr "\n";
+
+      pr "static PyObject *\n";
+      pr "put_lvm_%s_list (struct guestfs_lvm_%s_list *%ss)\n" typ typ typ;
+      pr "{\n";
+      pr "  PyObject *list;\n";
+      pr "  int i;\n";
+      pr "\n";
+      pr "  list = PyList_New (%ss->len);\n" typ;
+      pr "  for (i = 0; i < %ss->len; ++i)\n" typ;
+      pr "    PyList_SetItem (list, i, put_lvm_%s (&%ss->val[i]));\n" typ typ;
+      pr "  return list;\n";
+      pr "};\n";
+      pr "\n"
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+
+  (* Python wrapper functions. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      pr "static PyObject *\n";
+      pr "py_guestfs_%s (PyObject *self, PyObject *args)\n" name;
+      pr "{\n";
+
+      pr "  PyObject *py_g;\n";
+      pr "  guestfs_h *g;\n";
+      pr "  PyObject *py_r;\n";
+
+      let error_code =
+       match fst style with
+       | RErr | RInt _ | RBool _ -> pr "  int r;\n"; "-1"
+       | RConstString _ -> pr "  const char *r;\n"; "NULL"
+       | RString _ -> pr "  char *r;\n"; "NULL"
+       | RStringList _ -> pr "  char **r;\n"; "NULL"
+       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"; "NULL"
+       | RPVList n -> pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
+       | RVGList n -> pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
+       | RLVList n -> pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL" in
+
+      List.iter (
+       function
+       | String n -> pr "  const char *%s;\n" n
+       | OptString n -> pr "  const char *%s;\n" n
+       | StringList n ->
+           pr "  PyObject *py_%s;\n" n;
+           pr "  const char **%s;\n" n
+       | Bool n -> pr "  int %s;\n" n
+       | Int n -> pr "  int %s;\n" n
+      ) (snd style);
+
+      pr "\n";
+
+      (* Convert the parameters. *)
+      pr "  if (!PyArg_ParseTuple (args, (char *) \"O";
+      List.iter (
+       function
+       | String _ -> pr "s"
+       | OptString _ -> pr "z"
+       | StringList _ -> pr "O"
+       | Bool _ -> pr "i" (* XXX Python has booleans? *)
+       | Int _ -> pr "i"
+      ) (snd style);
+      pr ":guestfs_%s\",\n" name;
+      pr "                         &py_g";
+      List.iter (
+       function
+       | String n -> pr ", &%s" n
+       | OptString n -> pr ", &%s" n
+       | StringList n -> pr ", &py_%s" n
+       | Bool n -> pr ", &%s" n
+       | Int n -> pr ", &%s" n
+      ) (snd style);
+
+      pr "))\n";
+      pr "    return NULL;\n";
+
+      pr "  g = get_handle (py_g);\n";
+      List.iter (
+       function
+       | String _ | OptString _ | Bool _ | Int _ -> ()
+       | StringList n ->
+           pr "  %s = get_string_list (py_%s);\n" n n;
+           pr "  if (!%s) return NULL;\n" n
+      ) (snd style);
+
+      pr "\n";
+
+      pr "  r = guestfs_%s " name;
+      generate_call_args ~handle:"g" style;
+      pr ";\n";
+
+      List.iter (
+       function
+       | String _ | OptString _ | Bool _ | Int _ -> ()
+       | StringList n ->
+           pr "  free (%s);\n" n
+      ) (snd style);
+
+      pr "  if (r == %s) {\n" error_code;
+      pr "    PyErr_SetString (PyExc_RuntimeError, guestfs_last_error (g));\n";
+      pr "    return NULL;\n";
+      pr "  }\n";
+      pr "\n";
+
+      (match fst style with
+       | RErr ->
+          pr "  Py_INCREF (Py_None);\n";
+          pr "  py_r = Py_None;\n"
+       | RInt _
+       | RBool _ -> pr "  py_r = PyInt_FromLong ((long) r);\n"
+       | RConstString _ -> pr "  py_r = PyString_FromString (r);\n"
+       | RString _ ->
+          pr "  py_r = PyString_FromString (r);\n";
+          pr "  free (r);\n"
+       | RStringList _ ->
+          pr "  py_r = put_string_list (r);\n";
+          pr "  free_strings (r);\n"
+       | RIntBool _ ->
+          pr "  py_r = PyTuple_New (2);\n";
+          pr "  PyTuple_SetItem (py_r, 0, PyInt_FromLong ((long) r->i));\n";
+          pr "  PyTuple_SetItem (py_r, 1, PyInt_FromLong ((long) r->b));\n";
+          pr "  guestfs_free_int_bool (r);\n"
+       | RPVList n ->
+          pr "  py_r = put_lvm_pv_list (r);\n";
+          pr "  guestfs_free_lvm_pv_list (r);\n"
+       | RVGList n ->
+          pr "  py_r = put_lvm_vg_list (r);\n";
+          pr "  guestfs_free_lvm_vg_list (r);\n"
+       | RLVList n ->
+          pr "  py_r = put_lvm_lv_list (r);\n";
+          pr "  guestfs_free_lvm_lv_list (r);\n"
+      );
+
+      pr "  return py_r;\n";
+      pr "}\n";
+      pr "\n"
+  ) all_functions;
+
+  (* Table of functions. *)
+  pr "static PyMethodDef methods[] = {\n";
+  pr "  { (char *) \"create\", py_guestfs_create, METH_VARARGS, NULL },\n";
+  pr "  { (char *) \"close\", py_guestfs_close, METH_VARARGS, NULL },\n";
+  List.iter (
+    fun (name, _, _, _, _, _, _) ->
+      pr "  { (char *) \"%s\", py_guestfs_%s, METH_VARARGS, NULL },\n"
+       name name
+  ) all_functions;
+  pr "  { NULL, NULL, 0, NULL }\n";
+  pr "};\n";
+  pr "\n";
+
+  (* Init function. *)
+  pr "\
+void
+initlibguestfsmod (void)
+{
+  static int initialized = 0;
+
+  if (initialized) return;
+  Py_InitModule ((char *) \"libguestfsmod\", methods);
+  initialized = 1;
+}
+"
+
+(* Generate Python module. *)
+and generate_python_py () =
+  generate_header HashStyle LGPLv2;
+
+  pr "import libguestfsmod\n";
+  pr "\n";
+  pr "class GuestFS:\n";
+  pr "    def __init__ (self):\n";
+  pr "        self._o = libguestfsmod.create ()\n";
+  pr "\n";
+  pr "    def __del__ (self):\n";
+  pr "        libguestfsmod.close (self._o)\n";
+  pr "\n";
+
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      pr "    def %s " name;
+      generate_call_args ~handle:"self" style;
+      pr ":\n";
+      pr "        return libguestfsmod.%s " name;
+      generate_call_args ~handle:"self._o" style;
+      pr "\n";
+      pr "\n";
+  ) all_functions
+
 let output_to filename =
   let filename_new = filename ^ ".new" in
   chan := open_out filename_new;
@@ -2529,6 +3982,15 @@ let output_to filename =
 let () =
   check_functions ();
 
+  if not (Sys.file_exists "configure.ac") then (
+    eprintf "\
+You are probably running this from the wrong directory.
+Run it from the top source directory using the command
+  src/generator.ml
+";
+    exit 1
+  );
+
   let close = output_to "src/guestfs_protocol.x" in
   generate_xdr ();
   close ();
@@ -2553,6 +4015,10 @@ let () =
   generate_daemon_actions ();
   close ();
 
+  let close = output_to "tests.c" in
+  generate_tests ();
+  close ();
+
   let close = output_to "fish/cmds.c" in
   generate_fish_cmds ();
   close ();
@@ -2588,3 +4054,11 @@ let () =
   let close = output_to "perl/lib/Sys/Guestfs.pm" in
   generate_perl_pm ();
   close ();
+
+  let close = output_to "python/guestfs-py.c" in
+  generate_python_c ();
+  close ();
+
+  let close = output_to "python/guestfs.py" in
+  generate_python_py ();
+  close ();