[Libguestfs] [PATCH 02/10] New API: disk-create for creating blank disks.

Richard W.M. Jones
2014-01-28T16:24:49Z

This is a wrapper around either 'qemu-img create' or calls to open,
truncate and posix_fallocate which litter and complicate existing
code.
---
 Makefile.am                      |   1 +
 configure.ac                     |   1 +
 generator/actions.ml             |  52 ++++++-
 gobject/Makefile.inc             |   2 +
 po/POTFILES                      |   2 +
 src/Makefile.am                  |   1 +
 src/create.c                     | 319 +++++++++++++++++++++++++++++++++++++++
 tests/create/Makefile.am         |  27 ++++
 tests/create/test-disk-create.sh |  48 ++++++
 9 files changed, 451 insertions(+), 2 deletions(-)
 create mode 100644 src/create.c
 create mode 100644 tests/create/Makefile.am
 create mode 100755 tests/create/test-disk-create.sh

diff --git a/Makefile.am b/Makefile.am
index e39d11f..5b8f109 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -39,6 +39,7 @@ SUBDIRS += tests/tmpdirs
 SUBDIRS += tests/protocol
 SUBDIRS += tests/events
 SUBDIRS += tests/parallel
+SUBDIRS += tests/create
 SUBDIRS += tests/disks
 SUBDIRS += tests/mountable
 SUBDIRS += tests/network
diff --git a/configure.ac b/configure.ac
index c07462e..df8e962 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1745,6 +1745,7 @@ AC_CONFIG_FILES([Makefile
                  tests/btrfs/Makefile
                  tests/c-api/Makefile
                  tests/charsets/Makefile
+                 tests/create/Makefile
                  tests/data/Makefile
                  tests/disks/Makefile
                  tests/disks/test-qemu-drive-libvirt.xml
diff --git a/generator/actions.ml b/generator/actions.ml
index fa1a2c5..3637da0 100644
--- a/generator/actions.ml
+++ b/generator/actions.ml
@@ -3093,6 +3093,54 @@ Return the current backend settings.

 See L<guestfs(3)/BACKEND>, L<guestfs(3)/BACKEND SETTINGS>." };

+  { defaults with
+    name = "disk_create";
+    style = RErr, [String "filename"; String "format";
Int64 "size"], [OString "backingfile"; OString
"backingformat"; OString "preallocation"; OString
"compat"; OInt "clustersize"];
+    test_excuse = "tests in tests/create subdirectory";
+    shortdesc = "create a blank disk image";
+    longdesc = "\
+Create a blank disk image called C<filename> (a host file or device)
+with format C<format> (usually C<raw> or C<qcow2>, but other
formats
+are possible).  The size is C<size> bytes.
+
+If used with the optional C<backingfile> parameter, then a snapshot
+is created on top of the backing file.  In this case, C<size> must
+be passed as C<-1>.  The size of the snapshot is the same as the
+size of the backing file, which is discovered automatically.  You
+are encouraged to also pass C<backingformat> to describe the format
+of C<backingfile>.
+
+The other optional parameters are:
+
+=over 4
+
+=item C<preallocation>
+
+If format is C<raw>, then this can be either C<sparse> or
C<full>
+to create a sparse or fully allocated file respectively.  The default
+is C<sparse>.
+
+If format is C<qcow2>, then this can be either C<off> or
+C<metadata>.  Preallocating metadata can be faster when doing lots
+of writes, but uses more space.
+
+=item C<compat>
+
+C<qcow2> only:
+Pass the string C<1.1> to use the advanced qcow2 format supported
+by qemu E<ge> 1.1.
+
+=item C<clustersize>
+
+C<qcow2> only:
+Change the qcow2 cluster size.  The default is 65536 (bytes) and
+this settig may be any power of two between 512 and 2097152.
+
+=back
+
+Note that this call does not add the new disk to the handle.  You
+may need to call C<guestfs_add_drive_opts> separately." };
+
 ]

 (* daemon_functions are any functions which cause some action
@@ -11739,7 +11787,7 @@ let fish_commands = [
 This creates an empty (zeroed) file of the given size, and then adds
 so it can be further examined.

-For more advanced image creation, see L<qemu-img(1)> utility.
+For more advanced image creation, see L</disk-create>.

 Size can be specified using standard suffixes, eg. C<1M>.

@@ -11983,7 +12031,7 @@ not assigned to the file until they are needed.  Sparse
disk files
 only use space when written to, but they are slower and there is a
 danger you could run out of real disk space during a write operation.

-For more advanced image creation, see L<qemu-img(1)> utility.
+For more advanced image creation, see L</disk-create>.

 Size can be specified using standard suffixes, eg. C<1M>.

diff --git a/gobject/Makefile.inc b/gobject/Makefile.inc
index 7024754..7a8493a 100644
--- a/gobject/Makefile.inc
+++ b/gobject/Makefile.inc
@@ -52,6 +52,7 @@ guestfs_gobject_headers= \
   include/guestfs-gobject/optargs-mount_local.h \
   include/guestfs-gobject/optargs-umount_local.h \
   include/guestfs-gobject/optargs-add_drive_scratch.h \
+  include/guestfs-gobject/optargs-disk_create.h \
   include/guestfs-gobject/optargs-is_file.h \
   include/guestfs-gobject/optargs-is_dir.h \
   include/guestfs-gobject/optargs-umount.h \
@@ -127,6 +128,7 @@ guestfs_gobject_sources= \
   src/optargs-mount_local.c \
   src/optargs-umount_local.c \
   src/optargs-add_drive_scratch.c \
+  src/optargs-disk_create.c \
   src/optargs-is_file.c \
   src/optargs-is_dir.c \
   src/optargs-umount.c \
diff --git a/po/POTFILES b/po/POTFILES
index c6277e9..76d1b1d 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -170,6 +170,7 @@ gobject/src/optargs-copy_device_to_device.c
 gobject/src/optargs-copy_device_to_file.c
 gobject/src/optargs-copy_file_to_device.c
 gobject/src/optargs-copy_file_to_file.c
+gobject/src/optargs-disk_create.c
 gobject/src/optargs-e2fsck.c
 gobject/src/optargs-fstrim.c
 gobject/src/optargs-grep.c
@@ -267,6 +268,7 @@ src/canonical-name.c
 src/cleanup.c
 src/command.c
 src/conn-socket.c
+src/create.c
 src/dbdump.c
 src/drives.c
 src/errnostring-gperf.c
diff --git a/src/Makefile.am b/src/Makefile.am
index ba02061..4475281 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -90,6 +90,7 @@ libguestfs_la_SOURCES = \
 	canonical-name.c \
 	command.c \
 	conn-socket.c \
+	create.c \
 	dbdump.c \
 	drives.c \
 	errors.c \
diff --git a/src/create.c b/src/create.c
new file mode 100644
index 0000000..18c4402
--- /dev/null
+++ b/src/create.c
@@ -0,0 +1,319 @@
+/* libguestfs
+ * Copyright (C) 2012 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <errno.h>
+
+#include "guestfs.h"
+#include "guestfs-internal.h"
+#include "guestfs-internal-actions.h"
+#include "guestfs_protocol.h"
+
+static int disk_create_raw (guestfs_h *g, const char *filename, int64_t size,
const struct guestfs_disk_create_argv *optargs);
+static int disk_create_qcow2 (guestfs_h *g, const char *filename, int64_t size,
const char *backingfile, const struct guestfs_disk_create_argv *optargs);
+static char *qemu_escape_param (guestfs_h *g, const char *param);
+
+int
+guestfs__disk_create (guestfs_h *g, const char *filename,
+                      const char *format, int64_t size,
+                      const struct guestfs_disk_create_argv *optargs)
+{
+  const char *backingfile;
+
+  backingfile = optargs->bitmask &
GUESTFS_DISK_CREATE_BACKINGFILE_BITMASK ?
+    optargs->backingfile : NULL;
+
+  /* Ensure size is valid. */
+  if (backingfile) {
+    if (size != -1) {
+      error (g, _("if using a backing file, size must be passed as
-1"));
+      return -1;
+    }
+  } else {
+    /* XXX Actually size == 0 could be valid, although not useful and
+     * it causes qemu to break.
+     */
+    if (size <= 0) {
+      error (g, _("invalid size: %" PRIi64), size);
+      return -1;
+    }
+  }
+
+  /* Now the format-specific code. */
+  if (STREQ (format, "raw")) {
+    if (backingfile) {
+      error (g, _("backingfile cannot be used for raw format
disks"));
+      return -1;
+    }
+    if (disk_create_raw (g, filename, size, optargs) == -1)
+      return -1;
+  }
+  else if (STREQ (format, "qcow2")) {
+    if (disk_create_qcow2 (g, filename, size, backingfile, optargs) == -1)
+      return -1;
+  }
+  else {
+    /* Be conservative about what formats we support, since we don't
+     * want to make unlimited promises through the API.  We can always
+     * add more later.
+     */
+    error (g, _("unsupported format '%s'"), format);
+    return -1;
+  }
+
+  return 0;
+}
+
+static int
+disk_create_raw (guestfs_h *g, const char *filename, int64_t size,
+                 const struct guestfs_disk_create_argv *optargs)
+{
+  int allocated = 0;
+  int fd;
+  struct stat statbuf;
+
+  /* backingfile parameter not present checked above */
+
+  if (optargs->bitmask & GUESTFS_DISK_CREATE_BACKINGFORMAT_BITMASK) {
+    error (g, _("backingformat parameter cannot be used with raw
format"));
+    return -1;
+  }
+  if (optargs->bitmask & GUESTFS_DISK_CREATE_PREALLOCATION_BITMASK) {
+    if (STREQ (optargs->preallocation, "sparse"))
+      allocated = 0;
+    else if (STREQ (optargs->preallocation, "full"))
+      allocated = 1;
+    else {
+      error (g, _("invalid value for preallocation parameter
'%s'"),
+             optargs->preallocation);
+      return -1;
+    }
+  }
+  if (optargs->bitmask & GUESTFS_DISK_CREATE_COMPAT_BITMASK) {
+    error (g, _("compat parameter cannot be used with raw format"));
+    return -1;
+  }
+  if (optargs->bitmask & GUESTFS_DISK_CREATE_CLUSTERSIZE_BITMASK) {
+    error (g, _("clustersize parameter cannot be used with raw
format"));
+    return -1;
+  }
+
+  /* This version refuses the overwrite block devices or char devices.
+   * XXX It would be possible to make it work with block devices.
+   */
+  if (stat (filename, &statbuf) == 0) {
+    if (S_ISBLK (statbuf.st_mode)) {
+      error (g, _("refusing to overwrite block device '%s'"),
filename);
+      return -1;
+    }
+    if (S_ISCHR (statbuf.st_mode)) {
+      error (g, _("refusing to overwrite char device '%s'"),
filename);
+      return -1;
+    }
+  }
+
+  fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_TRUNC|O_CLOEXEC, 0666);
+  if (fd == -1) {
+    perrorf (g, _("cannot create raw file: %s"), filename);
+    return -1;
+  }
+
+  if (!allocated) {             /* Sparse file. */
+    if (ftruncate (fd, size) == -1) {
+      perrorf (g, _("%s: truncate"), filename);
+      close (fd);
+      unlink (filename);
+      return -1;
+    }
+  }
+  else {                        /* Allocated file. */
+#ifdef HAVE_POSIX_FALLOCATE
+    int err;
+
+    err = posix_fallocate (fd, 0, size);
+    if (err != 0) {
+      errno = err;
+      perrorf (g, _("%s: fallocate"), filename);
+      close (fd);
+      unlink (filename);
+      return -1;
+    }
+#else
+    /* Slow emulation of posix_fallocate on platforms which don't have it.
*/
+    char buffer[BUFSIZ];
+    size_t remaining = size;
+    size_t n;
+    ssize_t r;
+
+    memset (buffer, 0, sizeof buffer);
+
+    while (remaining > 0) {
+      n = remaining > sizeof buffer ? sizeof buffer : remaining;
+      r = write (fd, buffer, n);
+      if (r == -1) {
+        perrorf (g, _("%s: write"), filename);
+        close (fd);
+        unlink (filename);
+        return -1;
+      }
+      remaining -= r;
+    }
+#endif
+  }
+
+  if (close (fd) == -1) {
+    perrorf (g, _("%s: close"), filename);
+    unlink (filename);
+    return -1;
+  }
+
+  return 0;
+}
+
+/* http://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 */
+static int
+is_power_of_2 (unsigned v)
+{
+  return v && ((v & (v - 1)) == 0);
+}
+
+static int
+disk_create_qcow2 (guestfs_h *g, const char *orig_filename, int64_t size,
+                   const char *backingfile,
+                   const struct guestfs_disk_create_argv *optargs)
+{
+  CLEANUP_FREE char *filename = NULL;
+  const char *backingformat = NULL;
+  const char *preallocation = NULL;
+  const char *compat = NULL;
+  int clustersize = -1;
+  CLEANUP_FREE_STRINGSBUF DECLARE_STRINGSBUF (optionsv);
+  CLEANUP_CMD_CLOSE struct command *cmd = guestfs___new_command (g);
+  int r;
+
+  /* If the filename is something like "file:foo" then qemu-img will
+   * try to interpret that as "foo" in the file:/// protocol.  To
+   * avoid that, if the path is relative prefix it with "./" since
+   * qemu-img won't try to interpret such a path.
+   */
+  if (orig_filename[0] != '/')
+    filename = safe_asprintf (g, "./%s", orig_filename);
+  else
+    filename = safe_strdup (g, orig_filename);
+
+  if (optargs->bitmask & GUESTFS_DISK_CREATE_BACKINGFORMAT_BITMASK) {
+    backingformat = optargs->backingformat;
+    if (STRNEQ (backingformat, "raw") && STRNEQ
(backingformat, "qcow2")) {
+      error (g, _("invalid value for backingformat parameter
'%s'"),
+             backingformat);
+      return -1;
+    }
+  }
+  if (optargs->bitmask & GUESTFS_DISK_CREATE_PREALLOCATION_BITMASK) {
+    preallocation = optargs->preallocation;
+    if (STRNEQ (preallocation, "off") && STRNEQ
(preallocation, "metadata")) {
+      error (g, _("invalid value for preallocation parameter
'%s'"),
+             preallocation);
+      return -1;
+    }
+  }
+  if (optargs->bitmask & GUESTFS_DISK_CREATE_COMPAT_BITMASK) {
+    compat = optargs->compat;
+    if (STRNEQ (compat, "0.10") && STRNEQ (compat,
"1.1")) {
+      error (g, _("invalid value for compat parameter '%s'"),
compat);
+      return -1;
+    }
+  }
+  if (optargs->bitmask & GUESTFS_DISK_CREATE_CLUSTERSIZE_BITMASK) {
+    clustersize = optargs->clustersize;
+    if (clustersize < 512 || clustersize > 2097152 ||
+        !is_power_of_2 ((unsigned) clustersize)) {
+      error (g, _("invalid value for clustersize parameter
'%d'"),
+             clustersize);
+      return -1;
+    }
+  }
+
+  /* Assemble the qemu-img command line. */
+  guestfs___cmd_add_arg (cmd, "qemu-img");
+  guestfs___cmd_add_arg (cmd, "create");
+  guestfs___cmd_add_arg (cmd, "-f");
+  guestfs___cmd_add_arg (cmd, "qcow2");
+
+  /* -o parameter. */
+  if (backingfile) {
+    CLEANUP_FREE char *p = qemu_escape_param (g, backingfile);
+    guestfs___add_sprintf (g, &optionsv, "backing_file=%s", p);
+  }
+  if (backingformat)
+    guestfs___add_sprintf (g, &optionsv, "backing_fmt=%s",
backingformat);
+  if (preallocation)
+    guestfs___add_sprintf (g, &optionsv, "preallocation=%s",
preallocation);
+  if (compat)
+    guestfs___add_sprintf (g, &optionsv, "compat=%s", compat);
+  if (clustersize >= 0)
+    guestfs___add_sprintf (g, &optionsv, "cluster_size=%d",
clustersize);
+  guestfs___end_stringsbuf (g, &optionsv);
+
+  if (optionsv.size > 1) {
+    CLEANUP_FREE char *options = guestfs___join_strings (",",
optionsv.argv);
+    guestfs___cmd_add_arg (cmd, "-o");
+    guestfs___cmd_add_arg (cmd, options);
+  }
+
+  /* Complete the command line. */
+  guestfs___cmd_add_arg (cmd, filename);
+  if (size >= 0)
+    guestfs___cmd_add_arg_format (cmd, "%" PRIi64, size);
+
+  r = guestfs___cmd_run (cmd);
+  if (!WIFEXITED (r) || WEXITSTATUS (r) != 0) {
+    guestfs___external_command_failed (g, r, "qemu-img",
orig_filename);
+    return -1;
+  }
+
+  return 0;
+}
+
+/* XXX Duplicated in launch-direct.c. */
+static char *
+qemu_escape_param (guestfs_h *g, const char *param)
+{
+  size_t i, len = strlen (param);
+  char *p, *ret;
+
+  ret = p = safe_malloc (g, len*2 + 1); /* max length of escaped name*/
+  for (i = 0; i < len; ++i) {
+    *p++ = param[i];
+    if (param[i] == ',')
+      *p++ = ',';
+  }
+  *p = '\0';
+
+  return ret;
+}
diff --git a/tests/create/Makefile.am b/tests/create/Makefile.am
new file mode 100644
index 0000000..cdcecd2
--- /dev/null
+++ b/tests/create/Makefile.am
@@ -0,0 +1,27 @@
+# libguestfs
+# Copyright (C) 2014 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+include $(top_srcdir)/subdir-rules.mk
+
+TESTS = \
+	test-disk-create.sh
+
+TESTS_ENVIRONMENT = \
+	$(top_builddir)/run --test
+
+EXTRA_DIST = \
+	$(TESTS)
diff --git a/tests/create/test-disk-create.sh b/tests/create/test-disk-create.sh
new file mode 100755
index 0000000..b287233
--- /dev/null
+++ b/tests/create/test-disk-create.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+# Copyright (C) 2014 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# Test the disk-create API.
+
+export LANG=C
+
+set -e
+
+rm -f disk*.img file:*.img
+
+# XXX We should also test failure paths.
+
+../../fish/guestfish <<EOF
+  disk-create disk1.img  raw   256K
+  disk-create disk2.img  raw   256K preallocation:sparse
+  disk-create disk3.img  raw   256K preallocation:full
+  disk-create disk4.img  qcow2 256K
+  disk-create disk5.img  qcow2 256K preallocation:off
+  disk-create disk6.img  qcow2 256K preallocation:metadata
+  disk-create disk7.img  qcow2 256K compat:1.1
+  disk-create disk8.img  qcow2 256K clustersize:128K
+  disk-create disk9.img  qcow2 -1   backingfile:disk1.img compat:1.1
+  disk-create disk10.img qcow2 -1   backingfile:disk2.img backingformat:raw
+  disk-create disk11.img qcow2 -1   backingfile:disk4.img backingformat:qcow2
+
+  # Some annoying corner-cases in qemu-img.
+  disk-create disk:0.img qcow2 256K
+  disk-create file:0.img qcow2 256K
+  disk-create disk,0.img qcow2 256K
+  disk-create disk,,0.img qcow2 256K
+EOF
+
+rm disk*.img file:*.img
--
1.8.4.2