Skip to content

Commit

Permalink
linstor: Add configurable snapshot to secondary storage backup feature
Browse files Browse the repository at this point in the history
  • Loading branch information
rp- committed Nov 6, 2023
1 parent f6aeca5 commit c0afcf5
Show file tree
Hide file tree
Showing 10 changed files with 641 additions and 25 deletions.
6 changes: 6 additions & 0 deletions plugins/storage/volume/linstor/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
<artifactId>cloud-plugin-hypervisor-kvm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-engine-storage-snapshot</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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.api.storage;

import com.cloud.agent.api.to.DataTO;
import org.apache.cloudstack.storage.command.CopyCommand;

public class LinstorBackupSnapshotCommand extends CopyCommand
{
public LinstorBackupSnapshotCommand(DataTO srcData, DataTO destData, int timeout, boolean executeInSequence)
{
super(srcData, destData, timeout, executeInSequence);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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.api.storage;

import com.cloud.agent.api.to.DataTO;
import org.apache.cloudstack.storage.command.CopyCommand;

public class LinstorRevertBackupSnapshotCommand extends CopyCommand
{
public LinstorRevertBackupSnapshotCommand(DataTO srcData, DataTO destData, int timeout, boolean executeInSequence)
{
super(srcData, destData, timeout, executeInSequence);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// 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.log4j.Logger;
import org.joda.time.Duration;
import org.libvirt.LibvirtException;

@ResourceWrapper(handles = LinstorBackupSnapshotCommand.class)
public final class LinstorBackupSnapshotCommandWrapper
extends CommandWrapper<LinstorBackupSnapshotCommand, CopyCmdAnswer, LibvirtComputingResource>
{
private static final Logger s_logger = Logger.getLogger(LinstorBackupSnapshotCommandWrapper.class);

private String zfsSnapdev(boolean hide, String zfsUrl) {
Script script = new Script("/usr/bin/zfs", Duration.millis(5000));
script.add("set");
script.add("snapdev=" + (hide ? "hidden" : "visible"));
script.add(zfsUrl.substring(6)); // cutting zfs://
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) {
s_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);
s_logger.info("Backup snapshot " + srcFile + " to " + dstPath);
return dstPath;
}

private SnapshotObjectTO setCorrectSnapshotSize(final SnapshotObjectTO dst, final String dstPath) {
final File snapFile = new File(dstPath);
final long size = snapFile.exists() ? snapFile.length() : 0;

final SnapshotObjectTO snapshot = new SnapshotObjectTO();
snapshot.setPath(dst.getPath() + File.separator + dst.getName());
snapshot.setPhysicalSize(size);
return snapshot;
}

@Override
public CopyCmdAnswer execute(LinstorBackupSnapshotCommand cmd, LibvirtComputingResource serverResource)
{
s_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
s_logger.info("Src: " + srcPath + " | " + src.getName());
if (srcPath.startsWith("zfs://")) {
zfsHidden = true;
if (zfsSnapdev(false, srcPath) != null) {
return new CopyCmdAnswer("Unable to unhide zfs snapshot device.");
}
srcPath = "/dev/" + 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);
}
s_logger.info("Backup shrunk " + dstPath + " to actual size " + src.getVolume().getSize());

SnapshotObjectTO snapshot = setCorrectSnapshotSize(dst, dstPath);
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());
s_logger.error(error);
return new CopyCmdAnswer(cmd, e);
} finally {
cleanupSecondaryPool(secondaryPool);
if (zfsHidden) {
zfsSnapdev(true, src.getPath());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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 com.cloud.agent.api.to.DataStoreTO;
import com.cloud.api.storage.LinstorRevertBackupSnapshotCommand;
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 org.apache.cloudstack.storage.command.CopyCmdAnswer;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import org.apache.cloudstack.utils.qemu.QemuImg;
import org.apache.cloudstack.utils.qemu.QemuImgException;
import org.apache.cloudstack.utils.qemu.QemuImgFile;
import org.apache.log4j.Logger;
import org.libvirt.LibvirtException;

@ResourceWrapper(handles = LinstorRevertBackupSnapshotCommand.class)
public final class LinstorRevertBackupSnapshotCommandWrapper
extends CommandWrapper<LinstorRevertBackupSnapshotCommand, CopyCmdAnswer, LibvirtComputingResource>
{
private static final Logger s_logger = Logger.getLogger(LinstorRevertBackupSnapshotCommandWrapper.class);

private void convertQCow2ToRAW(final String srcPath, final String dstPath, int waitMilliSeconds)
throws LibvirtException, QemuImgException
{
final QemuImgFile srcQemuFile = new QemuImgFile(
srcPath, QemuImg.PhysicalDiskFormat.QCOW2);
final QemuImg qemu = new QemuImg(waitMilliSeconds);
final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.RAW);
qemu.convert(srcQemuFile, dstFile);
}

@Override
public CopyCmdAnswer execute(LinstorRevertBackupSnapshotCommand cmd, LibvirtComputingResource serverResource)
{
s_logger.debug("LinstorRevertBackupSnapshotCommandWrapper: " + cmd.getSrcTO().getPath() + " -> " + cmd.getDestTO().getPath());
final SnapshotObjectTO src = (SnapshotObjectTO) cmd.getSrcTO();
final VolumeObjectTO dst = (VolumeObjectTO) cmd.getDestTO();
KVMStoragePool secondaryPool = null;
final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr();
KVMStoragePool linstorPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.Linstor, dst.getDataStore().getUuid());

if (linstorPool == null) {
return new CopyCmdAnswer("Unable to get linstor storage pool from destination volume.");
}

try
{
final DataStoreTO srcDataStore = src.getDataStore();
File srcFile = new File(src.getPath());
secondaryPool = storagePoolMgr.getStoragePoolByURI(
srcDataStore.getUrl() + File.separator + srcFile.getParent());

convertQCow2ToRAW(
secondaryPool.getLocalPath() + File.separator + srcFile.getName(),
linstorPool.getPhysicalDisk(dst.getPath()).getPath(),
cmd.getWaitInMillSeconds());

final VolumeObjectTO dstVolume = new VolumeObjectTO();
dstVolume.setPath(dst.getPath());
return new CopyCmdAnswer(dstVolume);
} catch (final Exception e) {
final String error = String.format("Failed to revert snapshot with id [%s] with a pool %s, due to %s",
cmd.getSrcTO().getId(), cmd.getSrcTO().getDataStore().getUuid(), e.getMessage());
s_logger.error(error);
return new CopyCmdAnswer(cmd, e);
} finally {
LinstorBackupSnapshotCommandWrapper.cleanupSecondaryPool(secondaryPool);
}
}
}
Loading

0 comments on commit c0afcf5

Please sign in to comment.