diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index d385fa9ed07f..0122319cd3db 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -316,6 +316,8 @@ public class EventTypes { public static final String EVENT_VOLUME_UPDATE = "VOLUME.UPDATE"; public static final String EVENT_VOLUME_DESTROY = "VOLUME.DESTROY"; public static final String EVENT_VOLUME_RECOVER = "VOLUME.RECOVER"; + public static final String EVENT_VOLUME_IMPORT = "VOLUME.IMPORT"; + public static final String EVENT_VOLUME_UNMANAGE = "VOLUME.UNMANAGE"; public static final String EVENT_VOLUME_CHANGE_DISK_OFFERING = "VOLUME.CHANGE.DISK.OFFERING"; // Domains diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index f10769d73190..d817b8b03243 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -117,6 +117,7 @@ public class ApiConstants { public static final String CURRENT_START_IP = "currentstartip"; public static final String CURRENT_END_IP = "currentendip"; public static final String ENCRYPT = "encrypt"; + public static final String ENCRYPT_FORMAT = "encryptformat"; public static final String ENCRYPT_ROOT = "encryptroot"; public static final String ENCRYPTION_SUPPORTED = "encryptionsupported"; public static final String MIN_IOPS = "miniops"; @@ -191,6 +192,7 @@ public class ApiConstants { public static final String FORMAT = "format"; public static final String FOR_VIRTUAL_NETWORK = "forvirtualnetwork"; public static final String FOR_SYSTEM_VMS = "forsystemvms"; + public static final String FULL_PATH = "fullpath"; public static final String GATEWAY = "gateway"; public static final String IP6_GATEWAY = "ip6gateway"; public static final String GROUP = "group"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ImportVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ImportVolumeCmd.java new file mode 100644 index 000000000000..cfd49222f66e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ImportVolumeCmd.java @@ -0,0 +1,178 @@ +// 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.admin.volume; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DiskOfferingResponse; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.StoragePoolResponse; +import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.storage.volume.VolumeImportUnmanageService; + +import javax.inject.Inject; + +@APICommand(name = "importVolume", + description = "Import an unmanaged volume from a storage pool on a host into CloudStack", + responseObject = VolumeResponse.class, + responseView = ResponseObject.ResponseView.Full, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.Admin}, + since = "4.19.1") +public class ImportVolumeCmd extends BaseAsyncCmd { + + @Inject + public VolumeImportUnmanageService volumeImportService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + + @Parameter(name = ApiConstants.PATH, + type = BaseCmd.CommandType.STRING, + required = true, + description = "the path of the volume") + private String path; + + @Parameter(name = ApiConstants.STORAGE_ID, + type = BaseCmd.CommandType.UUID, + required = true, + entityType = StoragePoolResponse.class, + description = "the ID of the storage pool") + private Long storageId; + + @Parameter(name = ApiConstants.DISK_OFFERING_ID, + type = BaseCmd.CommandType.UUID, + entityType = DiskOfferingResponse.class, + description = "the ID of the disk offering linked to the volume") + private Long diskOfferingId; + + @Parameter(name = ApiConstants.ACCOUNT, + type = BaseCmd.CommandType.STRING, + description = "an optional account for the virtual machine. Must be used with domainId.") + private String accountName; + + @Parameter(name = ApiConstants.DOMAIN_ID, + type = BaseCmd.CommandType.UUID, + entityType = DomainResponse.class, + description = "import instance to the domain specified") + private Long domainId; + + @Parameter(name = ApiConstants.PROJECT_ID, + type = BaseCmd.CommandType.UUID, + entityType = ProjectResponse.class, + description = "import instance for the project") + private Long projectId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Long getStorageId() { + return storageId; + } + + public void setStorageId(Long storageId) { + this.storageId = storageId; + } + + public Long getDiskOfferingId() { + return diskOfferingId; + } + + public void setDiskOfferingId(Long diskOfferingId) { + this.diskOfferingId = diskOfferingId; + } + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public Long getDomainId() { + return domainId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public Long getProjectId() { + return projectId; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VOLUME_IMPORT; + } + + @Override + public String getEventDescription() { + return String.format("Importing unmanaged Volume with path: %s", path); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + VolumeResponse response = volumeImportService.importVolume(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + Account caller = CallContext.current().getCallingAccount(); + return caller.getAccountId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ListVolumesForImportCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ListVolumesForImportCmd.java new file mode 100644 index 000000000000..6dbadf3c1cb9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ListVolumesForImportCmd.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 org.apache.cloudstack.api.command.admin.volume; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.StoragePoolResponse; +import org.apache.cloudstack.api.response.VolumeForImportResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.storage.volume.VolumeImportUnmanageService; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; + +import javax.inject.Inject; + +@APICommand(name = "listVolumesForImport", + description = "Lists unmanaged volumes on a storage pool", + responseObject = VolumeForImportResponse.class, + responseView = ResponseObject.ResponseView.Full, + entityType = {VolumeOnStorageTO.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.Admin}, + since = "4.19.1") +public class ListVolumesForImportCmd extends BaseListCmd { + + @Inject + public VolumeImportUnmanageService volumeImportService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.STORAGE_ID, + type = BaseCmd.CommandType.UUID, + required = true, + entityType = StoragePoolResponse.class, + description = "the ID of the storage pool") + private Long storageId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getStorageId() { + return storageId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + ListResponse response = volumeImportService.listVolumesForImport(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + Account account = CallContext.current().getCallingAccount(); + if (account != null) { + return account.getId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmd.java new file mode 100644 index 000000000000..773f587f43b0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmd.java @@ -0,0 +1,127 @@ +// +// 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.admin.volume; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.storage.Volume; +import com.cloud.user.Account; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.storage.volume.VolumeImportUnmanageService; + +import javax.inject.Inject; + +@APICommand(name = "unmanageVolume", + description = "Unmanage a volume on storage pool.", + entityType = {Volume.class}, + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, + authorized = {RoleType.Admin}, + since = "4.19.1") +public class UnmanageVolumeCmd extends BaseAsyncCmd { + + @Inject + public VolumeImportUnmanageService volumeImportService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = VolumeResponse.class, + required = true, + description = "The ID of the volume to unmanage") + private Long volumeId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + + public Long getVolumeId() { + return volumeId; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VOLUME_UNMANAGE; + } + + @Override + public String getEventDescription() { + return String.format("unmanaging Volume with ID %s", volumeId); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, + ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = volumeImportService.unmanageVolume(volumeId); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to unmanage the volume"); + } + + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getLocalizedMessage()); + } + } + + @Override + public long getEntityOwnerId() { + Volume volume = _responseGenerator.findVolumeById(volumeId); + if (volume != null) { + return volume.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.Volume; + } + + @Override + public Long getApiResourceId() { + return volumeId; + } + +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VolumeForImportResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VolumeForImportResponse.java new file mode 100644 index 000000000000..045a7b85e0d3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/VolumeForImportResponse.java @@ -0,0 +1,140 @@ +// 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.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; + +import java.util.Map; + +@EntityReference(value = VolumeOnStorageTO.class) +public class VolumeForImportResponse extends BaseResponse { + + @SerializedName(ApiConstants.NAME) + @Param(description = "the name of the volume") + private String name; + + @SerializedName(ApiConstants.PATH) + @Param(description = "the path of the volume") + private String path; + + @SerializedName(ApiConstants.FULL_PATH) + @Param(description = "the full path of the volume") + private String fullPath; + + @SerializedName(ApiConstants.FORMAT) + @Param(description = "the format of the volume") + private String format; + + @SerializedName(ApiConstants.SIZE) + @Param(description = "the size of the volume") + private long size; + + @SerializedName(ApiConstants.VIRTUAL_SIZE) + @Param(description = "the virtual size of the volume") + private long virtualSize; + + @SerializedName(ApiConstants.ENCRYPT_FORMAT) + @Param(description = "the encrypt format of the volume") + private String qemuEncryptFormat; + + @SerializedName(ApiConstants.DETAILS) + @Param(description = "volume details in key/value pairs.") + private Map details; + + @SerializedName(ApiConstants.CHAIN_INFO) + @Param(description = "the chain info of the volume") + String chainInfo; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getFullPath() { + return fullPath; + } + + public void setFullPath(String fullPath) { + this.fullPath = fullPath; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public long getVirtualSize() { + return virtualSize; + } + + public void setVirtualSize(long virtualSize) { + this.virtualSize = virtualSize; + } + + public String getQemuEncryptFormat() { + return qemuEncryptFormat; + } + + public void setQemuEncryptFormat(String qemuEncryptFormat) { + this.qemuEncryptFormat = qemuEncryptFormat; + } + + public Map getDetails() { + return details; + } + + public void setDetails(Map details) { + this.details = details; + } + + public String getChainInfo() { + return chainInfo; + } + + public void setChainInfo(String chainInfo) { + this.chainInfo = chainInfo; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java new file mode 100644 index 000000000000..f908fd9c9a4d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java @@ -0,0 +1,35 @@ +// 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.volume; + +import com.cloud.utils.component.PluggableService; +import org.apache.cloudstack.api.command.admin.volume.ListVolumesForImportCmd; +import org.apache.cloudstack.api.command.admin.volume.ImportVolumeCmd; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.VolumeForImportResponse; +import org.apache.cloudstack.api.response.VolumeResponse; + +public interface VolumeImportUnmanageService extends PluggableService { + + ListResponse listVolumesForImport(ListVolumesForImportCmd cmd); + + VolumeResponse importVolume(ImportVolumeCmd cmd); + + boolean unmanageVolume(long volumeId); + +} diff --git a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeOnStorageTO.java b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeOnStorageTO.java new file mode 100644 index 000000000000..80e93c9f4225 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeOnStorageTO.java @@ -0,0 +1,137 @@ +// 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.volume; + +import com.cloud.hypervisor.Hypervisor; + +import java.util.HashMap; +import java.util.Map; + +public class VolumeOnStorageTO { + Hypervisor.HypervisorType hypervisorType; + private String path; + private String fullPath; + + private String name; + + private String format; + private long size; + private long virtualSize; + private String qemuEncryptFormat; + private Map details = new HashMap<>(); + + public enum Detail { + BACKING_FILE, BACKING_FILE_FORMAT, CLUSTER_SIZE, FILE_FORMAT, IS_LOCKED + } + + public VolumeOnStorageTO() { + } + + public VolumeOnStorageTO(Hypervisor.HypervisorType hypervisorType, String path, String name, String fullPath, String format, long size, long virtualSize) { + this.hypervisorType = hypervisorType; + this.path = path; + this.name = name; + this.fullPath = fullPath; + this.format = format; + this.size = size; + this.virtualSize = virtualSize; + } + + public VolumeOnStorageTO(Hypervisor.HypervisorType hypervisorType, String path, String name, long size) { + this.hypervisorType = hypervisorType; + this.path = path; + this.name = name; + this.size = size; + } + + public Hypervisor.HypervisorType getHypervisorType() { + return hypervisorType; + } + + public void setHypervisorType(Hypervisor.HypervisorType hypervisorType) { + this.hypervisorType = hypervisorType; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getFullPath() { + return fullPath; + } + + public void setFullPath(String fullPath) { + this.fullPath = fullPath; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public long getVirtualSize() { + return virtualSize; + } + + public void setVirtualSize(long virtualSize) { + this.virtualSize = virtualSize; + } + + public String getQemuEncryptFormat() { + return qemuEncryptFormat; + } + + public void setQemuEncryptFormat(String qemuEncryptFormat) { + this.qemuEncryptFormat = qemuEncryptFormat; + } + + public Map getDetails() { + return details; + } + + public void setDetails(Map details) { + this.details = details; + } + + public void addDetail(Detail detail, String value) { + details.put(detail, value); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/storage/volume/VolumeOnStorageTOTest.java b/api/src/test/java/org/apache/cloudstack/storage/volume/VolumeOnStorageTOTest.java new file mode 100644 index 000000000000..b36c6d9d8731 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/storage/volume/VolumeOnStorageTOTest.java @@ -0,0 +1,48 @@ +// 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.volume; + +import com.cloud.hypervisor.Hypervisor; +import org.junit.Assert; +import org.junit.Test; + +public class VolumeOnStorageTOTest { + + private static String path = "path"; + private static String name = "name"; + private static String fullPath = "fullPath"; + private static String format = "qcow2"; + private static long size = 10; + private static long virtualSize = 20; + private static String encryptFormat = "LUKS"; + + @Test + public void testVolumeOnStorageTO() { + VolumeOnStorageTO volumeOnStorageTO = new VolumeOnStorageTO(Hypervisor.HypervisorType.KVM, path, name, fullPath, + format, size, virtualSize); + volumeOnStorageTO.setQemuEncryptFormat(encryptFormat); + + Assert.assertEquals(path, volumeOnStorageTO.getPath()); + Assert.assertEquals(name, volumeOnStorageTO.getName()); + Assert.assertEquals(fullPath, volumeOnStorageTO.getFullPath()); + Assert.assertEquals(format, volumeOnStorageTO.getFormat()); + Assert.assertEquals(size, volumeOnStorageTO.getSize()); + Assert.assertEquals(virtualSize, volumeOnStorageTO.getVirtualSize()); + Assert.assertEquals(encryptFormat, volumeOnStorageTO.getQemuEncryptFormat()); + Assert.assertEquals(path, volumeOnStorageTO.getPath()); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/GetVolumesOnStorageAnswer.java b/core/src/main/java/com/cloud/agent/api/GetVolumesOnStorageAnswer.java new file mode 100644 index 000000000000..89f256867c29 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/GetVolumesOnStorageAnswer.java @@ -0,0 +1,42 @@ +// 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 org.apache.cloudstack.storage.volume.VolumeOnStorageTO; + +import java.util.List; + +public class GetVolumesOnStorageAnswer extends Answer { + private List volumes; + + GetVolumesOnStorageAnswer() { + } + + public GetVolumesOnStorageAnswer(GetVolumesOnStorageCommand cmd, List volumes) { + super(cmd, true, null); + this.volumes = volumes; + } + + public GetVolumesOnStorageAnswer(final GetVolumesOnStorageCommand cmd, final boolean success, final String details) { + super(cmd, success, details); + } + + public List getVolumes() { + return volumes; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/GetVolumesOnStorageCommand.java b/core/src/main/java/com/cloud/agent/api/GetVolumesOnStorageCommand.java new file mode 100644 index 000000000000..8fbbac914b36 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/GetVolumesOnStorageCommand.java @@ -0,0 +1,49 @@ +// +// 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 com.cloud.agent.api.to.StorageFilerTO; + +public class GetVolumesOnStorageCommand extends Command { + + StorageFilerTO pool; + private String volumePath; //filter by file path + + public GetVolumesOnStorageCommand() { + } + + public GetVolumesOnStorageCommand(StorageFilerTO pool, String filePath) { + this.pool = pool; + this.volumePath = filePath; + } + + public StorageFilerTO getPool() { + return pool; + } + + public String getVolumePath() { + return volumePath; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/test/java/com/cloud/agent/api/GetVolumesOnStorageAnswerTest.java b/core/src/test/java/com/cloud/agent/api/GetVolumesOnStorageAnswerTest.java new file mode 100644 index 000000000000..92205559826d --- /dev/null +++ b/core/src/test/java/com/cloud/agent/api/GetVolumesOnStorageAnswerTest.java @@ -0,0 +1,66 @@ +// 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 com.cloud.hypervisor.Hypervisor; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +public class GetVolumesOnStorageAnswerTest { + + private static String path = "path"; + private static String name = "name"; + private static String fullPath = "fullPath"; + private static String format = "qcow2"; + private static long size = 10; + private static long virtualSize = 20; + private static String encryptFormat = "LUKS"; + + private static GetVolumesOnStorageCommand command = Mockito.mock(GetVolumesOnStorageCommand.class); + + @Test + public void testGetVolumesOnStorageAnswer() { + VolumeOnStorageTO volumeOnStorageTO = new VolumeOnStorageTO(Hypervisor.HypervisorType.KVM, path, name, fullPath, + format, size, virtualSize); + volumeOnStorageTO.setQemuEncryptFormat(encryptFormat); + + List volumesOnStorageTO = new ArrayList<>(); + volumesOnStorageTO.add(volumeOnStorageTO); + + GetVolumesOnStorageAnswer answer = new GetVolumesOnStorageAnswer(command, volumesOnStorageTO); + List volumes = answer.getVolumes(); + + Assert.assertEquals(1, volumes.size()); + VolumeOnStorageTO volume = volumes.get(0); + + Assert.assertEquals(Hypervisor.HypervisorType.KVM, volume.getHypervisorType()); + Assert.assertEquals(path, volume.getPath()); + Assert.assertEquals(name, volume.getName()); + Assert.assertEquals(fullPath, volume.getFullPath()); + Assert.assertEquals(format, volume.getFormat()); + Assert.assertEquals(size, volume.getSize()); + Assert.assertEquals(virtualSize, volume.getVirtualSize()); + Assert.assertEquals(encryptFormat, volume.getQemuEncryptFormat()); + Assert.assertEquals(path, volume.getPath()); + } + +} diff --git a/core/src/test/java/com/cloud/agent/api/GetVolumesOnStorageCommandTest.java b/core/src/test/java/com/cloud/agent/api/GetVolumesOnStorageCommandTest.java new file mode 100644 index 000000000000..fba772b9d74e --- /dev/null +++ b/core/src/test/java/com/cloud/agent/api/GetVolumesOnStorageCommandTest.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; + +import com.cloud.agent.api.to.StorageFilerTO; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class GetVolumesOnStorageCommandTest { + + final StorageFilerTO pool = Mockito.mock(StorageFilerTO.class); + + final String localPath = "localPath"; + final String volumePath = "volumePath"; + + @Test + public void testGetVolumesOnStorageCommand() { + GetVolumesOnStorageCommand command = new GetVolumesOnStorageCommand(pool, volumePath); + + Assert.assertEquals(pool, command.getPool()); + Assert.assertEquals(volumePath, command.getVolumePath()); + } +} diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java index c4fbc2505aa4..74ededaf1f4b 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java @@ -165,7 +165,8 @@ List allocateTemplatedVolumes(Type type, String name, DiskOffering * @param chainInfo chain info for the volume. Hypervisor specific. * @return DiskProfile of imported volume */ - DiskProfile importVolume(Type type, String name, DiskOffering offering, Long sizeInBytes, Long minIops, Long maxIops, VirtualMachine vm, VirtualMachineTemplate template, + DiskProfile importVolume(Type type, String name, DiskOffering offering, Long sizeInBytes, Long minIops, Long maxIops, + Long zoneId, HypervisorType hypervisorType, VirtualMachine vm, VirtualMachineTemplate template, Account owner, Long deviceId, Long poolId, String path, String chainInfo); DiskProfile updateImportedVolume(Type type, DiskOffering offering, VirtualMachine vm, VirtualMachineTemplate template, diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index 5a3b56c2c5d1..579bf7bc5379 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -2188,7 +2188,7 @@ public void updateVolumeDiskChain(long volumeId, String path, String chainInfo, @Override public DiskProfile importVolume(Type type, String name, DiskOffering offering, Long sizeInBytes, Long minIops, Long maxIops, - VirtualMachine vm, VirtualMachineTemplate template, Account owner, + Long zoneId, HypervisorType hypervisorType, VirtualMachine vm, VirtualMachineTemplate template, Account owner, Long deviceId, Long poolId, String path, String chainInfo) { if (sizeInBytes == null) { sizeInBytes = offering.getDiskSize(); @@ -2197,9 +2197,10 @@ public DiskProfile importVolume(Type type, String name, DiskOffering offering, L minIops = minIops != null ? minIops : offering.getMinIops(); maxIops = maxIops != null ? maxIops : offering.getMaxIops(); - VolumeVO vol = new VolumeVO(type, name, vm.getDataCenterId(), owner.getDomainId(), owner.getId(), offering.getId(), offering.getProvisioningType(), sizeInBytes, minIops, maxIops, null); + VolumeVO vol = new VolumeVO(type, name, zoneId, owner.getDomainId(), owner.getId(), offering.getId(), offering.getProvisioningType(), sizeInBytes, minIops, maxIops, null); if (vm != null) { vol.setInstanceId(vm.getId()); + vol.setAttached(new Date()); } if (deviceId != null) { @@ -2222,17 +2223,16 @@ public DiskProfile importVolume(Type type, String name, DiskOffering offering, L } // display flag matters only for the User vms - if (VirtualMachine.Type.User.equals(vm.getType())) { + if (vm != null && VirtualMachine.Type.User.equals(vm.getType())) { UserVmVO userVm = _userVmDao.findById(vm.getId()); vol.setDisplayVolume(userVm.isDisplayVm()); } - vol.setFormat(getSupportedImageFormatForCluster(vm.getHypervisorType())); + vol.setFormat(getSupportedImageFormatForCluster(hypervisorType)); vol.setPoolId(poolId); vol.setPath(path); vol.setChainInfo(chainInfo); vol.setState(Volume.State.Ready); - vol.setAttached(new Date()); vol = _volsDao.persist(vol); return toDiskProfile(vol, offering); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java new file mode 100644 index 000000000000..f4b9cb1de798 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java @@ -0,0 +1,149 @@ +// +// 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.GetVolumesOnStorageAnswer; +import com.cloud.agent.api.GetVolumesOnStorageCommand; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage.StoragePoolType; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.commons.lang3.StringUtils; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@ResourceWrapper(handles = GetVolumesOnStorageCommand.class) +public final class LibvirtGetVolumesOnStorageCommandWrapper extends CommandWrapper { + + static final List SUPPORTED_STORAGE_POOL_TYPES = Arrays.asList(StoragePoolType.NetworkFilesystem, + StoragePoolType.Filesystem, StoragePoolType.RBD); + + @Override + public Answer execute(final GetVolumesOnStorageCommand command, final LibvirtComputingResource libvirtComputingResource) { + + final StorageFilerTO pool = command.getPool(); + if (!SUPPORTED_STORAGE_POOL_TYPES.contains(pool.getType())) { + return new GetVolumesOnStorageAnswer(command, false, String.format("pool type %s is unsupported", pool.getType())); + } + final String volumePath = command.getVolumePath(); + + List volumes = new ArrayList<>(); + + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + KVMStoragePool storagePool = storagePoolMgr.getStoragePool(pool.getType(), pool.getUuid(), true); + + if (volumePath != null) { + KVMPhysicalDisk disk = storagePool.getPhysicalDisk(volumePath); + if (disk != null) { + if (!isDiskFormatSupported(disk)) { + return new GetVolumesOnStorageAnswer(command, false, String.format("disk format %s is unsupported", disk.getFormat())); + } + Map info = getDiskFileInfo(storagePool, disk, true); + if (info == null) { + return new GetVolumesOnStorageAnswer(command, false, "failed to get information of disk file. The disk might be locked or unsupported"); + } + VolumeOnStorageTO volumeOnStorageTO = new VolumeOnStorageTO(Hypervisor.HypervisorType.KVM, disk.getName(), disk.getName(), disk.getPath(), + disk.getFormat().toString(), disk.getSize(), disk.getVirtualSize()); + if (disk.getQemuEncryptFormat() != null) { + volumeOnStorageTO.setQemuEncryptFormat(disk.getQemuEncryptFormat().toString()); + } + String backingFilePath = info.get(QemuImg.BACKING_FILE); + if (StringUtils.isNotBlank(backingFilePath)) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.BACKING_FILE, backingFilePath); + } + String backingFileFormat = info.get(QemuImg.BACKING_FILE_FORMAT); + if (StringUtils.isNotBlank(backingFileFormat)) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.BACKING_FILE_FORMAT, backingFileFormat); + } + String clusterSize = info.get(QemuImg.CLUSTER_SIZE); + if (StringUtils.isNotBlank(clusterSize)) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.CLUSTER_SIZE, clusterSize); + } + String fileFormat = info.get(QemuImg.FILE_FORMAT); + if (StringUtils.isNotBlank(clusterSize)) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.FILE_FORMAT, fileFormat); + } + Boolean isLocked = isDiskFileLocked(storagePool, disk); + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.IS_LOCKED, String.valueOf(isLocked)); + volumes.add(volumeOnStorageTO); + } + } else { + for (KVMPhysicalDisk disk: storagePool.listPhysicalDisks()) { + if (!isDiskFormatSupported(disk)) { + continue; + } + VolumeOnStorageTO volumeOnStorageTO = new VolumeOnStorageTO(Hypervisor.HypervisorType.KVM, disk.getName(), disk.getName(), disk.getPath(), + disk.getFormat().toString(), disk.getSize(), disk.getVirtualSize()); + if (disk.getQemuEncryptFormat() != null) { + volumeOnStorageTO.setQemuEncryptFormat(disk.getQemuEncryptFormat().toString()); + } + volumes.add(volumeOnStorageTO); + } + } + + return new GetVolumesOnStorageAnswer(command, volumes); + } + + private boolean isDiskFormatSupported(KVMPhysicalDisk disk) { + return PhysicalDiskFormat.QCOW2.equals(disk.getFormat()) || PhysicalDiskFormat.RAW.equals(disk.getFormat()); + } + + private boolean isDiskFileLocked(KVMStoragePool pool, KVMPhysicalDisk disk) { + if (PhysicalDiskFormat.QCOW2.equals(disk.getFormat())) { + Map info = getDiskFileInfo(pool, disk, false); + return info == null; + } + return false; // unknown + } + + private Map getDiskFileInfo(KVMStoragePool pool, KVMPhysicalDisk disk, boolean secure) { + try { + QemuImg qemu = new QemuImg(0); + QemuImgFile qemuFile = new QemuImgFile(disk.getPath(), disk.getFormat()); + if (StoragePoolType.RBD.equals(pool.getType())) { + String rbdDestFile = KVMPhysicalDisk.RBDStringBuilder(pool.getSourceHost(), + pool.getSourcePort(), + pool.getAuthUserName(), + pool.getAuthSecret(), + disk.getPath()); + qemuFile = new QemuImgFile(rbdDestFile, disk.getFormat()); + } + return qemu.info(qemuFile, secure); + } catch (QemuImgException | LibvirtException ex) { + logger.error("Failed to get info of disk file: " + ex.getMessage()); + } + return null; + } +} 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 1ddc16c89453..37cbadca290d 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 @@ -555,9 +555,13 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, Strin * @return A HashMap with string key-value information as returned by 'qemu-img info'. */ public Map info(final QemuImgFile file) throws QemuImgException { + return info(file, true); + } + + public Map info(final QemuImgFile file, boolean secure) throws QemuImgException { final Script s = new Script(_qemuImgPath); s.add("info"); - if (this.version >= QEMU_2_10) { + if (this.version >= QEMU_2_10 && secure) { s.add("-U"); } s.add(file.getFileName()); diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java index 3a551fce7ebb..e8d14f45fea6 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java @@ -48,6 +48,8 @@ import javax.naming.ConfigurationException; import javax.xml.datatype.XMLGregorianCalendar; +import com.cloud.agent.api.GetVolumesOnStorageAnswer; +import com.cloud.agent.api.GetVolumesOnStorageCommand; import com.cloud.hypervisor.vmware.mo.HostDatastoreBrowserMO; import com.vmware.vim25.FileInfo; import com.vmware.vim25.FileQueryFlags; @@ -66,6 +68,7 @@ import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; import org.apache.cloudstack.vm.UnmanagedInstanceTO; import org.apache.commons.collections.CollectionUtils; @@ -525,6 +528,8 @@ public Answer executeRequest(Command cmd) { answer = execute((DeleteStoragePoolCommand) cmd); } else if (clz == CopyVolumeCommand.class) { answer = execute((CopyVolumeCommand) cmd); + } else if (clz == GetVolumesOnStorageCommand.class) { + answer = execute((GetVolumesOnStorageCommand) cmd); } else if (clz == AttachIsoCommand.class) { answer = execute((AttachIsoCommand) cmd); } else if (clz == ValidateSnapshotCommand.class) { @@ -5935,6 +5940,55 @@ public CopyVolumeAnswer execute(CopyVolumeCommand cmd) { } } + private GetVolumesOnStorageAnswer execute(GetVolumesOnStorageCommand cmd) { + List volumes = new ArrayList<>(); + try { + VmwareHypervisorHost hyperHost = getHyperHost(getServiceContext()); + StorageFilerTO pool = cmd.getPool(); + + if (!Arrays.asList(StoragePoolType.NetworkFilesystem, StoragePoolType.VMFS, StoragePoolType.PreSetup, + StoragePoolType.DatastoreCluster).contains(pool.getType())) { + throw new Exception("Unsupported storage pool type " + pool.getType()); + } + + ManagedObjectReference morDatastore = HypervisorHostHelper.findDatastoreWithBackwardsCompatibility(hyperHost, pool.getUuid()); + + if (morDatastore == null) { + boolean vmfsDatastore = Arrays.asList(StoragePoolType.VMFS, StoragePoolType.PreSetup, StoragePoolType.DatastoreCluster).contains(pool.getType()); + morDatastore = hyperHost.mountDatastore(vmfsDatastore, pool.getHost(), pool.getPort(), pool.getPath(), pool.getUuid().replace("-", ""), true); + } + + assert (morDatastore != null); + + DatastoreMO dsMo = new DatastoreMO(getServiceContext(), morDatastore); + + HostDatastoreBrowserMO browserMo = dsMo.getHostDatastoreBrowserMO(); + FileQueryFlags fqf = new FileQueryFlags(); + fqf.setFileSize(true); + fqf.setFileType(true); + fqf.setModification(true); + fqf.setFileOwner(false); + + HostDatastoreBrowserSearchSpec spec = new HostDatastoreBrowserSearchSpec(); + spec.setSearchCaseInsensitive(true); + spec.setDetails(fqf); + + String dsPath = String.format("[%s]", dsMo.getName()); + + HostDatastoreBrowserSearchResults results = browserMo.searchDatastore(dsPath, spec); + List fileInfoList = results.getFile(); + for (FileInfo file : fileInfoList) { + VolumeOnStorageTO volumeOnStorageTO = new VolumeOnStorageTO(HypervisorType.VMware, file.getPath(), + file.getFriendlyName(), file.getFileSize()); + volumes.add(volumeOnStorageTO); + } + } catch (Exception e) { + return new GetVolumesOnStorageAnswer(cmd, false, e.getMessage()); + + } + return new GetVolumesOnStorageAnswer(cmd, volumes); + } + @Override public void disconnected() { } diff --git a/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanagedManagerImpl.java b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanagedManagerImpl.java new file mode 100644 index 000000000000..dc525e0d1812 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanagedManagerImpl.java @@ -0,0 +1,392 @@ +// 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.volume; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.GetVolumesOnStorageAnswer; +import com.cloud.agent.api.GetVolumesOnStorageCommand; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.configuration.ConfigurationManager; +import com.cloud.configuration.Resource; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventUtils; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.offering.DiskOffering; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.ScopeType; +import com.cloud.storage.Storage; +import com.cloud.storage.StoragePoolHostVO; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.storage.dao.VMTemplatePoolDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.ResourceLimitService; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.DiskProfile; + +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.volume.ImportVolumeCmd; +import org.apache.cloudstack.api.command.admin.volume.ListVolumesForImportCmd; +import org.apache.cloudstack.api.command.admin.volume.UnmanageVolumeCmd; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.VolumeForImportResponse; +import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class VolumeImportUnmanagedManagerImpl implements VolumeImportUnmanageService { + protected Logger logger = LogManager.getLogger(VolumeImportUnmanagedManagerImpl.class); + + private static final List volumeImportUnmanageSupportedHypervisors = + Arrays.asList(Hypervisor.HypervisorType.KVM); + + @Inject + private AccountManager accountMgr; + @Inject + private AgentManager agentManager; + @Inject + private HostDao hostDao; + @Inject + private DiskOfferingDao diskOfferingDao; + @Inject + private ResourceLimitService resourceLimitService; + @Inject + private ResponseGenerator responseGenerator; + @Inject + private VolumeDao volumeDao; + @Inject + private PrimaryDataStoreDao primaryDataStoreDao; + @Inject + private StoragePoolHostDao storagePoolHostDao; + @Inject + private ConfigurationManager configMgr; + @Inject + private DataCenterDao dcDao; + @Inject + private VolumeOrchestrationService volumeManager; + @Inject + private VMTemplatePoolDao templatePoolDao; + + private static final String DEFAULT_DISK_OFFERING_NAME = "Default Custom Offering for Volume Import"; + private static final String DEFAULT_DISK_OFFERING_UNIQUE_NAME = "Custom-Offering-Volume-Import"; + + private void logFailureAndThrowException(String msg) { + logger.error(msg); + throw new CloudRuntimeException(msg); + } + + @Override + public List> getCommands() { + final List> cmdList = new ArrayList<>(); + cmdList.add(ListVolumesForImportCmd.class); + cmdList.add(ImportVolumeCmd.class); + cmdList.add(UnmanageVolumeCmd.class); + return cmdList; + } + + @Override + public ListResponse listVolumesForImport(ListVolumesForImportCmd cmd) { + Long poolId = cmd.getStorageId(); + + List volumes = listVolumesForImportInternal(poolId, null); + StoragePoolVO pool = checkIfPoolAvailable(poolId); + + List responses = new ArrayList<>(); + for (VolumeOnStorageTO volume : volumes) { + if (checkIfVolumeManaged(pool, volume.getPath()) || checkIfVolumeForTemplate(pool, volume.getPath())) { + continue; + } + responses.add(createVolumeForImportResponse(volume, pool)); + } + ListResponse listResponses = new ListResponse<>(); + listResponses.setResponses(responses, responses.size()); + return listResponses; + } + + @Override + public VolumeResponse importVolume(ImportVolumeCmd cmd) { + // 1. verify owner + final Account caller = CallContext.current().getCallingAccount(); + if (caller.getType() != Account.Type.ADMIN) { + throw new PermissionDeniedException(String.format("Cannot import VM as the caller account [%s] is not ROOT Admin.", caller.getUuid())); + } + Account owner = accountMgr.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); + if (owner == null) { + logFailureAndThrowException("Cannot import volume due to unknown owner"); + } + + // 2. check if pool exists and not in maintenance + Long poolId = cmd.getStorageId(); + StoragePoolVO pool = checkIfPoolAvailable(poolId); + + // 3. check if the volume already exists in cloudstack by path + String volumePath = cmd.getPath(); + if (checkIfVolumeManaged(pool, volumePath)){ + logFailureAndThrowException("Volume is already managed by CloudStack: " + volumePath); + } + if (checkIfVolumeForTemplate(pool, volumePath)) { + logFailureAndThrowException("Volume is a base image of a template: " + volumePath); + } + + // 4. send a command to hypervisor to check + List volumes = listVolumesForImportInternal(poolId, volumePath); + if (CollectionUtils.isEmpty(volumes)) { + logFailureAndThrowException("Cannot find volume on storage pool: " + volumePath); + } + + VolumeOnStorageTO volume = volumes.get(0); + + // 5. check resource limitation + checkResourceLimitForImportVolume(owner, volume); + + // 6. get disk offering + DiskOfferingVO diskOffering = getOrCreateDiskOffering(owner, cmd.getDiskOfferingId(), pool.getDataCenterId()); + + // 7. create records + VolumeVO volumeVO = createRecordsForVolumeImport(volume, diskOffering, owner, pool); + + // 8. Update resource count + updateResourceLimitForVolumeImport(volumeVO); + + // 9. Publish event + publicUsageEventForVolumeImportAndUnmanage(volumeVO, true); + + return responseGenerator.createVolumeResponse(ResponseObject.ResponseView.Full, volumeVO); + } + + private List listVolumesForImportInternal(Long poolId, String volumePath) { + StoragePoolVO pool = checkIfPoolAvailable(poolId); + + Pair hostAndLocalPath = findHostAndLocalPathForVolumeImport(pool); + HostVO host = hostAndLocalPath.first(); + if (!volumeImportUnmanageSupportedHypervisors.contains(host.getHypervisorType())) { + logFailureAndThrowException("Import VM is not supported for hypervisor: " + host.getHypervisorType()); + } + + StorageFilerTO storageTO = new StorageFilerTO(pool); + GetVolumesOnStorageCommand command = new GetVolumesOnStorageCommand(storageTO, volumePath); + Answer answer = agentManager.easySend(host.getId(), command); + if (answer == null || !(answer instanceof GetVolumesOnStorageAnswer)) { + logFailureAndThrowException("Cannot get volumes on storage pool via host " + host.getName()); + } + if (!answer.getResult()) { + logFailureAndThrowException("Volume cannot be imported due to " + answer.getDetails()); + } + List volumes = ((GetVolumesOnStorageAnswer) answer).getVolumes(); + return volumes; + } + + @Override + public boolean unmanageVolume(long volumeId) { + // 1. check if volume can be unmanaged + VolumeVO volume = checkIfVolumeCanBeUnmanaged(volumeId); + + // 2. check if pool available + StoragePoolVO pool = checkIfPoolAvailable(volume.getPoolId()); + + // 3. Update resource count + updateResourceLimitForVolumeUnmanage(volume); + + // 4. publish events + publicUsageEventForVolumeImportAndUnmanage(volume, false); + + // 5. update the state/removed of record + unmanageVolumeFromDatabase(volume); + + return true; + } + + private StoragePoolVO checkIfPoolAvailable(Long poolId) { + StoragePoolVO pool = primaryDataStoreDao.findById(poolId); + if (pool == null) { + logFailureAndThrowException("Storage pool does not exist: ID = " + poolId); + } + if (pool.isInMaintenance()) { + logFailureAndThrowException("Storage pool is in maintenance: " + pool.getName()); + } + return pool; + } + + private Pair findHostAndLocalPathForVolumeImport(StoragePoolVO pool) { + List hosts = new ArrayList<>(); + if (ScopeType.HOST.equals(pool.getScope())) { + List storagePoolHostVOs = storagePoolHostDao.listByPoolId(pool.getId()); + if (CollectionUtils.isNotEmpty(storagePoolHostVOs)) { + for (StoragePoolHostVO storagePoolHostVO : storagePoolHostVOs) { + HostVO host = hostDao.findById(storagePoolHostVO.getHostId()); + if (host != null) { + return new Pair<>(host, storagePoolHostVO.getLocalPath()); + } + } + } + } else if (ScopeType.CLUSTER.equals(pool.getScope())) { + hosts = hostDao.findHypervisorHostInCluster((pool.getClusterId())); + } else if (ScopeType.ZONE.equals(pool.getScope())) { + hosts = hostDao.listAllHostsUpByZoneAndHypervisor(pool.getDataCenterId(), pool.getHypervisor()); + } + for (HostVO host : hosts) { + StoragePoolHostVO storagePoolHostVO = storagePoolHostDao.findByPoolHost(pool.getId(), host.getId()); + if (storagePoolHostVO != null) { + return new Pair<>(host, storagePoolHostVO.getLocalPath()); + } + } + logFailureAndThrowException("No host found to perform volume import"); + return null; + } + + private VolumeForImportResponse createVolumeForImportResponse(VolumeOnStorageTO volume, StoragePoolVO pool) { + VolumeForImportResponse response = new VolumeForImportResponse(); + response.setPath(volume.getPath()); + response.setName(volume.getName()); + response.setFullPath(volume.getFullPath()); + response.setFormat(volume.getFormat()); // TODO: always qcow2 for kvm, which is incorrect + response.setSize(volume.getSize()); + response.setVirtualSize(volume.getVirtualSize()); + response.setQemuEncryptFormat(volume.getQemuEncryptFormat()); + // TODO: add more information of storage pool + + response.setObjectName("volumeforimport"); + return response; + } + + private boolean checkIfVolumeManaged(StoragePoolVO pool, String volumePath) { + return volumeDao.findByPoolIdAndPath(pool.getId(), volumePath) != null; + } + + private boolean checkIfVolumeForTemplate(StoragePoolVO pool, String volumePath) { + return templatePoolDao.findByPoolPath(pool.getId(), volumePath) != null; + } + + private DiskOfferingVO getOrCreateDiskOffering(Account owner, Long diskOfferingId, Long zoneId) { + if (diskOfferingId != null) { + // check if disk offering exists and active + DiskOfferingVO diskOfferingVO = diskOfferingDao.findById(diskOfferingId); + if (diskOfferingVO == null) { + logFailureAndThrowException(String.format("Disk offering does not exist", diskOfferingId)); + } + if (!DiskOffering.State.Active.equals(diskOfferingVO.getState())) { + logFailureAndThrowException(String.format("Disk offering with ID %s is not active", diskOfferingId)); + } + // check if disk offering is accessible by the account/owner + try { + configMgr.checkDiskOfferingAccess(owner, diskOfferingVO, dcDao.findById(zoneId)); + return diskOfferingVO; + } catch (PermissionDeniedException ignored) { + logFailureAndThrowException(String.format("Disk offering with ID %s is not accessible by account %s", diskOfferingId, owner.getAccountName())); + } + } + return getOrCreateDefaultDiskOfferingIdForVolumeImport(); + } + + + private DiskOfferingVO getOrCreateDefaultDiskOfferingIdForVolumeImport() { + DiskOfferingVO diskOffering = diskOfferingDao.findByUniqueName(DEFAULT_DISK_OFFERING_UNIQUE_NAME); + if (diskOffering != null) { + return diskOffering; + } + DiskOfferingVO newDiskOffering = new DiskOfferingVO(DEFAULT_DISK_OFFERING_NAME, DEFAULT_DISK_OFFERING_NAME, + Storage.ProvisioningType.THIN, 0, null, true, null, null, null); + newDiskOffering.setUniqueName(DEFAULT_DISK_OFFERING_UNIQUE_NAME); + newDiskOffering = diskOfferingDao.persistDefaultDiskOffering(newDiskOffering); + return newDiskOffering; + } + + private VolumeVO createRecordsForVolumeImport(VolumeOnStorageTO volume, DiskOfferingVO diskOffering, + Account owner, StoragePoolVO pool) { + DiskProfile diskProfile = volumeManager.importVolume(Volume.Type.DATADISK, volume.getName(), diskOffering, + volume.getVirtualSize(), null, null, pool.getDataCenterId(), pool.getHypervisor(), null, null, + owner, null, pool.getId(), volume.getPath(), null); + return volumeDao.findById(diskProfile.getVolumeId()); + } + + private void checkResourceLimitForImportVolume(Account owner, VolumeOnStorageTO volume) { + Long volumeSize = volume.getSize(); + try { + resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.volume, 1); + resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.primary_storage, volumeSize); + } catch (ResourceAllocationException e) { + logger.error(String.format("VM resource allocation error for account: %s", owner.getUuid()), e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM resource allocation error for account: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage()))); + } + } + + private void updateResourceLimitForVolumeImport(VolumeVO volumeVO) { + resourceLimitService.incrementResourceCount(volumeVO.getAccountId(), Resource.ResourceType.volume); + resourceLimitService.incrementResourceCount(volumeVO.getAccountId(), Resource.ResourceType.primary_storage, volumeVO.getSize()); + } + + private void publicUsageEventForVolumeImportAndUnmanage(VolumeVO volumeVO, boolean isImport) { + try { + String eventType = isImport ? EventTypes.EVENT_VOLUME_IMPORT: EventTypes.EVENT_VOLUME_UNMANAGE; + UsageEventUtils.publishUsageEvent(eventType, volumeVO.getAccountId(), volumeVO.getDataCenterId(), + volumeVO.getId(), volumeVO.getName(), volumeVO.getDiskOfferingId(), null, volumeVO.getSize(), + Volume.class.getName(), volumeVO.getUuid(), volumeVO.isDisplayVolume()); + } catch (Exception e) { + logger.error(String.format("Failed to publish volume ID: %s usage records during volume import/unmanage", volumeVO.getUuid()), e); + } + } + + private void updateResourceLimitForVolumeUnmanage(VolumeVO volumeVO) { + resourceLimitService.decrementResourceCount(volumeVO.getAccountId(), Resource.ResourceType.volume); + resourceLimitService.decrementResourceCount(volumeVO.getAccountId(), Resource.ResourceType.primary_storage, volumeVO.getSize()); + } + + private VolumeVO checkIfVolumeCanBeUnmanaged(long volumeId) { + VolumeVO volumeVO = volumeDao.findById(volumeId); + if (volumeVO == null) { + logFailureAndThrowException(String.format("Volume (ID: %s) does not exist", volumeId)); + } + if (!Volume.State.Ready.equals(volumeVO.getState())) { + logFailureAndThrowException(String.format("Volume (ID: %s) is not ready", volumeId)); + } + if (volumeVO.getAttached() != null || volumeVO.getInstanceId() != null) { + logFailureAndThrowException(String.format("Volume (ID: %s) is attached to VM (ID: %s)", volumeId, volumeVO.getInstanceId())); + } + return volumeVO; + } + + private boolean unmanageVolumeFromDatabase(VolumeVO volume) { + volumeDao.remove(volume.getId()); + return true; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 8118fa148067..1636c70f6da4 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -852,7 +852,7 @@ private Pair importDisk(UnmanagedInstanceTO.Disk disk, } StoragePool storagePool = getStoragePool(disk, zone, cluster); DiskProfile profile = volumeManager.importVolume(type, name, diskOffering, diskSize, - minIops, maxIops, vm, template, owner, deviceId, storagePool.getId(), path, chainInfo); + minIops, maxIops, vm.getDataCenterId(), vm.getHypervisorType(), vm, template, owner, deviceId, storagePool.getId(), path, chainInfo); return new Pair(profile, storagePool); } diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index c80c2947a997..cb2cc28c7778 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -362,4 +362,6 @@ + + diff --git a/test/integration/smoke/test_import_unmanage_volumes.py b/test/integration/smoke/test_import_unmanage_volumes.py new file mode 100644 index 000000000000..c2ea416536e8 --- /dev/null +++ b/test/integration/smoke/test_import_unmanage_volumes.py @@ -0,0 +1,168 @@ +# 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. +""" Tests for importVolume and unmanageVolume APIs +""" +# Import Local Modules +from marvin.cloudstackAPI import unmanageVolume, listVolumesForImport, importVolume +from marvin.cloudstackTestCase import cloudstackTestCase, unittest +from marvin.codes import FAILED +from marvin.lib.base import (Account, + Domain, + Volume, + ServiceOffering, + DiskOffering, + VirtualMachine) +from marvin.lib.common import (get_domain, get_zone, get_suitable_test_template) + +# Import System modules +from nose.plugins.attrib import attr + +_multiprocess_shared_ = True + + +class TestImportAndUnmanageVolumes(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + testClient = super(TestImportAndUnmanageVolumes, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.testdata = cls.testClient.getParsedTestDataConfig() + + cls.services = testClient.getParsedTestDataConfig() + cls.hypervisor = testClient.getHypervisorInfo() + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient) + cls._cleanup = [] + + cls.service_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["tiny"] + ) + cls._cleanup.append(cls.service_offering) + + template = get_suitable_test_template( + cls.apiclient, + cls.zone.id, + cls.services["ostype"], + cls.hypervisor + ) + if template == FAILED: + assert False, "get_test_template() failed to return template" + + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + cls.services["mode"] = cls.zone.networktype + + cls.disk_offering = DiskOffering.create(cls.apiclient, + cls.services["disk_offering"]) + cls._cleanup.append(cls.disk_offering) + + cls.test_domain = Domain.create( + cls.apiclient, + cls.services["domain"]) + cls._cleanup.append(cls.test_domain) + + cls.test_account = Account.create( + cls.apiclient, + cls.services["account"], + admin=True, + domainid=cls.test_domain.id) + cls._cleanup.append(cls.test_account) + + # Create VM + cls.virtual_machine = VirtualMachine.create( + cls.apiclient, + cls.services["virtual_machine"], + templateid=template.id, + accountid=cls.test_account.name, + domainid=cls.test_account.domainid, + serviceofferingid=cls.service_offering.id, + mode=cls.services["mode"] + ) + cls._cleanup.append(cls.virtual_machine) + + cls.virtual_machine.stop(cls.apiclient, forced=True) + + @classmethod + def tearDownClass(cls): + super(TestImportAndUnmanageVolumes, cls).tearDownClass() + + def setUp(self): + if self.testClient.getHypervisorInfo().lower() != "kvm": + raise unittest.SkipTest("This is only available for KVM") + + @attr(tags=['advanced', 'basic', 'sg'], required_hardware=False) + def test_01_detach_unmanage_import_volume(self): + """Test listing Volumes with account & domain filter + """ + # Create DATA volume + volume = Volume.create( + self.apiclient, + self.testdata["volume"], + zoneid=self.zone.id, + account=self.test_account.name, + domainid=self.test_account.domainid, + diskofferingid=self.disk_offering.id + ) + + # Attach and Detach volume + try: + self.virtual_machine.attach_volume(self.apiclient, volume) + except Exception as e: + self.fail("Attach volume failed with Exception: %s" % e) + + self.virtual_machine.detach_volume(self.apiclient, volume) + + # List volume by id + volumes = Volume.list(self.apiclient, + id = volume.id) + self.assertTrue(isinstance(volumes, list), + "listVolumes response should return a valid list" + ) + self.assertTrue(len(volumes) > 0, + "listVolumes response should return a non-empty list" + ) + volume = volumes[0] + + # Unmanage volume + cmd = unmanageVolume.unmanageVolumeCmd() + cmd.id = volume.id + self.apiclient.unmanageVolume(cmd) + + # List VMs for import + cmd = listVolumesForImport.listVolumesForImportCmd() + cmd.storageid = volume.storageid + volumesForImport = self.apiclient.listVolumesForImport(cmd) + self.assertTrue(isinstance(volumesForImport, list), + "Check listVolumesForImport response returns a valid list" + ) + + # Import volume + cmd = importVolume.importVolumeCmd() + cmd.storageid = volume.storageid + cmd.path = volume.path + self.apiclient.importVolume(cmd) + + # List volume by name + volumes = Volume.list(self.apiclient, + storageid = volume.storageid, + name=volume.path) + self.assertTrue(isinstance(volumes, list), + "listVolumes response should return a valid list" + ) + self.assertTrue(len(volumes) > 0, + "listVolumes response should return a non-empty list" + ) \ No newline at end of file