wip: changes for imagetransfer handling

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2026-01-27 23:58:09 +05:30
parent 10f65b67d7
commit 7b45d2e118
23 changed files with 980 additions and 122 deletions

View File

@ -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<String, String> checkAndRepairVolume(CheckAndRepairVolumeCmd cmd) throws ResourceAllocationException;
Long getVolumePhysicalSize(Storage.ImageFormat format, String path, String chainInfo);
Long getCustomDiskOfferingIdForVolumeUpload(Account owner, DataCenter zone);
}

View File

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

View File

@ -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<RouteHandler> routeHandlers;
public VeeamControlServlet(List<RouteHandler> 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();

View File

@ -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<Class<?>> 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<Role> 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<AccountVO> 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<Disk> listAllDisks() {
List<VolumeJoinVO> 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<ImageTransfer> listAllImageTransfers() {
List<ImageTransferVO> 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();
}
}
}

View File

@ -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<Disk> result = VolumeJoinVOToDiskConverter.toDiskList(listDisks());
final List<Disk> 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<VolumeJoinVO> 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);
}
}

View File

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

View File

@ -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<Long, HostJoinVO> hostResolver,
final Function<Long, VolumeJoinVO> 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<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"));
return imageTransfer;
}
public static List<ImageTransfer> toImageTransferList(List<? extends ImageTransferVO> vos,
final Function<Long, HostJoinVO> hostResolver,
final Function<Long, VolumeJoinVO> 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;
}
}

View File

@ -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

View File

@ -23,11 +23,11 @@ import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class Actions {
public List<ActionLink> link;
public List<Link> link;
public Actions() {}
public Actions(final List<ActionLink> link) {
public Actions(final List<Link> link) {
this.link = link;
}
}

View File

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

View File

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

View File

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

View File

@ -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> 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<ImageTransfer> getImageTransfer() {
return imageTransfer;
}
public static ActionLink post(final String rel, final String href) {
return new ActionLink(rel, href, "post");
public void setImageTransfer(List<ImageTransfer> imageTransfer) {
this.imageTransfer = imageTransfer;
}
}

View File

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

View File

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

View File

@ -31,6 +31,7 @@
<property name="typeClass" value="org.apache.cloudstack.veeam.RouteHandler" />
</bean>
<bean id="veeamControlSsoService" class="org.apache.cloudstack.veeam.sso.SsoService"/>
<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"/>
@ -39,9 +40,12 @@
<bean id="vnicProfilesRouteHandler" class="org.apache.cloudstack.veeam.api.VnicProfilesRouteHandler"/>
<bean id="vmsRouteHandler" class="org.apache.cloudstack.veeam.api.VmsRouteHandler"/>
<bean id="disksRouteHandler" class="org.apache.cloudstack.veeam.api.DisksRouteHandler"/>
<bean id="veeamControlSsoService" class="org.apache.cloudstack.veeam.sso.SsoService"/>
<bean id="imageTransfersRouteHandler" class="org.apache.cloudstack.veeam.api.ImageTransfersRouteHandler"/>
<bean id="veeamControlService" class="org.apache.cloudstack.veeam.VeeamControlServiceImpl" >
<property name="routeHandlers" value="#{routeHandlerRegistry.registered}" />
</bean>
<bean id="userResourceAdapter" class="org.apache.cloudstack.veeam.adapter.UserResourceAdapter"/>
</beans>

View File

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

View File

@ -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<VolumeJoinVO, Long> {
List<VolumeJoinVO> searchByIds(Long... ids);
List<VolumeJoinVO> listByInstanceId(long instanceId);
List<VolumeJoinVO> listByHypervisor(Hypervisor.HypervisorType hypervisorType);
}

View File

@ -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<VolumeJoinVO, VolumeResponse> implements VolumeJoinDao {
@ -379,4 +380,16 @@ public class VolumeJoinDaoImpl extends GenericDaoBaseWithTagInformation<VolumeJo
return search(sc, null);
}
@Override
public List<VolumeJoinVO> listByHypervisor(Hypervisor.HypervisorType hypervisorType) {
SearchBuilder<VolumeJoinVO> sb = createSearchBuilder();
sb.and("vmType", sb.entity().getVmType(), SearchCriteria.Op.EQ);
sb.and("hypervisorType", sb.entity().getHypervisorType(), SearchCriteria.Op.EQ);
sb.done();
SearchCriteria<VolumeJoinVO> sc = sb.create();
sc.setParameters("vmType", VirtualMachine.Type.User);
sc.setParameters("hypervisorType", hypervisorType);
return search(sc, null);
}
}

View File

@ -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<DiskOfferingVO> 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<VolumeVO, CloudRuntimeException>() {
@ -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<DiskOfferingVO> 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<String, String> 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()));
}
}

View File

@ -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) {

View File

@ -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

View File

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