File-based disk-only VM snapshot with KVM as hypervisor (#10632)

Co-authored-by: João Jandre <joao@scclouds.com.br>
Co-authored-by: Fabricio Duarte <fabricio.duarte.jr@gmail.com>
This commit is contained in:
João Jandre 2025-07-16 03:54:07 -03:00 committed by GitHub
parent bb75abcffa
commit 53eb2c5b9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 2413 additions and 168 deletions

View File

@ -155,6 +155,14 @@ public class AgentProperties{
*/
public static final Property<Integer> CMDS_TIMEOUT = new Property<>("cmds.timeout", 7200);
/**
* The timeout (in seconds) for the snapshot merge operation, mainly used for classic volume snapshots and disk-only VM snapshots on file-based storage.<br>
* This configuration is only considered if libvirt.events.enabled is also true. <br>
* Data type: Integer.<br>
* Default value: <code>259200</code>
*/
public static final Property<Integer> QCOW2_DELTA_MERGE_TIMEOUT = new Property<>("qcow2.delta.merge.timeout", 60 * 60 * 72);
/**
* This parameter sets the VM migration speed (in mbps). The default value is -1,<br>
* which means that the agent will try to guess the speed of the guest network and consume all possible bandwidth.<br>

View File

@ -137,7 +137,7 @@ public interface VolumeApiService {
void updateDisplay(Volume volume, Boolean displayVolume);
Snapshot allocSnapshotForVm(Long vmId, Long volumeId, String snapshotName) throws ResourceAllocationException;
Snapshot allocSnapshotForVm(Long vmId, Long volumeId, String snapshotName, Long vmSnapshotId) throws ResourceAllocationException;
/**
* Checks if the storage pool supports the disk offering tags.

View File

@ -31,7 +31,8 @@ public interface VMSnapshot extends ControlledEntity, Identity, InternalIdentity
enum State {
Allocated("The VM snapshot is allocated but has not been created yet."), Creating("The VM snapshot is being created."), Ready(
"The VM snapshot is ready to be used."), Reverting("The VM snapshot is being used to revert"), Expunging("The volume is being expunging"), Removed(
"The volume is destroyed, and can't be recovered."), Error("The volume is in error state, and can't be recovered");
"The volume is destroyed, and can't be recovered."), Error("The volume is in error state, and can't be recovered"),
Hidden("The VM snapshot is hidden from the user and cannot be recovered.");
String _description;
@ -60,6 +61,8 @@ public interface VMSnapshot extends ControlledEntity, Identity, InternalIdentity
s_fsm.addTransition(Expunging, Event.ExpungeRequested, Expunging);
s_fsm.addTransition(Expunging, Event.OperationSucceeded, Removed);
s_fsm.addTransition(Expunging, Event.OperationFailed, Error);
s_fsm.addTransition(Expunging, Event.Hide, Hidden);
s_fsm.addTransition(Hidden, Event.ExpungeRequested, Expunging);
}
}
@ -68,7 +71,7 @@ public interface VMSnapshot extends ControlledEntity, Identity, InternalIdentity
}
enum Event {
CreateRequested, OperationFailed, OperationSucceeded, RevertRequested, ExpungeRequested,
CreateRequested, OperationFailed, OperationSucceeded, RevertRequested, ExpungeRequested, Hide,
}
@Override

View File

@ -153,7 +153,7 @@ public class CreateSnapshotFromVMSnapshotCmd extends BaseAsyncCreateCmd {
@Override
public void create() throws ResourceAllocationException {
Snapshot snapshot = this._volumeService.allocSnapshotForVm(getVmId(), getVolumeId(), getSnapshotName());
Snapshot snapshot = this._volumeService.allocSnapshotForVm(getVmId(), getVolumeId(), getSnapshotName(), getVMSnapshotId());
if (snapshot != null) {
this.setEntityId(snapshot.getId());
this.setEntityUuid(snapshot.getUuid());

View File

@ -0,0 +1,39 @@
/*
* 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.agent.api.storage;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.utils.Pair;
import java.util.Map;
public class CreateDiskOnlyVmSnapshotAnswer extends Answer {
protected Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath;
public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath) {
super(command, success, details);
this.mapVolumeToSnapshotSizeAndNewVolumePath = mapVolumeToSnapshotSizeAndNewVolumePath;
}
public Map<String, Pair<Long, String>> getMapVolumeToSnapshotSizeAndNewVolumePath() {
return mapVolumeToSnapshotSizeAndNewVolumePath;
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.agent.api.storage;
import com.cloud.agent.api.VMSnapshotBaseCommand;
import com.cloud.agent.api.VMSnapshotTO;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import java.util.List;
public class CreateDiskOnlyVmSnapshotCommand extends VMSnapshotBaseCommand {
protected VirtualMachine.State vmState;
public CreateDiskOnlyVmSnapshotCommand(String vmName, VMSnapshotTO snapshot, List<VolumeObjectTO> volumeTOs, String guestOSType, VirtualMachine.State vmState) {
super(vmName, snapshot, volumeTOs, guestOSType);
this.vmState = vmState;
}
public VirtualMachine.State getVmState() {
return vmState;
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.agent.api.storage;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.to.DataTO;
import java.util.List;
public class DeleteDiskOnlyVmSnapshotCommand extends Command {
List<DataTO> snapshots;
public DeleteDiskOnlyVmSnapshotCommand(List<DataTO> snapshots) {
this.snapshots = snapshots;
}
public List<DataTO> getSnapshots() {
return snapshots;
}
@Override
public boolean executeInSequence() {
return false;
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.agent.api.storage;
import com.cloud.agent.api.Command;
import com.cloud.vm.VirtualMachine;
import java.util.List;
public class MergeDiskOnlyVmSnapshotCommand extends Command {
private List<SnapshotMergeTreeTO> snapshotMergeTreeToList;
private VirtualMachine.State vmState;
private String vmName;
public MergeDiskOnlyVmSnapshotCommand(List<SnapshotMergeTreeTO> snapshotMergeTreeToList, VirtualMachine.State vmState, String vmName) {
this.snapshotMergeTreeToList = snapshotMergeTreeToList;
this.vmState = vmState;
this.vmName = vmName;
}
public List<SnapshotMergeTreeTO> getSnapshotMergeTreeToList() {
return snapshotMergeTreeToList;
}
public VirtualMachine.State getVmState() {
return vmState;
}
public String getVmName() {
return vmName;
}
@Override
public boolean executeInSequence() {
return false;
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.agent.api.storage;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import java.util.List;
public class RevertDiskOnlyVmSnapshotAnswer extends Answer {
List<VolumeObjectTO> volumeObjectTos;
public RevertDiskOnlyVmSnapshotAnswer(Command cmd, List<VolumeObjectTO> volumeObjectTos) {
super(cmd, true, null);
this.volumeObjectTos = volumeObjectTos;
}
public List<VolumeObjectTO> getVolumeObjectTos() {
return volumeObjectTos;
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.agent.api.storage;
import com.cloud.agent.api.Command;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import java.util.List;
public class RevertDiskOnlyVmSnapshotCommand extends Command {
private List<SnapshotObjectTO> snapshotObjectTos;
private String vmName;
public RevertDiskOnlyVmSnapshotCommand(List<SnapshotObjectTO> snapshotObjectTos, String vmName) {
super();
this.snapshotObjectTos = snapshotObjectTos;
this.vmName = vmName;
}
public List<SnapshotObjectTO> getSnapshotObjectTos() {
return snapshotObjectTos;
}
public String getVmName() {
return vmName;
}
@Override
public boolean executeInSequence() {
return false;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.agent.api.storage;
import com.cloud.agent.api.to.DataTO;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import java.util.List;
public class SnapshotMergeTreeTO {
DataTO parent;
DataTO child;
List<DataTO> grandChildren;
public SnapshotMergeTreeTO(DataTO parent, DataTO child, List<DataTO> grandChildren) {
this.parent = parent;
this.child = child;
this.grandChildren = grandChildren;
}
public DataTO getParent() {
return parent;
}
public DataTO getChild() {
return child;
}
public List<DataTO> getGrandChildren() {
return grandChildren;
}
public void addGrandChild(DataTO grandChild) {
grandChildren.add(grandChild);
}
@Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}

View File

@ -58,4 +58,6 @@ public interface SnapshotDao extends GenericDao<SnapshotVO, Long>, StateDao<Snap
*/
List<SnapshotVO> listByIds(Object... ids);
List<SnapshotVO> searchByVolumes(List<Long> volumeIds);
List<SnapshotVO> listByVolumeIdAndTypeNotInAndStateNotRemoved(long volumeId, Type... type);
}

View File

@ -19,6 +19,7 @@ package com.cloud.storage.dao;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.PostConstruct;
@ -56,6 +57,10 @@ public class SnapshotDaoImpl extends GenericDaoBase<SnapshotVO, Long> implements
private static final String GET_LAST_SNAPSHOT =
"SELECT snapshots.id FROM snapshot_store_ref, snapshots where snapshots.id = snapshot_store_ref.snapshot_id AND snapshosts.volume_id = ? AND snapshot_store_ref.role = ? ORDER BY created DESC";
private static final String VOLUME_ID = "volumeId";
private static final String NOT_TYPE = "notType";
private static final String STATUS = "status";
private SearchBuilder<SnapshotVO> snapshotIdsSearch;
private SearchBuilder<SnapshotVO> VolumeIdSearch;
private SearchBuilder<SnapshotVO> VolumeIdTypeSearch;
@ -66,6 +71,8 @@ public class SnapshotDaoImpl extends GenericDaoBase<SnapshotVO, Long> implements
private SearchBuilder<SnapshotVO> StatusSearch;
private SearchBuilder<SnapshotVO> notInStatusSearch;
private GenericSearchBuilder<SnapshotVO, Long> CountSnapshotsByAccount;
private SearchBuilder<SnapshotVO> volumeIdAndTypeNotInSearch;
@Inject
ResourceTagDao _tagsDao;
@Inject
@ -181,6 +188,12 @@ public class SnapshotDaoImpl extends GenericDaoBase<SnapshotVO, Long> implements
InstanceIdSearch.join("instanceSnapshots", volumeSearch, volumeSearch.entity().getId(), InstanceIdSearch.entity().getVolumeId(), JoinType.INNER);
InstanceIdSearch.done();
volumeIdAndTypeNotInSearch = createSearchBuilder();
volumeIdAndTypeNotInSearch.and(VOLUME_ID, volumeIdAndTypeNotInSearch.entity().getVolumeId(), SearchCriteria.Op.EQ);
volumeIdAndTypeNotInSearch.and(STATUS, volumeIdAndTypeNotInSearch.entity().getState(), SearchCriteria.Op.NEQ);
volumeIdAndTypeNotInSearch.and(NOT_TYPE, volumeIdAndTypeNotInSearch.entity().getTypeDescription(), SearchCriteria.Op.NOTIN);
volumeIdAndTypeNotInSearch.done();
}
@Override
@ -299,4 +312,14 @@ public class SnapshotDaoImpl extends GenericDaoBase<SnapshotVO, Long> implements
sc.setParameters("volumeIds", volumeIds.toArray());
return search(sc, null);
}
@Override
public List<SnapshotVO> listByVolumeIdAndTypeNotInAndStateNotRemoved(long volumeId, Type... types) {
SearchCriteria<SnapshotVO> sc = volumeIdAndTypeNotInSearch.create();
sc.setParameters(VOLUME_ID, volumeId);
sc.setParameters(NOT_TYPE, Arrays.stream(types).map(Type::toString).toArray());
sc.setParameters(STATUS, State.Destroyed);
return listBy(sc);
}
}

View File

@ -27,6 +27,8 @@ public interface VMSnapshotDao extends GenericDao<VMSnapshotVO, Long>, StateDao<
List<VMSnapshotVO> findByVm(Long vmId);
List<VMSnapshotVO> findByVmAndByType(Long vmId, VMSnapshot.Type type);
List<VMSnapshotVO> listExpungingSnapshot();
List<VMSnapshotVO> listByInstanceId(Long vmId, VMSnapshot.State... status);
@ -35,6 +37,8 @@ public interface VMSnapshotDao extends GenericDao<VMSnapshotVO, Long>, StateDao<
List<VMSnapshotVO> listByParent(Long vmSnapshotId);
List<VMSnapshotVO> listByParentAndStateIn(Long vmSnapshotId, VMSnapshot.State... states);
VMSnapshotVO findByName(Long vmId, String name);
List<VMSnapshotVO> listByAccountId(Long accountId);

View File

@ -42,6 +42,12 @@ public class VMSnapshotDaoImpl extends GenericDaoBase<VMSnapshotVO, Long> implem
private final SearchBuilder<VMSnapshotVO> SnapshotStatusSearch;
private final SearchBuilder<VMSnapshotVO> AllFieldsSearch;
private SearchBuilder<VMSnapshotVO> parentIdEqAndStateIn;
private static final String PARENT = "parent";
private static final String STATE = "state";
protected VMSnapshotDaoImpl() {
AllFieldsSearch = createSearchBuilder();
AllFieldsSearch.and("state", AllFieldsSearch.entity().getState(), Op.EQ);
@ -71,6 +77,11 @@ public class VMSnapshotDaoImpl extends GenericDaoBase<VMSnapshotVO, Long> implem
SnapshotStatusSearch.and("vm_id", SnapshotStatusSearch.entity().getVmId(), SearchCriteria.Op.EQ);
SnapshotStatusSearch.and("state", SnapshotStatusSearch.entity().getState(), SearchCriteria.Op.IN);
SnapshotStatusSearch.done();
parentIdEqAndStateIn = createSearchBuilder();
parentIdEqAndStateIn.and(PARENT, parentIdEqAndStateIn.entity().getParent(), Op.EQ);
parentIdEqAndStateIn.and(STATE, parentIdEqAndStateIn.entity().getState(), Op.IN);
parentIdEqAndStateIn.done();
}
@Override
@ -80,6 +91,14 @@ public class VMSnapshotDaoImpl extends GenericDaoBase<VMSnapshotVO, Long> implem
return listBy(sc, null);
}
@Override
public List<VMSnapshotVO> findByVmAndByType(Long vmId, VMSnapshot.Type type) {
SearchCriteria<VMSnapshotVO> sc = AllFieldsSearch.create();
sc.setParameters("vm_id", vmId);
sc.setParameters("vm_snapshot_type", type);
return listBy(sc, null);
}
@Override
public List<VMSnapshotVO> listExpungingSnapshot() {
SearchCriteria<VMSnapshotVO> sc = ExpungingSnapshotSearch.create();
@ -111,6 +130,14 @@ public class VMSnapshotDaoImpl extends GenericDaoBase<VMSnapshotVO, Long> implem
return listBy(sc, null);
}
@Override
public List<VMSnapshotVO> listByParentAndStateIn(Long vmSnapshotId, State... states) {
SearchCriteria<VMSnapshotVO> sc = parentIdEqAndStateIn.create();
sc.setParameters(PARENT, vmSnapshotId);
sc.setParameters(STATE, (Object[])states);
return listBy(sc, null);
}
@Override
public VMSnapshotVO findByName(Long vmId, String name) {
SearchCriteria<VMSnapshotVO> sc = AllFieldsSearch.create();

View File

@ -67,6 +67,8 @@ StateDao<ObjectInDataStoreStateMachine.State, ObjectInDataStoreStateMachine.Even
List<SnapshotDataStoreVO> findBySnapshotIdAndNotInDestroyedHiddenState(long snapshotId);
SnapshotDataStoreVO findBySnapshotIdInAnyState(long snapshotId, DataStoreRole role);
List<SnapshotDataStoreVO> listDestroyed(long storeId);
List<SnapshotDataStoreVO> findBySnapshotId(long snapshotId);

View File

@ -417,6 +417,12 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase<SnapshotDataStoreVO
return findOneBy(sc);
}
@Override
public SnapshotDataStoreVO findBySnapshotIdInAnyState(long snapshotId, DataStoreRole role) {
SearchCriteria<SnapshotDataStoreVO> sc = createSearchCriteriaBySnapshotIdAndStoreRole(snapshotId, role);
return findOneBy(sc);
}
@Override
public List<SnapshotDataStoreVO> listAllByVolumeAndDataStore(long volumeId, DataStoreRole role) {
SearchCriteria<SnapshotDataStoreVO> sc = searchFilteringStoreIdEqStateEqStoreRoleEqIdEqUpdateCountEqSnapshotIdEqVolumeIdEq.create();

View File

@ -23,7 +23,15 @@ import java.util.Objects;
import javax.inject.Inject;
import com.cloud.storage.VolumeApiServiceImpl;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.dao.VMInstanceDao;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.VMSnapshotVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event;
@ -100,6 +108,15 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase {
@Inject
SnapshotZoneDao snapshotZoneDao;
@Inject
private VMSnapshotDao vmSnapshotDao;
@Inject
private VMSnapshotDetailsDao vmSnapshotDetailsDao;
@Inject
private VMInstanceDao vmInstanceDao;
private final List<Snapshot.State> snapshotStatesAbleToDeleteSnapshot = Arrays.asList(Snapshot.State.Destroying, Snapshot.State.Destroyed, Snapshot.State.Error, Snapshot.State.Hidden);
public SnapshotDataStoreVO getSnapshotImageStoreRef(long snapshotId, long zoneId) {
@ -610,6 +627,9 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase {
@Override
public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op) {
if (SnapshotOperation.TAKE.equals(op)) {
return validateVmSnapshot(snapshot);
}
if (SnapshotOperation.REVERT.equals(op)) {
long volumeId = snapshot.getVolumeId();
VolumeVO volumeVO = volumeDao.findById(volumeId);
@ -626,6 +646,30 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase {
return StrategyPriority.DEFAULT;
}
private StrategyPriority validateVmSnapshot(Snapshot snapshot) {
VolumeVO volumeVO = volumeDao.findById(snapshot.getVolumeId());
Long instanceId = volumeVO.getInstanceId();
if (instanceId == null) {
return StrategyPriority.DEFAULT;
}
VMInstanceVO vm = vmInstanceDao.findById(instanceId);
if (vm == null) {
return StrategyPriority.DEFAULT;
}
for (VMSnapshotVO vmSnapshotVO : vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) {
List<VMSnapshotDetailsVO> vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId());
if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT.equals(vmSnapshotDetailsVO.getName()))) {
logger.warn("VM [{}] already has KVM File-Based storage VM snapshots. These VM snapshots and volume snapshots are not supported " +
"together for KVM. As restoring volume snapshots will erase the VM snapshots and cause data loss.", vm.getUuid());
return StrategyPriority.CANT_HANDLE;
}
}
return StrategyPriority.DEFAULT;
}
protected boolean isSnapshotStoredOnSameZoneStoreForQCOW2Volume(Snapshot snapshot, VolumeVO volumeVO) {
if (volumeVO == null || !ImageFormat.QCOW2.equals(volumeVO.getFormat())) {
return false;

View File

@ -25,6 +25,9 @@ import java.util.Map;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority;
import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions;
import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotStrategy;
@ -100,6 +103,13 @@ public class DefaultVMSnapshotStrategy extends ManagerBase implements VMSnapshot
@Inject
PrimaryDataStoreDao primaryDataStoreDao;
@Inject
VMSnapshotDetailsDao vmSnapshotDetailsDao;
protected static final String KVM_FILE_BASED_STORAGE_SNAPSHOT = "kvmFileBasedStorageSnapshot";
protected static final String STORAGE_SNAPSHOT = "kvmStorageSnapshot";
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
String value = configurationDao.getValue("vmsnapshot.create.wait");
@ -469,16 +479,39 @@ public class DefaultVMSnapshotStrategy extends ManagerBase implements VMSnapshot
@Override
public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) {
UserVmVO vm = userVmDao.findById(vmId);
if (vm.getState() == State.Running && !snapshotMemory) {
if (State.Running.equals(vm.getState()) && !snapshotMemory) {
logger.debug("Default VM snapshot strategy cannot handle VM snapshot for [{}] as it is running and its memory will not be affected.", vm);
return StrategyPriority.CANT_HANDLE;
}
if (vmHasKvmDiskOnlySnapshot(vm)) {
logger.debug("Default VM snapshot strategy cannot handle VM snapshot for [{}] as it has a disk-only VM snapshot using kvmFileBasedStorageSnapshot strategy." +
"These two strategies are not compatible, as reverting a disk-only VM snapshot will erase newer disk-and-memory VM snapshots.", vm);
return StrategyPriority.CANT_HANDLE;
}
List<VolumeVO> volumes = volumeDao.findByInstance(vmId);
for (VolumeVO volume : volumes) {
if (volume.getFormat() != ImageFormat.QCOW2) {
logger.debug("Default VM snapshot strategy cannot handle VM snapshot for [{}] as it has a volume [{}] that is not in the QCOW2 format.", vm, volume);
return StrategyPriority.CANT_HANDLE;
}
}
return StrategyPriority.DEFAULT;
}
private boolean vmHasKvmDiskOnlySnapshot(UserVm vm) {
if (!Hypervisor.HypervisorType.KVM.equals(vm.getHypervisorType())) {
return false;
}
for (VMSnapshotVO vmSnapshotVO : vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) {
List<VMSnapshotDetailsVO> vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId());
if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> vmSnapshotDetailsVO.getName().equals(KVM_FILE_BASED_STORAGE_SNAPSHOT))) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,689 @@
/*
* 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 org.apache.cloudstack.storage.vmsnapshot;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.VMSnapshotTO;
import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer;
import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotCommand;
import com.cloud.agent.api.storage.DeleteDiskOnlyVmSnapshotCommand;
import com.cloud.agent.api.storage.MergeDiskOnlyVmSnapshotCommand;
import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotAnswer;
import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotCommand;
import com.cloud.agent.api.storage.SnapshotMergeTreeTO;
import com.cloud.agent.api.to.DataTO;
import com.cloud.configuration.Resource;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.Storage;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeVO;
import com.cloud.user.ResourceLimitService;
import com.cloud.uservm.UserVm;
import com.cloud.utils.DateUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.fsm.NoTransitionException;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.VMSnapshotVO;
import org.apache.cloudstack.backup.BackupOfferingVO;
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority;
import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.snapshot.SnapshotObject;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import org.apache.commons.collections.CollectionUtils;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.stream.Collectors;
public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStrategy {
private static final List<Storage.StoragePoolType> supportedStoragePoolTypes = List.of(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem, Storage.StoragePoolType.SharedMountPoint);
@Inject
protected SnapshotDataStoreDao snapshotDataStoreDao;
@Inject
protected ResourceLimitService resourceLimitManager;
@Inject
protected BackupOfferingDao backupOfferingDao;
@Override
public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) {
Map<VolumeInfo, SnapshotObject> volumeInfoToSnapshotObjectMap = new HashMap<>();
try {
return takeVmSnapshotInternal(vmSnapshot, volumeInfoToSnapshotObjectMap);
} catch (CloudRuntimeException | NullPointerException | NoTransitionException ex) {
for (VolumeInfo volumeInfo : volumeInfoToSnapshotObjectMap.keySet()) {
volumeInfo.stateTransit(Volume.Event.OperationFailed);
SnapshotObject snapshot = volumeInfoToSnapshotObjectMap.get(volumeInfo);
try {
snapshot.processEvent(Snapshot.Event.OperationFailed);
} catch (NoTransitionException e) {
logger.error("Failed to change snapshot [{}] state due to [{}].", snapshot.getUuid(), e.getMessage(), e);
}
snapshot.processEvent(ObjectInDataStoreStateMachine.Event.OperationFailed);
}
try {
vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationFailed);
} catch (NoTransitionException e) {
throw new CloudRuntimeException(e);
}
throw new CloudRuntimeException(ex);
}
}
@Override
public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) {
logger.info("Starting VM snapshot delete process for snapshot [{}].", vmSnapshot.getUuid());
UserVmVO userVm = userVmDao.findById(vmSnapshot.getVmId());
VMSnapshotVO vmSnapshotBeingDeleted = (VMSnapshotVO) vmSnapshot;
Long hostId = vmSnapshotHelper.pickRunningHost(vmSnapshotBeingDeleted.getVmId());
long virtualSize = 0;
boolean isCurrent = vmSnapshotBeingDeleted.getCurrent();
transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.ExpungeRequested);
List<VolumeObjectTO> volumeTOs = vmSnapshotHelper.getVolumeTOList(vmSnapshotBeingDeleted.getVmId());
List<VMSnapshotVO> snapshotChildren = vmSnapshotDao.listByParentAndStateIn(vmSnapshotBeingDeleted.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden);
long realSize = getVMSnapshotRealSize(vmSnapshotBeingDeleted);
int numberOfChildren = snapshotChildren.size();
List<SnapshotVO> volumeSnapshotVos = new ArrayList<>();
if (isCurrent && numberOfChildren == 0) {
volumeSnapshotVos = mergeCurrentDeltaOnSnapshot(vmSnapshotBeingDeleted, userVm, hostId, volumeTOs);
} else if (numberOfChildren == 0) {
logger.debug("Deleting VM snapshot [{}] as no snapshots/volumes depend on it.", vmSnapshot.getUuid());
volumeSnapshotVos = deleteSnapshot(vmSnapshotBeingDeleted, hostId);
mergeOldSiblingWithOldParentIfOldParentIsDead(vmSnapshotDao.findByIdIncludingRemoved(vmSnapshotBeingDeleted.getParent()), userVm, hostId, volumeTOs);
} else if (!isCurrent && numberOfChildren == 1) {
VMSnapshotVO childSnapshot = snapshotChildren.get(0);
volumeSnapshotVos = mergeSnapshots(vmSnapshotBeingDeleted, childSnapshot, userVm, volumeTOs, hostId);
}
for (SnapshotVO snapshotVO : volumeSnapshotVos) {
snapshotVO.setState(Snapshot.State.Destroyed);
snapshotDao.update(snapshotVO.getId(), snapshotVO);
}
for (VolumeObjectTO volumeTo : volumeTOs) {
publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_DELETE, vmSnapshotBeingDeleted, userVm, volumeTo);
virtualSize += volumeTo.getSize();
}
publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY, vmSnapshotBeingDeleted, userVm, realSize, virtualSize);
if (numberOfChildren > 1 || (isCurrent && numberOfChildren == 1)) {
transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.Hide);
return true;
}
transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.OperationSucceeded);
vmSnapshotDetailsDao.removeDetails(vmSnapshotBeingDeleted.getId());
vmSnapshotBeingDeleted.setRemoved(DateUtil.now());
vmSnapshotDao.update(vmSnapshotBeingDeleted.getId(), vmSnapshotBeingDeleted);
return true;
}
@Override
public boolean revertVMSnapshot(VMSnapshot vmSnapshot) {
UserVmVO userVm = userVmDao.findById(vmSnapshot.getVmId());
if (!VirtualMachine.State.Stopped.equals(userVm.getState())) {
throw new CloudRuntimeException("VM must be stopped to revert disk-only VM snapshot.");
}
VMSnapshotVO vmSnapshotBeingReverted = (VMSnapshotVO) vmSnapshot;
Long hostId = vmSnapshotHelper.pickRunningHost(vmSnapshotBeingReverted.getVmId());
transitStateWithoutThrow(vmSnapshotBeingReverted, VMSnapshot.Event.RevertRequested);
List<SnapshotDataStoreVO> volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotBeingReverted);
List<SnapshotObjectTO> volumeSnapshotTos = volumeSnapshots.stream()
.map(snapshot -> (SnapshotObjectTO) snapshotDataFactory.getSnapshot(snapshot.getSnapshotId(), snapshot.getDataStoreId(), DataStoreRole.Primary).getTO())
.collect(Collectors.toList());
RevertDiskOnlyVmSnapshotCommand revertDiskOnlyVMSnapshotCommand = new RevertDiskOnlyVmSnapshotCommand(volumeSnapshotTos, userVm.getName());
Answer answer = agentMgr.easySend(hostId, revertDiskOnlyVMSnapshotCommand);
if (answer == null || !answer.getResult()) {
transitStateWithoutThrow(vmSnapshotBeingReverted, VMSnapshot.Event.OperationFailed);
logger.error(answer != null ? answer.getDetails() : String.format("Communication failure with host [%s].", hostId));
throw new CloudRuntimeException(String.format("Error reverting VM snapshot [%s].", vmSnapshot.getUuid()));
}
RevertDiskOnlyVmSnapshotAnswer revertDiskOnlyVMSnapshotAnswer = (RevertDiskOnlyVmSnapshotAnswer) answer;
for (VolumeObjectTO volumeObjectTo : revertDiskOnlyVMSnapshotAnswer.getVolumeObjectTos()) {
VolumeVO volumeVO = volumeDao.findById(volumeObjectTo.getVolumeId());
volumeVO.setPath(volumeObjectTo.getPath());
updateSizeIfNeeded(volumeSnapshots, volumeVO, volumeObjectTo);
volumeDao.update(volumeVO.getId(), volumeVO);
publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_REVERT, vmSnapshotBeingReverted, userVm, volumeObjectTo);
}
transitStateWithoutThrow(vmSnapshotBeingReverted, VMSnapshot.Event.OperationSucceeded);
VMSnapshotVO currentVmSnapshot = vmSnapshotDao.findCurrentSnapshotByVmId(userVm.getId());
currentVmSnapshot.setCurrent(false);
vmSnapshotBeingReverted.setCurrent(true);
vmSnapshotDao.update(currentVmSnapshot.getId(), currentVmSnapshot);
vmSnapshotDao.update(vmSnapshotBeingReverted.getId(), vmSnapshotBeingReverted);
mergeOldSiblingWithOldParentIfOldParentIsDead(currentVmSnapshot, userVm, hostId, vmSnapshotHelper.getVolumeTOList(userVm.getId()));
return true;
}
/**
* Updates the volume size if it changed due to the snapshot reversion.
* */
private void updateSizeIfNeeded(List<SnapshotDataStoreVO> volumeSnapshots, VolumeVO volumeVO, VolumeObjectTO volumeObjectTO) {
SnapshotDataStoreVO snapshotRef = volumeSnapshots.stream().filter(snapshotDataStoreVO -> snapshotDataStoreVO.getVolumeId() == volumeVO.getId()).
findFirst().
orElseThrow(() -> new CloudRuntimeException(String.format("Unable to map any snapshot to volume [%s].", volumeVO)));
if (volumeVO.getSize() == snapshotRef.getSize()) {
logger.debug("No need to update the volume size and launch a resize event as the snapshot [{}] and volume [{}] size are equal.", snapshotRef.getSnapshotId(), volumeVO.getUuid());
return;
}
long delta = volumeVO.getSize() - snapshotRef.getSize();
if (delta < 0) {
resourceLimitManager.incrementResourceCount(volumeVO.getAccountId(), Resource.ResourceType.primary_storage, -delta);
} else {
resourceLimitManager.decrementResourceCount(volumeVO.getAccountId(), Resource.ResourceType.primary_storage, delta);
}
volumeVO.setSize(snapshotRef.getSize());
volumeObjectTO.setSize(snapshotRef.getSize());
volumeDao.update(volumeVO.getId(), volumeVO);
UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_RESIZE, volumeVO.getAccountId(), volumeVO.getDataCenterId(), volumeVO.getId(), volumeVO.getName(),
volumeVO.getDiskOfferingId(), volumeVO.getTemplateId(), volumeVO.getSize(), Volume.class.getName(), volumeVO.getUuid());
}
private void mergeOldSiblingWithOldParentIfOldParentIsDead(VMSnapshotVO oldParent, UserVmVO userVm, Long hostId, List<VolumeObjectTO> volumeTOs) {
if (oldParent == null || oldParent.getRemoved() != null || !VMSnapshot.State.Hidden.equals(oldParent.getState())) {
return;
}
List<SnapshotVO> snapshotVos;
if (oldParent.getCurrent()) {
snapshotVos = mergeCurrentDeltaOnSnapshot(oldParent, userVm, hostId, volumeTOs);
} else {
List<VMSnapshotVO> oldSiblings = vmSnapshotDao.listByParentAndStateIn(oldParent.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden);
if (oldSiblings.size() > 1) {
logger.debug("The old snapshot [{}] is dead and still has more than one live child snapshot. We will keep it on storage still.", oldParent.getUuid());
return;
}
if (oldSiblings.isEmpty()) {
logger.warn("The old snapshot [{}] is dead, but it only had one child. This is an inconsistency and should be analysed/reported.", oldParent.getUuid());
return;
}
VMSnapshotVO oldSibling = oldSiblings.get(0);
logger.debug("Merging VM snapshot [{}] with [{}] as the former was hidden and only the latter depends on it.", oldParent.getUuid(), oldSibling.getUuid());
snapshotVos = mergeSnapshots(oldParent, oldSibling, userVm, volumeTOs, hostId);
}
for (SnapshotVO snapshotVO : snapshotVos) {
snapshotVO.setState(Snapshot.State.Destroyed);
snapshotDao.update(snapshotVO.getId(), snapshotVO);
}
vmSnapshotDetailsDao.removeDetails(oldParent.getId());
oldParent.setRemoved(DateUtil.now());
vmSnapshotDao.update(oldParent.getId(), oldParent);
transitStateWithoutThrow(oldParent, VMSnapshot.Event.ExpungeRequested);
transitStateWithoutThrow(oldParent, VMSnapshot.Event.OperationSucceeded);
}
@Override
public StrategyPriority canHandle(VMSnapshot vmSnapshot) {
if (!VMSnapshot.State.Allocated.equals(vmSnapshot.getState())) {
List<VMSnapshotDetailsVO> vmSnapshotDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT);
if (CollectionUtils.isEmpty(vmSnapshotDetails)) {
logger.debug("KVM file based storage VM snapshot strategy cannot handle [{}] as it is not a KVM file based storage VM snapshot.",
vmSnapshot.getUuid());
return StrategyPriority.CANT_HANDLE;
}
return StrategyPriority.HIGHEST;
}
long vmId = vmSnapshot.getVmId();
boolean memorySnapshot = VMSnapshot.Type.DiskAndMemory.equals(vmSnapshot.getType());
return canHandle(vmId, null, memorySnapshot);
}
@Override
public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) {
VirtualMachine vm = userVmDao.findById(vmId);
String cantHandleLog = String.format("KVM file based storage VM snapshot strategy cannot handle VM snapshot for [%s]", vm);
if (snapshotMemory) {
logger.debug("{} as a snapshot with memory was requested.", cantHandleLog);
return StrategyPriority.CANT_HANDLE;
}
if (!Hypervisor.HypervisorType.KVM.equals(vm.getHypervisorType())) {
logger.debug("{} as the hypervisor is not KVM.", cantHandleLog);
return StrategyPriority.CANT_HANDLE;
}
if (CollectionUtils.isNotEmpty(vmSnapshotDao.findByVmAndByType(vmId, VMSnapshot.Type.DiskAndMemory))) {
logger.debug("{} as there is already a VM snapshot with disk and memory.", cantHandleLog);
return StrategyPriority.CANT_HANDLE;
}
List<VolumeVO> volumes = volumeDao.findByInstance(vmId);
for (VolumeVO volume : volumes) {
StoragePoolVO storagePoolVO = storagePool.findById(volume.getPoolId());
if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) {
logger.debug(String.format("%s as the VM has a volume that is in a storage with unsupported type [%s].", cantHandleLog, storagePoolVO.getPoolType()));
return StrategyPriority.CANT_HANDLE;
}
List<SnapshotVO> snapshots = snapshotDao.listByVolumeIdAndTypeNotInAndStateNotRemoved(volume.getId(), Snapshot.Type.GROUP);
if (CollectionUtils.isNotEmpty(snapshots)) {
logger.debug("{} as VM has a volume with snapshots {}. Volume snapshots and KvmFileBasedStorageVmSnapshotStrategy are not compatible, as restoring volume snapshots will erase VM " +
"snapshots and cause data loss.", cantHandleLog, snapshots);
return StrategyPriority.CANT_HANDLE;
}
}
BackupOfferingVO backupOffering = backupOfferingDao.findById(vm.getBackupOfferingId());
if (backupOffering != null) {
logger.debug("{} as the VM has a backup offering. This strategy does not support snapshots on VMs with current backup providers.", cantHandleLog);
return StrategyPriority.CANT_HANDLE;
}
return StrategyPriority.HIGHEST;
}
private List<SnapshotVO> deleteSnapshot(VMSnapshotVO vmSnapshotVO, Long hostId) {
List<SnapshotDataStoreVO> volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVO);
List<DataTO> volumeSnapshotTOList = volumeSnapshots.stream()
.map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO())
.collect(Collectors.toList());
DeleteDiskOnlyVmSnapshotCommand deleteSnapshotCommand = new DeleteDiskOnlyVmSnapshotCommand(volumeSnapshotTOList);
Answer answer = agentMgr.easySend(hostId, deleteSnapshotCommand);
if (answer == null || !answer.getResult()) {
logger.error("Failed to delete VM snapshot [{}] due to {}.", vmSnapshotVO.getUuid(), answer != null ? answer.getDetails() : "Communication failure");
throw new CloudRuntimeException(String.format("Failed to delete VM snapshot [%s].", vmSnapshotVO.getUuid()));
}
logger.debug("Updating metadata of VM snapshot [{}].", vmSnapshotVO.getUuid());
List<SnapshotVO> snapshotVOList = new ArrayList<>();
for (SnapshotDataStoreVO snapshotDataStoreVO : volumeSnapshots) {
snapshotDataStoreDao.remove(snapshotDataStoreVO.getId());
snapshotVOList.add(snapshotDao.findById(snapshotDataStoreVO.getSnapshotId()));
}
return snapshotVOList;
}
private List<SnapshotVO> mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO childSnapshot, UserVmVO userVm, List<VolumeObjectTO> volumeObjectTOS, Long hostId) {
logger.debug("Merging VM snapshot [{}] with its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid());
List<VMSnapshotVO> snapshotGrandChildren = vmSnapshotDao.listByParentAndStateIn(childSnapshot.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden);
if (VirtualMachine.State.Running.equals(userVm.getState()) && !snapshotGrandChildren.isEmpty()) {
logger.debug("Removing VM snapshots that are part of the VM's [{}] current backing chain from the list of snapshots to be rebased.", userVm.getUuid());
removeCurrentBackingChainSnapshotFromVmSnapshotList(snapshotGrandChildren, userVm);
}
List<SnapshotMergeTreeTO> snapshotMergeTreeToList = generateSnapshotMergeTrees(vmSnapshotVO, childSnapshot, snapshotGrandChildren);
if (childSnapshot.getCurrent() && !VirtualMachine.State.Running.equals(userVm.getState())) {
for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) {
snapshotMergeTreeToList.stream().filter(snapshotTree -> Objects.equals(((SnapshotObjectTO) snapshotTree.getParent()).getVolume().getId(), volumeObjectTO.getId()))
.findFirst()
.orElseThrow(() -> new CloudRuntimeException(String.format("Failed to find volume snapshot for volume [%s].", volumeObjectTO.getUuid())))
.addGrandChild(volumeObjectTO);
}
}
MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(snapshotMergeTreeToList, userVm.getState(), userVm.getName());
Answer answer = agentMgr.easySend(hostId, mergeDiskOnlyVMSnapshotCommand);
if (answer == null || !answer.getResult()) {
throw new CloudRuntimeException(String.format("Failed to merge VM snapshot [%s] due to %s.", vmSnapshotVO.getUuid(), answer != null ? answer.getDetails() : "Communication failure"));
}
logger.debug("Updating metadata of VM snapshot [{}] and its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid());
List<SnapshotVO> snapshotVOList = new ArrayList<>();
for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeToList) {
SnapshotObjectTO childTO = (SnapshotObjectTO) snapshotMergeTreeTO.getChild();
SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotMergeTreeTO.getParent();
SnapshotDataStoreVO childSnapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotIdInAnyState(childTO.getId(), DataStoreRole.Primary);
childSnapshotDataStoreVO.setInstallPath(parentTO.getPath());
snapshotDataStoreDao.update(childSnapshotDataStoreVO.getId(), childSnapshotDataStoreVO);
snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(parentTO.getId(), childSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary);
snapshotVOList.add(snapshotDao.findById(parentTO.getId()));
}
childSnapshot.setParent(vmSnapshotVO.getParent());
vmSnapshotDao.update(childSnapshot.getId(), childSnapshot);
return snapshotVOList;
}
private List<SnapshotVO> mergeCurrentDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, UserVmVO userVmVO, Long hostId, List<VolumeObjectTO> volumeObjectTOS) {
logger.debug("Merging VM snapshot [{}] with the current volume delta.", vmSnapshotVo.getUuid());
List<SnapshotMergeTreeTO> snapshotMergeTreeTOList = new ArrayList<>();
List<SnapshotDataStoreVO> volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVo);
for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) {
SnapshotDataStoreVO volumeParentSnapshot = volumeSnapshots.stream().filter(snapshot -> Objects.equals(snapshot.getVolumeId(), volumeObjectTO.getId()))
.findFirst()
.orElseThrow(() -> new CloudRuntimeException(String.format("Failed to find volume snapshot for volume [%s].", volumeObjectTO.getUuid())));
DataTO parentSnapshot = snapshotDataFactory.getSnapshot(volumeParentSnapshot.getSnapshotId(), volumeParentSnapshot.getDataStoreId(), DataStoreRole.Primary).getTO();
snapshotMergeTreeTOList.add(new SnapshotMergeTreeTO(parentSnapshot, volumeObjectTO, new ArrayList<>()));
}
MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(snapshotMergeTreeTOList, userVmVO.getState(), userVmVO.getName());
Answer answer = agentMgr.easySend(hostId, mergeDiskOnlyVMSnapshotCommand);
if (answer == null || !answer.getResult()) {
throw new CloudRuntimeException(String.format("Failed to delete VM snapshot [%s] due to %s.", vmSnapshotVo.getUuid(), answer != null ? answer.getDetails() : "Communication failure"));
}
logger.debug("Updating metadata of VM snapshot [{}].", vmSnapshotVo.getUuid());
List<SnapshotVO> snapshotVOList = new ArrayList<>();
for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeTOList) {
VolumeObjectTO volumeObjectTO = (VolumeObjectTO) snapshotMergeTreeTO.getChild();
SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotMergeTreeTO.getParent();
VolumeVO volumeVO = volumeDao.findById(volumeObjectTO.getId());
volumeVO.setPath(parentTO.getPath());
volumeDao.update(volumeVO.getId(), volumeVO);
snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(parentTO.getId(), volumeVO.getPoolId(), DataStoreRole.Primary);
snapshotVOList.add(snapshotDao.findById(parentTO.getId()));
}
vmSnapshotVo.setCurrent(false);
if (vmSnapshotVo.getParent() != null) {
VMSnapshotVO parentSnapshot = vmSnapshotDao.findById(vmSnapshotVo.getParent());
parentSnapshot.setCurrent(true);
vmSnapshotDao.update(parentSnapshot.getId(), parentSnapshot);
}
return snapshotVOList;
}
/**
* Takes a disk-only VM snapshot, exceptions thrown will be caught deeper in the stack and treated there.
* @param vmSnapshot the definition of the VM Snapshot that will be created.
* @param volumeInfoToSnapshotObjectMap Empty map of VolumeInfo to SnapshotObject, will be populated within the method, used for treating the exceptions thrown.
* @return the VM Snapshot created.
* */
protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map<VolumeInfo, SnapshotObject> volumeInfoToSnapshotObjectMap) throws NoTransitionException {
UserVm userVm = userVmDao.findById(vmSnapshot.getVmId());
logger.info("Starting disk-only VM snapshot process for VM [{}].", userVm.getUuid());
Long hostId = vmSnapshotHelper.pickRunningHost(vmSnapshot.getVmId());
VMSnapshotVO vmSnapshotVO = (VMSnapshotVO) vmSnapshot;
List<VolumeObjectTO> volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId());
transitStateWithoutThrow(vmSnapshot, VMSnapshot.Event.CreateRequested);
VMSnapshotTO parentSnapshotTo = null;
VMSnapshotVO parentSnapshotVo = vmSnapshotDao.findCurrentSnapshotByVmId(userVm.getId());
if (parentSnapshotVo != null) {
parentSnapshotTo = vmSnapshotHelper.getSnapshotWithParents(parentSnapshotVo);
vmSnapshotVO.setParent(parentSnapshotTo.getId());
}
VMSnapshotOptions options = ((VMSnapshotVO) vmSnapshot).getOptions();
boolean quiesceVm = false;
if (options != null) {
quiesceVm = options.needQuiesceVM();
}
long virtualSize = createVolumeSnapshotMetadataAndCalculateVirtualSize(vmSnapshot, volumeInfoToSnapshotObjectMap, volumeTOs);
VMSnapshotTO target = new VMSnapshotTO(vmSnapshot.getId(), vmSnapshot.getName(), vmSnapshot.getType(), null, vmSnapshot.getDescription(), false, parentSnapshotTo, quiesceVm);
CreateDiskOnlyVmSnapshotCommand ccmd = new CreateDiskOnlyVmSnapshotCommand(userVm.getInstanceName(), target, volumeTOs, null, userVm.getState());
logger.info("Sending disk-only VM snapshot creation of VM Snapshot [{}] command for host [{}].", vmSnapshot.getUuid(), hostId);
Answer answer = agentMgr.easySend(hostId, ccmd);
if (answer != null && answer.getResult()) {
CreateDiskOnlyVmSnapshotAnswer createDiskOnlyVMSnapshotAnswer = (CreateDiskOnlyVmSnapshotAnswer) answer;
return processCreateVmSnapshotAnswer(vmSnapshot, volumeInfoToSnapshotObjectMap, createDiskOnlyVMSnapshotAnswer, userVm, vmSnapshotVO, virtualSize, parentSnapshotVo);
}
logger.error("Disk-only VM snapshot for VM [{}] failed{}.", userVm.getUuid(), answer != null ? " due to" + answer.getDetails() : "");
throw new CloudRuntimeException(String.format("Disk-only VM snapshot for VM [%s] failed.", userVm.getUuid()));
}
/**
* Updates the needed metadata of the given VM Snapshot and its associated volume snapshots.
* */
private VMSnapshot processCreateVmSnapshotAnswer(VMSnapshot vmSnapshot, Map<VolumeInfo, SnapshotObject> volumeInfoToSnapshotObjectMap, CreateDiskOnlyVmSnapshotAnswer answer, UserVm userVm, VMSnapshotVO vmSnapshotVO, long virtualSize, VMSnapshotVO parentSnapshotVo) throws NoTransitionException {
logger.debug("Processing CreateDiskOnlyVMSnapshotCommand answer for disk-only VM snapshot [{}].", vmSnapshot.getUuid());
Map<String, Pair<Long, String>> volumeUuidToSnapshotSizeAndNewVolumePathMap = answer.getMapVolumeToSnapshotSizeAndNewVolumePath();
long vmSnapshotSize = 0;
for (VolumeInfo volumeInfo : volumeInfoToSnapshotObjectMap.keySet()) {
VolumeVO volumeVO = (VolumeVO) volumeInfo.getVolume();
Pair<Long, String> snapSizeAndNewVolumePath = volumeUuidToSnapshotSizeAndNewVolumePathMap.get(volumeVO.getUuid());
SnapshotObject snapshot = volumeInfoToSnapshotObjectMap.get(volumeInfo);
snapshot.markBackedUp();
logger.debug("Updating metadata for volume [{}] and its corresponding snapshot [{}].", volumeVO, snapshot.getSnapshotVO());
SnapshotDataStoreVO snapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotId(snapshot.getId()).get(0);
snapshotDataStoreVO.setInstallPath(volumeVO.getPath());
snapshotDataStoreVO.setPhysicalSize(snapSizeAndNewVolumePath.first());
snapshotDataStoreVO.setState(ObjectInDataStoreStateMachine.State.Ready);
snapshotDataStoreDao.update(snapshotDataStoreVO.getId(), snapshotDataStoreVO);
vmSnapshotSize += snapSizeAndNewVolumePath.first();
volumeVO.setPath(snapSizeAndNewVolumePath.second());
volumeDao.update(volumeVO.getId(), volumeVO);
volumeInfo.stateTransit(Volume.Event.OperationSucceeded);
vmSnapshotDetailsDao.persist(new VMSnapshotDetailsVO(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT, String.valueOf(snapshot.getId()), true));
publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_CREATE, vmSnapshot, userVm, (VolumeObjectTO) volumeInfo.getTO());
}
vmSnapshotVO.setCurrent(true);
vmSnapshotDao.persist(vmSnapshotVO);
if (parentSnapshotVo != null) {
parentSnapshotVo.setCurrent(false);
vmSnapshotDao.persist(parentSnapshotVo);
}
vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationSucceeded);
publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_ON_PRIMARY, vmSnapshot, userVm, vmSnapshotSize, virtualSize);
return vmSnapshot;
}
private long createVolumeSnapshotMetadataAndCalculateVirtualSize(VMSnapshot vmSnapshot, Map<VolumeInfo, SnapshotObject> volumeInfoToSnapshotObjectMap, List<VolumeObjectTO> volumeTOs) throws NoTransitionException {
long virtualSize = 0;
for (VolumeObjectTO volumeObjectTO : volumeTOs) {
VolumeInfo volumeInfo = volumeDataFactory.getVolume(volumeObjectTO.getId());
volumeInfo.stateTransit(Volume.Event.SnapshotRequested);
virtualSize += volumeInfo.getSize();
String snapshotName = String.format("%s_%s", vmSnapshot.getId(), volumeObjectTO.getUuid());
SnapshotVO snapshot = new SnapshotVO(volumeInfo.getDataCenterId(), volumeInfo.getAccountId(), volumeInfo.getDomainId(), volumeInfo.getId(),
volumeInfo.getDiskOfferingId(), snapshotName, (short) Snapshot.Type.GROUP.ordinal(), Snapshot.Type.GROUP.name(), volumeInfo.getSize(), volumeInfo.getMinIops(),
volumeInfo.getMaxIops(), Hypervisor.HypervisorType.KVM, null);
logger.debug("Creating snapshot metadata [{}] as part of the disk-only snapshot process for VM [{}].", snapshot, volumeObjectTO.getVmName());
snapshot = snapshotDao.persist(snapshot);
SnapshotInfo snapshotInfo = snapshotDataFactory.getSnapshot(snapshot.getId(), volumeInfo.getDataStore());
SnapshotObject snapshotOnPrimary = (SnapshotObject) snapshotInfo.getDataStore().create(snapshotInfo);
snapshotOnPrimary.processEvent(Snapshot.Event.CreateRequested);
snapshotOnPrimary.processEvent(ObjectInDataStoreStateMachine.Event.CreateOnlyRequested);
volumeInfoToSnapshotObjectMap.put(volumeInfo, snapshotOnPrimary);
}
return virtualSize;
}
private List<SnapshotMergeTreeTO> generateSnapshotMergeTrees(VMSnapshotVO parent, VMSnapshotVO child, List<VMSnapshotVO> grandChildren) throws NoSuchElementException {
logger.debug("Generating list of Snapshot Merge Trees for the merge process of VM Snapshot [{}].", parent.getUuid());
List<SnapshotMergeTreeTO> snapshotMergeTrees = new ArrayList<>();
List<SnapshotDataStoreVO> parentVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(parent);
List<SnapshotDataStoreVO> childVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(child);
List<SnapshotDataStoreVO> grandChildrenVolumeSnapshots = new ArrayList<>();
for (VMSnapshotVO grandChild : grandChildren) {
grandChildrenVolumeSnapshots.addAll(getVolumeSnapshotsAssociatedWithVmSnapshot(grandChild));
}
for (SnapshotDataStoreVO parentSnapshotDataStoreVO : parentVolumeSnapshots) {
DataTO parentTO = snapshotDataFactory.getSnapshot(parentSnapshotDataStoreVO.getSnapshotId(), parentSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO();
DataTO childTO = childVolumeSnapshots.stream()
.filter(childSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), childSnapshot.getVolumeId()))
.map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO())
.findFirst().orElseThrow(() -> new CloudRuntimeException(String.format("Could not find child snapshot of parent [%s].", parentSnapshotDataStoreVO.getSnapshotId())));
List<DataTO> grandChildrenTOList = grandChildrenVolumeSnapshots.stream()
.filter(grandChildSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), grandChildSnapshot.getVolumeId()))
.map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO())
.collect(Collectors.toList());
snapshotMergeTrees.add(new SnapshotMergeTreeTO(parentTO, childTO, grandChildrenTOList));
}
logger.debug("Generated the following list of Snapshot Merge Trees for the VM snapshot [{}]: [{}].", parent.getUuid(), snapshotMergeTrees);
return snapshotMergeTrees;
}
/**
* For a given {@code VMSnapshotVO}, populates the {@code associatedVolumeSnapshots} list with all the volume snapshots that are
* part of the VMSnapshot.
* @param vmSnapshot the VMSnapshotVO that will have its size calculated
* @return the list that will be populated with the volume snapshots associated with the VM snapshot.
* */
private List<SnapshotDataStoreVO> getVolumeSnapshotsAssociatedWithVmSnapshot(VMSnapshotVO vmSnapshot) {
List<SnapshotDataStoreVO> associatedVolumeSnapshots = new ArrayList<>();
List<VMSnapshotDetailsVO> snapshotDetailList = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT);
for (VMSnapshotDetailsVO vmSnapshotDetailsVO : snapshotDetailList) {
SnapshotDataStoreVO snapshot = snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary);
if (snapshot == null) {
throw new CloudRuntimeException(String.format("Could not find snapshot for VM snapshot [%s].", vmSnapshot.getUuid()));
}
associatedVolumeSnapshots.add(snapshot);
}
return associatedVolumeSnapshots;
}
/**
* For a given {@code VMSnapshotVO}, returns the real size of the snapshot.
* @param vmSnapshot the VMSnapshotVO that will have its size calculated
* */
private long getVMSnapshotRealSize(VMSnapshotVO vmSnapshot) {
long realSize = 0;
List<VMSnapshotDetailsVO> snapshotDetailList = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT);
for (VMSnapshotDetailsVO vmSnapshotDetailsVO : snapshotDetailList) {
SnapshotDataStoreVO snapshot = snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary);
if (snapshot == null) {
throw new CloudRuntimeException(String.format("Could not find snapshot for VM snapshot [%s].", vmSnapshot.getUuid()));
}
realSize += snapshot.getPhysicalSize();
}
return realSize;
}
/**
* Given a list of VM snapshots, will remove any that are part of the current direct backing chain (all the direct ancestors of the current vm snapshot).
* This is done because, when using <a href="https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainBlockCommit">virDomainBlockCommit</a>}, Libvirt will maintain
* the current backing chain consistent; thus we only need to rebase the snapshots that are not on the current backing chain.
* */
private void removeCurrentBackingChainSnapshotFromVmSnapshotList(List<VMSnapshotVO> vmSnapshotList, UserVm userVm) {
VMSnapshotVO currentSnapshotVO = vmSnapshotDao.findCurrentSnapshotByVmId(vmSnapshotList.get(0).getVmId());
VMSnapshotTO currentSnapshotTO = vmSnapshotHelper.getSnapshotWithParents(currentSnapshotVO);
List<VMSnapshotTO> currentBranch = new ArrayList<>();
currentBranch.add(currentSnapshotTO);
VMSnapshotTO parent = currentSnapshotTO.getParent();
while (parent != null) {
currentBranch.add(parent);
parent = parent.getParent();
}
for (VMSnapshotVO vmSnapshotVO : vmSnapshotList) {
if (currentBranch.stream().anyMatch(currentBranchSnap -> Objects.equals(currentBranchSnap.getId(), vmSnapshotVO.getId()))) {
logger.trace("Removing snapshot [{}] from the list of VM snapshots of VM [{}] being rebased.", vmSnapshotVO.getUuid(), userVm.getUuid());
vmSnapshotList.remove(vmSnapshotVO);
return;
}
}
}
private void transitStateWithoutThrow(VMSnapshot vmSnapshot, VMSnapshot.Event event) {
try {
vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, event);
} catch (NoTransitionException e) {
String msg = String.format("Failed to change VM snapshot [%s] state with event [%s].", vmSnapshot, event.toString());
logger.error(msg, e);
throw new CloudRuntimeException(msg, e);
}
}
}

View File

@ -95,8 +95,6 @@ public class StorageVMSnapshotStrategy extends DefaultVMSnapshotStrategy {
@Inject
VMSnapshotDetailsDao vmSnapshotDetailsDao;
private static final String STORAGE_SNAPSHOT = "kvmStorageSnapshot";
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
return super.configure(name, params);
@ -356,12 +354,25 @@ public class StorageVMSnapshotStrategy extends DefaultVMSnapshotStrategy {
@Override
public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) {
if (SnapshotManager.VmStorageSnapshotKvm.value() && !snapshotMemory) {
UserVmVO vm = userVmDao.findById(vmId);
if (vm.getState() == VirtualMachine.State.Running) {
return StrategyPriority.HYPERVISOR;
}
UserVmVO vm = userVmDao.findById(vmId);
String cantHandleLog = String.format("Storage VM snapshot strategy cannot handle VM snapshot for [%s]", vm);
if (CollectionUtils.isNotEmpty(vmSnapshotDao.findByVmAndByType(vmId, VMSnapshot.Type.DiskAndMemory))) {
logger.debug("{} as it has VM snapshots with disk and memory.", cantHandleLog);
return StrategyPriority.CANT_HANDLE;
}
if (!VirtualMachine.State.Running.equals(vm.getState())) {
logger.debug("{} as the VM is not running.", cantHandleLog);
return StrategyPriority.CANT_HANDLE;
}
if (SnapshotManager.VmStorageSnapshotKvm.value() && !snapshotMemory) {
return StrategyPriority.HYPERVISOR;
}
logger.debug("{} as {}.", () -> cantHandleLog, () -> snapshotMemory ? "A VM snapshot with memory was requested" :
String.format("%s is false", SnapshotManager.VmStorageSnapshotKvm.key()));
return StrategyPriority.CANT_HANDLE;
}

View File

@ -48,4 +48,7 @@
<bean id="ScaleIOVMSnapshotStrategy"
class="org.apache.cloudstack.storage.vmsnapshot.ScaleIOVMSnapshotStrategy" />
<bean id="KvmFileBasedStorageVmSnapshotStrategy"
class="org.apache.cloudstack.storage.vmsnapshot.KvmFileBasedStorageVmSnapshotStrategy" />
</beans>

View File

@ -25,6 +25,7 @@ import java.util.List;
import javax.inject.Inject;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotStrategy;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
@ -283,6 +284,11 @@ public class VMSnapshotStrategyTest extends TestCase {
return Mockito.mock(VMSnapshotDao.class);
}
@Bean
public VMSnapshotDetailsDao vmSnapshotDetailsDao() {
return Mockito.mock(VMSnapshotDetailsDao.class);
}
@Bean
public ConfigurationDao configurationDao() {
return Mockito.mock(ConfigurationDao.class);

View File

@ -28,6 +28,7 @@ import com.cloud.storage.ScopeType;
import com.cloud.storage.Storage;
import com.cloud.storage.StoragePoolHostVO;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeApiServiceImpl;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.StoragePoolHostDao;
import com.cloud.storage.dao.VolumeDao;
@ -36,6 +37,12 @@ import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.VMInstanceDao;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.VMSnapshotVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import org.apache.cloudstack.backup.dao.BackupDao;
import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
@ -88,6 +95,12 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
@Inject
private AgentManager agentManager;
@Inject
private VMSnapshotDao vmSnapshotDao;
@Inject
private VMSnapshotDetailsDao vmSnapshotDetailsDao;
protected Host getLastVMHypervisorHost(VirtualMachine vm) {
Long hostId = vm.getLastHostId();
if (hostId == null) {
@ -402,6 +415,14 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
@Override
public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backupOffering) {
for (VMSnapshotVO vmSnapshotVO : vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) {
List<VMSnapshotDetailsVO> vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId());
if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT.equals(vmSnapshotDetailsVO.getName()))) {
logger.warn("VM [{}] has VM snapshots using the KvmFileBasedStorageVmSnapshot Strategy; this provider does not support backups on VMs with these snapshots!");
return false;
}
}
return Hypervisor.HypervisorType.KVM.equals(vm.getHypervisorType());
}

View File

@ -0,0 +1,77 @@
// 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;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;
import org.libvirt.event.BlockJobListener;
import org.libvirt.event.BlockJobStatus;
import org.libvirt.event.BlockJobType;
import java.util.concurrent.Semaphore;
public class BlockCommitListener implements BlockJobListener {
private Semaphore semaphore;
private String result;
private String vmName;
private Logger logger;
private String logid;
protected BlockCommitListener(Semaphore semaphore, String vmName, String logid) {
this.semaphore = semaphore;
this.vmName = vmName;
this.logid = logid;
logger = LogManager.getLogger(getClass());
}
protected String getResult() {
return result;
}
@Override
public void onEvent(Domain domain, String diskPath, BlockJobType type, BlockJobStatus status) {
if (!BlockJobType.COMMIT.equals(type) && !BlockJobType.ACTIVE_COMMIT.equals(type)) {
return;
}
switch (status) {
case COMPLETED:
result = null;
semaphore.release();
return;
case READY:
try {
ThreadContext.put("logcontextid", logid);
logger.debug("Pivoting disk [{}] of VM [{}].", diskPath, vmName);
domain.blockJobAbort(diskPath, Domain.BlockJobAbortFlags.PIVOT);
} catch (LibvirtException ex) {
result = String.format("Failed to pivot disk due to [%s].", ex.getMessage());
semaphore.release();
}
return;
default:
result = String.format("Failed to block commit disk with status [%s].", status);
semaphore.release();
}
}
}

View File

@ -31,6 +31,7 @@ import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
@ -46,6 +47,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -102,10 +105,12 @@ import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.ThreadContext;
import org.apache.xerces.impl.xpath.regex.Match;
import org.joda.time.Duration;
import org.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.DomainBlockJobInfo;
import org.libvirt.DomainBlockStats;
import org.libvirt.DomainInfo;
import org.libvirt.DomainInfo.DomainState;
@ -370,6 +375,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
public static final String CHECKPOINT_DELETE_COMMAND = "virsh checkpoint-delete --domain %s --checkpointname %s --metadata";
protected int qcow2DeltaMergeTimeout;
private String modifyVlanPath;
private String versionStringPath;
private String patchScriptPath;
@ -561,6 +568,14 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
public static final String CGROUP_V2 = "cgroup2fs";
/**
* Virsh command to merge (blockcommit) snapshot into the base file.<br><br>
* 1st parameter: VM's name;<br>
* 2nd parameter: disk's label (target.dev tag from VM's XML);<br>
* 3rd parameter: the absolute path of the base file;
*/
private static final String COMMAND_MERGE_SNAPSHOT = "virsh blockcommit %s %s --base %s";
public long getHypervisorLibvirtVersion() {
return hypervisorLibvirtVersion;
}
@ -1179,6 +1194,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
cmdsTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.CMDS_TIMEOUT) * 1000;
noMemBalloon = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VM_MEMBALLOON_DISABLE);
qcow2DeltaMergeTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.QCOW2_DELTA_MERGE_TIMEOUT);
qcow2DeltaMergeTimeout = qcow2DeltaMergeTimeout > 0 ? qcow2DeltaMergeTimeout : AgentProperties.QCOW2_DELTA_MERGE_TIMEOUT.getDefaultValue();
manualCpuSpeed = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_CPU_MANUAL_SPEED_MHZ);
@ -4645,6 +4662,13 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
}
}
public DiskDef getDiskWithPathOfVolumeObjectTO(List<DiskDef> disks, VolumeObjectTO vol) {
return disks.stream()
.filter(diskDef -> diskDef.getDiskPath() != null && diskDef.getDiskPath().contains(vol.getPath()))
.findFirst()
.orElseThrow(() -> new CloudRuntimeException(String.format("Unable to find volume [%s].", vol.getUuid())));
}
protected String getDiskPathFromDiskDef(DiskDef disk) {
final String path = disk.getDiskPath();
if (path != null) {
@ -5844,10 +5868,228 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
}
}
/**
* Retrieves the temporary path of the snapshot.
* @param diskPath Path of the disk to snapshot;
* @param snapshotName Snapshot name;
* @return the path of the disk replacing the disk with the snapshot.
*/
public String getSnapshotTemporaryPath(String diskPath, String snapshotName) {
String[] diskPathSplitted = diskPath.split(File.separator);
diskPathSplitted[diskPathSplitted.length - 1] = snapshotName;
return String.join(File.separator, diskPathSplitted);
}
public static String generateSecretUUIDFromString(String seed) {
return UUID.nameUUIDFromBytes(seed.getBytes()).toString();
}
/**
* Merges the snapshot into base file.
*
* @param vm Domain of the VM;
* @param diskLabel Disk label to manage snapshot and base file;
* @param baseFilePath Path of the base file;
* @param topFilePath Path of the top file, if null, the active image is used;
* @param active Whether the snapshot being merged is the active image;
* @param snapshotName Name of the snapshot;
* @param volume VolumeObjectTO of the corresponding volume;
* @param conn Libvirt connection;
* @throws LibvirtException
*/
public void mergeSnapshotIntoBaseFile(Domain vm, String diskLabel, String baseFilePath, String topFilePath, boolean active, String snapshotName, VolumeObjectTO volume,
Connect conn) throws LibvirtException {
if (AgentPropertiesFileHandler.getPropertyValue(AgentProperties.LIBVIRT_EVENTS_ENABLED)) {
mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(vm, diskLabel, baseFilePath, topFilePath, active, snapshotName, volume, conn);
} else {
mergeSnapshotIntoBaseFileWithoutEvents(vm, diskLabel, baseFilePath, topFilePath, active, snapshotName, volume, conn);
}
}
/**
* This method only works if LIBVIRT_EVENTS_ENABLED is true.
* */
protected void mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(Domain vm, String diskLabel, String baseFilePath, String topFilePath, boolean active, String snapshotName, VolumeObjectTO volume,
Connect conn) throws LibvirtException {
boolean isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit = LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(conn);
String vmName = vm.getName();
int commitFlags = 0;
if (isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit) {
commitFlags |= Domain.BlockCommitFlags.DELETE;
}
if (active) {
commitFlags |= Domain.BlockCommitFlags.ACTIVE;
}
Semaphore semaphore = getSemaphoreToWaitForMerge();
BlockCommitListener blockCommitListener = getBlockCommitListener(semaphore, vmName);
vm.addBlockJobListener(blockCommitListener);
logger.info("Starting block commit of snapshot [{}] of VM [{}]. Using parameters: diskLabel [{}]; baseFilePath [{}]; topFilePath [{}]; commitFlags [{}]", snapshotName,
vmName, diskLabel, baseFilePath, topFilePath, commitFlags);
vm.blockCommit(diskLabel, baseFilePath, topFilePath, 0, commitFlags);
Thread checkProgressThread = new Thread(() -> checkBlockCommitProgress(vm, diskLabel, vmName, snapshotName, topFilePath, baseFilePath));
checkProgressThread.start();
String errorMessage = String.format("the block commit of top file [%s] into base file [%s] for snapshot [%s] of VM [%s]." +
" The job will be left running to avoid data corruption, but ACS will return an error and volume [%s] will need to be normalized manually. If the commit" +
" involved the active image, the pivot will need to be manually done.", topFilePath, baseFilePath, snapshotName, vmName, volume);
try {
if (!semaphore.tryAcquire(qcow2DeltaMergeTimeout, TimeUnit.SECONDS)) {
throw new CloudRuntimeException("Timed out while waiting for " + errorMessage);
}
} catch (InterruptedException e) {
throw new CloudRuntimeException("Interrupted while waiting for " + errorMessage);
} finally {
vm.removeBlockJobListener(blockCommitListener);
}
String mergeResult = blockCommitListener.getResult();
try {
checkProgressThread.join();
} catch (InterruptedException ex) {
throw new CloudRuntimeException(String.format("Exception while running wait block commit task of snapshot [%s] and VM [%s].", snapshotName, vmName));
}
if (mergeResult != null) {
String commitError = String.format("Failed %s The failure occurred due to [%s].", errorMessage, mergeResult);
logger.error(commitError);
throw new CloudRuntimeException(commitError);
}
logger.info("Completed block commit of snapshot [{}] of VM [{}].", snapshotName, vmName);
manuallyDeleteUnusedSnapshotFile(isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit, topFilePath != null ? topFilePath : getSnapshotTemporaryPath(baseFilePath, snapshotName));
}
/**
* Merges the snapshot into base file to keep volume and VM behavior after stopping - starting.
* @param vm Domain of the VM;
* @param diskLabel Disk label to manage snapshot and base file;
* @param baseFilePath Path of the base file;
* @param snapshotName Name of the snapshot;
* @throws LibvirtException
*/
protected void mergeSnapshotIntoBaseFileWithoutEvents(Domain vm, String diskLabel, String baseFilePath, String topFilePath, boolean active, String snapshotName, VolumeObjectTO volume, Connect conn) throws LibvirtException {
boolean isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit = LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(conn);
String vmName = vm.getName();
String mergeCommand = buildMergeCommand(vmName, diskLabel, baseFilePath, topFilePath, active, isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit);
String mergeResult = Script.runSimpleBashScript(mergeCommand);
if (mergeResult == null) {
logger.debug("Successfully merged snapshot [{}] into VM [{}] {} base file.", snapshotName, vmName, volume);
manuallyDeleteUnusedSnapshotFile(isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit, getSnapshotTemporaryPath(baseFilePath, snapshotName));
return;
}
String errorMsg = String.format("Failed to merge snapshot [%s] into VM [%s] %s base file. Command [%s] resulted in [%s]. If the VM is stopped and then started, it"
+ " will start to write in the base file again. All changes made between the snapshot and the VM stop will be in the snapshot. If the VM is stopped, the snapshot must be"
+ " merged into the base file manually.", snapshotName, vmName, volume, mergeCommand, mergeResult);
logger.warn("%s VM XML: [{}].", errorMsg, vm.getXMLDesc(0));
throw new CloudRuntimeException(errorMsg);
}
protected String buildMergeCommand(String vmName, String diskLabel, String baseFilePath, String topFilePath, boolean active, boolean delete) {
StringBuilder cmd = new StringBuilder(COMMAND_MERGE_SNAPSHOT);
if (StringUtils.isNotEmpty(topFilePath)) {
cmd.append(" --top ");
cmd.append(topFilePath);
}
if (active) {
cmd.append(" --active --pivot");
}
if (delete) {
cmd.append(" --delete");
}
cmd.append(" --wait");
return String.format(cmd.toString(), vmName, diskLabel, baseFilePath);
}
/**
* This was created to facilitate testing.
* */
protected BlockCommitListener getBlockCommitListener(Semaphore semaphore, String vmName) {
return new BlockCommitListener(semaphore, vmName, ThreadContext.get("logcontextid"));
}
/**
* This was created to facilitate testing.
* */
protected Semaphore getSemaphoreToWaitForMerge() {
return new Semaphore(0);
}
protected void checkBlockCommitProgress(Domain vm, String diskLabel, String vmName, String snapshotName, String topFilePath, String baseFilePath) {
int timeout = qcow2DeltaMergeTimeout;
DomainBlockJobInfo result;
long lastCommittedBytes = 0;
long endBytes = 0;
String partialLog = String.format("of top file [%s] into base file [%s] for snapshot [%s] of VM [%s]", topFilePath, baseFilePath, snapshotName, vmName);
while (timeout > 0) {
timeout -= 1;
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
logger.debug("Thread that was tracking the progress {} was interrupted.", partialLog, ex);
return;
}
try {
result = vm.getBlockJobInfo(diskLabel, 0);
} catch (LibvirtException ex) {
logger.warn("Exception while getting block job info {}: [{}].", partialLog, ex.getMessage(), ex);
return;
}
if (result == null || result.type == 0 && result.end == 0 && result.cur == 0) {
logger.debug("Block commit job {} has already finished.", partialLog);
return;
}
long currentCommittedBytes = result.cur;
if (currentCommittedBytes > lastCommittedBytes) {
logger.debug("The block commit {} is at [{}] of [{}].", partialLog, currentCommittedBytes, result.end);
}
lastCommittedBytes = currentCommittedBytes;
endBytes = result.end;
}
logger.warn("Block commit {} has timed out after waiting at least {} seconds. The progress of the operation was [{}] of [{}].", partialLog, qcow2DeltaMergeTimeout, lastCommittedBytes, endBytes);
}
/**
* Manually deletes the unused snapshot file.<br/>
* This method is necessary due to Libvirt created the tag '--delete' on command 'virsh blockcommit' on version <b>1.2.9</b>, however it was only implemented on version
* <b>6.0.0</b>.
* @param snapshotPath The unused snapshot file to manually delete.
*/
protected void manuallyDeleteUnusedSnapshotFile(boolean isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit, String snapshotPath) {
if (isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit) {
logger.debug("The current Libvirt's version supports the flag '--delete' on command 'virsh blockcommit', we will skip the manually deletion of the"
+ " unused snapshot file [{}] as it already was automatically deleted.", snapshotPath);
return;
}
logger.debug("The current Libvirt's version does not supports the flag '--delete' on command 'virsh blockcommit', therefore we will manually delete the"
+ " unused snapshot file [{}].", snapshotPath);
deleteIfExists(snapshotPath);
}
protected void deleteIfExists(String snapshotPath) {
try {
Files.deleteIfExists(Paths.get(snapshotPath));
logger.debug("Manually deleted unused snapshot file [{}].", snapshotPath);
} catch (IOException ex) {
throw new CloudRuntimeException(String.format("Unable to manually delete unused snapshot file [%s] due to [%s].", snapshotPath, ex.getMessage()));
}
}
public void setInterfaceDefQueueSettings(Map<String, String> details, Integer cpus, InterfaceDef interfaceDef) {
String nicMultiqueueNumber = details.get(VmDetailConstants.NIC_MULTIQUEUE_NUMBER);
if (nicMultiqueueNumber != null) {

View File

@ -0,0 +1,198 @@
/*
* 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 com.cloud.agent.api.Answer;
import com.cloud.agent.api.VMSnapshotTO;
import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer;
import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotCommand;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef;
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.utils.Pair;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
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.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ResourceWrapper(handles = CreateDiskOnlyVmSnapshotCommand.class)
public class LibvirtCreateDiskOnlyVMSnapshotCommandWrapper extends CommandWrapper<CreateDiskOnlyVmSnapshotCommand, Answer, LibvirtComputingResource> {
private static final String SNAPSHOT_XML = "<domainsnapshot>\n" +
"<name>%s</name>\n" +
"<memory snapshot='no'/>\n" +
"<disks> \n" +
"%s" +
"</disks> \n" +
"</domainsnapshot>";
private static final String TAG_DISK_SNAPSHOT = "<disk name='%s' snapshot='external'>\n" +
"<source file='%s'/>\n" +
"</disk>\n";
@Override
public Answer execute(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
VirtualMachine.State state = cmd.getVmState();
if (VirtualMachine.State.Running.equals(state)) {
return takeDiskOnlyVmSnapshotOfRunningVm(cmd, resource);
}
return takeDiskOnlyVmSnapshotOfStoppedVm(cmd, resource);
}
protected Answer takeDiskOnlyVmSnapshotOfRunningVm(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
String vmName = cmd.getVmName();
logger.info("Taking disk-only VM snapshot of running VM [{}].", vmName);
Domain dm = null;
try {
LibvirtUtilitiesHelper libvirtUtilitiesHelper = resource.getLibvirtUtilitiesHelper();
Connect conn = libvirtUtilitiesHelper.getConnection();
List<VolumeObjectTO> volumeObjectTOS = cmd.getVolumeTOs();
List<LibvirtVMDef.DiskDef> disks = resource.getDisks(conn, vmName);
dm = resource.getDomain(conn, vmName);
if (dm == null) {
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, String.format("Creation of disk-only VM Snapshot failed as we could not find the VM [%s].", vmName), null);
}
VMSnapshotTO target = cmd.getTarget();
Pair<String, Map<String, Pair<Long, String>>> snapshotXmlAndVolumeToNewPathMap = createSnapshotXmlAndNewVolumePathMap(volumeObjectTOS, disks, target, resource);
dm.snapshotCreateXML(snapshotXmlAndVolumeToNewPathMap.first(), getFlagsToUseForRunningVmSnapshotCreation(target));
return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, snapshotXmlAndVolumeToNewPathMap.second());
} catch (LibvirtException e) {
String errorMsg = String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage());
logger.error(errorMsg, e);
if (e.getMessage().contains("QEMU guest agent is not connected")) {
errorMsg = "QEMU guest agent is not connected. If the VM has been recently started, it might connect soon. Otherwise the VM does not have the" +
" guest agent installed; thus the QuiesceVM parameter is not supported.";
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, errorMsg, null);
}
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, e.getMessage(), null);
} finally {
if (dm != null) {
try {
dm.free();
} catch (LibvirtException l) {
logger.trace("Ignoring libvirt error.", l);
}
}
}
}
protected Answer takeDiskOnlyVmSnapshotOfStoppedVm(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
String vmName = cmd.getVmName();
logger.info("Taking disk-only VM snapshot of stopped VM [{}].", vmName);
Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath = new HashMap<>();
List<VolumeObjectTO> volumeObjectTos = cmd.getVolumeTOs();
KVMStoragePoolManager storagePoolMgr = resource.getStoragePoolMgr();
try {
for (VolumeObjectTO volumeObjectTO : volumeObjectTos) {
PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore();
KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
String snapshotPath = UUID.randomUUID().toString();
String snapshotFullPath = kvmStoragePool.getLocalPathFor(snapshotPath);
QemuImgFile newDelta = new QemuImgFile(snapshotFullPath, QemuImg.PhysicalDiskFormat.QCOW2);
String currentDeltaFullPath = kvmStoragePool.getLocalPathFor(volumeObjectTO.getPath());
QemuImgFile currentDelta = new QemuImgFile(currentDeltaFullPath, QemuImg.PhysicalDiskFormat.QCOW2);
QemuImg qemuImg = new QemuImg(0);
logger.debug("Creating new delta for volume [{}] as part of the disk-only VM snapshot process for VM [{}].", volumeObjectTO.getUuid(), vmName);
qemuImg.create(newDelta, currentDelta);
mapVolumeToSnapshotSizeAndNewVolumePath.put(volumeObjectTO.getUuid(), new Pair<>(getFileSize(currentDeltaFullPath), snapshotPath));
}
} catch (LibvirtException | QemuImgException e) {
logger.error("Exception while creating disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e);
for (VolumeObjectTO volumeObjectTO : volumeObjectTos) {
Pair<Long, String> volSizeAndNewPath = mapVolumeToSnapshotSizeAndNewVolumePath.get(volumeObjectTO.getUuid());
PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore();
KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
if (volSizeAndNewPath == null) {
continue;
}
try {
Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volSizeAndNewPath.second())));
} catch (IOException ex) {
logger.warn("Tried to delete leftover snapshot at [{}] failed.", volSizeAndNewPath.second(), ex);
}
}
return new Answer(cmd, e);
}
return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, mapVolumeToSnapshotSizeAndNewVolumePath);
}
protected int getFlagsToUseForRunningVmSnapshotCreation(VMSnapshotTO target) {
int flags = target.getQuiescevm() ? Domain.SnapshotCreateFlags.QUIESCE : 0;
flags += Domain.SnapshotCreateFlags.DISK_ONLY +
Domain.SnapshotCreateFlags.ATOMIC +
Domain.SnapshotCreateFlags.NO_METADATA;
return flags;
}
protected Pair<String, Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS, List<LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
StringBuilder stringBuilder = new StringBuilder();
Map<String, Pair<Long, String>> volumeObjectToNewPathMap = new HashMap<>();
for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) {
LibvirtVMDef.DiskDef diskdef = resource.getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO);
String newPath = UUID.randomUUID().toString();
stringBuilder.append(String.format(TAG_DISK_SNAPSHOT, diskdef.getDiskLabel(), resource.getSnapshotTemporaryPath(diskdef.getDiskPath(), newPath)));
long snapSize = getFileSize(diskdef.getDiskPath());
volumeObjectToNewPathMap.put(volumeObjectTO.getUuid(), new Pair<>(snapSize, newPath));
}
String snapshotXml = String.format(SNAPSHOT_XML, target.getSnapshotName(), stringBuilder);
return new Pair<>(snapshotXml, volumeObjectToNewPathMap);
}
protected long getFileSize(String path) {
return new File(path).length();
}
}

View File

@ -0,0 +1,58 @@
/*
* 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 com.cloud.agent.api.Answer;
import com.cloud.agent.api.storage.DeleteDiskOnlyVmSnapshotCommand;
import com.cloud.agent.api.to.DataTO;
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 org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@ResourceWrapper(handles = DeleteDiskOnlyVmSnapshotCommand.class)
public class LibvirtDeleteDiskOnlyVMSnapshotCommandWrapper extends CommandWrapper<DeleteDiskOnlyVmSnapshotCommand, Answer, LibvirtComputingResource> {
@Override
public Answer execute(DeleteDiskOnlyVmSnapshotCommand command, LibvirtComputingResource resource) {
List<DataTO> snapshotsToDelete = command.getSnapshots();
KVMStoragePoolManager storagePoolMgr = resource.getStoragePoolMgr();
for (DataTO snapshot : snapshotsToDelete) {
PrimaryDataStoreTO dataStoreTO = (PrimaryDataStoreTO) snapshot.getDataStore();
KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(dataStoreTO.getPoolType(), dataStoreTO.getUuid());
try {
String path = kvmStoragePool.getLocalPathFor(snapshot.getPath());
logger.debug("Deleting snapshot [{}] file [{}] as part of VM snapshot deletion.", snapshot.getId(), path);
Files.deleteIfExists(Path.of(path));
} catch (IOException e) {
return new Answer(command, e);
}
}
return new Answer(command, true, null);
}
}

View File

@ -0,0 +1,150 @@
/*
* 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 com.cloud.agent.api.Answer;
import com.cloud.agent.api.storage.MergeDiskOnlyVmSnapshotCommand;
import com.cloud.agent.api.storage.SnapshotMergeTreeTO;
import com.cloud.agent.api.to.DataObjectType;
import com.cloud.agent.api.to.DataTO;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef;
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.utils.exception.CloudRuntimeException;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
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.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
@ResourceWrapper(handles = MergeDiskOnlyVmSnapshotCommand.class)
public class LibvirtMergeDiskOnlyVMSnapshotCommandWrapper extends CommandWrapper<MergeDiskOnlyVmSnapshotCommand, Answer, LibvirtComputingResource> {
@Override
public Answer execute(MergeDiskOnlyVmSnapshotCommand command, LibvirtComputingResource serverResource) {
VirtualMachine.State vmState = command.getVmState();
try {
if (VirtualMachine.State.Running.equals(vmState)) {
return mergeDiskOnlySnapshotsForRunningVM(command, serverResource);
}
return mergeDiskOnlySnapshotsForStoppedVM(command, serverResource);
} catch (LibvirtException | QemuImgException | CloudRuntimeException ex) {
return new Answer(command, ex);
}
}
protected Answer mergeDiskOnlySnapshotsForStoppedVM(MergeDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws QemuImgException, LibvirtException {
QemuImg qemuImg = new QemuImg(resource.getCmdsTimeout());
KVMStoragePoolManager storageManager = resource.getStoragePoolMgr();
List<SnapshotMergeTreeTO> snapshotMergeTreeTOList = cmd.getSnapshotMergeTreeToList();
logger.debug("Merging disk-only snapshots for stopped VM [{}] using the following Snapshot Merge Trees [{}].", cmd.getVmName(), snapshotMergeTreeTOList);
for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeTOList) {
DataTO parentTo = snapshotMergeTreeTO.getParent();
PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) parentTo.getDataStore();
KVMStoragePool storagePool = storageManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
String childLocalPath = storagePool.getLocalPathFor(snapshotMergeTreeTO.getChild().getPath());
QemuImgFile parent = new QemuImgFile(storagePool.getLocalPathFor(parentTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2);
QemuImgFile child = new QemuImgFile(childLocalPath, QemuImg.PhysicalDiskFormat.QCOW2);
logger.debug("Committing child delta [{}] into parent snapshot [{}].", parentTo, snapshotMergeTreeTO.getChild());
qemuImg.commit(child, parent, true);
List<QemuImgFile> grandChildren = snapshotMergeTreeTO.getGrandChildren().stream()
.map(snapshotTo -> new QemuImgFile(storagePool.getLocalPathFor(snapshotTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2))
.collect(Collectors.toList());
logger.debug("Rebasing grandChildren [{}] into parent at [{}].", grandChildren, parent.getFileName());
for (QemuImgFile grandChild : grandChildren) {
qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false);
}
logger.debug("Deleting child at [{}] as it is useless.", childLocalPath);
try {
Files.deleteIfExists(Path.of(childLocalPath));
} catch (IOException e) {
return new Answer(cmd, e);
}
}
return new Answer(cmd, true, null);
}
protected Answer mergeDiskOnlySnapshotsForRunningVM(MergeDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws LibvirtException, QemuImgException {
String vmName = cmd.getVmName();
List<SnapshotMergeTreeTO> snapshotMergeTreeTOList = cmd.getSnapshotMergeTreeToList();
LibvirtUtilitiesHelper libvirtUtilitiesHelper = resource.getLibvirtUtilitiesHelper();
Connect conn = libvirtUtilitiesHelper.getConnection();
Domain domain = resource.getDomain(conn, vmName);
List<LibvirtVMDef.DiskDef> disks = resource.getDisks(conn, vmName);
KVMStoragePoolManager storageManager = resource.getStoragePoolMgr();
QemuImg qemuImg = new QemuImg(resource.getCmdsTimeout());
logger.debug("Merging disk-only snapshots for running VM [{}] using the following Snapshot Merge Trees [{}].", vmName, snapshotMergeTreeTOList);
for (SnapshotMergeTreeTO mergeTreeTO : snapshotMergeTreeTOList) {
DataTO childTO = mergeTreeTO.getChild();
SnapshotObjectTO parentSnapshotTO = (SnapshotObjectTO) mergeTreeTO.getParent();
VolumeObjectTO volumeObjectTO = parentSnapshotTO.getVolume();
KVMStoragePool storagePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(volumeObjectTO, storageManager);
boolean active = DataObjectType.VOLUME.equals(childTO.getObjectType());
String label = resource.getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO).getDiskLabel();
String parentSnapshotLocalPath = storagePool.getLocalPathFor(parentSnapshotTO.getPath());
String childDeltaPath = storagePool.getLocalPathFor(childTO.getPath());
logger.debug("Found label [{}] for [{}]. Will merge delta at [{}] into delta at [{}].", label, volumeObjectTO, parentSnapshotLocalPath, childDeltaPath);
resource.mergeSnapshotIntoBaseFile(domain, label, parentSnapshotLocalPath, childDeltaPath, active, childTO.getPath(),
volumeObjectTO, conn);
QemuImgFile parent = new QemuImgFile(parentSnapshotLocalPath, QemuImg.PhysicalDiskFormat.QCOW2);
List<QemuImgFile> grandChildren = mergeTreeTO.getGrandChildren().stream()
.map(snapshotTo -> new QemuImgFile(storagePool.getLocalPathFor(snapshotTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2))
.collect(Collectors.toList());
logger.debug("Rebasing grandChildren [{}] into parent at [{}].", grandChildren, parentSnapshotLocalPath);
for (QemuImgFile grandChild : grandChildren) {
qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false);
}
}
return new Answer(cmd, true, null);
}
}

View File

@ -0,0 +1,111 @@
/*
* 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 com.cloud.agent.api.Answer;
import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotAnswer;
import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotCommand;
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 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.libvirt.LibvirtException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ResourceWrapper(handles = RevertDiskOnlyVmSnapshotCommand.class)
public class LibvirtRevertDiskOnlyVMSnapshotCommandWrapper extends CommandWrapper<RevertDiskOnlyVmSnapshotCommand, Answer, LibvirtComputingResource> {
@Override
public Answer execute(RevertDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
List<SnapshotObjectTO> snapshotObjectTos = cmd.getSnapshotObjectTos();
String vmName = cmd.getVmName();
logger.info("Reverting disk-only VM snapshot of VM [{}]", vmName);
KVMStoragePoolManager storagePoolMgr = resource.getStoragePoolMgr();
LibvirtUtilitiesHelper libvirtUtilitiesHelper = resource.getLibvirtUtilitiesHelper();
HashMap<SnapshotObjectTO, String> snapshotToNewDeltaPath = new HashMap<>();
try {
for (SnapshotObjectTO snapshotObjectTo : snapshotObjectTos) {
KVMStoragePool kvmStoragePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(snapshotObjectTo, storagePoolMgr);
String deltaPath = libvirtUtilitiesHelper.generateUUIDName();
String deltaFullPath = kvmStoragePool.getLocalPathFor(deltaPath);
QemuImgFile newDelta = new QemuImgFile(deltaFullPath, QemuImg.PhysicalDiskFormat.QCOW2);
String snapshotFullPath = kvmStoragePool.getLocalPathFor(snapshotObjectTo.getPath());
QemuImgFile currentDelta = new QemuImgFile(snapshotFullPath, QemuImg.PhysicalDiskFormat.QCOW2);
QemuImg qemuImg = new QemuImg(0);
logger.debug("Creating new delta for volume [{}] as part of the disk-only VM snapshot revert process for VM [{}].", snapshotObjectTo.getVolume().getUuid(), vmName);
qemuImg.create(newDelta, currentDelta);
snapshotToNewDeltaPath.put(snapshotObjectTo, deltaPath);
}
} catch (LibvirtException | QemuImgException e) {
logger.error("Exception while reverting disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e);
for (SnapshotObjectTO snapshotObjectTo : snapshotObjectTos) {
String newPath = snapshotToNewDeltaPath.get(snapshotObjectTo);
if (newPath == null) {
continue;
}
KVMStoragePool kvmStoragePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(snapshotObjectTo, storagePoolMgr);
try {
Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(newPath)));
} catch (IOException ex) {
logger.warn("Tried to delete leftover snapshot at [{}] failed.", newPath, ex);
}
}
return new Answer(cmd, e);
}
List<VolumeObjectTO> volumeObjectTos = new ArrayList<>();
for (SnapshotObjectTO snapshotObjectTo : snapshotObjectTos) {
VolumeObjectTO volumeObjectTo = snapshotObjectTo.getVolume();
KVMStoragePool kvmStoragePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(snapshotObjectTo, storagePoolMgr);
try {
Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volumeObjectTo.getPath())));
} catch (IOException ex) {
logger.warn("Got an error while trying to delete old volume delta [{}], there might be trash on storage [{}].", volumeObjectTo.getPath(),
kvmStoragePool.getUuid());
}
volumeObjectTo.setPath(snapshotToNewDeltaPath.get(snapshotObjectTo));
volumeObjectTos.add(volumeObjectTo);
}
return new RevertDiskOnlyVmSnapshotAnswer(cmd, volumeObjectTos);
}
}

View File

@ -22,6 +22,10 @@ import java.util.UUID;
import javax.naming.ConfigurationException;
import com.cloud.agent.api.to.DataTO;
import com.cloud.hypervisor.kvm.storage.KVMStoragePool;
import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.libvirt.Connect;
@ -145,4 +149,10 @@ public class LibvirtUtilitiesHelper {
result.second() ? "" : " does not"));
return result.second();
}
public KVMStoragePool getPrimaryPoolFromDataTo(DataTO dataTO, KVMStoragePoolManager storageManager) {
PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) dataTO.getDataStore();
return storageManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
}
}

View File

@ -136,7 +136,6 @@ import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef.DeviceType;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef.DiscardType;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef.DiskProtocol;
import com.cloud.hypervisor.kvm.resource.wrapper.LibvirtUtilitiesHelper;
import com.cloud.storage.JavaStorageLayer;
import com.cloud.storage.MigrationOptions;
import com.cloud.storage.ScopeType;
@ -1788,15 +1787,6 @@ public class KVMStorageProcessor implements StorageProcessor {
*/
private static final String TAG_AVOID_DISK_FROM_SNAPSHOT = "<disk name='%s' snapshot='no' />";
/**
* Virsh command to merge (blockcommit) snapshot into the base file.<br><br>
* 1st parameter: VM's name;<br>
* 2nd parameter: disk's label (target.dev tag from VM's XML);<br>
* 3rd parameter: the absolute path of the base file;
* 4th parameter: the flag '--delete', if Libvirt supports it. Libvirt started to support it on version <b>6.0.0</b>;
*/
private static final String COMMAND_MERGE_SNAPSHOT = "virsh blockcommit %s %s --base %s --active --wait %s --pivot";
/**
* Flag to take disk-only snapshots from VM.<br><br>
* Libvirt lib for java does not have the enum virDomainSnapshotCreateFlags.
@ -2238,7 +2228,7 @@ public class KVMStorageProcessor implements StorageProcessor {
String convertResult = convertBaseFileToSnapshotFileInStorageDir(ObjectUtils.defaultIfNull(secondaryPool, primaryPool), disk, snapshotPath, directoryPath, volume, cmd.getWait());
mergeSnapshotIntoBaseFile(vm, diskLabel, diskPath, snapshotName, volume, conn);
resource.mergeSnapshotIntoBaseFile(vm, diskLabel, diskPath, null, true, snapshotName, volume, conn);
validateConvertResult(convertResult, snapshotPath);
} catch (LibvirtException e) {
@ -2430,59 +2420,6 @@ public class KVMStorageProcessor implements StorageProcessor {
throw new CloudRuntimeException(convertResult);
}
/**
* Merges the snapshot into base file to keep volume and VM behavior after stopping - starting.
* @param vm Domain of the VM;
* @param diskLabel Disk label to manage snapshot and base file;
* @param baseFilePath Path of the base file;
* @param snapshotName Name of the snapshot;
* @throws LibvirtException
*/
protected void mergeSnapshotIntoBaseFile(Domain vm, String diskLabel, String baseFilePath, String snapshotName, VolumeObjectTO volume,
Connect conn) throws LibvirtException {
boolean isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit = LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(conn);
String vmName = vm.getName();
String mergeCommand = String.format(COMMAND_MERGE_SNAPSHOT, vmName, diskLabel, baseFilePath, isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit ? "--delete" : "");
String mergeResult = Script.runSimpleBashScript(mergeCommand);
if (mergeResult == null) {
logger.debug(String.format("Successfully merged snapshot [%s] into VM [%s] %s base file.", snapshotName, vmName, volume));
manuallyDeleteUnusedSnapshotFile(isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit, getSnapshotTemporaryPath(baseFilePath, snapshotName));
return;
}
String errorMsg = String.format("Failed to merge snapshot [%s] into VM [%s] %s base file. Command [%s] resulted in [%s]. If the VM is stopped and then started, it"
+ " will start to write in the base file again. All changes made between the snapshot and the VM stop will be in the snapshot. If the VM is stopped, the snapshot must be"
+ " merged into the base file manually.", snapshotName, vmName, volume, mergeCommand, mergeResult);
logger.warn(String.format("%s VM XML: [%s].", errorMsg, vm.getXMLDesc(0)));
throw new CloudRuntimeException(errorMsg);
}
/**
* Manually deletes the unused snapshot file.<br/>
* This method is necessary due to Libvirt created the tag '--delete' on command 'virsh blockcommit' on version <b>1.2.9</b>, however it was only implemented on version
* <b>6.0.0</b>.
* @param snapshotPath The unused snapshot file to manually delete.
*/
protected void manuallyDeleteUnusedSnapshotFile(boolean isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit, String snapshotPath) {
if (isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit) {
logger.debug(String.format("The current Libvirt's version supports the flag '--delete' on command 'virsh blockcommit', we will skip the manually deletion of the"
+ " unused snapshot file [%s] as it already was automatically deleted.", snapshotPath));
return;
}
logger.debug(String.format("The current Libvirt's version does not supports the flag '--delete' on command 'virsh blockcommit', therefore we will manually delete the"
+ " unused snapshot file [%s].", snapshotPath));
try {
Files.deleteIfExists(Paths.get(snapshotPath));
logger.debug(String.format("Manually deleted unused snapshot file [%s].", snapshotPath));
} catch (IOException ex) {
throw new CloudRuntimeException(String.format("Unable to manually delete unused snapshot file [%s] due to [%s].", snapshotPath, ex.getMessage()));
}
}
/**
* Creates the snapshot directory in the primary storage, if it does not exist; then, converts the base file (VM's old writing file) to the snapshot directory.
* @param pool Storage to create folder, if not exists;
@ -2569,7 +2506,7 @@ public class KVMStorageProcessor implements StorageProcessor {
String diskLabelToSnapshot = diskToSnapshotAndDisksToAvoid.first();
String disksToAvoidsOnSnapshot = diskToSnapshotAndDisksToAvoid.second().stream().map(diskLabel -> String.format(TAG_AVOID_DISK_FROM_SNAPSHOT, diskLabel))
.collect(Collectors.joining());
String snapshotTemporaryPath = getSnapshotTemporaryPath(diskPath, snapshotName);
String snapshotTemporaryPath = resource.getSnapshotTemporaryPath(diskPath, snapshotName);
String createSnapshotXmlFormated = String.format(XML_CREATE_DISK_SNAPSHOT, snapshotName, diskLabelToSnapshot, snapshotTemporaryPath, disksToAvoidsOnSnapshot);
@ -2620,18 +2557,6 @@ public class KVMStorageProcessor implements StorageProcessor {
return new Pair<>(diskLabelToSnapshot, disksToAvoid);
}
/**
* Retrieves the temporary path of the snapshot.
* @param diskPath Path of the disk to snapshot;
* @param snapshotName Snapshot name;
* @return the path of the disk replacing the disk with the snapshot.
*/
protected String getSnapshotTemporaryPath(String diskPath, String snapshotName) {
String[] diskPathSplitted = diskPath.split(File.separator);
diskPathSplitted[diskPathSplitted.length - 1] = snapshotName;
return String.join(File.separator, diskPathSplitted);
}
/**
* Validate if the primary storage has enough capacity to take a disk snapshot, as the snapshot will duplicate the disk to backup.
* @param primaryPool Primary storage to verify capacity;

View File

@ -829,6 +829,46 @@ public class QemuImg {
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

View File

@ -17,6 +17,7 @@
package org.apache.cloudstack.utils.qemu;
import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
public class QemuImgFile {
@ -68,4 +69,9 @@ public class QemuImgFile {
return this.format;
}
@Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}

View File

@ -41,6 +41,8 @@ import java.io.IOException;
import java.net.NetworkInterface;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@ -49,6 +51,7 @@ import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.Semaphore;
import javax.naming.ConfigurationException;
import javax.xml.parsers.DocumentBuilderFactory;
@ -64,6 +67,7 @@ import org.apache.cloudstack.api.ApiConstants.IoDriverPolicy;
import org.apache.cloudstack.storage.command.AttachAnswer;
import org.apache.cloudstack.storage.command.AttachCommand;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
import org.apache.cloudstack.utils.linux.CPUStat;
@ -73,6 +77,7 @@ import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.SystemUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.joda.time.Duration;
import org.junit.Assert;
import org.junit.Before;
@ -261,6 +266,12 @@ public class LibvirtComputingResourceTest {
@Mock
DomainBlockStats domainBlockStatsMock;
@Mock
SnapshotObjectTO snapshotObjectToMock;
@Mock
BlockCommitListener blockCommitListenerMock;
private final static long HYPERVISOR_LIBVIRT_VERSION_SUPPORTS_IOURING = 6003000;
private final static long HYPERVISOR_QEMU_VERSION_SUPPORTS_IOURING = 5000000;
@ -6620,4 +6631,152 @@ public class LibvirtComputingResourceTest {
Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).recreateCheckpointsOfDisk(Mockito.any(), Mockito.any(), Mockito.any());
Assert.assertTrue(result);
}
@Test
public void getSnapshotTemporaryPathTestReturnExpectedResult(){
String path = "/path/to/disk";
String snapshotName = "snapshot";
String expectedResult = "/path/to/snapshot";
String result = libvirtComputingResourceSpy.getSnapshotTemporaryPath(path, snapshotName);
Assert.assertEquals(expectedResult, result);
}
@Test
public void mergeSnapshotIntoBaseFileTestActiveAndDeleteFlags() throws Exception {
libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10;
try (MockedStatic<LibvirtUtilitiesHelper> libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class);
MockedStatic<ThreadContext> threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) {
libvirtUtilitiesHelperMockedStatic.when(() ->
LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenAnswer(invocation -> true);
Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge();
threadContextMockedStatic.when(() ->
ThreadContext.get(Mockito.anyString())).thenReturn("logid");
Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any());
Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt());
Mockito.doNothing().when(domainMock).removeBlockJobListener(Mockito.any());
String diskLabel = "vda";
String baseFilePath = "/file";
String snapshotName = "snap";
libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock);
Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, Domain.BlockCommitFlags.ACTIVE | Domain.BlockCommitFlags.DELETE);
Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(true, "/" + snapshotName);
}
}
@Test
public void mergeSnapshotIntoBaseFileTestActiveFlag() throws Exception {
try (MockedStatic<LibvirtUtilitiesHelper> libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class);
MockedStatic<ThreadContext> threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) {
libvirtUtilitiesHelperMockedStatic.when(() ->
LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenAnswer(invocation -> false);
Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge();
threadContextMockedStatic.when(() ->
ThreadContext.get(Mockito.anyString())).thenReturn("logid");
Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any());
Mockito.doNothing().when(domainMock).removeBlockJobListener(Mockito.any());
Mockito.doNothing().when(libvirtComputingResourceSpy).manuallyDeleteUnusedSnapshotFile(Mockito.anyBoolean(), Mockito.anyString());
String diskLabel = "vda";
String baseFilePath = "/file";
String snapshotName = "snap";
libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock);
Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, Domain.BlockCommitFlags.ACTIVE);
Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(false, "/" + snapshotName);
}
}
@Test
public void mergeSnapshotIntoBaseFileTestDeleteFlag() throws Exception {
try (MockedStatic<LibvirtUtilitiesHelper> libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class);
MockedStatic<ThreadContext> threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) {
libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10;
libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenReturn(true);
Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge();
threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid");
Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any());
Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt());
Mockito.doNothing().when(domainMock).removeBlockJobListener(Mockito.any());
Mockito.doNothing().when(libvirtComputingResourceSpy).manuallyDeleteUnusedSnapshotFile(Mockito.anyBoolean(), Mockito.anyString());
String diskLabel = "vda";
String baseFilePath = "/file";
String snapshotName = "snap";
libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock);
Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, Domain.BlockCommitFlags.DELETE);
Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(true, "/" + snapshotName);
}
}
@Test
public void mergeSnapshotIntoBaseFileTestNoFlags() throws Exception {
try (MockedStatic<LibvirtUtilitiesHelper> libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class);
MockedStatic<ThreadContext> threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) {
libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10;
libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenReturn(false);
Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge();
threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid");
Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any());
Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt());
Mockito.doNothing().when(domainMock).removeBlockJobListener(Mockito.any());
Mockito.doNothing().when(libvirtComputingResourceSpy).manuallyDeleteUnusedSnapshotFile(Mockito.anyBoolean(), Mockito.anyString());
String diskLabel = "vda";
String baseFilePath = "/file";
String snapshotName = "snap";
libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock);
Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, 0);
Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(false, "/" + snapshotName);
}
}
@Test (expected = CloudRuntimeException.class)
public void mergeSnapshotIntoBaseFileTestMergeFailsThrowException() throws Exception {
try (MockedStatic<LibvirtUtilitiesHelper> libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class);
MockedStatic<ThreadContext> threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) {
libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10;
libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenReturn(false);
Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge();
threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid");
Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any());
Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt());
Mockito.doNothing().when(domainMock).removeBlockJobListener(Mockito.any());
Mockito.doReturn(blockCommitListenerMock).when(libvirtComputingResourceSpy).getBlockCommitListener(Mockito.any(), Mockito.any());
Mockito.doReturn("Failed").when(blockCommitListenerMock).getResult();
String diskLabel = "vda";
String baseFilePath = "/file";
String snapshotName = "snap";
libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock);
}
}
@Test (expected = CloudRuntimeException.class)
public void manuallyDeleteUnusedSnapshotFileTestLibvirtDoesNotSupportsFlagDeleteExceptionOnFileDeletionThrowsException() throws IOException {
try (MockedStatic<Files> filesMockedStatic = Mockito.mockStatic(Files.class)) {
filesMockedStatic.when(() -> Files.deleteIfExists(Mockito.any(Path.class))).thenThrow(IOException.class);
libvirtComputingResourceSpy.manuallyDeleteUnusedSnapshotFile(false, "");
}
}
@Test
public void manuallyDeleteUnusedSnapshotFileTestLibvirtSupportingFlagDeleteOnCommandVirshBlockcommitIsTrueReturn() {
libvirtComputingResourceSpy.manuallyDeleteUnusedSnapshotFile(true, "");
Mockito.verify(libvirtComputingResourceSpy, Mockito.never()).deleteIfExists("");
}
}

View File

@ -22,7 +22,6 @@ import com.cloud.exception.InternalErrorException;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef;
import com.cloud.hypervisor.kvm.resource.wrapper.LibvirtUtilitiesHelper;
import com.cloud.storage.template.TemplateConstants;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
@ -67,8 +66,8 @@ public class KVMStorageProcessorTest {
@Mock
KVMStoragePoolManager storagePoolManager;
@Mock
LibvirtComputingResource resource;
LibvirtComputingResource resource = Mockito.mock(LibvirtComputingResource.class);
@InjectMocks
private KVMStorageProcessor storageProcessor;
@ -161,16 +160,6 @@ public class KVMStorageProcessorTest {
}
}
@Test
public void validateGetSnapshotTemporaryPath(){
String path = "/path/to/disk";
String snapshotName = "snapshot";
String expectedResult = "/path/to/snapshot";
String result = storageProcessor.getSnapshotTemporaryPath(path, snapshotName);
Assert.assertEquals(expectedResult, result);
}
@Test
public void validateGetSnapshotPathInPrimaryStorage(){
String path = "/path/to/disk";
@ -245,6 +234,7 @@ public class KVMStorageProcessorTest {
public void validateTakeVolumeSnapshotFailToCreateSnapshotThrowLibvirtException() throws LibvirtException{
Mockito.doReturn(diskToSnapshotAndDisksToAvoidMock).when(storageProcessorSpy).getDiskToSnapshotAndDisksToAvoid(Mockito.any(), Mockito.anyString(), Mockito.any());
Mockito.doReturn(new HashSet<>()).when(diskToSnapshotAndDisksToAvoidMock).second();
Mockito.doReturn("").when(resource).getSnapshotTemporaryPath(Mockito.anyString(), Mockito.anyString());
Mockito.doThrow(LibvirtException.class).when(domainMock).snapshotCreateXML(Mockito.anyString(), Mockito.anyInt());
storageProcessorSpy.takeVolumeSnapshot(new ArrayList<>(), "", "", domainMock);
@ -257,6 +247,7 @@ public class KVMStorageProcessorTest {
Mockito.doReturn(diskToSnapshotAndDisksToAvoidMock).when(storageProcessorSpy).getDiskToSnapshotAndDisksToAvoid(Mockito.any(), Mockito.anyString(), Mockito.any());
Mockito.doReturn(expectedResult).when(diskToSnapshotAndDisksToAvoidMock).first();
Mockito.doReturn(new HashSet<>()).when(diskToSnapshotAndDisksToAvoidMock).second();
Mockito.doReturn("").when(resource).getSnapshotTemporaryPath(Mockito.anyString(), Mockito.anyString());
Mockito.doReturn(null).when(domainMock).snapshotCreateXML(Mockito.anyString(), Mockito.anyInt());
String result = storageProcessorSpy.takeVolumeSnapshot(new ArrayList<>(), "", "", domainMock);
@ -313,46 +304,6 @@ public class KVMStorageProcessorTest {
}
}
@Test (expected = CloudRuntimeException.class)
public void validateMergeSnapshotIntoBaseFileErrorOnMergeThrowCloudRuntimeException() throws Exception {
try (MockedStatic<Script> ignored = Mockito.mockStatic(
Script.class); MockedStatic<LibvirtUtilitiesHelper> ignored2 = Mockito.mockStatic(
LibvirtUtilitiesHelper.class)) {
Mockito.when(Script.runSimpleBashScript(Mockito.anyString())).thenReturn("");
Mockito.when(LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any()))
.thenReturn(true);
storageProcessorSpy.mergeSnapshotIntoBaseFile(domainMock, "", "", "", volumeObjectToMock, connectMock);
}
}
@Test
public void validateMergeSnapshotIntoBaseFileMergeSuccessDoNothing() throws Exception {
try (MockedStatic<Script> scriptMockedStatic = Mockito.mockStatic(
Script.class); MockedStatic<LibvirtUtilitiesHelper> libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(
LibvirtUtilitiesHelper.class)) {
Mockito.when(Script.runSimpleBashScript(Mockito.anyString())).thenReturn(null);
Mockito.when(LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any()))
.thenReturn(true);
Mockito.doNothing().when(storageProcessorSpy).manuallyDeleteUnusedSnapshotFile(Mockito.anyBoolean(),
Mockito.any());
storageProcessorSpy.mergeSnapshotIntoBaseFile(domainMock, "", "", "", volumeObjectToMock, connectMock);
libvirtUtilitiesHelperMockedStatic.verify(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any()), Mockito.times(1));
scriptMockedStatic.verify(() -> Script.runSimpleBashScript(Mockito.anyString()), Mockito.times(1));
}
}
@Test (expected = CloudRuntimeException.class)
public void validateManuallyDeleteUnusedSnapshotFileLibvirtDoesNotSupportsFlagDeleteExceptionOnFileDeletionThrowsException() throws IOException {
try (MockedStatic<Files> ignored = Mockito.mockStatic(Files.class)) {
Mockito.when(Files.deleteIfExists(Mockito.any(Path.class))).thenThrow(IOException.class);
storageProcessorSpy.manuallyDeleteUnusedSnapshotFile(false, "");
}
}
@Test
public void validateIsAvailablePoolSizeDividedByDiskSizeLesserThanMinRate(){
Assert.assertTrue(storageProcessorSpy.isAvailablePoolSizeDividedByDiskSizeLesserThanMinRate(10499l, 10000l));

View File

@ -35,11 +35,16 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import com.cloud.projects.Project;
import com.cloud.projects.ProjectManager;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd;
import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd;
import org.apache.cloudstack.api.command.user.volume.ChangeOfferingForVolumeCmd;
import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd;
@ -159,8 +164,6 @@ import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao;
import com.cloud.offering.DiskOffering;
import com.cloud.org.Cluster;
import com.cloud.org.Grouping;
import com.cloud.projects.Project;
import com.cloud.projects.ProjectManager;
import com.cloud.resource.ResourceManager;
import com.cloud.resource.ResourceState;
import com.cloud.serializer.GsonHelper;
@ -362,6 +365,11 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
@Inject
EndPointSelector _epSelector;
@Inject
private VMSnapshotDetailsDao vmSnapshotDetailsDao;
public static final String KVM_FILE_BASED_STORAGE_SNAPSHOT = "kvmFileBasedStorageSnapshot";
protected Gson _gson;
private static final List<HypervisorType> SupportedHypervisorsForVolResize = Arrays.asList(HypervisorType.KVM, HypervisorType.XenServer,
@ -1402,6 +1410,29 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
shrinkOk);
}
protected void validateNoVmSnapshots(VolumeVO volume) {
if (volume.getInstanceId() != null) {
if (vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(volume.getInstanceId())) {
throw new InvalidParameterValueException("The volume is attached to a VM with memory&disk VM snapshots; therefore, it cannot be resized.");
}
}
}
protected boolean vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(long instanceId) {
for (VMSnapshotVO vmSnapshotVO : _vmSnapshotDao.findByVm(instanceId)) {
if (VMSnapshot.Type.DiskAndMemory.equals(vmSnapshotVO.getType())) {
return true;
}
List<VMSnapshotDetailsVO> vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId());
if (vmSnapshotDetails.stream().
noneMatch(vmSnapshotDetailsVO -> KVM_FILE_BASED_STORAGE_SNAPSHOT.equals(vmSnapshotDetailsVO.getName()))) {
return true;
}
}
return false;
}
/**
* A volume should not be resized if it covers ALL the following scenarios: <br>
* 1 - Root volume <br>
@ -2378,12 +2409,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
// if the caller is looking to change the size of the volume
if (newSize != null && currentSize != newSize) {
if (volume.getInstanceId() != null) {
// Check that VM to which this volume is attached does not have VM snapshots
if (_vmSnapshotDao.findByVm(volume.getInstanceId()).size() > 0) {
throw new InvalidParameterValueException("A volume that is attached to a VM with any VM snapshots cannot be resized.");
}
}
validateNoVmSnapshots(volume);
if (!validateVolumeSizeInBytes(newSize)) {
throw new InvalidParameterValueException("Requested size out of range");
@ -4009,7 +4035,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
}
@Override
public Snapshot allocSnapshotForVm(Long vmId, Long volumeId, String snapshotName) throws ResourceAllocationException {
public Snapshot allocSnapshotForVm(Long vmId, Long volumeId, String snapshotName, Long vmSnapshotId) throws ResourceAllocationException {
Account caller = CallContext.current().getCallingAccount();
VMInstanceVO vm = _vmInstanceDao.findById(vmId);
if (vm == null) {
@ -4061,6 +4087,10 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
throw new InvalidParameterValueException("Cannot perform this operation, unsupported on storage pool type " + storagePool.getPoolType());
}
if (vmSnapshotDetailsDao.listDetails(vmSnapshotId).stream().anyMatch(vmSnapshotDetailsVO -> KVM_FILE_BASED_STORAGE_SNAPSHOT.equals(vmSnapshotDetailsVO.getName()))) {
throw new InvalidParameterValueException("Cannot perform this operation, unsupported VM snapshot type.");
}
return snapshotMgr.allocSnapshot(volumeId, Snapshot.MANUAL_POLICY_ID, snapshotName, null, true, null);
}

View File

@ -1571,6 +1571,8 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
SnapshotStrategy snapshotStrategy = _storageStrategyFactory.getSnapshotStrategy(snapshot, SnapshotOperation.TAKE);
if (snapshotStrategy == null) {
_snapshotDao.remove(snapshotId);
logger.debug("No strategy found for creation of snapshot [{}], removing its record from the database.", snapshot);
throw new CloudRuntimeException(String.format("Can't find snapshot strategy to deal with snapshot:%s", snapshot.getSnapshotVO()));
}

View File

@ -27,7 +27,6 @@ import java.util.Map;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.storage.snapshot.SnapshotManager;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants;
@ -379,14 +378,8 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme
//Other Storage volume plugins could integrate this with their own functionality for group snapshots
VMSnapshotStrategy snapshotStrategy = storageStrategyFactory.getVmSnapshotStrategy(userVmVo.getId(), rootVolumePool.getId(), snapshotMemory);
if (snapshotStrategy == null) {
String message;
if (!SnapshotManager.VmStorageSnapshotKvm.value() && !snapshotMemory) {
message = "Creating a snapshot of a running KVM instance without memory is not supported";
} else {
message = "KVM does not support the type of snapshot requested";
}
logger.debug(message);
String message = String.format("No strategy was able to handle requested snapshot for VM [%s].", userVmVo.getUuid());
logger.error(message);
throw new CloudRuntimeException(message);
}

View File

@ -41,6 +41,14 @@ import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.host.HostVO;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd;
@ -99,12 +107,9 @@ import com.cloud.dc.HostPodVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.HostPodDao;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.org.Grouping;
@ -113,8 +118,6 @@ import com.cloud.projects.ProjectManager;
import com.cloud.serializer.GsonHelper;
import com.cloud.server.ManagementService;
import com.cloud.server.TaggedResourceService;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.Storage.ProvisioningType;
import com.cloud.storage.Volume.Type;
import com.cloud.storage.dao.DiskOfferingDao;
@ -268,6 +271,9 @@ public class VolumeApiServiceImplTest {
@Mock
private ManagementService managementService;
@Mock
private VMSnapshotDetailsDao vmSnapshotDetailsDaoMock;
private long accountMockId = 456l;
private long volumeMockId = 12313l;
private long vmInstanceMockId = 1123l;
@ -2202,4 +2208,85 @@ public class VolumeApiServiceImplTest {
Assert.fail();
}
}
@Test
public void validateNoVmSnapshotsTestNoInstanceId() {
Mockito.doReturn(null).when(volumeVoMock).getInstanceId();
volumeApiServiceImpl.validateNoVmSnapshots(volumeVoMock);
Mockito.verify(volumeApiServiceImpl, Mockito.never()).vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(Mockito.anyLong());
}
@Test
public void validateNoVmSnapshotsTestVmHasVmSnapshotsExceptKvmDiskOnlySnapshotsIsFalse() {
Mockito.doReturn(1L).when(volumeVoMock).getInstanceId();
Mockito.doReturn(false).when(volumeApiServiceImpl).vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(1L);
volumeApiServiceImpl.validateNoVmSnapshots(volumeVoMock);
}
@Test (expected = InvalidParameterValueException.class)
public void validateNoVmSnapshotsTestVmHasVmSnapshotsExceptKvmDiskOnlySnapshotsIsTrue() {
Mockito.doReturn(1L).when(volumeVoMock).getInstanceId();
Mockito.doReturn(true).when(volumeApiServiceImpl).vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(1L);
volumeApiServiceImpl.validateNoVmSnapshots(volumeVoMock);
}
@Test
public void vmHasVmSnapshotsExceptKvmDiskOnlySnapshotsTestMultipleDiskAndMemory () {
List<VMSnapshotVO> snapList = generateVmSnapshotVoList(VMSnapshot.Type.DiskAndMemory, VMSnapshot.Type.DiskAndMemory);
Mockito.doReturn(snapList).when(_vmSnapshotDao).findByVm(0L);
Assert.assertTrue(volumeApiServiceImpl.vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(0L));
}
@Test
public void vmHasVmSnapshotsExceptKvmDiskOnlySnapshotsTestOnlyOneDiskAndMemory () {
List<VMSnapshotVO> snapList = generateVmSnapshotVoList(VMSnapshot.Type.Disk, VMSnapshot.Type.DiskAndMemory);
Mockito.doReturn(snapList).when(_vmSnapshotDao).findByVm(0L);
VMSnapshotDetailsVO snapDetail = new VMSnapshotDetailsVO(0L, VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT, "0", true);
Mockito.doReturn(List.of(snapDetail)).when(vmSnapshotDetailsDaoMock).listDetails(Mockito.anyLong());
Assert.assertTrue(volumeApiServiceImpl.vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(0L));
}
@Test
public void vmHasVmSnapshotsExceptKvmDiskOnlySnapshotsTestDiskSnapshotsButNotKvmFileBasedSnapshots () {
List<VMSnapshotVO> snapList = generateVmSnapshotVoList(VMSnapshot.Type.Disk, VMSnapshot.Type.Disk);
Mockito.doReturn(snapList).when(_vmSnapshotDao).findByVm(0L);
Assert.assertTrue(volumeApiServiceImpl.vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(0L));
}
@Test
public void vmHasVmSnapshotsExceptKvmDiskOnlySnapshotsTestOnlyOneKvmFileBasedSnapshot () {
List<VMSnapshotVO> snapList = generateVmSnapshotVoList(VMSnapshot.Type.Disk, VMSnapshot.Type.Disk);
Mockito.doReturn(snapList).when(_vmSnapshotDao).findByVm(0L);
VMSnapshotDetailsVO snapDetail = new VMSnapshotDetailsVO(0L, VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT, "0", true);
Mockito.doReturn(List.of(snapDetail)).when(vmSnapshotDetailsDaoMock).listDetails(0);
Assert.assertTrue(volumeApiServiceImpl.vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(0L));
}
@Test
public void vmHasVmSnapshotsExceptKvmDiskOnlySnapshotsTestAllKvmFileBasedSnapshots () {
List<VMSnapshotVO> snapList = generateVmSnapshotVoList(VMSnapshot.Type.Disk, VMSnapshot.Type.Disk);
Mockito.doReturn(snapList).when(_vmSnapshotDao).findByVm(0L);
VMSnapshotDetailsVO snapDetail = new VMSnapshotDetailsVO(0L, VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT, "0", true);
Mockito.doReturn(List.of(snapDetail)).when(vmSnapshotDetailsDaoMock).listDetails(0);
Mockito.doReturn(List.of(snapDetail)).when(vmSnapshotDetailsDaoMock).listDetails(1);
Assert.assertFalse(volumeApiServiceImpl.vmHasVmSnapshotsExceptKvmDiskOnlySnapshots(0L));
}
private List<VMSnapshotVO> generateVmSnapshotVoList(VMSnapshot.Type t1, VMSnapshot.Type t2) {
VMSnapshotVO mock1 = Mockito.mock(VMSnapshotVO.class);
Mockito.doReturn(t1).when(mock1).getType();
Mockito.doReturn(0L).when(mock1).getId();
VMSnapshotVO mock2 = Mockito.mock(VMSnapshotVO.class);
Mockito.doReturn(t2).when(mock2).getType();
Mockito.doReturn(1L).when(mock2).getId();
return List.of(mock1, mock2);
}
}

View File

@ -201,8 +201,8 @@ export default {
},
show: (record) => {
return (((['Running'].includes(record.state) && record.hypervisor !== 'LXC') ||
(['Stopped'].includes(record.state) && ((record.hypervisor !== 'KVM' && record.hypervisor !== 'LXC') ||
(record.hypervisor === 'KVM' && record.pooltype === 'PowerFlex')))) && record.vmtype !== 'sharedfsvm')
(['Stopped'].includes(record.state) && (!['KVM', 'LXC'].includes(record.hypervisor) ||
(record.hypervisor === 'KVM' && ['PowerFlex', 'Filesystem', 'NetworkFilesystem', 'SharedMountPoint'].includes(record.pooltype))))) && record.vmtype !== 'sharedfsvm')
},
disabled: (record) => { return record.hostcontrolstate === 'Offline' && record.hypervisor === 'KVM' },
mapping: {