diff --git a/api/src/com/cloud/storage/VolumeApiService.java b/api/src/com/cloud/storage/VolumeApiService.java index f562ce2c207..673fffca7cd 100644 --- a/api/src/com/cloud/storage/VolumeApiService.java +++ b/api/src/com/cloud/storage/VolumeApiService.java @@ -102,4 +102,6 @@ public interface VolumeApiService { boolean isDisplayResourceEnabled(Long id); void updateDisplay(Volume volume, Boolean displayVolume); + + Snapshot allocSnapshotForVm(Long vmId, Long volumeId, String snapshotName) throws ResourceAllocationException; } diff --git a/api/src/com/cloud/storage/snapshot/SnapshotApiService.java b/api/src/com/cloud/storage/snapshot/SnapshotApiService.java index 013704d2e06..eb1393543c0 100644 --- a/api/src/com/cloud/storage/snapshot/SnapshotApiService.java +++ b/api/src/com/cloud/storage/snapshot/SnapshotApiService.java @@ -108,5 +108,7 @@ public interface SnapshotApiService { Snapshot revertSnapshot(Long snapshotId); + Snapshot backupSnapshotFromVmSnapshot(Long snapshotId, Long vmId, Long volumeId, Long vmSnapshotId); + SnapshotPolicy updateSnapshotPolicy(UpdateSnapshotPolicyCmd updateSnapshotPolicyCmd); } diff --git a/api/src/com/cloud/vm/snapshot/VMSnapshotService.java b/api/src/com/cloud/vm/snapshot/VMSnapshotService.java index 12767b3db15..0d4d22c7cb1 100644 --- a/api/src/com/cloud/vm/snapshot/VMSnapshotService.java +++ b/api/src/com/cloud/vm/snapshot/VMSnapshotService.java @@ -36,7 +36,7 @@ public interface VMSnapshotService { VMSnapshot getVMSnapshotById(Long id); - VMSnapshot creatVMSnapshot(Long vmId, Long vmSnapshotId, Boolean quiescevm); + VMSnapshot createVMSnapshot(Long vmId, Long vmSnapshotId, Boolean quiescevm); VMSnapshot allocVMSnapshot(Long vmId, String vsDisplayName, String vsDescription, Boolean snapshotMemory) throws ResourceAllocationException; diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 00e9d388c28..708b0d1e622 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -240,6 +240,7 @@ public class ApiConstants { public static final String SIGNATURE = "signature"; public static final String SIGNATURE_VERSION = "signatureversion"; public static final String SIZE = "size"; + public static final String SNAPSHOT = "snapshot"; public static final String SNAPSHOT_ID = "snapshotid"; public static final String SNAPSHOT_POLICY_ID = "snapshotpolicyid"; public static final String SNAPSHOT_TYPE = "snapshottype"; diff --git a/api/src/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java b/api/src/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java index 45238894d38..e79feb75632 100644 --- a/api/src/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java +++ b/api/src/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java @@ -143,7 +143,7 @@ public class CreateSnapshotCmd extends BaseAsyncCreateCmd { } public static String getResultObjectName() { - return "snapshot"; + return ApiConstants.SNAPSHOT; } @Override diff --git a/api/src/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java b/api/src/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java new file mode 100644 index 00000000000..7a35d3462f5 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java @@ -0,0 +1,219 @@ +// 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.api.command.user.snapshot; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandJobType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SnapshotResponse; +import org.apache.cloudstack.api.response.VMSnapshotResponse; +import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.log4j.Logger; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.projects.Project; +import com.cloud.storage.Snapshot; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.vm.snapshot.VMSnapshot; + +@APICommand(name = "createSnapshotFromVMSnapshot", description = "Creates an instant snapshot of a volume from existing vm snapshot.", responseObject = SnapshotResponse.class, entityType = {Snapshot.class}, since = "4.10.0", + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class CreateSnapshotFromVMSnapshotCmd extends BaseAsyncCreateCmd { + public static final Logger s_logger = Logger.getLogger(CreateSnapshotFromVMSnapshotCmd.class.getName()); + private static final String s_name = "createsnapshotfromvmsnapshotresponse"; + + // /////////////////////////////////////////////////// + // ////////////// API parameters ///////////////////// + // /////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VOLUME_ID, type = CommandType.UUID, entityType = VolumeResponse.class, required = true, description = "The ID of the disk volume") + private Long volumeId; + + @Parameter(name=ApiConstants.VM_SNAPSHOT_ID, type=CommandType.UUID, entityType=VMSnapshotResponse.class, + required=true, description="The ID of the VM snapshot") + private Long vmSnapshotId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "the name of the snapshot") + private String snapshotName; + + private String syncObjectType = BaseAsyncCmd.snapshotHostSyncObject; + + // /////////////////////////////////////////////////// + // ///////////////// Accessors /////////////////////// + // /////////////////////////////////////////////////// + + public Long getVolumeId() { + return volumeId; + } + + public Long getVMSnapshotId() { + return vmSnapshotId; + } + + public String getSnapshotName() { + return snapshotName; + } + + private Long getVmId() { + VMSnapshot vmsnapshot = _entityMgr.findById(VMSnapshot.class, getVMSnapshotId()); + if (vmsnapshot == null) { + throw new InvalidParameterValueException("Unable to find vm snapshot by id=" + getVMSnapshotId()); + } + UserVm vm = _entityMgr.findById(UserVm.class, vmsnapshot.getVmId()); + if (vm == null) { + throw new InvalidParameterValueException("Unable to find vm by vm snapshot id=" + getVMSnapshotId()); + } + return vm.getId(); + } + private Long getHostId() { + VMSnapshot vmsnapshot = _entityMgr.findById(VMSnapshot.class, getVMSnapshotId()); + if (vmsnapshot == null) { + throw new InvalidParameterValueException("Unable to find vm snapshot by id=" + getVMSnapshotId()); + } + UserVm vm = _entityMgr.findById(UserVm.class, vmsnapshot.getVmId()); + if (vm != null) { + if(vm.getHostId() != null) { + return vm.getHostId(); + } else if(vm.getLastHostId() != null) { + return vm.getLastHostId(); + } + } + return null; + } + + + // /////////////////////////////////////////////////// + // ///////////// API Implementation/////////////////// + // /////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + public static String getResultObjectName() { + return ApiConstants.SNAPSHOT; + } + + @Override + public long getEntityOwnerId() { + + VMSnapshot vmsnapshot = _entityMgr.findById(VMSnapshot.class, getVMSnapshotId()); + if (vmsnapshot == null) { + throw new InvalidParameterValueException("Unable to find vmsnapshot by id=" + getVMSnapshotId()); + } + + Account account = _accountService.getAccount(vmsnapshot.getAccountId()); + //Can create templates for enabled projects/accounts only + if (account.getType() == Account.ACCOUNT_TYPE_PROJECT) { + Project project = _projectService.findByProjectAccountId(vmsnapshot.getAccountId()); + if (project == null) { + throw new InvalidParameterValueException("Unable to find project by account id=" + account.getUuid()); + } + if (project.getState() != Project.State.Active) { + throw new PermissionDeniedException("Can't add resources to the project id=" + project.getUuid() + " in state=" + project.getState() + " as it's no longer active"); + } + } else if (account.getState() == Account.State.disabled) { + throw new PermissionDeniedException("The owner of template is disabled: " + account); + } + + return vmsnapshot.getAccountId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_SNAPSHOT_CREATE; + } + + @Override + public String getEventDescription() { + return "creating snapshot from vm snapshot : " + getVMSnapshotId(); + } + + @Override + public ApiCommandJobType getInstanceType() { + return ApiCommandJobType.Snapshot; + } + + @Override + public void create() throws ResourceAllocationException { + Snapshot snapshot = this._volumeService.allocSnapshotForVm(getVmId(), getVolumeId(), getSnapshotName()); + if (snapshot != null) { + this.setEntityId(snapshot.getId()); + this.setEntityUuid(snapshot.getUuid()); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create snapshot from vm snapshot"); + } + } + + @Override + public void execute() { + s_logger.info("CreateSnapshotFromVMSnapshotCmd with vm snapshot id:" + getVMSnapshotId() + " and snapshot id:" + getEntityId() + " starts:" + System.currentTimeMillis()); + CallContext.current().setEventDetails("Vm Snapshot Id: "+ getVMSnapshotId()); + Snapshot snapshot = null; + try { + snapshot = _snapshotService.backupSnapshotFromVmSnapshot(getEntityId(), getVmId(), getVolumeId(), getVMSnapshotId()); + if (snapshot != null) { + SnapshotResponse response = _responseGenerator.createSnapshotResponse(snapshot); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create snapshot due to an internal error creating snapshot from vm snapshot " + getVMSnapshotId()); + } + } catch (InvalidParameterValueException ex) { + throw ex; + } catch (Exception e) { + s_logger.debug("Failed to create snapshot", e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create snapshot due to an internal error creating snapshot from vm snapshot " + getVMSnapshotId()); + } finally { + if (snapshot == null) { + try { + _snapshotService.deleteSnapshot(getEntityId()); + } catch (Exception e) { + s_logger.debug("Failed to clean failed snapshot" + getEntityId()); + } + } + } + } + + + @Override + public String getSyncObjType() { + if (getSyncObjId() != null) { + return syncObjectType; + } + return null; + } + + @Override + public Long getSyncObjId() { + if (getHostId() != null) { + return getHostId(); + } + return null; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java b/api/src/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java index f18793ac581..3e37bbe5e36 100644 --- a/api/src/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java +++ b/api/src/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java @@ -110,7 +110,7 @@ public class CreateVMSnapshotCmd extends BaseAsyncCreateCmd { @Override public void execute() { CallContext.current().setEventDetails("VM Id: " + getVmId()); - VMSnapshot result = _vmSnapshotService.creatVMSnapshot(getVmId(), getEntityId(), getQuiescevm()); + VMSnapshot result = _vmSnapshotService.createVMSnapshot(getVmId(), getEntityId(), getQuiescevm()); if (result != null) { VMSnapshotResponse response = _responseGenerator.createVMSnapshotResponse(result); response.setResponseName(getCommandName()); diff --git a/core/src/com/cloud/agent/api/RestoreVMSnapshotAnswer.java b/core/src/com/cloud/agent/api/RestoreVMSnapshotAnswer.java new file mode 100644 index 00000000000..390f49a199e --- /dev/null +++ b/core/src/com/cloud/agent/api/RestoreVMSnapshotAnswer.java @@ -0,0 +1,63 @@ +// +// 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; + +import java.util.List; + +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import com.cloud.vm.VirtualMachine; + +public class RestoreVMSnapshotAnswer extends Answer { + + private List volumeTOs; + private VirtualMachine.PowerState vmState; + + public RestoreVMSnapshotAnswer(RestoreVMSnapshotCommand cmd, boolean result, String message) { + super(cmd, result, message); + } + + public RestoreVMSnapshotAnswer() { + super(); + } + + public RestoreVMSnapshotAnswer(RestoreVMSnapshotCommand cmd, List volumeTOs, VirtualMachine.PowerState vmState) { + super(cmd, true, ""); + this.volumeTOs = volumeTOs; + this.vmState = vmState; + } + + public VirtualMachine.PowerState getVmState() { + return vmState; + } + + public List getVolumeTOs() { + return volumeTOs; + } + + public void setVolumeTOs(List volumeTOs) { + this.volumeTOs = volumeTOs; + } + + public void setVmState(VirtualMachine.PowerState vmState) { + this.vmState = vmState; + } + +} diff --git a/core/src/com/cloud/agent/api/RestoreVMSnapshotCommand.java b/core/src/com/cloud/agent/api/RestoreVMSnapshotCommand.java new file mode 100644 index 00000000000..2769475ba0c --- /dev/null +++ b/core/src/com/cloud/agent/api/RestoreVMSnapshotCommand.java @@ -0,0 +1,52 @@ +// +// 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; + +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +public class RestoreVMSnapshotCommand extends VMSnapshotBaseCommand { + + List snapshots; + Map snapshotAndParents; + + public RestoreVMSnapshotCommand(String vmName, VMSnapshotTO snapshot, List volumeTOs, String guestOSType) { + super(vmName, snapshot, volumeTOs, guestOSType); + } + + public List getSnapshots() { + return snapshots; + } + + public void setSnapshots(List snapshots) { + this.snapshots = snapshots; + } + + public Map getSnapshotAndParents() { + return snapshotAndParents; + } + + public void setSnapshotAndParents(Map snapshotAndParents) { + this.snapshotAndParents = snapshotAndParents; + } + +} diff --git a/engine/components-api/src/com/cloud/vm/snapshot/VMSnapshotManager.java b/engine/components-api/src/com/cloud/vm/snapshot/VMSnapshotManager.java index e7e3372b0db..ce8a818ac1d 100644 --- a/engine/components-api/src/com/cloud/vm/snapshot/VMSnapshotManager.java +++ b/engine/components-api/src/com/cloud/vm/snapshot/VMSnapshotManager.java @@ -17,7 +17,11 @@ package com.cloud.vm.snapshot; +import java.util.List; + +import com.cloud.agent.api.RestoreVMSnapshotCommand; import com.cloud.utils.component.Manager; +import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; public interface VMSnapshotManager extends VMSnapshotService, Manager { @@ -42,4 +46,7 @@ public interface VMSnapshotManager extends VMSnapshotService, Manager { boolean syncVMSnapshot(VMInstanceVO vm, Long hostId); boolean hasActiveVMSnapshotTasks(Long vmId); + + RestoreVMSnapshotCommand createRestoreCommand(UserVmVO userVm, List vmSnapshotVOs); + } diff --git a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java index f982de8f614..b1c69b2c22a 100644 --- a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java @@ -85,6 +85,8 @@ import com.cloud.agent.api.PlugNicCommand; import com.cloud.agent.api.PrepareForMigrationCommand; import com.cloud.agent.api.RebootAnswer; import com.cloud.agent.api.RebootCommand; +import com.cloud.agent.api.RestoreVMSnapshotAnswer; +import com.cloud.agent.api.RestoreVMSnapshotCommand; import com.cloud.agent.api.ScaleVmCommand; import com.cloud.agent.api.StartAnswer; import com.cloud.agent.api.StartCommand; @@ -201,6 +203,7 @@ import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.VMSnapshotManager; +import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMachineManager, VmWorkJobHandler, Listener, Configurable { @@ -1721,6 +1724,18 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } } + UserVmVO userVm = _userVmDao.findById(vm.getId()); + if (userVm != null) { + List vmSnapshots = _vmSnapshotDao.findByVm(vm.getId()); + RestoreVMSnapshotCommand command = _vmSnapshotMgr.createRestoreCommand(userVm, vmSnapshots); + if (command != null) { + RestoreVMSnapshotAnswer restoreVMSnapshotAnswer = (RestoreVMSnapshotAnswer) _agentMgr.send(hostId, command); + if (restoreVMSnapshotAnswer == null || !restoreVMSnapshotAnswer.getResult()) { + s_logger.warn("Unable to restore the vm snapshot from image file after live migration of vm with vmsnapshots: " + restoreVMSnapshotAnswer.getDetails()); + } + } + } + return true; } @@ -2603,7 +2618,11 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac private void orchestrateReboot(final String vmUuid, final Map params) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException { final VMInstanceVO vm = _vmDao.findByUuid(vmUuid); - + // if there are active vm snapshots task, state change is not allowed + if(_vmSnapshotMgr.hasActiveVMSnapshotTasks(vm.getId())){ + s_logger.error("Unable to reboot VM " + vm + " due to: " + vm.getInstanceName() + " has active VM snapshots tasks"); + throw new CloudRuntimeException("Unable to reboot VM " + vm + " due to: " + vm.getInstanceName() + " has active VM snapshots tasks"); + } final DataCenter dc = _entityMgr.findById(DataCenter.class, vm.getDataCenterId()); final Host host = _hostDao.findById(vm.getHostId()); if (host == null) { diff --git a/engine/storage/snapshot/src/org/apache/cloudstack/storage/snapshot/XenserverSnapshotStrategy.java b/engine/storage/snapshot/src/org/apache/cloudstack/storage/snapshot/XenserverSnapshotStrategy.java index a9a3f4ccb9a..16a47c6fa1e 100644 --- a/engine/storage/snapshot/src/org/apache/cloudstack/storage/snapshot/XenserverSnapshotStrategy.java +++ b/engine/storage/snapshot/src/org/apache/cloudstack/storage/snapshot/XenserverSnapshotStrategy.java @@ -218,6 +218,12 @@ public class XenserverSnapshotStrategy extends SnapshotStrategyBase { @Override public boolean deleteSnapshot(Long snapshotId) { SnapshotVO snapshotVO = snapshotDao.findById(snapshotId); + + if (snapshotVO.getState() == Snapshot.State.Allocated) { + snapshotDao.remove(snapshotId); + return true; + } + if (snapshotVO.getState() == Snapshot.State.Destroyed) { return true; } diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 38dabdad0ae..42d80b373b1 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -22,6 +22,7 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.Reader; +import java.io.StringReader; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; @@ -44,6 +45,9 @@ import java.util.regex.Pattern; import javax.ejb.Local; import javax.naming.ConfigurationException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import com.google.common.base.Strings; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; @@ -62,7 +66,14 @@ import org.libvirt.Domain; import org.libvirt.DomainBlockStats; import org.libvirt.DomainInfo; import org.libvirt.DomainInfo.DomainState; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; import org.libvirt.DomainInterfaceStats; +import org.libvirt.DomainSnapshot; import org.libvirt.LibvirtException; import org.libvirt.MemoryStatistic; import org.libvirt.NodeInfo; @@ -142,6 +153,7 @@ import com.cloud.utils.NumbersUtil; import com.cloud.utils.StringUtils; import com.cloud.utils.Pair; import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.Ternary; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; import com.cloud.utils.script.OutputInterpreter; @@ -2776,6 +2788,22 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv DomainState state = null; Domain dm = null; + // delete the metadata of vm snapshots before stopping + try { + dm = conn.domainLookupByName(vmName); + cleanVMSnapshotMetadata(dm); + } catch (LibvirtException e) { + s_logger.debug("Failed to get vm :" + e.getMessage()); + } finally { + try { + if (dm != null) { + dm.free(); + } + } catch (LibvirtException l) { + s_logger.trace("Ignoring libvirt error.", l); + } + } + s_logger.debug("Try to stop the vm at first"); String ret = stopVM(conn, vmName, false); if (ret == Script.ERR_TIMEOUT) { @@ -3481,4 +3509,77 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv } return device; } + + public List> cleanVMSnapshotMetadata(Domain dm) throws LibvirtException { + s_logger.debug("Cleaning the metadata of vm snapshots of vm " + dm.getName()); + List> vmsnapshots = new ArrayList>(); + if (dm.snapshotNum() == 0) { + return vmsnapshots; + } + String currentSnapshotName = null; + try { + DomainSnapshot snapshotCurrent = dm.snapshotCurrent(); + String snapshotXML = snapshotCurrent.getXMLDesc(); + snapshotCurrent.free(); + DocumentBuilder builder; + try { + builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + + InputSource is = new InputSource(); + is.setCharacterStream(new StringReader(snapshotXML)); + Document doc = builder.parse(is); + Element rootElement = doc.getDocumentElement(); + + currentSnapshotName = getTagValue("name", rootElement); + } catch (ParserConfigurationException e) { + s_logger.debug(e.toString()); + } catch (SAXException e) { + s_logger.debug(e.toString()); + } catch (IOException e) { + s_logger.debug(e.toString()); + } + } catch (LibvirtException e) { + s_logger.debug("Fail to get the current vm snapshot for vm: " + dm.getName() + ", continue"); + } + int flags = 2; // VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY = 2 + String[] snapshotNames = dm.snapshotListNames(); + Arrays.sort(snapshotNames); + for (String snapshotName: snapshotNames) { + DomainSnapshot snapshot = dm.snapshotLookupByName(snapshotName); + Boolean isCurrent = (currentSnapshotName != null && currentSnapshotName.equals(snapshotName)) ? true: false; + vmsnapshots.add(new Ternary(snapshotName, isCurrent, snapshot.getXMLDesc())); + } + for (String snapshotName: snapshotNames) { + DomainSnapshot snapshot = dm.snapshotLookupByName(snapshotName); + snapshot.delete(flags); // clean metadata of vm snapshot + } + return vmsnapshots; + } + + private static String getTagValue(String tag, Element eElement) { + NodeList nlList = eElement.getElementsByTagName(tag).item(0).getChildNodes(); + Node nValue = nlList.item(0); + + return nValue.getNodeValue(); + } + + public void restoreVMSnapshotMetadata(Domain dm, String vmName, List> vmsnapshots) { + s_logger.debug("Restoring the metadata of vm snapshots of vm " + vmName); + for (Ternary vmsnapshot: vmsnapshots) { + String snapshotName = vmsnapshot.first(); + Boolean isCurrent = vmsnapshot.second(); + String snapshotXML = vmsnapshot.third(); + s_logger.debug("Restoring vm snapshot " + snapshotName + " on " + vmName + " with XML:\n " + snapshotXML); + try { + int flags = 1; // VIR_DOMAIN_SNAPSHOT_CREATE_REDEFINE = 1 + if (isCurrent) { + flags += 2; // VIR_DOMAIN_SNAPSHOT_CREATE_CURRENT = 2 + } + dm.snapshotCreateXML(snapshotXML, flags); + } catch (LibvirtException e) { + s_logger.debug("Failed to restore vm snapshot " + snapshotName + ", continue"); + continue; + } + } + } } diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateVMSnapshotCommandWrapper.java new file mode 100644 index 00000000000..c7941e7fc89 --- /dev/null +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateVMSnapshotCommandWrapper.java @@ -0,0 +1,82 @@ +// +// 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 org.apache.log4j.Logger; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.DomainInfo.DomainState; +import org.libvirt.LibvirtException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CreateVMSnapshotAnswer; +import com.cloud.agent.api.CreateVMSnapshotCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = CreateVMSnapshotCommand.class) +public final class LibvirtCreateVMSnapshotCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(LibvirtCreateVMSnapshotCommandWrapper.class); + + @Override + public Answer execute(final CreateVMSnapshotCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + String vmName = cmd.getVmName(); + String vmSnapshotName = cmd.getTarget().getSnapshotName(); + + Domain dm = null; + try { + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + Connect conn = libvirtUtilitiesHelper.getConnection(); + dm = libvirtComputingResource.getDomain(conn, vmName); + + if (dm == null) { + return new CreateVMSnapshotAnswer(cmd, false, + "Create VM Snapshot Failed due to can not find vm: " + vmName); + } + + DomainState domainState = dm.getInfo().state ; + if (domainState != DomainState.VIR_DOMAIN_RUNNING) { + return new CreateVMSnapshotAnswer(cmd, false, + "Create VM Snapshot Failed due to vm is not running: " + vmName + " with domainState = " + domainState); + } + + String vmSnapshotXML = "" + " " + vmSnapshotName + "" + + " " + ""; + + dm.snapshotCreateXML(vmSnapshotXML); + + return new CreateVMSnapshotAnswer(cmd, cmd.getTarget(), cmd.getVolumeTOs()); + } catch (LibvirtException e) { + String msg = " Create VM snapshot failed due to " + e.toString(); + s_logger.warn(msg, e); + return new CreateVMSnapshotAnswer(cmd, false, msg); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException l) { + s_logger.trace("Ignoring libvirt error.", l); + }; + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVMSnapshotCommandWrapper.java new file mode 100644 index 00000000000..9efec950af3 --- /dev/null +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVMSnapshotCommandWrapper.java @@ -0,0 +1,110 @@ +// +// 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 org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.log4j.Logger; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.DomainSnapshot; +import org.libvirt.LibvirtException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.DeleteVMSnapshotAnswer; +import com.cloud.agent.api.DeleteVMSnapshotCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Volume; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = DeleteVMSnapshotCommand.class) +public final class LibvirtDeleteVMSnapshotCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(LibvirtDeleteVMSnapshotCommandWrapper.class); + + @Override + public Answer execute(final DeleteVMSnapshotCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + String vmName = cmd.getVmName(); + + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + Domain dm = null; + DomainSnapshot snapshot = null; + try { + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + Connect conn = libvirtUtilitiesHelper.getConnection(); + dm = libvirtComputingResource.getDomain(conn, vmName); + + snapshot = dm.snapshotLookupByName(cmd.getTarget().getSnapshotName()); + + snapshot.delete(0); // only remove this snapshot, not children + + return new DeleteVMSnapshotAnswer(cmd, cmd.getVolumeTOs()); + } catch (LibvirtException e) { + String msg = " Delete VM snapshot failed due to " + e.toString(); + + if (dm == null) { + s_logger.debug("Can not find running vm: " + vmName + ", now we are trying to delete the vm snapshot using qemu-img if the format of root volume is QCOW2"); + VolumeObjectTO rootVolume = null; + for (VolumeObjectTO volume: cmd.getVolumeTOs()) { + if (volume.getVolumeType() == Volume.Type.ROOT) { + rootVolume = volume; + break; + } + } + if (rootVolume != null && ImageFormat.QCOW2.equals(rootVolume.getFormat())) { + PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) rootVolume.getDataStore(); + KVMPhysicalDisk rootDisk = storagePoolMgr.getPhysicalDisk(primaryStore.getPoolType(), + primaryStore.getUuid(), rootVolume.getPath()); + String qemu_img_snapshot = Script.runSimpleBashScript("qemu-img snapshot -l " + rootDisk.getPath() + " | tail -n +3 | awk -F ' ' '{print $2}' | grep ^" + cmd.getTarget().getSnapshotName() + "$"); + if (qemu_img_snapshot == null) { + s_logger.info("Cannot find snapshot " + cmd.getTarget().getSnapshotName() + " in file " + rootDisk.getPath() + ", return true"); + return new DeleteVMSnapshotAnswer(cmd, cmd.getVolumeTOs()); + } + int result = Script.runSimpleBashScriptForExitValue("qemu-img snapshot -d " + cmd.getTarget().getSnapshotName() + " " + rootDisk.getPath()); + if (result != 0) { + return new DeleteVMSnapshotAnswer(cmd, false, + "Delete VM Snapshot Failed due to can not remove snapshot from image file " + rootDisk.getPath() + " : " + result); + } else { + return new DeleteVMSnapshotAnswer(cmd, cmd.getVolumeTOs()); + } + } + } else if (snapshot == null) { + s_logger.debug("Can not find vm snapshot " + cmd.getTarget().getSnapshotName() + " on vm: " + vmName + ", return true"); + return new DeleteVMSnapshotAnswer(cmd, cmd.getVolumeTOs()); + } + + s_logger.warn(msg, e); + return new DeleteVMSnapshotAnswer(cmd, false, msg); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException l) { + s_logger.trace("Ignoring libvirt error.", l); + }; + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java index 6736c512dd6..9e7b78e5047 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java @@ -44,6 +44,7 @@ import com.cloud.hypervisor.kvm.resource.MigrateKVMAsync; import com.cloud.hypervisor.kvm.resource.VifDriver; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Ternary; @ResourceWrapper(handles = MigrateCommand.class) public final class LibvirtMigrateCommandWrapper extends CommandWrapper { @@ -67,6 +68,7 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper> vmsnapshots = null; try { final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); @@ -99,6 +101,9 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(LibvirtRestoreVMSnapshotCommandWrapper.class); + + @Override + public Answer execute(final RestoreVMSnapshotCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + String vmName = cmd.getVmName(); + List listVolumeTo = cmd.getVolumeTOs(); + VirtualMachine.PowerState vmState = VirtualMachine.PowerState.PowerOn; + + Domain dm = null; + try { + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + Connect conn = libvirtUtilitiesHelper.getConnection(); + dm = libvirtComputingResource.getDomain(conn, vmName); + + if (dm == null) { + return new RestoreVMSnapshotAnswer(cmd, false, + "Restore VM Snapshot Failed due to can not find vm: " + vmName); + } + String xmlDesc = dm.getXMLDesc(0); + + List snapshots = cmd.getSnapshots(); + Map snapshotAndParents = cmd.getSnapshotAndParents(); + for (VMSnapshotTO snapshot: snapshots) { + VMSnapshotTO parent = snapshotAndParents.get(snapshot.getId()); + String vmSnapshotXML = libvirtUtilitiesHelper.generateVMSnapshotXML(snapshot, parent, xmlDesc); + s_logger.debug("Restoring vm snapshot " + snapshot.getSnapshotName() + " on " + vmName + " with XML:\n " + vmSnapshotXML); + try { + int flags = 1; // VIR_DOMAIN_SNAPSHOT_CREATE_REDEFINE = 1 + if (snapshot.getCurrent()) { + flags += 2; // VIR_DOMAIN_SNAPSHOT_CREATE_CURRENT = 2 + } + dm.snapshotCreateXML(vmSnapshotXML, flags); + } catch (LibvirtException e) { + s_logger.debug("Failed to restore vm snapshot " + snapshot.getSnapshotName() + " on " + vmName); + return new RestoreVMSnapshotAnswer(cmd, false, e.toString()); + } + } + + return new RestoreVMSnapshotAnswer(cmd, listVolumeTo, vmState); + } catch (LibvirtException e) { + String msg = " Restore snapshot failed due to " + e.toString(); + s_logger.warn(msg, e); + return new RestoreVMSnapshotAnswer(cmd, false, msg); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException l) { + s_logger.trace("Ignoring libvirt error.", l); + }; + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertToVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertToVMSnapshotCommandWrapper.java new file mode 100644 index 00000000000..086d6ef7a89 --- /dev/null +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertToVMSnapshotCommandWrapper.java @@ -0,0 +1,95 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.util.List; + +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.log4j.Logger; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.DomainSnapshot; +import org.libvirt.LibvirtException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.RevertToVMSnapshotAnswer; +import com.cloud.agent.api.RevertToVMSnapshotCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.snapshot.VMSnapshot; + +@ResourceWrapper(handles = RevertToVMSnapshotCommand.class) +public final class LibvirtRevertToVMSnapshotCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(LibvirtRevertToVMSnapshotCommandWrapper.class); + + @Override + public Answer execute(final RevertToVMSnapshotCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + String vmName = cmd.getVmName(); + List listVolumeTo = cmd.getVolumeTOs(); + VMSnapshot.Type vmSnapshotType = cmd.getTarget().getType(); + Boolean snapshotMemory = vmSnapshotType == VMSnapshot.Type.DiskAndMemory; + VirtualMachine.PowerState vmState = null; + + Domain dm = null; + try { + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + Connect conn = libvirtUtilitiesHelper.getConnection(); + dm = libvirtComputingResource.getDomain(conn, vmName); + + if (dm == null) { + return new RevertToVMSnapshotAnswer(cmd, false, + "Revert to VM Snapshot Failed due to can not find vm: " + vmName); + } + + DomainSnapshot snapshot = dm.snapshotLookupByName(cmd.getTarget().getSnapshotName()); + if (snapshot == null) + return new RevertToVMSnapshotAnswer(cmd, false, "Cannot find vmSnapshot with name: " + cmd.getTarget().getSnapshotName()); + + dm.revertToSnapshot(snapshot); + snapshot.free(); + + if (!snapshotMemory) { + dm.destroy(); + if (dm.isPersistent() == 1) + dm.undefine(); + vmState = VirtualMachine.PowerState.PowerOff; + } else { + vmState = VirtualMachine.PowerState.PowerOn; + } + + return new RevertToVMSnapshotAnswer(cmd, listVolumeTo, vmState); + } catch (LibvirtException e) { + String msg = " Revert to VM snapshot failed due to " + e.toString(); + s_logger.warn(msg, e); + return new RevertToVMSnapshotAnswer(cmd, false, msg); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException l) { + s_logger.trace("Ignoring libvirt error.", l); + }; + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUtilitiesHelper.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUtilitiesHelper.java index 7a93e1f3170..2881ed04220 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUtilitiesHelper.java +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUtilitiesHelper.java @@ -25,6 +25,7 @@ import javax.naming.ConfigurationException; import org.libvirt.Connect; import org.libvirt.LibvirtException; +import com.cloud.agent.api.VMSnapshotTO; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtConnection; import com.cloud.storage.StorageLayer; @@ -96,4 +97,16 @@ public class LibvirtUtilitiesHelper { final Script script = new Script(scriptPath, TIMEOUT); return script; } + + public String generateVMSnapshotXML(VMSnapshotTO snapshot, VMSnapshotTO parent, String domainXmlDesc) { + String parentName = (parent == null)? "": (" " + parent.getSnapshotName() + "\n"); + String vmSnapshotXML = "\n" + + " " + snapshot.getSnapshotName() + "\n" + + " running\n" + + parentName + + " " + (int) Math.rint(snapshot.getCreateTime()/1000) + "\n" + + domainXmlDesc + + ""; + return vmSnapshotXML; + } } \ No newline at end of file diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index f11cb21084e..55b458a59cc 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -695,8 +695,10 @@ public class KVMStorageProcessor implements StorageProcessor { final String secondaryStoragePoolUrl = nfsImageStore.getUrl(); // NOTE: snapshot name is encoded in snapshot path final int index = snapshot.getPath().lastIndexOf("/"); + final boolean isCreatedFromVmSnapshot = (index == -1) ? true: false; // -1 means the snapshot is created from existing vm snapshot final String snapshotName = snapshot.getPath().substring(index + 1); + String descName = snapshotName; final String volumePath = snapshot.getVolume().getPath(); String snapshotDestPath = null; String snapshotRelPath = null; @@ -768,20 +770,23 @@ public class KVMStorageProcessor implements StorageProcessor { command.add("-b", snapshotDisk.getPath()); command.add("-n", snapshotName); command.add("-p", snapshotDestPath); - command.add("-t", snapshotName); + if (isCreatedFromVmSnapshot) { + descName = UUID.randomUUID().toString(); + } + command.add("-t", descName); final String result = command.execute(); if (result != null) { s_logger.debug("Failed to backup snaptshot: " + result); return new CopyCmdAnswer(result); } - final File snapFile = new File(snapshotDestPath + "/" + snapshotName); + final File snapFile = new File(snapshotDestPath + "/" + descName); if(snapFile.exists()){ size = snapFile.length(); } } final SnapshotObjectTO newSnapshot = new SnapshotObjectTO(); - newSnapshot.setPath(snapshotRelPath + File.separator + snapshotName); + newSnapshot.setPath(snapshotRelPath + File.separator + descName); newSnapshot.setPhysicalSize(size); return new CopyCmdAnswer(newSnapshot); } catch (final LibvirtException e) { @@ -791,48 +796,52 @@ public class KVMStorageProcessor implements StorageProcessor { s_logger.debug("Failed to backup snapshot: ", e); return new CopyCmdAnswer(e.toString()); } finally { - try { - /* Delete the snapshot on primary */ - DomainInfo.DomainState state = null; - Domain vm = null; - if (vmName != null) { - try { - vm = resource.getDomain(conn, vmName); - state = vm.getInfo().state; - } catch (final LibvirtException e) { - s_logger.trace("Ignoring libvirt error.", e); - } - } - - final KVMStoragePool primaryStorage = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), - primaryStore.getUuid()); - if (state == DomainInfo.DomainState.VIR_DOMAIN_RUNNING && !primaryStorage.isExternalSnapshot()) { - final DomainSnapshot snap = vm.snapshotLookupByName(snapshotName); - snap.delete(0); - - /* - * libvirt on RHEL6 doesn't handle resume event emitted from - * qemu - */ - vm = resource.getDomain(conn, vmName); - state = vm.getInfo().state; - if (state == DomainInfo.DomainState.VIR_DOMAIN_PAUSED) { - vm.resume(); - } - } else { - if (primaryPool.getType() != StoragePoolType.RBD) { - final Script command = new Script(_manageSnapshotPath, _cmdsTimeout, s_logger); - command.add("-d", snapshotDisk.getPath()); - command.add("-n", snapshotName); - final String result = command.execute(); - if (result != null) { - s_logger.debug("Failed to delete snapshot on primary: " + result); - // return new CopyCmdAnswer("Failed to backup snapshot: " + result); + if (isCreatedFromVmSnapshot) { + s_logger.debug("Ignoring removal of vm snapshot on primary as this snapshot is created from vm snapshot"); + } else { + try { + /* Delete the snapshot on primary */ + DomainInfo.DomainState state = null; + Domain vm = null; + if (vmName != null) { + try { + vm = resource.getDomain(conn, vmName); + state = vm.getInfo().state; + } catch (final LibvirtException e) { + s_logger.trace("Ignoring libvirt error.", e); } } + + final KVMStoragePool primaryStorage = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), + primaryStore.getUuid()); + if (state == DomainInfo.DomainState.VIR_DOMAIN_RUNNING && !primaryStorage.isExternalSnapshot()) { + final DomainSnapshot snap = vm.snapshotLookupByName(snapshotName); + snap.delete(0); + + /* + * libvirt on RHEL6 doesn't handle resume event emitted from + * qemu + */ + vm = resource.getDomain(conn, vmName); + state = vm.getInfo().state; + if (state == DomainInfo.DomainState.VIR_DOMAIN_PAUSED) { + vm.resume(); + } + } else { + if (primaryPool.getType() != StoragePoolType.RBD) { + final Script command = new Script(_manageSnapshotPath, _cmdsTimeout, s_logger); + command.add("-d", snapshotDisk.getPath()); + command.add("-n", snapshotName); + final String result = command.execute(); + if (result != null) { + s_logger.debug("Failed to delete snapshot on primary: " + result); + // return new CopyCmdAnswer("Failed to backup snapshot: " + result); + } + } + } + } catch (final Exception ex) { + s_logger.debug("Failed to delete snapshots on primary", ex); } - } catch (final Exception ex) { - s_logger.debug("Failed to delete snapshots on primary", ex); } try { diff --git a/server/src/com/cloud/api/ApiResponseHelper.java b/server/src/com/cloud/api/ApiResponseHelper.java index 6815797c05c..0b470261c93 100644 --- a/server/src/com/cloud/api/ApiResponseHelper.java +++ b/server/src/com/cloud/api/ApiResponseHelper.java @@ -577,6 +577,7 @@ public class ApiResponseHelper implements ResponseGenerator { vmSnapshotResponse.setParentName(vmSnapshotParent.getDisplayName()); } } + populateOwner(vmSnapshotResponse, vmSnapshot); Project project = ApiDBUtils.findProjectByProjectAccountId(vmSnapshot.getAccountId()); if (project != null) { vmSnapshotResponse.setProjectId(project.getUuid()); diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java index b6a263705fa..5ab7c362f0d 100644 --- a/server/src/com/cloud/server/ManagementServerImpl.java +++ b/server/src/com/cloud/server/ManagementServerImpl.java @@ -409,6 +409,7 @@ import org.apache.cloudstack.api.command.user.securitygroup.ListSecurityGroupsCm import org.apache.cloudstack.api.command.user.securitygroup.RevokeSecurityGroupEgressCmd; import org.apache.cloudstack.api.command.user.securitygroup.RevokeSecurityGroupIngressCmd; import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotCmd; +import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotFromVMSnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotPolicyCmd; import org.apache.cloudstack.api.command.user.snapshot.DeleteSnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.DeleteSnapshotPoliciesCmd; @@ -2837,6 +2838,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(RevokeSecurityGroupEgressCmd.class); cmdList.add(RevokeSecurityGroupIngressCmd.class); cmdList.add(CreateSnapshotCmd.class); + cmdList.add(CreateSnapshotFromVMSnapshotCmd.class); cmdList.add(DeleteSnapshotCmd.class); cmdList.add(CreateSnapshotPolicyCmd.class); cmdList.add(UpdateSnapshotPolicyCmd.class); diff --git a/server/src/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/com/cloud/storage/VolumeApiServiceImpl.java index 759c7b440ff..89e1fd90b4e 100644 --- a/server/src/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/com/cloud/storage/VolumeApiServiceImpl.java @@ -2195,6 +2195,53 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic return snapshotMgr.allocSnapshot(volumeId, policyId, snapshotName, locationType); } + @Override + public Snapshot allocSnapshotForVm(Long vmId, Long volumeId, String snapshotName) throws ResourceAllocationException { + Account caller = CallContext.current().getCallingAccount(); + VMInstanceVO vm = _vmInstanceDao.findById(vmId); + if (vm == null) { + throw new InvalidParameterValueException("Creating snapshot failed due to vm:" + vmId + " doesn't exist"); + } + _accountMgr.checkAccess(caller, null, true, vm); + + VolumeInfo volume = volFactory.getVolume(volumeId); + if (volume == null) { + throw new InvalidParameterValueException("Creating snapshot failed due to volume:" + volumeId + " doesn't exist"); + } + _accountMgr.checkAccess(caller, null, true, volume); + VirtualMachine attachVM = volume.getAttachedVM(); + if (attachVM == null || attachVM.getId() != vm.getId()) { + throw new InvalidParameterValueException("Creating snapshot failed due to volume:" + volumeId + " doesn't attach to vm :" + vm); + } + + DataCenter zone = _dcDao.findById(volume.getDataCenterId()); + if (zone == null) { + throw new InvalidParameterValueException("Can't find zone by id " + volume.getDataCenterId()); + } + + if (Grouping.AllocationState.Disabled == zone.getAllocationState() && !_accountMgr.isRootAdmin(caller.getId())) { + throw new PermissionDeniedException("Cannot perform this operation, Zone is currently disabled: " + zone.getName()); + } + + if (volume.getState() != Volume.State.Ready) { + throw new InvalidParameterValueException("VolumeId: " + volumeId + " is not in " + Volume.State.Ready + " state but " + volume.getState() + ". Cannot take snapshot."); + } + + if ( volume.getTemplateId() != null ) { + VMTemplateVO template = _templateDao.findById(volume.getTemplateId()); + if( template != null && template.getTemplateType() == Storage.TemplateType.SYSTEM ) { + throw new InvalidParameterValueException("VolumeId: " + volumeId + " is for System VM , Creating snapshot against System VM volumes is not supported"); + } + } + + StoragePool storagePool = (StoragePool)volume.getDataStore(); + if (storagePool == null) { + throw new InvalidParameterValueException("VolumeId: " + volumeId + " please attach this volume to a VM before create snapshot for it"); + } + + return snapshotMgr.allocSnapshot(volumeId, Snapshot.MANUAL_POLICY_ID, snapshotName, null); + } + @Override @ActionEvent(eventType = EventTypes.EVENT_VOLUME_EXTRACT, eventDescription = "extracting volume", async = true) public String extractVolume(ExtractVolumeCmd cmd) { diff --git a/server/src/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/com/cloud/storage/snapshot/SnapshotManagerImpl.java index bb0fe3720f2..d9a93c3cad1 100644 --- a/server/src/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -101,6 +101,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; @@ -262,6 +263,11 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement if (vm.getState() != State.Stopped && vm.getState() != State.Shutdowned) { throw new InvalidParameterValueException("The VM the specified disk is attached to is not in the shutdown state."); } + // If target VM has associated VM snapshots then don't allow to revert from snapshot + List vmSnapshots = _vmSnapshotDao.findByVm(instanceId); + if (vmSnapshots.size() > 0) { + throw new InvalidParameterValueException("Unable to revert snapshot for VM, please remove VM snapshots before reverting VM from snapshot"); + } } SnapshotInfo snapshotInfo = snapshotFactory.getSnapshot(snapshotId, DataStoreRole.Image); @@ -363,6 +369,76 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement return snapshotSrv.backupSnapshot(snapshot); } + @Override + public Snapshot backupSnapshotFromVmSnapshot(Long snapshotId, Long vmId, Long volumeId, Long vmSnapshotId) { + VMInstanceVO vm = _vmDao.findById(vmId); + if (vm == null) { + throw new InvalidParameterValueException("Creating snapshot failed due to vm:" + vmId + " doesn't exist"); + } + if (! HypervisorType.KVM.equals(vm.getHypervisorType())) { + throw new InvalidParameterValueException("Unsupported hypervisor type " + vm.getHypervisorType() + ". This supports KVM only"); + } + + VMSnapshotVO vmSnapshot = _vmSnapshotDao.findById(vmSnapshotId); + if (vmSnapshot == null) { + throw new InvalidParameterValueException("Creating snapshot failed due to vmSnapshot:" + vmSnapshotId + " doesn't exist"); + } + // check vmsnapshot permissions + Account caller = CallContext.current().getCallingAccount(); + _accountMgr.checkAccess(caller, null, true, vmSnapshot); + + SnapshotVO snapshot = _snapshotDao.findById(snapshotId); + if (snapshot == null) { + throw new InvalidParameterValueException("Creating snapshot failed due to snapshot:" + snapshotId + " doesn't exist"); + } + + VolumeInfo volume = volFactory.getVolume(volumeId); + if (volume == null) { + throw new InvalidParameterValueException("Creating snapshot failed due to volume:" + volumeId + " doesn't exist"); + } + + if (volume.getState() != Volume.State.Ready) { + throw new InvalidParameterValueException("VolumeId: " + volumeId + " is not in " + Volume.State.Ready + " state but " + volume.getState() + ". Cannot take snapshot."); + } + + DataStore store = volume.getDataStore(); + SnapshotDataStoreVO parentSnapshotDataStoreVO = _snapshotStoreDao.findParent(store.getRole(), store.getId(), volumeId); + if (parentSnapshotDataStoreVO != null) { + //Double check the snapshot is removed or not + SnapshotVO parentSnap = _snapshotDao.findById(parentSnapshotDataStoreVO.getSnapshotId()); + if (parentSnap != null && parentSnapshotDataStoreVO.getInstallPath() != null && parentSnapshotDataStoreVO.getInstallPath().equals(vmSnapshot.getName())) { + throw new InvalidParameterValueException("Creating snapshot failed due to snapshot : " + parentSnap.getUuid() + " is created from the same vm snapshot"); + } + } + SnapshotInfo snapshotInfo = this.snapshotFactory.getSnapshot(snapshotId, store); + snapshotInfo = (SnapshotInfo) store.create(snapshotInfo); + SnapshotDataStoreVO snapshotOnPrimaryStore = this._snapshotStoreDao.findBySnapshot(snapshot.getId(), store.getRole()); + snapshotOnPrimaryStore.setState(ObjectInDataStoreStateMachine.State.Ready); + snapshotOnPrimaryStore.setInstallPath(vmSnapshot.getName()); + _snapshotStoreDao.update(snapshotOnPrimaryStore.getId(), snapshotOnPrimaryStore); + snapshot.setState(Snapshot.State.CreatedOnPrimary); + _snapshotDao.update(snapshot.getId(), snapshot); + + snapshotInfo = this.snapshotFactory.getSnapshot(snapshotId, store); + + Long snapshotOwnerId = vm.getAccountId(); + + try { + SnapshotStrategy snapshotStrategy = _storageStrategyFactory.getSnapshotStrategy(snapshot, SnapshotOperation.BACKUP); + if (snapshotStrategy == null) { + throw new CloudRuntimeException("Unable to find snaphot strategy to handle snapshot with id '" + snapshotId + "'"); + } + snapshotInfo = snapshotStrategy.backupSnapshot(snapshotInfo); + + } catch(Exception e) { + s_logger.debug("Failed to backup snapshot from vm snapshot", e); + _resourceLimitMgr.decrementResourceCount(snapshotOwnerId, ResourceType.snapshot); + _resourceLimitMgr.decrementResourceCount(snapshotOwnerId, ResourceType.secondary_storage, new Long(volume.getSize())); + throw new CloudRuntimeException("Failed to backup snapshot from vm snapshot", e); + } + return snapshotInfo; + } + @Override public SnapshotVO getParentSnapshot(VolumeInfo volume) { long preId = _snapshotDao.getLastSnapshot(volume.getId(), DataStoreRole.Primary); diff --git a/server/src/com/cloud/vm/UserVmManagerImpl.java b/server/src/com/cloud/vm/UserVmManagerImpl.java index cf586797b5d..ec7a6f6ff62 100644 --- a/server/src/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/com/cloud/vm/UserVmManagerImpl.java @@ -102,6 +102,8 @@ import com.cloud.agent.api.GetVmIpAddressCommand; import com.cloud.agent.api.GetVmStatsAnswer; import com.cloud.agent.api.GetVmStatsCommand; import com.cloud.agent.api.PvlanSetupCommand; +import com.cloud.agent.api.RestoreVMSnapshotAnswer; +import com.cloud.agent.api.RestoreVMSnapshotCommand; import com.cloud.agent.api.StartAnswer; import com.cloud.agent.api.VmDiskStatsEntry; import com.cloud.agent.api.VmStatsEntry; @@ -3804,11 +3806,17 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } + finalizeCommandsOnStart(cmds, profile); return true; } @Override public boolean finalizeCommandsOnStart(Commands cmds, VirtualMachineProfile profile) { + UserVmVO vm = _vmDao.findById(profile.getId()); + List vmSnapshots = _vmSnapshotDao.findByVm(vm.getId()); + RestoreVMSnapshotCommand command = _vmSnapshotMgr.createRestoreCommand(vm, vmSnapshots); + if (command != null) + cmds.addCommand("restoreVMSnapshot", command); return true; } @@ -3893,6 +3901,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return false; } + Answer answer = cmds.getAnswer("restoreVMSnapshot"); + if (answer != null && answer instanceof RestoreVMSnapshotAnswer) { + RestoreVMSnapshotAnswer restoreVMSnapshotAnswer = (RestoreVMSnapshotAnswer) answer; + if (restoreVMSnapshotAnswer == null || !restoreVMSnapshotAnswer.getResult()) { + s_logger.warn("Unable to restore the vm snapshot from image file to the VM: " + restoreVMSnapshotAnswer.getDetails()); + } + } + return true; } diff --git a/server/src/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java b/server/src/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java index bb1536d2810..d155186c1db 100644 --- a/server/src/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java +++ b/server/src/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java @@ -33,6 +33,8 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions; import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotStrategy; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.jobs.AsyncJob; @@ -44,8 +46,11 @@ import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; import org.apache.cloudstack.framework.jobs.impl.OutcomeImpl; import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; import org.apache.cloudstack.jobs.JobInfo; +import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import com.cloud.agent.api.RestoreVMSnapshotCommand; +import com.cloud.agent.api.VMSnapshotTO; import com.cloud.api.query.MutualExclusiveIdsManagerBase; import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; @@ -59,8 +64,10 @@ import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; import com.cloud.projects.Project.ListProjectResourcesCriteria; import com.cloud.service.dao.ServiceOfferingDetailsDao; +import com.cloud.storage.GuestOSVO; import com.cloud.storage.Snapshot; import com.cloud.storage.SnapshotVO; +import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.Volume; import com.cloud.storage.Volume.Type; import com.cloud.storage.VolumeVO; @@ -119,7 +126,8 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme @Inject HypervisorCapabilitiesDao _hypervisorCapabilitiesDao; @Inject StorageStrategyFactory storageStrategyFactory; - + @Inject + VolumeDataFactory volumeDataFactory; @Inject EntityManager _entityMgr; @Inject @@ -263,7 +271,7 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme throw new InvalidParameterValueException("Creating VM snapshot failed due to VM:" + vmId + " is a system VM or does not exist"); } - if (_snapshotDao.listByInstanceId(vmId, Snapshot.State.BackedUp).size() > 0) { + if (_snapshotDao.listByInstanceId(vmId, Snapshot.State.BackedUp).size() > 0 && ! HypervisorType.KVM.equals(userVmVo.getHypervisorType())) { throw new InvalidParameterValueException( "VM snapshot for this VM is not allowed. This VM has volumes attached which has snapshots, please remove all snapshots before taking VM snapshot"); } @@ -298,8 +306,8 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme throw new InvalidParameterValueException("Creating vm snapshot failed due to VM:" + vmId + " is not in the running or Stopped state"); } - if(snapshotMemory && userVmVo.getState() == VirtualMachine.State.Stopped){ - throw new InvalidParameterValueException("Can not snapshot memory when VM is in stopped state"); + if(snapshotMemory && userVmVo.getState() != VirtualMachine.State.Running){ + throw new InvalidParameterValueException("Can not snapshot memory when VM is not in Running state"); } // for KVM, only allow snapshot with memory when VM is in running state @@ -323,6 +331,9 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme if (activeSnapshots.size() > 0) { throw new CloudRuntimeException("There is other active volume snapshot tasks on the instance to which the volume is attached, please try again later."); } + if (userVmVo.getHypervisorType() == HypervisorType.KVM && volume.getFormat() != ImageFormat.QCOW2) { + throw new CloudRuntimeException("We only support create vm snapshots from vm with QCOW2 image"); + } } // check if there are other active VM snapshot tasks @@ -367,7 +378,7 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme @Override @ActionEvent(eventType = EventTypes.EVENT_VM_SNAPSHOT_CREATE, eventDescription = "creating VM snapshot", async = true) - public VMSnapshot creatVMSnapshot(Long vmId, Long vmSnapshotId, Boolean quiescevm) { + public VMSnapshot createVMSnapshot(Long vmId, Long vmSnapshotId, Boolean quiescevm) { UserVmVO userVm = _userVMDao.findById(vmId); if (userVm == null) { throw new InvalidParameterValueException("Create vm to snapshot failed due to vm: " + vmId + " is not found"); @@ -717,6 +728,40 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme } } + @Override + public RestoreVMSnapshotCommand createRestoreCommand(UserVmVO userVm, List vmSnapshotVOs) { + if (!HypervisorType.KVM.equals(userVm.getHypervisorType())) + return null; + + List snapshots = new ArrayList(); + Map snapshotAndParents = new HashMap(); + for (VMSnapshotVO vmSnapshotVO: vmSnapshotVOs) { + if (vmSnapshotVO.getType() == VMSnapshot.Type.DiskAndMemory) { + VMSnapshotVO snapshot = _vmSnapshotDao.findById(vmSnapshotVO.getId()); + VMSnapshotTO parent = getSnapshotWithParents(snapshot).getParent(); + VMSnapshotOptions options = snapshot.getOptions(); + boolean quiescevm = true; + if (options != null) + quiescevm = options.needQuiesceVM(); + VMSnapshotTO vmSnapshotTO = new VMSnapshotTO(snapshot.getId(), snapshot.getName(), snapshot.getType(), + snapshot.getCreated().getTime(), snapshot.getDescription(), snapshot.getCurrent(), parent, quiescevm); + snapshots.add(vmSnapshotTO); + snapshotAndParents.put(vmSnapshotVO.getId(), parent); + } + } + if (snapshotAndParents.isEmpty()) + return null; + + // prepare RestoreVMSnapshotCommand + String vmInstanceName = userVm.getInstanceName(); + List volumeTOs = getVolumeTOList(userVm.getId()); + GuestOSVO guestOS = _guestOSDao.findById(userVm.getGuestOSId()); + RestoreVMSnapshotCommand restoreSnapshotCommand = new RestoreVMSnapshotCommand(vmInstanceName, null, volumeTOs, guestOS.getDisplayName()); + restoreSnapshotCommand.setSnapshots(snapshots); + restoreSnapshotCommand.setSnapshotAndParents(snapshotAndParents); + return restoreSnapshotCommand; + } + @Override public VMSnapshot getVMSnapshotById(Long id) { VMSnapshotVO vmSnapshot = _vmSnapshotDao.findById(id); @@ -1050,4 +1095,42 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme return workJob; } + + private List getVolumeTOList(Long vmId) { + List volumeTOs = new ArrayList(); + List volumeVos = _volumeDao.findByInstance(vmId); + VolumeInfo volumeInfo = null; + for (VolumeVO volume : volumeVos) { + volumeInfo = volumeDataFactory.getVolume(volume.getId()); + + volumeTOs.add((VolumeObjectTO)volumeInfo.getTO()); + } + return volumeTOs; + } + + private VMSnapshotTO convert2VMSnapshotTO(VMSnapshotVO vo) { + return new VMSnapshotTO(vo.getId(), vo.getName(), vo.getType(), vo.getCreated().getTime(), vo.getDescription(), vo.getCurrent(), null, true); + } + + private VMSnapshotTO getSnapshotWithParents(VMSnapshotVO snapshot) { + Map snapshotMap = new HashMap(); + List allSnapshots = _vmSnapshotDao.findByVm(snapshot.getVmId()); + for (VMSnapshotVO vmSnapshotVO : allSnapshots) { + snapshotMap.put(vmSnapshotVO.getId(), vmSnapshotVO); + } + + VMSnapshotTO currentTO = convert2VMSnapshotTO(snapshot); + VMSnapshotTO result = currentTO; + VMSnapshotVO current = snapshot; + while (current.getParent() != null) { + VMSnapshotVO parent = snapshotMap.get(current.getParent()); + if (parent == null) { + break; + } + currentTO.setParent(convert2VMSnapshotTO(parent)); + current = snapshotMap.get(current.getParent()); + currentTO = currentTO.getParent(); + } + return result; + } } diff --git a/server/test/com/cloud/storage/snapshot/SnapshotManagerTest.java b/server/test/com/cloud/storage/snapshot/SnapshotManagerTest.java index 7f0f71b5fd6..58ee2e72d72 100644 --- a/server/test/com/cloud/storage/snapshot/SnapshotManagerTest.java +++ b/server/test/com/cloud/storage/snapshot/SnapshotManagerTest.java @@ -56,6 +56,8 @@ import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +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.junit.After; import org.junit.Assert; @@ -89,6 +91,8 @@ public class SnapshotManagerTest { @Mock VMSnapshotDao _vmSnapshotDao; @Mock + VMSnapshotVO vmSnapshotMock; + @Mock Account account; @Mock UserVmVO vmMock; @@ -118,11 +122,16 @@ public class SnapshotManagerTest { ResourceManager _resourceMgr; @Mock DataStore storeMock; + @Mock + SnapshotDataStoreDao _snapshotStoreDao; + @Mock + SnapshotDataStoreVO snapshotStoreMock; private static final long TEST_SNAPSHOT_ID = 3L; private static final long TEST_VOLUME_ID = 4L; private static final long TEST_VM_ID = 5L; private static final long TEST_STORAGE_POOL_ID = 6L; + private static final long TEST_VM_SNAPSHOT_ID = 6L; @Before public void setup() throws ResourceAllocationException { @@ -138,6 +147,7 @@ public class SnapshotManagerTest { _snapshotMgr._storagePoolDao = _storagePoolDao; _snapshotMgr._resourceMgr = _resourceMgr; _snapshotMgr._vmSnapshotDao = _vmSnapshotDao; + _snapshotMgr._snapshotStoreDao = _snapshotStoreDao; when(_snapshotDao.findById(anyLong())).thenReturn(snapshotMock); when(snapshotMock.getVolumeId()).thenReturn(TEST_VOLUME_ID); @@ -147,9 +157,11 @@ public class SnapshotManagerTest { when(volumeMock.getState()).thenReturn(Volume.State.Ready); when(volumeFactory.getVolume(anyLong())).thenReturn(volumeInfoMock); when(volumeInfoMock.getDataStore()).thenReturn(storeMock); + when(volumeInfoMock.getState()).thenReturn(Volume.State.Ready); when(storeMock.getId()).thenReturn(TEST_STORAGE_POOL_ID); when(snapshotFactory.getSnapshot(anyLong(), Mockito.any(DataStoreRole.class))).thenReturn(snapshotInfoMock); + when(_storageStrategyFactory.getSnapshotStrategy(Mockito.any(SnapshotVO.class), Mockito.eq(SnapshotOperation.BACKUP))).thenReturn(snapshotStrategy); when(_storageStrategyFactory.getSnapshotStrategy(Mockito.any(SnapshotVO.class), Mockito.eq(SnapshotOperation.REVERT))).thenReturn(snapshotStrategy); when(_storageStrategyFactory.getSnapshotStrategy(Mockito.any(SnapshotVO.class), Mockito.eq(SnapshotOperation.DELETE))).thenReturn(snapshotStrategy); @@ -278,4 +290,46 @@ public class SnapshotManagerTest { Snapshot snapshot = _snapshotMgr.revertSnapshot(TEST_SNAPSHOT_ID); Assert.assertNotNull(snapshot); } + + // vm on Xenserver, expected exception + @Test(expected = InvalidParameterValueException.class) + public void testBackupSnapshotFromVmSnapshotF1() { + when(_vmDao.findById(anyLong())).thenReturn(vmMock); + when(vmMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.XenServer); + Snapshot snapshot = _snapshotMgr.backupSnapshotFromVmSnapshot(TEST_SNAPSHOT_ID, TEST_VM_ID, TEST_VOLUME_ID, TEST_VM_SNAPSHOT_ID); + Assert.assertNull(snapshot); + } + + // vm on KVM, first time + @Test + public void testBackupSnapshotFromVmSnapshotF2() { + when(_vmDao.findById(anyLong())).thenReturn(vmMock); + when(vmMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(_vmSnapshotDao.findById(anyLong())).thenReturn(vmSnapshotMock); + when(_snapshotStoreDao.findParent(any(DataStoreRole.class), anyLong(), anyLong())).thenReturn(null); + when(snapshotFactory.getSnapshot(anyLong(), Mockito.any(DataStore.class))).thenReturn(snapshotInfoMock); + when(storeMock.create(snapshotInfoMock)).thenReturn(snapshotInfoMock); + when(_snapshotStoreDao.findBySnapshot(anyLong(), any(DataStoreRole.class))).thenReturn(snapshotStoreMock); + when(_snapshotStoreDao.update(anyLong(), any(SnapshotDataStoreVO.class))).thenReturn(true); + when(_snapshotDao.update(anyLong(), any(SnapshotVO.class))).thenReturn(true); + when(vmMock.getAccountId()).thenReturn(2L); + when(snapshotStrategy.backupSnapshot(any(SnapshotInfo.class))).thenReturn(snapshotInfoMock);;; + + Snapshot snapshot = _snapshotMgr.backupSnapshotFromVmSnapshot(TEST_SNAPSHOT_ID, TEST_VM_ID, TEST_VOLUME_ID, TEST_VM_SNAPSHOT_ID); + Assert.assertNotNull(snapshot); + } + + // vm on KVM, already backed up + @Test(expected = InvalidParameterValueException.class) + public void testBackupSnapshotFromVmSnapshotF3() { + when(_vmDao.findById(anyLong())).thenReturn(vmMock); + when(vmMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(_vmSnapshotDao.findById(anyLong())).thenReturn(vmSnapshotMock); + when(_snapshotStoreDao.findParent(any(DataStoreRole.class), anyLong(), anyLong())).thenReturn(snapshotStoreMock); + when(snapshotStoreMock.getInstallPath()).thenReturn("VM_SNAPSHOT_NAME"); + when(vmSnapshotMock.getName()).thenReturn("VM_SNAPSHOT_NAME"); + Snapshot snapshot = _snapshotMgr.backupSnapshotFromVmSnapshot(TEST_SNAPSHOT_ID, TEST_VM_ID, TEST_VOLUME_ID, TEST_VM_SNAPSHOT_ID); + Assert.assertNull(snapshot); + } + } diff --git a/setup/db/db/schema-4920to41000.sql b/setup/db/db/schema-4920to41000.sql index ef1a63cd45e..b8b54e393d0 100644 --- a/setup/db/db/schema-4920to41000.sql +++ b/setup/db/db/schema-4920to41000.sql @@ -53,3 +53,12 @@ ALTER TABLE `cloud`.`image_store_details` CHANGE COLUMN `value` `value` VARCHAR( NULL DEFAULT '1' COMMENT 'True if the detail can be displayed to the end user' AFTER `value`; ALTER TABLE `snapshots` ADD COLUMN `location_type` VARCHAR(32) COMMENT 'Location of snapshot (ex. Primary)'; + +-- Database change for CLOUDSTACK-8746 (VM Snapshotting implementation for KVM) +UPDATE `cloud`.`hypervisor_capabilities` SET `vm_snapshot_enabled` = 1 WHERE `hypervisor_type` ='KVM' AND `hypervisor_version` = 'default'; + +-- [VM-SNAPSHOT] add role permissions for new API command createSnapshotFromVMSnapshot +INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, `sort_order`) values (UUID(), 2, 'createSnapshotFromVMSnapshot', 'ALLOW', 318) ON DUPLICATE KEY UPDATE rule=rule; +INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, `sort_order`) values (UUID(), 3, 'createSnapshotFromVMSnapshot', 'ALLOW', 302) ON DUPLICATE KEY UPDATE rule=rule; +INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, `sort_order`) values (UUID(), 4, 'createSnapshotFromVMSnapshot', 'ALLOW', 260) ON DUPLICATE KEY UPDATE rule=rule; + diff --git a/test/integration/smoke/test_vm_snapshots.py b/test/integration/smoke/test_vm_snapshots.py index 5fcb80a8aed..31b6822afbc 100644 --- a/test/integration/smoke/test_vm_snapshots.py +++ b/test/integration/smoke/test_vm_snapshots.py @@ -42,7 +42,7 @@ class TestVmSnapshot(cloudstackTestCase): cls._cleanup = [] cls.unsupportedHypervisor = False cls.hypervisor = testClient.getHypervisorInfo() - if cls.hypervisor.lower() in (KVM.lower(), "hyperv", "lxc"): + if cls.hypervisor.lower() in ("hyperv", "lxc"): cls.unsupportedHypervisor = True return diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 16ee0b7baad..950c22a61b6 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -2830,6 +2830,10 @@ div.detail-group.actions td { background-position: -82px -686px; } +#navigation ul li.vmsnapshots span.icon { + background: url(../images/sprites.png) no-repeat -34px -666px; +} + #navigation ul li.affinityGroups span.icon { background-position: -73px -87px; } @@ -12822,6 +12826,7 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it } .revertSnapshot .icon, +.revertToVMSnapshot .icon, .restoreVM .icon, .restore .icon, .recover .icon { @@ -12838,6 +12843,7 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it } .revertSnapshot:hover .icon, +.revertToVMSnapshot:hover .icon, .restoreVM:hover .icon, .restore:hover .icon { background-position: -168px -613px; diff --git a/ui/l10n/en.js b/ui/l10n/en.js index 075a0d801f1..f6669ca823f 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -1865,6 +1865,7 @@ var dictionary = {"ICMP.code":"ICMP Code", "message.action.stop.systemvm":"Please confirm that you want to stop this system VM.", "message.action.take.snapshot":"Please confirm that you want to take a snapshot of this volume.", "message.action.unmanage.cluster":"Please confirm that you want to unmanage the cluster.", +"message.action.vmsnapshot.create":"Please confirm that you want to take a snapshot of this instance.
Please notice that the instance will be paused during the snapshoting, and resumed after snapshotting, if it runs on KVM.", "message.action.vmsnapshot.delete":"Please confirm that you want to delete this VM snapshot.", "message.action.vmsnapshot.revert":"Revert VM snapshot", "message.activate.project":"Are you sure you want to activate this project?", @@ -2281,4 +2282,4 @@ var dictionary = {"ICMP.code":"ICMP Code", "state.detached":"Detached", "title.upload.volume":"Upload Volume", "ui.listView.filters.all":"All", -"ui.listView.filters.mine":"Mine"}; \ No newline at end of file +"ui.listView.filters.mine":"Mine"}; diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 19db2570296..ad70fc2bf89 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -28,6 +28,7 @@ addRow: 'false', createForm: { title: 'label.action.vmsnapshot.create', + desc: 'message.action.vmsnapshot.create', fields: { name: { label: 'label.name', @@ -423,7 +424,7 @@ path: 'storage.volumes', label: 'label.volumes' }, { - path: 'vmsnapshots', + path: 'storage.vmsnapshots', label: 'label.snapshots' }, { path: 'affinityGroups', @@ -2701,8 +2702,7 @@ allowedActions.push("stop"); allowedActions.push("restart"); - if ((jsonObj.hypervisor != 'KVM' || g_kvmsnapshotenabled == true) - && (jsonObj.hypervisor != 'LXC')) { + if (jsonObj.hypervisor != 'LXC') { allowedActions.push("snapshot"); } @@ -2738,8 +2738,7 @@ allowedActions.push("destroy"); allowedActions.push("reinstall"); - if ((jsonObj.hypervisor != 'KVM' || g_kvmsnapshotenabled == true) - && (jsonObj.hypervisor != 'LXC')) { + if (jsonObj.hypervisor != 'KVM' && jsonObj.hypervisor != 'LXC') { allowedActions.push("snapshot"); } diff --git a/ui/scripts/storage.js b/ui/scripts/storage.js index b26e60ba36e..0f51f31c74d 100644 --- a/ui/scripts/storage.js +++ b/ui/scripts/storage.js @@ -2330,7 +2330,350 @@ } } } - } + }, + }, + + /** + * VM Snapshots + */ + vmsnapshots: { + type: 'select', + title: 'label.vmsnapshot', + listView: { + id: 'vmsnapshots', + isMaximized: true, + fields: { + displayname: { + label: 'label.name' + }, + state: { + label: 'label.state', + indicator: { + 'Ready': 'on', + 'Error': 'off' + } + }, + type: { + label: 'label.vmsnapshot.type' + }, + current: { + label: 'label.vmsnapshot.current', + converter: cloudStack.converters.toBooleanText + }, + parentName: { + label: 'label.vmsnapshot.parentname' + }, + created: { + label: 'label.date', + converter: cloudStack.converters.toLocalDate + } + }, + + advSearchFields: { + name: { + label: 'label.name' + }, + + domainid: { + label: 'label.domain', + select: function(args) { + if (isAdmin() || isDomainAdmin()) { + $.ajax({ + url: createURL('listDomains'), + data: { + listAll: true, + details: 'min' + }, + success: function(json) { + var array1 = [{ + id: '', + description: '' + }]; + var domains = json.listdomainsresponse.domain; + if (domains != null && domains.length > 0) { + for (var i = 0; i < domains.length; i++) { + array1.push({ + id: domains[i].id, + description: domains[i].path + }); + } + } + array1.sort(function(a, b) { + return a.description.localeCompare(b.description); + }); + args.response.success({ + data: array1 + }); + } + }); + } else { + args.response.success({ + data: null + }); + } + }, + isHidden: function(args) { + if (isAdmin() || isDomainAdmin()) + return false; + else + return true; + } + }, + + account: { + label: 'label.account', + isHidden: function(args) { + if (isAdmin() || isDomainAdmin()) + return false; + else + return true; + } + }, + tagKey: { + label: 'label.tag.key' + }, + tagValue: { + label: 'label.tag.value' + } + }, + + dataProvider: function(args) { + var data = {}; + listViewDataProvider(args, data); + + if (args.context != null) { + if ("instances" in args.context) { + $.extend(data, { + virtualMachineId: args.context.instances[0].id + }); + } + } + $.ajax({ + url: createURL('listVMSnapshot&listAll=true'), + data: data, + dataType: "json", + async: true, + success: function(json) { + var jsonObj; + jsonObj = json.listvmsnapshotresponse.vmSnapshot; + args.response.success({ + actionFilter: vmSnapshotActionfilter, + data: jsonObj + }); + } + }); + }, + //dataProvider end + detailView: { + tabs: { + details: { + title: 'label.details', + fields: { + id: { + label: 'label.id' + }, + name: { + label: 'label.name' + }, + displayname: { + label: 'label.display.name' + }, + type: { + label: 'label.vmsnapshot.type' + }, + description: { + label: 'label.description' + }, + state: { + label: 'label.state', + indicator: { + 'Ready': 'on', + 'Error': 'off' + } + }, + current: { + label: 'label.vmsnapshot.current', + converter: cloudStack.converters.toBooleanText + }, + parentName: { + label: 'label.vmsnapshot.parentname' + }, + domain: { + label: 'label.domain' + }, + account: { + label: 'label.account' + }, + virtualmachineid: { + label: 'label.vm.id' + }, + created: { + label: 'label.date', + converter: cloudStack.converters.toLocalDate + } + }, + dataProvider: function(args) { + $.ajax({ + url: createURL("listVMSnapshot&listAll=true&vmsnapshotid=" + args.context.vmsnapshots[0].id), + dataType: "json", + async: true, + success: function(json) { + var jsonObj; + jsonObj = json.listvmsnapshotresponse.vmSnapshot[0]; + args.response.success({ + actionFilter: vmSnapshotActionfilter, + data: jsonObj + }); + } + }); + }, + tags: cloudStack.api.tags({ + resourceType: 'VMSnapshot', + contextId: 'vmsnapshots' + }) + } + }, + actions: { + //delete a snapshot + remove: { + label: 'label.action.vmsnapshot.delete', + messages: { + confirm: function(args) { + return 'message.action.vmsnapshot.delete'; + }, + notification: function(args) { + return 'label.action.vmsnapshot.delete'; + } + }, + action: function(args) { + $.ajax({ + url: createURL("deleteVMSnapshot&vmsnapshotid=" + args.context.vmsnapshots[0].id), + dataType: "json", + async: true, + success: function(json) { + var jid = json.deletevmsnapshotresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + }, + notification: { + poll: pollAsyncJobResult + } + }, + revertToVMSnapshot: { + label: 'label.action.vmsnapshot.revert', + messages: { + confirm: function(args) { + return 'label.action.vmsnapshot.revert'; + }, + notification: function(args) { + return 'message.action.vmsnapshot.revert'; + } + }, + action: function(args) { + $.ajax({ + url: createURL("revertToVMSnapshot&vmsnapshotid=" + args.context.vmsnapshots[0].id), + dataType: "json", + async: true, + success: function(json) { + var jid = json.reverttovmsnapshotresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + + }, + notification: { + poll: pollAsyncJobResult + } + }, + takeSnapshot: { + label: 'Create Snapshot From VM Snapshot', + messages: { + confirm: function(args) { + return 'Please confirm that you want to create a volume snapshot from the vm snapshot.'; + }, + notification: function(args) { + return 'Volume snapshot is created from vm snapshot'; + } + }, + createForm: { + title: 'label.action.take.snapshot', + desc: 'message.action.take.snapshot', + fields: { + name: { + label: 'label.name', + }, + volume: { + label: 'label.volume', + validation: { + required: true + }, + select: function(args) { + $.ajax({ + url: createURL("listVolumes&virtualMachineId=" + args.context.vmsnapshots[0].virtualmachineid), + dataType: "json", + async: true, + success: function(json) { + var volumes = json.listvolumesresponse.volume; + var items = []; + $(volumes).each(function() { + items.push({ + id: this.id, + description: this.name + }); + }); + args.response.success({ + data: items + }); + + } + }); + } + } + } + }, + action: function(args) { + var data = { + volumeid: args.data.volume, + vmsnapshotid: args.context.vmsnapshots[0].id + }; + if (args.data.name != null && args.data.name.length > 0) { + $.extend(data, { + name: args.data.name + }); + } + $.ajax({ + url: createURL("createSnapshotFromVMSnapshot"), + data: data, + dataType: "json", + async: true, + success: function(json) { + var jid = json.createsnapshotfromvmsnapshotresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + + }, + notification: { + poll: pollAsyncJobResult + } + } + } + } + //detailview end + } } } }; @@ -2422,6 +2765,23 @@ } allowedActions.push("remove"); + return allowedActions; + }; + + var vmSnapshotActionfilter = cloudStack.actionFilter.vmSnapshotActionfilter = function(args) { + var jsonObj = args.context.item; + + if (jsonObj.state == 'Error') { + return ["remove"]; + } + + var allowedActions = []; + if (jsonObj.state == "Ready") { + allowedActions.push("remove"); + allowedActions.push("revertToVMSnapshot"); + allowedActions.push("takeSnapshot"); + } + return allowedActions; } diff --git a/ui/scripts/vm_snapshots.js b/ui/scripts/vm_snapshots.js deleted file mode 100644 index 98d7e37e852..00000000000 --- a/ui/scripts/vm_snapshots.js +++ /dev/null @@ -1,198 +0,0 @@ -// 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. -(function($, cloudStack) { - cloudStack.sections.vmsnapshots = { - title: 'label.vmsnapshot', - id: 'vmsnapshots', - listView: { - id: 'vmsnapshots', - isMaximized: true, - fields: { - displayname: { - label: 'label.name' - }, - state: { - label: 'label.state', - indicator: { - 'Ready': 'on', - 'Error': 'off' - } - }, - type: { - label: 'label.vmsnapshot.type' - }, - current: { - label: 'label.vmsnapshot.current', - converter: cloudStack.converters.toBooleanText - }, - parentName: { - label: 'label.vmsnapshot.parentname' - }, - created: { - label: 'label.date', - converter: cloudStack.converters.toLocalDate - } - }, - - dataProvider: function(args) { - var apiCmd = "listVMSnapshot&listAll=true"; - if (args.context != null) { - if ("instances" in args.context) { - apiCmd += "&virtualmachineid=" + args.context.instances[0].id; - } - } - $.ajax({ - url: createURL(apiCmd), - dataType: "json", - async: true, - success: function(json) { - var jsonObj; - jsonObj = json.listvmsnapshotresponse.vmSnapshot; - args.response.success({ - data: jsonObj - }); - } - }); - }, - //dataProvider end - detailView: { - tabs: { - details: { - title: 'label.details', - fields: { - id: { - label: 'label.id' - }, - name: { - label: 'label.name' - }, - displayname: { - label: 'label.display.name' - }, - type: { - label: 'label.vmsnapshot.type' - }, - description: { - label: 'label.description' - }, - state: { - label: 'label.state', - indicator: { - 'Ready': 'on', - 'Error': 'off' - } - }, - current: { - label: 'label.vmsnapshot.current', - converter: cloudStack.converters.toBooleanText - }, - parentName: { - label: 'label.vmsnapshot.parentname' - }, - created: { - label: 'label.date', - converter: cloudStack.converters.toLocalDate - } - }, - dataProvider: function(args) { - $.ajax({ - url: createURL("listVMSnapshot&listAll=true&vmsnapshotid=" + args.context.vmsnapshots[0].id), - dataType: "json", - async: true, - success: function(json) { - var jsonObj; - jsonObj = json.listvmsnapshotresponse.vmSnapshot[0]; - args.response.success({ - //actionFilter: vmActionfilter, - data: jsonObj - }); - } - }); - }, - tags: cloudStack.api.tags({ - resourceType: 'VMSnapshot', - contextId: 'vmsnapshots' - }) - } - }, - actions: { - //delete a snapshot - remove: { - label: 'label.action.vmsnapshot.delete', - messages: { - confirm: function(args) { - return 'message.action.vmsnapshot.delete'; - }, - notification: function(args) { - return 'label.action.vmsnapshot.delete'; - } - }, - action: function(args) { - $.ajax({ - url: createURL("deleteVMSnapshot&vmsnapshotid=" + args.context.vmsnapshots[0].id), - dataType: "json", - async: true, - success: function(json) { - var jid = json.deletevmsnapshotresponse.jobid; - args.response.success({ - _custom: { - jobId: jid - } - }); - } - }); - }, - notification: { - poll: pollAsyncJobResult - } - }, - restart: { - label: 'label.action.vmsnapshot.revert', - messages: { - confirm: function(args) { - return 'label.action.vmsnapshot.revert'; - }, - notification: function(args) { - return 'message.action.vmsnapshot.revert'; - } - }, - action: function(args) { - $.ajax({ - url: createURL("revertToVMSnapshot&vmsnapshotid=" + args.context.vmsnapshots[0].id), - dataType: "json", - async: true, - success: function(json) { - var jid = json.reverttovmsnapshotresponse.jobid; - args.response.success({ - _custom: { - jobId: jid - } - }); - } - }); - - }, - notification: { - poll: pollAsyncJobResult - } - } - } - } - //detailview end - } - } -})(jQuery, cloudStack);