content: // Licensed to the Apache Software Foundation (ASF) under one
  // or more contributor license agreements.  See the NOTICE file
  // distributed with this work for additional information
  // regarding copyright ownership.  The ASF licenses this file
  // to you under the Apache License, Version 2.0 (the
  // "License"); you may not use this file except in compliance
  // the License.  You may obtain a copy of the License at
  //
  // http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing,
  // software distributed under the License is distributed on an
  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  // KIND, either express or implied.  See the License for the
  // specific language governing permissions and limitations
  // under the License.
  package org.apache.cloudstack.utils.qemu;
  
  import java.nio.file.Files;
  import java.nio.file.Paths;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.regex.Pattern;
  
  import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector;
  import org.apache.commons.lang.NotImplementedException;
  import org.apache.commons.lang3.StringUtils;
  import org.libvirt.LibvirtException;
  
  import com.cloud.hypervisor.kvm.resource.LibvirtConnection;
  import com.cloud.storage.Storage;
  import com.cloud.utils.script.OutputInterpreter;
  import com.cloud.utils.script.Script;
  import org.apache.logging.log4j.Logger;
  import org.apache.logging.log4j.LogManager;
  
  import static java.util.regex.Pattern.CASE_INSENSITIVE;
  
  public class QemuImg {
      private Logger logger = LogManager.getLogger(this.getClass());
  
      public static final String BACKING_FILE = "backing_file";
      public static final String BACKING_FILE_FORMAT = "backing_file_format";
      public static final String CLUSTER_SIZE = "cluster_size";
      public static final String FILE_FORMAT = "file_format";
      public static final String IMAGE = "image";
      public static final String VIRTUAL_SIZE = "virtual_size";
      public static final String ENCRYPTED = "encrypted";
      public static final String ENCRYPT_FORMAT = "encrypt.format";
      public static final String ENCRYPT_KEY_SECRET = "encrypt.key-secret";
      public static final String TARGET_ZERO_FLAG = "--target-is-zero";
      public static final long QEMU_2_10 = 2010000;
      public static final long QEMU_5_10 = 5010000;
  
      public static final int MIN_BITMAP_VERSION = 3;
  
      /* The qemu-img binary. We expect this to be in $PATH */
      public String _qemuImgPath = "qemu-img";
      private String cloudQemuImgPath = "cloud-qemu-img";
      private int timeout;
      private boolean skipZero = false;
      private boolean skipTargetVolumeCreation = false;
      private boolean noCache = false;
      private long version;
  
      private String getQemuImgPathScript = String.format("which %s >& /dev/null; " +
                      "if [ $? -gt 0 ]; then echo \"%s\"; else echo \"%s\"; fi",
              cloudQemuImgPath, _qemuImgPath, cloudQemuImgPath);
  
      /* Shouldn't we have KVMPhysicalDisk and LibvirtVMDef read this? */
      public static enum PhysicalDiskFormat {
          RAW("raw"), QCOW2("qcow2"), VMDK("vmdk"), FILE("file"), RBD("rbd"), SHEEPDOG("sheepdog"), HTTP("http"), HTTPS("https"), TAR("tar"), DIR("dir"), LUKS("luks");
          String format;
  
          private PhysicalDiskFormat(final String format) {
              this.format = format;
          }
  
          @Override
          public String toString() {
              return format;
          }
      }
  
      public static enum PreallocationType {
          Off("off"),
          Metadata("metadata"),
          Full("full");
  
          private final String preallocationType;
  
          private PreallocationType(final String preallocationType){
              this.preallocationType = preallocationType;
          }
  
          @Override
          public String toString(){
              return preallocationType;
          }
  
          public static PreallocationType getPreallocationType(final Storage.ProvisioningType provisioningType){
              switch (provisioningType){
                  case THIN:
                      return PreallocationType.Off;
                  case SPARSE:
                      return PreallocationType.Metadata;
                  case FAT:
                      return PreallocationType.Full;
                  default:
                      throw new NotImplementedException(String.format("type %s not defined as member-value of PreallocationType", provisioningType));
              }
          }
      }
  
      public enum BitmapOperation {
          Add, Remove, Clear, Enable, Disable, Merge
      }
  
      /**
       * Create a QemuImg object that supports skipping target zeroes
       * We detect this support via qemu-img help since support can
       * be backported rather than found in a specific version.
       *
       * @param timeout script timeout, default 0
       * @param skipZeroIfSupported Don't write zeroes to target device during convert, if supported by qemu-img
       * @param noCache Ensure we flush writes to target disk (useful for block device targets)
       */
      public QemuImg(final int timeout, final boolean skipZeroIfSupported, final boolean noCache) throws LibvirtException {
          if (skipZeroIfSupported) {
              final Script s = new Script(_qemuImgPath, timeout);
              s.add("--help");
  
              final OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser();
              final String result = s.execute(parser);
  
              // Older Qemu returns output in result due to --help reporting error status
              if (result != null) {
                  if (result.contains(TARGET_ZERO_FLAG)) {
                      this.skipZero = true;
                  }
              } else {
                  if (parser.getLines().contains(TARGET_ZERO_FLAG)) {
                      this.skipZero = true;
                  }
              }
          }
          this.timeout = timeout;
          this.noCache = noCache;
          this.version = LibvirtConnection.getConnection().getVersion();
      }
  
      /**
       * Creates a QemuImg object.
       *
       * @param timeout
       *            The timeout of scripts executed by this QemuImg object.
       */
      public QemuImg(final int timeout) throws LibvirtException, QemuImgException {
          this(timeout, false, false);
      }
  
      /**
       * Sets the timeout of the scripts executed by this QemuImg object.
       *
       * @param timeout
       *            The timeout of the object.
       * @return void
       */
      public void setTimeout(final int timeout) {
          this.timeout = timeout;
      }
  
      /**
       * Creates a QemuImg object.
       *
       * @param qemuImgPath
       *            An alternative path to the qemu-img binary.
       * @return void
       */
      public QemuImg(final String qemuImgPath) throws LibvirtException {
          this(0, false, false);
          _qemuImgPath = qemuImgPath;
      }
  
      /* These are all methods supported by the qemu-img tool. */
  
      /**
       * Creates a new image.
       *
       * This method is a facade for 'qemu-img create'.
       *
       * @param file
       *            The file to be created.
       * @param backingFile
       *            A backing file, if used (for example with qcow2).
       * @param options
       *            Options for the create. Takes a Map<String, String> with key value
       *            pairs which are passed on to qemu-img without validation.
       * @return void
       */
      public void create(final QemuImgFile file, final QemuImgFile backingFile, final Map<String, String> options) throws QemuImgException {
          create(file, backingFile, options, null);
      }
  
      /**
       * Creates a new image.
       *
       * This method is a facade for 'qemu-img create'.
       *
       * @param file
       *            The file to be created.
       * @param backingFile
       *            A backing file, if used (for example with qcow2).
       * @param options
       *            Options for the creation. Takes a Map<String, String> with key value
       *            pairs which are passed on to qemu-img without validation.
       * @param qemuObjects
       *            Pass list of qemu Object to create - see objects in qemu man page.
       * @return void
       */
      public void create(final QemuImgFile file, final QemuImgFile backingFile, final Map<String, String> options, final List<QemuObject> qemuObjects) throws QemuImgException {
          final Script s = new Script(_qemuImgPath, timeout);
          s.add("create");
  
          if (this.version >= QEMU_2_10 && qemuObjects != null) {
              for (QemuObject o : qemuObjects) {
                  s.add(o.toCommandFlag());
              }
          }
  
          if (options != null && !options.isEmpty()) {
              s.add("-o");
              final StringBuilder optionsStr = new StringBuilder();
              final Iterator<Map.Entry<String, String>> optionsIter = options.entrySet().iterator();
              while(optionsIter.hasNext()){
                  final Map.Entry option = optionsIter.next();
                  optionsStr.append(option.getKey()).append('=').append(option.getValue());
                  if(optionsIter.hasNext()){
                      //Add "," only if there are more options
                      optionsStr.append(',');
                  }
              }
              s.add(optionsStr.toString());
          }
  
          /*
              -b for a backing file does not show up in the docs, but it works.
              Shouldn't this be -o backing_file=filename instead?
           */
          s.add("-f");
          if (backingFile != null) {
              s.add(backingFile.getFormat().toString());
              s.add("-F");
              s.add(backingFile.getFormat().toString());
              s.add("-b");
              s.add(backingFile.getFileName());
          } else {
              s.add(file.getFormat().toString());
          }
  
          s.add(file.getFileName());
          if (file.getSize() != 0L) {
              s.add(Long.toString(file.getSize()));
          } else if (backingFile == null) {
              throw new QemuImgException("No size was passed, and no backing file was passed");
          }
  
          final String result = s.execute();
          if (result != null) {
              throw new QemuImgException(result);
          }
      }
  
      /**
       * Creates a new image.
       *
       * This method is a facade for {@link QemuImg#create(QemuImgFile, QemuImgFile, Map)}.
       *
       * @param file
       *            The file to be created.
       * @return void
       */
      public void create(final QemuImgFile file) throws QemuImgException {
          this.create(file, null, null);
      }
  
      /**
       * Creates a new image.
       *
       * This method is a facade for {@link QemuImg#create(QemuImgFile, QemuImgFile, Map)}.
       *
       * @param file
       *            The file to be created.
       * @param backingFile
       *            A backing file, if used (for example with qcow2).
       * @return void
       */
      public void create(final QemuImgFile file, final QemuImgFile backingFile) throws QemuImgException {
          this.create(file, backingFile, null);
      }
  
      /**
       * Creates a new image.
       *
       * This method is a facade for {@link QemuImg#create(QemuImgFile, QemuImgFile, Map)}.
       *
       * @param file
       *            The file to be created.
       * @param options
       *            Options for the creation. Takes a Map<String, String> with key value
       *            pairs which are passed on to qemu-img without validation.
       * @return void
       */
      public void create(final QemuImgFile file, final Map<String, String> options) throws QemuImgException {
          this.create(file, null, options);
      }
  
      /**
       * Converts an image from source to destination.
       *
       * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format.
       *
       * @param srcFile
       *            The source file.
       * @param destFile
       *            The destination file.
       * @param options
       *            Options for the conversion. Takes a Map<String, String> with key value
       *            pairs which are passed on to qemu-img without validation.
       * @param snapshotName
       *            If it is provided, conversion uses it as parameter.
       * @param forceSourceFormat
       *            If true, specifies the source format in the conversion command.
       * @return void
       */
      public void convert(final QemuImgFile srcFile, final QemuImgFile destFile,
                          final Map<String, String> options, final String snapshotName, final boolean forceSourceFormat) throws QemuImgException, LibvirtException {
          convert(srcFile, destFile, options, null, snapshotName, forceSourceFormat);
      }
  
      /**
       * Converts an image from source to destination.
       *
       * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format.
       *
       * @param srcFile
       *            The source file.
       * @param destFile
       *            The destination file.
       * @param options
       *            Options for the conversion. Takes a Map<String, String> with key value
       *            pairs which are passed on to qemu-img without validation.
       * @param qemuObjects
       *            Pass qemu Objects to create - see objects in the qemu main page.
       * @param snapshotName
       *            If it is provided, conversion uses it as parameter.
       * @param forceSourceFormat
       *            If true, specifies the source format in the conversion command.
       * @return void
       */
      public void convert(final QemuImgFile srcFile, final QemuImgFile destFile,
                          final Map<String, String> options, final List<QemuObject> qemuObjects, final String snapshotName, final boolean forceSourceFormat) throws QemuImgException {
          QemuImageOptions imageOpts = new QemuImageOptions(srcFile.getFormat(), srcFile.getFileName(), null);
          convert(srcFile, destFile, options, qemuObjects, imageOpts, snapshotName, forceSourceFormat);
      }
  
      /**
       * Converts an image from source to destination.
       *
       * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format.
       *
       * @param srcFile
       *            The source file.
       * @param destFile
       *            The destination file.
       * @param options
       *            Options for the conversion. Takes a Map<String, String> with key value
       *            pairs which are passed on to qemu-img without validation.
       * @param qemuObjects
       *            Pass qemu Objects to create - see objects in the qemu main page.
       * @param srcImageOpts
       *            pass qemu --image-opts to convert.
       * @param snapshotName
       *            If it is provided, conversion uses it as parameter.
       * @param forceSourceFormat
       *            If true, specifies the source format in the conversion command.
       * @return void
       */
      public void convert(final QemuImgFile srcFile, final QemuImgFile destFile,
                          final Map<String, String> options, final List<QemuObject> qemuObjects, final QemuImageOptions srcImageOpts, final String snapshotName, final boolean forceSourceFormat) throws QemuImgException {
          convert(srcFile, destFile, options, qemuObjects, srcImageOpts, snapshotName, forceSourceFormat, false);
      }
  
      /**
       * Converts an image from source to destination.
       *
       * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format.
       *
       * @param srcFile
       *            The source file.
       * @param destFile
       *            The destination file.
       * @param options
       *            Options for the conversion. Takes a Map<String, String> with key value
       *            pairs which are passed on to qemu-img without validation.
       * @param qemuObjects
       *            Pass qemu Objects to create - see objects in the qemu main page.
       * @param srcImageOpts
       *            pass qemu --image-opts to convert.
       * @param snapshotName
       *            If it is provided, conversion uses it as parameter.
       * @param forceSourceFormat
       *            If true, specifies the source format in the conversion command.
       * @param keepBitmaps
       *            If true, copies the bitmaps to the destination image.
       * @return void
       */
      public void convert(final QemuImgFile srcFile, final QemuImgFile destFile,
                          final Map<String, String> options, final List<QemuObject> qemuObjects, final QemuImageOptions srcImageOpts, final String snapshotName, final boolean forceSourceFormat,
                          boolean keepBitmaps) throws QemuImgException {
  
          Script script = new Script(_qemuImgPath, timeout);
          if (StringUtils.isNotBlank(snapshotName)) {
              String qemuPath = Script.runSimpleBashScript(getQemuImgPathScript);
              script = new Script(qemuPath, timeout);
          }
  
          script.add("convert");
  
          if (skipZero && Files.exists(Paths.get(destFile.getFileName()))) {
              script.add("-n");
              script.add(TARGET_ZERO_FLAG);
              script.add("-W");
              // with target-is-zero we skip zeros in 1M chunks for compatibility
              script.add("-S");
              script.add("1M");
          } else if (skipTargetVolumeCreation) {
              script.add("-n");
          }
  
          script.add("-O");
          script.add(destFile.getFormat().toString());
  
          addScriptOptionsFromMap(options, script);
          addSnapshotToConvertCommand(srcFile.getFormat().toString(), snapshotName, forceSourceFormat, script, version);
  
          if (noCache) {
              script.add("-t");
              script.add("none");
          }
  
          if (this.version >= QEMU_2_10) {
              script.add("-U");
  
              if (forceSourceFormat) {
                  srcImageOpts.setFormat(srcFile.getFormat());
              }
              script.add(srcImageOpts.toCommandFlag());
  
              if (qemuObjects != null) {
                  for (QemuObject o : qemuObjects) {
                      script.add(o.toCommandFlag());
                  }
              }
          } else {
              if (forceSourceFormat) {
                  script.add("-f");
                  script.add(srcFile.getFormat().toString());
              }
              script.add(srcFile.getFileName());
          }
  
          if (this.version >= QEMU_5_10 && keepBitmaps && Qcow2Inspector.validateQcow2Version(srcFile.getFileName(), MIN_BITMAP_VERSION)) {
              script.add("--bitmaps");
          }
  
          script.add(destFile.getFileName());
  
          final String result = script.execute();
          if (result != null) {
              throw new QemuImgException(result);
          }
  
          if (srcFile.getSize() < destFile.getSize()) {
              this.resize(destFile, destFile.getSize());
          }
      }
  
      /**
       * Qemu version 2.0.0 added (via commit <a href="https://github.com/qemu/qemu/commit/ef80654d0dc1edf2dd2a51feff8cc3e1102a6583">ef80654d0dc1edf2dd2a51feff8cc3e1102a6583</a>) the
       * flag "-l" to inform the snapshot name or ID
       */
      private static final int QEMU_VERSION_THAT_ADDS_FLAG_L_TO_CONVERT_SNAPSHOT = 2000000;
  
      /**
       * Adds a flag to inform snapshot name or ID on conversion. If the QEMU version is less than {@link QemuImg#QEMU_VERSION_THAT_ADDS_FLAG_L_TO_CONVERT_SNAPSHOT), adds the
       * flag "-s", otherwise, adds the flag "-l".
       */
      protected void addSnapshotToConvertCommand(String srcFormat, String snapshotName, boolean forceSourceFormat, Script script, Long qemuVersion) {
          if (StringUtils.isBlank(snapshotName)) {
              return;
          }
  
          if (qemuVersion >= QEMU_VERSION_THAT_ADDS_FLAG_L_TO_CONVERT_SNAPSHOT) {
              script.add("-l");
              script.add(String.format("snapshot.name=%s", snapshotName));
              return;
          }
  
          logger.debug(String.format("Current QEMU version [%s] does not support flag \"-l\" (added on version >= 2.0.0) to inform the snapshot name or ID on conversion."
                  + " Adding the old flag \"-s\" instead.", qemuVersion));
  
          if (!forceSourceFormat) {
              script.add("-f");
              script.add(srcFormat);
          }
  
          script.add("-s");
          script.add(snapshotName);
      }
  
       /**
       * Converts an image from source to destination.
       *
       * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format.
       *
       * @param srcFile
       *            The source file.
       * @param destFile
       *            The destination file.
       * @param options
       *            Options for the conversion. Takes a Map<String, String> with key value
       *            pairs which are passed on to qemu-img without validation.
       * @param snapshotName
       *            If it is provided, conversion uses it as parameter.
       * @return void
       */
      public void convert(final QemuImgFile srcFile, final QemuImgFile destFile,
                          final Map<String, String> options, final String snapshotName) throws QemuImgException, LibvirtException {
          this.convert(srcFile, destFile, options, snapshotName, false);
      }
  
      /**
       * Converts an image from source to destination.
       *
       * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format.
       *
       * @param srcFile
       *            The source file.
       * @param destFile
       *            The destination file.
       * @return void
       */
      public void convert(final QemuImgFile srcFile, final QemuImgFile destFile) throws QemuImgException, LibvirtException {
          this.convert(srcFile, destFile, null, null);
      }
  
      /**
       * Converts an image from source to destination.
       *
       * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format.
       *
       * @param srcFile
       *            The source file.
       * @param destFile
       *            The destination file.
       * @param forceSourceFormat
       *            If true, specifies the source format in the conversion command.
       * @return void
       */
      public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, final boolean forceSourceFormat) throws QemuImgException, LibvirtException {
          this.convert(srcFile, destFile, null, null, forceSourceFormat);
      }
  
      /**
       * Converts an image from source to destination.
       *
       * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format.
       *
       * @param srcFile
       *            The source file.
       * @param destFile
       *            The destination file.
       * @param snapshotName
       *            The snapshot name.
       * @return void
       */
      public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, String snapshotName) throws QemuImgException, LibvirtException {
          this.convert(srcFile, destFile, null, snapshotName);
      }
  
      /**
       * Executes 'qemu-img info' for the given file. Qemu-img returns a human-readable output and this method parses the result to machine-readable data.
       * - Spaces in keys are replaced by underscores (_).
       * - Sizes (virtual_size and disk_size) are returned in bytes.
       * - Paths (image and backing_file) are the absolute path to the file.
       *
       * @param file
       *            A QemuImgFile object containing the file to get the information from.
       * @return A HashMap with string key-value information as returned by 'qemu-img info'.
       */
      public Map<String, String> info(final QemuImgFile file) throws QemuImgException {
          return info(file, true);
      }
  
      public Map<String, String> info(final QemuImgFile file, boolean secure) throws QemuImgException {
          final Script s = new Script(_qemuImgPath);
          s.add("info");
          if (this.version >= QEMU_2_10 && secure) {
              s.add("-U");
          }
          s.add(file.getFileName());
  
          final OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser();
          final String result = s.execute(parser);
          if (result != null) {
              throw new QemuImgException(result);
          }
  
          final HashMap<String, String> info = new HashMap<String, String>();
          final String[] outputBuffer = parser.getLines().trim().split("\n");
          for (int i = 0; i < outputBuffer.length; i++) {
              final String[] lineBuffer = outputBuffer[i].split(":", 2);
              if (lineBuffer.length == 2) {
                  final String key = lineBuffer[0].trim().replace(" ", "_");
                  String value = null;
  
                  if (key.equals("virtual_size")) {
                      value = lineBuffer[1].trim().replaceAll("^.*\\(([0-9]+).*$", "$1");
                  } else {
                      value = lineBuffer[1].trim();
                  }
  
                  info.put(key, value);
              }
          }
  
          // set some missing attributes in passed file, if found
          if (info.containsKey(VIRTUAL_SIZE) && file.getSize() == 0L) {
              file.setSize(Long.parseLong(info.get(VIRTUAL_SIZE)));
          }
  
          if (info.containsKey(FILE_FORMAT) && file.getFormat() == null) {
              file.setFormat(PhysicalDiskFormat.valueOf(info.get(FILE_FORMAT).toUpperCase()));
          }
  
          return info;
      }
  
      /* create snapshots in image */
      public void snapshot(final QemuImageOptions srcImageOpts, final String snapshotName, final List<QemuObject> qemuObjects) throws QemuImgException {
          final Script s = new Script(_qemuImgPath, timeout);
          s.add("snapshot");
          s.add("-c");
          s.add(snapshotName);
  
          for (QemuObject o : qemuObjects) {
              s.add(o.toCommandFlag());
          }
  
          s.add(srcImageOpts.toCommandFlag());
  
          final String result = s.execute();
          if (result != null) {
              throw new QemuImgException(result);
          }
      }
  
      /* delete snapshots in image */
      public void deleteSnapshot(final QemuImageOptions srcImageOpts, final String snapshotName, final List<QemuObject> qemuObjects) throws QemuImgException {
          final Script s = new Script(_qemuImgPath, timeout);
          s.add("snapshot");
          s.add("-d");
          s.add(snapshotName);
  
          for (QemuObject o : qemuObjects) {
              s.add(o.toCommandFlag());
          }
  
          s.add(srcImageOpts.toCommandFlag());
  
          final String result = s.execute();
          if (result != null) {
              // support idempotent delete calls, if no snapshot exists we are good.
              if (result.contains("snapshot not found") || result.contains("Can't find the snapshot")) {
                  return;
              }
              throw new QemuImgException(result);
          }
      }
  
      private void addScriptOptionsFromMap(Map<String, String> options, Script s) {
          if (options != null && !options.isEmpty()) {
              s.add("-o");
              final StringBuffer optionsBuffer = new StringBuffer();
              for (final Map.Entry<String, String> option : options.entrySet()) {
                  optionsBuffer.append(option.getKey()).append('=').append(option.getValue()).append(',');
              }
              String optionsStr = optionsBuffer.toString();
              optionsStr = optionsStr.replaceAll(",$", "");
              s.add(optionsStr);
          }
      }
  
      /**
       * Rebases the backing file of the image.
       *
       * This method is a facade for 'qemu-img rebase'.
       *
       * @param file
       *            The file to be rebased.
       * @param backingFile
       *            The new backing file.
       * @param backingFileFormat
       *            The format of the new backing file.
       * @param secure
       *            Indicates whether 'safe mode' is active. When active, the operation will be more expensive and will only be possible if the old
       *            backing file still exists. However, if safe mode is off, the changes in the file name and format will be made without validation,
       *            so, if the backing file is wrongly specified the contents of the image may be corrupted.
       */
      public void rebase(final QemuImgFile file, final QemuImgFile backingFile, final String backingFileFormat, final boolean secure) throws QemuImgException {
          if (backingFile == null) {
              throw new QemuImgException("No backing file was passed");
          }
          final Script s = new Script(_qemuImgPath, timeout);
          s.add("rebase");
          if (! secure) {
              s.add("-u");
          }
          s.add("-F");
          if (backingFileFormat != null) {
              s.add(backingFileFormat);
          } else {
              s.add(backingFile.getFormat().toString());
          }
          s.add("-b");
          s.add(backingFile.getFileName());
  
          s.add(file.getFileName());
          final String result = s.execute();
          if (result != null) {
              throw new QemuImgException(result);
          }
      }
  
      /**
       * Resizes an image.
       *
       * This method is a facade for 'qemu-img resize'.
       *
       * A negative size value will get prefixed with '-' and a positive with '+'. Sizes are in bytes and will be passed on that way.
       *
       * @param file
       *            The file to be resized.
       * @param size
       *            The new size.
       * @param delta
       *            Flag to inform if the new size is a delta.
       */
      public void resize(final QemuImgFile file, final long size, final boolean delta) throws QemuImgException {
          String newSize = null;
  
          if (size == 0) {
              throw new QemuImgException("size should never be exactly zero");
          }
  
          if (delta) {
              if (size > 0) {
                  newSize = "+" + Long.toString(size);
              } else {
                  newSize = Long.toString(size);
              }
          } else {
              if (size <= 0) {
                  throw new QemuImgException("size should not be negative if 'delta' is false!");
              }
              newSize = Long.toString(size);
          }
  
          final Script s = new Script(_qemuImgPath);
          s.add("resize");
          s.add(file.getFileName());
          s.add(newSize);
          s.execute();
      }
  
      /**
       * Resizes an image.
       *
       * This method is a facade for {@link QemuImg#resize(QemuImgFile, long, boolean)}.
       *
       * A negative size value will get prefixed with - and a positive with +. Sizes are in bytes and will be passed on that way.
       *
       * @param imageOptions
       *         Qemu style image options to be used in the resizing process.
       * @param qemuObjects
       *         Qemu style options (e.g. for passing secrets).
       * @param size
       */
      public void resize(final QemuImageOptions imageOptions, final List<QemuObject> qemuObjects, final long size) throws QemuImgException {
          final Script s = new Script(_qemuImgPath);
          s.add("resize");
  
          for (QemuObject o : qemuObjects) {
              s.add(o.toCommandFlag());
          }
  
          s.add(imageOptions.toCommandFlag());
          s.add(Long.toString(size));
  
          final String result = s.execute();
          if (result != null) {
              throw new QemuImgException(result);
          }
      }
  
      /**
       * Resizes an image.
       *
       * This method is a facade for {@link QemuImg#resize(QemuImgFile, long, boolean)}.
       *
       * A negative size value will get prefixed with - and a positive with +. Sizes are in bytes and will be passed on that way.
       *
       * @param file
       *            The file to be resized.
       * @param size
       *            The new size.
       */
      public void resize(final QemuImgFile file, final long size) throws QemuImgException {
          this.resize(file, size, false);
      }
  
      /**
       * Commits an image.
       *
       * This method is a facade for 'qemu-img commit'.
       *
       * @param file
       *            The file to be commited.
       * @param base
       *            If base is not specified, the immediate backing file of the top image (which is {@code file}) will be used.
       * @param skipEmptyingFiles
       *            If true, the commited file(s) will not be emptied. If base is informed, skipEmptyingFiles is implied.
       */
      public void commit(QemuImgFile file, QemuImgFile base, boolean skipEmptyingFiles) throws QemuImgException {
          if (file == null) {
              throw new QemuImgException("File should not be null");
          }
  
          final Script s = new Script(_qemuImgPath, timeout);
          s.add("commit");
          if (skipEmptyingFiles) {
              s.add("-d");
          }
  
          if (file.getFormat() != null) {
              s.add("-f");
              s.add(file.getFormat().format);
          }
  
          if (base != null) {
              s.add("-b");
              s.add(base.getFileName());
          }
  
          s.add(file.getFileName());
          final String result = s.execute();
          if (result != null) {
              throw new QemuImgException(result);
          }
      }
  
      /**
       * Does qemu-img support --target-is-zero
       * @return boolean
       */
      public boolean supportsSkipZeros() {
          return this.skipZero;
      }
  
      public void setSkipZero(boolean skipZero) {
          this.skipZero = skipZero;
      }
  
      public void setSkipTargetVolumeCreation(boolean skipTargetVolumeCreation) {
          this.skipTargetVolumeCreation = skipTargetVolumeCreation;
      }
  
      public boolean supportsImageFormat(QemuImg.PhysicalDiskFormat format) {
          final Script s = new Script(_qemuImgPath, timeout);
          s.add("--help");
  
          final OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser();
          String result = s.execute(parser);
          String output = parser.getLines();
  
          // Older Qemu returns output in result due to --help reporting error status
          if (result != null) {
             output = result;
          }
  
          return helpSupportsImageFormat(output, format);
      }
  
      protected static boolean helpSupportsImageFormat(String text, QemuImg.PhysicalDiskFormat format) {
          Pattern pattern = Pattern.compile("Supported\\sformats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE);
          return pattern.matcher(text).find();
      }
  
      /**
       * check for any leaks for an image and repair.
       *
       * @param imageOptions
       *         Qemu style image options to be used in the checking process.
       * @param qemuObjects
       *         Qemu style options (e.g. for passing secrets).
       * @param repair
       *         Boolean option whether to repair any leaks
       */
      public String checkAndRepair(final QemuImgFile file, final QemuImageOptions imageOptions, final List<QemuObject> qemuObjects, final String repair) throws QemuImgException {
          final Script script = new Script(_qemuImgPath);
          script.add("check");
          if (imageOptions == null) {
              script.add(file.getFileName());
          }
  
          for (QemuObject o : qemuObjects) {
              script.add(o.toCommandFlag());
          }
  
          if (imageOptions != null) {
              script.add(imageOptions.toCommandFlag());
          }
  
          if (StringUtils.isNotEmpty(repair)) {
              script.add("-r");
              script.add(repair);
          }
  
          script.add("--output=json");
          script.add("2>/dev/null");
  
          final String result = Script.runBashScriptIgnoreExitValue(script.toString(), 3);
          if (result != null) {
              logger.debug(String.format("Check volume execution result %s", result));
          }
  
          return result;
      }
  
  
      /**
       * Perform one or more modifications of the persistent bitmap in {@code srcFile}
       * <br>
       * This method is a facade for 'qemu-img bitmap'.
       * <br>
       * Currently only the {@link BitmapOperation#Remove} is implemented
       *
       * @param bitmapOperation
       *         The operation to be performed
       * @param srcfile
       *         The src file where the operation will be performed
       * @param bitmapName
       *         The name of the bitmap
       */
      public void bitmap(BitmapOperation bitmapOperation, QemuImgFile srcfile, String bitmapName) throws QemuImgException {
          if (bitmapOperation != BitmapOperation.Remove) {
              throw new QemuImgException("Operation not implemented.");
          }
          removeBitmap(srcfile, bitmapName);
      }
  
      private void removeBitmap(QemuImgFile srcFile, String bitmapName) throws QemuImgException {
          final Script script = new Script(_qemuImgPath);
          script.add("bitmap");
          script.add("--remove");
          script.add(srcFile.getFileName());
          script.add(bitmapName);
  
          String result = script.execute();
          if (result != null) {
              throw new QemuImgException(String.format("Exception while removing bitmap [%s] from file [%s]. Result is [%s].", srcFile.getFileName(), bitmapName, result));
          }
      }
  }
  