veeam control changes

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2026-01-29 17:50:51 +05:30
parent 2bc3114120
commit 3460a5de99
13 changed files with 371 additions and 151 deletions

View File

@ -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()));

View File

@ -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<String, String> 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<String> 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<Cluster> 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;
}

View File

@ -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<String, String> 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<String> 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<DataCenter> 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) {

View File

@ -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<String, String> 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<String> 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<Disk> 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);

View File

@ -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<String, String> 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<String> 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<Host> 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) {

View File

@ -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<String, String> 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<String> 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<ImageTransfer> 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);
}
}

View File

@ -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<String, String> 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<String> 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<Network> 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;
}

View File

@ -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<String, String> 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<String> 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;
}

View File

@ -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<String, String> 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<String> 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<VnicProfile> 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;
}

View File

@ -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<Link> 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;
}

View File

@ -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<String> 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";
}
}

View File

@ -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<String, String> extractIdAndSubPath(final String path, final String baseRoute) {
public static List<String> 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<String> 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;
}
}

View File

@ -32,6 +32,7 @@
</bean>
<bean id="veeamControlSsoService" class="org.apache.cloudstack.veeam.sso.SsoService"/>
<bean id="pkiResourceRouteHandler" class="org.apache.cloudstack.veeam.services.PkiResourceRouteHandler"/>
<bean id="veeamControlApiService" class="org.apache.cloudstack.veeam.api.ApiService" />
<bean id="dataCentersRouteHandler" class="org.apache.cloudstack.veeam.api.DataCentersRouteHandler"/>
<bean id="clustersRouteHandler" class="org.apache.cloudstack.veeam.api.ClustersRouteHandler"/>