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 index 4be60562797..ad1be6af85e 100644 --- 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 @@ -28,6 +28,7 @@ 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.ApiConstants; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.user.job.QueryAsyncJobResultCmd; import org.apache.cloudstack.api.command.user.network.ListNetworksCmd; @@ -70,6 +71,7 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.org.Grouping; import com.cloud.storage.Volume; import com.cloud.storage.VolumeApiService; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.user.Account; import com.cloud.user.AccountService; import com.cloud.user.AccountVO; @@ -116,6 +118,9 @@ public class UserResourceAdapter extends ManagerBase { @Inject VolumeJoinDao volumeJoinDao; + @Inject + VolumeDetailsDao volumeDetailsDao; + @Inject VolumeApiService volumeApiService; @@ -222,19 +227,26 @@ public class UserResourceAdapter extends ManagerBase { if (pool == null) { throw new InvalidParameterValueException("Storage domain with ID " + domain.id + " not found"); } - if (StringUtils.isBlank(request.provisionedSize)) { + String sizeStr = request.provisionedSize; + if (StringUtils.isBlank(sizeStr)) { throw new InvalidParameterValueException("Provisioned size must be specified"); } - long sizeInGb; + long provisionedSizeInGb; try { - sizeInGb = Long.parseLong(request.provisionedSize); + provisionedSizeInGb = Long.parseLong(sizeStr); } catch (NumberFormatException ex) { - throw new InvalidParameterValueException("Invalid provisioned size: " + request.provisionedSize); + throw new InvalidParameterValueException("Invalid provisioned size: " + sizeStr); } - if (sizeInGb <= 0) { + if (provisionedSizeInGb <= 0) { throw new InvalidParameterValueException("Provisioned size must be greater than zero"); } - sizeInGb = Math.max(1L, sizeInGb / (1024L * 1024L * 1024L)); + provisionedSizeInGb = Math.max(1L, provisionedSizeInGb / (1024L * 1024L * 1024L)); + Long initialSize = null; + if (StringUtils.isNotBlank(request.initialSize)) { + try { + initialSize = Long.parseLong(request.initialSize); + } catch (NumberFormatException ignored) {} + } Account serviceAccount = createServiceAccountIfNeeded(); DataCenterVO zone = dataCenterDao.findById(pool.getDataCenterId()); if (zone == null || !Grouping.AllocationState.Enabled.equals(zone.getAllocationState())) { @@ -246,14 +258,14 @@ public class UserResourceAdapter extends ManagerBase { } CallContext.register(serviceAccount.getId(), serviceAccount.getId()); try { - return createDisk(serviceAccount, pool, name, diskOfferingId, sizeInGb); + return createDisk(serviceAccount, pool, name, diskOfferingId, provisionedSizeInGb, initialSize); } finally { CallContext.unregister(); } } @NotNull - private Disk createDisk(Account serviceAccount, StoragePoolVO pool, String name, Long diskOfferingId, long sizeInGb) { + private Disk createDisk(Account serviceAccount, StoragePoolVO pool, String name, Long diskOfferingId, long sizeInGb, Long initialSize) { Volume volume; try { volume = volumeApiService.allocVolume(serviceAccount.getId(), pool.getDataCenterId(), diskOfferingId, null, @@ -265,6 +277,9 @@ public class UserResourceAdapter extends ManagerBase { throw new CloudRuntimeException("Failed to create volume"); } volume = volumeApiService.createVolume(volume.getId(), null, null, pool.getId(), true); + if (initialSize != null) { + volumeDetailsDao.addDetail(volume.getId(), ApiConstants.VIRTUAL_SIZE, String.valueOf(initialSize), true); + } // Implementation for creating a Disk resource return VolumeJoinVOToDiskConverter.toDisk(volumeJoinDao.findById(volume.getId())); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ClustersRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ClustersRouteHandler.java index 6459ad06f82..4c4dda45f8c 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ClustersRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ClustersRouteHandler.java @@ -31,12 +31,12 @@ import org.apache.cloudstack.veeam.api.dto.Cluster; import org.apache.cloudstack.veeam.api.dto.Clusters; import org.apache.cloudstack.veeam.utils.Negotiation; import org.apache.cloudstack.veeam.utils.PathUtil; +import org.apache.commons.collections.CollectionUtils; import com.cloud.api.query.dao.DataCenterJoinDao; import com.cloud.api.query.vo.DataCenterJoinVO; import com.cloud.dc.ClusterVO; import com.cloud.dc.dao.ClusterDao; -import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; public class ClustersRouteHandler extends ManagerBase implements RouteHandler { @@ -76,21 +76,19 @@ public class ClustersRouteHandler extends ManagerBase implements RouteHandler { return; } - Pair idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); - if (idAndSubPath != null) { - // /api/clusters/{id} - if (idAndSubPath.first() != null) { - if (idAndSubPath.second() == null) { - handleGetById(idAndSubPath.first(), resp, outFormat, io); - return; - } + List idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); + if (CollectionUtils.isNotEmpty(idAndSubPath)) { + String id = idAndSubPath.get(0); + if (idAndSubPath.size() == 1) { + handleGetById(id, resp, outFormat, io); + return; } } resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); } - public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, + protected void handleGet(final HttpServletRequest req, final HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { final List result = ClusterVOToClusterConverter.toClusterList(listClusters(), this::getZoneById); final Clusters response = new Clusters(result); @@ -102,7 +100,7 @@ public class ClustersRouteHandler extends ManagerBase implements RouteHandler { return clusterDao.listAll(); } - public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { final ClusterVO vo = clusterDao.findByUuid(id); if (vo == null) { @@ -114,7 +112,7 @@ public class ClustersRouteHandler extends ManagerBase implements RouteHandler { io.getWriter().write(resp, 200, response, outFormat); } - private DataCenterJoinVO getZoneById(Long zoneId) { + protected DataCenterJoinVO getZoneById(Long zoneId) { if (zoneId == null) { return null; } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandler.java index 17fece0e7ee..5c84a20bc10 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandler.java @@ -37,6 +37,7 @@ import org.apache.cloudstack.veeam.api.dto.StorageDomain; import org.apache.cloudstack.veeam.api.dto.StorageDomains; import org.apache.cloudstack.veeam.utils.Negotiation; import org.apache.cloudstack.veeam.utils.PathUtil; +import org.apache.commons.collections.CollectionUtils; import com.cloud.api.query.dao.DataCenterJoinDao; import com.cloud.api.query.dao.ImageStoreJoinDao; @@ -46,7 +47,6 @@ import com.cloud.api.query.vo.ImageStoreJoinVO; import com.cloud.api.query.vo.StoragePoolJoinVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; -import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; public class DataCentersRouteHandler extends ManagerBase implements RouteHandler { @@ -95,20 +95,20 @@ public class DataCentersRouteHandler extends ManagerBase implements RouteHandler return; } - Pair idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); - if (idAndSubPath != null) { - // /api/datacenters/{id} - if (idAndSubPath.first() != null) { - if (idAndSubPath.second() == null) { - handleGetById(idAndSubPath.first(), resp, outFormat, io); + List idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); + if (CollectionUtils.isNotEmpty(idAndSubPath)) { + String id = idAndSubPath.get(0); + if (idAndSubPath.size() == 1) { + handleGetById(id, resp, outFormat, io); + return; + } else if (idAndSubPath.size() == 2) { + String subPath = idAndSubPath.get(1); + if ("storagedomains".equals(subPath)) { + handleGetStorageDomainsByDcId(id, resp, outFormat, io); return; } - if ("storagedomains".equals(idAndSubPath.second())) { - handleGetStorageDomainsByDcId(idAndSubPath.first(), resp, outFormat, io); - return; - } - if ("networks".equals(idAndSubPath.second())) { - handleGetNetworksByDcId(idAndSubPath.first(), resp, outFormat, io); + if ("networks".equals(subPath)) { + handleGetNetworksByDcId(id, resp, outFormat, io); return; } } @@ -117,7 +117,7 @@ public class DataCentersRouteHandler extends ManagerBase implements RouteHandler resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); } - public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, + protected void handleGet(final HttpServletRequest req, final HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { final List result = DataCenterJoinVOToDataCenterConverter.toDCList(listDCs()); final DataCenters response = new DataCenters(result); @@ -129,7 +129,7 @@ public class DataCentersRouteHandler extends ManagerBase implements RouteHandler return dataCenterJoinDao.listAll(); } - public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { final DataCenterJoinVO dataCenterVO = dataCenterJoinDao.findByUuid(id); if (dataCenterVO == null) { @@ -153,7 +153,7 @@ public class DataCentersRouteHandler extends ManagerBase implements RouteHandler return networkDao.listAll(); } - public void handleGetStorageDomainsByDcId(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetStorageDomainsByDcId(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { final DataCenterJoinVO dataCenterVO = dataCenterJoinDao.findByUuid(id); if (dataCenterVO == null) { @@ -168,7 +168,7 @@ public class DataCentersRouteHandler extends ManagerBase implements RouteHandler io.getWriter().write(resp, 200, response, outFormat); } - public void handleGetNetworksByDcId(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetNetworksByDcId(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { final DataCenterJoinVO dataCenterVO = dataCenterJoinDao.findByUuid(id); if (dataCenterVO == null) { 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 cf588fe23ea..6cac244e133 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 @@ -31,9 +31,9 @@ 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 org.apache.commons.collections.CollectionUtils; 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; @@ -78,21 +78,19 @@ public class DisksRouteHandler extends ManagerBase implements RouteHandler { io.methodNotAllowed(resp, "GET", outFormat); return; } - Pair idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); - if (idAndSubPath != null) { - // /api/disks/{id} - if (idAndSubPath.first() != null) { - if (idAndSubPath.second() == null) { - handleGetById(idAndSubPath.first(), resp, outFormat, io); - return; - } + List idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); + if (CollectionUtils.isNotEmpty(idAndSubPath)) { + String id = idAndSubPath.get(0); + if (idAndSubPath.size() == 1) { + handleGetById(id, resp, outFormat, io); + return; } } resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); } - public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, + protected void handleGet(final HttpServletRequest req, final HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { final List result = userResourceAdapter.listAllDisks(); final Disks response = new Disks(result); @@ -100,7 +98,7 @@ public class DisksRouteHandler extends ManagerBase implements RouteHandler { io.getWriter().write(resp, 200, response, outFormat); } - public void handlePost(final HttpServletRequest req, final HttpServletResponse resp, + protected 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); @@ -113,7 +111,7 @@ public class DisksRouteHandler extends ManagerBase implements RouteHandler { } } - public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { Disk response = userResourceAdapter.getDisk(id); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/HostsRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/HostsRouteHandler.java index b33fa9bda9c..6ed3a3af0b7 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/HostsRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/HostsRouteHandler.java @@ -31,10 +31,10 @@ import org.apache.cloudstack.veeam.api.dto.Host; import org.apache.cloudstack.veeam.api.dto.Hosts; import org.apache.cloudstack.veeam.utils.Negotiation; import org.apache.cloudstack.veeam.utils.PathUtil; +import org.apache.commons.collections.CollectionUtils; import com.cloud.api.query.dao.HostJoinDao; import com.cloud.api.query.vo.HostJoinVO; -import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; public class HostsRouteHandler extends ManagerBase implements RouteHandler { @@ -71,21 +71,19 @@ public class HostsRouteHandler extends ManagerBase implements RouteHandler { return; } - Pair idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); - if (idAndSubPath != null) { - // /api/hosts/{id} - if (idAndSubPath.first() != null) { - if (idAndSubPath.second() == null) { - handleGetById(idAndSubPath.first(), resp, outFormat, io); - return; - } + List idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); + if (CollectionUtils.isNotEmpty(idAndSubPath)) { + String id = idAndSubPath.get(0); + if (idAndSubPath.size() == 1) { + handleGetById(id, resp, outFormat, io); + return; } } resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); } - public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, + protected void handleGet(final HttpServletRequest req, final HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { final List result = HostJoinVOToHostConverter.toHostList(listHosts()); final Hosts response = new Hosts(result); @@ -97,7 +95,7 @@ public class HostsRouteHandler extends ManagerBase implements RouteHandler { return hostJoinDao.listAll(); } - public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { final HostJoinVO vo = hostJoinDao.findByUuid(id); if (vo == null) { 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 index 58b7a418a63..a469afc08b5 100644 --- 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 @@ -31,9 +31,9 @@ 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 org.apache.commons.collections.CollectionUtils; 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; @@ -73,17 +73,28 @@ public class ImageTransfersRouteHandler extends ManagerBase implements RouteHand 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); + List idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); + if (CollectionUtils.isNotEmpty(idAndSubPath)) { + String id = idAndSubPath.get(0); + if (idAndSubPath.size() == 1) { + if (!"GET".equalsIgnoreCase(method)) { + io.methodNotAllowed(resp, "GET", outFormat); + return; + } + handleGetById(id, resp, outFormat, io); + return; + } else if (idAndSubPath.size() == 2) { + if (!"POST".equalsIgnoreCase(method)) { + io.methodNotAllowed(resp, "POST", outFormat); + return; + } + String subPath = idAndSubPath.get(1); + if ("cancel".equals(subPath)) { + handleCancelById(id, resp, outFormat, io); + return; + } + if ("finalize".equals(subPath)) { + handleFinalizeById(id, resp, outFormat, io); return; } } @@ -92,7 +103,7 @@ public class ImageTransfersRouteHandler extends ManagerBase implements RouteHand resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); } - public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, + protected 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(); @@ -101,10 +112,10 @@ public class ImageTransfersRouteHandler extends ManagerBase implements RouteHand io.getWriter().write(resp, 400, response, outFormat); } - public void handlePost(final HttpServletRequest req, final HttpServletResponse resp, + protected 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); + logger.info("Received POST request on /api/imagetransfers endpoint. Request-data: {}", data); try { ImageTransfer request = io.getMapper().jsonMapper().readValue(data, ImageTransfer.class); ImageTransfer response = userResourceAdapter.handleCreateImageTransfer(request); @@ -114,7 +125,7 @@ public class ImageTransfersRouteHandler extends ManagerBase implements RouteHand } } - public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { ImageTransfer response = userResourceAdapter.getImageTransfer(id); @@ -123,4 +134,16 @@ public class ImageTransfersRouteHandler extends ManagerBase implements RouteHand io.getWriter().write(resp, 404, e.getMessage(), outFormat); } } + + protected void handleCancelById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + final VeeamControlServlet io) throws IOException { + //ToDo: implement cancel logic + io.getWriter().write(resp, 200, "Image transfer cancelled successfully", outFormat); + } + + protected void handleFinalizeById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + final VeeamControlServlet io) throws IOException { + //ToDo: implement finalize logic + io.getWriter().write(resp, 200, "Image transfer finalized successfully", outFormat); + } } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/NetworksRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/NetworksRouteHandler.java index c3bab348f4e..2b895a2a647 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/NetworksRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/NetworksRouteHandler.java @@ -31,12 +31,12 @@ import org.apache.cloudstack.veeam.api.dto.Network; import org.apache.cloudstack.veeam.api.dto.Networks; import org.apache.cloudstack.veeam.utils.Negotiation; import org.apache.cloudstack.veeam.utils.PathUtil; +import org.apache.commons.collections.CollectionUtils; import com.cloud.api.query.dao.DataCenterJoinDao; import com.cloud.api.query.vo.DataCenterJoinVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; -import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; public class NetworksRouteHandler extends ManagerBase implements RouteHandler { @@ -76,21 +76,19 @@ public class NetworksRouteHandler extends ManagerBase implements RouteHandler { return; } - Pair idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); - if (idAndSubPath != null) { - // /api/networks/{id} - if (idAndSubPath.first() != null) { - if (idAndSubPath.second() == null) { - handleGetById(idAndSubPath.first(), resp, outFormat, io); - return; - } + List idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); + if (CollectionUtils.isNotEmpty(idAndSubPath)) { + String id = idAndSubPath.get(0); + if (idAndSubPath.size() == 1) { + handleGetById(id, resp, outFormat, io); + return; } } resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); } - public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, + protected void handleGet(final HttpServletRequest req, final HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { final List result = NetworkVOToNetworkConverter.toNetworkList(listNetworks(), this::getZoneById); final Networks response = new Networks(result); @@ -102,7 +100,7 @@ public class NetworksRouteHandler extends ManagerBase implements RouteHandler { return networkDao.listAll(); } - public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { final NetworkVO vo = networkDao.findByUuid(id); if (vo == null) { @@ -114,7 +112,7 @@ public class NetworksRouteHandler extends ManagerBase implements RouteHandler { io.getWriter().write(resp, 200, response, outFormat); } - private DataCenterJoinVO getZoneById(Long zoneId) { + protected DataCenterJoinVO getZoneById(Long zoneId) { if (zoneId == null) { return null; } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java index 08d747e955a..6971c81b69f 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java @@ -40,13 +40,13 @@ import org.apache.cloudstack.veeam.api.response.VmCollectionResponse; import org.apache.cloudstack.veeam.api.response.VmEntityResponse; import org.apache.cloudstack.veeam.utils.Negotiation; import org.apache.cloudstack.veeam.utils.PathUtil; +import org.apache.commons.collections.CollectionUtils; import com.cloud.api.query.dao.HostJoinDao; import com.cloud.api.query.dao.UserVmJoinDao; import com.cloud.api.query.dao.VolumeJoinDao; import com.cloud.api.query.vo.HostJoinVO; import com.cloud.api.query.vo.UserVmJoinVO; -import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; public class VmsRouteHandler extends ManagerBase implements RouteHandler { @@ -97,16 +97,17 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { handleGet(req, resp, outFormat, io); return; } - Pair idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); - if (idAndSubPath != null) { - // /api/vms/{id} - if (idAndSubPath.first() != null) { - if (idAndSubPath.second() == null) { - handleGetById(idAndSubPath.first(), resp, outFormat, io); - return; - } - if ("diskattachments".equals(idAndSubPath.second())) { - handleGetDisAttachmentsByVmId(idAndSubPath.first(), resp, outFormat, io); + + List idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); + if (CollectionUtils.isNotEmpty(idAndSubPath)) { + String id = idAndSubPath.get(0); + if (idAndSubPath.size() == 1) { + handleGetById(id, resp, outFormat, io); + return; + } else if (idAndSubPath.size() == 2) { + String subPath = idAndSubPath.get(1); + if ("diskattachments".equals(subPath)) { + handleGetDisAttachmentsByVmId(id, resp, outFormat, io); return; } } @@ -115,7 +116,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); } - public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, + protected void handleGet(final HttpServletRequest req, final HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { final VmListQuery q = fromRequest(req); @@ -154,7 +155,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { io.getWriter().write(resp, 200, response, outFormat); } - private static VmListQuery fromRequest(final HttpServletRequest req) { + protected static VmListQuery fromRequest(final HttpServletRequest req) { final VmListQuery q = new VmListQuery(); q.setSearch(req.getParameter("search")); q.setMax(parseIntOrNull(req.getParameter("max"))); @@ -162,7 +163,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { return q; } - private static Integer parseIntOrNull(final String s) { + protected static Integer parseIntOrNull(final String s) { if (s == null || s.trim().isEmpty()) return null; try { return Integer.parseInt(s.trim()); @@ -176,7 +177,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { return userVmJoinDao.listAll(); } - public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { final UserVmJoinVO userVmJoinVO = userVmJoinDao.findByUuid(id); if (userVmJoinVO == null) { @@ -188,7 +189,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { io.getWriter().write(resp, 200, response, outFormat); } - public void handleGetDisAttachmentsByVmId(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetDisAttachmentsByVmId(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { final UserVmJoinVO userVmJoinVO = userVmJoinDao.findByUuid(id); if (userVmJoinVO == null) { @@ -202,7 +203,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { io.getWriter().write(resp, 200, response, outFormat); } - private HostJoinVO getHostById(Long hostId) { + protected HostJoinVO getHostById(Long hostId) { if (hostId == null) { return null; } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VnicProfilesRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VnicProfilesRouteHandler.java index 9c2ffcca912..ba7e040e455 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VnicProfilesRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VnicProfilesRouteHandler.java @@ -31,12 +31,12 @@ import org.apache.cloudstack.veeam.api.dto.VnicProfile; import org.apache.cloudstack.veeam.api.dto.VnicProfiles; import org.apache.cloudstack.veeam.utils.Negotiation; import org.apache.cloudstack.veeam.utils.PathUtil; +import org.apache.commons.collections.CollectionUtils; import com.cloud.api.query.dao.DataCenterJoinDao; import com.cloud.api.query.vo.DataCenterJoinVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; -import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; public class VnicProfilesRouteHandler extends ManagerBase implements RouteHandler { @@ -76,21 +76,19 @@ public class VnicProfilesRouteHandler extends ManagerBase implements RouteHandle return; } - Pair idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); - if (idAndSubPath != null) { - // /api/vnicprofiles/{id} - if (idAndSubPath.first() != null) { - if (idAndSubPath.second() == null) { - handleGetById(idAndSubPath.first(), resp, outFormat, io); - return; - } + List idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE); + if (CollectionUtils.isNotEmpty(idAndSubPath)) { + String id = idAndSubPath.get(0); + if (idAndSubPath.size() == 1) { + handleGetById(id, resp, outFormat, io); + return; } } resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); } - public void handleGet(final HttpServletRequest req, final HttpServletResponse resp, + protected void handleGet(final HttpServletRequest req, final HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { final List result = NetworkVOToVnicProfileConverter.toVnicProfileList(listNetworks(), this::getZoneById); final VnicProfiles response = new VnicProfiles(result); @@ -102,7 +100,7 @@ public class VnicProfilesRouteHandler extends ManagerBase implements RouteHandle return networkDao.listAll(); } - public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, + protected void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { final NetworkVO vo = networkDao.findByUuid(id); if (vo == null) { @@ -114,7 +112,7 @@ public class VnicProfilesRouteHandler extends ManagerBase implements RouteHandle io.getWriter().write(resp, 200, response, outFormat); } - private DataCenterJoinVO getZoneById(Long zoneId) { + protected DataCenterJoinVO getZoneById(Long zoneId) { if (zoneId == null) { return null; } 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 index ff97f9469fe..5fc4313bdb1 100644 --- 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 @@ -27,6 +27,7 @@ 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.Actions; import org.apache.cloudstack.veeam.api.dto.ImageTransfer; import org.apache.cloudstack.veeam.api.dto.Link; import org.apache.cloudstack.veeam.api.dto.Ref; @@ -41,11 +42,16 @@ public class ImageTransferVOToImageTransferConverter { final String basePath = VeeamControlService.ContextPath.value(); imageTransfer.setId(vo.getUuid()); imageTransfer.setHref(basePath + ImageTransfersRouteHandler.BASE_ROUTE + "/" + vo.getUuid()); - imageTransfer.setActive(Boolean.toString(true)); + imageTransfer.setActive(Boolean.toString(vo.getProgress() != null && vo.getProgress() > 0 && vo.getProgress() < 100)); imageTransfer.setDirection(vo.getDirection().name()); imageTransfer.setFormat("cow"); - imageTransfer.setInactivityTimeout(Integer.toString(60)); + imageTransfer.setInactivityTimeout(Integer.toString(3600)); imageTransfer.setPhase(vo.getPhase().name()); + if (org.apache.cloudstack.backup.ImageTransfer.Phase.finished.equals(vo.getPhase())) { + imageTransfer.setPhase("finished_success"); + } else if (org.apache.cloudstack.backup.ImageTransfer.Phase.failed.equals(vo.getPhase())) { + imageTransfer.setPhase("finished_failed"); + } imageTransfer.setProxyUrl(vo.getTransferUrl()); imageTransfer.setShallow(Boolean.toString(false)); imageTransfer.setTimeoutPolicy("legacy"); @@ -61,14 +67,13 @@ public class ImageTransferVOToImageTransferConverter { VolumeJoinVO volumeVo = volumeResolver.apply(vo.getDiskId()); if (volumeVo != null) { imageTransfer.setDisk(Ref.of(basePath + DisksRouteHandler.BASE_ROUTE + "/" + volumeVo.getUuid(), volumeVo.getUuid())); + imageTransfer.setImage(Ref.of(null, 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")); + imageTransfer.setActions(new Actions(links)); return imageTransfer; } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/services/PkiResourceRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/services/PkiResourceRouteHandler.java new file mode 100644 index 00000000000..19b1b88d7f3 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/services/PkiResourceRouteHandler.java @@ -0,0 +1,173 @@ +// 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.services; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Enumeration; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.cloudstack.utils.server.ServerPropertiesUtil; +import org.apache.cloudstack.veeam.RouteHandler; +import org.apache.cloudstack.veeam.VeeamControlServlet; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.utils.component.ManagerBase; + +public class PkiResourceRouteHandler extends ManagerBase implements RouteHandler { + private static final String BASE_ROUTE = "/services/pki-resource"; + private static final String RESOURCE_KEY = "resource"; + private static final String RESOURCE_VALUE = "ca-certificate"; + private static final String FORMAT_KEY = "format"; + private static final String FORMAT_VALUE = "X509-PEM-CA"; + private static final Charset OUTPUT_CHARSET = StandardCharsets.ISO_8859_1; + + @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 sanitizedPath = getSanitizedPath(path); + if (sanitizedPath.equals(BASE_ROUTE) && "GET".equalsIgnoreCase(req.getMethod())) { + handleGet(req, resp, outFormat, io); + return; + } + + resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found"); + } + + protected void handleGet(HttpServletRequest req, HttpServletResponse resp, + Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException { + try { + final String resource = req.getParameter(RESOURCE_KEY); + final String format = req.getParameter(FORMAT_KEY); + + if (StringUtils.isNotBlank(resource) && !RESOURCE_VALUE.equals(resource)) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unsupported resource"); + return; + } + + if (StringUtils.isNotBlank(format) && !FORMAT_VALUE.equals(format)) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unsupported format"); + return; + } + + final String keystorePath = ServerPropertiesUtil.getKeystoreFile(); + final String keystorePassword = ServerPropertiesUtil.getKeystorePassword(); + + Path path = Path.of(keystorePath); + if (keystorePath.isBlank() || !Files.exists(path)) { + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "CloudStack HTTPS keystore not found"); + return; + } + + final X509Certificate caCert = + extractCaFromKeystore(path, keystorePassword); + + // DER encoding → browser downloads as .cer (oVirt behavior) + final byte[] pemBytes = + toPem(caCert).getBytes(OUTPUT_CHARSET); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setHeader("Cache-Control", "no-store"); + resp.setContentType("application/x-x509-ca-cert; charset=" + OUTPUT_CHARSET.name()); + resp.setHeader("Content-Disposition", + "attachment; filename=\"pki-resource.cer\""); + resp.setContentLength(pemBytes.length); + + try (OutputStream os = resp.getOutputStream()) { + os.write(pemBytes); + } + } catch (IOException | KeyStoreException | CertificateException | NoSuchAlgorithmException e) { + String msg = "Failed to retrieve server CA certificate"; + logger.error(msg, e); + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg); + } + } + + private static X509Certificate extractCaFromKeystore(Path ksPath, String ksPassword) + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { + + final String path = ksPath.toString().toLowerCase(); + final String storeType = + (path.endsWith(".p12") || path.endsWith(".pfx")) + ? "PKCS12" + : KeyStore.getDefaultType(); + + KeyStore ks = KeyStore.getInstance(storeType); + try (var in = Files.newInputStream(ksPath)) { + ks.load(in, ksPassword != null ? ksPassword.toCharArray() : new char[0]); + } + + // Prefer HTTPS keypair alias (one with a chain) + String alias = null; + Enumeration aliases = ks.aliases(); + while (aliases.hasMoreElements()) { + String a = aliases.nextElement(); + Certificate[] chain = ks.getCertificateChain(a); + if (chain != null && chain.length > 0) { + alias = a; + break; + } + } + + if (alias == null && ks.aliases().hasMoreElements()) { + alias = ks.aliases().nextElement(); + } + + if (alias == null) { + throw new IllegalStateException("No certificate aliases in keystore"); + } + + Certificate[] chain = ks.getCertificateChain(alias); + Certificate cert = + (chain != null && chain.length > 0) + ? chain[chain.length - 1] // root-most + : ks.getCertificate(alias); + + if (!(cert instanceof X509Certificate)) { + throw new IllegalStateException("Certificate is not X509"); + } + + return (X509Certificate) cert; + } + + private static String toPem(X509Certificate cert) throws CertificateEncodingException { + String base64 = Base64.getMimeEncoder(64, new byte[]{'\n'}) + .encodeToString(cert.getEncoded()); + return "-----BEGIN CERTIFICATE-----\n" + + base64 + + "\n-----END CERTIFICATE-----\n"; + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/PathUtil.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/PathUtil.java index 11a5f2b337d..b69748bf8bd 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/PathUtil.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/PathUtil.java @@ -17,46 +17,58 @@ package org.apache.cloudstack.veeam.utils; -import com.cloud.utils.Pair; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import com.cloud.utils.UuidUtils; public class PathUtil { - public static Pair extractIdAndSubPath(final String path, final String baseRoute) { + public static List extractIdAndSubPath(final String path, final String baseRoute) { - // baseRoute = "/api/datacenters" - if (!path.startsWith(baseRoute)) { - return null; - } + if (StringUtils.isBlank(path)) { + return null; + } - // Remove base route - String rest = path.substring(baseRoute.length()); + // Remove base route (be tolerant of trailing slash in baseRoute) + String rest = path; + if (StringUtils.isNotBlank(baseRoute)) { + String normalizedBase = baseRoute.endsWith("/") && baseRoute.length() > 1 + ? baseRoute.substring(0, baseRoute.length() - 1) + : baseRoute; + if (rest.startsWith(normalizedBase)) { + rest = rest.substring(normalizedBase.length()); + } + } - // Expect "" or "/{id}" or "/{id}/{sub}" - if (rest.isEmpty()) { - return null; // /api/datacenters (no id) - } + // Expect "/{id}" or "/{id}/..." (no empty segments) + if (StringUtils.isBlank(rest) || !rest.startsWith("/")) { + return null; // /api/datacenters (no id) or invalid format + } - if (!rest.startsWith("/")) { - return null; - } + rest = rest.substring(1); // remove leading '/' - rest = rest.substring(1); // remove leading '/' + if (StringUtils.isBlank(rest)) { + return null; + } - final String[] parts = rest.split("/", -1); + final String[] parts = rest.split("/", -1); - if (parts.length == 1) { - // /api/datacenters/{id} - if (parts[0].isEmpty()) return null; - return new Pair<>(parts[0], null); - } + // Collect non-blank segments + List validParts = new ArrayList<>(); + for (String part : parts) { + if (StringUtils.isNotBlank(part)) { + validParts.add(part); + } + } - if (parts.length == 2) { - // /api/datacenters/{id}/{subPath} - if (parts[0].isEmpty() || parts[1].isEmpty()) return null; - return new Pair<>(parts[0], parts[1]); - } + // Validate first segment is a UUID + if (validParts.isEmpty() || !UuidUtils.isUuid(validParts.get(0))) { + return null; + } - // deeper paths not handled here - return null; + return validParts; } } 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 1b549abcfce..b247550cf14 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 @@ -32,6 +32,7 @@ +