diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java index 695376216732..75cb09fbb709 100644 --- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java +++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java @@ -155,6 +155,14 @@ public class AgentProperties{ */ public static final Property 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.
+ * This configuration is only considered if libvirt.events.enabled is also true.
+ * Data type: Integer.
+ * Default value: 259200 + */ + public static final Property SNAPSHOT_MERGE_TIMEOUT = new Property<>("snapshot.merge.timeout", 60 * 60 * 72); + /** * This parameter sets the VM migration speed (in mbps). The default value is -1,
* which means that the agent will try to guess the speed of the guest network and consume all possible bandwidth.
diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index b7b5423244cf..98c9cf037224 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -137,7 +137,7 @@ Volume updateVolume(long volumeId, String path, String state, Long storageId, 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. diff --git a/api/src/main/java/com/cloud/vm/snapshot/VMSnapshot.java b/api/src/main/java/com/cloud/vm/snapshot/VMSnapshot.java index 3897df2d5e67..24e93af15621 100644 --- a/api/src/main/java/com/cloud/vm/snapshot/VMSnapshot.java +++ b/api/src/main/java/com/cloud/vm/snapshot/VMSnapshot.java @@ -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 String getDescription() { 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 @@ enum Type { } enum Event { - CreateRequested, OperationFailed, OperationSucceeded, RevertRequested, ExpungeRequested, + CreateRequested, OperationFailed, OperationSucceeded, RevertRequested, ExpungeRequested, Hide, } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java index cdd908dfb87d..75089cece8e1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java @@ -153,7 +153,7 @@ public ApiCommandResourceType getApiResourceType() { @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()); diff --git a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java new file mode 100644 index 000000000000..4d61249c7cbc --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java @@ -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> mapVolumeToSnapshotSizeAndNewVolumePath; + + public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map> mapVolumeToSnapshotSizeAndNewVolumePath) { + super(command, success, details); + this.mapVolumeToSnapshotSizeAndNewVolumePath = mapVolumeToSnapshotSizeAndNewVolumePath; + } + + public Map> getMapVolumeToSnapshotSizeAndNewVolumePath() { + return mapVolumeToSnapshotSizeAndNewVolumePath; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java new file mode 100644 index 000000000000..952bf0c971de --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java @@ -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 volumeTOs, String guestOSType, VirtualMachine.State vmState) { + super(vmName, snapshot, volumeTOs, guestOSType); + this.vmState = vmState; + } + + public VirtualMachine.State getVmState() { + return vmState; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/DeleteDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/DeleteDiskOnlyVmSnapshotCommand.java new file mode 100644 index 000000000000..368a506f2127 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/DeleteDiskOnlyVmSnapshotCommand.java @@ -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 snapshots; + + public DeleteDiskOnlyVmSnapshotCommand(List snapshots) { + this.snapshots = snapshots; + } + + public List getSnapshots() { + return snapshots; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java new file mode 100644 index 000000000000..463189b8b0b8 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java @@ -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 snapshotMergeTreeToList; + private VirtualMachine.State vmState; + private String vmName; + + public MergeDiskOnlyVmSnapshotCommand(List snapshotMergeTreeToList, VirtualMachine.State vmState, String vmName) { + this.snapshotMergeTreeToList = snapshotMergeTreeToList; + this.vmState = vmState; + this.vmName = vmName; + } + + public List getSnapshotMergeTreeToList() { + return snapshotMergeTreeToList; + } + + public VirtualMachine.State getVmState() { + return vmState; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotAnswer.java new file mode 100644 index 000000000000..2ecf587d59d1 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotAnswer.java @@ -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 volumeObjectTos; + + public RevertDiskOnlyVmSnapshotAnswer(Command cmd, List volumeObjectTos) { + super(cmd, true, null); + this.volumeObjectTos = volumeObjectTos; + } + + public List getVolumeObjectTos() { + return volumeObjectTos; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotCommand.java new file mode 100644 index 000000000000..6effca7dfa07 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotCommand.java @@ -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 snapshotObjectTos; + private String vmName; + + public RevertDiskOnlyVmSnapshotCommand(List snapshotObjectTos, String vmName) { + super(); + this.snapshotObjectTos = snapshotObjectTos; + this.vmName = vmName; + } + + public List getSnapshotObjectTos() { + return snapshotObjectTos; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java b/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java new file mode 100644 index 000000000000..78f23105e192 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java @@ -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 grandChildren; + + public SnapshotMergeTreeTO(DataTO parent, DataTO child, List grandChildren) { + this.parent = parent; + this.child = child; + this.grandChildren = grandChildren; + } + + public DataTO getParent() { + return parent; + } + + public DataTO getChild() { + return child; + } + + public List getGrandChildren() { + return grandChildren; + } + + public void addGrandChild(DataTO grandChild) { + grandChildren.add(grandChild); + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java index 171634fb1044..3cda7d42760e 100755 --- a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java @@ -58,4 +58,6 @@ public interface SnapshotDao extends GenericDao, StateDao listByIds(Object... ids); List searchByVolumes(List volumeIds); + + List listByVolumeIdAndTypeNotInAndStateNotRemoved(long volumeId, Type... type); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java index f5fc9c47d036..c479a386d794 100755 --- a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java @@ -19,6 +19,7 @@ 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 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 snapshotIdsSearch; private SearchBuilder VolumeIdSearch; private SearchBuilder VolumeIdTypeSearch; @@ -66,6 +71,8 @@ public class SnapshotDaoImpl extends GenericDaoBase implements private SearchBuilder StatusSearch; private SearchBuilder notInStatusSearch; private GenericSearchBuilder CountSnapshotsByAccount; + + private SearchBuilder volumeIdAndTypeNotInSearch; @Inject ResourceTagDao _tagsDao; @Inject @@ -181,6 +188,12 @@ protected void init() { 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 List searchByVolumes(List volumeIds) { sc.setParameters("volumeIds", volumeIds.toArray()); return search(sc, null); } + + @Override + public List listByVolumeIdAndTypeNotInAndStateNotRemoved(long volumeId, Type... types) { + SearchCriteria 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); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java index 0143aaa1e735..4045af58d4bc 100644 --- a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java @@ -27,6 +27,8 @@ public interface VMSnapshotDao extends GenericDao, StateDao< List findByVm(Long vmId); + List findByVmAndByType(Long vmId, VMSnapshot.Type type); + List listExpungingSnapshot(); List listByInstanceId(Long vmId, VMSnapshot.State... status); @@ -35,6 +37,8 @@ public interface VMSnapshotDao extends GenericDao, StateDao< List listByParent(Long vmSnapshotId); + List listByParentAndStateIn(Long vmSnapshotId, VMSnapshot.State... states); + VMSnapshotVO findByName(Long vmId, String name); List listByAccountId(Long accountId); diff --git a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java index 03a978f85469..83411b3cf8f7 100644 --- a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java @@ -42,6 +42,12 @@ public class VMSnapshotDaoImpl extends GenericDaoBase implem private final SearchBuilder SnapshotStatusSearch; private final SearchBuilder AllFieldsSearch; + private SearchBuilder 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 @@ protected VMSnapshotDaoImpl() { 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 List findByVm(Long vmId) { return listBy(sc, null); } + @Override + public List findByVmAndByType(Long vmId, VMSnapshot.Type type) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("vm_id", vmId); + sc.setParameters("vm_snapshot_type", type); + return listBy(sc, null); + } + @Override public List listExpungingSnapshot() { SearchCriteria sc = ExpungingSnapshotSearch.create(); @@ -111,6 +130,14 @@ public List listByParent(Long vmSnapshotId) { return listBy(sc, null); } + @Override + public List listByParentAndStateIn(Long vmSnapshotId, State... states) { + SearchCriteria 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 sc = AllFieldsSearch.create(); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java index f0072a414fb4..db4c64bd0ab8 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java @@ -67,6 +67,8 @@ public interface SnapshotDataStoreDao extends GenericDao findBySnapshotIdAndNotInDestroyedHiddenState(long snapshotId); + SnapshotDataStoreVO findBySnapshotIdInAnyState(long snapshotId, DataStoreRole role); + List listDestroyed(long storeId); List findBySnapshotId(long snapshotId); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java index 2b064be6b60a..b5faa6caedf1 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java @@ -417,6 +417,12 @@ public SnapshotDataStoreVO findBySourceSnapshot(long snapshotId, DataStoreRole r return findOneBy(sc); } + @Override + public SnapshotDataStoreVO findBySnapshotIdInAnyState(long snapshotId, DataStoreRole role) { + SearchCriteria sc = createSearchCriteriaBySnapshotIdAndStoreRole(snapshotId, role); + return findOneBy(sc); + } + @Override public List listAllByVolumeAndDataStore(long volumeId, DataStoreRole role) { SearchCriteria sc = searchFilteringStoreIdEqStateEqStoreRoleEqIdEqUpdateCountEqSnapshotIdEqVolumeIdEq.create(); diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java index 0801731630b6..8db067131bf7 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java @@ -23,7 +23,15 @@ 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 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 void doInTransactionWithoutResult(TransactionStatus status) { @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 StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperat 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 vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId()); + if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> vmSnapshotDetailsVO.getName().equals(VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT))) { + 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; diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java index 09f569e6f193..8ec3d7843e7a 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java @@ -25,6 +25,9 @@ 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 params) throws ConfigurationException { String value = configurationDao.getValue("vmsnapshot.create.wait"); @@ -469,16 +479,39 @@ public boolean deleteVMSnapshotFromDB(VMSnapshot vmSnapshot, boolean unmanage) { @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 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 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 volumes = volumeDao.findByInstance(vmId); for (VolumeVO volume : volumes) { if (volume.getFormat() != ImageFormat.QCOW2) { + logger.debug("Default VM snapshot 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 vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId()); + if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> vmSnapshotDetailsVO.getName().equals(KVM_FILE_BASED_STORAGE_SNAPSHOT))) { + return true; + } + } + + return false; + } } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java new file mode 100644 index 000000000000..21f73f69479e --- /dev/null +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java @@ -0,0 +1,684 @@ +/* + * 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 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 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 volumeTOs = vmSnapshotHelper.getVolumeTOList(vmSnapshotBeingDeleted.getVmId()); + List snapshotChildren = vmSnapshotDao.listByParentAndStateIn(vmSnapshotBeingDeleted.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden); + + long realSize = getVMSnapshotRealSize(vmSnapshotBeingDeleted); + int numberOfChildren = snapshotChildren.size(); + + List 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 (!userVm.getState().equals(VirtualMachine.State.Stopped)) { + throw new CloudRuntimeException("VM must be stopped to revert disk-only VM snapshot."); + } + + VMSnapshotVO vmSnapshotBeingReverted = (VMSnapshotVO) vmSnapshot; + + transitStateWithoutThrow(vmSnapshotBeingReverted, VMSnapshot.Event.RevertRequested); + + Long hostId = vmSnapshotHelper.pickRunningHost(vmSnapshotBeingReverted.getVmId()); + List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotBeingReverted); + List 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 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; + } + + resourceLimitManager.decrementResourceCount(volumeVO.getAccountId(), Resource.ResourceType.primary_storage, volumeVO.getSize() - snapshotRef.getSize()); + 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 volumeTOs) { + if (oldParent == null || oldParent.getRemoved() != null || !VMSnapshot.State.Hidden.equals(oldParent.getState())) { + return; + } + + List snapshotVos; + + if (oldParent.getCurrent()) { + snapshotVos = mergeCurrentDeltaOnSnapshot(oldParent, userVm, hostId, volumeTOs); + } else { + List 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 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 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 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 deleteSnapshot(VMSnapshotVO vmSnapshotVO, Long hostId) { + List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVO); + List 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 snapshotVOList = new ArrayList<>(); + for (SnapshotDataStoreVO snapshotDataStoreVO : volumeSnapshots) { + snapshotDataStoreDao.remove(snapshotDataStoreVO.getId()); + snapshotVOList.add(snapshotDao.findById(snapshotDataStoreVO.getSnapshotId())); + } + return snapshotVOList; + } + + private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO childSnapshot, UserVmVO userVm, List volumeObjectTOS, Long hostId) { + logger.debug("Merging VM snapshot [{}] with its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid()); + + List snapshotGrandChildren = vmSnapshotDao.listByParentAndStateIn(childSnapshot.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden); + + if (userVm.getState().equals(VirtualMachine.State.Running) && !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 snapshotMergeTreeToList = generateSnapshotMergeTrees(vmSnapshotVO, childSnapshot, snapshotGrandChildren); + + if (childSnapshot.getCurrent() && !userVm.getState().equals(VirtualMachine.State.Running)) { + 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 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 mergeCurrentDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, UserVmVO userVmVO, Long hostId, List volumeObjectTOS) { + logger.debug("Merging VM snapshot [{}] with the current volume delta.", vmSnapshotVo.getUuid()); + List snapshotMergeTreeTOList = new ArrayList<>(); + List 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 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 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 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 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> volumeUuidToSnapshotSizeAndNewVolumePathMap = answer.getMapVolumeToSnapshotSizeAndNewVolumePath(); + long vmSnapshotSize = 0; + + for (VolumeInfo volumeInfo : volumeInfoToSnapshotObjectMap.keySet()) { + VolumeVO volumeVO = (VolumeVO) volumeInfo.getVolume(); + Pair 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 volumeInfoToSnapshotObjectMap, List 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 generateSnapshotMergeTrees(VMSnapshotVO parent, VMSnapshotVO child, List grandChildren) throws NoSuchElementException { + logger.debug("Generating list of Snapshot Merge Trees for the merge process of VM Snapshot [{}].", parent.getUuid()); + + List snapshotMergeTrees = new ArrayList<>(); + List parentVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(parent); + List childVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(child); + List 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 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 getVolumeSnapshotsAssociatedWithVmSnapshot(VMSnapshotVO vmSnapshot) { + List associatedVolumeSnapshots = new ArrayList<>(); + List 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 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 virDomainBlockCommit}, 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 vmSnapshotList, UserVm userVm) { + VMSnapshotVO currentSnapshotVO = vmSnapshotDao.findCurrentSnapshotByVmId(vmSnapshotList.get(0).getVmId()); + VMSnapshotTO currentSnapshotTO = vmSnapshotHelper.getSnapshotWithParents(currentSnapshotVO); + + List 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); + } + } +} diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java index e33edc9ce801..70ac14ec79d8 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java @@ -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 params) throws ConfigurationException { return super.configure(name, params); @@ -356,12 +354,25 @@ public StrategyPriority canHandle(VMSnapshot vmSnapshot) { @Override public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) { + 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) { - UserVmVO vm = userVmDao.findById(vmId); - if (vm.getState() == VirtualMachine.State.Running) { - return StrategyPriority.HYPERVISOR; - } + 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; } diff --git a/engine/storage/snapshot/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-snapshot-storage-context.xml b/engine/storage/snapshot/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-snapshot-storage-context.xml index fc561159c8ea..32ac6e9fc8b7 100644 --- a/engine/storage/snapshot/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-snapshot-storage-context.xml +++ b/engine/storage/snapshot/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-snapshot-storage-context.xml @@ -48,4 +48,7 @@ + + diff --git a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java index 7bcfd4dda581..829ca5ade39b 100644 --- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java +++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java @@ -25,6 +25,7 @@ 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 VMSnapshotDao vmSnapshotDao() { return Mockito.mock(VMSnapshotDao.class); } + @Bean + public VMSnapshotDetailsDao vmSnapshotDetailsDao() { + return Mockito.mock(VMSnapshotDetailsDao.class); + } + @Bean public ConfigurationDao configurationDao() { return Mockito.mock(ConfigurationDao.class); diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index 1e3b5aaa8e5e..bc90753fe632 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -27,6 +27,7 @@ import com.cloud.storage.ScopeType; 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; @@ -35,6 +36,12 @@ 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; @@ -87,6 +94,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) { @@ -400,6 +413,14 @@ public Backup createNewBackupEntryForRestorePoint(Backup.RestorePoint restorePoi @Override public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backupOffering) { + for (VMSnapshotVO vmSnapshotVO : vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) { + List vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId()); + if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> vmSnapshotDetailsVO.getName().equals(VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT))) { + 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()); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java new file mode 100644 index 000000000000..d360aa481372 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java @@ -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(); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index a632fd5adfd2..0bc4276ab946 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -29,6 +29,7 @@ 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; @@ -44,6 +45,8 @@ 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; @@ -100,10 +103,12 @@ 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; @@ -368,6 +373,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 snapshotMergeTimeout; + private String modifyVlanPath; private String versionStringPath; private String patchScriptPath; @@ -559,6 +566,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.

+ * 1st parameter: VM's name;
+ * 2nd parameter: disk's label (target.dev tag from VM's XML);
+ * 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; } @@ -1177,6 +1192,8 @@ public boolean configure(final String name, final Map params) th cmdsTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.CMDS_TIMEOUT) * 1000; noMemBalloon = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VM_MEMBALLOON_DISABLE); + snapshotMergeTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.SNAPSHOT_MERGE_TIMEOUT); + snapshotMergeTimeout = snapshotMergeTimeout > 0 ? snapshotMergeTimeout : AgentProperties.SNAPSHOT_MERGE_TIMEOUT.getDefaultValue(); manualCpuSpeed = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_CPU_MANUAL_SPEED_MHZ); @@ -4636,6 +4653,13 @@ public List getVmDiskStat(final Connect conn, final String vmN } } + public DiskDef getDiskWithPathOfVolumeObjectTO(List 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) { @@ -5819,10 +5843,229 @@ public void cleanOldSecretsByDiskDef(Connect conn, List disks) throws L } } + /** + * 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(snapshotMergeTimeout, 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 = snapshotMergeTimeout; + 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, + snapshotMergeTimeout, lastCommittedBytes, endBytes); + } + + /** + * Manually deletes the unused snapshot file.
+ * This method is necessary due to Libvirt created the tag '--delete' on command 'virsh blockcommit' on version 1.2.9, however it was only implemented on version + * 6.0.0. + * @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 details, Integer cpus, InterfaceDef interfaceDef) { String nicMultiqueueNumber = details.get(VmDetailConstants.NIC_MULTIQUEUE_NUMBER); if (nicMultiqueueNumber != null) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java new file mode 100644 index 000000000000..84d17a1a1161 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java @@ -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 { + + private static final String SNAPSHOT_XML = "\n" + + "%s\n" + + "\n" + + " \n" + + "%s" + + " \n" + + ""; + + private static final String TAG_DISK_SNAPSHOT = "\n" + + "\n" + + "\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 volumeObjectTOS = cmd.getVolumeTOs(); + List 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>> 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> mapVolumeToSnapshotSizeAndNewVolumePath = new HashMap<>(); + + List 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 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>> createSnapshotXmlAndNewVolumePathMap(List volumeObjectTOS, List disks, VMSnapshotTO target, LibvirtComputingResource resource) { + StringBuilder stringBuilder = new StringBuilder(); + Map> 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(); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteDiskOnlyVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteDiskOnlyVMSnapshotCommandWrapper.java new file mode 100644 index 000000000000..15df8627a8a7 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteDiskOnlyVMSnapshotCommandWrapper.java @@ -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 { + + @Override + public Answer execute(DeleteDiskOnlyVmSnapshotCommand command, LibvirtComputingResource resource) { + List 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); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMergeDiskOnlyVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMergeDiskOnlyVMSnapshotCommandWrapper.java new file mode 100644 index 000000000000..ad0678080ed7 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMergeDiskOnlyVMSnapshotCommandWrapper.java @@ -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 { + + @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 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 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 snapshotMergeTreeTOList = cmd.getSnapshotMergeTreeToList(); + + LibvirtUtilitiesHelper libvirtUtilitiesHelper = resource.getLibvirtUtilitiesHelper(); + Connect conn = libvirtUtilitiesHelper.getConnection(); + Domain domain = resource.getDomain(conn, vmName); + List 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 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); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertDiskOnlyVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertDiskOnlyVMSnapshotCommandWrapper.java new file mode 100644 index 000000000000..1aa79d48eec2 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertDiskOnlyVMSnapshotCommandWrapper.java @@ -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 { + + @Override + public Answer execute(RevertDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) { + List 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 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 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); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUtilitiesHelper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUtilitiesHelper.java index a2d161ac94bf..70e7f074c879 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUtilitiesHelper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUtilitiesHelper.java @@ -22,6 +22,10 @@ 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 static boolean isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Con 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()); + } + } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index decbe9c013a2..0de4e11817e0 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -136,7 +136,6 @@ 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 Answer createVolume(final CreateObjectCommand cmd) { */ private static final String TAG_AVOID_DISK_FROM_SNAPSHOT = ""; - /** - * Virsh command to merge (blockcommit) snapshot into the base file.

- * 1st parameter: VM's name;
- * 2nd parameter: disk's label (target.dev tag from VM's XML);
- * 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 6.0.0; - */ - 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.

* Libvirt lib for java does not have the enum virDomainSnapshotCreateFlags. @@ -2238,7 +2228,7 @@ private SnapshotObjectTO takeFullVolumeSnapshotOfRunningVm(CreateObjectCommand c 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 @@ protected void validateConvertResult(String convertResult, String snapshotPath) 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.
- * This method is necessary due to Libvirt created the tag '--delete' on command 'virsh blockcommit' on version 1.2.9, however it was only implemented on version - * 6.0.0. - * @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 @@ protected String takeVolumeSnapshot(List disks, String snapshotName, St 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 @@ protected Pair> getDiskToSnapshotAndDisksToAvoid(List(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; diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java index 751a16a15541..bdfba5769b92 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java @@ -826,6 +826,46 @@ public void resize(final QemuImgFile file, final long size) throws QemuImgExcept 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 deleteFile + * If true, the commited file(s) will be deleted. + */ + public void commit( QemuImgFile file, QemuImgFile base, boolean deleteFile) throws QemuImgException { + if (file == null) { + throw new QemuImgException("File should not be null"); + } + + final Script s = new Script(_qemuImgPath, timeout); + s.add("commit"); + if (deleteFile) { + 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 diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImgFile.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImgFile.java index b461d967c893..1d00fd18e689 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImgFile.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImgFile.java @@ -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 PhysicalDiskFormat getFormat() { return this.format; } + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } + } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index 35c5a3bea930..e2aa48cf8fbb 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -41,6 +41,8 @@ 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.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.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.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 void recreateCheckpointsOnVmTestVolumesHaveCheckpoints() { 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.snapshotMergeTimeout = 10; + + try (MockedStatic libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); + MockedStatic 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 libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); + MockedStatic 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 libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + libvirtComputingResourceSpy.snapshotMergeTimeout = 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 libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + libvirtComputingResourceSpy.snapshotMergeTimeout = 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 libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + libvirtComputingResourceSpy.snapshotMergeTimeout = 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 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(""); + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java index f6f9b3224357..cc1e38a908b3 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java @@ -22,7 +22,6 @@ 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 void testIsEnoughSpaceForDownloadTemplateOnTemporaryLocationNotExistingLo } } - @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 void validateGetDiskToSnapshotAndDisksToAvoidPathFoundReturnLabels() thro 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 void validateTakeVolumeSnapshotSuccessReturnDiskLabel() throws LibvirtExc 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 void convertBaseFileToSnapshotFileInPrimaryStorageDirTestConvertSuccessRe } } - @Test (expected = CloudRuntimeException.class) - public void validateMergeSnapshotIntoBaseFileErrorOnMergeThrowCloudRuntimeException() throws Exception { - try (MockedStatic