diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index 4145e2b89eb..f0640abf879 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -88,10 +88,14 @@ public interface AccountService { Account getActiveAccountById(long accountId); + Account getActiveAccountByUuid(String accountUuid); + Account getAccount(long accountId); User getActiveUser(long userId); + User getOneActiveUserForAccount(Account account); + User getUserIncludingRemoved(long userId); boolean isRootAdmin(Long accountId); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java index e11d20d0646..0e5d598505f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java @@ -85,6 +85,8 @@ public class AssignVMCmd extends BaseCmd { "In case no security groups are provided the Instance is part of the default security group.") private List securityGroupIdList; + private boolean skipNetwork = false; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -113,6 +115,34 @@ public class AssignVMCmd extends BaseCmd { return securityGroupIdList; } + public boolean isSkipNetwork() { + return skipNetwork; + } + + ///////////////////////////////////////////////////// + /////////////////// Setters ///////////////////////// + ///////////////////////////////////////////////////// + + public void setVirtualMachineId(Long virtualMachineId) { + this.virtualMachineId = virtualMachineId; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + public void setSkipNetwork(boolean skipNetwork) { + this.skipNetwork = skipNetwork; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java index f3985351228..f50abaf73c9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java @@ -70,6 +70,21 @@ public class AssignVolumeCmd extends BaseCmd implements UserCmd { return projectid; } + ///////////////////////////////////////////////////// + /////////////////// Setter/////////////////////////// + ///////////////////////////////////////////////////// + public void setVolumeId(Long volumeId) { + this.volumeId = volumeId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public void setProjectId(Long projectid) { + this.projectid = projectid; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java index 9f7a4ad6e05..9aba2ba97fd 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java @@ -50,4 +50,6 @@ public interface AsyncJobDao extends GenericDao { // Returns the number of pending jobs for the given Management server msids. // NOTE: This is the msid and NOT the id long countPendingNonPseudoJobs(Long... msIds); + + List listPendingJobIdsForAccount(long accountId); } diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java index a2f1f36b863..1dfb1738f0e 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java @@ -266,4 +266,14 @@ public class AsyncJobDaoImpl extends GenericDaoBase implements List results = customSearch(sc, null); return results.get(0); } + + @Override + public List listPendingJobIdsForAccount(long accountId) { + GenericSearchBuilder sb = createSearchBuilder(Long.class); + sb.and("accountId", sb.entity().getAccountId(), SearchCriteria.Op.EQ); + sb.selectFields(sb.entity().getId()); + SearchCriteria sc = sb.create(); + sc.setParameters("accountId", accountId); + return customSearch(sc, null); + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java index 04416559c57..4c0087cccef 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java @@ -88,8 +88,8 @@ public class LibvirtStartBackupCommandWrapper extends CommandWrapper ContextPath = new ConfigKey<>("Advanced", String.class, "integration.veeam.control.context.path", "/ovirt-engine", "Context path for Veeam Integration REST API server", false); - ConfigKey Username = new ConfigKey<>("Advanced", String.class, "integration.veeam.api.username", + ConfigKey Username = new ConfigKey<>("Advanced", String.class, "integration.veeam.control.api.username", "veeam", "Username for Basic Auth on Veeam Integration REST API server", true); - ConfigKey Password = new ConfigKey<>("Advanced", String.class, "integration.veeam.api.password", + ConfigKey Password = new ConfigKey<>("Advanced", String.class, "integration.veeam.control.api.password", "change-me", "Password for Basic Auth on Veeam Integration REST API server", true); + ConfigKey ServiceAccountId = new ConfigKey<>("Advanced", String.class, + "integration.veeam.control.service.account", "", + "ID of the service account used to perform operations on resources. " + + "Preferably an admin-level account with permissions to access resources across the environment " + + "and optionally assign them to other users.", + true); + ConfigKey InstanceRestoreAssignOwner = new ConfigKey<>("Advanced", Boolean.class, + "integration.veeam.instance.restore.assign.owner", + "false", "Attempt to assign restored Instance to the owner based on OVF and network " + + "details. If the assignment fails or set to false then the Instance will remain owned by the service " + + "account", true); } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServiceImpl.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServiceImpl.java index 12e6b58b1ff..683d0052f9d 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServiceImpl.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServiceImpl.java @@ -76,7 +76,9 @@ public class VeeamControlServiceImpl extends ManagerBase implements VeeamControl Port, ContextPath, Username, - Password + Password, + ServiceAccountId, + InstanceRestoreAssignOwner }; } } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java index c816ef41f31..d83c64504f5 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java @@ -42,6 +42,7 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.backup.DeleteVmCheckpointCmd; import org.apache.cloudstack.api.command.admin.backup.FinalizeBackupCmd; import org.apache.cloudstack.api.command.admin.backup.StartBackupCmd; +import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; import org.apache.cloudstack.api.command.admin.vm.DeployVMCmdByAdmin; import org.apache.cloudstack.api.command.user.job.QueryAsyncJobResultCmd; import org.apache.cloudstack.api.command.user.network.ListNetworksCmd; @@ -54,6 +55,8 @@ 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.vmsnapshot.CreateVMSnapshotCmd; import org.apache.cloudstack.api.command.user.vmsnapshot.DeleteVMSnapshotCmd; +import org.apache.cloudstack.api.command.user.vmsnapshot.RevertToVMSnapshotCmd; +import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd; 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; @@ -72,7 +75,6 @@ import org.apache.cloudstack.backup.dao.ImageTransferDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.jobs.dao.AsyncJobDao; import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; -import org.apache.cloudstack.jobs.JobInfo; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; @@ -116,7 +118,6 @@ import org.apache.cloudstack.veeam.api.dto.VnicProfile; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import com.cloud.api.query.dao.AsyncJobJoinDao; import com.cloud.api.query.dao.DataCenterJoinDao; @@ -138,9 +139,11 @@ import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DataCenterDao; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.NetworkModel; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; import com.cloud.offering.ServiceOffering; @@ -173,6 +176,9 @@ import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +// ToDo: fix list APIs to support pagination, etc +// ToDo: check access on objects + public class ServerAdapter extends ManagerBase { private static final String SERVICE_ACCOUNT_NAME = "veemserviceuser"; private static final String SERVICE_ACCOUNT_ROLE_NAME = "Veeam Service Role"; @@ -279,7 +285,8 @@ public class ServerAdapter extends ManagerBase { @Inject ResourceTagDao resourceTagDao; - //ToDo: check access on objects + @Inject + NetworkModel networkModel; protected static Tag getDummyTagByName(String name) { Tag tag = new Tag(); @@ -340,7 +347,7 @@ public class ServerAdapter extends ManagerBase { } } - protected Pair createServiceAccountIfNeeded() { + protected Pair getDefaultServiceAccount() { UserAccount userAccount = accountService.getActiveUserAccount(SERVICE_ACCOUNT_NAME, 1L); if (userAccount == null) { userAccount = createServiceAccount(); @@ -351,9 +358,64 @@ public class ServerAdapter extends ManagerBase { accountService.getActiveAccountById(userAccount.getAccountId())); } + protected Pair getServiceAccount() { + String serviceAccountUuid = VeeamControlService.ServiceAccountId.value(); + if (StringUtils.isEmpty(serviceAccountUuid)) { + throw new CloudRuntimeException("Service account is not configured, unable to proceed"); + } + Account account = accountService.getActiveAccountByUuid(serviceAccountUuid); + if (account == null) { + throw new CloudRuntimeException("Service account with ID " + serviceAccountUuid + " not found, unable to proceed"); + } + User user = accountService.getOneActiveUserForAccount(account); + if (user == null) { + throw new CloudRuntimeException("No active user found for service account with ID " + serviceAccountUuid); + } + return new Pair<>(user, account); + } + + protected void waitForJobCompletion(long jobId) { + long timeoutNanos = TimeUnit.MINUTES.toNanos(5); + final long deadline = System.nanoTime() + timeoutNanos; + long sleepMillis = 500; + while (true) { + AsyncJobVO job = asyncJobDao.findById(jobId); + if (job == null) { + logger.warn("Async job with ID {} not found", jobId); + return; + } + if (job.getStatus() == AsyncJobVO.Status.SUCCEEDED || job.getStatus() == AsyncJobVO.Status.FAILED) { + return; + } + if (System.nanoTime() > deadline) { + logger.warn("Timed out waiting for {} completion", job); + } + try { + Thread.sleep(sleepMillis); + // back off gradually to reduce DB pressure + sleepMillis = Math.min(5000, sleepMillis + 500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted while waiting for async job completion"); + } + } + } + + protected void waitForJobCompletion(AsyncJobJoinVO job) { + if (job == null) { + logger.warn("Async job not found"); + return; + } + if (job.getStatus() == AsyncJobVO.Status.SUCCEEDED.ordinal() || + job.getStatus() == AsyncJobVO.Status.FAILED.ordinal()) { + logger.warn("Async job with ID {} already completed with status {}", job.getId(), job.getStatus()); + } + waitForJobCompletion(job.getId()); + } + @Override public boolean start() { - createServiceAccountIfNeeded(); + getServiceAccount(); //find public custom disk offering return true; } @@ -445,7 +507,6 @@ public class ServerAdapter extends ManagerBase { } public List listAllInstances() { - // Todo: add filtering, pagination List vms = userVmJoinDao.listAll(); return UserVmJoinVOToVmConverter.toVmList(vms, this::getHostById); } @@ -512,7 +573,7 @@ public class ServerAdapter extends ManagerBase { bootType = ApiConstants.BootType.UEFI; bootMode = ApiConstants.BootMode.SECURE; } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { return createInstance(zoneId, clusterId, name, displayName, cpu, memory, userdata, bootType, bootMode); @@ -561,7 +622,7 @@ public class ServerAdapter extends ManagerBase { if (bootMode != null) { cmd.setBootMode(bootMode.toString()); } - // ToDo: handle other. + // ToDo: handle any other field? cmd.setHypervisor(Hypervisor.HypervisorType.KVM.name()); cmd.setBlankInstance(true); Map details = new HashMap<>(); @@ -581,19 +642,33 @@ public class ServerAdapter extends ManagerBase { } public Vm updateInstance(String uuid, Vm request) { - // ToDo: what to do?! + logger.warn("Received request to update VM with ID {}. No action, returning existing VM data.", uuid); return getInstance(uuid, false, false, false); } - public void deleteInstance(String uuid) { + public VmAction deleteInstance(String uuid) { UserVmVO vo = userVmDao.findByUuid(uuid); if (vo == null) { throw new InvalidParameterValueException("VM with ID " + uuid + " not found"); } + Pair serviceUserAccount = getServiceAccount(); + CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { - userVmService.destroyVm(vo.getId(), true); - } catch (ResourceUnavailableException e) { + DestroyVMCmd cmd = new DestroyVMCmd(); + cmd.setHttpMethod(BaseCmd.HTTPMethod.POST.name()); + ComponentContext.inject(cmd); + Map params = new HashMap<>(); + params.put(ApiConstants.ID, vo.getUuid()); + params.put(ApiConstants.EXPUNGE, Boolean.TRUE.toString()); + ApiServerService.AsyncCmdResult result = + apiServerService.processAsyncCmd(cmd, params, ctx, serviceUserAccount.first().getId(), + serviceUserAccount.second()); + AsyncJobJoinVO asyncJobJoinVO = asyncJobJoinDao.findById(result.jobId); + return AsyncJobJoinVOToJobConverter.toVmAction(asyncJobJoinVO, userVmJoinDao.findById(vo.getId())); + } catch (Exception e) { throw new CloudRuntimeException("Failed to delete VM: " + e.getMessage(), e); + } finally { + CallContext.unregister(); } } @@ -602,7 +677,7 @@ public class ServerAdapter extends ManagerBase { if (vo == null) { throw new InvalidParameterValueException("VM with ID " + uuid + " not found"); } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { StartVMCmd cmd = new StartVMCmd(); @@ -627,7 +702,7 @@ public class ServerAdapter extends ManagerBase { if (vo == null) { throw new InvalidParameterValueException("VM with ID " + uuid + " not found"); } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { StopVMCmd cmd = new StopVMCmd(); @@ -653,7 +728,7 @@ public class ServerAdapter extends ManagerBase { if (vo == null) { throw new InvalidParameterValueException("VM with ID " + uuid + " not found"); } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { StopVMCmd cmd = new StopVMCmd(); @@ -674,9 +749,13 @@ public class ServerAdapter extends ManagerBase { } } + protected Long getVolumePhysicalSize(VolumeJoinVO vo) { + return volumeApiService.getVolumePhysicalSize(vo.getFormat(), vo.getPath(), vo.getChainInfo()); + } + public List listAllDisks() { List kvmVolumes = volumeJoinDao.listByHypervisor(Hypervisor.HypervisorType.KVM); - return VolumeJoinVOToDiskConverter.toDiskList(kvmVolumes); + return VolumeJoinVOToDiskConverter.toDiskList(kvmVolumes, this::getVolumePhysicalSize); } public Disk getDisk(String uuid) { @@ -684,7 +763,7 @@ public class ServerAdapter extends ManagerBase { if (vo == null) { throw new InvalidParameterValueException("Disk with ID " + uuid + " not found"); } - return VolumeJoinVOToDiskConverter.toDisk(vo); + return VolumeJoinVOToDiskConverter.toDisk(vo, this::getVolumePhysicalSize); } public Disk copyDisk(String uuid) { @@ -723,7 +802,7 @@ public class ServerAdapter extends ManagerBase { protected List listDiskAttachmentsByInstanceId(final long instanceId) { List kvmVolumes = volumeJoinDao.listByInstanceId(instanceId); - return VolumeJoinVOToDiskConverter.toDiskAttachmentList(kvmVolumes); + return VolumeJoinVOToDiskConverter.toDiskAttachmentList(kvmVolumes, this::getVolumePhysicalSize); } public List listDiskAttachmentsByInstanceUuid(final String uuid) { @@ -734,7 +813,35 @@ public class ServerAdapter extends ManagerBase { return listDiskAttachmentsByInstanceId(vo.getId()); } - public DiskAttachment handleInstanceAttachDisk(final String vmUuid, final DiskAttachment request) { + protected void assignVolumeToAccount(VolumeVO volumeVO, long accountId, Pair serviceUserAccount) { + Account account = accountService.getActiveAccountById(accountId); + if (account == null) { + throw new InvalidParameterValueException("Account with ID " + accountId + " not found"); + } + CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); + try { + AssignVolumeCmd cmd = new AssignVolumeCmd(); + ComponentContext.inject(cmd); + Map params = new HashMap<>(); + cmd.setVolumeId(volumeVO.getId()); + params.put(ApiConstants.VOLUME_ID, volumeVO.getUuid()); + if (Account.Type.PROJECT.equals(account.getType())) { + cmd.setProjectId(account.getId()); + params.put(ApiConstants.PROJECT_ID, account.getUuid()); + } else { + cmd.setAccountId(account.getId()); + params.put(ApiConstants.ACCOUNT_ID, account.getUuid()); + } + cmd.setFullUrlParams(params); + volumeApiService.assignVolumeToAccount(cmd); + } catch (ResourceAllocationException | CloudRuntimeException e) { + logger.error("Failed to assign {} to {}: {}", volumeVO, account, e.getMessage(), e); + } finally { + CallContext.unregister(); + } + } + + public DiskAttachment attachInstanceDisk(final String vmUuid, final DiskAttachment request) { UserVmVO vmVo = userVmDao.findByUuid(vmUuid); if (vmVo == null) { throw new InvalidParameterValueException("VM with ID " + vmUuid + " not found"); @@ -746,12 +853,25 @@ public class ServerAdapter extends ManagerBase { if (volumeVO == null) { throw new InvalidParameterValueException("Disk with ID " + request.getDisk().getId() + " not found"); } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); + if (vmVo.getAccountId() != volumeVO.getAccountId()) { + if (VeeamControlService.InstanceRestoreAssignOwner.value()) { + assignVolumeToAccount(volumeVO, vmVo.getAccountId(), serviceUserAccount); + } else { + throw new PermissionDeniedException("Disk with ID " + request.getDisk().getId() + + " belongs to a different account and cannot be attached to the VM"); + } + } CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { - Volume volume = volumeApiService.attachVolumeToVM(vmVo.getId(), volumeVO.getId(), 0L, false); + Long deviceId = null; + List volumes = volumeDao.findUsableVolumesForInstance(vmVo.getId()); + if (CollectionUtils.isEmpty(volumes)) { + deviceId = 0L; + } + Volume volume = volumeApiService.attachVolumeToVM(vmVo.getId(), volumeVO.getId(), deviceId, false); VolumeJoinVO attachedVolumeVO = volumeJoinDao.findById(volume.getId()); - return VolumeJoinVOToDiskConverter.toDiskAttachment(attachedVolumeVO); + return VolumeJoinVOToDiskConverter.toDiskAttachment(attachedVolumeVO, this::getVolumePhysicalSize); } finally { CallContext.unregister(); } @@ -765,7 +885,7 @@ public class ServerAdapter extends ManagerBase { volumeApiService.deleteVolume(vo.getId(), accountService.getSystemAccount()); } - public Disk handleCreateDisk(Disk request) { + public Disk createDisk(Disk request) { if (request == null) { throw new InvalidParameterValueException("Request disk data is empty"); } @@ -805,7 +925,7 @@ public class ServerAdapter extends ManagerBase { initialSize = Long.parseLong(request.getInitialSize()); } catch (NumberFormatException ignored) {} } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); Account serviceAccount = serviceUserAccount.second(); DataCenterVO zone = dataCenterDao.findById(pool.getDataCenterId()); if (zone == null || !Grouping.AllocationState.Enabled.equals(zone.getAllocationState())) { @@ -815,7 +935,7 @@ public class ServerAdapter extends ManagerBase { if (diskOfferingId == null) { throw new CloudRuntimeException("Failed to find custom offering for disk" + zone.getName()); } - CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); + CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { return createDisk(serviceAccount, pool, name, diskOfferingId, provisionedSizeInGb, initialSize); } finally { @@ -841,7 +961,7 @@ public class ServerAdapter extends ManagerBase { } // Implementation for creating a Disk resource - return VolumeJoinVOToDiskConverter.toDisk(volumeJoinDao.findById(volume.getId())); + return VolumeJoinVOToDiskConverter.toDisk(volumeJoinDao.findById(volume.getId()), this::getVolumePhysicalSize); } protected List listNicsByInstance(final long instanceId, final String instanceUuid) { @@ -861,7 +981,42 @@ public class ServerAdapter extends ManagerBase { return listNicsByInstance(vo.getId(), vo.getUuid()); } - public Nic handleAttachInstanceNic(final String vmUuid, final Nic request) { + protected boolean accountCannotAccessNetwork(NetworkVO networkVO, long accountId) { + Account account = accountService.getActiveAccountById(accountId); + try { + networkModel.checkNetworkPermissions(account, networkVO); + return false; + } catch (CloudRuntimeException e) { + logger.debug("{} cannot access {}: {}", account, networkVO, e.getMessage()); + } + return true; + } + + protected void assignVmToAccount(UserVmVO vmVO, long accountId, Pair serviceUserAccount) { + Account account = accountService.getActiveAccountById(accountId); + if (account == null) { + throw new InvalidParameterValueException("Account with ID " + accountId + " not found"); + } + CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); + try { + AssignVMCmd cmd = new AssignVMCmd(); + ComponentContext.inject(cmd); + cmd.setVirtualMachineId(vmVO.getId()); + cmd.setAccountName(account.getAccountName()); + cmd.setDomainId(account.getDomainId()); + if (Account.Type.PROJECT.equals(account.getType())) { + cmd.setProjectId(account.getId()); + } + userVmService.moveVmToUser(cmd); + } catch (ResourceAllocationException | CloudRuntimeException | ResourceUnavailableException | + InsufficientCapacityException e) { + logger.error("Failed to assign {} to {}: {}", vmVO, account, e.getMessage(), e); + } finally { + CallContext.unregister(); + } + } + + public Nic attachInstanceNic(final String vmUuid, final Nic request) { UserVmVO vmVo = userVmDao.findByUuid(vmUuid); if (vmVo == null) { throw new InvalidParameterValueException("VM with ID " + vmUuid + " not found"); @@ -873,7 +1028,13 @@ public class ServerAdapter extends ManagerBase { if (networkVO == null) { throw new InvalidParameterValueException("VNic profile " + request.getVnicProfile().getId() + " not found"); } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); + if (vmVo.getAccountId() != networkVO.getAccountId() && + networkVO.getAccountId() != Account.ACCOUNT_ID_SYSTEM && + VeeamControlService.InstanceRestoreAssignOwner.value() && + accountCannotAccessNetwork(networkVO, vmVo.getAccountId())) { + assignVmToAccount(vmVo, networkVO.getAccountId(), serviceUserAccount); + } CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { AddNicToVMCmd cmd = new AddNicToVMCmd(); @@ -907,7 +1068,7 @@ public class ServerAdapter extends ManagerBase { return ImageTransferVOToImageTransferConverter.toImageTransfer(vo, this::getHostById, this::getVolumeById); } - public ImageTransfer handleCreateImageTransfer(ImageTransfer request) { + public ImageTransfer createImageTransfer(ImageTransfer request) { if (request == null) { throw new InvalidParameterValueException("Request image transfer data is empty"); } @@ -934,7 +1095,7 @@ public class ServerAdapter extends ManagerBase { return createImageTransfer(backupId, volumeVO.getId(), direction, format); } - public boolean handleCancelImageTransfer(String uuid) { + public boolean cancelImageTransfer(String uuid) { ImageTransferVO vo = imageTransferDao.findByUuid(uuid); if (vo == null) { throw new InvalidParameterValueException("Image transfer with ID " + uuid + " not found"); @@ -942,7 +1103,7 @@ public class ServerAdapter extends ManagerBase { return incrementalBackupService.cancelImageTransfer(vo.getId()); } - public boolean handleFinalizeImageTransfer(String uuid) { + public boolean finalizeImageTransfer(String uuid) { ImageTransferVO vo = imageTransferDao.findByUuid(uuid); if (vo == null) { throw new InvalidParameterValueException("Image transfer with ID " + uuid + " not found"); @@ -951,7 +1112,7 @@ public class ServerAdapter extends ManagerBase { } private ImageTransfer createImageTransfer(Long backupId, Long volumeId, Direction direction, Format format) { - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { org.apache.cloudstack.backup.ImageTransfer imageTransfer = @@ -992,8 +1153,10 @@ public class ServerAdapter extends ManagerBase { } public List listAllJobs() { - // ToDo: find active jobs for service account - return Collections.emptyList(); + Pair serviceUserAccount = getServiceAccount(); + List jobIds = asyncJobDao.listPendingJobIdsForAccount(serviceUserAccount.second().getId()); + List jobJoinVOs = asyncJobJoinDao.listByIds(jobIds); + return AsyncJobJoinVOToJobConverter.toJobList(jobJoinVOs); } public Job getJob(String uuid) { @@ -1013,12 +1176,12 @@ public class ServerAdapter extends ManagerBase { return VmSnapshotVOToSnapshotConverter.toSnapshotList(snapshots, vo.getUuid()); } - public Snapshot handleCreateInstanceSnapshot(final String vmUuid, final Snapshot request) { + public Snapshot createInstanceSnapshot(final String vmUuid, final Snapshot request) { UserVmVO vmVo = userVmDao.findByUuid(vmUuid); if (vmVo == null) { throw new InvalidParameterValueException("VM with ID " + vmUuid + " not found"); } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { CreateVMSnapshotCmd cmd = new CreateVMSnapshotCmd(); @@ -1060,7 +1223,7 @@ public class ServerAdapter extends ManagerBase { if (vo == null) { throw new InvalidParameterValueException("Snapshot with ID " + uuid + " not found"); } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { DeleteVMSnapshotCmd cmd = new DeleteVMSnapshotCmd(); @@ -1074,10 +1237,10 @@ public class ServerAdapter extends ManagerBase { if (jobVo == null) { throw new CloudRuntimeException("Failed to find job for snapshot deletion"); } - action = AsyncJobJoinVOToJobConverter.toAction(jobVo); - if (async) { - // ToDo: wait for job completion? + if (!async) { + waitForJobCompletion(jobVo); } + action = AsyncJobJoinVOToJobConverter.toAction(jobVo); } catch (Exception e) { throw new CloudRuntimeException("Failed to delete snapshot: " + e.getMessage(), e); } finally { @@ -1086,34 +1249,33 @@ public class ServerAdapter extends ManagerBase { return action; } - public ResourceAction revertToSnapshot(String uuid) { - throw new InvalidParameterValueException("revertToSnapshot with ID " + uuid + " not implemented"); -// ResourceAction action = null; -// VMSnapshotVO vo = vmSnapshotDao.findByUuid(uuid); -// if (vo == null) { -// throw new InvalidParameterValueException("Snapshot with ID " + uuid + " not found"); -// } -// Pair serviceUserAccount = createServiceAccountIfNeeded(); -// CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); -// try { -// RevertToVMSnapshotCmd cmd = new RevertToVMSnapshotCmd(); -// ComponentContext.inject(cmd); -// Map params = new HashMap<>(); -// params.put(ApiConstants.VM_SNAPSHOT_ID, vo.getUuid()); -// ApiServerService.AsyncCmdResult result = -// apiServerService.processAsyncCmd(cmd, params, ctx, serviceUserAccount.first().getId(), -// serviceUserAccount.second()); -// AsyncJobJoinVO jobVo = asyncJobJoinDao.findById(result.jobId); -// if (jobVo == null) { -// throw new CloudRuntimeException("Failed to find job for snapshot revert"); -// } -// action = AsyncJobJoinVOToJobConverter.toAction(jobVo); -// } catch (Exception e) { -// throw new CloudRuntimeException("Failed to revert to snapshot: " + e.getMessage(), e); -// } finally { -// CallContext.unregister(); -// } -// return action; + public ResourceAction revertInstanceToSnapshot(String uuid) { + ResourceAction action = null; + VMSnapshotVO vo = vmSnapshotDao.findByUuid(uuid); + if (vo == null) { + throw new InvalidParameterValueException("Snapshot with ID " + uuid + " not found"); + } + Pair serviceUserAccount = getServiceAccount(); + CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); + try { + RevertToVMSnapshotCmd cmd = new RevertToVMSnapshotCmd(); + ComponentContext.inject(cmd); + Map params = new HashMap<>(); + params.put(ApiConstants.VM_SNAPSHOT_ID, vo.getUuid()); + ApiServerService.AsyncCmdResult result = + apiServerService.processAsyncCmd(cmd, params, ctx, serviceUserAccount.first().getId(), + serviceUserAccount.second()); + AsyncJobJoinVO jobVo = asyncJobJoinDao.findById(result.jobId); + if (jobVo == null) { + throw new CloudRuntimeException("Failed to find job for snapshot revert"); + } + action = AsyncJobJoinVOToJobConverter.toAction(jobVo); + } catch (Exception e) { + throw new CloudRuntimeException("Failed to revert to snapshot: " + e.getMessage(), e); + } finally { + CallContext.unregister(); + } + return action; } public List listBackupsByInstanceUuid(final String uuid) { @@ -1130,7 +1292,7 @@ public class ServerAdapter extends ManagerBase { if (vmVo == null) { throw new InvalidParameterValueException("VM with ID " + vmUuid + " not found"); } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { StartBackupCmd cmd = new StartBackupCmd(); @@ -1156,42 +1318,6 @@ public class ServerAdapter extends ManagerBase { } } - @Nullable - private BackupVO getBackupFromJob(ApiServerService.AsyncCmdResult result, UserVmVO vmVo) { - AsyncJobVO jobVo = null; - // wait for job to complete and get backup ID - long timeoutNanos = TimeUnit.MINUTES.toNanos(2); - final long deadline = System.nanoTime() + timeoutNanos; - long sleepMillis = 1000; - while (System.nanoTime() < deadline) { - jobVo = asyncJobDao.findByIdIncludingRemoved(result.jobId); - if (jobVo == null) { - throw new CloudRuntimeException("Failed to find job for backup creation"); - } - if (!JobInfo.Status.IN_PROGRESS.equals(jobVo.getStatus())) { - break; - } - try { - Thread.sleep(sleepMillis); - // back off gradually to reduce DB pressure - sleepMillis = Math.min(5000, sleepMillis + 500); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new CloudRuntimeException("Interrupted while waiting for backup creation job", ie); - } - } - // if still in progress after timeout, fail fast - if (jobVo != null && JobInfo.Status.IN_PROGRESS.equals(jobVo.getStatus())) { - throw new CloudRuntimeException("Timed out waiting for backup creation job"); - } - BackupVO vo = null; - List backups = backupDao.searchByVmIds(List.of(vmVo.getId())); - if (CollectionUtils.isNotEmpty(backups)) { - vo = backups.get(0); - } - return vo; - } - public Backup getBackup(String uuid) { BackupVO vo = backupDao.findByUuidIncludingRemoved(uuid); if (vo == null) { @@ -1203,12 +1329,7 @@ public class ServerAdapter extends ManagerBase { public List listDisksByBackupUuid(final String uuid) { throw new InvalidParameterValueException("List Backup Disks with ID " + uuid + " not implemented"); -// ToDo: implement -// BackupVO vo = backupDao.findByUuid(uuid); -// if (vo == null) { -// throw new InvalidParameterValueException("Backup with ID " + uuid + " not found"); -// } -// return VolumeJoinVOToDiskConverter.toDiskList(volumes); + // This won't be feasible with current structure } public Backup finalizeBackup(final String vmUuid, final String backupUuid) { @@ -1220,7 +1341,7 @@ public class ServerAdapter extends ManagerBase { if (backup == null) { throw new InvalidParameterValueException("Backup with ID " + backupUuid + " not found"); } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext ctx = CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { FinalizeBackupCmd cmd = new FinalizeBackupCmd(); @@ -1271,7 +1392,7 @@ public class ServerAdapter extends ManagerBase { logger.warn("Checkpoint ID {} does not match active checkpoint for VM {}", checkpointId, vmUuid); return; } - Pair serviceUserAccount = createServiceAccountIfNeeded(); + Pair serviceUserAccount = getServiceAccount(); CallContext.register(serviceUserAccount.first(), serviceUserAccount.second()); try { DeleteVmCheckpointCmd cmd = new DeleteVmCheckpointCmd(); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DisksRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DisksRouteHandler.java index 7ba8daf2865..f0fc1368d56 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DisksRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DisksRouteHandler.java @@ -130,7 +130,7 @@ public class DisksRouteHandler extends ManagerBase implements RouteHandler { String data = RouteHandler.getRequestData(req, logger); try { Disk request = io.getMapper().jsonMapper().readValue(data, Disk.class); - Disk response = serverAdapter.handleCreateDisk(request); + Disk response = serverAdapter.createDisk(request); io.getWriter().write(resp, HttpServletResponse.SC_CREATED, response, outFormat); } catch (JsonProcessingException | CloudRuntimeException e) { io.badRequest(resp, e.getMessage(), outFormat); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandler.java index 6a26d54beaf..33371bc3c35 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandler.java @@ -115,7 +115,7 @@ public class ImageTransfersRouteHandler extends ManagerBase implements RouteHand String data = RouteHandler.getRequestData(req, logger); try { ImageTransfer request = io.getMapper().jsonMapper().readValue(data, ImageTransfer.class); - ImageTransfer response = serverAdapter.handleCreateImageTransfer(request); + ImageTransfer response = serverAdapter.createImageTransfer(request); io.getWriter().write(resp, HttpServletResponse.SC_CREATED, response, outFormat); } catch (JsonProcessingException | CloudRuntimeException e) { io.badRequest(resp, e.getMessage(), outFormat); @@ -128,27 +128,27 @@ public class ImageTransfersRouteHandler extends ManagerBase implements RouteHand ImageTransfer response = serverAdapter.getImageTransfer(id); io.getWriter().write(resp, HttpServletResponse.SC_OK, response, outFormat); } catch (InvalidParameterValueException e) { - io.notFound(resp, e.getMessage(), outFormat); + io.badRequest(resp, e.getMessage(), outFormat); } } protected void handleCancelById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { - serverAdapter.handleCancelImageTransfer(id); + serverAdapter.cancelImageTransfer(id); io.getWriter().write(resp, HttpServletResponse.SC_OK, "Image transfer cancelled successfully", outFormat); - } catch (InvalidParameterValueException e) { - io.notFound(resp, e.getMessage(), outFormat); + } catch (CloudRuntimeException e) { + io.badRequest(resp, e.getMessage(), outFormat); } } protected void handleFinalizeById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { - serverAdapter.handleFinalizeImageTransfer(id); + serverAdapter.finalizeImageTransfer(id); io.getWriter().write(resp, HttpServletResponse.SC_OK, "Image transfer finalized successfully", outFormat); } catch (CloudRuntimeException e) { - io.notFound(resp, e.getMessage(), outFormat); + io.badRequest(resp, e.getMessage(), outFormat); } } } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java index dba1c2bd169..22c8286878d 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java @@ -345,15 +345,15 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { protected void handleDeleteById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { - serverAdapter.deleteInstance(id); - io.getWriter().write(resp, HttpServletResponse.SC_OK, "", outFormat); + VmAction vm = serverAdapter.deleteInstance(id); + io.getWriter().write(resp, HttpServletResponse.SC_OK, vm, outFormat); } catch (CloudRuntimeException e) { io.notFound(resp, e.getMessage(), outFormat); } } - protected void handleStartVmById(final String id, final HttpServletRequest req, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, - final VeeamControlServlet io) throws IOException { + protected void handleStartVmById(final String id, final HttpServletRequest req, final HttpServletResponse resp, + final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { VmAction vm = serverAdapter.startInstance(id); io.getWriter().write(resp, HttpServletResponse.SC_ACCEPTED, vm, outFormat); @@ -362,8 +362,8 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { } } - protected void handleStopVmById(final String id, final HttpServletRequest req, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, - final VeeamControlServlet io) throws IOException { + protected void handleStopVmById(final String id, final HttpServletRequest req, final HttpServletResponse resp, + final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { VmAction vm = serverAdapter.stopInstance(id); io.getWriter().write(resp, HttpServletResponse.SC_ACCEPTED, vm, outFormat); @@ -372,8 +372,8 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { } } - protected void handleShutdownVmById(final String id, final HttpServletRequest req, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, - final VeeamControlServlet io) throws IOException { + protected void handleShutdownVmById(final String id, final HttpServletRequest req, final HttpServletResponse resp, + final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { VmAction vm = serverAdapter.shutdownInstance(id); io.getWriter().write(resp, HttpServletResponse.SC_ACCEPTED, vm, outFormat); @@ -382,8 +382,8 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { } } - protected void handleGetDiskAttachmentsByVmId(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, - final VeeamControlServlet io) throws IOException { + protected void handleGetDiskAttachmentsByVmId(final String id, final HttpServletResponse resp, + final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { List disks = serverAdapter.listDiskAttachmentsByInstanceUuid(id); NamedList response = NamedList.of("disk_attachment", disks); @@ -399,15 +399,15 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { String data = RouteHandler.getRequestData(req, logger); try { DiskAttachment request = io.getMapper().jsonMapper().readValue(data, DiskAttachment.class); - DiskAttachment response = serverAdapter.handleInstanceAttachDisk(id, request); + DiskAttachment response = serverAdapter.attachInstanceDisk(id, request); io.getWriter().write(resp, HttpServletResponse.SC_CREATED, response, outFormat); } catch (JsonProcessingException | CloudRuntimeException e) { io.badRequest(resp, e.getMessage(), outFormat); } } - protected void handleGetNicsByVmId(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, - final VeeamControlServlet io) throws IOException { + protected void handleGetNicsByVmId(final String id, final HttpServletResponse resp, + final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { List nics = serverAdapter.listNicsByInstanceUuid(id); NamedList response = NamedList.of("nic", nics); @@ -423,7 +423,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { String data = RouteHandler.getRequestData(req, logger); try { Nic request = io.getMapper().jsonMapper().readValue(data, Nic.class); - Nic response = serverAdapter.handleAttachInstanceNic(id, request); + Nic response = serverAdapter.attachInstanceNic(id, request); io.getWriter().write(resp, HttpServletResponse.SC_CREATED, response, outFormat); } catch (JsonProcessingException | CloudRuntimeException e) { io.badRequest(resp, e.getMessage(), outFormat); @@ -447,7 +447,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { String data = RouteHandler.getRequestData(req, logger); try { Snapshot request = io.getMapper().jsonMapper().readValue(data, Snapshot.class); - Snapshot response = serverAdapter.handleCreateInstanceSnapshot(id, request); + Snapshot response = serverAdapter.createInstanceSnapshot(id, request); io.getWriter().write(resp, HttpServletResponse.SC_ACCEPTED, response, outFormat); } catch (JsonProcessingException | CloudRuntimeException e) { io.badRequest(resp, e.getMessage(), outFormat); @@ -455,7 +455,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { } protected void handleGetSnapshotById(final String id, final HttpServletResponse resp, - final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { + final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { try { Snapshot response = serverAdapter.getSnapshot(id); io.getWriter().write(resp, HttpServletResponse.SC_OK, response, outFormat); @@ -484,9 +484,13 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler { protected void handleRestoreSnapshotById(final String id, final HttpServletRequest req, final HttpServletResponse resp, final Negotiation.OutFormat outFormat, final VeeamControlServlet io) throws IOException { - //ToDo: implement String data = RouteHandler.getRequestData(req, logger); - io.badRequest(resp, "Not implemented", outFormat); + try { + ResourceAction response = serverAdapter.revertInstanceToSnapshot(id); + io.getWriter().write(resp, HttpServletResponse.SC_ACCEPTED, response, outFormat); + } catch (CloudRuntimeException e) { + io.badRequest(resp, e.getMessage(), outFormat); + } } protected void handleGetBackupsByVmId(final String id, final HttpServletResponse resp, diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/AsyncJobJoinVOToJobConverter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/AsyncJobJoinVOToJobConverter.java index 49bf1f1caba..625c9d9e469 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/AsyncJobJoinVOToJobConverter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/AsyncJobJoinVOToJobConverter.java @@ -18,6 +18,8 @@ package org.apache.cloudstack.veeam.api.converter; import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import org.apache.cloudstack.jobs.JobInfo; import org.apache.cloudstack.veeam.VeeamControlService; @@ -83,6 +85,10 @@ public class AsyncJobJoinVOToJobConverter { return job; } + public static List toJobList(List vos) { + return vos.stream().map(AsyncJobJoinVOToJobConverter::toJob).collect(Collectors.toList()); + } + protected static void fillAction(final ResourceAction action, final AsyncJobJoinVO vo) { final String basePath = VeeamControlService.ContextPath.value(); action.setJob(Ref.of(basePath + JobsRouteHandler.BASE_ROUTE + vo.getUuid(), vo.getUuid())); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverter.java index 6c7c8bddd79..42431dc357b 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverter.java @@ -160,16 +160,16 @@ public final class UserVmJoinVOToVmConverter { basePath + ApiService.BASE_ROUTE + "/cpuprofiles/" + src.getServiceOfferingUuid(), src.getServiceOfferingUuid())); if (allContent) { - dst.setInitialization(getOvfInitialization(dst)); + dst.setInitialization(getOvfInitialization(dst, src)); } return dst; } - private static Vm.Initialization getOvfInitialization(Vm vm) { + private static Vm.Initialization getOvfInitialization(Vm vm, UserVmJoinVO vo) { final Vm.Initialization.Configuration configuration = new Vm.Initialization.Configuration(); configuration.setType("ovf"); - configuration.setData(OvfXmlUtil.toXml(vm)); + configuration.setData(OvfXmlUtil.toXml(vm, vo)); final Vm.Initialization initialization = new Vm.Initialization(); initialization.setConfiguration(configuration); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverter.java index 497f4d7f441..b1be9b98804 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverter.java @@ -19,6 +19,7 @@ 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.Backup; @@ -34,14 +35,12 @@ import org.apache.cloudstack.veeam.api.dto.Ref; import org.apache.cloudstack.veeam.api.dto.StorageDomain; import org.apache.cloudstack.veeam.api.dto.Vm; -import com.cloud.api.ApiDBUtils; import com.cloud.api.query.vo.VolumeJoinVO; import com.cloud.storage.Storage; import com.cloud.storage.Volume; -import com.cloud.storage.VolumeStats; public class VolumeJoinVOToDiskConverter { - public static Disk toDisk(final VolumeJoinVO vol) { + public static Disk toDisk(final VolumeJoinVO vol, final Function physicalSizeResolver) { final Disk disk = new Disk(); final String basePath = VeeamControlService.ContextPath.value(); final String apiBasePath = basePath + ApiService.BASE_ROUTE; @@ -64,19 +63,12 @@ public class VolumeJoinVOToDiskConverter { disk.setProvisionedSize(String.valueOf(size)); disk.setActualSize(String.valueOf(actualSize)); disk.setTotalSize(String.valueOf(size)); - VolumeStats vs = null; - if (List.of(Storage.ImageFormat.VHD, Storage.ImageFormat.QCOW2, Storage.ImageFormat.RAW).contains(vol.getFormat())) { - if (vol.getPath() != null) { - vs = ApiDBUtils.getVolumeStatistics(vol.getPath()); - } - } else if (vol.getFormat() == Storage.ImageFormat.OVA) { - if (vol.getChainInfo() != null) { - vs = ApiDBUtils.getVolumeStatistics(vol.getChainInfo()); - } + Long physicalSize = null; + if (physicalSizeResolver != null) { + physicalSize = physicalSizeResolver.apply(vol); } - if (vs != null) { - disk.setTotalSize(String.valueOf(vs.getVirtualSize())); - disk.setActualSize(String.valueOf(vs.getPhysicalSize())); + if (physicalSize != null) { + disk.setActualSize(String.valueOf(physicalSize)); } // Disk format @@ -122,9 +114,10 @@ public class VolumeJoinVOToDiskConverter { return disk; } - public static List toDiskList(final List srcList) { + public static List toDiskList(final List srcList, + final Function physicalSizeResolver) { return srcList.stream() - .map(VolumeJoinVOToDiskConverter::toDisk) + .map(vo -> toDisk(vo, physicalSizeResolver)) .collect(Collectors.toList()); } @@ -143,7 +136,8 @@ public class VolumeJoinVOToDiskConverter { return disks; } - public static DiskAttachment toDiskAttachment(final VolumeJoinVO vol) { + public static DiskAttachment toDiskAttachment(final VolumeJoinVO vol, + final Function physicalSizeResolver) { final DiskAttachment da = new DiskAttachment(); final String basePath = VeeamControlService.ContextPath.value(); @@ -154,7 +148,7 @@ public class VolumeJoinVOToDiskConverter { da.setHref(da.getVm().getHref() + "/diskattachments/" + diskAttachmentId);; // Links - da.setDisk(toDisk(vol)); + da.setDisk(toDisk(vol, physicalSizeResolver)); // Properties da.setActive("true"); @@ -167,9 +161,10 @@ public class VolumeJoinVOToDiskConverter { return da; } - public static List toDiskAttachmentList(final List srcList) { + public static List toDiskAttachmentList(final List srcList, + final Function physicalSizeResolver) { return srcList.stream() - .map(VolumeJoinVOToDiskConverter::toDiskAttachment) + .map(vo -> toDiskAttachment(vo, physicalSizeResolver)) .collect(Collectors.toList()); } @@ -190,9 +185,9 @@ public class VolumeJoinVOToDiskConverter { if (state == null) { return "ok"; } - switch (state.name().toLowerCase()) { - case "ready": - case "allocated": + switch (state) { + case Ready: + case Allocated: return "ok"; default: return "locked"; diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtil.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtil.java index b4bc8517a80..ebee1e242d6 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtil.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtil.java @@ -42,6 +42,8 @@ import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import com.cloud.api.query.vo.UserVmJoinVO; + public class OvfXmlUtil { private static final String NS_OVF = "http://schemas.dmtf.org/ovf/envelope/1/"; @@ -58,7 +60,7 @@ public class OvfXmlUtil { return sdf; }); - public static String toXml(final Vm vm) { + public static String toXml(final Vm vm, final UserVmJoinVO vo) { final String vmId = vm.getId(); final String vmName = vm.getName(); final String vmDesc = defaultString(vm.getDescription()); @@ -169,6 +171,32 @@ public class OvfXmlUtil { } sb.append(""); + if (vo != null) { + // -- Add a section for CloudStack-specific metadata that some consumers might look for (e.g. for import back into CloudStack) --- + // Add CloudStack-specific metadata section + sb.append("
"); + sb.append("CloudStack specific metadata"); + sb.append(""); + sb.append("").append(vo.getAccountUuid()).append(""); + sb.append("").append(vo.getDomainUuid()).append(""); + sb.append("").append(escapeText(vo.getProjectUuid())).append(""); + sb.append("").append(vo.getServiceOfferingUuid()).append(""); + sb.append(""); + for (DiskAttachment da : diskAttachments(vm)) { + if (da == null || da.getDisk() == null || StringUtils.isBlank(da.getDisk().getId())) { + continue; + } + final org.apache.cloudstack.veeam.api.dto.Disk d = da.getDisk(); + sb.append(""); + sb.append("").append(escapeText(d.getId())).append(""); + sb.append("").append(d.getDiskProfile().getId()).append(""); + sb.append(""); + } + sb.append(""); + sb.append(""); + sb.append("
"); + } + // --- Content / VirtualSystem --- sb.append(""); sb.append("").append(escapeText(vmName)).append(""); @@ -191,7 +219,7 @@ public class OvfXmlUtil { sb.append("false"); sb.append("false"); sb.append("0"); - sb.append("").append(ZERO_UUID).append(""); + sb.append("").append(vo.getAccountUuid()).append(""); sb.append("0"); sb.append("").append(escapeText(booleanString(vm.getBios() != null && vm.getBios().getBootMenu() != null ? vm.getBios().getBootMenu().getEnabled() : null, "false"))).append(""); sb.append("true"); diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index d9f4963165e..ab7662f4430 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -17,18 +17,22 @@ package org.apache.cloudstack.network.contrail.management; +import java.net.InetAddress; import java.util.List; import java.util.Map; -import java.net.InetAddress; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; +import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; +import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; @@ -37,20 +41,15 @@ import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; -import org.apache.cloudstack.acl.ControlledEntity; -import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.api.response.ApiKeyPairResponse; -import org.apache.cloudstack.api.response.ListResponse; -import org.apache.cloudstack.acl.SecurityChecker.AccessType; -import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; -import org.apache.cloudstack.context.CallContext; - +import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; import com.cloud.api.query.vo.ControlledViewEntity; import com.cloud.configuration.ResourceLimit; import com.cloud.configuration.dao.ResourceCountDao; @@ -614,4 +613,14 @@ public class MockAccountManager extends ManagerBase implements AccountManager { @Override public void checkCallerRoleTypeAllowedForUserOrAccountOperations(Account userAccount, User user) { } + + @Override + public Account getActiveAccountByUuid(String accountUuid) { + return null; + } + + @Override + public User getOneActiveUserForAccount(Account account) { + return null; + } } diff --git a/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDao.java index 756425f5093..43974bcf9cc 100644 --- a/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDao.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.api.query.dao; +import java.util.List; + import org.apache.cloudstack.api.response.AsyncJobResponse; import org.apache.cloudstack.framework.jobs.AsyncJob; @@ -28,4 +30,6 @@ public interface AsyncJobJoinDao extends GenericDao { AsyncJobJoinVO newAsyncJobView(AsyncJob vol); + List listByIds(List ids); + } diff --git a/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java index 10ef67bbbea..93af9a04e14 100644 --- a/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java @@ -16,17 +16,17 @@ // under the License. package com.cloud.api.query.dao; +import java.util.Collections; import java.util.Date; import java.util.List; - import javax.inject.Inject; -import org.springframework.stereotype.Component; - import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.response.AsyncJobResponse; import org.apache.cloudstack.framework.jobs.AsyncJob; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; import com.cloud.api.ApiResponseHelper; import com.cloud.api.ApiSerializerHelper; @@ -115,4 +115,16 @@ public class AsyncJobJoinDaoImpl extends GenericDaoBase im } + @Override + public List listByIds(List ids) { + if (CollectionUtils.isEmpty(ids)) { + return Collections.emptyList(); + } + SearchBuilder idsSearch = createSearchBuilder(); + idsSearch.and("ids", idsSearch.entity().getId(), SearchCriteria.Op.IN); + idsSearch.done(); + SearchCriteria sc = idsSearch.create(); + sc.setParameters("ids", ids.toArray()); + return listBy(sc); + } } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 2011d455646..c9f4feea8e9 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -2755,6 +2755,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return _accountDao.findById(accountId); } + @Override + public Account getActiveAccountByUuid(String accountUuid) { + return _accountDao.findByUuid(accountUuid); + } + @Override public Account getAccount(long accountId) { return _accountDao.findByIdIncludingRemoved(accountId); @@ -2773,6 +2778,15 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return _userDao.findById(userId); } + @Override + public User getOneActiveUserForAccount(Account account) { + List users = _userDao.listByAccount(account.getId()); + if (CollectionUtils.isEmpty(users)) { + return null; + } + return users.get(0); + } + @Override public User getUserIncludingRemoved(long userId) { return _userDao.findByIdIncludingRemoved(userId); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 3fa6cd105c9..5a5f127d050 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -8017,7 +8017,10 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir logger.trace("Verifying if the new account [{}] has access to the specified domain [{}].", newAccount, domain); _accountMgr.checkAccess(newAccount, domain); - Network newNetwork = ensureDestinationNetwork(cmd, vm, newAccount); + Network newNetwork = null; + if (!cmd.isSkipNetwork()) { + newNetwork = ensureDestinationNetwork(cmd, vm, newAccount); + } try { Transaction.execute(new TransactionCallbackNoReturn() { @Override diff --git a/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java b/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java index deca4e9a7cf..855a9cfcb5b 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java @@ -158,7 +158,7 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme backup.setAccountId(vm.getAccountId()); backup.setDomainId(vm.getDomainId()); backup.setZoneId(vm.getDataCenterId()); - backup.setStatus(Backup.Status.ReadyForTransfer); + backup.setStatus(Backup.Status.Queued); backup.setBackupOfferingId(vm.getBackupOfferingId()); backup.setDate(new Date()); @@ -236,6 +236,7 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme // todo: set it in the backend backup.setType("Incremental"); } + updateBackupState(backup, Backup.Status.ReadyForTransfer); return backup; } @@ -308,12 +309,12 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme // Delete old checkpoint if exists (POC: skip actual libvirt call) if (oldCheckpointId != null) { // todo: In production: send command to delete oldCheckpointId via virsh checkpoint-delete - logger.debug("Would delete old checkpoint: " + oldCheckpointId); + logger.debug("Would delete old checkpoint: {}", oldCheckpointId); } // Delete backup session record - backup.setStatus(Backup.Status.BackedUp); - backupDao.update(backupId, backup); + updateBackupState(backup, Backup.Status.BackedUp); + backupDao.remove(backup.getId()); return backup;