From 7b45d2e1184a3b74a6b7e9cb8f21a98e9054da8c Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 27 Jan 2026 23:58:09 +0530 Subject: [PATCH] wip: changes for imagetransfer handling Signed-off-by: Abhishek Kumar --- .../com/cloud/storage/VolumeApiService.java | 9 + .../backup/IncrementalBackupService.java | 2 + .../cloudstack/veeam/VeeamControlServlet.java | 9 +- .../veeam/adapter/UserResourceAdapter.java | 330 ++++++++++++++++++ .../veeam/api/DisksRouteHandler.java | 38 +- .../veeam/api/ImageTransfersRouteHandler.java | 126 +++++++ ...ageTransferVOToImageTransferConverter.java | 88 +++++ .../VolumeJoinVOToDiskConverter.java | 4 +- .../cloudstack/veeam/api/dto/Actions.java | 4 +- .../cloudstack/veeam/api/dto/Backup.java | 36 ++ .../apache/cloudstack/veeam/api/dto/Disk.java | 3 + .../veeam/api/dto/ImageTransfer.java | 202 +++++++++++ .../{ActionLink.java => ImageTransfers.java} | 24 +- .../{ResponseMapper.java => Mapper.java} | 4 +- .../veeam/utils/ResponseWriter.java | 4 +- .../spring-veeam-control-service-context.xml | 6 +- .../veeam/VeeamControlServiceImplTest.java | 24 ++ .../cloud/api/query/dao/VolumeJoinDao.java | 3 + .../api/query/dao/VolumeJoinDaoImpl.java | 13 + .../cloud/storage/VolumeApiServiceImpl.java | 112 +++--- .../backup/IncrementalBackupServiceImpl.java | 42 ++- .../storage/VolumeApiServiceImplTest.java | 12 +- .../resource/NfsSecondaryStorageResource.java | 7 +- 23 files changed, 980 insertions(+), 122 deletions(-) create mode 100644 plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/UserResourceAdapter.java create mode 100644 plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandler.java create mode 100644 plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/ImageTransferVOToImageTransferConverter.java create mode 100644 plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Backup.java create mode 100644 plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ImageTransfer.java rename plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/{ActionLink.java => ImageTransfers.java} (65%) rename plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/{ResponseMapper.java => Mapper.java} (97%) create mode 100644 plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServiceImplTest.java diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index 1a9bcc6ee98..b74f230d2fb 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -22,6 +22,7 @@ import java.net.MalformedURLException; import java.util.List; import java.util.Map; +import com.cloud.dc.DataCenter; import com.cloud.exception.ResourceAllocationException; import com.cloud.offering.DiskOffering; import com.cloud.user.Account; @@ -70,6 +71,10 @@ public interface VolumeApiService { */ Volume allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationException; + Volume allocVolume(long ownerId, Long zoneId, Long diskOfferingId, Long vmId, Long snapshotId, String name, + Long cmdSize, Boolean displayVolume, Long cmdMinIops, Long cmdMaxIops, String customId) + throws ResourceAllocationException; + /** * Creates the volume based on the given criteria * @@ -80,6 +85,8 @@ public interface VolumeApiService { */ Volume createVolume(CreateVolumeCmd cmd); + Volume createVolume(long volumeId, Long vmId, Long snapshotId, Long storageId, Boolean display); + /** * Resizes the volume based on the given criteria * @@ -203,4 +210,6 @@ public interface VolumeApiService { Pair checkAndRepairVolume(CheckAndRepairVolumeCmd cmd) throws ResourceAllocationException; Long getVolumePhysicalSize(Storage.ImageFormat format, String path, String chainInfo); + + Long getCustomDiskOfferingIdForVolumeUpload(Account owner, DataCenter zone); } diff --git a/api/src/main/java/org/apache/cloudstack/backup/IncrementalBackupService.java b/api/src/main/java/org/apache/cloudstack/backup/IncrementalBackupService.java index 02c079626b4..28f69cc38ad 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/IncrementalBackupService.java +++ b/api/src/main/java/org/apache/cloudstack/backup/IncrementalBackupService.java @@ -55,6 +55,8 @@ public interface IncrementalBackupService extends PluggableService { */ ImageTransferResponse createImageTransfer(CreateImageTransferCmd cmd); + ImageTransfer createImageTransfer(long volumeId, Long backupId, ImageTransfer.Direction direction); + /** * Finalize an image transfer * Marks transfer as complete (NBD is closed globally in finalize backup) diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java index 7c38e4cf249..7ebff969981 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java @@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.cloudstack.veeam.utils.Negotiation; -import org.apache.cloudstack.veeam.utils.ResponseMapper; +import org.apache.cloudstack.veeam.utils.Mapper; import org.apache.cloudstack.veeam.utils.ResponseWriter; import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; @@ -38,11 +38,12 @@ public class VeeamControlServlet extends HttpServlet { private static final Logger LOGGER = LogManager.getLogger(VeeamControlServlet.class); private final ResponseWriter writer; + private final Mapper mapper; private final List routeHandlers; public VeeamControlServlet(List routeHandlers) { this.routeHandlers = routeHandlers; - ResponseMapper mapper = new ResponseMapper(); + mapper = new Mapper(); writer = new ResponseWriter(mapper); } @@ -50,6 +51,10 @@ public class VeeamControlServlet extends HttpServlet { return writer; } + public Mapper getMapper() { + return mapper; + } + @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { String method = req.getMethod(); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/UserResourceAdapter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/UserResourceAdapter.java new file mode 100644 index 00000000000..4be60562797 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/UserResourceAdapter.java @@ -0,0 +1,330 @@ +// 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.veeam.adapter; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.RoleService; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.Rule; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.command.user.job.QueryAsyncJobResultCmd; +import org.apache.cloudstack.api.command.user.network.ListNetworksCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.api.command.user.vm.ListVMsCmd; +import org.apache.cloudstack.api.command.user.vm.StartVMCmd; +import org.apache.cloudstack.api.command.user.vm.StopVMCmd; +import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.DeleteVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.DetachVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.ListVolumesCmd; +import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; +import org.apache.cloudstack.backup.ImageTransfer.Direction; +import org.apache.cloudstack.backup.ImageTransferVO; +import org.apache.cloudstack.backup.IncrementalBackupService; +import org.apache.cloudstack.backup.dao.ImageTransferDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.veeam.api.converter.ImageTransferVOToImageTransferConverter; +import org.apache.cloudstack.veeam.api.converter.VolumeJoinVOToDiskConverter; +import org.apache.cloudstack.veeam.api.dto.Disk; +import org.apache.cloudstack.veeam.api.dto.ImageTransfer; +import org.apache.cloudstack.veeam.api.dto.Ref; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import com.cloud.api.query.dao.HostJoinDao; +import com.cloud.api.query.dao.VolumeJoinDao; +import com.cloud.api.query.vo.HostJoinVO; +import com.cloud.api.query.vo.VolumeJoinVO; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.org.Grouping; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeApiService; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.user.AccountVO; +import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.dao.AccountDao; +import com.cloud.utils.EnumUtils; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; + +public class UserResourceAdapter extends ManagerBase { + private static final String SERVICE_ACCOUNT_NAME = "veemserviceuser"; + private static final String SERVICE_ACCOUNT_ROLE_NAME = "Veeam Service Role"; + private static final String SERVICE_ACCOUNT_FIRST_NAME = "Veeam"; + private static final String SERVICE_ACCOUNT_LAST_NAME = "Service User"; + private static final List> SERVICE_ACCOUNT_ROLE_ALLOWED_APIS = Arrays.asList( + QueryAsyncJobResultCmd.class, + ListVMsCmd.class, + DeployVMCmd.class, + StartVMCmd.class, + StopVMCmd.class, + DestroyVMCmd.class, + ListVolumesCmd.class, + CreateVolumeCmd.class, + DeleteVolumeCmd.class, + AttachVolumeCmd.class, + DetachVolumeCmd.class, + ResizeVolumeCmd.class, + ListNetworksCmd.class + ); + + @Inject + DataCenterDao dataCenterDao; + + @Inject + RoleService roleService; + + @Inject + AccountService accountService; + + @Inject + AccountDao accountDao; + + @Inject + VolumeJoinDao volumeJoinDao; + + @Inject + VolumeApiService volumeApiService; + + @Inject + PrimaryDataStoreDao primaryDataStoreDao; + + @Inject + ImageTransferDao imageTransferDao; + + @Inject + HostJoinDao hostJoinDao; + + @Inject + IncrementalBackupService incrementalBackupService; + + protected Role createServiceAccountRole() { + Role role = roleService.createRole(SERVICE_ACCOUNT_ROLE_NAME, RoleType.User, + SERVICE_ACCOUNT_ROLE_NAME, false); + for (Class allowedApi : SERVICE_ACCOUNT_ROLE_ALLOWED_APIS) { + final String apiName = BaseCmd.getCommandNameByClass(allowedApi); + roleService.createRolePermission(role, new Rule(apiName), RolePermissionEntity.Permission.ALLOW, + String.format("Allow %s", apiName)); + } + roleService.createRolePermission(role, new Rule("*"), RolePermissionEntity.Permission.DENY, + "Deny all"); + logger.debug("Created default role for Veeam service account in projects: {}", role); + return role; + } + + public Role getServiceAccountRole() { + List roles = roleService.findRolesByName(SERVICE_ACCOUNT_ROLE_NAME); + if (CollectionUtils.isNotEmpty(roles)) { + Role role = roles.get(0); + logger.debug("Found default role for Veeam service account in projects: {}", role); + return role; + } + return createServiceAccountRole(); + } + + protected Account createServiceAccount() { + CallContext.register(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM); + try { + Role role = getServiceAccountRole(); + UserAccount userAccount = accountService.createUserAccount(SERVICE_ACCOUNT_NAME, + UUID.randomUUID().toString(), SERVICE_ACCOUNT_FIRST_NAME, + SERVICE_ACCOUNT_LAST_NAME, null, null, SERVICE_ACCOUNT_NAME, Account.Type.NORMAL, role.getId(), + 1L, null, null, null, null, User.Source.NATIVE); + Account account = accountService.getAccount(userAccount.getAccountId()); + logger.debug("Created Veeam service account: {}", account); + return account; + } finally { + CallContext.unregister(); + } + } + + protected Account createServiceAccountIfNeeded() { + List accounts = accountDao.findAccountsByName(SERVICE_ACCOUNT_NAME); + for (AccountVO account : accounts) { + if (Account.State.ENABLED.equals(account.getState())) { + logger.debug("Veeam service account found: {}", account); + return account; + } + } + return createServiceAccount(); + } + + @Override + public boolean start() { + createServiceAccountIfNeeded(); + //find public custom disk offering + return true; + } + + public List listAllDisks() { + List kvmVolumes = volumeJoinDao.listByHypervisor(Hypervisor.HypervisorType.KVM); + return VolumeJoinVOToDiskConverter.toDiskList(kvmVolumes); + } + + public Disk getDisk(String uuid) { + VolumeJoinVO vo = volumeJoinDao.findByUuid(uuid); + if (vo == null) { + throw new InvalidParameterValueException("Disk with ID " + uuid + " not found"); + } + return VolumeJoinVOToDiskConverter.toDisk(vo); + } + + public Disk handleCreateDisk(Disk request) { + if (request == null) { + throw new InvalidParameterValueException("Request disk data is empty"); + } + String name = request.name; + if (StringUtils.isBlank(name) && !name.startsWith("Veeam_KvmBackupDisk_")) { + throw new InvalidParameterValueException("Only worker VM disk creation is supported"); + } + if (request.storageDomains == null || CollectionUtils.isEmpty(request.storageDomains.storageDomain) || + request.storageDomains.storageDomain.size() > 1) { + throw new InvalidParameterValueException("Exactly one storage domain must be specified"); + } + Ref domain = request.storageDomains.storageDomain.get(0); + if (domain == null || domain.id == null) { + throw new InvalidParameterValueException("Storage domain ID must be specified"); + } + StoragePoolVO pool = primaryDataStoreDao.findByUuid(domain.id); + if (pool == null) { + throw new InvalidParameterValueException("Storage domain with ID " + domain.id + " not found"); + } + if (StringUtils.isBlank(request.provisionedSize)) { + throw new InvalidParameterValueException("Provisioned size must be specified"); + } + long sizeInGb; + try { + sizeInGb = Long.parseLong(request.provisionedSize); + } catch (NumberFormatException ex) { + throw new InvalidParameterValueException("Invalid provisioned size: " + request.provisionedSize); + } + if (sizeInGb <= 0) { + throw new InvalidParameterValueException("Provisioned size must be greater than zero"); + } + sizeInGb = Math.max(1L, sizeInGb / (1024L * 1024L * 1024L)); + Account serviceAccount = createServiceAccountIfNeeded(); + DataCenterVO zone = dataCenterDao.findById(pool.getDataCenterId()); + if (zone == null || !Grouping.AllocationState.Enabled.equals(zone.getAllocationState())) { + throw new InvalidParameterValueException("Datacenter for the specified storage domain is not found or not active"); + } + Long diskOfferingId = volumeApiService.getCustomDiskOfferingIdForVolumeUpload(serviceAccount, zone); + if (diskOfferingId == null) { + throw new CloudRuntimeException("Failed to find custom offering for disk" + zone.getName()); + } + CallContext.register(serviceAccount.getId(), serviceAccount.getId()); + try { + return createDisk(serviceAccount, pool, name, diskOfferingId, sizeInGb); + } finally { + CallContext.unregister(); + } + } + + @NotNull + private Disk createDisk(Account serviceAccount, StoragePoolVO pool, String name, Long diskOfferingId, long sizeInGb) { + Volume volume; + try { + volume = volumeApiService.allocVolume(serviceAccount.getId(), pool.getDataCenterId(), diskOfferingId, null, + null, name, sizeInGb, null, null, null, null); + } catch (ResourceAllocationException e) { + throw new CloudRuntimeException(e.getMessage(), e); + } + if (volume == null) { + throw new CloudRuntimeException("Failed to create volume"); + } + volume = volumeApiService.createVolume(volume.getId(), null, null, pool.getId(), true); + + // Implementation for creating a Disk resource + return VolumeJoinVOToDiskConverter.toDisk(volumeJoinDao.findById(volume.getId())); + } + + public List listAllImageTransfers() { + List imageTransfers = imageTransferDao.listAll(); + return ImageTransferVOToImageTransferConverter.toImageTransferList(imageTransfers, this::getHostById, this::getVolumeById); + } + + private HostJoinVO getHostById(Long hostId) { + if (hostId == null) { + return null; + } + return hostJoinDao.findById(hostId); + } + + private VolumeJoinVO getVolumeById(Long volumeId) { + if (volumeId == null) { + return null; + } + return volumeJoinDao.findById(volumeId); + } + + public ImageTransfer getImageTransfer(String uuid) { + ImageTransferVO vo = imageTransferDao.findByUuid(uuid); + if (vo == null) { + throw new InvalidParameterValueException("Image transfer with ID " + uuid + " not found"); + } + return ImageTransferVOToImageTransferConverter.toImageTransfer(vo, this::getHostById, this::getVolumeById); + } + + public ImageTransfer handleCreateImageTransfer(ImageTransfer request) { + if (request == null) { + throw new InvalidParameterValueException("Request image transfer data is empty"); + } + if (request.getDisk() == null || StringUtils.isBlank(request.getDisk().id)) { + throw new InvalidParameterValueException("Disk ID must be specified"); + } + VolumeJoinVO volumeVO = volumeJoinDao.findByUuid(request.getDisk().id); + if (volumeVO == null) { + throw new InvalidParameterValueException("Disk with ID " + request.getDisk().id + " not found"); + } + Direction direction = EnumUtils.fromString(Direction.class, request.getDirection()); + if (direction == null) { + throw new InvalidParameterValueException("Invalid or missing direction"); + } + return createImageTransfer(null, volumeVO.getId(), direction); + } + + private ImageTransfer createImageTransfer(Long backupId, Long volumeId, Direction direction) { + Account serviceAccount = createServiceAccountIfNeeded(); + CallContext.register(serviceAccount.getId(), serviceAccount.getId()); + try { + org.apache.cloudstack.backup.ImageTransfer imageTransfer = + incrementalBackupService.createImageTransfer(volumeId, null, direction); + ImageTransferVO imageTransferVO = imageTransferDao.findById(imageTransfer.getId()); + return ImageTransferVOToImageTransferConverter.toImageTransfer(imageTransferVO, this::getHostById, this::getVolumeById); + } finally { + CallContext.unregister(); + } + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DisksRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DisksRouteHandler.java index 708daf059db..cf588fe23ea 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DisksRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DisksRouteHandler.java @@ -26,22 +26,23 @@ import javax.servlet.http.HttpServletResponse; import org.apache.cloudstack.veeam.RouteHandler; import org.apache.cloudstack.veeam.VeeamControlServlet; -import org.apache.cloudstack.veeam.api.converter.VolumeJoinVOToDiskConverter; +import org.apache.cloudstack.veeam.adapter.UserResourceAdapter; import org.apache.cloudstack.veeam.api.dto.Disk; import org.apache.cloudstack.veeam.api.dto.Disks; import org.apache.cloudstack.veeam.utils.Negotiation; import org.apache.cloudstack.veeam.utils.PathUtil; -import com.cloud.api.query.dao.VolumeJoinDao; -import com.cloud.api.query.vo.VolumeJoinVO; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.fasterxml.jackson.core.JsonProcessingException; public class DisksRouteHandler extends ManagerBase implements RouteHandler { public static final String BASE_ROUTE = "/api/disks"; @Inject - VolumeJoinDao volumeJoinDao; + UserResourceAdapter userResourceAdapter; @Override public boolean start() { @@ -93,33 +94,32 @@ public class DisksRouteHandler extends ManagerBase implements RouteHandler { public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { - final List result = VolumeJoinVOToDiskConverter.toDiskList(listDisks()); + final List result = userResourceAdapter.listAllDisks(); final Disks response = new Disks(result); - io.getWriter().write(resp, 400, response, outFormat); + io.getWriter().write(resp, 200, response, outFormat); } public void handlePost(final HttpServletRequest req, final HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { String data = RouteHandler.getRequestData(req); logger.info("Received POST request on /api/disks endpoint, but method: POST is not supported atm. Request-data: {}", data); - - io.getWriter().write(resp, 400, "Unable to process at the moment", outFormat); - } - - protected List listDisks() { - return volumeJoinDao.listAll(); + try { + Disk request = io.getMapper().jsonMapper().readValue(data, Disk.class); + Disk response = userResourceAdapter.handleCreateDisk(request); + io.getWriter().write(resp, 201, response, outFormat); + } catch (JsonProcessingException | CloudRuntimeException e) { + io.getWriter().write(resp, 400, e.getMessage(), outFormat); + } } public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { - final VolumeJoinVO volumeJoinVO = volumeJoinDao.findByUuid(id); - if (volumeJoinVO == null) { - io.notFound(resp, "DataCenter not found: " + id, outFormat); - return; + try { + Disk response = userResourceAdapter.getDisk(id); + io.getWriter().write(resp, 200, response, outFormat); + } catch (InvalidParameterValueException e) { + io.getWriter().write(resp, 404, e.getMessage(), outFormat); } - Disk response = VolumeJoinVOToDiskConverter.toDisk(volumeJoinVO); - - io.getWriter().write(resp, 200, response, outFormat); } } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandler.java new file mode 100644 index 00000000000..58b7a418a63 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandler.java @@ -0,0 +1,126 @@ +// 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.veeam.api; + +import java.io.IOException; +import java.util.List; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.cloudstack.veeam.RouteHandler; +import org.apache.cloudstack.veeam.VeeamControlServlet; +import org.apache.cloudstack.veeam.adapter.UserResourceAdapter; +import org.apache.cloudstack.veeam.api.dto.ImageTransfer; +import org.apache.cloudstack.veeam.api.dto.ImageTransfers; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.apache.cloudstack.veeam.utils.PathUtil; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.Pair; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.fasterxml.jackson.core.JsonProcessingException; + +public class ImageTransfersRouteHandler extends ManagerBase implements RouteHandler { + public static final String BASE_ROUTE = "/api/imagetransfers"; + + @Inject + UserResourceAdapter userResourceAdapter; + + @Override + public boolean start() { + return true; + } + + @Override + public int priority() { + return 5; + } + + @Override + public boolean canHandle(String method, String path) { + return getSanitizedPath(path).startsWith(BASE_ROUTE); + } + + @Override + public void handle(HttpServletRequest req, HttpServletResponse resp, String path, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { + final String method = req.getMethod(); + final String sanitizedPath = getSanitizedPath(path); + if (sanitizedPath.equals(BASE_ROUTE)) { + if ("GET".equalsIgnoreCase(method)) { + handleGet(req, resp, outFormat, io); + return; + } + if ("POST".equalsIgnoreCase(method)) { + handlePost(req, resp, outFormat, io); + return; + } + } + + if (!"GET".equalsIgnoreCase(method)) { + io.methodNotAllowed(resp, "GET", outFormat); + return; + } + Pair idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); + if (idAndSubPath != null) { + // /api/imagetransfers/{id} + if (idAndSubPath.first() != null) { + if (idAndSubPath.second() == null) { + handleGetById(idAndSubPath.first(), resp, outFormat, io); + return; + } + } + } + + resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); + } + + public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, + Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { + final List result = userResourceAdapter.listAllImageTransfers(); + final ImageTransfers response = new ImageTransfers(); + response.setImageTransfer(result); + + io.getWriter().write(resp, 400, response, outFormat); + } + + public void handlePost(final HttpServletRequest req, final HttpServletResponse resp, + Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { + String data = RouteHandler.getRequestData(req); + logger.info("Received POST request on /api/imagetransfers endpoint, but method: POST is not supported atm. Request-data: {}", data); + try { + ImageTransfer request = io.getMapper().jsonMapper().readValue(data, ImageTransfer.class); + ImageTransfer response = userResourceAdapter.handleCreateImageTransfer(request); + io.getWriter().write(resp, 201, response, outFormat); + } catch (JsonProcessingException | CloudRuntimeException e) { + io.getWriter().write(resp, 400, e.getMessage(), outFormat); + } + } + + public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + final VeeamControlServlet io) throws IOException { + try { + ImageTransfer response = userResourceAdapter.getImageTransfer(id); + io.getWriter().write(resp, 200, response, outFormat); + } catch (InvalidParameterValueException e) { + io.getWriter().write(resp, 404, e.getMessage(), outFormat); + } + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/ImageTransferVOToImageTransferConverter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/ImageTransferVOToImageTransferConverter.java new file mode 100644 index 00000000000..ff97f9469fe --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/ImageTransferVOToImageTransferConverter.java @@ -0,0 +1,88 @@ +// 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.veeam.api.converter; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apache.cloudstack.backup.ImageTransferVO; +import org.apache.cloudstack.veeam.VeeamControlService; +import org.apache.cloudstack.veeam.api.DisksRouteHandler; +import org.apache.cloudstack.veeam.api.HostsRouteHandler; +import org.apache.cloudstack.veeam.api.ImageTransfersRouteHandler; +import org.apache.cloudstack.veeam.api.dto.ImageTransfer; +import org.apache.cloudstack.veeam.api.dto.Link; +import org.apache.cloudstack.veeam.api.dto.Ref; + +import com.cloud.api.query.vo.HostJoinVO; +import com.cloud.api.query.vo.VolumeJoinVO; + +public class ImageTransferVOToImageTransferConverter { + public static ImageTransfer toImageTransfer(ImageTransferVO vo, final Function hostResolver, + final Function volumeResolver) { + ImageTransfer imageTransfer = new ImageTransfer(); + final String basePath = VeeamControlService.ContextPath.value(); + imageTransfer.setId(vo.getUuid()); + imageTransfer.setHref(basePath + ImageTransfersRouteHandler.BASE_ROUTE + "/" + vo.getUuid()); + imageTransfer.setActive(Boolean.toString(true)); + imageTransfer.setDirection(vo.getDirection().name()); + imageTransfer.setFormat("cow"); + imageTransfer.setInactivityTimeout(Integer.toString(60)); + imageTransfer.setPhase(vo.getPhase().name()); + imageTransfer.setProxyUrl(vo.getTransferUrl()); + imageTransfer.setShallow(Boolean.toString(false)); + imageTransfer.setTimeoutPolicy("legacy"); + imageTransfer.setTransferUrl(vo.getTransferUrl()); + imageTransfer.setTransferred(Long.toString(0)); + if (hostResolver != null) { + HostJoinVO hostVo = hostResolver.apply(vo.getHostId()); + if (hostVo != null) { + imageTransfer.setHost(Ref.of(basePath + HostsRouteHandler.BASE_ROUTE + "/" + hostVo.getUuid(), hostVo.getUuid())); + } + } + if (volumeResolver != null) { + VolumeJoinVO volumeVo = volumeResolver.apply(vo.getDiskId()); + if (volumeVo != null) { + imageTransfer.setDisk(Ref.of(basePath + DisksRouteHandler.BASE_ROUTE + "/" + volumeVo.getUuid(), volumeVo.getUuid())); + } + } + final List links = new ArrayList<>(); + links.add(getLink(imageTransfer, "cancel")); + links.add(getLink(imageTransfer, "resume")); + links.add(getLink(imageTransfer, "pause")); + links.add(getLink(imageTransfer, "finalize")); + links.add(getLink(imageTransfer, "extend")); + return imageTransfer; + } + + public static List toImageTransferList(List vos, + final Function hostResolver, + final Function volumeResolver) { + return vos.stream().map(vo -> toImageTransfer(vo, hostResolver, volumeResolver)) + .collect(Collectors.toList()); + } + + private static Link getLink(ImageTransfer it, String rel) { + final Link link = new Link(); + link.rel = rel; + link.href = it.getHref() + "/" + rel; + return link; + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverter.java index 3b2305f5218..44f56bfbd00 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverter.java @@ -97,8 +97,8 @@ public class VolumeJoinVOToDiskConverter { // Disk profile (optional) disk.diskProfile = Ref.of( - basePath + "/diskprofiles/" + vol.getDiskOfferingId(), - String.valueOf(vol.getDiskOfferingId()) + basePath + "/diskprofiles/" + vol.getDiskOfferingUuid(), + String.valueOf(vol.getDiskOfferingUuid()) ); // Storage domains diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Actions.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Actions.java index a834c579973..9b4d0d16917 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Actions.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Actions.java @@ -23,11 +23,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public final class Actions { - public List link; + public List link; public Actions() {} - public Actions(final List link) { + public Actions(final List link) { this.link = link; } } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Backup.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Backup.java new file mode 100644 index 00000000000..217a16d8131 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Backup.java @@ -0,0 +1,36 @@ +// 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.veeam.api.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; + +public class Backup { + + @JsonProperty("creation_date") + @JacksonXmlProperty(localName = "creation_date") + private String creationDate; + + public String getCreationDate() { + return creationDate; + } + + public void setCreationDate(String creationDate) { + this.creationDate = creationDate; + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Disk.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Disk.java index 812501f5615..f61cd5d890e 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Disk.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Disk.java @@ -45,6 +45,9 @@ public final class Disk { @JsonProperty("propagate_errors") public String propagateErrors; + @JsonProperty("initial_size") + public String initialSize; + @JsonProperty("provisioned_size") public String provisionedSize; diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ImageTransfer.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ImageTransfer.java new file mode 100644 index 00000000000..3a17b79ca05 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ImageTransfer.java @@ -0,0 +1,202 @@ +// 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.veeam.api.dto; + + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JacksonXmlRootElement(localName = "image_transfer") +public class ImageTransfer { + + private String id; + private String href; + + private String active; + private String direction; + private String format; + + @JsonProperty("inactivity_timeout") + private String inactivityTimeout; + + private String phase; + + @JsonProperty("proxy_url") + private String proxyUrl; + + private String shallow; + + @JsonProperty("timeout_policy") + private String timeoutPolicy; + + @JsonProperty("transfer_url") + private String transferUrl; + + private String transferred; + + private Backup backup; + + private Ref host; + private Ref image; + private Ref disk; + private Actions actions; + + @JacksonXmlElementWrapper(useWrapping = false) + public List link; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getActive() { + return active; + } + + public void setActive(String active) { + this.active = active; + } + + public String getDirection() { + return direction; + } + + public void setDirection(String direction) { + this.direction = direction; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public String getInactivityTimeout() { + return inactivityTimeout; + } + + public void setInactivityTimeout(String inactivityTimeout) { + this.inactivityTimeout = inactivityTimeout; + } + + public String getPhase() { + return phase; + } + + public void setPhase(String phase) { + this.phase = phase; + } + + public String getProxyUrl() { + return proxyUrl; + } + + public void setProxyUrl(String proxyUrl) { + this.proxyUrl = proxyUrl; + } + + public String getShallow() { + return shallow; + } + + public void setShallow(String shallow) { + this.shallow = shallow; + } + + public String getTimeoutPolicy() { + return timeoutPolicy; + } + + public void setTimeoutPolicy(String timeoutPolicy) { + this.timeoutPolicy = timeoutPolicy; + } + + public String getTransferUrl() { + return transferUrl; + } + + public void setTransferUrl(String transferUrl) { + this.transferUrl = transferUrl; + } + + public String getTransferred() { + return transferred; + } + + public void setTransferred(String transferred) { + this.transferred = transferred; + } + + public Backup getBackup() { + return backup; + } + + public void setBackup(Backup backup) { + this.backup = backup; + } + + public Ref getHost() { + return host; + } + + public void setHost(Ref host) { + this.host = host; + } + + public Ref getImage() { + return image; + } + + public void setImage(Ref image) { + this.image = image; + } + + public Ref getDisk() { + return disk; + } + + public void setDisk(Ref disk) { + this.disk = disk; + } + + public Actions getActions() { + return actions; + } + + public void setActions(Actions actions) { + this.actions = actions; + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ActionLink.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ImageTransfers.java similarity index 65% rename from plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ActionLink.java rename to plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ImageTransfers.java index fe127d63364..4414846de60 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ActionLink.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ImageTransfers.java @@ -17,23 +17,23 @@ package org.apache.cloudstack.veeam.api.dto; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; @JsonInclude(JsonInclude.Include.NON_NULL) -public final class ActionLink { - public String rel; // start/stop/reboot/shutdown... - public String href; // /api/vms/{id}/start - public String method; // "post" +@JacksonXmlRootElement(localName = "image_transfers") +public class ImageTransfers { + @JsonProperty("image_transfer") + private List imageTransfer; - public ActionLink() {} - - public ActionLink(final String rel, final String href, final String method) { - this.rel = rel; - this.href = href; - this.method = method; + public List getImageTransfer() { + return imageTransfer; } - public static ActionLink post(final String rel, final String href) { - return new ActionLink(rel, href, "post"); + public void setImageTransfer(List imageTransfer) { + this.imageTransfer = imageTransfer; } } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseMapper.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/Mapper.java similarity index 97% rename from plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseMapper.java rename to plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/Mapper.java index 46b3a993aa7..0d6af22599e 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseMapper.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/Mapper.java @@ -23,11 +23,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.dataformat.xml.XmlMapper; -public class ResponseMapper { +public class Mapper { private final ObjectMapper json; private final XmlMapper xml; - public ResponseMapper() { + public Mapper() { this.json = new ObjectMapper(); this.xml = new XmlMapper(); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseWriter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseWriter.java index 7dcdc3e647f..461bb000f87 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseWriter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseWriter.java @@ -30,9 +30,9 @@ import org.apache.logging.log4j.Logger; public final class ResponseWriter { private static final Logger LOGGER = LogManager.getLogger(ResponseWriter.class); - private final ResponseMapper mapper; + private final Mapper mapper; - public ResponseWriter(final ResponseMapper mapper) { + public ResponseWriter(final Mapper mapper) { this.mapper = mapper; } diff --git a/plugins/integrations/veeam-control-service/src/main/resources/META-INF/cloudstack/veeam-control-service/spring-veeam-control-service-context.xml b/plugins/integrations/veeam-control-service/src/main/resources/META-INF/cloudstack/veeam-control-service/spring-veeam-control-service-context.xml index e56009aacd4..1b549abcfce 100644 --- a/plugins/integrations/veeam-control-service/src/main/resources/META-INF/cloudstack/veeam-control-service/spring-veeam-control-service-context.xml +++ b/plugins/integrations/veeam-control-service/src/main/resources/META-INF/cloudstack/veeam-control-service/spring-veeam-control-service-context.xml @@ -31,6 +31,7 @@ + @@ -39,9 +40,12 @@ - + + + + diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServiceImplTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServiceImplTest.java new file mode 100644 index 00000000000..ee3d99fca40 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServiceImplTest.java @@ -0,0 +1,24 @@ +package org.apache.cloudstack.veeam; + +import org.apache.cloudstack.veeam.api.dto.ImageTransfer; +import org.apache.cloudstack.veeam.utils.Mapper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import com.fasterxml.jackson.core.JsonProcessingException; + +@RunWith(MockitoJUnitRunner.class) +public class VeeamControlServiceImplTest { + + @Test + public void test_parseImageTransfer() { + String data = "{\"active\":false,\"direction\":\"upload\",\"format\":\"cow\",\"inactivity_timeout\":3600,\"phase\":\"cancelled\",\"shallow\":false,\"transferred\":0,\"link\":[],\"disk\":{\"id\":\"dba4d72d-01de-4267-aa8e-305996b53599\"},\"image\":{},\"backup\":{\"creation_date\":0}}"; + Mapper mapper = new Mapper(); + try { + ImageTransfer request = mapper.jsonMapper().readValue(data, ImageTransfer.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDao.java index 87485e86fc9..e61ad1d8e2d 100644 --- a/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDao.java @@ -22,6 +22,7 @@ import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.response.VolumeResponse; import com.cloud.api.query.vo.VolumeJoinVO; +import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.Volume; import com.cloud.utils.db.GenericDao; @@ -36,4 +37,6 @@ public interface VolumeJoinDao extends GenericDao { List searchByIds(Long... ids); List listByInstanceId(long instanceId); + + List listByHypervisor(Hypervisor.HypervisorType hypervisorType); } diff --git a/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDaoImpl.java index 9361abef604..0261398a232 100644 --- a/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDaoImpl.java @@ -45,6 +45,7 @@ import com.cloud.user.VmDiskStatisticsVO; import com.cloud.user.dao.VmDiskStatisticsDao; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VirtualMachine; @Component public class VolumeJoinDaoImpl extends GenericDaoBaseWithTagInformation implements VolumeJoinDao { @@ -379,4 +380,16 @@ public class VolumeJoinDaoImpl extends GenericDaoBaseWithTagInformation listByHypervisor(Hypervisor.HypervisorType hypervisorType) { + SearchBuilder sb = createSearchBuilder(); + sb.and("vmType", sb.entity().getVmType(), SearchCriteria.Op.EQ); + sb.and("hypervisorType", sb.entity().getHypervisorType(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("vmType", VirtualMachine.Type.User); + sc.setParameters("hypervisorType", hypervisorType); + return search(sc, null); + } + } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 68af9750317..3b833a1c150 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -639,21 +639,6 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic return null; } - private Long getCustomDiskOfferingIdForVolumeUpload(Account owner, DataCenter zone) { - Long offeringId = getDefaultCustomOfferingId(owner, zone); - if (offeringId != null) { - return offeringId; - } - List offerings = _diskOfferingDao.findCustomDiskOfferings(); - for (DiskOfferingVO offering : offerings) { - try { - _configMgr.checkDiskOfferingAccess(owner, offering, zone); - return offering.getId(); - } catch (PermissionDeniedException ignored) {} - } - return null; - } - @DB protected VolumeVO persistVolume(final Account owner, final Long zoneId, final String volumeName, final String url, final String format, final Long diskOfferingId, final Volume.State state) { return Transaction.execute(new TransactionCallbackWithException() { @@ -719,17 +704,31 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic * If the retrieved volume name is null, empty or blank, then A random name * will be generated using getRandomVolumeName method. * - * @param cmd + * @param userSpecifiedName * @return Either the retrieved name or a random name. */ - public String getVolumeNameFromCommand(CreateVolumeCmd cmd) { - String userSpecifiedName = cmd.getVolumeName(); - - if (StringUtils.isBlank(userSpecifiedName)) { - userSpecifiedName = getRandomVolumeName(); + public String getVolumeNameFromCommand(String userSpecifiedName) { + if (StringUtils.isNotBlank(userSpecifiedName)) { + return userSpecifiedName; } - return userSpecifiedName; + return getRandomVolumeName(); + } + + @Override + public Long getCustomDiskOfferingIdForVolumeUpload(Account owner, DataCenter zone) { + Long offeringId = getDefaultCustomOfferingId(owner, zone); + if (offeringId != null) { + return offeringId; + } + List offerings = _diskOfferingDao.findCustomDiskOfferings(); + for (DiskOfferingVO offering : offerings) { + try { + _configMgr.checkDiskOfferingAccess(owner, offering, zone); + return offering.getId(); + } catch (PermissionDeniedException ignored) {} + } + return null; } /* @@ -741,11 +740,20 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic @DB @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating volume", create = true) public VolumeVO allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationException { + return allocVolume(cmd.getEntityOwnerId(), cmd.getZoneId(), cmd.getDiskOfferingId(), cmd.getVirtualMachineId(), + cmd.getSnapshotId(), getVolumeNameFromCommand(cmd.getVolumeName()), cmd.getSize(), + cmd.getDisplayVolume(), cmd.getMinIops(), cmd.getMaxIops(), cmd.getCustomId()); + } + + @Override + @DB + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating volume", create = true) + public VolumeVO allocVolume(long ownerId, Long zoneId, Long diskOfferingId, Long vmId, Long snapshotId, + String name, Long cmdSize, Boolean displayVolume, Long cmdMinIops, Long cmdMaxIops, String customId) + throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); - long ownerId = cmd.getEntityOwnerId(); Account owner = _accountMgr.getActiveAccountById(ownerId); - Boolean displayVolume = cmd.getDisplayVolume(); // permission check _accountMgr.checkAccess(caller, null, true, _accountMgr.getActiveAccountById(ownerId)); @@ -758,8 +766,6 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } } - Long zoneId = cmd.getZoneId(); - Long diskOfferingId = null; DiskOfferingVO diskOffering = null; Long size = null; Long minIops = null; @@ -768,13 +774,13 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic VolumeVO parentVolume = null; // validate input parameters before creating the volume - if (cmd.getSnapshotId() == null && cmd.getDiskOfferingId() == null) { + if (snapshotId == null && diskOfferingId == null) { throw new InvalidParameterValueException("At least one of disk Offering ID or snapshot ID must be passed whilst creating volume"); } // disallow passing disk offering ID with DATA disk volume snapshots - if (cmd.getSnapshotId() != null && cmd.getDiskOfferingId() != null) { - SnapshotVO snapshot = _snapshotDao.findById(cmd.getSnapshotId()); + if (snapshotId != null && diskOfferingId != null) { + SnapshotVO snapshot = _snapshotDao.findById(snapshotId); if (snapshot != null) { parentVolume = _volsDao.findByIdIncludingRemoved(snapshot.getVolumeId()); if (parentVolume != null && parentVolume.getVolumeType() != Volume.Type.ROOT) @@ -784,10 +790,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } Map details = new HashMap<>(); - if (cmd.getDiskOfferingId() != null) { // create a new volume - - diskOfferingId = cmd.getDiskOfferingId(); - size = cmd.getSize(); + if (diskOfferingId != null) { // create a new volume + size = cmdSize; Long sizeInGB = size; if (size != null) { if (size > 0) { @@ -833,8 +837,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic if (isCustomizedIops != null) { if (isCustomizedIops) { - minIops = cmd.getMinIops(); - maxIops = cmd.getMaxIops(); + minIops = cmdMinIops; + maxIops = cmdMaxIops; if (minIops == null && maxIops == null) { minIops = 0L; @@ -866,8 +870,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } } - if (cmd.getSnapshotId() != null) { // create volume from snapshot - Long snapshotId = cmd.getSnapshotId(); + if (snapshotId != null) { // create volume from snapshot SnapshotVO snapshotCheck = _snapshotDao.findById(snapshotId); if (snapshotCheck == null) { throw new InvalidParameterValueException("unable to find a snapshot with id " + snapshotId); @@ -918,7 +921,6 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic // one step operation - create volume in VM's cluster and attach it // to the VM - Long vmId = cmd.getVirtualMachineId(); if (vmId != null) { // Check that the virtual machine ID is valid and it's a user vm UserVmVO vm = _userVmDao.findById(vmId); @@ -960,10 +962,10 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic throw new InvalidParameterValueException("Zone is not configured to use local storage but volume's disk offering " + diskOffering.getName() + " uses it"); } - String userSpecifiedName = getVolumeNameFromCommand(cmd); + String userSpecifiedName = getVolumeNameFromCommand(name); - return commitVolume(cmd.getSnapshotId(), caller, owner, displayVolume, zoneId, diskOfferingId, provisioningType, size, minIops, maxIops, parentVolume, userSpecifiedName, - _uuidMgr.generateUuid(Volume.class, cmd.getCustomId()), details); + return commitVolume(snapshotId, caller, owner, displayVolume, zoneId, diskOfferingId, provisioningType, size, minIops, maxIops, parentVolume, userSpecifiedName, + _uuidMgr.generateUuid(Volume.class, customId), details); } @Override @@ -1075,25 +1077,33 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic @DB @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating volume", async = true) public VolumeVO createVolume(CreateVolumeCmd cmd) { - VolumeVO volume = _volsDao.findById(cmd.getEntityId()); + return createVolume(cmd.getEntityId(), cmd.getVirtualMachineId(), cmd.getSnapshotId(), cmd.getStorageId(), + cmd.getDisplayVolume()); + } + + @Override + @DB + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating volume", async = true) + public VolumeVO createVolume(long volumeId, Long vmId, Long snapshotId, Long storageId, Boolean display) { + VolumeVO volume = _volsDao.findById(volumeId); boolean created = true; try { - if (cmd.getSnapshotId() != null) { - volume = createVolumeFromSnapshot(volume, cmd.getSnapshotId(), cmd.getVirtualMachineId()); + if (snapshotId != null) { + volume = createVolumeFromSnapshot(volume, snapshotId, vmId); if (volume.getState() != Volume.State.Ready) { created = false; } // if VM Id is provided, attach the volume to the VM - if (cmd.getVirtualMachineId() != null) { + if (vmId != null) { try { - attachVolumeToVM(cmd.getVirtualMachineId(), volume.getId(), volume.getDeviceId(), false); + attachVolumeToVM(vmId, volume.getId(), volume.getDeviceId(), false); } catch (Exception ex) { StringBuilder message = new StringBuilder("Volume: "); message.append(volume.getUuid()); message.append(" created successfully, but failed to attach the newly created volume to VM: "); - message.append(cmd.getVirtualMachineId()); + message.append(vmId); message.append(" due to error: "); message.append(ex.getMessage()); if (logger.isDebugEnabled()) { @@ -1102,20 +1112,20 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic throw new CloudRuntimeException(message.toString()); } } - } else if (cmd.getStorageId() != null) { - allocateVolumeOnStorage(cmd.getEntityId(), cmd.getStorageId()); + } else if (storageId != null) { + allocateVolumeOnStorage(volumeId, storageId); } return volume; } catch (Exception e) { created = false; - VolumeInfo vol = volFactory.getVolume(cmd.getEntityId()); + VolumeInfo vol = volFactory.getVolume(volumeId); vol.stateTransit(Volume.Event.DestroyRequested); throw new CloudRuntimeException(String.format("Failed to create volume: %s", volume), e); } finally { if (!created) { VolumeVO finalVolume = volume; logger.trace("Decrementing volume resource count for account {} as volume failed to create on the backend", () -> _accountMgr.getAccount(finalVolume.getAccountId())); - _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), cmd.getDisplayVolume(), + _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), display, volume.getSize(), _diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId())); } } diff --git a/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java b/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java index 5eb83516a0e..daa28427467 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java @@ -273,8 +273,8 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme } } - private ImageTransferVO createDownloadImageTransfer(CreateImageTransferCmd cmd, VolumeVO volume) { - Long backupId = cmd.getBackupId(); + private ImageTransferVO createDownloadImageTransfer(Long backupId, VolumeVO volume) { + final String direction = ImageTransfer.Direction.download.toString(); BackupVO backup = backupDao.findById(backupId); if (backup == null) { throw new CloudRuntimeException("Backup not found: " + backupId); @@ -288,7 +288,7 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme host.getPrivateIpAddress(), volume.getUuid(), backup.getNbdPort(), - cmd.getDirection().toString() + direction ); try { @@ -339,7 +339,8 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme return hosts.get(0); } - private ImageTransferVO createUploadImageTransfer(CreateImageTransferCmd cmd, VolumeVO volume) { + private ImageTransferVO createUploadImageTransfer(VolumeVO volume) { + final String direction = ImageTransfer.Direction.upload.toString(); String transferId = UUID.randomUUID().toString(); int nbdPort = allocateNbdPort(); @@ -356,7 +357,7 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme volume.getUuid(), volumePath, nbdPort, - cmd.getDirection().toString() + direction ); try { @@ -374,14 +375,14 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme host.getPrivateIpAddress(), volume.getUuid(), nbdPort, - cmd.getDirection().toString() + direction ); EndPoint ssvm = _epSelector.findSsvm(volume.getDataCenterId()); transferAnswer = (CreateImageTransferAnswer) ssvm.sendMessage(transferCmd); if (!transferAnswer.getResult()) { - StopNBDServerCommand stopNbdServerCommand = new StopNBDServerCommand(transferId, cmd.getDirection().toString(), nbdPort); + StopNBDServerCommand stopNbdServerCommand = new StopNBDServerCommand(transferId, direction, nbdPort); throw new CloudRuntimeException("Failed to create image transfer: " + transferAnswer.getDetails()); } @@ -407,26 +408,33 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme @Override public ImageTransferResponse createImageTransfer(CreateImageTransferCmd cmd) { + ImageTransfer imageTransfer = createImageTransfer(cmd.getVolumeId(), cmd.getBackupId(), cmd.getDirection()); + if (imageTransfer instanceof ImageTransferVO) { + ImageTransferVO imageTransferVO = (ImageTransferVO) imageTransfer; + return toImageTransferResponse(imageTransferVO); + } + return toImageTransferResponse(imageTransferDao.findById(imageTransfer.getId())); + } + + @Override + public ImageTransfer createImageTransfer(long volumeId, Long backupId, ImageTransfer.Direction direction) { ImageTransfer imageTransfer; - Long volumeId = cmd.getVolumeId(); - VolumeVO volume = volumeDao.findById(cmd.getVolumeId()); + VolumeVO volume = volumeDao.findById(volumeId); ImageTransferVO existingTransfer = imageTransferDao.findByVolume(volume.getId()); if (existingTransfer != null) { throw new CloudRuntimeException("Image transfer already in progress for volume: " + volume.getUuid()); } - if (cmd.getDirection().equals(ImageTransfer.Direction.upload)) { - imageTransfer = createUploadImageTransfer(cmd, volume); - } else if (cmd.getDirection().equals(ImageTransfer.Direction.download)) { - imageTransfer = createDownloadImageTransfer(cmd, volume); + if (ImageTransfer.Direction.upload.equals(direction)) { + imageTransfer = createUploadImageTransfer(volume); + } else if (ImageTransfer.Direction.download.equals(direction)) { + imageTransfer = createDownloadImageTransfer(backupId, volume); } else { - throw new CloudRuntimeException("Invalid direction: " + cmd.getDirection()); + throw new CloudRuntimeException("Invalid direction: " + direction); } - ImageTransferVO imageTransferVO = imageTransferDao.findById(imageTransfer.getId()); - ImageTransferResponse response = toImageTransferResponse(imageTransferVO); - return response; + return imageTransferDao.findById(imageTransfer.getId()); } private void finalizeDownloadImageTransfer(ImageTransferVO imageTransfer) { diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index 0575b430ef1..e014ad72cfc 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -597,26 +597,22 @@ public class VolumeApiServiceImplTest { @Test public void testNullGetVolumeNameFromCmd() { - when(createVol.getVolumeName()).thenReturn(null); - Assert.assertNotNull(volumeApiServiceImpl.getVolumeNameFromCommand(createVol)); + Assert.assertNotNull(volumeApiServiceImpl.getVolumeNameFromCommand(null)); } @Test public void testEmptyGetVolumeNameFromCmd() { - when(createVol.getVolumeName()).thenReturn(""); - Assert.assertNotNull(volumeApiServiceImpl.getVolumeNameFromCommand(createVol)); + Assert.assertNotNull(volumeApiServiceImpl.getVolumeNameFromCommand("")); } @Test public void testBlankGetVolumeNameFromCmd() { - when(createVol.getVolumeName()).thenReturn(" "); - Assert.assertNotNull(volumeApiServiceImpl.getVolumeNameFromCommand(createVol)); + Assert.assertNotNull(volumeApiServiceImpl.getVolumeNameFromCommand(" ")); } @Test public void testNonEmptyGetVolumeNameFromCmd() { - when(createVol.getVolumeName()).thenReturn("abc"); - Assert.assertSame(volumeApiServiceImpl.getVolumeNameFromCommand(createVol), "abc"); + Assert.assertSame(volumeApiServiceImpl.getVolumeNameFromCommand("abc"), "abc"); } @Test diff --git a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java index 88b93d7643b..449525f8328 100644 --- a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java +++ b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java @@ -54,8 +54,6 @@ import java.util.stream.Stream; import javax.naming.ConfigurationException; -import com.cloud.agent.api.ConvertSnapshotCommand; - import org.apache.cloudstack.backup.CreateImageTransferAnswer; import org.apache.cloudstack.backup.CreateImageTransferCommand; import org.apache.cloudstack.backup.FinalizeImageTransferCommand; @@ -101,8 +99,8 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.joda.time.DateTime; import org.joda.time.format.ISODateTimeFormat; @@ -112,6 +110,7 @@ import com.cloud.agent.api.CheckHealthAnswer; import com.cloud.agent.api.CheckHealthCommand; import com.cloud.agent.api.Command; import com.cloud.agent.api.ComputeChecksumCommand; +import com.cloud.agent.api.ConvertSnapshotCommand; import com.cloud.agent.api.DeleteSnapshotsDirCommand; import com.cloud.agent.api.GetStorageStatsAnswer; import com.cloud.agent.api.GetStorageStatsCommand; @@ -3827,7 +3826,7 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S // Open firewall port for image server if (_inSystemVM) { String rule = String.format("-p tcp -m state --state NEW -m tcp --dport %d -j ACCEPT", imageServerPort); - IpTablesHelper.addConditionally(IpTablesHelper.INPUT_CHAIN, true, rule, + IpTablesHelper.addConditionally(IpTablesHelper.INPUT_CHAIN, true, rule, String.format("Error in opening up image server port %d", imageServerPort)); }