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
  // with 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 com.cloud.hypervisor.kvm.resource.wrapper;
  
  import java.io.File;
  import java.io.IOException;
  
  import com.cloud.agent.api.to.DataStoreTO;
  import com.cloud.agent.api.to.NfsTO;
  import com.cloud.api.storage.LinstorBackupSnapshotCommand;
  import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
  import com.cloud.hypervisor.kvm.storage.KVMStoragePool;
  import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
  import com.cloud.resource.CommandWrapper;
  import com.cloud.resource.ResourceWrapper;
  import com.cloud.storage.Storage;
  import com.cloud.utils.script.Script;
  import org.apache.cloudstack.storage.command.CopyCmdAnswer;
  import org.apache.cloudstack.storage.to.SnapshotObjectTO;
  import org.apache.cloudstack.utils.qemu.QemuImg;
  import org.apache.cloudstack.utils.qemu.QemuImgException;
  import org.apache.cloudstack.utils.qemu.QemuImgFile;
  import org.apache.commons.io.FileUtils;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
  import org.joda.time.Duration;
  import org.libvirt.LibvirtException;
  
  @ResourceWrapper(handles = LinstorBackupSnapshotCommand.class)
  public final class LinstorBackupSnapshotCommandWrapper
      extends CommandWrapper<LinstorBackupSnapshotCommand, CopyCmdAnswer, LibvirtComputingResource>
  {
      protected static Logger LOGGER = LogManager.getLogger(LinstorBackupSnapshotCommandWrapper.class);
  
      private static String zfsDatasetName(String zfsFullSnapshotUrl) {
          String zfsFullPath = zfsFullSnapshotUrl.substring(6);
          int atPos = zfsFullPath.indexOf('@');
          return atPos >= 0 ? zfsFullPath.substring(0, atPos) : zfsFullPath;
      }
  
      private String zfsSnapdev(boolean hide, String zfsUrl) {
          Script script = new Script("zfs", Duration.millis(5000));
          script.add("set");
          script.add("snapdev=" + (hide ? "hidden" : "visible"));
          script.add(zfsDatasetName(zfsUrl));  // cutting zfs:// and @snapshotname
          return script.execute();
      }
  
      private String qemuShrink(String path, long sizeByte, long timeout) {
          Script qemuImg = new Script("qemu-img", Duration.millis(timeout));
          qemuImg.add("resize");
          qemuImg.add("--shrink");
          qemuImg.add(path);
          qemuImg.add("" + sizeByte);
          return qemuImg.execute();
      }
  
      static void cleanupSecondaryPool(final KVMStoragePool secondaryPool) {
          if (secondaryPool != null) {
              try {
                  secondaryPool.delete();
              } catch (final Exception e) {
                  LOGGER.debug("Failed to delete secondary storage", e);
              }
          }
      }
  
      private String convertImageToQCow2(
          final String srcPath,
          final SnapshotObjectTO dst,
          final KVMStoragePool secondaryPool,
          int waitMilliSeconds
          )
          throws LibvirtException, QemuImgException, IOException
      {
          final String dstDir = secondaryPool.getLocalPath() + File.separator + dst.getPath();
          FileUtils.forceMkdir(new File(dstDir));
  
          final String dstPath = dstDir + File.separator + dst.getName();
          final QemuImgFile srcFile = new QemuImgFile(srcPath, QemuImg.PhysicalDiskFormat.RAW);
          final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.QCOW2);
  
          // NOTE: the qemu img will also contain the drbd metadata at the end
          final QemuImg qemu = new QemuImg(waitMilliSeconds);
          qemu.convert(srcFile, dstFile);
          LOGGER.info("Backup snapshot '{}' to '{}'", srcPath, dstPath);
          return dstPath;
      }
  
      private SnapshotObjectTO setCorrectSnapshotSize(final SnapshotObjectTO dst, final String dstPath) {
          final File snapFile = new File(dstPath);
          long size;
          if (snapFile.exists()) {
              size = snapFile.length();
          } else {
              LOGGER.warn("Snapshot file {} does not exist. Reporting size 0", dstPath);
              size = 0;
          }
  
          dst.setPath(dst.getPath() + File.separator + dst.getName());
          dst.setPhysicalSize(size);
          return dst;
      }
  
      @Override
      public CopyCmdAnswer execute(LinstorBackupSnapshotCommand cmd, LibvirtComputingResource serverResource)
      {
          LOGGER.debug("LinstorBackupSnapshotCommandWrapper: " + cmd.getSrcTO().getPath() + " -> " + cmd.getDestTO().getPath());
          final SnapshotObjectTO src = (SnapshotObjectTO) cmd.getSrcTO();
          final SnapshotObjectTO dst = (SnapshotObjectTO) cmd.getDestTO();
          KVMStoragePool secondaryPool = null;
          final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr();
          KVMStoragePool linstorPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.Linstor, src.getDataStore().getUuid());
          boolean zfsHidden = false;
          String srcPath = src.getPath();
  
          if (linstorPool == null) {
              return new CopyCmdAnswer("Unable to get linstor storage pool from destination volume.");
          }
  
          final DataStoreTO dstDataStore = dst.getDataStore();
          if (!(dstDataStore instanceof NfsTO)) {
              return new CopyCmdAnswer("Backup Linstor snapshot: Only NFS secondary supported at present!");
          }
  
          try
          {
              // provide the linstor snapshot block device
              // on lvm thin this should already be there in /dev/mapper/vg-snapshotname
              // on zfs we need to unhide the snapshot block device
              LOGGER.info("Src: " + srcPath + " | " + src.getName());
              if (srcPath.startsWith("zfs://")) {
                  zfsHidden = true;
                  if (zfsSnapdev(false, src.getPath()) != null) {
                      return new CopyCmdAnswer("Unable to unhide zfs snapshot device.");
                  }
                  srcPath = "/dev/zvol/" + srcPath.substring(6);
              }
  
              secondaryPool = storagePoolMgr.getStoragePoolByURI(dstDataStore.getUrl());
  
              String dstPath = convertImageToQCow2(srcPath, dst, secondaryPool, cmd.getWaitInMillSeconds());
  
              // resize to real volume size, cutting of drbd metadata
              String result = qemuShrink(dstPath, src.getVolume().getSize(), cmd.getWaitInMillSeconds());
              if (result != null) {
                  return new CopyCmdAnswer("qemu-img shrink failed: " + result);
              }
              LOGGER.info("Backup shrunk " + dstPath + " to actual size " + src.getVolume().getSize());
  
              SnapshotObjectTO snapshot = setCorrectSnapshotSize(dst, dstPath);
              LOGGER.info("Actual file size for '{}' is {}", dstPath, snapshot.getPhysicalSize());
              return new CopyCmdAnswer(snapshot);
          } catch (final Exception e) {
              final String error = String.format("Failed to backup snapshot with id [%s] with a pool %s, due to %s",
                  cmd.getSrcTO().getId(), cmd.getSrcTO().getDataStore().getUuid(), e.getMessage());
              LOGGER.error(error);
              return new CopyCmdAnswer(cmd, e);
          } finally {
              cleanupSecondaryPool(secondaryPool);
              if (zfsHidden) {
                  zfsSnapdev(true, src.getPath());
              }
          }
      }
  }
  