virt-builder-repository allows users to easily create or update
a virt-builder source repository out of disk images. The tool can
be run in either interactive or automated mode.
---
.gitignore | 3 +
builder/Makefile.am | 82 +++++-
builder/repository_main.ml | 487 ++++++++++++++++++++++++++++++++++++
builder/test-docs.sh | 3 +
builder/virt-builder-repository.pod | 183 ++++++++++++++
5 files changed, 757 insertions(+), 1 deletion(-)
create mode 100644 builder/repository_main.ml
create mode 100644 builder/virt-builder-repository.pod
diff --git a/.gitignore b/.gitignore
index dde7a529b..152c7e8ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -93,13 +93,16 @@ Makefile.in
/builder/oUnit-*
/builder/*.qcow2
/builder/stamp-virt-builder.pod
+/builder/stamp-virt-builder-repository.pod
/builder/stamp-virt-index-validate.pod
/builder/test-config/virt-builder/repos.d/test-index.conf
/builder/test-console-*.sh
/builder/test-simplestreams/virt-builder/repos.d/cirros.conf
/builder/test-website/virt-builder/repos.d/libguestfs.conf
/builder/virt-builder
+/builder/virt-builder-repository
/builder/virt-builder.1
+/builder/virt-builder-repository.1
/builder/virt-index-validate
/builder/virt-index-validate.1
/builder/*.xz
diff --git a/builder/Makefile.am b/builder/Makefile.am
index 218f64b4c..f5a5138cd 100644
--- a/builder/Makefile.am
+++ b/builder/Makefile.am
@@ -21,6 +21,8 @@ AM_YFLAGS = -d
EXTRA_DIST = \
$(SOURCES_MLI) $(SOURCES_ML) $(SOURCES_C) \
+ $(REPOSITORY_SOURCES_ML) \
+ $(REPOSITORY_SOURCES_MLI) \
libguestfs.gpg \
opensuse.gpg \
test-console.sh \
@@ -38,6 +40,7 @@ EXTRA_DIST = \
test-virt-index-validate-good-2 \
test-virt-index-validate-good-3 \
virt-builder.pod \
+ virt-builder-repository.pod \
virt-index-validate.pod \
yajl_tests.ml
@@ -85,13 +88,44 @@ SOURCES_C = \
setlocale-c.c \
yajl-c.c
+REPOSITORY_SOURCES_ML = \
+ utils.ml \
+ index.ml \
+ cache.ml \
+ downloader.ml \
+ sigchecker.ml \
+ ini_reader.ml \
+ index_parser.ml \
+ yajl.ml \
+ paths.ml \
+ sources.ml \
+ repository_main.ml
+
+REPOSITORY_SOURCES_MLI = \
+ cache.mli \
+ downloader.mli \
+ index.mli \
+ index_parser.mli \
+ ini_reader.mli \
+ sigchecker.mli \
+ sources.mli \
+ yajl.mli
+
+REPOSITORY_SOURCES_C = \
+ index-scan.c \
+ index-struct.c \
+ index-parse.c \
+ index-parser-c.c \
+ yajl-c.c
+
+
man_MANS =
noinst_DATA =
bin_PROGRAMS =
if HAVE_OCAML
-bin_PROGRAMS += virt-builder
+bin_PROGRAMS += virt-builder virt-builder-repository
virt_builder_SOURCES = $(SOURCES_C)
virt_builder_CPPFLAGS = \
@@ -115,6 +149,26 @@ virt_builder_CFLAGS = \
BOBJECTS = $(SOURCES_ML:.ml=.cmo)
XOBJECTS = $(BOBJECTS:.cmo=.cmx)
+virt_builder_repository_SOURCES = $(REPOSITORY_SOURCES_C)
+virt_builder_repository_CPPFLAGS = \
+ -I. \
+ -I$(top_builddir) \
+ -I$(top_srcdir)/gnulib/lib -I$(top_builddir)/gnulib/lib \
+ -I$(shell $(OCAMLC) -where) \
+ -I$(top_srcdir)/gnulib/lib \
+ -I$(top_srcdir)/src \
+ -I$(top_srcdir)/fish
+virt_builder_repository_CFLAGS = \
+ -pthread \
+ $(WARN_CFLAGS) $(WERROR_CFLAGS) \
+ -Wno-unused-macros \
+ $(LIBLZMA_CFLAGS) \
+ $(LIBTINFO_CFLAGS) \
+ $(LIBXML2_CFLAGS) \
+ $(YAJL_CFLAGS)
+REPOSITORY_BOBJECTS = $(REPOSITORY_SOURCES_ML:.ml=.cmo)
+REPOSITORY_XOBJECTS = $(REPOSITORY_BOBJECTS:.cmo=.cmx)
+
# -I $(top_builddir)/lib/.libs is a hack which forces corresponding -L
# option to be passed to gcc, so we don't try linking against an
# installed copy of libguestfs.
@@ -149,8 +203,10 @@ OCAMLFLAGS = $(OCAML_FLAGS) $(OCAML_WARN_ERROR)
if !HAVE_OCAMLOPT
OBJECTS = $(BOBJECTS)
+REPOSITORY_OBJECTS = $(REPOSITORY_BOBJECTS)
else
OBJECTS = $(XOBJECTS)
+REPOSITORY_OBJECTS = $(REPOSITORY_XOBJECTS)
endif
OCAMLLINKFLAGS = mlguestfs.$(MLARCHIVE) mllib.$(MLARCHIVE) customize.$(MLARCHIVE)
$(LINK_CUSTOM_OCAMLC_ONLY)
@@ -165,6 +221,16 @@ virt_builder_LINK = \
$(OCAMLFIND) $(BEST) $(OCAMLFLAGS) $(OCAMLPACKAGES) $(OCAMLLINKFLAGS) \
$(OBJECTS) -o $@
+virt_builder_repository_DEPENDENCIES = \
+ $(REPOSITORY_OBJECTS) \
+ ../mllib/mllib.$(MLARCHIVE) \
+ ../customize/customize.$(MLARCHIVE) \
+ $(top_srcdir)/ocaml-link.sh
+virt_builder_repository_LINK = \
+ $(top_srcdir)/ocaml-link.sh -cclib '$(OCAMLCLIBS)' -- \
+ $(OCAMLFIND) $(BEST) $(OCAMLFLAGS) $(OCAMLPACKAGES) $(OCAMLLINKFLAGS) \
+ $(REPOSITORY_OBJECTS) -o $@
+
# Manual pages and HTML files for the website.
man_MANS += virt-builder.1
@@ -183,6 +249,20 @@ stamp-virt-builder.pod: virt-builder.pod
$(top_srcdir)/customize/customize-synop
$<
touch $@
+man_MANS += virt-builder-repository.1
+noinst_DATA += $(top_builddir)/website/virt-builder-repository.1.html
+
+virt-builder-repository.1 $(top_builddir)/website/virt-builder-repository.1.html:
stamp-virt-builder-repository.pod
+
+stamp-virt-builder-repository.pod: virt-builder-repository.pod
+ $(PODWRAPPER) \
+ --man virt-builder-repository.1 \
+ --html $(top_builddir)/website/virt-builder-repository.1.html \
+ --license GPLv2+ \
+ --warning safe \
+ $<
+ touch $@
+
# Tests.
TESTS_ENVIRONMENT = $(top_builddir)/run --test
diff --git a/builder/repository_main.ml b/builder/repository_main.ml
new file mode 100644
index 000000000..1af54cc1b
--- /dev/null
+++ b/builder/repository_main.ml
@@ -0,0 +1,487 @@
+(* virt-builder
+ * Copyright (C) 2016 SUSE Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+open Common_gettext.Gettext
+open Common_utils
+open Getopt.OptionName
+open Utils
+open Yajl
+open Xpath_helpers
+
+open Printf
+
+module StringSet = Set.Make(String)
+
+type cmdline = {
+ gpg : string;
+ gpgkey : string option;
+ interactive : bool;
+ keep_unsigned : bool;
+ repo : string;
+}
+
+type disk_image_infos = {
+ format : string;
+ size : int64;
+}
+
+let parse_cmdline () =
+ let gpg = ref "gpg" in
+ let gpgkey = ref "" in
+ let interactive = ref false in
+ let keep_unsigned = ref false in
+
+ let argspec = [
+ [ L"gpg" ], Getopt.Set_string ("gpg", gpg), s_"Set GPG
binary/command";
+ [ S 'K'; L"gpg-key" ], Getopt.Set_string ("gpgkey",
gpgkey),
+ s_"ID of the GPG key to sign the repo with";
+ [ S 'i'; L"interactive" ], Getopt.Set interactive, s_"Ask the
user about missing data";
+ [ L"keep-index" ], Getopt.Set keep_unsigned, s_"Keep unsigned
index";
+ ] in
+
+ let args = ref [] in
+ let anon_fun s = push_front s args in
+ let usage_msg =
+ sprintf (f_"\
+%s: create a repository for virt-builder
+
+ virt-builder-repository REPOSITORY_PATH
+
+A short summary of the options is given below. For detailed help please
+read the man page virt-builder-repository(1).
+")
+ prog in
+ let opthandle = create_standard_options argspec ~anon_fun usage_msg in
+ Getopt.parse opthandle;
+
+ (* Dereference options. *)
+ let args = List.rev !args in
+ let gpg = !gpg in
+ let gpgkey = match !gpgkey with "" -> None | s -> Some s in
+ let interactive = !interactive in
+ let keep_unsigned = !keep_unsigned in
+
+ (* Check options *)
+ let repo =
+ (match args with
+ | [repo] -> repo
+ | [] ->
+ error (f_"virt-builder-repository /path/to/repo\nUse '/path/to/repo'
to point to the repository folder.")
+ | _ ->
+ error (f_"too many parameters, only one path to repository is allowed")
+ ) in
+
+ {
+ gpg = gpg;
+ gpgkey = gpgkey;
+ interactive = interactive;
+ keep_unsigned = keep_unsigned;
+ repo = repo;
+ }
+
+let increment_revision revision =
+ match revision with
+ | Utils.Rev_int n -> Utils.Rev_int (n + 1)
+ | Utils.Rev_string s ->
+ if Str.string_match (Str.regexp "^\\(.*[-._]\\)\\([0-9]+\\)$") s 0 then
+ let prefix = Str.matched_group 1 s in
+ let suffix = int_of_string (Str.matched_group 2 s) in
+ Utils.Rev_string (prefix ^ (string_of_int (suffix + 1)))
+ else
+ Utils.Rev_string (s ^ ".1")
+
+let osinfo_ids = ref None
+
+let osinfo_get_short_ids () =
+ match !osinfo_ids with
+ | Some ids -> ids
+ | None -> (
+ let ids = ref [] in
+ let g = open_guestfs () in
+
+ Osinfo.read_osinfo_db g#ocaml_handle (
+ fun filepath ->
+ let doc = Xml.parse_file filepath in
+ let xpathctx = Xml.xpath_new_context doc in
+ let id = xpath_string_default xpathctx "/libosinfo/os/short-id"
"" in
+ if id <> "" then
+ ids := id :: !ids
+ );
+ g#close ();
+ let ids_set = StringSet.of_list(!ids) in
+ osinfo_ids := Some ids_set;
+ ids_set
+ )
+
+(* Move files in tmprepo into the repository *)
+let do_mv src dest =
+ let cmd = [ "mv"; src; dest ] in
+ run_command cmd
+
+let compress_to file outdir =
+ info "Copying image to temporary folder ...%!";
+ let outimg = outdir // (Filename.basename file) in
+ let cmd = [ "cp" ] @
+ (if verbose () then [ "-v" ] else []) @
+ [ file; outimg ] in
+ let r = run_command cmd in
+ if r <> 0 then
+ error (f_"cp command failed '%s'") file;
+
+ info "Compressing ...%!";
+ let cmd = [ "xz"; "-f"; "--best";
+ "--block-size=16777216"; outimg ] in
+ if run_command cmd <> 0 then
+ exit 1;
+ outimg ^ ".xz"
+
+let get_disk_image_infos filepath =
+ let qemuimg_cmd = "qemu-img info --output json " ^ (quote filepath) in
+ let lines = external_command qemuimg_cmd in
+ let line = String.concat "\n" lines in
+ let infos = yajl_tree_parse line in
+ { format = object_get_string "format" infos;
+ size = object_get_number "virtual-size" infos }
+
+let process_image filename repo tmprepo index interactive sigchecker =
+ info "Preparing %s..." filename;
+
+ let filepath = repo // filename in
+ let { format = format; size = size } = get_disk_image_infos filepath in
+ let xz_path = compress_to filepath tmprepo in
+ let checksum = Checksums.compute_checksum "sha512" xz_path in
+ let compressed_size = (Unix.stat xz_path).Unix.st_size in
+
+ let ask message = (
+ printf message;
+ let value = read_line () in
+ match value with
+ | "" -> None
+ | s -> Some s
+ ) in
+
+ let rec ask_id () = (
+ printf (f_"Identifier: ");
+ let id = read_line () in
+ if not (Str.string_match (Str.regexp "[a-zA-Z0-9-_.]+") id 0) then (
+ warning (f_"Allowed characters are letters, digits, - _ and .");
+ ask_id ();
+ ) else
+ id;
+ ) in
+
+ let ask_arch () = (
+ printf (f_"Architecture. Choose one from the list below:\n");
+ let arches = ["x86_64"; "aarch64"; "armv7l";
"i686"; "ppc64"; "ppc64le"; "s390x" ] in
+ iteri (
+ fun i arch -> printf " [%d] %s\n" (i + 1) arch
+ ) arches;
+
+ let arch = ref None in
+ while !arch <> None do
+ let input = read_line () in
+ if input = "exit" || input = "q" || input = "quit"
then
+ exit 0
+ else
+ try
+ let i = int_of_string input in
+ arch := Some (List.nth arches (i - 1))
+ with Failure _ -> arch := Some input
+ done;
+ match !arch with
+ | None -> "" (* Should never happen *)
+ | Some arch -> arch
+ ) in
+
+ let ask_osinfo () =
+ printf (f_ "osinfo short ID (can be found with osinfo-query os command):
");
+ let value = read_line () in
+ match value with
+ | "" -> None
+ | osinfo ->
+ let osinfo_ids = osinfo_get_short_ids () in
+ if not (StringSet.mem osinfo osinfo_ids) then
+ warning (f_"'%s' is not a libosinfo OS id") osinfo;
+ Some osinfo in
+
+ (* Do we have an entry for that file already? *)
+ let file_entry =
+ try
+ List.hd (
+ List.filter (
+ fun (id, { Index.file_uri=file_uri }) ->
+ (Filename.basename file_uri) = ((Filename.basename filename) ^
".xz")
+ ) index
+ )
+ with
+ | Failure _ ->
+ let entry = { Index.printable_name = None;
+ osinfo = None;
+ file_uri = "";
+ arch = "";
+ signature_uri = None;
+ checksums = None;
+ revision = Utils.Rev_int 0;
+ format = Some format;
+ size = size;
+ compressed_size = Some (Int64.of_int compressed_size);
+ expand = None;
+ lvexpand = None;
+ notes = [];
+ hidden = false;
+ aliases = None;
+ sigchecker = sigchecker;
+ proxy = Curl.UnsetProxy } in
+ ("", entry) in
+
+ let id, { Index.printable_name = printable_name;
+ osinfo = osinfo;
+ arch = arch;
+ checksums = checksums;
+ revision = revision;
+ expand = expand;
+ lvexpand = lvexpand;
+ notes = notes;
+ hidden = hidden;
+ aliases = aliases } = file_entry in
+
+ let old_checksum =
+ match checksums with
+ | Some csums -> (
+ try
+ let csum = List.find (
+ fun c ->
+ match c with
+ | Checksums.SHA512 _ -> true
+ | _ -> false
+ ) csums in
+ Checksums.string_of_csum csum
+ with
+ | _ -> ""
+ )
+ | None -> "" in
+
+ let id =
+ if id = "" && interactive then
+ ask_id ()
+ else (
+ if id = "" then
+ error (f_"Missing image identifier");
+ id
+ ) in
+
+ let printable_name =
+ if printable_name = None && interactive then
+ ask (f_"Display name: ")
+ else
+ printable_name in
+
+ let arch =
+ if arch = "" then (
+ if interactive then ask_arch ()
+ else error (f_"missing architecture for %s") id;
+ ) else arch in
+
+ let osinfo =
+ if osinfo = None && interactive then
+ ask_osinfo ()
+ else
+ osinfo in
+
+ let expand =
+ if expand = None && interactive then
+ ask (f_"Expandable partition: ")
+ else
+ expand in
+
+ let lvexpand =
+ if lvexpand = None && interactive then
+ ask (f_"Expandable volume: ")
+ else
+ lvexpand in
+
+ let revision =
+ if old_checksum <> checksum then
+ increment_revision revision
+ else
+ revision in
+
+ (id, { Index.printable_name = printable_name;
+ osinfo = osinfo;
+ file_uri = Filename.basename xz_path;
+ arch = arch;
+ signature_uri = None;
+ checksums = Some [(Checksums.SHA512 checksum)];
+ revision = revision;
+ format = Some format;
+ size = size;
+ compressed_size = Some (Int64.of_int compressed_size);
+ expand = expand;
+ lvexpand = lvexpand;
+ notes = notes;
+ hidden = hidden;
+ aliases = aliases;
+ sigchecker = sigchecker;
+ proxy = Curl.UnsetProxy })
+
+let main () =
+ let cmdline = parse_cmdline () in
+
+ (* If debugging, echo the command line arguments. *)
+ debug "command line: %s\n" (String.concat " " (Array.to_list
Sys.argv));
+
+ (* Check that the paths are existing *)
+ if not (Sys.file_exists cmdline.repo) then
+ error (f_"Repository folder '%s' doesn't exist") cmdline.repo;
+
+ (* Create a temporary folder to work in *)
+ let tmpdir = Mkdtemp.temp_dir ~base_dir:cmdline.repo
+ "virt-builder-repository." "" in
+ rmdir_on_exit tmpdir;
+
+ let tmprepo = tmpdir // "repo" in
+ Unix.mkdir tmprepo 0o700;
+
+ let sigchecker = Sigchecker.create ~gpg:cmdline.gpg
+ ~check_signature:false
+ ~gpgkey:No_Key
+ ~tmpdir in
+
+ let index =
+ try
+ let index_filename =
+ List.find (
+ fun filename -> Sys.file_exists (cmdline.repo // filename)
+ ) [ "index.asc"; "index" ] in
+
+ let downloader = Downloader.create ~curl:"do-not-use-curl"
+ ~cache:None ~tmpdir in
+
+ let source = { Sources.name = "input";
+ uri = cmdline.repo // index_filename;
+ gpgkey = No_Key;
+ proxy = Curl.SystemProxy;
+ format = Sources.FormatNative } in
+
+ Index_parser.get_index ~downloader ~sigchecker ~template:true source
+ with Not_found -> [] in
+
+ (* Check for index/interactive consistency *)
+ if not cmdline.interactive && index == [] then
+ error (f_"the repository needs to contain an index file when running in
automated mode");
+
+ debug "Searching for images ...\n";
+
+ let images =
+ let is_supported_format file =
+ let extension = last_part_of file '.' in
+ match extension with
+ | Some ext ->
+ let allowed = StringSet.of_list ["qcow2"; "raw";
"img"] in
+ StringSet.mem ext allowed
+ | None ->
+ file <> "index" in
+ let files = Array.to_list (Sys.readdir cmdline.repo) in
+ List.filter is_supported_format files in
+
+ debug " + %s\n" (String.concat "\n + " images);
+
+ let outindex_path = tmprepo // "index" in
+ let index_channel = open_out outindex_path in
+
+ (* Generate entries for uncompressed images *)
+ let written_ids =
+ List.map (
+ fun filename ->
+ let id, new_entry = process_image filename cmdline.repo tmprepo
+ index cmdline.interactive sigchecker in
+
+ Index_parser.write_entry index_channel (id, new_entry);
+ id
+ ) images in
+
+ (* Write the unchanged entries *)
+ List.iter (
+ fun (id, entry) ->
+ let written = List.exists (fun written_id -> written_id = id) written_ids in
+ if not written then
+ let { Index.file_uri = file_uri } = entry in
+ if Sys.file_exists file_uri then (
+ let rel_path =
+ try
+ subdirectory cmdline.repo file_uri
+ with
+ | Invalid_argument _ ->
+ file_uri in
+ debug "adding unchanged entry: %s" rel_path;
+ let rel_entry = { entry with Index.file_uri = rel_path } in
+ Index_parser.write_entry index_channel (id, rel_entry);
+ );
+ ) index;
+
+ close_out index_channel;
+
+ (* GPG sign the generated index *)
+ (match cmdline.gpgkey with
+ | None ->
+ debug "Skip index signing"
+ | Some gpgkey ->
+ debug "Signing index with GPG key %s" gpgkey;
+ let cmd = [ cmdline.gpg; "--armor";
+ "--output"; (tmprepo // "index.gpg");
+ "--export"; gpgkey ] in
+ if run_command cmd <> 0 then
+ error (f_"Failed to export GPG key %s") gpgkey;
+
+ let cmd = [ cmdline.gpg; "--armor";
+ "--default-key"; gpgkey;
+ "--clearsign"; (tmprepo // "index") ] in
+ if run_command cmd <> 0 then
+ error (f_"Failed to sign index");
+
+ (* Remove the index file since we have the signed version of it *)
+ if not cmdline.keep_unsigned then
+ Sys.remove (tmprepo // "index")
+ );
+
+ debug "Creating index backup copy";
+
+ List.iter (
+ fun filename ->
+ let filepath = cmdline.repo // filename in
+ if Sys.file_exists filepath then
+ if do_mv filepath (filepath ^ ".bak") <> 0 then
+ error (f_"Failed to create %s backup copy") filename
+ ) ["index"; "index.asc"];
+
+ debug "Moving files to final destination";
+
+ Array.iter (
+ fun filename ->
+ if do_mv (tmprepo // filename) cmdline.repo <> 0 then
+ error (f_"Failed to move %s in repository") (tmprepo // filename)
+ ) (Sys.readdir tmprepo);
+
+ debug "Cleanup";
+
+ (* Remove the processed image files *)
+ List.iter (
+ fun filename -> Sys.remove (cmdline.repo // filename)
+ ) images
+
+let () = run_main_and_handle_errors main
diff --git a/builder/test-docs.sh b/builder/test-docs.sh
index 83e2f4961..e65fdd3fe 100755
--- a/builder/test-docs.sh
+++ b/builder/test-docs.sh
@@ -23,3 +23,6 @@ $srcdir/../podcheck.pl virt-builder.pod virt-builder \
--insert $srcdir/../customize/customize-synopsis.pod:__CUSTOMIZE_SYNOPSIS__ \
--insert $srcdir/../customize/customize-options.pod:__CUSTOMIZE_OPTIONS__ \
--ignore=--check-signatures,--no-check-signatures
+
+$srcdir/../podcheck.pl virt-builder-repository.pod virt-builder-repository \
+ --ignore=--check-signatures,--no-check-signatures
diff --git a/builder/virt-builder-repository.pod b/builder/virt-builder-repository.pod
new file mode 100644
index 000000000..697deddec
--- /dev/null
+++ b/builder/virt-builder-repository.pod
@@ -0,0 +1,183 @@
+=begin html
+
+<img src="virt-builder.svg" width="250"
+ style="float: right; clear: right;" />
+
+=end html
+
+=head1 NAME
+
+virt-builder-repository - Build virt-builder source repository easily
+
+=head1 SYNOPSIS
+
+ virt-builder-repository /path/to/repository
+ [-i|--interactive] [--gpg-key KEYID]
+
+=head1 DESCRIPTION
+
+Virt-builder is a tool for quickly building new virtual machines. It can
+be configured to use template repositories. However creating and
+maintaining a repository involves many tasks which can be automated.
+virt-builder-repository is a tool helping to manage these repositories.
+
+Virt-builder-repository loops over the files in the C</path/to/repository>
+folder, compresses the files with a name ending by C<qcow2>, C<raw> or
+C<img>, extracts data from them and creates or updates the C<index> file.
+
+Some of the image-related data needed for the index file can't be
+computed from the image file. virt-builder-repository first tries to
+find them in the existing index file. If data are still missing after
+this, they are prompted in interactive mode, otherwise an error will
+be triggered.
+
+If a C<KEYID> is provided, the generated index file will be signed
+with this GPG key.
+
+=head1 EXAMPLES
+
+=head2 Create the initial repository
+
+Create a folder and copy the disk image template files in it. Then
+run a command like the following one:
+
+ virt-builder-repository --gpg-key "joe(a)hacker.org" -i /path/to/folder
+
+Note that this example command runs in interactive mode. To run in
+automated mode, a minimal index file needs to be created before running
+the command containing sections like this one:
+
+ [template_id]
+ name=template display name
+ file=template_filename.qcow.xz
+ arch=x86_64
+ revision=0
+ expand=/dev/sda3
+
+The file value needs to match the image name extended with the ".xz"
+suffix. Other optional data can be prefilled, for more informations,
+see L<virt-builder(1)/Creating and signing the index file> man page.
+
+=head2 Update images in an existing repository
+
+In this use case, an new image or a new revision of an existing image
+needs to be added to the repository. Place the corresponding image
+template files in the repository folder.
+
+To update the revision of an image, the file needs to have the same
+name than the existing one (without the C<xz> extension).
+
+As in the repository creation use case, a minimal fragment can be
+added to the index file for the automated mode. This can be done
+on the signed index even if it may sound a strange idea: the index
+will be resigned by the tool.
+
+To remove an image from the repository, just remove the corresponding
+compressed image file before running virt-builder-repository.
+
+Then running the following command will complete and update the index
+file:
+
+ virt-builder-repository --gpg-key "joe(a)hacker.org" -i /path/to/folder
+
+virt-builder-repository works in a temporary folder inside the repository
+one. If anything wrong happens when running the tool, the repository is
+left untouched.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<--help>
+
+Display help.
+
+=item B<--gpg> GPG
+
+Specify an alternate L<gpg(1)> (GNU Privacy Guard) binary. You can
+also use this to add gpg parameters, for example to specify an
+alternate home directory:
+
+ virt-builder-repository --gpg "gpg --homedir /tmp" [...]
+
+This can also be used to avoid gpg asking for the key passphrase:
+
+ virt-builder-repository --gpg "gpg --passphrase-file /tmp/pass --batch" [...]
+
+=item B<-K> KEYID
+
+=item B<--gpg-key> KEYID
+
+Specify the GPG key to be used to sign the repository index file.
+If not provided, the index will left unsigned. C<KEYID> is used to
+identify the GPG key to use. This value is passed to gpg's
+C<--default-key> option and can can thus be an email address or a
+fingerprint.
+
+B<NOTE>: by default, virt-builder-repository searches for the key
+in the user's GPG key ring.
+
+=item B<--keep-index>
+
+When using a GPG key, don't remove the unsigned index.
+
+=item B<-i>
+
+=item B<--interactive>
+
+Prompt for missing data.
+
+=item B<--colors>
+
+=item B<--colours>
+
+Use ANSI colour sequences to colourize messages. This is the default
+when the output is a tty. If the output of the program is redirected
+to a file, ANSI colour sequences are disabled unless you use this
+option.
+
+=item B<-q>
+
+=item B<--quiet>
+
+Don't print ordinary progress messages.
+
+=item B<-v>
+
+=item B<--verbose>
+
+Enable debug messages and/or produce verbose output.
+
+When reporting bugs, use this option and attach the complete output to
+your bug report.
+
+=item B<-V>
+
+=item B<--version>
+
+Display version number and exit.
+
+=item B<-x>
+
+Enable tracing of libguestfs API calls.
+
+
+=back
+
+=head1 EXIT STATUS
+
+This program returns 0 if successful, or non-zero if there was an
+error.
+
+=head1 SEE ALSO
+
+L<virt-builder(1)>
+L<http://libguestfs.org/>.
+
+=head1 AUTHOR
+
+Cédric Bosdonnat L<mailto:cbosdonnat@suse.com>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2016 SUSE Inc.
--
2.11.0