From 99e34dd443cd0f02e589815da05f4f12e0296942 Mon Sep 17 00:00:00 2001 From: dahn Date: Fri, 20 Feb 2026 16:20:14 +0100 Subject: [PATCH 001/205] [20.3] resource allocation --- .../com/cloud/user/ResourceLimitService.java | 2 + .../service/NetworkOrchestrationService.java | 2 +- .../orchestration/NetworkOrchestrator.java | 49 +++++++++++++------ .../com/cloud/network/NetworkServiceImpl.java | 2 +- .../resourcelimit/CheckedReservation.java | 32 ++++++++---- .../ResourceLimitManagerImpl.java | 36 +++++++++----- .../resourcelimit/CheckedReservationTest.java | 17 +------ .../ResourceLimitManagerImplTest.java | 6 ++- .../com/cloud/vpc/MockNetworkManagerImpl.java | 2 +- .../vpc/MockResourceLimitManagerImpl.java | 10 ++++ 10 files changed, 101 insertions(+), 57 deletions(-) diff --git a/api/src/main/java/com/cloud/user/ResourceLimitService.java b/api/src/main/java/com/cloud/user/ResourceLimitService.java index 666529808bf1..fc486601d16d 100644 --- a/api/src/main/java/com/cloud/user/ResourceLimitService.java +++ b/api/src/main/java/com/cloud/user/ResourceLimitService.java @@ -185,6 +185,7 @@ public interface ResourceLimitService { */ public void checkResourceLimit(Account account, ResourceCount.ResourceType type, long... count) throws ResourceAllocationException; public void checkResourceLimitWithTag(Account account, ResourceCount.ResourceType type, String tag, long... count) throws ResourceAllocationException; + public void checkResourceLimitWithTag(Account account, Long domainId, boolean considerSystemAccount, ResourceCount.ResourceType type, String tag, long... count) throws ResourceAllocationException; /** * Gets the count of resources for a resource type and account @@ -284,4 +285,5 @@ void checkVmResourceLimitsForTemplateChange(Account owner, Boolean display, Serv void incrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory); void decrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory); + long recalculateDomainResourceCount(final long domainId, final ResourceType type, String tag); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index dbab3320316a..d1f391cf625c 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -302,7 +302,7 @@ void implementNetworkElementsAndResources(DeployDestination dest, ReservationCon void removeDhcpServiceInSubnet(Nic nic); - boolean resourceCountNeedsUpdate(NetworkOffering ntwkOff, ACLType aclType); + boolean isResourceCountUpdateNeeded(NetworkOffering networkOffering); void prepareAllNicsForMigration(VirtualMachineProfile vm, DeployDestination dest); diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 899ce51022ba..f8bf613d3e70 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -39,12 +39,14 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.configuration.Resource; import com.cloud.dc.ASNumberVO; import com.cloud.bgp.BGPService; import com.cloud.dc.VlanDetailsVO; import com.cloud.dc.dao.ASNumberDao; import com.cloud.dc.dao.VlanDetailsDao; import com.cloud.network.dao.NsxProviderDao; +import com.cloud.resourcelimit.CheckedReservation; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -62,6 +64,7 @@ import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.dao.NetworkPermissionDao; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -441,6 +444,8 @@ public void setDhcpProviders(final List dhcpProviders) { ClusterDao clusterDao; @Inject RoutedIpv4Manager routedIpv4Manager; + @Inject + private ReservationDao reservationDao; protected StateMachine2 _stateMachine; ScheduledExecutorService _executor; @@ -2721,12 +2726,6 @@ private Network createGuestNetwork(final long networkOfferingId, final String na return null; } - final boolean updateResourceCount = resourceCountNeedsUpdate(ntwkOff, aclType); - //check resource limits - if (updateResourceCount) { - _resourceLimitMgr.checkResourceLimit(owner, ResourceType.network, isDisplayNetworkEnabled); - } - // Validate network offering if (ntwkOff.getState() != NetworkOffering.State.Enabled) { // see NetworkOfferingVO @@ -2745,6 +2744,8 @@ private Network createGuestNetwork(final long networkOfferingId, final String na boolean ipv6 = false; + try (CheckedReservation networkReservation = new CheckedReservation(owner, domainId, Resource.ResourceType.network, null, null, 1L, reservationDao, _resourceLimitMgr)) { + if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) { ipv6 = true; } @@ -3084,8 +3085,8 @@ public Network doInTransaction(final TransactionStatus status) { } } - if (updateResourceCount) { - _resourceLimitMgr.incrementResourceCount(owner.getId(), ResourceType.network, isDisplayNetworkEnabled); + if (isResourceCountUpdateNeeded(ntwkOff)) { + changeAccountResourceCountOrRecalculateDomainResourceCount(owner.getAccountId(), domainId, isDisplayNetworkEnabled, true); } UsageEventUtils.publishNetworkCreation(network); @@ -3096,6 +3097,10 @@ public Network doInTransaction(final TransactionStatus status) { CallContext.current().setEventDetails("Network Id: " + network.getId()); CallContext.current().putContextParameter(Network.class, network.getUuid()); return network; + } catch (Exception e) { + logger.error(e); + throw new RuntimeException(e); + } } @Override @@ -3460,9 +3465,8 @@ public List doInTransaction(TransactionStatus status) { } final NetworkOffering ntwkOff = _entityMgr.findById(NetworkOffering.class, networkFinal.getNetworkOfferingId()); - final boolean updateResourceCount = resourceCountNeedsUpdate(ntwkOff, networkFinal.getAclType()); - if (updateResourceCount) { - _resourceLimitMgr.decrementResourceCount(networkFinal.getAccountId(), ResourceType.network, networkFinal.getDisplayNetwork()); + if (isResourceCountUpdateNeeded(ntwkOff)) { + changeAccountResourceCountOrRecalculateDomainResourceCount(networkFinal.getAccountId(), networkFinal.getDomainId(), networkFinal.getDisplayNetwork(), false); } } return deletedVlans.second(); @@ -3485,6 +3489,23 @@ public List doInTransaction(TransactionStatus status) { return success; } + /** + * If it is a shared network with {@link ACLType#Domain}, it will belong to account {@link Account#ACCOUNT_ID_SYSTEM} and the resources will be not incremented for the + * domain. Therefore, we force the recalculation of the domain's resource count in this case. Otherwise, it will change the count for the account owner. + * @param incrementAccountResourceCount If true, the account resource count will be incremented by 1; otherwise, it will decremented by 1. + */ + private void changeAccountResourceCountOrRecalculateDomainResourceCount(Long accountId, Long domainId, boolean displayNetwork, boolean incrementAccountResourceCount) { + if (Account.ACCOUNT_ID_SYSTEM == accountId && ObjectUtils.isNotEmpty(domainId)) { + _resourceLimitMgr.recalculateDomainResourceCount(domainId, ResourceType.network, null); + } else { + if (incrementAccountResourceCount) { + _resourceLimitMgr.incrementResourceCount(accountId, ResourceType.network, displayNetwork); + } else { + _resourceLimitMgr.decrementResourceCount(accountId, ResourceType.network, displayNetwork); + } + } + } + private void publishDeletedVlanRanges(List deletedVlanRangeToPublish) { if (CollectionUtils.isNotEmpty(deletedVlanRangeToPublish)) { for (VlanVO vlan : deletedVlanRangeToPublish) { @@ -3494,10 +3515,8 @@ private void publishDeletedVlanRanges(List deletedVlanRangeToPublish) { } @Override - public boolean resourceCountNeedsUpdate(final NetworkOffering ntwkOff, final ACLType aclType) { - //Update resource count only for Isolated account specific non-system networks - final boolean updateResourceCount = ntwkOff.getGuestType() == GuestType.Isolated && !ntwkOff.isSystemOnly() && aclType == ACLType.Account; - return updateResourceCount; + public boolean isResourceCountUpdateNeeded(NetworkOffering networkOffering) { + return !networkOffering.isSystemOnly(); } protected Pair> deleteVlansInNetwork(final NetworkVO network, final long userId, final Account callerAccount) { diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index cfa4574d9964..54c1d1339434 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -3168,7 +3168,7 @@ public Network updateGuestNetwork(final UpdateNetworkCmd cmd) { if (displayNetwork != null && displayNetwork != network.getDisplayNetwork()) { // Update resource count if it needs to be updated NetworkOffering networkOffering = _networkOfferingDao.findById(network.getNetworkOfferingId()); - if (_networkMgr.resourceCountNeedsUpdate(networkOffering, network.getAclType())) { + if (_networkMgr.isResourceCountUpdateNeeded(networkOffering)) { _resourceLimitMgr.changeResourceCount(network.getAccountId(), Resource.ResourceType.network, displayNetwork); } diff --git a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java index d66e1eb912ad..211ca65a9d8f 100644 --- a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java +++ b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java @@ -49,6 +49,7 @@ public class CheckedReservation implements AutoCloseable { ResourceLimitService resourceLimitService; private final Account account; + private Long domainId; private final ResourceType resourceType; private Long amount; private List reservations; @@ -73,12 +74,12 @@ private void removeAllReservations() { this.reservations = null; } - protected void checkLimitAndPersistReservations(Account account, ResourceType resourceType, Long resourceId, List resourceLimitTags, Long amount) throws ResourceAllocationException { + protected void checkLimitAndPersistReservations(Account account, Long domainId, ResourceType resourceType, Long resourceId, List resourceLimitTags, Long amount) throws ResourceAllocationException { try { - checkLimitAndPersistReservation(account, resourceType, resourceId, null, amount); + checkLimitAndPersistReservation(account, domainId, resourceType, resourceId, null, amount); if (CollectionUtils.isNotEmpty(resourceLimitTags)) { for (String tag : resourceLimitTags) { - checkLimitAndPersistReservation(account, resourceType, resourceId, tag, amount); + checkLimitAndPersistReservation(account, domainId, resourceType, resourceId, tag, amount); } } } catch (ResourceAllocationException rae) { @@ -87,11 +88,11 @@ protected void checkLimitAndPersistReservations(Account account, ResourceType re } } - protected void checkLimitAndPersistReservation(Account account, ResourceType resourceType, Long resourceId, String tag, Long amount) throws ResourceAllocationException { + protected void checkLimitAndPersistReservation(Account account, Long domainId, ResourceType resourceType, Long resourceId, String tag, Long amount) throws ResourceAllocationException { if (amount > 0) { - resourceLimitService.checkResourceLimitWithTag(account, resourceType, tag, amount); + resourceLimitService.checkResourceLimitWithTag(account, domainId, true, resourceType, tag, amount); } - ReservationVO reservationVO = new ReservationVO(account.getAccountId(), account.getDomainId(), resourceType, tag, amount); + ReservationVO reservationVO = new ReservationVO(account.getAccountId(), domainId, resourceType, tag, amount); if (resourceId != null) { reservationVO.setResourceId(resourceId); } @@ -114,9 +115,20 @@ public CheckedReservation(Account account, ResourceType resourceType, List resourceLimitTags, Long amount, ReservationDao reservationDao, ResourceLimitService resourceLimitService) throws ResourceAllocationException { + this(account, account.getDomainId(), resourceType, resourceId, resourceLimitTags, amount, reservationDao, resourceLimitService); + } + + public CheckedReservation(Account account, Long domainId, ResourceType resourceType, Long resourceId, List resourceLimitTags, Long amount, + ReservationDao reservationDao, ResourceLimitService resourceLimitService) throws ResourceAllocationException { this.reservationDao = reservationDao; this.resourceLimitService = resourceLimitService; this.account = account; + + this.domainId = domainId; + if (domainId == null) { + this.domainId = account.getDomainId(); + } + this.resourceType = resourceType; this.amount = amount; this.reservations = new ArrayList<>(); @@ -127,7 +139,7 @@ public CheckedReservation(Account account, ResourceType resourceType, Long resou setGlobalLock(); if (quotaLimitLock.lock(TRY_TO_GET_LOCK_TIME)) { try { - checkLimitAndPersistReservations(account, resourceType, resourceId, resourceLimitTags, amount); + checkLimitAndPersistReservations(account, this.domainId, resourceType, resourceId, resourceLimitTags, amount); CallContext.current().putContextParameter(getContextParameterKey(), getIds()); } catch (NullPointerException npe) { throw new CloudRuntimeException("not enough means to check limits", npe); @@ -138,11 +150,11 @@ public CheckedReservation(Account account, ResourceType resourceType, Long resou throw new ResourceAllocationException(String.format("unable to acquire resource reservation \"%s\"", quotaLimitLock.getName()), resourceType); } } else { - checkLimitAndPersistReservations(account, resourceType, resourceId, resourceLimitTags, amount); + checkLimitAndPersistReservations(account, this.domainId, resourceType, resourceId, resourceLimitTags, amount); } } else { logger.debug("not reserving any amount of resources for {} in domain {}, type: {}, tag: {}", - account.getAccountName(), account.getDomainId(), resourceType, getResourceLimitTagsAsString()); + account.getAccountName(), this.domainId, resourceType, getResourceLimitTagsAsString()); } } @@ -153,7 +165,7 @@ public CheckedReservation(Account account, ResourceType resourceType, Long amoun @NotNull private void setGlobalLock() { - String lockName = String.format("CheckedReservation-%s/%d", account.getDomainId(), resourceType.getOrdinal()); + String lockName = String.format("CheckedReservation-%s/%d", this.domainId, resourceType.getOrdinal()); setQuotaLimitLock(GlobalLock.getInternLock(lockName)); } diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index d4d91b6de7bc..8db204cf177b 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -36,6 +36,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.network.dao.NetworkDomainDao; import com.cloud.utils.Ternary; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.response.AccountResponse; @@ -192,6 +193,8 @@ public class ResourceLimitManagerImpl extends ManagerBase implements ResourceLim ServiceOfferingDao serviceOfferingDao; @Inject DiskOfferingDao diskOfferingDao; + @Inject + private NetworkDomainDao networkDomainDao; protected GenericSearchBuilder templateSizeSearch; protected GenericSearchBuilder snapshotSizeSearch; @@ -488,15 +491,7 @@ public long findCorrectResourceLimitForDomain(Domain domain, ResourceType type, return max; } - protected void checkDomainResourceLimit(final Account account, final Project project, final ResourceType type, String tag, long numResources) throws ResourceAllocationException { - // check all domains in the account's domain hierarchy - Long domainId; - if (project != null) { - domainId = project.getDomainId(); - } else { - domainId = account.getDomainId(); - } - + protected void checkDomainResourceLimit(Long domainId, final ResourceType type, String tag, long numResources) throws ResourceAllocationException { while (domainId != null) { DomainVO domain = _domainDao.findById(domainId); // no limit check if it is ROOT domain @@ -618,11 +613,16 @@ public void checkResourceLimit(final Account account, final ResourceType type, l @Override public void checkResourceLimitWithTag(final Account account, final ResourceType type, String tag, long... count) throws ResourceAllocationException { + checkResourceLimitWithTag(account, null, false, type, tag, count); + } + + @Override + public void checkResourceLimitWithTag(final Account account, Long domainId, boolean considerSystemAccount, final ResourceType type, String tag, long... count) throws ResourceAllocationException { final long numResources = ((count.length == 0) ? 1 : count[0]); Project project = null; // Don't place any limits on system or root admin accounts - if (_accountMgr.isRootAdmin(account.getId())) { + if (_accountMgr.isRootAdmin(account.getId()) && !(considerSystemAccount && Account.ACCOUNT_ID_SYSTEM == account.getId())) { return; } @@ -630,6 +630,14 @@ public void checkResourceLimitWithTag(final Account account, final ResourceType project = _projectDao.findByProjectAccountId(account.getId()); } + if (domainId == null) { + if (project != null) { + domainId = project.getDomainId(); + } else { + domainId = account.getDomainId(); + } + } + Long domainIdFinal = domainId; final Project projectFinal = project; Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { @Override @@ -639,7 +647,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) throws Resour // Check account limits checkAccountResourceLimit(account, projectFinal, type, tag, numResources); // check all domains in the account's domain hierarchy - checkDomainResourceLimit(account, projectFinal, type, tag, numResources); + checkDomainResourceLimit(domainIdFinal, type, tag, numResources); } }); } @@ -1155,7 +1163,7 @@ protected boolean updateResourceCountForAccount(final long accountId, final Reso * @param type the resource type to do the recalculation for * @return the resulting new resource count */ - protected long recalculateDomainResourceCount(final long domainId, final ResourceType type, String tag) { + public long recalculateDomainResourceCount(final long domainId, final ResourceType type, String tag) { List accounts = _accountDao.findActiveAccountsForDomain(domainId); List childDomains = _domainDao.findImmediateChildrenForParent(domainId); @@ -1196,6 +1204,10 @@ protected long recalculateDomainResourceCount(final long domainId, final Resourc newResourceCount += _projectDao.countProjectsForDomain(domainId); } + if (type == ResourceType.network) { + newResourceCount += networkDomainDao.listDomainNetworkMapByDomain(domainId).size(); + } + // TODO make sure that the resource counts are not null for (ResourceCountVO resourceCount : resourceCounts) { if (resourceCount.getResourceOwnerType() == ResourceOwnerType.Domain && resourceCount.getDomainId() == domainId) { diff --git a/server/src/test/java/com/cloud/resourcelimit/CheckedReservationTest.java b/server/src/test/java/com/cloud/resourcelimit/CheckedReservationTest.java index 247647dd010e..5e72f2044939 100644 --- a/server/src/test/java/com/cloud/resourcelimit/CheckedReservationTest.java +++ b/server/src/test/java/com/cloud/resourcelimit/CheckedReservationTest.java @@ -149,23 +149,10 @@ public void testReservationPersistAndCallContextParam() { @Test public void testMultipleReservationsWithOneFailing() { List tags = List.of("abc", "xyz"); - when(account.getAccountId()).thenReturn(1L); - when(account.getDomainId()).thenReturn(4L); Map persistedReservations = new HashMap<>(); - Mockito.when(reservationDao.persist(Mockito.any(ReservationVO.class))).thenAnswer((Answer) invocation -> { - ReservationVO reservationVO = (ReservationVO) invocation.getArguments()[0]; - Long id = (long) (persistedReservations.size() + 1); - ReflectionTestUtils.setField(reservationVO, "id", id); - persistedReservations.put(id, reservationVO); - return reservationVO; - }); - Mockito.when(reservationDao.remove(Mockito.anyLong())).thenAnswer((Answer) invocation -> { - Long id = (Long) invocation.getArguments()[0]; - persistedReservations.remove(id); - return true; - }); + try { - Mockito.doThrow(ResourceAllocationException.class).when(resourceLimitService).checkResourceLimitWithTag(account, Resource.ResourceType.cpu, "xyz", 1L); + Mockito.doThrow(ResourceAllocationException.class).when(resourceLimitService).checkResourceLimitWithTag(account, account.getDomainId(), true, Resource.ResourceType.cpu, "xyz", 1L); try (CheckedReservation vmReservation = new CheckedReservation(account, Resource.ResourceType.user_vm, tags, 1L, reservationDao, resourceLimitService); CheckedReservation cpuReservation = new CheckedReservation(account, Resource.ResourceType.cpu, tags, 1L, reservationDao, resourceLimitService); CheckedReservation memReservation = new CheckedReservation(account, Resource.ResourceType.memory, tags, 256L, reservationDao, resourceLimitService); diff --git a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java index 53ccc830dd2d..e3bfdc636358 100644 --- a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java +++ b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java @@ -587,10 +587,11 @@ public void testCheckResourceLimitWithTag() { public void testCheckResourceLimitWithTagNonAdmin() throws ResourceAllocationException { AccountVO account = Mockito.mock(AccountVO.class); Mockito.when(account.getId()).thenReturn(1L); + Mockito.when(account.getDomainId()).thenReturn(1L); Mockito.when(accountManager.isRootAdmin(1L)).thenReturn(false); Mockito.doReturn(new ArrayList()).when(resourceLimitManager).lockAccountAndOwnerDomainRows(Mockito.anyLong(), Mockito.any(Resource.ResourceType.class), Mockito.anyString()); Mockito.doNothing().when(resourceLimitManager).checkAccountResourceLimit(account, null, Resource.ResourceType.cpu, hostTags.get(0), 1); - Mockito.doNothing().when(resourceLimitManager).checkDomainResourceLimit(account, null, Resource.ResourceType.cpu, hostTags.get(0), 1); + Mockito.doNothing().when(resourceLimitManager).checkDomainResourceLimit(1L, Resource.ResourceType.cpu, hostTags.get(0), 1); try { resourceLimitManager.checkResourceLimitWithTag(account, Resource.ResourceType.cpu, hostTags.get(0), 1); } catch (ResourceAllocationException e) { @@ -606,9 +607,10 @@ public void testCheckResourceLimitWithTagProject() throws ResourceAllocationExce Mockito.when(accountManager.isRootAdmin(1L)).thenReturn(false); ProjectVO projectVO = Mockito.mock(ProjectVO.class); Mockito.when(projectDao.findByProjectAccountId(Mockito.anyLong())).thenReturn(projectVO); + Mockito.when(projectVO.getDomainId()).thenReturn(1L); Mockito.doReturn(new ArrayList()).when(resourceLimitManager).lockAccountAndOwnerDomainRows(Mockito.anyLong(), Mockito.any(Resource.ResourceType.class), Mockito.anyString()); Mockito.doNothing().when(resourceLimitManager).checkAccountResourceLimit(account, projectVO, Resource.ResourceType.cpu, hostTags.get(0), 1); - Mockito.doNothing().when(resourceLimitManager).checkDomainResourceLimit(account, projectVO, Resource.ResourceType.cpu, hostTags.get(0), 1); + Mockito.doNothing().when(resourceLimitManager).checkDomainResourceLimit(1L, Resource.ResourceType.cpu, hostTags.get(0), 1); try { resourceLimitManager.checkResourceLimitWithTag(account, Resource.ResourceType.cpu, hostTags.get(0), 1); } catch (ResourceAllocationException e) { diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java index 673d0b7a48c5..371181ae8825 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -951,7 +951,7 @@ public void removeDhcpServiceInSubnet(Nic nic) { } @Override - public boolean resourceCountNeedsUpdate(NetworkOffering ntwkOff, ACLType aclType) { + public boolean isResourceCountUpdateNeeded(NetworkOffering ntwkOff) { return false; //To change body of implemented methods use File | Settings | File Templates. } diff --git a/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java index 3f3220d09341..7e8dbbf04c6c 100644 --- a/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java @@ -237,6 +237,11 @@ public void checkResourceLimitWithTag(Account account, ResourceType type, String } + @Override + public void checkResourceLimitWithTag(Account account, Long domainId, boolean considerSystemAccount, ResourceType type, String tag, long... count) throws ResourceAllocationException { + + } + @Override public List getResourceLimitHostTags() { return null; @@ -381,4 +386,9 @@ public void incrementVmMemoryResourceCount(long accountId, Boolean display, Serv public void decrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory) { } + + @Override + public long recalculateDomainResourceCount(long domainId, ResourceType type, String tag) { + return 0; + } } From 2671026f90cee403daa3651163a4ea986b0c96c4 Mon Sep 17 00:00:00 2001 From: dahn Date: Fri, 20 Feb 2026 16:37:24 +0100 Subject: [PATCH 002/205] [20.3] resource instance limits --- .../java/com/cloud/vm/UserVmManagerImpl.java | 259 +++++++++--------- 1 file changed, 131 insertions(+), 128 deletions(-) diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 815ac4f70fe8..0e1c3bb91945 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -54,7 +54,6 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; -import com.cloud.network.NetworkService; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -249,6 +248,7 @@ import com.cloud.kubernetes.cluster.KubernetesServiceHelper; import com.cloud.network.IpAddressManager; import com.cloud.network.Network; +import com.cloud.network.NetworkService; import com.cloud.network.Network.GuestType; import com.cloud.network.Network.IpAddresses; import com.cloud.network.Network.Provider; @@ -655,19 +655,19 @@ public void setKubernetesServiceHelpers(final List kube @Inject VnfTemplateManager vnfTemplateManager; - private static final ConfigKey VmIpFetchWaitInterval = new ConfigKey("Advanced", Integer.class, "externaldhcp.vmip.retrieval.interval", "180", + private static final ConfigKey VmIpFetchWaitInterval = new ConfigKey<>("Advanced", Integer.class, "externaldhcp.vmip.retrieval.interval", "180", "Wait Interval (in seconds) for shared network vm dhcp ip addr fetch for next iteration ", true); - private static final ConfigKey VmIpFetchTrialMax = new ConfigKey("Advanced", Integer.class, "externaldhcp.vmip.max.retry", "10", + private static final ConfigKey VmIpFetchTrialMax = new ConfigKey<>("Advanced", Integer.class, "externaldhcp.vmip.max.retry", "10", "The max number of retrieval times for shared network vm dhcp ip fetch, in case of failures", true); - private static final ConfigKey VmIpFetchThreadPoolMax = new ConfigKey("Advanced", Integer.class, "externaldhcp.vmipFetch.threadPool.max", "10", + private static final ConfigKey VmIpFetchThreadPoolMax = new ConfigKey<>("Advanced", Integer.class, "externaldhcp.vmipFetch.threadPool.max", "10", "number of threads for fetching vms ip address", true); - private static final ConfigKey VmIpFetchTaskWorkers = new ConfigKey("Advanced", Integer.class, "externaldhcp.vmipfetchtask.workers", "10", + private static final ConfigKey VmIpFetchTaskWorkers = new ConfigKey<>("Advanced", Integer.class, "externaldhcp.vmipfetchtask.workers", "10", "number of worker threads for vm ip fetch task ", true); - private static final ConfigKey AllowDeployVmIfGivenHostFails = new ConfigKey("Advanced", Boolean.class, "allow.deploy.vm.if.deploy.on.given.host.fails", "false", + private static final ConfigKey AllowDeployVmIfGivenHostFails = new ConfigKey<>("Advanced", Boolean.class, "allow.deploy.vm.if.deploy.on.given.host.fails", "false", "allow vm to deploy on different host if vm fails to deploy on the given host ", true); private static final ConfigKey KvmAdditionalConfigAllowList = new ConfigKey<>(String.class, @@ -679,7 +679,7 @@ public void setKubernetesServiceHelpers(final List kube private static final ConfigKey VmwareAdditionalConfigAllowList = new ConfigKey<>(String.class, "allow.additional.vm.configuration.list.vmware", "Advanced", "", "Comma separated list of allowed additional configuration options.", true, ConfigKey.Scope.Global, null, null, EnableAdditionalVmConfig.key(), null, null, ConfigKey.Kind.CSV, null); - private static final ConfigKey VmDestroyForcestop = new ConfigKey("Advanced", Boolean.class, "vm.destroy.forcestop", "false", + private static final ConfigKey VmDestroyForcestop = new ConfigKey<>("Advanced", Boolean.class, "vm.destroy.forcestop", "false", "On destroy, force-stop takes this value ", true); @Override @@ -1150,7 +1150,7 @@ private UserVm rebootVirtualMachine(long userId, long vmId, boolean enterSetup, if (dc.getNetworkType() == DataCenter.NetworkType.Advanced) { //List all networks of vm List vmNetworks = _vmNetworkMapDao.getNetworks(vmId); - List routers = new ArrayList(); + List routers = new ArrayList<>(); //List the stopped routers for(long vmNetworkId : vmNetworks) { List router = _routerDao.listStopped(vmNetworkId); @@ -3126,7 +3126,7 @@ public UserVm updateVirtualMachine(long id, String displayName, String group, Bo // Verify that vm's hostName is unique - List vmNtwks = new ArrayList(nics.size()); + List vmNtwks = new ArrayList<>(nics.size()); for (Nic nic : nics) { vmNtwks.add(_networkDao.findById(nic.getNetworkId())); } @@ -3692,7 +3692,7 @@ public UserVm createBasicSecurityGroupVirtualMachine(DataCenter zone, ServiceOff StorageUnavailableException, ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); - List networkList = new ArrayList(); + List networkList = new ArrayList<>(); // Verify that caller can perform actions in behalf of vm owner _accountMgr.checkAccess(caller, null, true, owner); @@ -3718,7 +3718,7 @@ public UserVm createBasicSecurityGroupVirtualMachine(DataCenter zone, ServiceOff //add the default securityGroup only if no security group is specified if (securityGroupIdList == null || securityGroupIdList.isEmpty()) { if (securityGroupIdList == null) { - securityGroupIdList = new ArrayList(); + securityGroupIdList = new ArrayList<>(); } SecurityGroup defaultGroup = _securityGroupMgr.getDefaultSecurityGroup(owner.getId()); if (defaultGroup != null) { @@ -3750,7 +3750,7 @@ public UserVm createAdvancedSecurityGroupVirtualMachine(DataCenter zone, Service Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, String vmType) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); - List networkList = new ArrayList(); + List networkList = new ArrayList<>(); boolean isSecurityGroupEnabledNetworkUsed = false; boolean isVmWare = (template.getHypervisorType() == HypervisorType.VMware || (hypervisor != null && hypervisor == HypervisorType.VMware)); @@ -3828,7 +3828,7 @@ public UserVm createAdvancedSecurityGroupVirtualMachine(DataCenter zone, Service //add the default securityGroup only if no security group is specified if (securityGroupIdList == null || securityGroupIdList.isEmpty()) { if (securityGroupIdList == null) { - securityGroupIdList = new ArrayList(); + securityGroupIdList = new ArrayList<>(); } SecurityGroup defaultGroup = _securityGroupMgr.getDefaultSecurityGroup(owner.getId()); @@ -3863,7 +3863,7 @@ public UserVm createAdvancedVirtualMachine(DataCenter zone, ServiceOffering serv StorageUnavailableException, ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); - List networkList = new ArrayList(); + List networkList = new ArrayList<>(); // Verify that caller can perform actions in behalf of vm owner _accountMgr.checkAccess(caller, null, true, owner); @@ -4573,11 +4573,11 @@ protected void verifyIfHypervisorSupportsRootdiskSizeOverride(HypervisorType hyp private void checkIfHostNameUniqueInNtwkDomain(String hostName, List networkList) { // Check that hostName is unique in the network domain - Map> ntwkDomains = new HashMap>(); + Map> ntwkDomains = new HashMap<>(); for (Network network : networkList) { String ntwkDomain = network.getNetworkDomain(); if (!ntwkDomains.containsKey(ntwkDomain)) { - List ntwkIds = new ArrayList(); + List ntwkIds = new ArrayList<>(); ntwkIds.add(network.getId()); ntwkDomains.put(ntwkDomain, ntwkIds); } else { @@ -4718,10 +4718,10 @@ private UserVmVO commitUserVm(final boolean isImport, final DataCenter zone, fin logger.debug("Allocating in the DB for vm"); DataCenterDeployment plan = new DataCenterDeployment(zone.getId()); - List computeTags = new ArrayList(); + List computeTags = new ArrayList<>(); computeTags.add(offering.getHostTag()); - List rootDiskTags = new ArrayList(); + List rootDiskTags = new ArrayList<>(); DiskOfferingVO rootDiskOfferingVO = _diskOfferingDao.findById(rootDiskOfferingId); rootDiskTags.add(rootDiskOfferingVO.getTags()); @@ -4934,7 +4934,7 @@ public void generateUsageEvent(VirtualMachine vm, boolean isDisplay, String even VirtualMachine.class.getName(), vm.getUuid(), isDisplay); } else { - Map customParameters = new HashMap(); + Map customParameters = new HashMap<>(); customParameters.put(UsageEventVO.DynamicParameters.cpuNumber.name(), serviceOffering.getCpu().toString()); customParameters.put(UsageEventVO.DynamicParameters.cpuSpeed.name(), serviceOffering.getSpeed().toString()); customParameters.put(UsageEventVO.DynamicParameters.memory.name(), serviceOffering.getRamSize().toString()); @@ -4951,7 +4951,7 @@ public void collectVmNetworkStatistics (final UserVm userVm) { } logger.debug("Collect vm network statistics from host before stopping Vm"); long hostId = userVm.getHostId(); - List vmNames = new ArrayList(); + List vmNames = new ArrayList<>(); vmNames.add(userVm.getInstanceName()); final HostVO host = _hostDao.findById(hostId); Account account = _accountMgr.getAccount(userVm.getAccountId()); @@ -5527,132 +5527,137 @@ public Pair> startVirtualMach if (owner.getState() == Account.State.DISABLED) { throw new PermissionDeniedException(String.format("The owner of %s is disabled: %s", vm, owner)); } - VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); - if (VirtualMachineManager.ResourceCountRunningVMsonly.value()) { - // check if account/domain is with in resource limits to start a new vm - ServiceOfferingVO offering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); - resourceLimitService.checkVmResourceLimit(owner, vm.isDisplayVm(), offering, template); - } - // check if vm is security group enabled - if (_securityGroupMgr.isVmSecurityGroupEnabled(vmId) && _securityGroupMgr.getSecurityGroupsForVm(vmId).isEmpty() - && !_securityGroupMgr.isVmMappedToDefaultSecurityGroup(vmId) && _networkModel.canAddDefaultSecurityGroup()) { - // if vm is not mapped to security group, create a mapping - if (logger.isDebugEnabled()) { - logger.debug("Vm " + vm + " is security group enabled, but not mapped to default security group; creating the mapping automatically"); - } - - SecurityGroup defaultSecurityGroup = _securityGroupMgr.getDefaultSecurityGroup(vm.getAccountId()); - if (defaultSecurityGroup != null) { - List groupList = new ArrayList(); - groupList.add(defaultSecurityGroup.getId()); - _securityGroupMgr.addInstanceToGroups(vm, groupList); - } - } - // Choose deployment planner - // Host takes 1st preference, Cluster takes 2nd preference and Pod takes 3rd - // Default behaviour is invoked when host, cluster or pod are not specified - boolean isRootAdmin = _accountService.isRootAdmin(callerAccount.getId()); - Pod destinationPod = getDestinationPod(podId, isRootAdmin); - Cluster destinationCluster = getDestinationCluster(clusterId, isRootAdmin); - HostVO destinationHost = getDestinationHost(hostId, isRootAdmin, isExplicitHost); - DataCenterDeployment plan = null; - boolean deployOnGivenHost = false; - if (destinationHost != null) { - logger.debug("Destination Host to deploy the VM is specified, specifying a deployment plan to deploy the VM"); - _hostDao.loadHostTags(destinationHost); - validateStrictHostTagCheck(vm, destinationHost); - - final ServiceOfferingVO offering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); - Pair cpuCapabilityAndCapacity = _capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(destinationHost, offering, false); - if (!cpuCapabilityAndCapacity.first() || !cpuCapabilityAndCapacity.second()) { - String errorMsg; - if (!cpuCapabilityAndCapacity.first()) { - errorMsg = String.format("Cannot deploy the VM to specified host %s, requested CPU and speed is more than the host capability", destinationHost); + Pair> vmParamPair; + try (CheckedReservation vmReservation = new CheckedReservation(owner, ResourceType.user_vm, vm.getId(), null, 1L, reservationDao, _resourceLimitMgr)) { + VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); + if (VirtualMachineManager.ResourceCountRunningVMsonly.value()) { + // check if account/domain is with in resource limits to start a new vm + ServiceOfferingVO offering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); + resourceLimitService.checkVmResourceLimit(owner, vm.isDisplayVm(), offering, template); + } + // check if vm is security group enabled + if (_securityGroupMgr.isVmSecurityGroupEnabled(vmId) && _securityGroupMgr.getSecurityGroupsForVm(vmId).isEmpty() + && !_securityGroupMgr.isVmMappedToDefaultSecurityGroup(vmId) && _networkModel.canAddDefaultSecurityGroup()) { + // if vm is not mapped to security group, create a mapping + if (logger.isDebugEnabled()) { + logger.debug("Vm " + vm + " is security group enabled, but not mapped to default security group; creating the mapping automatically"); + } + + SecurityGroup defaultSecurityGroup = _securityGroupMgr.getDefaultSecurityGroup(vm.getAccountId()); + if (defaultSecurityGroup != null) { + List groupList = new ArrayList<>(); + groupList.add(defaultSecurityGroup.getId()); + _securityGroupMgr.addInstanceToGroups(vm, groupList); + } + } + // Choose deployment planner + // Host takes 1st preference, Cluster takes 2nd preference and Pod takes 3rd + // Default behaviour is invoked when host, cluster or pod are not specified + boolean isRootAdmin = _accountService.isRootAdmin(callerAccount.getId()); + Pod destinationPod = getDestinationPod(podId, isRootAdmin); + Cluster destinationCluster = getDestinationCluster(clusterId, isRootAdmin); + HostVO destinationHost = getDestinationHost(hostId, isRootAdmin, isExplicitHost); + DataCenterDeployment plan = null; + boolean deployOnGivenHost = false; + if (destinationHost != null) { + logger.debug("Destination Host to deploy the VM is specified, specifying a deployment plan to deploy the VM"); + _hostDao.loadHostTags(destinationHost); + validateStrictHostTagCheck(vm, destinationHost); + + final ServiceOfferingVO offering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); + Pair cpuCapabilityAndCapacity = _capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(destinationHost, offering, false); + if (!cpuCapabilityAndCapacity.first() || !cpuCapabilityAndCapacity.second()) { + String errorMsg; + if (!cpuCapabilityAndCapacity.first()) { + errorMsg = String.format("Cannot deploy the VM to specified host %s, requested CPU and speed is more than the host capability", destinationHost); + } else { + errorMsg = String.format("Cannot deploy the VM to specified host %s, host does not have enough free CPU or RAM, please check the logs", destinationHost); + } + logger.info(errorMsg); + if (!AllowDeployVmIfGivenHostFails.value()) { + throw new InvalidParameterValueException(errorMsg); + } } else { - errorMsg = String.format("Cannot deploy the VM to specified host %s, host does not have enough free CPU or RAM, please check the logs", destinationHost); + plan = new DataCenterDeployment(vm.getDataCenterId(), destinationHost.getPodId(), destinationHost.getClusterId(), destinationHost.getId(), null, null); + if (!AllowDeployVmIfGivenHostFails.value()) { + deployOnGivenHost = true; + } } - logger.info(errorMsg); + } else if (destinationCluster != null) { + logger.debug("Destination Cluster to deploy the VM is specified, specifying a deployment plan to deploy the VM"); + plan = new DataCenterDeployment(vm.getDataCenterId(), destinationCluster.getPodId(), destinationCluster.getId(), null, null, null); if (!AllowDeployVmIfGivenHostFails.value()) { - throw new InvalidParameterValueException(errorMsg); - }; - } else { - plan = new DataCenterDeployment(vm.getDataCenterId(), destinationHost.getPodId(), destinationHost.getClusterId(), destinationHost.getId(), null, null); + deployOnGivenHost = true; + } + } else if (destinationPod != null) { + logger.debug("Destination Pod to deploy the VM is specified, specifying a deployment plan to deploy the VM"); + plan = new DataCenterDeployment(vm.getDataCenterId(), destinationPod.getId(), null, null, null, null); if (!AllowDeployVmIfGivenHostFails.value()) { deployOnGivenHost = true; } } - } else if (destinationCluster != null) { - logger.debug("Destination Cluster to deploy the VM is specified, specifying a deployment plan to deploy the VM"); - plan = new DataCenterDeployment(vm.getDataCenterId(), destinationCluster.getPodId(), destinationCluster.getId(), null, null, null); - if (!AllowDeployVmIfGivenHostFails.value()) { - deployOnGivenHost = true; - } - } else if (destinationPod != null) { - logger.debug("Destination Pod to deploy the VM is specified, specifying a deployment plan to deploy the VM"); - plan = new DataCenterDeployment(vm.getDataCenterId(), destinationPod.getId(), null, null, null, null); - if (!AllowDeployVmIfGivenHostFails.value()) { - deployOnGivenHost = true; - } - } - // Set parameters - Map params = null; - if (vm.isUpdateParameters()) { - _vmDao.loadDetails(vm); - - String password = getCurrentVmPasswordOrDefineNewPassword(String.valueOf(additionalParams.getOrDefault(VirtualMachineProfile.Param.VmPassword, "")), vm, template); + // Set parameters + Map params = null; + if (vm.isUpdateParameters()) { + _vmDao.loadDetails(vm); - if (!validPassword(password)) { - throw new InvalidParameterValueException("A valid password for this virtual machine was not provided."); - } + String password = getCurrentVmPasswordOrDefineNewPassword(String.valueOf(additionalParams.getOrDefault(VirtualMachineProfile.Param.VmPassword, "")), vm, template); - // Check if an SSH key pair was selected for the instance and if so - // use it to encrypt & save the vm password - encryptAndStorePassword(vm, password); + if (!validPassword(password)) { + throw new InvalidParameterValueException("A valid password for this virtual machine was not provided."); + } - params = createParameterInParameterMap(params, additionalParams, VirtualMachineProfile.Param.VmPassword, password); - } + // Check if an SSH key pair was selected for the instance and if so + // use it to encrypt & save the vm password + encryptAndStorePassword(vm, password); - if(additionalParams.containsKey(VirtualMachineProfile.Param.BootIntoSetup)) { - if (! HypervisorType.VMware.equals(vm.getHypervisorType())) { - throw new InvalidParameterValueException(ApiConstants.BOOT_INTO_SETUP + " makes no sense for " + vm.getHypervisorType()); + params = createParameterInParameterMap(params, additionalParams, VirtualMachineProfile.Param.VmPassword, password); } - Object paramValue = additionalParams.get(VirtualMachineProfile.Param.BootIntoSetup); - if (logger.isTraceEnabled()) { + + if (additionalParams.containsKey(VirtualMachineProfile.Param.BootIntoSetup)) { + if (!HypervisorType.VMware.equals(vm.getHypervisorType())) { + throw new InvalidParameterValueException(ApiConstants.BOOT_INTO_SETUP + " makes no sense for " + vm.getHypervisorType()); + } + Object paramValue = additionalParams.get(VirtualMachineProfile.Param.BootIntoSetup); + if (logger.isTraceEnabled()) { logger.trace("It was specified whether to enter setup mode: " + paramValue.toString()); + } + params = createParameterInParameterMap(params, additionalParams, VirtualMachineProfile.Param.BootIntoSetup, paramValue); } - params = createParameterInParameterMap(params, additionalParams, VirtualMachineProfile.Param.BootIntoSetup, paramValue); - } - VirtualMachineEntity vmEntity = _orchSrvc.getVirtualMachine(vm.getUuid()); + VirtualMachineEntity vmEntity = _orchSrvc.getVirtualMachine(vm.getUuid()); - DeploymentPlanner planner = null; - if (deploymentPlannerToUse != null) { - // if set to null, the deployment planner would be later figured out either from global config var, or from - // the service offering - planner = _planningMgr.getDeploymentPlannerByName(deploymentPlannerToUse); - if (planner == null) { - throw new InvalidParameterValueException("Can't find a planner by name " + deploymentPlannerToUse); + DeploymentPlanner planner = null; + if (deploymentPlannerToUse != null) { + // if set to null, the deployment planner would be later figured out either from global config var, or from + // the service offering + planner = _planningMgr.getDeploymentPlannerByName(deploymentPlannerToUse); + if (planner == null) { + throw new InvalidParameterValueException("Can't find a planner by name " + deploymentPlannerToUse); + } } - } - vmEntity.setParamsToEntity(additionalParams); + vmEntity.setParamsToEntity(additionalParams); - String reservationId = vmEntity.reserve(planner, plan, new ExcludeList(), Long.toString(callerUser.getId())); - vmEntity.deploy(reservationId, Long.toString(callerUser.getId()), params, deployOnGivenHost); + String reservationId = vmEntity.reserve(planner, plan, new ExcludeList(), Long.toString(callerUser.getId())); + vmEntity.deploy(reservationId, Long.toString(callerUser.getId()), params, deployOnGivenHost); - Pair> vmParamPair = new Pair(vm, params); - if (vm != null && vm.isUpdateParameters()) { - // this value is not being sent to the backend; need only for api - // display purposes - if (template.isEnablePassword()) { - if (vm.getDetail(VmDetailConstants.PASSWORD) != null) { - userVmDetailsDao.removeDetail(vm.getId(), VmDetailConstants.PASSWORD); + vmParamPair = new Pair(vm, params); + if (vm != null && vm.isUpdateParameters()) { + // this value is not being sent to the backend; need only for api + // display purposes + if (template.isEnablePassword()) { + if (vm.getDetail(VmDetailConstants.PASSWORD) != null) { + userVmDetailsDao.removeDetail(vm.getId(), VmDetailConstants.PASSWORD); + } + vm.setUpdateParameters(false); + _vmDao.update(vm.getId(), vm); } - vm.setUpdateParameters(false); - _vmDao.update(vm.getId(), vm); } + } catch (Exception e) { + logger.error("Failed to start VM {}", vm, e); + throw new CloudRuntimeException("Failed to start VM " + vm, e); } - return vmParamPair; } @@ -5830,7 +5835,7 @@ public void collectVmDiskStatistics(final UserVm userVm) { return; } long hostId = userVm.getHostId(); - List vmNames = new ArrayList(); + List vmNames = new ArrayList<>(); vmNames.add(userVm.getInstanceName()); final HostVO host = _hostDao.findById(hostId); Account account = _accountMgr.getAccount(userVm.getAccountId()); @@ -6494,7 +6499,7 @@ protected List getSecurityGroupIdList(SecurityGroupAction cmd) { //transform group names to ids here if (cmd.getSecurityGroupNameList() != null) { - List securityGroupIds = new ArrayList(); + List securityGroupIds = new ArrayList<>(); for (String groupName : cmd.getSecurityGroupNameList()) { SecurityGroup sg = _securityGroupMgr.getSecurityGroup(groupName, cmd.getEntityOwnerId()); if (sg == null) { @@ -7232,7 +7237,7 @@ private List getVmVolumesForMigrateVmWithStorage(VMInstanceVO vm) { } private Map getVolumePoolMappingForMigrateVmWithStorage(VMInstanceVO vm, Map volumeToPool) { - Map volToPoolObjectMap = new HashMap(); + Map volToPoolObjectMap = new HashMap<>(); List vmVolumes = getVmVolumesForMigrateVmWithStorage(vm); @@ -8149,10 +8154,8 @@ protected void addAdditionalNetworksToVm(UserVmVO vm, Account newAccount, List Date: Fri, 20 Feb 2026 17:19:58 +0100 Subject: [PATCH 003/205] [20.3] Implement/fix limit validation for projects --- .../cloud/projects/ProjectManagerImpl.java | 51 +++++++++++++++---- .../resourcelimit/CheckedReservation.java | 4 +- .../com/cloud/resourcelimit/Reserver.java | 24 +++++++++ .../ResourceLimitManagerImpl.java | 5 -- 4 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 server/src/main/java/com/cloud/resourcelimit/Reserver.java diff --git a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java index 7a743e3ce767..8302f0ddf150 100644 --- a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java +++ b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java @@ -36,6 +36,7 @@ import javax.mail.MessagingException; import javax.naming.ConfigurationException; +import com.cloud.resourcelimit.CheckedReservation; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ProjectRole; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -47,6 +48,7 @@ import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.cloudstack.utils.mailing.MailAddress; import org.apache.cloudstack.utils.mailing.SMTPMailProperties; import org.apache.cloudstack.utils.mailing.SMTPMailSender; @@ -159,6 +161,8 @@ public class ProjectManagerImpl extends ManagerBase implements ProjectManager, C private VpcManager _vpcMgr; @Inject MessageBus messageBus; + @Inject + private ReservationDao reservationDao; protected boolean _invitationRequired = false; protected long _invitationTimeOut = 86400000; @@ -272,8 +276,7 @@ public Project createProject(final String name, final String displayText, String owner = _accountDao.findById(user.getAccountId()); } - //do resource limit check - _resourceLimitMgr.checkResourceLimit(owner, ResourceType.project); + try (CheckedReservation projectReservation = new CheckedReservation(owner, ResourceType.project, null, null, 1L, reservationDao, _resourceLimitMgr)) { final Account ownerFinal = owner; User finalUser = user; @@ -308,6 +311,7 @@ public Project doInTransaction(TransactionStatus status) { messageBus.publish(_name, ProjectManager.MESSAGE_CREATE_TUNGSTEN_PROJECT_EVENT, PublishScope.LOCAL, project); return project; + } } @Override @@ -491,6 +495,9 @@ public Boolean doInTransaction(TransactionStatus status) { //remove account ProjectAccountVO projectAccount = _projectAccountDao.findByProjectIdAccountId(projectId, account.getId()); success = _projectAccountDao.remove(projectAccount.getId()); + if (projectAccount.getAccountRole() == Role.Admin) { + _resourceLimitMgr.decrementResourceCount(account.getId(), ResourceType.project); + } //remove all invitations for account if (success) { @@ -594,12 +601,22 @@ public boolean addUserToProject(Long projectId, String username, String email, L if (username == null) { throw new InvalidParameterValueException("User information (ID) is required to add user to the project"); } + + boolean shouldIncrementResourceCount = projectRole != null && Role.Admin == projectRole; + try (CheckedReservation cr = new CheckedReservation(userAccount, ResourceType.project, shouldIncrementResourceCount ? 1L : 0L, reservationDao, _resourceLimitMgr)) { if (assignUserToProject(project, user.getId(), user.getAccountId(), projectRole, Optional.ofNullable(role).map(ProjectRole::getId).orElse(null)) != null) { + if (shouldIncrementResourceCount) { + _resourceLimitMgr.incrementResourceCount(userAccount.getId(), ResourceType.project); + } return true; + } else { + logger.warn("Failed to add user to project: {}", project); + return false; + } + } catch (ResourceAllocationException e) { + throw new RuntimeException(e); } - logger.warn("Failed to add user to project: {}", project); - return false; } } @@ -652,14 +669,18 @@ public boolean canModifyProjectAccount(Account caller, long accountId) { } private void updateProjectAccount(ProjectAccountVO futureOwner, Role newAccRole, Long accountId) throws ResourceAllocationException { - _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(accountId), ResourceType.project); + Account account = _accountMgr.getAccount(accountId); + boolean shouldIncrementResourceCount = Role.Admin == newAccRole; + + try (CheckedReservation checkedReservation = new CheckedReservation(account, ResourceType.project, shouldIncrementResourceCount ? 1L : 0L, reservationDao, _resourceLimitMgr)) { futureOwner.setAccountRole(newAccRole); _projectAccountDao.update(futureOwner.getId(), futureOwner); - if (newAccRole != null && Role.Admin == newAccRole) { + if (shouldIncrementResourceCount) { _resourceLimitMgr.incrementResourceCount(accountId, ResourceType.project); } else { _resourceLimitMgr.decrementResourceCount(accountId, ResourceType.project); } + } } @Override @@ -701,7 +722,8 @@ public void doInTransactionWithoutResult(TransactionStatus status) throws Resour " doesn't belong to the project. Add it to the project first and then change the project's ownership"); } - //do resource limit check + try (CheckedReservation checkedReservation = new CheckedReservation(futureOwnerAccount, ResourceType.project, null, null, 1L, reservationDao, _resourceLimitMgr)) { + _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(futureOwnerAccount.getId()), ResourceType.project); //unset the role for the old owner @@ -714,7 +736,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) throws Resour futureOwner.setAccountRole(Role.Admin); _projectAccountDao.update(futureOwner.getId(), futureOwner); _resourceLimitMgr.incrementResourceCount(futureOwnerAccount.getId(), ResourceType.project); - + } } else { logger.trace("Future owner {}is already the owner of the project {}", newOwnerName, project); } @@ -857,13 +879,22 @@ public boolean addAccountToProject(long projectId, String accountName, String em if (account == null) { throw new InvalidParameterValueException("Account information is required for assigning account to the project"); } + + boolean shouldIncrementResourceCount = projectRoleType != null && Role.Admin == projectRoleType; + try (CheckedReservation cr = new CheckedReservation(account, ResourceType.project, shouldIncrementResourceCount ? 1L : 0L, reservationDao, _resourceLimitMgr)) { if (assignAccountToProject(project, account.getId(), projectRoleType, null, Optional.ofNullable(projectRole).map(ProjectRole::getId).orElse(null)) != null) { + if (shouldIncrementResourceCount) { + _resourceLimitMgr.incrementResourceCount(account.getId(), ResourceType.project); + } return true; } else { logger.warn("Failed to add account {} to project {}", accountName, project); return false; } + } catch (ResourceAllocationException e) { + throw new RuntimeException(e); + } } } @@ -1042,7 +1073,9 @@ public Boolean doInTransaction(TransactionStatus status) { boolean success = true; ProjectAccountVO projectAccount = _projectAccountDao.findByProjectIdUserId(projectId, user.getAccountId(), user.getId()); success = _projectAccountDao.remove(projectAccount.getId()); - + if (projectAccount.getAccountRole() == Role.Admin) { + _resourceLimitMgr.decrementResourceCount(user.getAccountId(), ResourceType.project); + } if (success) { logger.debug("Removed user {} from project. Removing any invite sent to the user", user); ProjectInvitation invite = _projectInvitationDao.findByUserIdProjectId(user.getId(), user.getAccountId(), projectId); diff --git a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java index 211ca65a9d8f..8d2e19d475e2 100644 --- a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java +++ b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java @@ -40,7 +40,7 @@ import com.cloud.utils.exception.CloudRuntimeException; -public class CheckedReservation implements AutoCloseable { +public class CheckedReservation implements Reserver { protected Logger logger = LogManager.getLogger(getClass()); private static final int TRY_TO_GET_LOCK_TIME = 120; @@ -174,7 +174,7 @@ protected void setQuotaLimitLock(GlobalLock quotaLimitLock) { } @Override - public void close() throws Exception { + public void close() { removeAllReservations(); } diff --git a/server/src/main/java/com/cloud/resourcelimit/Reserver.java b/server/src/main/java/com/cloud/resourcelimit/Reserver.java new file mode 100644 index 000000000000..4d5e3ac30c50 --- /dev/null +++ b/server/src/main/java/com/cloud/resourcelimit/Reserver.java @@ -0,0 +1,24 @@ +// 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 com.cloud.resourcelimit; + +public interface Reserver extends AutoCloseable { + + void close(); + +} diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 8db204cf177b..01dcd1125daf 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -1199,11 +1199,6 @@ public long recalculateDomainResourceCount(final long domainId, final ResourceTy long newResourceCount = 0L; ResourceCountVO domainRC = null; - // calculate project count here - if (type == ResourceType.project) { - newResourceCount += _projectDao.countProjectsForDomain(domainId); - } - if (type == ResourceType.network) { newResourceCount += networkDomainDao.listDomainNetworkMapByDomain(domainId).size(); } From c9e644af74a36f60053012c38656589e255a97b5 Mon Sep 17 00:00:00 2001 From: dahn Date: Fri, 20 Feb 2026 17:26:25 +0100 Subject: [PATCH 004/205] [20.3] resource allocation vpc --- .../main/java/com/cloud/network/vpc/VpcManagerImpl.java | 3 +++ .../java/com/cloud/network/vpc/VpcManagerImplTest.java | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java index 7fcdf5ce56e9..4a952bb582dc 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -50,6 +50,7 @@ import com.cloud.dc.Vlan; import com.cloud.network.dao.NsxProviderDao; import com.cloud.network.element.NsxProviderVO; +import com.cloud.resourcelimit.CheckedReservation; import com.google.common.collect.Sets; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.alert.AlertService; @@ -1246,6 +1247,7 @@ public Vpc createVpc(final long zoneId, final long vpcOffId, final long vpcOwner vpc.setPublicMtu(publicMtu); vpc.setDisplay(Boolean.TRUE.equals(displayVpc)); + try (CheckedReservation vpcReservation = new CheckedReservation(owner, ResourceType.vpc, null, null, 1L, reservationDao, _resourceLimitMgr)) { if (vpc.getCidr() == null && cidrSize != null) { // Allocate a CIDR for VPC Ipv4GuestSubnetNetworkMap subnet = routedIpv4Manager.getOrCreateIpv4SubnetForVpc(vpc, cidrSize); @@ -1265,6 +1267,7 @@ public Vpc createVpc(final long zoneId, final long vpcOffId, final long vpcOwner routedIpv4Manager.persistBgpPeersForVpc(newVpc.getId(), bgpPeerIds); } return newVpc; + } } private void validateVpcCidrSize(Account caller, long accountId, VpcOffering vpcOffering, String cidr, Integer cidrSize, long zoneId) { diff --git a/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java b/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java index ee56a092dd18..92d3baa8ac2d 100644 --- a/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java @@ -54,6 +54,7 @@ import com.cloud.offering.NetworkOffering; import com.cloud.offerings.NetworkOfferingServiceMapVO; import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; +import com.cloud.resourcelimit.CheckedReservation; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountVO; @@ -81,6 +82,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnitRunner; @@ -516,7 +518,7 @@ public void testCreateVpc() { VpcVO vpc = Mockito.mock(VpcVO.class); Mockito.when(vpcDao.persist(any(), anyMap())).thenReturn(vpc); Mockito.when(vpc.getUuid()).thenReturn("uuid"); - try { + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { doNothing().when(resourceLimitService).checkResourceLimit(account, Resource.ResourceType.vpc); manager.createVpc(zoneId, vpcOfferingId, vpcOwnerId, vpcName, vpcName, ip4Cidr, vpcDomain, ip4Dns[0], ip4Dns[1], null, null, true, 1500, null, null, null); @@ -533,7 +535,7 @@ public void testCreateRoutedVpc() { Mockito.when(vpc.getUuid()).thenReturn("uuid"); doReturn(true).when(routedIpv4Manager).isRoutedVpc(any()); doNothing().when(routedIpv4Manager).getOrCreateIpv4SubnetForVpc(any(), anyString()); - try { + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { doNothing().when(resourceLimitService).checkResourceLimit(account, Resource.ResourceType.vpc); manager.createVpc(zoneId, vpcOfferingId, vpcOwnerId, vpcName, vpcName, ip4Cidr, vpcDomain, ip4Dns[0], ip4Dns[1], null, null, true, 1500, null, null, null); @@ -556,7 +558,7 @@ public void testCreateRoutedVpcWithDynamicRouting() { Ipv4GuestSubnetNetworkMap ipv4GuestSubnetNetworkMap = Mockito.mock(Ipv4GuestSubnetNetworkMap.class); doReturn(ipv4GuestSubnetNetworkMap).when(routedIpv4Manager).getOrCreateIpv4SubnetForVpc(any(), anyInt()); List bgpPeerIds = Arrays.asList(11L, 12L); - try { + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { doNothing().when(resourceLimitService).checkResourceLimit(account, Resource.ResourceType.vpc); manager.createVpc(zoneId, vpcOfferingId, vpcOwnerId, vpcName, vpcName, null, vpcDomain, ip4Dns[0], ip4Dns[1], null, null, true, 1500, 24, null, bgpPeerIds); From d8bdc8b3b893ff813b9ef21ee5b8ec4685e56dff Mon Sep 17 00:00:00 2001 From: dahn Date: Fri, 20 Feb 2026 17:27:15 +0100 Subject: [PATCH 005/205] Check resource reservation on volume creation --- .../com/cloud/user/ResourceLimitService.java | 2 +- .../resourcelimit/ResourceLimitManagerImpl.java | 3 ++- .../com/cloud/storage/VolumeApiServiceImpl.java | 16 ++++++++++++++-- .../cloud/vpc/MockResourceLimitManagerImpl.java | 5 +++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/com/cloud/user/ResourceLimitService.java b/api/src/main/java/com/cloud/user/ResourceLimitService.java index fc486601d16d..936095551220 100644 --- a/api/src/main/java/com/cloud/user/ResourceLimitService.java +++ b/api/src/main/java/com/cloud/user/ResourceLimitService.java @@ -247,7 +247,7 @@ public interface ResourceLimitService { void updateTaggedResourceLimitsAndCountsForAccounts(List responses, String tag); void updateTaggedResourceLimitsAndCountsForDomains(List responses, String tag); void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering) throws ResourceAllocationException; - + List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering); void checkVolumeResourceLimitForDiskOfferingChange(Account owner, Boolean display, Long currentSize, Long newSize, DiskOffering currentOffering, DiskOffering newOffering) throws ResourceAllocationException; diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 01dcd1125daf..09a0dda3aaa2 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -1647,7 +1647,8 @@ public List getResourceLimitStorageTags(DiskOffering diskOffering) { return tags; } - protected List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering) { + @Override + public List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering) { if (Boolean.FALSE.equals(display)) { return new ArrayList<>(); } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 4f8b55d16fb8..38102619be5c 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -35,6 +35,7 @@ import javax.inject.Inject; +import com.cloud.resourcelimit.CheckedReservation; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.InternalIdentity; @@ -87,6 +88,7 @@ import org.apache.cloudstack.framework.jobs.impl.OutcomeImpl; import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; import org.apache.cloudstack.jobs.JobInfo; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.cloudstack.resourcedetail.DiskOfferingDetailVO; import org.apache.cloudstack.resourcedetail.SnapshotPolicyDetailVO; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; @@ -354,6 +356,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic private BackupDao backupDao; @Inject HostPodDao podDao; + @Inject + private ReservationDao reservationDao; protected Gson _gson; @@ -918,8 +922,12 @@ public VolumeVO allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationExcept Storage.ProvisioningType provisioningType = diskOffering.getProvisioningType(); - // Check that the resource limit for volume & primary storage won't be exceeded - _resourceLimitMgr.checkVolumeResourceLimit(owner,displayVolume, size, diskOffering); + List tags = _resourceLimitMgr.getResourceLimitStorageTagsForResourceCountOperation(displayVolume, diskOffering); + if (tags.size() == 1 && tags.get(0) == null) { + tags = new ArrayList<>(); + } + try (CheckedReservation volumeReservation = new CheckedReservation(owner, ResourceType.volume, null, tags, 1L, reservationDao, _resourceLimitMgr); + CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, null, tags, size, reservationDao, _resourceLimitMgr)) { // Verify that zone exists DataCenterVO zone = _dcDao.findById(zoneId); @@ -942,6 +950,10 @@ public VolumeVO allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationExcept return commitVolume(cmd, caller, owner, displayVolume, zoneId, diskOfferingId, provisioningType, size, minIops, maxIops, parentVolume, userSpecifiedName, _uuidMgr.generateUuid(Volume.class, cmd.getCustomId()), details); + } catch (Exception e) { + logger.error(e); + throw new RuntimeException(e); + } } @Override diff --git a/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java index 7e8dbbf04c6c..045f21785295 100644 --- a/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java @@ -277,6 +277,11 @@ public void checkVolumeResourceLimit(Account owner, Boolean display, Long size, } + @Override + public List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering) { + return null; + } + @Override public void checkVolumeResourceLimitForDiskOfferingChange(Account owner, Boolean display, Long currentSize, Long newSize, DiskOffering currentOffering, DiskOffering newOffering) throws ResourceAllocationException { From ae177a165561fe77436d70c3d5b5a229e67846b9 Mon Sep 17 00:00:00 2001 From: dahn Date: Fri, 20 Feb 2026 17:27:34 +0100 Subject: [PATCH 006/205] Fix: KVM Direct Download URL injection --- .../direct/download/DirectTemplateDownloaderImpl.java | 11 ++++++----- .../download/MetalinkDirectTemplateDownloader.java | 2 +- .../direct/download/NfsDirectTemplateDownloader.java | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/DirectTemplateDownloaderImpl.java b/core/src/main/java/org/apache/cloudstack/direct/download/DirectTemplateDownloaderImpl.java index a1485463eaa0..05619e5632b0 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/DirectTemplateDownloaderImpl.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/DirectTemplateDownloaderImpl.java @@ -21,6 +21,7 @@ import com.cloud.utils.UriUtils; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.utils.security.DigestHelper; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -33,6 +34,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.UUID; public abstract class DirectTemplateDownloaderImpl implements DirectTemplateDownloader { @@ -128,15 +130,14 @@ public void setFollowRedirects(boolean followRedirects) { */ protected File createTemporaryDirectoryAndFile(String downloadDir) { createFolder(downloadDir); - return new File(downloadDir + File.separator + getFileNameFromUrl()); + return new File(downloadDir + File.separator + getTemporaryFileName()); } /** - * Return filename from url + * Return filename from the temporary download file */ - public String getFileNameFromUrl() { - String[] urlParts = url.split("/"); - return urlParts[urlParts.length - 1]; + public String getTemporaryFileName() { + return String.format("%s.%s", UUID.randomUUID(), FilenameUtils.getExtension(url)); } @Override diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/MetalinkDirectTemplateDownloader.java b/core/src/main/java/org/apache/cloudstack/direct/download/MetalinkDirectTemplateDownloader.java index 2050b9ef09f7..854c310cde9a 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/MetalinkDirectTemplateDownloader.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/MetalinkDirectTemplateDownloader.java @@ -97,7 +97,7 @@ public Pair downloadTemplate() { DirectTemplateDownloader urlDownloader = createDownloaderForMetalinks(getUrl(), getTemplateId(), getDestPoolPath(), getChecksum(), headers, connectTimeout, soTimeout, null, temporaryDownloadPath); try { - setDownloadedFilePath(downloadDir + File.separator + getFileNameFromUrl()); + setDownloadedFilePath(downloadDir + File.separator + getTemporaryFileName()); File f = new File(getDownloadedFilePath()); if (f.exists()) { f.delete(); diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/NfsDirectTemplateDownloader.java b/core/src/main/java/org/apache/cloudstack/direct/download/NfsDirectTemplateDownloader.java index 21184ef07fe9..6b0959b78ffb 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/NfsDirectTemplateDownloader.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/NfsDirectTemplateDownloader.java @@ -69,7 +69,7 @@ public Pair downloadTemplate() { String mount = String.format(mountCommand, srcHost + ":" + srcPath, "/mnt/" + mountSrcUuid); Script.runSimpleBashScript(mount); String downloadDir = getDestPoolPath() + File.separator + getDirectDownloadTempPath(getTemplateId()); - setDownloadedFilePath(downloadDir + File.separator + getFileNameFromUrl()); + setDownloadedFilePath(downloadDir + File.separator + getTemporaryFileName()); Script.runSimpleBashScript("cp /mnt/" + mountSrcUuid + srcPath + " " + getDownloadedFilePath()); Script.runSimpleBashScript("umount /mnt/" + mountSrcUuid); return new Pair<>(true, getDownloadedFilePath()); From d75140b657986bb214ab06f12b323b52b30eb8fe Mon Sep 17 00:00:00 2001 From: dahn Date: Fri, 20 Feb 2026 17:28:48 +0100 Subject: [PATCH 007/205] [20.3] handle user's canned policy when a bucket is deleted --- .../java/com/cloud/agent/api/to/BucketTO.java | 7 ++ .../driver/MinIOObjectStoreDriverImpl.java | 81 ++++++++++++------- .../MinIOObjectStoreDriverImplTest.java | 7 +- 3 files changed, 67 insertions(+), 28 deletions(-) diff --git a/api/src/main/java/com/cloud/agent/api/to/BucketTO.java b/api/src/main/java/com/cloud/agent/api/to/BucketTO.java index f7e4bfea80fb..fd8237998a74 100644 --- a/api/src/main/java/com/cloud/agent/api/to/BucketTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/BucketTO.java @@ -26,10 +26,13 @@ public final class BucketTO { private String secretKey; + private long accountId; + public BucketTO(Bucket bucket) { this.name = bucket.getName(); this.accessKey = bucket.getAccessKey(); this.secretKey = bucket.getSecretKey(); + this.accountId = bucket.getAccountId(); } public BucketTO(String name) { @@ -47,4 +50,8 @@ public String getAccessKey() { public String getSecretKey() { return this.secretKey; } + + public long getAccountId() { + return this.accountId; + } } diff --git a/plugins/storage/object/minio/src/main/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImpl.java b/plugins/storage/object/minio/src/main/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImpl.java index 9dc4b30414e6..28e3b85e1a50 100644 --- a/plugins/storage/object/minio/src/main/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImpl.java +++ b/plugins/storage/object/minio/src/main/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImpl.java @@ -24,6 +24,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -98,6 +100,51 @@ protected String getUserOrAccessKeyForAccount(Account account) { return String.format("%s-%s", ACS_PREFIX, account.getUuid()); } + private void updateCannedPolicy(long storeId, Account account, String excludeBucket) { + List buckets = _bucketDao.listByObjectStoreIdAndAccountId(storeId, account.getId()); + + String resources = buckets.stream() + .map(BucketVO::getName) + .filter(name -> !Objects.equals(name, excludeBucket)) + .map(name -> "\"arn:aws:s3:::" + name + "/*\"") + .collect(Collectors.joining(",\n")); + String policy; + if (resources.isEmpty()) { + // Resource cannot be empty in a canned Policy so deny access to all resources if the user has no buckets + policy = " {\n" + + " \"Statement\": [\n" + + " {\n" + + " \"Action\": \"s3:*\",\n" + + " \"Effect\": \"Deny\",\n" + + " \"Resource\": [\"arn:aws:s3:::*\", \"arn:aws:s3:::*/*\"]\n" + + " }\n" + + " ],\n" + + " \"Version\": \"2012-10-17\"\n" + + " }"; + } else { + policy = " {\n" + + " \"Statement\": [\n" + + " {\n" + + " \"Action\": \"s3:*\",\n" + + " \"Effect\": \"Allow\",\n" + + " \"Resource\": [" + resources + "]\n" + + " }\n" + + " ],\n" + + " \"Version\": \"2012-10-17\"\n" + + " }"; + } + + MinioAdminClient minioAdminClient = getMinIOAdminClient(storeId); + String policyName = getUserOrAccessKeyForAccount(account) + "-policy"; + String userName = getUserOrAccessKeyForAccount(account); + try { + minioAdminClient.addCannedPolicy(policyName, policy); + minioAdminClient.setPolicy(userName, false, policyName); + } catch (NoSuchAlgorithmException | IOException | InvalidKeyException e) { + throw new CloudRuntimeException(e); + } + } + @Override public Bucket createBucket(Bucket bucket, boolean objectLock) { //ToDo Client pool mgmt @@ -125,33 +172,8 @@ public Bucket createBucket(Bucket bucket, boolean objectLock) { throw new CloudRuntimeException(e); } - List buckets = _bucketDao.listByObjectStoreIdAndAccountId(storeId, accountId); - StringBuilder resources_builder = new StringBuilder(); - for(BucketVO exitingBucket : buckets) { - resources_builder.append("\"arn:aws:s3:::"+exitingBucket.getName()+"/*\",\n"); - } - resources_builder.append("\"arn:aws:s3:::"+bucketName+"/*\"\n"); - - String policy = " {\n" + - " \"Statement\": [\n" + - " {\n" + - " \"Action\": \"s3:*\",\n" + - " \"Effect\": \"Allow\",\n" + - " \"Principal\": \"*\",\n" + - " \"Resource\": ["+resources_builder+"]" + - " }\n" + - " ],\n" + - " \"Version\": \"2012-10-17\"\n" + - " }"; - MinioAdminClient minioAdminClient = getMinIOAdminClient(storeId); - String policyName = getUserOrAccessKeyForAccount(account) + "-policy"; - String userName = getUserOrAccessKeyForAccount(account); - try { - minioAdminClient.addCannedPolicy(policyName, policy); - minioAdminClient.setPolicy(userName, false, policyName); - } catch (Exception e) { - throw new CloudRuntimeException(e); - } + updateCannedPolicy(storeId, account,null); + String accessKey = _accountDetailsDao.findDetail(accountId, MINIO_ACCESS_KEY).getValue(); String secretKey = _accountDetailsDao.findDetail(accountId, MINIO_SECRET_KEY).getValue(); ObjectStoreVO store = _storeDao.findById(storeId); @@ -183,6 +205,8 @@ public List listBuckets(long storeId) { @Override public boolean deleteBucket(BucketTO bucket, long storeId) { String bucketName = bucket.getName(); + long accountId = bucket.getAccountId(); + Account account = _accountDao.findById(accountId); MinioClient minioClient = getMinIOClient(storeId); try { if(!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { @@ -197,6 +221,9 @@ public boolean deleteBucket(BucketTO bucket, long storeId) { } catch (Exception e) { throw new CloudRuntimeException(e); } + + updateCannedPolicy(storeId, account, bucketName); + return true; } diff --git a/plugins/storage/object/minio/src/test/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImplTest.java b/plugins/storage/object/minio/src/test/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImplTest.java index 1a8b3d9663a2..d3298a235ca4 100644 --- a/plugins/storage/object/minio/src/test/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImplTest.java +++ b/plugins/storage/object/minio/src/test/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImplTest.java @@ -129,10 +129,15 @@ public void testCreateBucket() throws Exception { @Test public void testDeleteBucket() throws Exception { String bucketName = "test-bucket"; - BucketTO bucket = new BucketTO(bucketName); + BucketVO bucketVO = new BucketVO(1L, 1L, 1L, bucketName, 1, false, false, false, null); + BucketTO bucket = new BucketTO(bucketVO); + when(accountDao.findById(1L)).thenReturn(account); + when(account.getUuid()).thenReturn(UUID.randomUUID().toString()); + when(bucketDao.listByObjectStoreIdAndAccountId(anyLong(), anyLong())).thenReturn(new ArrayList()); doReturn(minioClient).when(minioObjectStoreDriverImpl).getMinIOClient(anyLong()); when(minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())).thenReturn(true); doNothing().when(minioClient).removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); + doReturn(minioAdminClient).when(minioObjectStoreDriverImpl).getMinIOAdminClient(anyLong()); boolean success = minioObjectStoreDriverImpl.deleteBucket(bucket, 1L); assertTrue(success); verify(minioClient, times(1)).bucketExists(any()); From 9a4f3415507835c88ed1720a0a2a08907405bad8 Mon Sep 17 00:00:00 2001 From: dahn Date: Fri, 20 Feb 2026 17:29:25 +0100 Subject: [PATCH 008/205] Check resource reservation on volume snapshot creation --- .../storage/snapshot/SnapshotManagerImpl.java | 37 ++++++++++++------- .../storage/snapshot/SnapshotManagerTest.java | 11 ++++-- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index 19cde4da0f17..d5475948c59b 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -32,6 +32,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.resourcelimit.CheckedReservation; import org.apache.cloudstack.acl.SecurityChecker; import com.cloud.api.ApiDBUtils; import org.apache.cloudstack.annotation.AnnotationService; @@ -67,6 +68,7 @@ import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.cloudstack.resourcedetail.SnapshotPolicyDetailVO; import org.apache.cloudstack.resourcedetail.dao.SnapshotPolicyDetailsDao; import org.apache.cloudstack.snapshot.SnapshotHelper; @@ -240,6 +242,8 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement @Inject private AnnotationDao annotationDao; + @Inject + private ReservationDao reservationDao; @Inject protected SnapshotHelper snapshotHelper; @Inject @@ -1705,20 +1709,6 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Type snapshotType = getSnapshotType(policyId); Account owner = _accountMgr.getAccount(volume.getAccountId()); - ResourceType storeResourceType = getStoreResourceType(volume.getDataCenterId(), locationType); - try { - _resourceLimitMgr.checkResourceLimit(owner, ResourceType.snapshot); - _resourceLimitMgr.checkResourceLimit(owner, storeResourceType, volume.getSize()); - } catch (ResourceAllocationException e) { - if (snapshotType != Type.MANUAL) { - String msg = String.format("Snapshot resource limit exceeded for account %s. Failed to create recurring snapshots", owner); - logger.warn(msg); - _alertMgr.sendAlert(AlertManager.AlertType.ALERT_TYPE_UPDATE_RESOURCE_COUNT, 0L, 0L, msg, "Snapshot resource limit exceeded for account id : " + owner.getId() - + ". Failed to create recurring snapshots; please use updateResourceLimit to increase the limit"); - } - throw e; - } - // Determine the name for this snapshot // Snapshot Name: VMInstancename + volumeName + timeString String timeString = DateUtil.getDateDisplayString(DateUtil.GMT_TIMEZONE, new Date(), DateUtil.YYYYMMDD_FORMAT); @@ -1750,6 +1740,14 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, hypervisorType = volume.getHypervisorType(); } + ResourceType storeResourceType = ResourceType.secondary_storage; + if (!isBackupSnapshotToSecondaryForZone(volume.getDataCenterId()) || + Snapshot.LocationType.PRIMARY.equals(locationType)) { + storeResourceType = ResourceType.primary_storage; + } + + try (CheckedReservation volumeSnapshotReservation = new CheckedReservation(owner, ResourceType.snapshot, null, null, 1L, reservationDao, _resourceLimitMgr); + CheckedReservation storageReservation = new CheckedReservation(owner, storeResourceType, null, null, volume.getSize(), reservationDao, _resourceLimitMgr)) { SnapshotVO snapshotVO = new SnapshotVO(volume.getDataCenterId(), volume.getAccountId(), volume.getDomainId(), volume.getId(), volume.getDiskOfferingId(), snapshotName, (short)snapshotType.ordinal(), snapshotType.name(), volume.getSize(), volume.getMinIops(), volume.getMaxIops(), hypervisorType, locationType); @@ -1761,6 +1759,17 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), ResourceType.snapshot); _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), storeResourceType, volume.getSize()); return snapshot; + } catch (Exception e) { + if (e instanceof ResourceAllocationException) { + if (snapshotType != Type.MANUAL) { + String msg = String.format("Snapshot resource limit exceeded for account id : %s. Failed to create recurring snapshots", owner.getId()); + logger.warn(msg); + _alertMgr.sendAlert(AlertManager.AlertType.ALERT_TYPE_UPDATE_RESOURCE_COUNT, 0L, 0L, msg, msg + ". Please, use updateResourceLimit to increase the limit"); + } + throw (ResourceAllocationException) e; + } + throw new CloudRuntimeException(e); + } } @Override diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java index 28903c72cc3c..5513536ab758 100755 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java @@ -36,6 +36,7 @@ import com.cloud.api.ApiDBUtils; import com.cloud.exception.PermissionDeniedException; +import com.cloud.resourcelimit.CheckedReservation; import com.cloud.storage.Storage; import org.apache.cloudstack.api.command.user.snapshot.ExtractSnapshotCmd; import org.apache.cloudstack.context.CallContext; @@ -65,6 +66,7 @@ import org.mockito.BDDMockito; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -233,8 +235,6 @@ public void setup() throws ResourceAllocationException { when(_storageStrategyFactory.getSnapshotStrategy(Mockito.any(SnapshotVO.class), Mockito.eq(SnapshotOperation.BACKUP))).thenReturn(snapshotStrategy); when(_storageStrategyFactory.getSnapshotStrategy(Mockito.any(SnapshotVO.class), Mockito.eq(SnapshotOperation.REVERT))).thenReturn(snapshotStrategy); - doNothing().when(_resourceLimitMgr).checkResourceLimit(any(Account.class), any(ResourceType.class)); - doNothing().when(_resourceLimitMgr).checkResourceLimit(any(Account.class), any(ResourceType.class), anyLong()); doNothing().when(_resourceLimitMgr).decrementResourceCount(anyLong(), any(ResourceType.class), anyLong()); doNothing().when(_resourceLimitMgr).incrementResourceCount(anyLong(), any(ResourceType.class)); doNothing().when(_resourceLimitMgr).incrementResourceCount(anyLong(), any(ResourceType.class), anyLong()); @@ -317,7 +317,12 @@ public void testAllocSnapshotF4() throws ResourceAllocationException { when(mockList2.size()).thenReturn(0); when(_vmSnapshotDao.listByInstanceId(TEST_VM_ID, VMSnapshot.State.Creating, VMSnapshot.State.Reverting, VMSnapshot.State.Expunging)).thenReturn(mockList2); when(_snapshotDao.persist(any(SnapshotVO.class))).thenReturn(snapshotMock); - _snapshotMgr.allocSnapshot(TEST_VOLUME_ID, Snapshot.MANUAL_POLICY_ID, null, null); + + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { + _snapshotMgr.allocSnapshot(TEST_VOLUME_ID, Snapshot.MANUAL_POLICY_ID, null, null); + } catch (ResourceAllocationException e) { + Assert.fail(String.format("Failure with exception: %s", e.getMessage())); + } } @Test(expected = InvalidParameterValueException.class) From b07831b56c1e9067bc24151ef737fa1e4c619411 Mon Sep 17 00:00:00 2001 From: Daniel Augusto Veronezi Salvador <38945620+GutoVeronezi@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:44:24 -0300 Subject: [PATCH 009/205] Implement/fix limit validation for secondary storage --- .../manager/BareMetalTemplateAdapter.java | 1 - .../storage/ImageStoreUploadMonitorImpl.java | 26 +- .../cloud/storage/VolumeApiServiceImpl.java | 3 +- .../template/HypervisorTemplateAdapter.java | 35 +- .../cloud/template/TemplateAdapterBase.java | 3 - .../cloud/template/TemplateManagerImpl.java | 111 +++-- .../template/TemplateManagerImplTest.java | 402 +++--------------- 7 files changed, 158 insertions(+), 423 deletions(-) diff --git a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java index 940897de3c95..c6c38a398098 100644 --- a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java +++ b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java @@ -106,7 +106,6 @@ public VMTemplateVO create(TemplateProfile profile) { } } - _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); return template; } diff --git a/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java b/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java index 334e9f108356..408eb69917a2 100755 --- a/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java +++ b/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java @@ -26,6 +26,10 @@ import javax.naming.ConfigurationException; import com.cloud.agent.api.to.OVFInformationTO; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.resourcelimit.CheckedReservation; +import com.cloud.user.Account; +import com.cloud.user.dao.AccountDao; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; @@ -37,6 +41,7 @@ import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.cloudstack.storage.command.UploadStatusAnswer; import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus; import org.apache.cloudstack.storage.command.UploadStatusCommand; @@ -117,6 +122,10 @@ public class ImageStoreUploadMonitorImpl extends ManagerBase implements ImageSto private TemplateJoinDao templateJoinDao; @Inject private DeployAsIsHelper deployAsIsHelper; + @Inject + private ReservationDao reservationDao; + @Inject + private AccountDao accountDao; private long _nodeId; private ScheduledExecutorService _executor = null; @@ -436,8 +445,23 @@ public void doInTransactionWithoutResult(TransactionStatus status) { break; } } + + Account owner = accountDao.findById(template.getAccountId()); + long templateSize = answer.getVirtualSize(); + + try (CheckedReservation secondaryStorageReservation = new CheckedReservation(owner, Resource.ResourceType.secondary_storage, null, null, templateSize, reservationDao, _resourceLimitMgr)) { + _resourceLimitMgr.incrementResourceCount(owner.getId(), Resource.ResourceType.secondary_storage, templateSize); + } catch (ResourceAllocationException e) { + tmpTemplateDataStore.setDownloadState(VMTemplateStorageResourceAssoc.Status.UPLOAD_ERROR); + tmpTemplateDataStore.setState(State.Failed); + stateMachine.transitTo(tmpTemplate, VirtualMachineTemplate.Event.OperationFailed, null, _templateDao); + msg = String.format("Upload of template [%s] failed because its owner [%s] does not have enough secondary storage space available.", template.getUuid(), owner.getUuid()); + logger.warn(msg); + sendAlert = true; + break; + } + stateMachine.transitTo(tmpTemplate, VirtualMachineTemplate.Event.OperationSucceeded, null, _templateDao); - _resourceLimitMgr.incrementResourceCount(template.getAccountId(), Resource.ResourceType.secondary_storage, answer.getVirtualSize()); //publish usage event String etype = EventTypes.EVENT_TEMPLATE_CREATE; if (tmpTemplate.getFormat() == Storage.ImageFormat.ISO) { diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 38102619be5c..a975bd624084 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -107,6 +107,7 @@ import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; +import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; @@ -522,7 +523,7 @@ public GetUploadParamsResponse doInTransaction(TransactionStatus status) throws Account account = _accountDao.findById(accountId); Domain domain = domainDao.findById(account.getDomainId()); - command.setDefaultMaxSecondaryStorageInGB(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null)); + command.setDefaultMaxSecondaryStorageInGB(ByteScaleUtils.bytesToGibibytes(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null))); command.setAccountId(accountId); Gson gson = new GsonBuilder().create(); String metadata = EncryptionUtil.encodeData(gson.toJson(command), key); diff --git a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java index 1422e788e24c..c096ef0eb1df 100644 --- a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java +++ b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java @@ -37,10 +37,8 @@ import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.command.user.iso.DeleteIsoCmd; -import org.apache.cloudstack.api.command.user.iso.GetUploadParamsForIsoCmd; import org.apache.cloudstack.api.command.user.iso.RegisterIsoCmd; import org.apache.cloudstack.api.command.user.template.DeleteTemplateCmd; -import org.apache.cloudstack.api.command.user.template.GetUploadParamsForTemplateCmd; import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.direct.download.DirectDownloadManager; @@ -66,6 +64,7 @@ import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.storage.heuristics.HeuristicRuleHelper; import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; +import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.security.DigestHelper; import org.apache.commons.collections.CollectionUtils; @@ -217,19 +216,6 @@ public TemplateProfile prepare(RegisterIsoCmd cmd) throws ResourceAllocationExce profile.setSize(templateSize); } profile.setUrl(url); - // Check that the resource limit for secondary storage won't be exceeded - _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(cmd.getEntityOwnerId()), - ResourceType.secondary_storage, - UriUtils.getRemoteSize(url, followRedirects)); - return profile; - } - - @Override - public TemplateProfile prepare(GetUploadParamsForIsoCmd cmd) throws ResourceAllocationException { - TemplateProfile profile = super.prepare(cmd); - - // Check that the resource limit for secondary storage won't be exceeded - _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(cmd.getEntityOwnerId()), ResourceType.secondary_storage); return profile; } @@ -247,19 +233,7 @@ public TemplateProfile prepare(RegisterTemplateCmd cmd) throws ResourceAllocatio profile.setSize(templateSize); } profile.setUrl(url); - // Check that the resource limit for secondary storage won't be exceeded - _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(cmd.getEntityOwnerId()), - ResourceType.secondary_storage, - UriUtils.getRemoteSize(url, followRedirects)); - return profile; - } - - @Override - public TemplateProfile prepare(GetUploadParamsForTemplateCmd cmd) throws ResourceAllocationException { - TemplateProfile profile = super.prepare(cmd); - // Check that the resource limit for secondary storage won't be exceeded - _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(cmd.getEntityOwnerId()), ResourceType.secondary_storage); return profile; } @@ -287,7 +261,6 @@ public VMTemplateVO create(TemplateProfile profile) { persistDirectDownloadTemplate(template.getId(), profile.getSize()); } - _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); return template; } @@ -434,7 +407,7 @@ public List doInTransaction(TransactionStatus if(payloads.isEmpty()) { throw new CloudRuntimeException("unable to find zone or an image store with enough capacity"); } - _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); + return payloads; } }); @@ -477,7 +450,7 @@ private void postUploadAllocation(List imageStores, VMTemplateVO temp Account account = _accountDao.findById(accountId); Domain domain = _domainDao.findById(account.getDomainId()); - payload.setDefaultMaxSecondaryStorageInGB(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null)); + payload.setDefaultMaxSecondaryStorageInGB(ByteScaleUtils.bytesToGibibytes(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null))); payload.setAccountId(accountId); payload.setRemoteEndPoint(ep.getPublicAddr()); payload.setRequiresHvm(template.requiresHvm()); @@ -543,8 +516,8 @@ protected Void createTemplateAsyncCallBack(AsyncCallbackDispatcher 0) { + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.secondary_storage, secondaryStorageUsage); + } + if (template != null) { + CallContext.current().putContextParameter(VirtualMachineTemplate.class, template.getUuid()); + return template; + } } + + throw new CloudRuntimeException("Failed to create ISO"); } @Override @@ -375,18 +396,32 @@ public VirtualMachineTemplate registerTemplate(RegisterTemplateCmd cmd) throws U } TemplateAdapter adapter = getAdapter(HypervisorType.getType(cmd.getHypervisor())); + Account owner = _accountMgr.getAccount(cmd.getEntityOwnerId()); + + long secondaryStorageUsage = adapter instanceof HypervisorTemplateAdapter && !cmd.isDirectDownload() ? + UriUtils.getRemoteSize(cmd.getUrl(), StorageManager.DataStoreDownloadFollowRedirects.value()) : 0L; + + try (CheckedReservation templateReservation = new CheckedReservation(owner, ResourceType.template, null, null, 1L, reservationDao, _resourceLimitMgr); + CheckedReservation secondaryStorageReservation = new CheckedReservation(owner, ResourceType.secondary_storage, null, null, secondaryStorageUsage, reservationDao, _resourceLimitMgr)) { TemplateProfile profile = adapter.prepare(cmd); VMTemplateVO template = adapter.create(profile); + // Secondary storage resource usage will be recalculated in com.cloud.template.HypervisorTemplateAdapter.createTemplateAsyncCallBack + // for HypervisorTemplateAdapter + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); + if (secondaryStorageUsage > 0) { + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.secondary_storage, secondaryStorageUsage); + } + if (template != null) { CallContext.current().putContextParameter(VirtualMachineTemplate.class, template.getUuid()); if (cmd instanceof RegisterVnfTemplateCmd) { vnfTemplateManager.persistVnfTemplate(template.getId(), (RegisterVnfTemplateCmd) cmd); } return template; - } else { - throw new CloudRuntimeException("Failed to create a Template"); } + } + throw new CloudRuntimeException("Failed to create a Template"); } /** @@ -450,17 +485,35 @@ private GetUploadParamsResponse registerPostUploadInternal(TemplateAdapter adapt @Override @ActionEvent(eventType = EventTypes.EVENT_ISO_CREATE, eventDescription = "Creating post upload ISO") public GetUploadParamsResponse registerIsoForPostUpload(GetUploadParamsForIsoCmd cmd) throws ResourceAllocationException, MalformedURLException { - TemplateAdapter adapter = getAdapter(HypervisorType.None); - TemplateProfile profile = adapter.prepare(cmd); - return registerPostUploadInternal(adapter, profile); + Account owner = _accountMgr.getAccount(cmd.getEntityOwnerId()); + + try (CheckedReservation templateReservation = new CheckedReservation(owner, ResourceType.template, null, null, 1L, reservationDao, _resourceLimitMgr)) { + TemplateAdapter adapter = getAdapter(HypervisorType.None); + TemplateProfile profile = adapter.prepare(cmd); + + GetUploadParamsResponse response = registerPostUploadInternal(adapter, profile); + + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); + + return response; + } } @Override @ActionEvent(eventType = EventTypes.EVENT_TEMPLATE_CREATE, eventDescription = "Creating post upload Template") public GetUploadParamsResponse registerTemplateForPostUpload(GetUploadParamsForTemplateCmd cmd) throws ResourceAllocationException, MalformedURLException { - TemplateAdapter adapter = getAdapter(HypervisorType.getType(cmd.getHypervisor())); - TemplateProfile profile = adapter.prepare(cmd); - return registerPostUploadInternal(adapter, profile); + Account owner = _accountMgr.getAccount(cmd.getEntityOwnerId()); + + try (CheckedReservation templateReservation = new CheckedReservation(owner, ResourceType.template, null, null, 1L, reservationDao, _resourceLimitMgr)) { + TemplateAdapter adapter = getAdapter(HypervisorType.getType(cmd.getHypervisor())); + TemplateProfile profile = adapter.prepare(cmd); + + GetUploadParamsResponse response = registerPostUploadInternal(adapter, profile); + + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); + + return response; + } } @Override @@ -827,9 +880,6 @@ public boolean copy(long userId, VMTemplateVO template, DataStore srcSecStore, D // find the size of the template to be copied TemplateDataStoreVO srcTmpltStore = _tmplStoreDao.findByStoreTemplate(srcSecStore.getId(), tmpltId); - _resourceLimitMgr.checkResourceLimit(account, ResourceType.template); - _resourceLimitMgr.checkResourceLimit(account, ResourceType.secondary_storage, new Long(srcTmpltStore.getSize()).longValue()); - // Event details String copyEventType; if (template.getFormat().equals(ImageFormat.ISO)) { @@ -976,21 +1026,21 @@ public VirtualMachineTemplate copyTemplate(CopyTemplateCmd cmd) throws StorageUn // sync template from cache store to region store if it is not there, for cases where we are going to migrate existing NFS to S3. _tmpltSvr.syncTemplateToRegionStore(template, srcSecStore); } + + AccountVO templateOwner = _accountDao.findById(template.getAccountId()); + for (Long destZoneId : destZoneIds) { DataStore dstSecStore = getImageStore(destZoneId, templateId); if (dstSecStore != null) { logger.debug("There is Template {} in secondary storage {} in zone {} , don't need to copy", template, dstSecStore, dataCenterVOs.get(destZoneId)); continue; } + try (CheckedReservation secondaryStorageReservation = new CheckedReservation(templateOwner, ResourceType.secondary_storage, null, null, template.getSize(), reservationDao, _resourceLimitMgr)) { if (!copy(userId, template, srcSecStore, dataCenterVOs.get(destZoneId))) { failedZones.add(dataCenterVOs.get(destZoneId).getName()); + continue; } - else{ - if (template.getSize() != null) { - // increase resource count - long accountId = template.getAccountId(); - _resourceLimitMgr.incrementResourceCount(accountId, ResourceType.secondary_storage, template.getSize()); - } + _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.secondary_storage, template.getSize()); } } } @@ -1013,9 +1063,6 @@ private boolean addTemplateToZone(VMTemplateVO template, long dstZoneId, long so AccountVO account = _accountDao.findById(template.getAccountId()); - - _resourceLimitMgr.checkResourceLimit(account, ResourceType.template); - try { _tmpltDao.addTemplateToZone(template, dstZoneId); return true; @@ -1940,8 +1987,9 @@ public VMTemplateVO createPrivateTemplateRecord(CreateTemplateCmd cmd, Account t } } - _resourceLimitMgr.checkResourceLimit(templateOwner, ResourceType.template); - _resourceLimitMgr.checkResourceLimit(templateOwner, ResourceType.secondary_storage, new Long(volume != null ? volume.getSize() : snapshot.getSize()).longValue()); + long templateSize = volume != null ? volume.getSize() : snapshot.getSize(); + try (CheckedReservation templateReservation = new CheckedReservation(templateOwner, ResourceType.template, null, null, 1L, reservationDao, _resourceLimitMgr); + CheckedReservation secondaryStorageReservation = new CheckedReservation(templateOwner, ResourceType.secondary_storage, null, null, templateSize, reservationDao, _resourceLimitMgr)) { if (!isAdmin || featured == null) { featured = Boolean.FALSE; @@ -2030,8 +2078,7 @@ public VMTemplateVO createPrivateTemplateRecord(CreateTemplateCmd cmd, Account t } _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.template); - _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.secondary_storage, - new Long(volume != null ? volume.getSize() : snapshot.getSize())); + _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.secondary_storage, templateSize); } if (template != null) { @@ -2040,7 +2087,7 @@ public VMTemplateVO createPrivateTemplateRecord(CreateTemplateCmd cmd, Account t } else { throw new CloudRuntimeException("Failed to create a Template"); } - + } } @Override diff --git a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java index 576930e46f4b..7893d28d9cab 100755 --- a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java +++ b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java @@ -19,26 +19,17 @@ package com.cloud.template; -import com.cloud.agent.AgentManager; -import com.cloud.api.query.dao.UserVmJoinDao; -import com.cloud.configuration.Resource; -import com.cloud.dc.dao.DataCenterDao; -import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao; -import com.cloud.domain.dao.DomainDao; -import com.cloud.event.dao.UsageEventDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.ResourceAllocationException; import com.cloud.host.Status; -import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.HypervisorGuruManager; -import com.cloud.projects.ProjectManager; +import com.cloud.resourcelimit.CheckedReservation; import com.cloud.storage.DataStoreRole; import com.cloud.storage.GuestOSVO; import com.cloud.storage.Snapshot; import com.cloud.storage.SnapshotVO; import com.cloud.storage.Storage; -import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import com.cloud.storage.StoragePoolStatus; import com.cloud.storage.TemplateProfile; @@ -47,13 +38,11 @@ import com.cloud.storage.VMTemplateVO; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.GuestOSDao; -import com.cloud.storage.dao.LaunchPermissionDao; import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VMTemplateDetailsDao; import com.cloud.storage.dao.VMTemplatePoolDao; -import com.cloud.storage.dao.VMTemplateZoneDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; @@ -62,13 +51,11 @@ import com.cloud.user.User; import com.cloud.user.UserData; import com.cloud.user.UserVO; -import com.cloud.user.dao.AccountDao; -import com.cloud.utils.component.ComponentContext; import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; +import junit.framework.TestCase; import org.apache.cloudstack.api.command.user.template.CreateTemplateCmd; import org.apache.cloudstack.api.command.user.template.DeleteTemplateCmd; import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; @@ -77,54 +64,34 @@ import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; -import org.apache.cloudstack.engine.subsystem.api.storage.StorageCacheManager; import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.TemplateService; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.messagebus.MessageBus; -import org.apache.cloudstack.secstorage.dao.SecondaryStorageHeuristicDao; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.cloudstack.secstorage.heuristics.HeuristicType; -import org.apache.cloudstack.snapshot.SnapshotHelper; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.storage.heuristics.HeuristicRuleHelper; import org.apache.cloudstack.storage.template.VnfTemplateManager; -import org.apache.cloudstack.test.utils.SpringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.FilterType; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.filter.TypeFilter; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.support.AnnotationConfigContextLoader; - -import javax.inject.Inject; -import java.io.IOException; + import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -136,79 +103,77 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(loader = AnnotationConfigContextLoader.class) -public class TemplateManagerImplTest { +@RunWith(MockitoJUnitRunner.class) +public class TemplateManagerImplTest extends TestCase { - @Inject - TemplateManagerImpl templateManager = new TemplateManagerImpl(); + @Spy + @InjectMocks + TemplateManagerImpl templateManager; - @Inject + @Mock DataStoreManager dataStoreManager; - @Inject + @Mock VMTemplateDao vmTemplateDao; - @Inject + @Mock VMTemplatePoolDao vmTemplatePoolDao; - @Inject + @Mock TemplateDataStoreDao templateDataStoreDao; - @Inject + @Mock StoragePoolHostDao storagePoolHostDao; - @Inject + @Mock PrimaryDataStoreDao primaryDataStoreDao; - @Inject + @Mock ResourceLimitService resourceLimitMgr; - @Inject + @Mock ImageStoreDao imgStoreDao; - @Inject + @Mock GuestOSDao guestOSDao; - @Inject - VMTemplateDao tmpltDao; - - @Inject + @Mock SnapshotDao snapshotDao; - @Inject + @Mock + VolumeDao volumeDao; + + @Mock VMTemplateDetailsDao tmpltDetailsDao; - @Inject + @Mock StorageStrategyFactory storageStrategyFactory; - @Inject + @Mock VMInstanceDao _vmInstanceDao; - @Inject - private VMTemplateDao _tmpltDao; + @Mock + ReservationDao reservationDao; - @Inject + @Mock HypervisorGuruManager _hvGuruMgr; - @Inject + @Mock AccountManager _accountMgr; - @Inject + + @Mock VnfTemplateManager vnfTemplateManager; - @Inject - TemplateDeployAsIsDetailsDao templateDeployAsIsDetailsDao; - @Inject + @Mock HeuristicRuleHelper heuristicRuleHelperMock; public class CustomThreadPoolExecutor extends ThreadPoolExecutor { @@ -238,7 +203,6 @@ public int getCount() { @Before public void setUp() { - ComponentContext.initComponentsLifeCycle(); AccountVO account = new AccountVO("admin", 1L, "networkDomain", Account.Type.NORMAL, "uuid"); UserVO user = new UserVO(1, "testuser", "password", "firstname", "lastName", "email", "timezone", UUID.randomUUID().toString(), User.Source.UNKNOWN); CallContext.register(user, account); @@ -272,7 +236,7 @@ public void testForceDeleteTemplate() { List adapters = new ArrayList(); adapters.add(templateAdapter); when(cmd.getId()).thenReturn(0L); - when(_tmpltDao.findById(cmd.getId())).thenReturn(template); + when(vmTemplateDao.findById(cmd.getId())).thenReturn(template); when(cmd.getZoneId()).thenReturn(null); when(template.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.None); @@ -293,7 +257,6 @@ public void testForceDeleteTemplate() { //case 2.2: When Force delete flag is 'false' and VM instance VO list is non empty. when(cmd.isForced()).thenReturn(false); VMInstanceVO vmInstanceVO = mock(VMInstanceVO.class); - when(vmInstanceVO.getInstanceName()).thenReturn("mydDummyVM"); vmInstanceVOList.add(vmInstanceVO); when(_vmInstanceDao.listNonExpungedByTemplate(anyLong())).thenReturn(vmInstanceVOList); try { @@ -308,7 +271,6 @@ public void testPrepareTemplateIsSeeded() { when(mockTemplate.getId()).thenReturn(202l); StoragePoolVO mockPool = mock(StoragePoolVO.class); - when(mockPool.getId()).thenReturn(2l); PrimaryDataStore mockPrimaryDataStore = mock(PrimaryDataStore.class); when(mockPrimaryDataStore.getId()).thenReturn(2l); @@ -316,7 +278,6 @@ public void testPrepareTemplateIsSeeded() { VMTemplateStoragePoolVO mockTemplateStore = mock(VMTemplateStoragePoolVO.class); when(mockTemplateStore.getDownloadState()).thenReturn(VMTemplateStorageResourceAssoc.Status.DOWNLOADED); - when(dataStoreManager.getPrimaryDataStore(anyLong())).thenReturn(mockPrimaryDataStore); when(vmTemplateDao.findById(anyLong(), anyBoolean())).thenReturn(mockTemplate); when(vmTemplatePoolDao.findByPoolTemplate(anyLong(), anyLong(), nullable(String.class))).thenReturn(mockTemplateStore); @@ -332,13 +293,11 @@ public void testPrepareTemplateNotDownloaded() { when(mockTemplate.getId()).thenReturn(202l); StoragePoolVO mockPool = mock(StoragePoolVO.class); - when(mockPool.getId()).thenReturn(2l); PrimaryDataStore mockPrimaryDataStore = mock(PrimaryDataStore.class); when(mockPrimaryDataStore.getId()).thenReturn(2l); when(mockPrimaryDataStore.getDataCenterId()).thenReturn(1l); - when(dataStoreManager.getPrimaryDataStore(anyLong())).thenReturn(mockPrimaryDataStore); when(vmTemplateDao.findById(anyLong(), anyBoolean())).thenReturn(mockTemplate); when(vmTemplatePoolDao.findByPoolTemplate(anyLong(), anyLong(), nullable(String.class))).thenReturn(null); when(templateDataStoreDao.findByTemplateZoneDownloadStatus(202l, 1l, VMTemplateStorageResourceAssoc.Status.DOWNLOADED)).thenReturn(null); @@ -353,7 +312,6 @@ public void testPrepareTemplateNoHostConnectedToPool() { when(mockTemplate.getId()).thenReturn(202l); StoragePoolVO mockPool = mock(StoragePoolVO.class); - when(mockPool.getId()).thenReturn(2l); PrimaryDataStore mockPrimaryDataStore = mock(PrimaryDataStore.class); when(mockPrimaryDataStore.getId()).thenReturn(2l); @@ -361,7 +319,6 @@ public void testPrepareTemplateNoHostConnectedToPool() { TemplateDataStoreVO mockTemplateDataStore = mock(TemplateDataStoreVO.class); - when(dataStoreManager.getPrimaryDataStore(anyLong())).thenReturn(mockPrimaryDataStore); when(vmTemplateDao.findById(anyLong(), anyBoolean())).thenReturn(mockTemplate); when(vmTemplatePoolDao.findByPoolTemplate(anyLong(), anyLong(), nullable(String.class))).thenReturn(null); when(templateDataStoreDao.findByTemplateZoneDownloadStatus(202l, 1l, VMTemplateStorageResourceAssoc.Status.DOWNLOADED)).thenReturn(mockTemplateDataStore); @@ -412,20 +369,10 @@ public void testTemplateScheduledForDownloadInDisabledPool() { PrimaryDataStore mockPrimaryDataStore = mock(PrimaryDataStore.class); VMTemplateStoragePoolVO mockTemplateStore = mock(VMTemplateStoragePoolVO.class); - when(mockPrimaryDataStore.getId()).thenReturn(2l); - when(mockPool.getId()).thenReturn(2l); when(mockPool.getStatus()).thenReturn(StoragePoolStatus.Disabled); - when(mockPool.getDataCenterId()).thenReturn(1l); - when(mockTemplate.getId()).thenReturn(202l); - when(mockTemplateStore.getDownloadState()).thenReturn(VMTemplateStorageResourceAssoc.Status.DOWNLOADED); when(vmTemplateDao.findById(anyLong())).thenReturn(mockTemplate); - when(dataStoreManager.getPrimaryDataStore(anyLong())).thenReturn(mockPrimaryDataStore); - when(vmTemplateDao.findById(anyLong(), anyBoolean())).thenReturn(mockTemplate); - when(vmTemplatePoolDao.findByPoolTemplate(anyLong(), anyLong(), nullable(String.class))).thenReturn(mockTemplateStore); when(primaryDataStoreDao.findById(anyLong())).thenReturn(mockPool); - doNothing().when(mockTemplateStore).setMarkedForGC(anyBoolean()); - ExecutorService preloadExecutor = new CustomThreadPoolExecutor(8, 8, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), new NamedThreadFactory("Template-Preloader")); templateManager._preloadExecutor = preloadExecutor; @@ -443,15 +390,10 @@ public void testTemplateScheduledForDownloadInMultiplePool() { StoragePoolVO mockPool1 = mock(StoragePoolVO.class); when(mockPool1.getId()).thenReturn(2l); - when(mockPool1.getStatus()).thenReturn(StoragePoolStatus.Up); when(mockPool1.getDataCenterId()).thenReturn(1l); StoragePoolVO mockPool2 = mock(StoragePoolVO.class); - when(mockPool2.getId()).thenReturn(3l); - when(mockPool2.getStatus()).thenReturn(StoragePoolStatus.Up); when(mockPool2.getDataCenterId()).thenReturn(1l); StoragePoolVO mockPool3 = mock(StoragePoolVO.class); - when(mockPool3.getId()).thenReturn(4l); - when(mockPool3.getStatus()).thenReturn(StoragePoolStatus.Up); when(mockPool3.getDataCenterId()).thenReturn(2l); pools.add(mockPool1); pools.add(mockPool2); @@ -464,9 +406,6 @@ public void testTemplateScheduledForDownloadInMultiplePool() { when(dataStoreManager.getPrimaryDataStore(anyLong())).thenReturn(mockPrimaryDataStore); when(vmTemplateDao.findById(anyLong(), anyBoolean())).thenReturn(mockTemplate); when(vmTemplatePoolDao.findByPoolTemplate(anyLong(), anyLong(), nullable(String.class))).thenReturn(mockTemplateStore); - when(primaryDataStoreDao.findById(2l)).thenReturn(mockPool1); - when(primaryDataStoreDao.findById(3l)).thenReturn(mockPool2); - when(primaryDataStoreDao.findById(4l)).thenReturn(mockPool3); when(primaryDataStoreDao.listByStatus(StoragePoolStatus.Up)).thenReturn(pools); doNothing().when(mockTemplateStore).setMarkedForGC(anyBoolean()); @@ -494,7 +433,6 @@ public void testCreatePrivateTemplateRecordForRegionStore() throws ResourceAlloc when(mockCreateCmd.getVolumeId()).thenReturn(null); when(mockCreateCmd.getSnapshotId()).thenReturn(1L); when(mockCreateCmd.getOsTypeId()).thenReturn(1L); - when(mockCreateCmd.getEventDescription()).thenReturn("test"); when(mockCreateCmd.getDetails()).thenReturn(null); when(mockCreateCmd.getZoneId()).thenReturn(null); @@ -507,20 +445,17 @@ public void testCreatePrivateTemplateRecordForRegionStore() throws ResourceAlloc when(mockSnapshot.getState()).thenReturn(Snapshot.State.BackedUp); when(mockSnapshot.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.XenServer); - doNothing().when(resourceLimitMgr).checkResourceLimit(any(Account.class), eq(Resource.ResourceType.template)); - doNothing().when(resourceLimitMgr).checkResourceLimit(any(Account.class), eq(Resource.ResourceType.secondary_storage), anyLong()); - GuestOSVO mockGuestOS = mock(GuestOSVO.class); when(guestOSDao.findById(anyLong())).thenReturn(mockGuestOS); - when(tmpltDao.getNextInSequence(eq(Long.class), eq("id"))).thenReturn(1L); + when(vmTemplateDao.getNextInSequence(eq(Long.class), eq("id"))).thenReturn(1L); List mockRegionStores = new ArrayList<>(); ImageStoreVO mockRegionStore = mock(ImageStoreVO.class); mockRegionStores.add(mockRegionStore); when(imgStoreDao.findRegionImageStores()).thenReturn(mockRegionStores); - when(tmpltDao.persist(any(VMTemplateVO.class))).thenAnswer(new Answer() { + when(vmTemplateDao.persist(any(VMTemplateVO.class))).thenAnswer(new Answer() { @Override public VMTemplateVO answer(InvocationOnMock invocationOnMock) throws Throwable { Object[] args = invocationOnMock.getArguments(); @@ -528,8 +463,10 @@ public VMTemplateVO answer(InvocationOnMock invocationOnMock) throws Throwable { } }); - VMTemplateVO template = templateManager.createPrivateTemplateRecord(mockCreateCmd, mockTemplateOwner); - assertTrue("Template in a region store should have cross zones set", template.isCrossZones()); + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { + VMTemplateVO template = templateManager.createPrivateTemplateRecord(mockCreateCmd, mockTemplateOwner); + assertTrue("Template in a region store should have cross zones set", template.isCrossZones()); + } } @Test @@ -541,7 +478,7 @@ public void testLinkUserDataToTemplate() { when(cmd.getUserdataPolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); VMTemplateVO template = Mockito.mock(VMTemplateVO.class); - when(_tmpltDao.findById(anyLong())).thenReturn(template); + when(vmTemplateDao.findById(anyLong())).thenReturn(template); VirtualMachineTemplate resultTemplate = templateManager.linkUserDataToTemplate(cmd); @@ -557,7 +494,6 @@ public void testLinkUserDataToTemplateByProvidingBothISOAndTemplateId() { when(cmd.getUserdataPolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); VMTemplateVO template = Mockito.mock(VMTemplateVO.class); - when(_tmpltDao.findById(1L)).thenReturn(template); templateManager.linkUserDataToTemplate(cmd); } @@ -571,7 +507,6 @@ public void testLinkUserDataToTemplateByNotProvidingBothISOAndTemplateId() { when(cmd.getUserdataPolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); VMTemplateVO template = Mockito.mock(VMTemplateVO.class); - when(_tmpltDao.findById(1L)).thenReturn(template); templateManager.linkUserDataToTemplate(cmd); } @@ -584,7 +519,7 @@ public void testLinkUserDataToTemplateWhenNoTemplate() { when(cmd.getUserdataId()).thenReturn(2L); when(cmd.getUserdataPolicy()).thenReturn(UserData.UserDataOverridePolicy.ALLOWOVERRIDE); - when(_tmpltDao.findById(anyLong())).thenReturn(null); + when(vmTemplateDao.findById(anyLong())).thenReturn(null); templateManager.linkUserDataToTemplate(cmd); } @@ -599,7 +534,7 @@ public void testUnLinkUserDataToTemplate() { VMTemplateVO template = Mockito.mock(VMTemplateVO.class); when(template.getId()).thenReturn(1L); - when(_tmpltDao.findById(1L)).thenReturn(template); + when(vmTemplateDao.findById(1L)).thenReturn(template); VirtualMachineTemplate resultTemplate = templateManager.linkUserDataToTemplate(cmd); @@ -630,7 +565,6 @@ public void getImageStoreTestStoreUuidIsNullAndThereIsNoActiveHeuristicRulesShou DataStore dataStore = Mockito.mock(DataStore.class); VolumeVO volumeVO = Mockito.mock(VolumeVO.class); - Mockito.when(dataStoreManager.getDataStore(Mockito.anyString(), Mockito.any(DataStoreRole.class))).thenReturn(null); Mockito.when(heuristicRuleHelperMock.getImageStoreIfThereIsHeuristicRule(Mockito.anyLong(), Mockito.any(HeuristicType.class), Mockito.any(VolumeVO.class))).thenReturn(null); Mockito.when(dataStoreManager.getImageStoreWithFreeCapacity(Mockito.anyLong())).thenReturn(dataStore); @@ -643,7 +577,6 @@ public void getImageStoreTestStoreUuidIsNullAndThereIsActiveHeuristicRulesShould DataStore dataStore = Mockito.mock(DataStore.class); VolumeVO volumeVO = Mockito.mock(VolumeVO.class); - Mockito.when(dataStoreManager.getDataStore(Mockito.anyString(), Mockito.any(DataStoreRole.class))).thenReturn(null); Mockito.when(heuristicRuleHelperMock.getImageStoreIfThereIsHeuristicRule(Mockito.anyLong(), Mockito.any(HeuristicType.class), Mockito.any(VolumeVO.class))).thenReturn(dataStore); templateManager.getImageStore(null, 1L, volumeVO); @@ -773,243 +706,4 @@ public void verifyHeuristicRulesForZoneTestTemplateNotISOFormatShouldCheckForTem Mockito.verify(heuristicRuleHelperMock, Mockito.times(1)).getImageStoreIfThereIsHeuristicRule(1L, HeuristicType.TEMPLATE, vmTemplateVOMock); } - @Configuration - @ComponentScan(basePackageClasses = {TemplateManagerImpl.class}, - includeFilters = {@ComponentScan.Filter(value = TestConfiguration.Library.class, type = FilterType.CUSTOM)}, - useDefaultFilters = false) - public static class TestConfiguration extends SpringUtils.CloudStackTestConfiguration { - - @Bean - public DataStoreManager dataStoreManager() { - return Mockito.mock(DataStoreManager.class); - } - - @Bean - public VMTemplateDao vmTemplateDao() { - return Mockito.mock(VMTemplateDao.class); - } - - @Bean - public StorageStrategyFactory storageStrategyFactory() { - return Mockito.mock(StorageStrategyFactory.class); - } - - @Bean - public VMTemplatePoolDao vmTemplatePoolDao() { - return Mockito.mock(VMTemplatePoolDao.class); - } - - @Bean - public TemplateDataStoreDao templateDataStoreDao() { - return Mockito.mock(TemplateDataStoreDao.class); - } - - @Bean - public VMTemplateZoneDao vmTemplateZoneDao() { - return Mockito.mock(VMTemplateZoneDao.class); - } - - @Bean - public VMInstanceDao vmInstanceDao() { - return Mockito.mock(VMInstanceDao.class); - } - - @Bean - public PrimaryDataStoreDao primaryDataStoreDao() { - return Mockito.mock(PrimaryDataStoreDao.class); - } - - @Bean - public StoragePoolHostDao storagePoolHostDao() { - return Mockito.mock(StoragePoolHostDao.class); - } - - @Bean - public AccountDao accountDao() { - return Mockito.mock(AccountDao.class); - } - - @Bean - public AgentManager agentMgr() { - return Mockito.mock(AgentManager.class); - } - - @Bean - public AccountManager accountManager() { - return Mockito.mock(AccountManager.class); - } - - @Bean - public HostDao hostDao() { - return Mockito.mock(HostDao.class); - } - - @Bean - public DataCenterDao dcDao() { - return Mockito.mock(DataCenterDao.class); - } - - @Bean - public UserVmDao userVmDao() { - return Mockito.mock(UserVmDao.class); - } - - @Bean - public VolumeDao volumeDao() { - return Mockito.mock(VolumeDao.class); - } - - @Bean - public SnapshotDao snapshotDao() { - return Mockito.mock(SnapshotDao.class); - } - - @Bean - public ConfigurationDao configDao() { - return Mockito.mock(ConfigurationDao.class); - } - - @Bean - public DomainDao domainDao() { - return Mockito.mock(DomainDao.class); - } - - @Bean - public GuestOSDao guestOSDao() { - return Mockito.mock(GuestOSDao.class); - } - - @Bean - public StorageManager storageManager() { - return Mockito.mock(StorageManager.class); - } - - @Bean - public UsageEventDao usageEventDao() { - return Mockito.mock(UsageEventDao.class); - } - - @Bean - public ResourceLimitService resourceLimitMgr() { - return Mockito.mock(ResourceLimitService.class); - } - - @Bean - public LaunchPermissionDao launchPermissionDao() { - return Mockito.mock(LaunchPermissionDao.class); - } - - @Bean - public ProjectManager projectMgr() { - return Mockito.mock(ProjectManager.class); - } - - @Bean - public VolumeDataFactory volFactory() { - return Mockito.mock(VolumeDataFactory.class); - } - - @Bean - public TemplateDataFactory tmplFactory() { - return Mockito.mock(TemplateDataFactory.class); - } - - @Bean - public SnapshotDataFactory snapshotFactory() { - return Mockito.mock(SnapshotDataFactory.class); - } - - @Bean - public TemplateService tmpltSvr() { - return Mockito.mock(TemplateService.class); - } - - @Bean - public VolumeOrchestrationService volumeMgr() { - return Mockito.mock(VolumeOrchestrationService.class); - } - - @Bean - public EndPointSelector epSelector() { - return Mockito.mock(EndPointSelector.class); - } - - @Bean - public UserVmJoinDao userVmJoinDao() { - return Mockito.mock(UserVmJoinDao.class); - } - - @Bean - public SnapshotDataStoreDao snapshotStoreDao() { - return Mockito.mock(SnapshotDataStoreDao.class); - } - - @Bean - public ImageStoreDao imageStoreDao() { - return Mockito.mock(ImageStoreDao.class); - } - - @Bean - public MessageBus messageBus() { - return Mockito.mock(MessageBus.class); - } - - @Bean - public StorageCacheManager cacheMgr() { - return Mockito.mock(StorageCacheManager.class); - } - - @Bean - public TemplateAdapter templateAdapter() { - return Mockito.mock(TemplateAdapter.class); - } - - @Bean - public VMTemplateDetailsDao vmTemplateDetailsDao() { - return Mockito.mock(VMTemplateDetailsDao.class); - } - - @Bean - public HypervisorGuruManager hypervisorGuruManager() { - return Mockito.mock(HypervisorGuruManager.class); - } - - @Bean - public VnfTemplateManager vnfTemplateManager() { - return Mockito.mock(VnfTemplateManager.class); - } - - @Bean - public TemplateDeployAsIsDetailsDao templateDeployAsIsDetailsDao() { - return Mockito.mock(TemplateDeployAsIsDetailsDao.class); - } - - @Bean - public SnapshotHelper snapshotHelper() { - return Mockito.mock(SnapshotHelper.class); - } - - @Bean - public SnapshotService snapshotService() { - return Mockito.mock(SnapshotService.class); - } - - @Bean - public SecondaryStorageHeuristicDao secondaryStorageHeuristicDao() { - return Mockito.mock(SecondaryStorageHeuristicDao.class); - } - - @Bean - public HeuristicRuleHelper heuristicRuleHelper() { - return Mockito.mock(HeuristicRuleHelper.class); - } - - public static class Library implements TypeFilter { - @Override - public boolean match(MetadataReader mdr, MetadataReaderFactory arg1) throws IOException { - ComponentScan cs = TestConfiguration.class.getAnnotation(ComponentScan.class); - return SpringUtils.includedInBasePackageClasses(mdr.getClassMetadata().getClassName(), cs); - } - } - } } From 822696c4181bac27e155b1c61530f7fd6bd53ba2 Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:54:08 +0530 Subject: [PATCH 010/205] Fix resource limit reservation and check during StartVirtualMachine --- .../java/com/cloud/vm/UserVmManagerImpl.java | 270 +++++++++--------- 1 file changed, 140 insertions(+), 130 deletions(-) diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 0e1c3bb91945..7fcf242ea20f 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -5493,13 +5493,135 @@ public Pair> startVirtualMach return startVirtualMachine(vmId, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, true); } + private Pair> startVirtualMachineUnchecked(UserVmVO vm, VMTemplateVO template, Long podId, + Long clusterId, Long hostId, @NotNull Map additionalParams, String deploymentPlannerToUse, + boolean isExplicitHost, boolean isRootAdmin) throws ResourceUnavailableException, InsufficientCapacityException { + + // check if vm is security group enabled + if (_securityGroupMgr.isVmSecurityGroupEnabled(vm.getId()) && _securityGroupMgr.getSecurityGroupsForVm(vm.getId()).isEmpty() + && !_securityGroupMgr.isVmMappedToDefaultSecurityGroup(vm.getId()) && _networkModel.canAddDefaultSecurityGroup()) { + // if vm is not mapped to security group, create a mapping + if (logger.isDebugEnabled()) { + logger.debug("Vm " + vm + " is security group enabled, but not mapped to default security group; creating the mapping automatically"); + } + + SecurityGroup defaultSecurityGroup = _securityGroupMgr.getDefaultSecurityGroup(vm.getAccountId()); + if (defaultSecurityGroup != null) { + List groupList = new ArrayList<>(); + groupList.add(defaultSecurityGroup.getId()); + _securityGroupMgr.addInstanceToGroups(vm, groupList); + } + } + + // Choose deployment planner + // Host takes 1st preference, Cluster takes 2nd preference and Pod takes 3rd + // Default behaviour is invoked when host, cluster or pod are not specified + Pod destinationPod = getDestinationPod(podId, isRootAdmin); + Cluster destinationCluster = getDestinationCluster(clusterId, isRootAdmin); + HostVO destinationHost = getDestinationHost(hostId, isRootAdmin, isExplicitHost); + DataCenterDeployment plan = null; + boolean deployOnGivenHost = false; + if (destinationHost != null) { + logger.debug("Destination Host to deploy the VM is specified, specifying a deployment plan to deploy the VM"); + _hostDao.loadHostTags(destinationHost); + validateStrictHostTagCheck(vm, destinationHost); + + final ServiceOfferingVO offering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); + Pair cpuCapabilityAndCapacity = _capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(destinationHost, offering, false); + if (!cpuCapabilityAndCapacity.first() || !cpuCapabilityAndCapacity.second()) { + String errorMsg; + if (!cpuCapabilityAndCapacity.first()) { + errorMsg = String.format("Cannot deploy the VM to specified host %s, requested CPU and speed is more than the host capability", destinationHost); + } else { + errorMsg = String.format("Cannot deploy the VM to specified host %s, host does not have enough free CPU or RAM, please check the logs", destinationHost); + } + logger.info(errorMsg); + if (!AllowDeployVmIfGivenHostFails.value()) { + throw new InvalidParameterValueException(errorMsg); + } + } else { + plan = new DataCenterDeployment(vm.getDataCenterId(), destinationHost.getPodId(), destinationHost.getClusterId(), destinationHost.getId(), null, null); + if (!AllowDeployVmIfGivenHostFails.value()) { + deployOnGivenHost = true; + } + } + } else if (destinationCluster != null) { + logger.debug("Destination Cluster to deploy the VM is specified, specifying a deployment plan to deploy the VM"); + plan = new DataCenterDeployment(vm.getDataCenterId(), destinationCluster.getPodId(), destinationCluster.getId(), null, null, null); + if (!AllowDeployVmIfGivenHostFails.value()) { + deployOnGivenHost = true; + } + } else if (destinationPod != null) { + logger.debug("Destination Pod to deploy the VM is specified, specifying a deployment plan to deploy the VM"); + plan = new DataCenterDeployment(vm.getDataCenterId(), destinationPod.getId(), null, null, null, null); + if (!AllowDeployVmIfGivenHostFails.value()) { + deployOnGivenHost = true; + } + } + + // Set parameters + Map params = null; + if (vm.isUpdateParameters()) { + _vmDao.loadDetails(vm); + String password = getCurrentVmPasswordOrDefineNewPassword(String.valueOf(additionalParams.getOrDefault(VirtualMachineProfile.Param.VmPassword, "")), vm, template); + if (!validPassword(password)) { + throw new InvalidParameterValueException("A valid password for this virtual machine was not provided."); + } + // Check if an SSH key pair was selected for the instance and if so + // use it to encrypt & save the vm password + encryptAndStorePassword(vm, password); + params = createParameterInParameterMap(params, additionalParams, VirtualMachineProfile.Param.VmPassword, password); + } + + if (additionalParams.containsKey(VirtualMachineProfile.Param.BootIntoSetup)) { + if (!HypervisorType.VMware.equals(vm.getHypervisorType())) { + throw new InvalidParameterValueException(ApiConstants.BOOT_INTO_SETUP + " makes no sense for " + vm.getHypervisorType()); + } + Object paramValue = additionalParams.get(VirtualMachineProfile.Param.BootIntoSetup); + if (logger.isTraceEnabled()) { + logger.trace("It was specified whether to enter setup mode: " + paramValue.toString()); + } + params = createParameterInParameterMap(params, additionalParams, VirtualMachineProfile.Param.BootIntoSetup, paramValue); + } + + VirtualMachineEntity vmEntity = _orchSrvc.getVirtualMachine(vm.getUuid()); + + DeploymentPlanner planner = null; + if (deploymentPlannerToUse != null) { + // if set to null, the deployment planner would be later figured out either from global config var, or from + // the service offering + planner = _planningMgr.getDeploymentPlannerByName(deploymentPlannerToUse); + if (planner == null) { + throw new InvalidParameterValueException("Can't find a planner by name " + deploymentPlannerToUse); + } + } + vmEntity.setParamsToEntity(additionalParams); + + UserVO callerUser = _userDao.findById(CallContext.current().getCallingUserId()); + String reservationId = vmEntity.reserve(planner, plan, new ExcludeList(), Long.toString(callerUser.getId())); + vmEntity.deploy(reservationId, Long.toString(callerUser.getId()), params, deployOnGivenHost); + + Pair> vmParamPair = new Pair(vm, params); + if (vm.isUpdateParameters()) { + // this value is not being sent to the backend; need only for api + // display purposes + if (template.isEnablePassword()) { + if (vm.getDetail(VmDetailConstants.PASSWORD) != null) { + userVmDetailsDao.removeDetail(vm.getId(), VmDetailConstants.PASSWORD); + } + vm.setUpdateParameters(false); + _vmDao.update(vm.getId(), vm); + } + } + return vmParamPair; + } + @Override public Pair> startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, @NotNull Map additionalParams, String deploymentPlannerToUse, boolean isExplicitHost) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { // Input validation final Account callerAccount = CallContext.current().getCallingAccount(); - UserVO callerUser = _userDao.findById(CallContext.current().getCallingUserId()); // if account is removed, return error if (callerAccount == null || callerAccount.getRemoved() != null) { @@ -5527,138 +5649,26 @@ public Pair> startVirtualMach if (owner.getState() == Account.State.DISABLED) { throw new PermissionDeniedException(String.format("The owner of %s is disabled: %s", vm, owner)); } - Pair> vmParamPair; - try (CheckedReservation vmReservation = new CheckedReservation(owner, ResourceType.user_vm, vm.getId(), null, 1L, reservationDao, _resourceLimitMgr)) { - VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); - if (VirtualMachineManager.ResourceCountRunningVMsonly.value()) { - // check if account/domain is with in resource limits to start a new vm - ServiceOfferingVO offering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); - resourceLimitService.checkVmResourceLimit(owner, vm.isDisplayVm(), offering, template); - } - // check if vm is security group enabled - if (_securityGroupMgr.isVmSecurityGroupEnabled(vmId) && _securityGroupMgr.getSecurityGroupsForVm(vmId).isEmpty() - && !_securityGroupMgr.isVmMappedToDefaultSecurityGroup(vmId) && _networkModel.canAddDefaultSecurityGroup()) { - // if vm is not mapped to security group, create a mapping - if (logger.isDebugEnabled()) { - logger.debug("Vm " + vm + " is security group enabled, but not mapped to default security group; creating the mapping automatically"); - } - - SecurityGroup defaultSecurityGroup = _securityGroupMgr.getDefaultSecurityGroup(vm.getAccountId()); - if (defaultSecurityGroup != null) { - List groupList = new ArrayList<>(); - groupList.add(defaultSecurityGroup.getId()); - _securityGroupMgr.addInstanceToGroups(vm, groupList); - } - } - // Choose deployment planner - // Host takes 1st preference, Cluster takes 2nd preference and Pod takes 3rd - // Default behaviour is invoked when host, cluster or pod are not specified - boolean isRootAdmin = _accountService.isRootAdmin(callerAccount.getId()); - Pod destinationPod = getDestinationPod(podId, isRootAdmin); - Cluster destinationCluster = getDestinationCluster(clusterId, isRootAdmin); - HostVO destinationHost = getDestinationHost(hostId, isRootAdmin, isExplicitHost); - DataCenterDeployment plan = null; - boolean deployOnGivenHost = false; - if (destinationHost != null) { - logger.debug("Destination Host to deploy the VM is specified, specifying a deployment plan to deploy the VM"); - _hostDao.loadHostTags(destinationHost); - validateStrictHostTagCheck(vm, destinationHost); - - final ServiceOfferingVO offering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); - Pair cpuCapabilityAndCapacity = _capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(destinationHost, offering, false); - if (!cpuCapabilityAndCapacity.first() || !cpuCapabilityAndCapacity.second()) { - String errorMsg; - if (!cpuCapabilityAndCapacity.first()) { - errorMsg = String.format("Cannot deploy the VM to specified host %s, requested CPU and speed is more than the host capability", destinationHost); - } else { - errorMsg = String.format("Cannot deploy the VM to specified host %s, host does not have enough free CPU or RAM, please check the logs", destinationHost); - } - logger.info(errorMsg); - if (!AllowDeployVmIfGivenHostFails.value()) { - throw new InvalidParameterValueException(errorMsg); - } - } else { - plan = new DataCenterDeployment(vm.getDataCenterId(), destinationHost.getPodId(), destinationHost.getClusterId(), destinationHost.getId(), null, null); - if (!AllowDeployVmIfGivenHostFails.value()) { - deployOnGivenHost = true; - } - } - } else if (destinationCluster != null) { - logger.debug("Destination Cluster to deploy the VM is specified, specifying a deployment plan to deploy the VM"); - plan = new DataCenterDeployment(vm.getDataCenterId(), destinationCluster.getPodId(), destinationCluster.getId(), null, null, null); - if (!AllowDeployVmIfGivenHostFails.value()) { - deployOnGivenHost = true; - } - } else if (destinationPod != null) { - logger.debug("Destination Pod to deploy the VM is specified, specifying a deployment plan to deploy the VM"); - plan = new DataCenterDeployment(vm.getDataCenterId(), destinationPod.getId(), null, null, null, null); - if (!AllowDeployVmIfGivenHostFails.value()) { - deployOnGivenHost = true; - } - } - - // Set parameters - Map params = null; - if (vm.isUpdateParameters()) { - _vmDao.loadDetails(vm); - - String password = getCurrentVmPasswordOrDefineNewPassword(String.valueOf(additionalParams.getOrDefault(VirtualMachineProfile.Param.VmPassword, "")), vm, template); - - if (!validPassword(password)) { - throw new InvalidParameterValueException("A valid password for this virtual machine was not provided."); - } - - // Check if an SSH key pair was selected for the instance and if so - // use it to encrypt & save the vm password - encryptAndStorePassword(vm, password); - - params = createParameterInParameterMap(params, additionalParams, VirtualMachineProfile.Param.VmPassword, password); - } - - if (additionalParams.containsKey(VirtualMachineProfile.Param.BootIntoSetup)) { - if (!HypervisorType.VMware.equals(vm.getHypervisorType())) { - throw new InvalidParameterValueException(ApiConstants.BOOT_INTO_SETUP + " makes no sense for " + vm.getHypervisorType()); - } - Object paramValue = additionalParams.get(VirtualMachineProfile.Param.BootIntoSetup); - if (logger.isTraceEnabled()) { - logger.trace("It was specified whether to enter setup mode: " + paramValue.toString()); - } - params = createParameterInParameterMap(params, additionalParams, VirtualMachineProfile.Param.BootIntoSetup, paramValue); - } - - VirtualMachineEntity vmEntity = _orchSrvc.getVirtualMachine(vm.getUuid()); - - DeploymentPlanner planner = null; - if (deploymentPlannerToUse != null) { - // if set to null, the deployment planner would be later figured out either from global config var, or from - // the service offering - planner = _planningMgr.getDeploymentPlannerByName(deploymentPlannerToUse); - if (planner == null) { - throw new InvalidParameterValueException("Can't find a planner by name " + deploymentPlannerToUse); - } - } - vmEntity.setParamsToEntity(additionalParams); - - String reservationId = vmEntity.reserve(planner, plan, new ExcludeList(), Long.toString(callerUser.getId())); - vmEntity.deploy(reservationId, Long.toString(callerUser.getId()), params, deployOnGivenHost); + boolean isRootAdmin = _accountService.isRootAdmin(callerAccount.getId()); - vmParamPair = new Pair(vm, params); - if (vm != null && vm.isUpdateParameters()) { - // this value is not being sent to the backend; need only for api - // display purposes - if (template.isEnablePassword()) { - if (vm.getDetail(VmDetailConstants.PASSWORD) != null) { - userVmDetailsDao.removeDetail(vm.getId(), VmDetailConstants.PASSWORD); - } - vm.setUpdateParameters(false); - _vmDao.update(vm.getId(), vm); - } + VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); + if (VirtualMachineManager.ResourceCountRunningVMsonly.value()) { + ServiceOfferingVO offering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); + List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(offering, template); + try (CheckedReservation vmReservation = new CheckedReservation(owner, ResourceType.user_vm, resourceLimitHostTags, 1l, reservationDao, resourceLimitService); + CheckedReservation cpuReservation = new CheckedReservation(owner, ResourceType.cpu, resourceLimitHostTags, Long.valueOf(offering.getCpu()), reservationDao, resourceLimitService); + CheckedReservation memReservation = new CheckedReservation(owner, ResourceType.memory, resourceLimitHostTags, Long.valueOf(offering.getRamSize()), reservationDao, resourceLimitService); + ) { + return startVirtualMachineUnchecked(vm, template, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, isExplicitHost, isRootAdmin); + } catch (ResourceAllocationException | CloudRuntimeException e) { + throw e; + } catch (Exception e) { + logger.error("Failed to start VM {} : error during resource reservation and allocation", e); + throw new CloudRuntimeException(e); } - } catch (Exception e) { - logger.error("Failed to start VM {}", vm, e); - throw new CloudRuntimeException("Failed to start VM " + vm, e); + } else { + return startVirtualMachineUnchecked(vm, template, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, isExplicitHost, isRootAdmin); } - return vmParamPair; } /** From ca7b08accebfaefce323c3c3810a144336256a99 Mon Sep 17 00:00:00 2001 From: abh1sar Date: Mon, 2 Mar 2026 11:06:13 +0530 Subject: [PATCH 011/205] secondary storage resource limit for download --- .../VMTemplateStorageResourceAssoc.java | 3 +- .../agent/api/storage/DownloadAnswer.java | 2 +- .../image/BaseImageStoreDriverImpl.java | 9 +- .../ResourceLimitManagerImpl.java | 4 +- .../storage/download/DownloadActiveState.java | 5 ++ .../storage/download/DownloadErrorState.java | 5 ++ .../download/DownloadInactiveState.java | 6 ++ .../download/DownloadLimitReachedState.java | 54 +++++++++++ .../storage/download/DownloadListener.java | 89 +++++++++++++++++-- .../cloud/storage/download/DownloadState.java | 6 +- .../template/HypervisorTemplateAdapter.java | 9 +- .../storage/template/DownloadManagerImpl.java | 2 +- 12 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 server/src/main/java/com/cloud/storage/download/DownloadLimitReachedState.java diff --git a/api/src/main/java/com/cloud/storage/VMTemplateStorageResourceAssoc.java b/api/src/main/java/com/cloud/storage/VMTemplateStorageResourceAssoc.java index db702a61f2bc..7d5b2d7c57d7 100644 --- a/api/src/main/java/com/cloud/storage/VMTemplateStorageResourceAssoc.java +++ b/api/src/main/java/com/cloud/storage/VMTemplateStorageResourceAssoc.java @@ -23,9 +23,10 @@ public interface VMTemplateStorageResourceAssoc extends InternalIdentity { public static enum Status { - UNKNOWN, DOWNLOAD_ERROR, NOT_DOWNLOADED, DOWNLOAD_IN_PROGRESS, DOWNLOADED, ABANDONED, UPLOADED, NOT_UPLOADED, UPLOAD_ERROR, UPLOAD_IN_PROGRESS, CREATING, CREATED, BYPASSED + UNKNOWN, DOWNLOAD_ERROR, NOT_DOWNLOADED, DOWNLOAD_IN_PROGRESS, DOWNLOADED, ABANDONED, LIMIT_REACHED, UPLOADED, NOT_UPLOADED, UPLOAD_ERROR, UPLOAD_IN_PROGRESS, CREATING, CREATED, BYPASSED } + List ERROR_DOWNLOAD_STATES = List.of(Status.DOWNLOAD_ERROR, Status.ABANDONED, Status.LIMIT_REACHED, Status.UNKNOWN); List PENDING_DOWNLOAD_STATES = List.of(Status.NOT_DOWNLOADED, Status.DOWNLOAD_IN_PROGRESS); String getInstallPath(); diff --git a/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java index 0c6373134b18..1c5eb7b9a9af 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java @@ -140,7 +140,7 @@ public void setTemplateSize(long templateSize) { } public Long getTemplateSize() { - return templateSize; + return templateSize == 0 ? templatePhySicalSize : templateSize; } public void setTemplatePhySicalSize(long templatePhySicalSize) { diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java index a2e9eff2a08a..61b1a84cdc6a 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java @@ -230,8 +230,10 @@ protected Void createTemplateAsyncCallback(AsyncCallbackDispatcher caller = context.getParentCallback(); - if (answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR || - answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.ABANDONED || answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.UNKNOWN) { + if (VMTemplateStorageResourceAssoc.ERROR_DOWNLOAD_STATES.contains(answer.getDownloadStatus())) { CreateCmdResult result = new CreateCmdResult(null, null); result.setSuccess(false); result.setResult(answer.getErrorString()); diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 09a0dda3aaa2..43c3b3832584 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -258,7 +258,7 @@ public boolean configure(final String name, final Map params) th templateSizeSearch = _vmTemplateStoreDao.createSearchBuilder(SumCount.class); templateSizeSearch.select("sum", Func.SUM, templateSizeSearch.entity().getSize()); - templateSizeSearch.and("downloadState", templateSizeSearch.entity().getDownloadState(), Op.EQ); + templateSizeSearch.and("downloadState", templateSizeSearch.entity().getDownloadState(), Op.IN); templateSizeSearch.and("destroyed", templateSizeSearch.entity().getDestroyed(), Op.EQ); SearchBuilder join1 = _vmTemplateDao.createSearchBuilder(); join1.and("accountId", join1.entity().getAccountId(), Op.EQ); @@ -1410,7 +1410,7 @@ public long calculateSecondaryStorageForAccount(long accountId) { long totalTemplatesSize = 0; SearchCriteria sc = templateSizeSearch.create(); - sc.setParameters("downloadState", Status.DOWNLOADED); + sc.setParameters("downloadState", Status.DOWNLOADED, Status.DOWNLOAD_IN_PROGRESS); sc.setParameters("destroyed", false); sc.setJoinParameters("templates", "accountId", accountId); List templates = _vmTemplateStoreDao.customSearch(sc, null); diff --git a/server/src/main/java/com/cloud/storage/download/DownloadActiveState.java b/server/src/main/java/com/cloud/storage/download/DownloadActiveState.java index 889ffbc0b1ce..35b23985dd20 100644 --- a/server/src/main/java/com/cloud/storage/download/DownloadActiveState.java +++ b/server/src/main/java/com/cloud/storage/download/DownloadActiveState.java @@ -95,6 +95,11 @@ public String handleAbort() { return Status.ABANDONED.toString(); } + @Override + public String handleLimitReached() { + return Status.LIMIT_REACHED.toString(); + } + @Override public String handleDisconnect() { diff --git a/server/src/main/java/com/cloud/storage/download/DownloadErrorState.java b/server/src/main/java/com/cloud/storage/download/DownloadErrorState.java index a0834456a2d0..5dddefb01924 100644 --- a/server/src/main/java/com/cloud/storage/download/DownloadErrorState.java +++ b/server/src/main/java/com/cloud/storage/download/DownloadErrorState.java @@ -60,6 +60,11 @@ public String handleAbort() { return Status.ABANDONED.toString(); } + @Override + public String handleLimitReached() { + return Status.LIMIT_REACHED.toString(); + } + @Override public String getName() { return Status.DOWNLOAD_ERROR.toString(); diff --git a/server/src/main/java/com/cloud/storage/download/DownloadInactiveState.java b/server/src/main/java/com/cloud/storage/download/DownloadInactiveState.java index 8fee3d0437c1..69d46879ebeb 100644 --- a/server/src/main/java/com/cloud/storage/download/DownloadInactiveState.java +++ b/server/src/main/java/com/cloud/storage/download/DownloadInactiveState.java @@ -36,6 +36,12 @@ public String handleAbort() { return getName(); } + @Override + public String handleLimitReached() { + // ignore and stay put + return getName(); + } + @Override public String handleDisconnect() { //ignore and stay put diff --git a/server/src/main/java/com/cloud/storage/download/DownloadLimitReachedState.java b/server/src/main/java/com/cloud/storage/download/DownloadLimitReachedState.java new file mode 100644 index 000000000000..8ce5668299e6 --- /dev/null +++ b/server/src/main/java/com/cloud/storage/download/DownloadLimitReachedState.java @@ -0,0 +1,54 @@ +// 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 com.cloud.storage.download; + +import org.apache.cloudstack.storage.command.DownloadProgressCommand.RequestType; +import org.apache.logging.log4j.Level; + +import com.cloud.agent.api.storage.DownloadAnswer; +import com.cloud.storage.VMTemplateStorageResourceAssoc.Status; + +public class DownloadLimitReachedState extends DownloadInactiveState { + + public DownloadLimitReachedState(DownloadListener dl) { + super(dl); + } + + @Override + public String getName() { + return Status.LIMIT_REACHED.toString(); + } + + @Override + public String handleEvent(DownloadEvent event, Object eventObj) { + if (logger.isTraceEnabled()) { + getDownloadListener().log("handleEvent, event type=" + event + ", curr state=" + getName(), Level.TRACE); + } + return Status.LIMIT_REACHED.toString(); + } + + @Override + public void onEntry(String prevState, DownloadEvent event, Object evtObj) { + if (!prevState.equalsIgnoreCase(getName())) { + DownloadAnswer answer = new DownloadAnswer("Storage Limit Reached", Status.LIMIT_REACHED); + getDownloadListener().callback(answer); + getDownloadListener().cancelStatusTask(); + getDownloadListener().cancelTimeoutTask(); + getDownloadListener().scheduleImmediateStatusCheck(RequestType.PURGE); + } + } +} diff --git a/server/src/main/java/com/cloud/storage/download/DownloadListener.java b/server/src/main/java/com/cloud/storage/download/DownloadListener.java index 42b0e394db4c..058881fdb54a 100644 --- a/server/src/main/java/com/cloud/storage/download/DownloadListener.java +++ b/server/src/main/java/com/cloud/storage/download/DownloadListener.java @@ -25,6 +25,15 @@ import javax.inject.Inject; +import com.cloud.configuration.Resource; +import com.cloud.resourcelimit.CheckedReservation; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.ResourceLimitService; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; @@ -34,10 +43,13 @@ import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; import org.apache.cloudstack.managed.context.ManagedContextTimerTask; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.cloudstack.storage.command.DownloadCommand; import org.apache.cloudstack.storage.command.DownloadCommand.ResourceType; import org.apache.cloudstack.storage.command.DownloadProgressCommand; import org.apache.cloudstack.storage.command.DownloadProgressCommand.RequestType; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.utils.cache.LazyCache; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -107,6 +119,7 @@ protected void runInContext() { public static final String DOWNLOAD_ERROR = Status.DOWNLOAD_ERROR.toString(); public static final String DOWNLOAD_IN_PROGRESS = Status.DOWNLOAD_IN_PROGRESS.toString(); public static final String DOWNLOAD_ABANDONED = Status.ABANDONED.toString(); + public static final String DOWNLOAD_LIMIT_REACHED = Status.LIMIT_REACHED.toString(); private EndPoint _ssAgent; @@ -137,6 +150,18 @@ protected void runInContext() { private DataStoreManager _storeMgr; @Inject private VolumeService _volumeSrv; + @Inject + private VMTemplateDao _templateDao; + @Inject + private TemplateDataStoreDao _templateDataStoreDao; + @Inject + private VolumeDao _volumeDao; + @Inject + private ResourceLimitService _resourceLimitMgr; + @Inject + private AccountManager _accountMgr; + @Inject + ReservationDao _reservationDao; private LazyCache> zoneHypervisorsCache; @@ -160,7 +185,7 @@ public DownloadListener(EndPoint ssAgent, DataStore store, DataObject object, Ti _downloadMonitor = downloadMonitor; _cmd = cmd; initStateMachine(); - _currState = getState(Status.NOT_DOWNLOADED.toString()); + _currState = getState(NOT_DOWNLOADED); this._timer = timer; _timeoutTask = new TimeoutTask(this); this._timer.schedule(_timeoutTask, 3 * STATUS_POLL_INTERVAL); @@ -184,11 +209,12 @@ public void setCurrState(Status currState) { } private void initStateMachine() { - _stateMap.put(Status.NOT_DOWNLOADED.toString(), new NotDownloadedState(this)); - _stateMap.put(Status.DOWNLOADED.toString(), new DownloadCompleteState(this)); - _stateMap.put(Status.DOWNLOAD_ERROR.toString(), new DownloadErrorState(this)); - _stateMap.put(Status.DOWNLOAD_IN_PROGRESS.toString(), new DownloadInProgressState(this)); - _stateMap.put(Status.ABANDONED.toString(), new DownloadAbandonedState(this)); + _stateMap.put(NOT_DOWNLOADED, new NotDownloadedState(this)); + _stateMap.put(DOWNLOADED, new DownloadCompleteState(this)); + _stateMap.put(DOWNLOAD_ERROR, new DownloadErrorState(this)); + _stateMap.put(DOWNLOAD_IN_PROGRESS, new DownloadInProgressState(this)); + _stateMap.put(DOWNLOAD_ABANDONED, new DownloadAbandonedState(this)); + _stateMap.put(DOWNLOAD_LIMIT_REACHED, new DownloadLimitReachedState(this)); } private DownloadState getState(String stateName) { @@ -239,10 +265,53 @@ public boolean isRecurring() { return false; } + private Long getAccountIdForDataObject() { + if (object == null) { + return null; + } + if (DataObjectType.TEMPLATE.equals(object.getType())) { + VMTemplateVO t = _templateDao.findById(object.getId()); + return t != null ? t.getAccountId() : null; + } else if (DataObjectType.VOLUME.equals(object.getType())) { + VolumeVO v = _volumeDao.findById(object.getId()); + return v != null ? v.getAccountId() : null; + } + return null; + } + + private Long getSizeFromDB() { + Long lastSize = 0L; + if (DataObjectType.TEMPLATE.equals(object.getType())) { + TemplateDataStoreVO t = _templateDataStoreDao.findByStoreTemplate(object.getDataStore().getId(), object.getId()); + lastSize = t.getSize(); + } else if (DataObjectType.VOLUME.equals(object.getType())) { + VolumeVO v = _volumeDao.findById(object.getId()); + lastSize = v.getSize(); + } + return lastSize; + } + + private Boolean checkAndUpdateResourceLimits(DownloadAnswer answer) { + Long lastSize = getSizeFromDB(); + Long currentSize = answer.getTemplateSize(); + + if (currentSize > lastSize) { + Long accountId = getAccountIdForDataObject(); + Account account = _accountMgr.getAccount(accountId); + Long usage = currentSize - lastSize; + try (CheckedReservation secStorageReservation = new CheckedReservation(account, Resource.ResourceType.secondary_storage, usage, _reservationDao, _resourceLimitMgr)) { + _resourceLimitMgr.incrementResourceCount(accountId, Resource.ResourceType.secondary_storage, usage); + } catch (Exception e) { + return false; + } + } + return true; + } + @Override public boolean processAnswers(long agentId, long seq, Answer[] answers) { boolean processed = false; - if (answers != null & answers.length > 0) { + if (answers != null && answers.length > 0) { if (answers[0] instanceof DownloadAnswer) { final DownloadAnswer answer = (DownloadAnswer)answers[0]; if (getJobId() == null) { @@ -250,7 +319,11 @@ public boolean processAnswers(long agentId, long seq, Answer[] answers) { } else if (!getJobId().equalsIgnoreCase(answer.getJobId())) { return false;//TODO } - transition(DownloadEvent.DOWNLOAD_ANSWER, answer); + if (!checkAndUpdateResourceLimits(answer)) { + transition(DownloadEvent.LIMIT_REACHED, answer); + } else { + transition(DownloadEvent.DOWNLOAD_ANSWER, answer); + } processed = true; } } diff --git a/server/src/main/java/com/cloud/storage/download/DownloadState.java b/server/src/main/java/com/cloud/storage/download/DownloadState.java index 68723b53e354..e58d1f3f39f7 100644 --- a/server/src/main/java/com/cloud/storage/download/DownloadState.java +++ b/server/src/main/java/com/cloud/storage/download/DownloadState.java @@ -26,7 +26,7 @@ public abstract class DownloadState { public static enum DownloadEvent { - DOWNLOAD_ANSWER, ABANDON_DOWNLOAD, TIMEOUT_CHECK, DISCONNECT + DOWNLOAD_ANSWER, ABANDON_DOWNLOAD, LIMIT_REACHED, TIMEOUT_CHECK, DISCONNECT }; protected Logger logger = LogManager.getLogger(getClass()); @@ -51,6 +51,8 @@ public String handleEvent(DownloadEvent event, Object eventObj) { return handleAnswer(answer); case ABANDON_DOWNLOAD: return handleAbort(); + case LIMIT_REACHED: + return handleLimitReached(); case TIMEOUT_CHECK: Date now = new Date(); long update = now.getTime() - dl.getLastUpdated().getTime(); @@ -78,6 +80,8 @@ public void onExit() { public abstract String handleAbort(); + public abstract String handleLimitReached(); + public abstract String handleDisconnect(); public abstract String handleAnswer(DownloadAnswer answer); diff --git a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java index c096ef0eb1df..7e5811a2ece8 100644 --- a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java +++ b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java @@ -481,13 +481,12 @@ protected Void createTemplateAsyncCallBack(AsyncCallbackDispatcher context) { TemplateApiResult result = callback.getResult(); TemplateInfo template = context.template; + VMTemplateVO tmplt = _tmpltDao.findById(template.getId()); if (result.isSuccess()) { - VMTemplateVO tmplt = _tmpltDao.findById(template.getId()); // need to grant permission for public templates if (tmplt.isPublicTemplate()) { _messageBus.publish(_name, TemplateManager.MESSAGE_REGISTER_PUBLIC_TEMPLATE_EVENT, PublishScope.LOCAL, tmplt.getId()); } - long accountId = tmplt.getAccountId(); if (template.getSize() != null) { // publish usage event String etype = EventTypes.EVENT_TEMPLATE_CREATE; @@ -517,7 +516,11 @@ protected Void createTemplateAsyncCallBack(AsyncCallbackDispatcher Date: Fri, 6 Mar 2026 10:45:36 +0530 Subject: [PATCH 012/205] volume download fix --- .../storage/image/BaseImageStoreDriverImpl.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java index 61b1a84cdc6a..26b39e30776f 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java @@ -286,19 +286,22 @@ protected Void createTemplateAsyncCallback(AsyncCallbackDispatcher caller = context.getParentCallback(); - if (answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR || - answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.ABANDONED || answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.UNKNOWN) { + if (VMTemplateStorageResourceAssoc.ERROR_DOWNLOAD_STATES.contains(answer.getDownloadStatus())) { CreateCmdResult result = new CreateCmdResult(null, null); result.setSuccess(false); result.setResult(answer.getErrorString()); From 021a18dd98338de9772b3b7c89cc43802d1a862e Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:19:56 +0530 Subject: [PATCH 013/205] secondary storage resource limit for upload --- .../storage/command/UploadStatusCommand.java | 10 ++ .../storage/ImageStoreUploadMonitorImpl.java | 169 +++++++++++++----- .../resource/HttpUploadServerHandler.java | 2 + .../resource/NfsSecondaryStorageResource.java | 33 +++- 4 files changed, 173 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/UploadStatusCommand.java b/core/src/main/java/org/apache/cloudstack/storage/command/UploadStatusCommand.java index 9e6b76e467ff..f78744046f73 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/command/UploadStatusCommand.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/UploadStatusCommand.java @@ -28,6 +28,7 @@ public enum EntityType { } private String entityUuid; private EntityType entityType; + private Boolean abort; protected UploadStatusCommand() { } @@ -37,6 +38,11 @@ public UploadStatusCommand(String entityUuid, EntityType entityType) { this.entityType = entityType; } + public UploadStatusCommand(String entityUuid, EntityType entityType, Boolean abort) { + this(entityUuid, entityType); + this.abort = abort; + } + public String getEntityUuid() { return entityUuid; } @@ -45,6 +51,10 @@ public EntityType getEntityType() { return entityType; } + public Boolean getAbort() { + return abort; + } + @Override public boolean executeInSequence() { return false; diff --git a/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java b/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java index 408eb69917a2..b56e5b562136 100755 --- a/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java +++ b/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java @@ -26,10 +26,10 @@ import javax.naming.ConfigurationException; import com.cloud.agent.api.to.OVFInformationTO; -import com.cloud.exception.ResourceAllocationException; import com.cloud.resourcelimit.CheckedReservation; import com.cloud.user.Account; import com.cloud.user.dao.AccountDao; +import com.cloud.user.AccountManager; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; @@ -126,6 +126,8 @@ public class ImageStoreUploadMonitorImpl extends ManagerBase implements ImageSto private ReservationDao reservationDao; @Inject private AccountDao accountDao; + @Inject + private AccountManager _accountMgr; private long _nodeId; private ScheduledExecutorService _executor = null; @@ -214,6 +216,36 @@ protected class UploadStatusCheck extends ManagedContextRunnable { public UploadStatusCheck() { } + private Answer sendUploadStatusCommandForVolume(EndPoint ep, UploadStatusCommand cmd, VolumeVO volume) { + Answer answer = null; + try { + answer = ep.sendMessage(cmd); + } catch (CloudRuntimeException e) { + logger.warn("Unable to get upload status for volume {}. Error details: {}", volume, e.getMessage()); + answer = new UploadStatusAnswer(cmd, UploadStatus.UNKNOWN, e.getMessage()); + } + if (answer == null || !(answer instanceof UploadStatusAnswer)) { + logger.warn("No or invalid answer corresponding to UploadStatusCommand for volume {}", volume); + return null; + } + return answer; + } + + private Answer sendUploadStatusCommandForTemplate(EndPoint ep, UploadStatusCommand cmd, VMTemplateVO template) { + Answer answer = null; + try { + answer = ep.sendMessage(cmd); + } catch (CloudRuntimeException e) { + logger.warn("Unable to get upload status for template {}. Error details: {}", template, e.getMessage()); + answer = new UploadStatusAnswer(cmd, UploadStatus.UNKNOWN, e.getMessage()); + } + if (answer == null || !(answer instanceof UploadStatusAnswer)) { + logger.warn("No or invalid answer corresponding to UploadStatusCommand for template {}", template); + return null; + } + return answer; + } + @Override protected void runInContext() { // 1. Select all entries with download_state = Not_Downloaded or Download_In_Progress @@ -240,18 +272,17 @@ protected void runInContext() { UploadStatusCommand cmd = new UploadStatusCommand(volume.getUuid(), EntityType.Volume); if (host != null && host.getManagementServerId() != null) { if (_nodeId == host.getManagementServerId().longValue()) { - Answer answer = null; - try { - answer = ep.sendMessage(cmd); - } catch (CloudRuntimeException e) { - logger.warn("Unable to get upload status for volume {}. Error details: {}", volume, e.getMessage()); - answer = new UploadStatusAnswer(cmd, UploadStatus.UNKNOWN, e.getMessage()); - } - if (answer == null || !(answer instanceof UploadStatusAnswer)) { - logger.warn("No or invalid answer corresponding to UploadStatusCommand for volume {}", volume); + Answer answer = sendUploadStatusCommandForVolume(ep, cmd, volume); + if (answer == null) { continue; } - handleVolumeStatusResponse((UploadStatusAnswer)answer, volume, volumeDataStore); + if (!handleVolumeStatusResponse((UploadStatusAnswer)answer, volume, volumeDataStore)) { + cmd = new UploadStatusCommand(volume.getUuid(), EntityType.Volume, true); + answer = sendUploadStatusCommandForVolume(ep, cmd, volume); + if (answer == null) { + logger.warn("Unable to abort upload for volume {}", volume); + } + } } } else { String error = "Volume " + volume.getUuid() + " failed to upload as SSVM is either destroyed or SSVM agent not in 'Up' state"; @@ -284,18 +315,17 @@ protected void runInContext() { UploadStatusCommand cmd = new UploadStatusCommand(template.getUuid(), EntityType.Template); if (host != null && host.getManagementServerId() != null) { if (_nodeId == host.getManagementServerId().longValue()) { - Answer answer = null; - try { - answer = ep.sendMessage(cmd); - } catch (CloudRuntimeException e) { - logger.warn("Unable to get upload status for template {}. Error details: {}", template, e.getMessage()); - answer = new UploadStatusAnswer(cmd, UploadStatus.UNKNOWN, e.getMessage()); - } - if (answer == null || !(answer instanceof UploadStatusAnswer)) { - logger.warn("No or invalid answer corresponding to UploadStatusCommand for template {}", template); + Answer answer = sendUploadStatusCommandForTemplate(ep, cmd, template); + if (answer == null) { continue; } - handleTemplateStatusResponse((UploadStatusAnswer)answer, template, templateDataStore); + if (!handleTemplateStatusResponse((UploadStatusAnswer) answer, template, templateDataStore)) { + cmd = new UploadStatusCommand(template.getUuid(), EntityType.Template, true); + answer = sendUploadStatusCommandForTemplate(ep, cmd, template); + if (answer == null) { + logger.warn("Unable to abort upload for template {}", template); + } + } } } else { String error = String.format( @@ -312,7 +342,41 @@ protected void runInContext() { } } - private void handleVolumeStatusResponse(final UploadStatusAnswer answer, final VolumeVO volume, final VolumeDataStoreVO volumeDataStore) { + private Boolean checkAndUpdateSecondaryStorageResourceLimit(Long accountId, Long lastSize, Long currentSize) { + if (lastSize >= currentSize) { + return true; + } + Long usage = currentSize - lastSize; + try (CheckedReservation secStorageReservation = new CheckedReservation(_accountMgr.getAccount(accountId), Resource.ResourceType.secondary_storage, null, null, usage, reservationDao, _resourceLimitMgr)) { + _resourceLimitMgr.incrementResourceCount(accountId, Resource.ResourceType.secondary_storage, usage); + return true; + } catch (Exception e) { + _resourceLimitMgr.decrementResourceCount(accountId, Resource.ResourceType.secondary_storage, lastSize); + return false; + } + } + + private Boolean checkAndUpdateVolumeResourceLimit(VolumeVO volume, VolumeDataStoreVO volumeDataStore, UploadStatusAnswer answer) { + boolean success = true; + Long currentSize = answer.getVirtualSize() != 0 ? answer.getVirtualSize() : answer.getPhysicalSize(); + Long lastSize = volume.getSize() != null ? volume.getSize() : 0L; + if (!checkAndUpdateSecondaryStorageResourceLimit(volume.getAccountId(), volume.getSize(), currentSize)) { + volumeDataStore.setDownloadState(VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR); + volumeDataStore.setState(State.Failed); + volumeDataStore.setErrorString("Storage Limit Reached"); + Account owner = accountDao.findById(volume.getAccountId()); + String msg = String.format("Upload of volume [%s] failed because its owner [%s] does not have enough secondary storage space available.", volume.getUuid(), owner.getUuid()); + logger.error(msg); + success = false; + } + VolumeVO volumeUpdate = _volumeDao.findById(volume.getId()); + volumeUpdate.setSize(currentSize); + _volumeDao.update(volumeUpdate.getId(), volumeUpdate); + return success; + } + + private boolean handleVolumeStatusResponse(final UploadStatusAnswer answer, final VolumeVO volume, final VolumeDataStoreVO volumeDataStore) { + final boolean[] needAbort = new boolean[]{false}; final StateMachine2 stateMachine = Volume.State.getStateMachine(); Transaction.execute(new TransactionCallbackNoReturn() { @Override @@ -324,6 +388,11 @@ public void doInTransactionWithoutResult(TransactionStatus status) { try { switch (answer.getStatus()) { case COMPLETED: + if (!checkAndUpdateVolumeResourceLimit(tmpVolume, tmpVolumeDataStore, answer)) { + stateMachine.transitTo(tmpVolume, Event.OperationFailed, null, _volumeDao); + sendAlert = true; + break; + } tmpVolumeDataStore.setDownloadState(VMTemplateStorageResourceAssoc.Status.DOWNLOADED); tmpVolumeDataStore.setState(State.Ready); tmpVolumeDataStore.setInstallPath(answer.getInstallPath()); @@ -335,7 +404,6 @@ public void doInTransactionWithoutResult(TransactionStatus status) { volumeUpdate.setSize(answer.getVirtualSize()); _volumeDao.update(tmpVolume.getId(), volumeUpdate); stateMachine.transitTo(tmpVolume, Event.OperationSucceeded, null, _volumeDao); - _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), Resource.ResourceType.secondary_storage, answer.getVirtualSize()); // publish usage events UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_UPLOAD, tmpVolume.getAccountId(), @@ -348,6 +416,12 @@ public void doInTransactionWithoutResult(TransactionStatus status) { } break; case IN_PROGRESS: + if (!checkAndUpdateVolumeResourceLimit(tmpVolume, tmpVolumeDataStore, answer)) { + stateMachine.transitTo(tmpVolume, Event.OperationFailed, null, _volumeDao); + sendAlert = true; + needAbort[0] = true; + break; + } if (tmpVolume.getState() == Volume.State.NotUploaded) { tmpVolumeDataStore.setDownloadState(VMTemplateStorageResourceAssoc.Status.DOWNLOAD_IN_PROGRESS); tmpVolumeDataStore.setDownloadPercent(answer.getDownloadPercent()); @@ -396,10 +470,29 @@ public void doInTransactionWithoutResult(TransactionStatus status) { } } }); + return !needAbort[0]; + } + + private Boolean checkAndUpdateTemplateResourceLimit(VMTemplateVO template, TemplateDataStoreVO templateDataStore, UploadStatusAnswer answer) { + boolean success = true; + Long currentSize = answer.getVirtualSize() != 0 ? answer.getVirtualSize() : answer.getPhysicalSize(); + Long lastSize = template.getSize() != null ? template.getSize() : 0L; + if (!checkAndUpdateSecondaryStorageResourceLimit(template.getAccountId(), lastSize, currentSize)) { + templateDataStore.setDownloadState(VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR); + templateDataStore.setErrorString("Storage Limit Reached"); + templateDataStore.setState(State.Failed); + Account owner = accountDao.findById(template.getAccountId()); + String msg = String.format("Upload of template [%s] failed because its owner [%s] does not have enough secondary storage space available.", template.getUuid(), owner.getUuid()); + logger.error(msg); + success = false; + } + templateDataStore.setSize(currentSize); + return success; } - private void handleTemplateStatusResponse(final UploadStatusAnswer answer, final VMTemplateVO template, final TemplateDataStoreVO templateDataStore) { + private boolean handleTemplateStatusResponse(final UploadStatusAnswer answer, final VMTemplateVO template, final TemplateDataStoreVO templateDataStore) { final StateMachine2 stateMachine = VirtualMachineTemplate.State.getStateMachine(); + final boolean[] needAbort = new boolean[]{false}; Transaction.execute(new TransactionCallbackNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { @@ -410,6 +503,11 @@ public void doInTransactionWithoutResult(TransactionStatus status) { try { switch (answer.getStatus()) { case COMPLETED: + if (!checkAndUpdateTemplateResourceLimit(tmpTemplate, tmpTemplateDataStore, answer)) { + stateMachine.transitTo(tmpTemplate, VirtualMachineTemplate.Event.OperationFailed, null, _templateDao); + sendAlert = true; + break; + } tmpTemplateDataStore.setDownloadState(VMTemplateStorageResourceAssoc.Status.DOWNLOADED); tmpTemplateDataStore.setState(State.Ready); tmpTemplateDataStore.setInstallPath(answer.getInstallPath()); @@ -445,22 +543,6 @@ public void doInTransactionWithoutResult(TransactionStatus status) { break; } } - - Account owner = accountDao.findById(template.getAccountId()); - long templateSize = answer.getVirtualSize(); - - try (CheckedReservation secondaryStorageReservation = new CheckedReservation(owner, Resource.ResourceType.secondary_storage, null, null, templateSize, reservationDao, _resourceLimitMgr)) { - _resourceLimitMgr.incrementResourceCount(owner.getId(), Resource.ResourceType.secondary_storage, templateSize); - } catch (ResourceAllocationException e) { - tmpTemplateDataStore.setDownloadState(VMTemplateStorageResourceAssoc.Status.UPLOAD_ERROR); - tmpTemplateDataStore.setState(State.Failed); - stateMachine.transitTo(tmpTemplate, VirtualMachineTemplate.Event.OperationFailed, null, _templateDao); - msg = String.format("Upload of template [%s] failed because its owner [%s] does not have enough secondary storage space available.", template.getUuid(), owner.getUuid()); - logger.warn(msg); - sendAlert = true; - break; - } - stateMachine.transitTo(tmpTemplate, VirtualMachineTemplate.Event.OperationSucceeded, null, _templateDao); //publish usage event String etype = EventTypes.EVENT_TEMPLATE_CREATE; @@ -477,6 +559,12 @@ public void doInTransactionWithoutResult(TransactionStatus status) { } break; case IN_PROGRESS: + if (!checkAndUpdateTemplateResourceLimit(tmpTemplate, tmpTemplateDataStore, answer)) { + stateMachine.transitTo(tmpTemplate, VirtualMachineTemplate.Event.OperationFailed, null, _templateDao); + sendAlert = true; + needAbort[0] = true; + break; + } if (tmpTemplate.getState() == VirtualMachineTemplate.State.NotUploaded) { tmpTemplateDataStore.setDownloadState(VMTemplateStorageResourceAssoc.Status.DOWNLOAD_IN_PROGRESS); stateMachine.transitTo(tmpTemplate, VirtualMachineTemplate.Event.UploadRequested, null, _templateDao); @@ -526,6 +614,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) { } } }); + return !needAbort[0]; } } diff --git a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/HttpUploadServerHandler.java b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/HttpUploadServerHandler.java index a580105d52a5..605749649bb0 100644 --- a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/HttpUploadServerHandler.java +++ b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/HttpUploadServerHandler.java @@ -130,6 +130,7 @@ public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { if (decoder != null) { decoder.cleanFiles(); } + storageResource.deregisterUploadChannel(uuid); requestProcessed = false; } @@ -182,6 +183,7 @@ public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Excep requestProcessed = true; return; } + storageResource.registerUploadChannel(uuid, ctx.channel()); //set the base directory to download the file DiskFileUpload.baseDirectory = uploadEntity.getInstallPathPrefix(); this.processTimeout = uploadEntity.getProcessTimeout(); diff --git a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java index 9b50666258e2..dc27f74bf3b2 100644 --- a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java +++ b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -254,7 +255,8 @@ public void setTimeout(int timeout) { protected String _parent = "/mnt/SecStorage"; final private String _tmpltpp = "template.properties"; protected String createTemplateFromSnapshotXenScript; - private HashMap uploadEntityStateMap = new HashMap<>(); + private final Map uploadEntityStateMap = new ConcurrentHashMap<>(); + private final Map uploadChannelMap = new ConcurrentHashMap<>(); private String _ssvmPSK = null; private long processTimeout; @@ -2374,6 +2376,20 @@ private UploadStatusAnswer execute(UploadStatusCommand cmd) { String entityUuid = cmd.getEntityUuid(); if (uploadEntityStateMap.containsKey(entityUuid)) { UploadEntity uploadEntity = uploadEntityStateMap.get(entityUuid); + if (Boolean.TRUE.equals(cmd.getAbort())) { + updateStateMapWithError(entityUuid, "Upload Entity aborted"); + String errorMsg = uploadEntity.getErrorMessage(); + if (errorMsg == null) { + errorMsg = "Upload aborted by management server"; + } + Channel channel = uploadChannelMap.remove(entityUuid); + if (channel != null && channel.isActive()) { + logger.info("Closing upload channel for entity {}", entityUuid); + channel.close(); + } + uploadEntityStateMap.remove(entityUuid); + return new UploadStatusAnswer(cmd, UploadStatus.ERROR, errorMsg); + } if (uploadEntity.getUploadState() == UploadEntity.Status.ERROR) { uploadEntityStateMap.remove(entityUuid); return new UploadStatusAnswer(cmd, UploadStatus.ERROR, uploadEntity.getErrorMessage()); @@ -2392,6 +2408,7 @@ private UploadStatusAnswer execute(UploadStatusCommand cmd) { UploadStatusAnswer answer = new UploadStatusAnswer(cmd, UploadStatus.IN_PROGRESS); long downloadedSize = FileUtils.sizeOfDirectory(new File(uploadEntity.getInstallPathPrefix())); int downloadPercent = (int)(100 * downloadedSize / uploadEntity.getContentLength()); + answer.setPhysicalSize(downloadedSize); answer.setDownloadPercent(Math.min(downloadPercent, 100)); return answer; } @@ -3421,6 +3438,10 @@ private int getSizeInGB(long sizeInBytes) { public String postUpload(String uuid, String filename, long processTimeout) { UploadEntity uploadEntity = uploadEntityStateMap.get(uuid); + if (uploadEntity == null) { + logger.warn("Upload entity not found for uuid: {}. Upload may have been aborted.", uuid); + return "Upload entity not found. Upload may have been aborted."; + } int installTimeoutPerGig = 180 * 60 * 1000; String resourcePath = uploadEntity.getInstallPathPrefix(); @@ -3571,6 +3592,16 @@ protected String getPostUploadPSK() { return _ssvmPSK; } + public void registerUploadChannel(String uuid, Channel channel) { + uploadChannelMap.put(uuid, channel); + } + + public void deregisterUploadChannel(String uuid) { + if (uuid != null) { + uploadChannelMap.remove(uuid); + } + } + public void updateStateMapWithError(String uuid, String errorMessage) { UploadEntity uploadEntity = null; if (uploadEntityStateMap.get(uuid) != null) { From c522e963a7a1082230b46fdbf6161e553ff95a08 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Mon, 9 Mar 2026 08:23:58 -0300 Subject: [PATCH 014/205] Consider infinite resources when calculating secondary storage limit for upload operations --- .../command/TemplateOrVolumePostUploadCommand.java | 8 ++++++++ .../main/java/com/cloud/storage/VolumeApiServiceImpl.java | 3 +-- .../com/cloud/template/HypervisorTemplateAdapter.java | 3 +-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/TemplateOrVolumePostUploadCommand.java b/core/src/main/java/org/apache/cloudstack/storage/command/TemplateOrVolumePostUploadCommand.java index 3ac83031eaf5..9acfe30bf43f 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/command/TemplateOrVolumePostUploadCommand.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/TemplateOrVolumePostUploadCommand.java @@ -19,6 +19,9 @@ package org.apache.cloudstack.storage.command; +import com.cloud.configuration.Resource; +import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; + public class TemplateOrVolumePostUploadCommand { long entityId; @@ -185,6 +188,11 @@ public void setDescription(String description) { this.description = description; } + public void setDefaultMaxSecondaryStorageInBytes(long defaultMaxSecondaryStorageInBytes) { + this.defaultMaxSecondaryStorageInGB = defaultMaxSecondaryStorageInBytes != Resource.RESOURCE_UNLIMITED ? + ByteScaleUtils.bytesToGibibytes(defaultMaxSecondaryStorageInBytes) : Resource.RESOURCE_UNLIMITED; + } + public void setDefaultMaxSecondaryStorageInGB(long defaultMaxSecondaryStorageInGB) { this.defaultMaxSecondaryStorageInGB = defaultMaxSecondaryStorageInGB; } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index a975bd624084..884ece90b1cc 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -107,7 +107,6 @@ import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; -import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; @@ -523,7 +522,7 @@ public GetUploadParamsResponse doInTransaction(TransactionStatus status) throws Account account = _accountDao.findById(accountId); Domain domain = domainDao.findById(account.getDomainId()); - command.setDefaultMaxSecondaryStorageInGB(ByteScaleUtils.bytesToGibibytes(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null))); + command.setDefaultMaxSecondaryStorageInBytes(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null)); command.setAccountId(accountId); Gson gson = new GsonBuilder().create(); String metadata = EncryptionUtil.encodeData(gson.toJson(command), key); diff --git a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java index 7e5811a2ece8..632add684d7a 100644 --- a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java +++ b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java @@ -64,7 +64,6 @@ import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.storage.heuristics.HeuristicRuleHelper; import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; -import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.security.DigestHelper; import org.apache.commons.collections.CollectionUtils; @@ -450,7 +449,7 @@ private void postUploadAllocation(List imageStores, VMTemplateVO temp Account account = _accountDao.findById(accountId); Domain domain = _domainDao.findById(account.getDomainId()); - payload.setDefaultMaxSecondaryStorageInGB(ByteScaleUtils.bytesToGibibytes(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null))); + payload.setDefaultMaxSecondaryStorageInBytes(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null)); payload.setAccountId(accountId); payload.setRemoteEndPoint(ep.getPublicAddr()); payload.setRequiresHvm(template.requiresHvm()); From 2377dc5e63f21e646ca54abaae8287c521717280 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Fri, 6 Mar 2026 10:17:37 -0300 Subject: [PATCH 015/205] [20.3] Address limit checks for VM, CPU, memory, volume, and primary storage --- .../com/cloud/projects/ProjectService.java | 4 +- .../com/cloud/user/ResourceLimitService.java | 15 +- .../user/account/AddAccountToProjectCmd.java | 3 +- .../user/account/AddUserToProjectCmd.java | 3 +- .../cloudstack}/resourcelimit/Reserver.java | 8 +- .../test/AddAccountToProjectCmdTest.java | 3 + .../cloud/vm/VirtualMachineManagerImpl.java | 17 +- .../orchestration/NetworkOrchestrator.java | 3 - .../orchestration/VolumeOrchestrator.java | 20 ++- .../cloud/projects/ProjectManagerImpl.java | 8 +- .../resourcelimit/CheckedReservation.java | 91 +++++++--- .../resourcelimit/ReservationHelper.java | 35 ++++ .../ResourceLimitManagerImpl.java | 142 ++++++--------- .../cloud/storage/VolumeApiServiceImpl.java | 118 ++++++++---- .../storage/snapshot/SnapshotManagerImpl.java | 15 +- .../cloud/template/TemplateManagerImpl.java | 5 +- .../java/com/cloud/vm/UserVmManagerImpl.java | 135 +++++++------- .../vm/snapshot/VMSnapshotManagerImpl.java | 22 ++- .../VolumeImportUnmanageManagerImpl.java | 21 ++- .../vm/UnmanagedVMsManagerImpl.java | 168 ++++++++++-------- .../ResourceLimitManagerImplTest.java | 83 ++------- .../storage/VolumeApiServiceImplTest.java | 52 ++++-- .../com/cloud/vm/UserVmManagerImplTest.java | 44 ++--- .../vm/snapshot/VMSnapshotManagerTest.java | 8 +- .../vpc/MockResourceLimitManagerImpl.java | 23 +-- .../VolumeImportUnmanageManagerImplTest.java | 3 - .../vm/UnmanagedVMsManagerImplTest.java | 53 ++---- 27 files changed, 595 insertions(+), 507 deletions(-) rename {server/src/main/java/com/cloud => api/src/main/java/org/apache/cloudstack}/resourcelimit/Reserver.java (73%) create mode 100644 server/src/main/java/com/cloud/resourcelimit/ReservationHelper.java diff --git a/api/src/main/java/com/cloud/projects/ProjectService.java b/api/src/main/java/com/cloud/projects/ProjectService.java index 5080cb5a7812..d11e9ae0446d 100644 --- a/api/src/main/java/com/cloud/projects/ProjectService.java +++ b/api/src/main/java/com/cloud/projects/ProjectService.java @@ -82,7 +82,7 @@ public interface ProjectService { Project updateProject(long id, String name, String displayText, String newOwnerName, Long userId, Role newRole) throws ResourceAllocationException; - boolean addAccountToProject(long projectId, String accountName, String email, Long projectRoleId, Role projectRoleType); + boolean addAccountToProject(long projectId, String accountName, String email, Long projectRoleId, Role projectRoleType) throws ResourceAllocationException; boolean deleteAccountFromProject(long projectId, String accountName); @@ -100,6 +100,6 @@ public interface ProjectService { Project findByProjectAccountIdIncludingRemoved(long projectAccountId); - boolean addUserToProject(Long projectId, String username, String email, Long projectRoleId, Role projectRole); + boolean addUserToProject(Long projectId, String username, String email, Long projectRoleId, Role projectRole) throws ResourceAllocationException; } diff --git a/api/src/main/java/com/cloud/user/ResourceLimitService.java b/api/src/main/java/com/cloud/user/ResourceLimitService.java index 936095551220..d725c4a967ba 100644 --- a/api/src/main/java/com/cloud/user/ResourceLimitService.java +++ b/api/src/main/java/com/cloud/user/ResourceLimitService.java @@ -30,6 +30,7 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.ServiceOffering; import com.cloud.template.VirtualMachineTemplate; +import org.apache.cloudstack.resourcelimit.Reserver; public interface ResourceLimitService { @@ -246,12 +247,12 @@ public interface ResourceLimitService { List getResourceLimitStorageTags(DiskOffering diskOffering); void updateTaggedResourceLimitsAndCountsForAccounts(List responses, String tag); void updateTaggedResourceLimitsAndCountsForDomains(List responses, String tag); - void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering) throws ResourceAllocationException; + void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException; List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering); void checkVolumeResourceLimitForDiskOfferingChange(Account owner, Boolean display, Long currentSize, Long newSize, - DiskOffering currentOffering, DiskOffering newOffering) throws ResourceAllocationException; + DiskOffering currentOffering, DiskOffering newOffering, List reservations) throws ResourceAllocationException; - void checkPrimaryStorageResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering) throws ResourceAllocationException; + void checkPrimaryStorageResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException; void incrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); void decrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); @@ -268,20 +269,18 @@ void updateVolumeResourceCountForDiskOfferingChange(long accountId, Boolean disp void incrementVolumePrimaryStorageResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); void decrementVolumePrimaryStorageResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); - void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template) throws ResourceAllocationException; + void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException; void incrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template); void decrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template); void checkVmResourceLimitsForServiceOfferingChange(Account owner, Boolean display, Long currentCpu, Long newCpu, - Long currentMemory, Long newMemory, ServiceOffering currentOffering, ServiceOffering newOffering, VirtualMachineTemplate template) throws ResourceAllocationException; + Long currentMemory, Long newMemory, ServiceOffering currentOffering, ServiceOffering newOffering, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException; void checkVmResourceLimitsForTemplateChange(Account owner, Boolean display, ServiceOffering offering, - VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate) throws ResourceAllocationException; + VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate, List reservations) throws ResourceAllocationException; - void checkVmCpuResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu) throws ResourceAllocationException; void incrementVmCpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu); void decrementVmCpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu); - void checkVmMemoryResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory) throws ResourceAllocationException; void incrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory); void decrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddAccountToProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddAccountToProjectCmd.java index 93021487040b..6342709280ae 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddAccountToProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddAccountToProjectCmd.java @@ -18,6 +18,7 @@ import java.util.List; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.api.ApiArgValidator; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.BaseCmd; @@ -106,7 +107,7 @@ public ProjectAccount.Role getRoleType() { ///////////////////////////////////////////////////// @Override - public void execute() { + public void execute() throws ResourceAllocationException { if (accountName == null && email == null) { throw new InvalidParameterValueException("Either accountName or email is required"); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddUserToProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddUserToProjectCmd.java index 9bdc85bc5c71..0a2d8824a5bd 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddUserToProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddUserToProjectCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.account; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiArgValidator; @@ -111,7 +112,7 @@ public String getEventDescription() { ///////////////////////////////////////////////////// @Override - public void execute() { + public void execute() throws ResourceAllocationException { validateInput(); boolean result = _projectService.addUserToProject(getProjectId(), getUsername(), getEmail(), getProjectRoleId(), getRoleType()); if (result) { diff --git a/server/src/main/java/com/cloud/resourcelimit/Reserver.java b/api/src/main/java/org/apache/cloudstack/resourcelimit/Reserver.java similarity index 73% rename from server/src/main/java/com/cloud/resourcelimit/Reserver.java rename to api/src/main/java/org/apache/cloudstack/resourcelimit/Reserver.java index 4d5e3ac30c50..6b3f57b6aa5e 100644 --- a/server/src/main/java/com/cloud/resourcelimit/Reserver.java +++ b/api/src/main/java/org/apache/cloudstack/resourcelimit/Reserver.java @@ -15,8 +15,14 @@ // specific language governing permissions and limitations // under the License. -package com.cloud.resourcelimit; +package org.apache.cloudstack.resourcelimit; +/** + * Interface implemented by CheckedReservation. + *

+ * This is defined in cloud-api to allow methods declared in modules that do not depend on cloud-server + * to receive CheckedReservations as parameters. + */ public interface Reserver extends AutoCloseable { void close(); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/AddAccountToProjectCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/AddAccountToProjectCmdTest.java index f100822b8c77..cd0390aa2689 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/AddAccountToProjectCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/AddAccountToProjectCmdTest.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.api.command.test; +import com.cloud.exception.ResourceAllocationException; import junit.framework.Assert; import junit.framework.TestCase; @@ -149,6 +150,8 @@ public void testExecuteForNullAccountNameEmail() { addAccountToProjectCmd.execute(); } catch (InvalidParameterValueException exception) { Assert.assertEquals("Either accountName or email is required", exception.getLocalizedMessage()); + } catch (ResourceAllocationException exception) { + Assert.fail(); } } diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index c0976fe137e9..2dcb8fa20594 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -4757,9 +4757,20 @@ private void removeCustomOfferingDetails(long vmId) { private void saveCustomOfferingDetails(long vmId, ServiceOffering serviceOffering) { Map details = userVmDetailsDao.listDetailsKeyPairs(vmId); - details.put(UsageEventVO.DynamicParameters.cpuNumber.name(), serviceOffering.getCpu().toString()); - details.put(UsageEventVO.DynamicParameters.cpuSpeed.name(), serviceOffering.getSpeed().toString()); - details.put(UsageEventVO.DynamicParameters.memory.name(), serviceOffering.getRamSize().toString()); + + // We need to restore only the customizable parameters. If we save a parameter that is not customizable and attempt + // to restore a VM snapshot, com.cloud.vm.UserVmManagerImpl.validateCustomParameters will fail. + ServiceOffering unfilledOffering = _serviceOfferingDao.findByIdIncludingRemoved(serviceOffering.getId()); + if (unfilledOffering.getCpu() == null) { + details.put(UsageEventVO.DynamicParameters.cpuNumber.name(), serviceOffering.getCpu().toString()); + } + if (unfilledOffering.getSpeed() == null) { + details.put(UsageEventVO.DynamicParameters.cpuSpeed.name(), serviceOffering.getSpeed().toString()); + } + if (unfilledOffering.getRamSize() == null) { + details.put(UsageEventVO.DynamicParameters.memory.name(), serviceOffering.getRamSize().toString()); + } + List detailList = new ArrayList<>(); for (Map.Entry entry: details.entrySet()) { UserVmDetailVO detailVO = new UserVmDetailVO(vmId, entry.getKey(), entry.getValue(), true); diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index f8bf613d3e70..4fd5cbd1949a 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -3097,9 +3097,6 @@ public Network doInTransaction(final TransactionStatus status) { CallContext.current().setEventDetails("Network Id: " + network.getId()); CallContext.current().putContextParameter(Network.class, network.getUuid()); return network; - } catch (Exception e) { - logger.error(e); - throw new RuntimeException(e); } } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index fdaec016cf53..0fc61d815882 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -39,6 +39,7 @@ import javax.naming.ConfigurationException; import com.cloud.exception.ResourceAllocationException; +import com.cloud.resourcelimit.ReservationHelper; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.dao.VMTemplateDao; @@ -77,6 +78,7 @@ import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; import org.apache.cloudstack.resourcedetail.DiskOfferingDetailVO; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.secret.PassphraseVO; import org.apache.cloudstack.secret.dao.PassphraseDao; import org.apache.cloudstack.snapshot.SnapshotHelper; @@ -1867,14 +1869,20 @@ protected void updateVolumeSize(DataStore store, VolumeVO vol) throws ResourceAl template == null ? null : template.getSize(), vol.getPassphraseId() != null); - if (newSize != vol.getSize()) { - DiskOfferingVO diskOffering = diskOfferingDao.findByIdIncludingRemoved(vol.getDiskOfferingId()); + if (newSize == vol.getSize()) { + return; + } + + DiskOfferingVO diskOffering = diskOfferingDao.findByIdIncludingRemoved(vol.getDiskOfferingId()); + + List reservations = new ArrayList<>(); + try { VMInstanceVO vm = vol.getInstanceId() != null ? vmInstanceDao.findById(vol.getInstanceId()) : null; if (vm == null || vm.getType() == VirtualMachine.Type.User) { // Update resource count for user vm volumes when volume is attached if (newSize > vol.getSize()) { _resourceLimitMgr.checkPrimaryStorageResourceLimit(_accountMgr.getActiveAccountById(vol.getAccountId()), - vol.isDisplay(), newSize - vol.getSize(), diskOffering); + vol.isDisplay(), newSize - vol.getSize(), diskOffering, reservations); _resourceLimitMgr.incrementVolumePrimaryStorageResourceCount(vol.getAccountId(), vol.isDisplay(), newSize - vol.getSize(), diskOffering); } else { @@ -1882,9 +1890,11 @@ protected void updateVolumeSize(DataStore store, VolumeVO vol) throws ResourceAl vol.getSize() - newSize, diskOffering); } } - vol.setSize(newSize); - _volsDao.persist(vol); + } finally { + ReservationHelper.closeAll(reservations); } + vol.setSize(newSize); + _volsDao.persist(vol); } @Override diff --git a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java index 8302f0ddf150..43efccd04f98 100644 --- a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java +++ b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java @@ -544,7 +544,7 @@ public ProjectVO findByProjectAccountIdIncludingRemoved(long projectAccountId) { @Override @ActionEvent(eventType = EventTypes.EVENT_PROJECT_USER_ADD, eventDescription = "adding user to project", async = true) - public boolean addUserToProject(Long projectId, String username, String email, Long projectRoleId, Role projectRole) { + public boolean addUserToProject(Long projectId, String username, String email, Long projectRoleId, Role projectRole) throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); Project project = getProject(projectId); @@ -614,8 +614,6 @@ public boolean addUserToProject(Long projectId, String username, String email, L logger.warn("Failed to add user to project: {}", project); return false; } - } catch (ResourceAllocationException e) { - throw new RuntimeException(e); } } } @@ -814,7 +812,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) throws Resour @Override @ActionEvent(eventType = EventTypes.EVENT_PROJECT_ACCOUNT_ADD, eventDescription = "adding account to project", async = true) - public boolean addAccountToProject(long projectId, String accountName, String email, Long projectRoleId, Role projectRoleType) { + public boolean addAccountToProject(long projectId, String accountName, String email, Long projectRoleId, Role projectRoleType) throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); //check that the project exists @@ -892,8 +890,6 @@ public boolean addAccountToProject(long projectId, String accountName, String em logger.warn("Failed to add account {} to project {}", accountName, project); return false; } - } catch (ResourceAllocationException e) { - throw new RuntimeException(e); } } } diff --git a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java index 8d2e19d475e2..5f9913e2ee5f 100644 --- a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java +++ b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java @@ -20,13 +20,16 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.reservation.ReservationVO; import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.user.ResourceReservation; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -48,12 +51,15 @@ public class CheckedReservation implements Reserver { ReservationDao reservationDao; ResourceLimitService resourceLimitService; - private final Account account; + private Account account; private Long domainId; - private final ResourceType resourceType; - private Long amount; + private ResourceType resourceType; + private Long resourceId; + private Long reservationAmount; + private List reservationTags; + private Long existingAmount; + private List existingLimitTags; private List reservations; - private List resourceLimitTags; private String getContextParameterKey() { return getResourceReservationContextParameterKey(resourceType); @@ -100,9 +106,25 @@ protected void checkLimitAndPersistReservation(Account account, Long domainId, R this.reservations.add(reservation); } - public CheckedReservation(Account account, ResourceType resourceType, List resourceLimitTags, Long amount, - ReservationDao reservationDao, ResourceLimitService resourceLimitService) throws ResourceAllocationException { - this(account, resourceType, null, resourceLimitTags, amount, reservationDao, resourceLimitService); + // TODO: refactor these into a Builder to avoid having so many constructors + public CheckedReservation(Account account, ResourceType resourceType, List resourceLimitTags, Long reservationAmount, + ReservationDao reservationDao, ResourceLimitService resourceLimitService) throws ResourceAllocationException { + this(account, resourceType, null, resourceLimitTags, null, reservationAmount, null, reservationDao, resourceLimitService); + } + + public CheckedReservation(Account account, ResourceType resourceType, Long resourceId, List reservedTags, + List existingTags, Long reservationAmount, Long existingAmount, ReservationDao reservationDao, + ResourceLimitService resourceLimitService) throws ResourceAllocationException { + this(account, null, resourceType, resourceId, reservedTags, existingTags, reservationAmount, existingAmount, reservationDao, resourceLimitService); + } + + public CheckedReservation(Account account, Long domainId, ResourceType resourceType, Long resourceId, List reservedTags, + Long reservationAmount, ReservationDao reservationDao, ResourceLimitService resourceLimitService) throws ResourceAllocationException { + this(account, domainId, resourceType, resourceId, reservedTags, null, reservationAmount, null, reservationDao, resourceLimitService); + } + + public CheckedReservation(Account account, ResourceType resourceType, Long resourceId, List reservedTags, Long reservationAmount, ReservationDao reservationDao, ResourceLimitService resourceLimitService) throws ResourceAllocationException { + this(account, null, resourceType, resourceId, reservedTags, null, reservationAmount, null, reservationDao, resourceLimitService); } /** @@ -110,36 +132,43 @@ public CheckedReservation(Account account, ResourceType resourceType, List resourceLimitTags, Long amount, - ReservationDao reservationDao, ResourceLimitService resourceLimitService) throws ResourceAllocationException { - this(account, account.getDomainId(), resourceType, resourceId, resourceLimitTags, amount, reservationDao, resourceLimitService); - } + public CheckedReservation(Account account, Long domainId, ResourceType resourceType, Long resourceId, List reservedTags, + List existingTags, Long reservationAmount, Long existingAmount, ReservationDao reservationDao, + ResourceLimitService resourceLimitService) throws ResourceAllocationException { + + if (ObjectUtils.allNull(account, domainId)) { + logger.debug("Not reserving any {} resources, as no account/domain was provided.", resourceType); + return; + } - public CheckedReservation(Account account, Long domainId, ResourceType resourceType, Long resourceId, List resourceLimitTags, Long amount, - ReservationDao reservationDao, ResourceLimitService resourceLimitService) throws ResourceAllocationException { this.reservationDao = reservationDao; this.resourceLimitService = resourceLimitService; this.account = account; - this.domainId = domainId; if (domainId == null) { - this.domainId = account.getDomainId(); + domainId = account.getDomainId(); } + this.domainId = domainId; this.resourceType = resourceType; - this.amount = amount; + this.reservationAmount = reservationAmount; + this.existingAmount = existingAmount; this.reservations = new ArrayList<>(); - this.resourceLimitTags = resourceLimitTags; - if (this.amount != null && this.amount != 0) { - if (amount > 0) { + this.reservationTags = getTagsWithoutNull(reservedTags); + this.existingLimitTags = getTagsWithoutNull(existingTags); + + // TODO: refactor me + if (this.reservationAmount != null && this.reservationAmount != 0) { + if (reservationAmount > 0) { setGlobalLock(); if (quotaLimitLock.lock(TRY_TO_GET_LOCK_TIME)) { try { - checkLimitAndPersistReservations(account, this.domainId, resourceType, resourceId, resourceLimitTags, amount); + adjustCountToNotConsiderExistingAmount(); + checkLimitAndPersistReservations(account, this.domainId, resourceType, resourceId, reservationTags, reservationAmount); CallContext.current().putContextParameter(getContextParameterKey(), getIds()); } catch (NullPointerException npe) { throw new CloudRuntimeException("not enough means to check limits", npe); @@ -150,7 +179,7 @@ public CheckedReservation(Account account, Long domainId, ResourceType resourceT throw new ResourceAllocationException(String.format("unable to acquire resource reservation \"%s\"", quotaLimitLock.getName()), resourceType); } } else { - checkLimitAndPersistReservations(account, this.domainId, resourceType, resourceId, resourceLimitTags, amount); + checkLimitAndPersistReservations(account, this.domainId, resourceType, resourceId, reservationTags, reservationAmount); } } else { logger.debug("not reserving any amount of resources for {} in domain {}, type: {}, tag: {}", @@ -158,6 +187,20 @@ public CheckedReservation(Account account, Long domainId, ResourceType resourceT } } + protected List getTagsWithoutNull(List tags) { + if (tags == null) { + return null; + } + return tags.stream().filter(Objects::nonNull).collect(Collectors.toList()); + } + + protected void adjustCountToNotConsiderExistingAmount() throws ResourceAllocationException { + if (existingAmount == null || existingAmount == 0) { + return; + } + checkLimitAndPersistReservations(account, domainId, resourceType, resourceId, existingLimitTags, -1 * existingAmount); + } + public CheckedReservation(Account account, ResourceType resourceType, Long amount, ReservationDao reservationDao, ResourceLimitService resourceLimitService) throws ResourceAllocationException { this(account, resourceType, null, amount, reservationDao, resourceLimitService); @@ -183,11 +226,11 @@ public Account getAccount() { } public String getResourceLimitTagsAsString() { - return CollectionUtils.isNotEmpty(resourceLimitTags) ? StringUtils.join(resourceLimitTags) : null; + return CollectionUtils.isNotEmpty(reservationTags) ? StringUtils.join(reservationTags) : null; } public Long getReservedAmount() { - return amount; + return reservationAmount; } public List getReservations() { diff --git a/server/src/main/java/com/cloud/resourcelimit/ReservationHelper.java b/server/src/main/java/com/cloud/resourcelimit/ReservationHelper.java new file mode 100644 index 000000000000..cffa17176faa --- /dev/null +++ b/server/src/main/java/com/cloud/resourcelimit/ReservationHelper.java @@ -0,0 +1,35 @@ +// +// 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 com.cloud.resourcelimit; + +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.resourcelimit.Reserver; + +import java.util.List; + +public class ReservationHelper { + + public static void closeAll(List reservations) throws CloudRuntimeException { + for (Reserver reservation : reservations) { + reservation.close(); + } + } + +} diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 43c3b3832584..6d2ec103ca21 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -52,6 +52,7 @@ import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.reservation.ReservationVO; import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; @@ -168,6 +169,8 @@ public class ResourceLimitManagerImpl extends ManagerBase implements ResourceLim @Inject private ReservationDao reservationDao; @Inject + private ResourceLimitService resourceLimitService; + @Inject protected SnapshotDao _snapshotDao; @Inject private SnapshotDataStoreDao _snapshotDataStoreDao; @@ -1662,54 +1665,53 @@ public List getResourceLimitStorageTagsForResourceCountOperation(Boolean } @Override - public void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering) throws ResourceAllocationException { + public void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException { List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering); if (CollectionUtils.isEmpty(tags)) { return; } - for (String tag : tags) { - checkResourceLimitWithTag(owner, ResourceType.volume, tag); - if (size != null) { - checkResourceLimitWithTag(owner, ResourceType.primary_storage, tag, size); - } + + CheckedReservation volumeReservation = new CheckedReservation(owner, ResourceType.volume, tags, 1L, reservationDao, resourceLimitService); + reservations.add(volumeReservation); + + if (size != null) { + CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, tags, size, reservationDao, resourceLimitService); + reservations.add(primaryStorageReservation); } } @Override - public void checkPrimaryStorageResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering) throws ResourceAllocationException { + public void checkPrimaryStorageResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException { List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering); if (CollectionUtils.isEmpty(tags)) { return; } - if (size != null) { - for (String tag : tags) { - checkResourceLimitWithTag(owner, ResourceType.primary_storage, tag, size); - } - } + CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, tags, size, reservationDao, resourceLimitService); + reservations.add(primaryStorageReservation); } @Override public void checkVolumeResourceLimitForDiskOfferingChange(Account owner, Boolean display, Long currentSize, Long newSize, - DiskOffering currentOffering, DiskOffering newOffering + DiskOffering currentOffering, DiskOffering newOffering, List reservations ) throws ResourceAllocationException { Ternary, Set, Set> updatedResourceLimitStorageTags = getResourceLimitStorageTagsForDiskOfferingChange(display, currentOffering, newOffering); if (updatedResourceLimitStorageTags == null) { return; } - Set sameTags = updatedResourceLimitStorageTags.first(); - Set newTags = updatedResourceLimitStorageTags.second(); - - if (newSize > currentSize) { - for (String tag : sameTags) { - checkResourceLimitWithTag(owner, ResourceType.primary_storage, tag, newSize - currentSize); - } + List currentTags = getResourceLimitStorageTagsForResourceCountOperation(true, currentOffering); + List tagsAfterUpdate = getResourceLimitStorageTagsForResourceCountOperation(true, newOffering); + if (currentTags.isEmpty() && tagsAfterUpdate.isEmpty()) { + return; } - for (String tag : newTags) { - checkResourceLimitWithTag(owner, ResourceType.volume, tag, 1L); - checkResourceLimitWithTag(owner, ResourceType.primary_storage, tag, newSize); - } + CheckedReservation volumeReservation = new CheckedReservation(owner, ResourceType.volume, null, tagsAfterUpdate, + currentTags, 1L, 1L, reservationDao, resourceLimitService); + reservations.add(volumeReservation); + + CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, null, + tagsAfterUpdate, currentTags, newSize, currentSize, reservationDao, resourceLimitService); + reservations.add(primaryStorageReservation); } @DB @@ -1932,18 +1934,23 @@ protected List getResourceLimitHostTagsForResourceCountOperation(Boolean } @Override - public void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template) throws ResourceAllocationException { + public void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException { List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); if (CollectionUtils.isEmpty(tags)) { return; } + + CheckedReservation vmReservation = new CheckedReservation(owner, ResourceType.user_vm, tags, 1L, reservationDao, resourceLimitService); + reservations.add(vmReservation); + Long cpu = serviceOffering.getCpu() != null ? Long.valueOf(serviceOffering.getCpu()) : 0L; + CheckedReservation cpuReservation = new CheckedReservation(owner, ResourceType.cpu, tags, cpu, reservationDao, resourceLimitService); + reservations.add(cpuReservation); + Long ram = serviceOffering.getRamSize() != null ? Long.valueOf(serviceOffering.getRamSize()) : 0L; - for (String tag : tags) { - checkResourceLimitWithTag(owner, ResourceType.user_vm, tag); - checkResourceLimitWithTag(owner, ResourceType.cpu, tag, cpu); - checkResourceLimitWithTag(owner, ResourceType.memory, tag, ram); - } + CheckedReservation memReservation = new CheckedReservation(owner, ResourceType.memory, tags, ram, reservationDao, resourceLimitService); + reservations.add(memReservation); + } @Override @@ -1989,76 +1996,53 @@ public void doInTransactionWithoutResult(TransactionStatus status) { @Override public void checkVmResourceLimitsForTemplateChange(Account owner, Boolean display, ServiceOffering offering, - VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate) throws ResourceAllocationException { + VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate, List reservations) throws ResourceAllocationException { checkVmResourceLimitsForServiceOfferingAndTemplateChange(owner, display, null, null, - null, null, offering, offering, currentTemplate, newTemplate); + null, null, offering, offering, currentTemplate, newTemplate, reservations); } @Override public void checkVmResourceLimitsForServiceOfferingChange(Account owner, Boolean display, Long currentCpu, Long newCpu, Long currentMemory, Long newMemory, - ServiceOffering currentOffering, ServiceOffering newOffering, VirtualMachineTemplate template + ServiceOffering currentOffering, ServiceOffering newOffering, VirtualMachineTemplate template, List reservations ) throws ResourceAllocationException { checkVmResourceLimitsForServiceOfferingAndTemplateChange(owner, display, currentCpu, newCpu, currentMemory, newMemory, currentOffering, - newOffering != null ? newOffering : currentOffering, template, template); + newOffering != null ? newOffering : currentOffering, template, template, reservations); } private void checkVmResourceLimitsForServiceOfferingAndTemplateChange(Account owner, Boolean display, Long currentCpu, Long newCpu, Long currentMemory, Long newMemory, ServiceOffering currentOffering, ServiceOffering newOffering, - VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate + VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate, List reservations ) throws ResourceAllocationException { - Ternary, Set, Set> updatedResourceLimitHostTags = getResourceLimitHostTagsForVmServiceOfferingAndTemplateChange(display, currentOffering, newOffering, currentTemplate, newTemplate); - if (updatedResourceLimitHostTags == null) { + List currentTags = getResourceLimitHostTagsForResourceCountOperation(true, currentOffering, currentTemplate); + List tagsAfterUpdate = getResourceLimitHostTagsForResourceCountOperation(true, newOffering, newTemplate); + if (currentTags.isEmpty() && tagsAfterUpdate.isEmpty()) { return; } + CheckedReservation vmReservation = new CheckedReservation(owner, ResourceType.user_vm, null, tagsAfterUpdate, + currentTags, 1L, 1L, reservationDao, resourceLimitService); + reservations.add(vmReservation); + if (currentCpu == null) { currentCpu = currentOffering.getCpu() != null ? Long.valueOf(currentOffering.getCpu()) : 0L; } if (newCpu == null) { newCpu = newOffering.getCpu() != null ? Long.valueOf(newOffering.getCpu()) : 0L; } + CheckedReservation cpuReservation = new CheckedReservation(owner, ResourceType.cpu, null, tagsAfterUpdate, + currentTags, newCpu, currentCpu, reservationDao, resourceLimitService); + reservations.add(cpuReservation); + if (currentMemory == null) { currentMemory = currentOffering.getRamSize() != null ? Long.valueOf(currentOffering.getRamSize()) : 0L; } if (newMemory == null) { newMemory = newOffering.getRamSize() != null ? Long.valueOf(newOffering.getRamSize()) : 0L; } - - Set sameTags = updatedResourceLimitHostTags.first(); - Set newTags = updatedResourceLimitHostTags.second(); - - if (newCpu - currentCpu > 0 || newMemory - currentMemory > 0) { - for (String tag : sameTags) { - if (newCpu - currentCpu > 0) { - checkResourceLimitWithTag(owner, ResourceType.cpu, tag, newCpu - currentCpu); - } - - if (newMemory - currentMemory > 0) { - checkResourceLimitWithTag(owner, ResourceType.memory, tag, newMemory - currentMemory); - } - } - } - - for (String tag : newTags) { - checkResourceLimitWithTag(owner, ResourceType.user_vm, tag, 1L); - checkResourceLimitWithTag(owner, ResourceType.cpu, tag, newCpu); - checkResourceLimitWithTag(owner, ResourceType.memory, tag, newMemory); - } - } - - @Override - public void checkVmCpuResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu) throws ResourceAllocationException { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); - if (CollectionUtils.isEmpty(tags)) { - return; - } - if (cpu == null) { - cpu = serviceOffering.getCpu() != null ? Long.valueOf(serviceOffering.getCpu()) : 0L; - } - for (String tag : tags) { - checkResourceLimitWithTag(owner, ResourceType.cpu, tag, cpu); - } + CheckedReservation memReservation = new CheckedReservation(owner, ResourceType.memory, null, tagsAfterUpdate, + currentTags, newMemory, currentMemory, reservationDao, resourceLimitService); + reservations.add(memReservation); } @Override @@ -2089,20 +2073,6 @@ public void decrementVmCpuResourceCount(long accountId, Boolean display, Service } } - @Override - public void checkVmMemoryResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory) throws ResourceAllocationException { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); - if (CollectionUtils.isEmpty(tags)) { - return; - } - if (memory == null) { - memory = serviceOffering.getRamSize() != null ? Long.valueOf(serviceOffering.getRamSize()) : 0L; - } - for (String tag : tags) { - checkResourceLimitWithTag(owner, ResourceType.memory, tag, memory); - } - } - @Override public void incrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory) { List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 884ece90b1cc..7186b07334df 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -36,6 +36,7 @@ import javax.inject.Inject; import com.cloud.resourcelimit.CheckedReservation; +import com.cloud.resourcelimit.ReservationHelper; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.InternalIdentity; @@ -93,6 +94,7 @@ import org.apache.cloudstack.resourcedetail.SnapshotPolicyDetailVO; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; import org.apache.cloudstack.resourcedetail.dao.SnapshotPolicyDetailsDao; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.snapshot.SnapshotHelper; import org.apache.cloudstack.storage.command.AttachAnswer; import org.apache.cloudstack.storage.command.AttachCommand; @@ -425,9 +427,17 @@ public VolumeVO uploadVolume(UploadVolumeCmd cmd) throws ResourceAllocationExcep Long diskOfferingId = cmd.getDiskOfferingId(); String imageStoreUuid = cmd.getImageStoreUuid(); - validateVolume(caller, ownerId, zoneId, volumeName, url, format, diskOfferingId); + VolumeVO volume; - VolumeVO volume = persistVolume(owner, zoneId, volumeName, url, format, diskOfferingId, Volume.State.Allocated); + List reservations = new ArrayList<>(); + try { + + validateVolume(caller, ownerId, zoneId, volumeName, url, format, diskOfferingId, reservations); + volume = persistVolume(owner, zoneId, volumeName, url, format, diskOfferingId, Volume.State.Allocated); + + } finally { + ReservationHelper.closeAll(reservations); + } VolumeInfo vol = volFactory.getVolume(volume.getId()); @@ -467,7 +477,9 @@ public GetUploadParamsResponse uploadVolume(final GetUploadParamsForVolumeCmd cm final Long diskOfferingId = cmd.getDiskOfferingId(); String imageStoreUuid = cmd.getImageStoreUuid(); - validateVolume(caller, ownerId, zoneId, volumeName, null, format, diskOfferingId); + List reservations = new ArrayList<>(); + try { + validateVolume(caller, ownerId, zoneId, volumeName, null, format, diskOfferingId, reservations); return Transaction.execute(new TransactionCallbackWithException() { @Override @@ -535,9 +547,13 @@ public GetUploadParamsResponse doInTransaction(TransactionStatus status) throws return response; } }); + + } finally { + ReservationHelper.closeAll(reservations); + } } - private boolean validateVolume(Account caller, long ownerId, Long zoneId, String volumeName, String url, String format, Long diskOfferingId) throws ResourceAllocationException { + private boolean validateVolume(Account caller, long ownerId, Long zoneId, String volumeName, String url, String format, Long diskOfferingId, List reservations) throws ResourceAllocationException { // permission check Account volumeOwner = _accountMgr.getActiveAccountById(ownerId); @@ -548,7 +564,7 @@ private boolean validateVolume(Account caller, long ownerId, Long zoneId, String _accountMgr.checkAccess(caller, null, true, volumeOwner); // Check that the resource limit for volumes won't be exceeded - _resourceLimitMgr.checkVolumeResourceLimit(volumeOwner, true, null, diskOffering); + _resourceLimitMgr.checkVolumeResourceLimit(volumeOwner, true, null, diskOffering, reservations); // Verify that zone exists DataCenterVO zone = _dcDao.findById(zoneId); @@ -926,8 +942,10 @@ public VolumeVO allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationExcept if (tags.size() == 1 && tags.get(0) == null) { tags = new ArrayList<>(); } - try (CheckedReservation volumeReservation = new CheckedReservation(owner, ResourceType.volume, null, tags, 1L, reservationDao, _resourceLimitMgr); - CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, null, tags, size, reservationDao, _resourceLimitMgr)) { + + List reservations = new ArrayList<>(); + try { + _resourceLimitMgr.checkVolumeResourceLimit(owner, displayVolume, size, diskOffering, reservations); // Verify that zone exists DataCenterVO zone = _dcDao.findById(zoneId); @@ -950,9 +968,8 @@ public VolumeVO allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationExcept return commitVolume(cmd, caller, owner, displayVolume, zoneId, diskOfferingId, provisioningType, size, minIops, maxIops, parentVolume, userSpecifiedName, _uuidMgr.generateUuid(Volume.class, cmd.getCustomId()), details); - } catch (Exception e) { - logger.error(e); - throw new RuntimeException(e); + } finally { + ReservationHelper.closeAll(reservations); } } @@ -1278,7 +1295,10 @@ public VolumeVO resizeVolume(ResizeVolumeCmd cmd) throws ResourceAllocationExcep if (dataStore != null && dataStore.getDriver() instanceof PrimaryDataStoreDriver) { newSize = ((PrimaryDataStoreDriver) dataStore.getDriver()).getVolumeSizeRequiredOnPool(newSize, null, isEncryptionRequired); } - validateVolumeResizeWithSize(volume, currentSize, newSize, shrinkOk, diskOffering, newDiskOffering); + + List reservations = new ArrayList<>(); + try { + validateVolumeResizeWithSize(volume, currentSize, newSize, shrinkOk, diskOffering, newDiskOffering, reservations); // Note: The storage plug-in in question should perform validation on the IOPS to check if a sufficient number of IOPS is available to perform // the requested change @@ -1406,6 +1426,10 @@ public VolumeVO resizeVolume(ResizeVolumeCmd cmd) throws ResourceAllocationExcep return orchestrateResizeVolume(volume.getId(), currentSize, newSize, newMinIops, newMaxIops, newHypervisorSnapshotReserve, newDiskOffering != null ? cmd.getNewDiskOfferingId() : null, shrinkOk); + + } finally { + ReservationHelper.closeAll(reservations); + } } /** @@ -1839,12 +1863,11 @@ public Volume recoverVolume(long volumeId) { throw new InvalidParameterValueException("Please specify a volume in Destroy state."); } + DiskOffering diskOffering = _diskOfferingDao.findById(volume.getDiskOfferingId()); + + List reservations = new ArrayList<>(); try { - _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(volume.getAccountId()), ResourceType.primary_storage, volume.isDisplayVolume(), volume.getSize()); - } catch (ResourceAllocationException e) { - logger.error("primary storage resource limit check failed", e); - throw new InvalidParameterValueException(e.getMessage()); - } + _resourceLimitMgr.checkVolumeResourceLimit(_accountMgr.getAccount(volume.getAccountId()), volume.isDisplayVolume(), volume.getSize(), diskOffering, reservations); try { _volsDao.detachVolume(volume.getId()); @@ -1856,6 +1879,12 @@ public Volume recoverVolume(long volumeId) { _resourceLimitMgr.incrementVolumeResourceCount(volume.getAccountId(), volume.isDisplay(), volume.getSize(), _diskOfferingDao.findById(volume.getDiskOfferingId())); + } catch (ResourceAllocationException e) { + logger.error("primary storage resource limit check failed", e); + throw new InvalidParameterValueException(e.getMessage()); + } finally { + ReservationHelper.closeAll(reservations); + } publishVolumeCreationUsageEvent(volume); @@ -2085,7 +2114,9 @@ public Volume changeDiskOfferingForVolumeInternal(Long volumeId, Long newDiskOff newSize = ((PrimaryDataStoreDriver) dataStore.getDriver()).getVolumeSizeRequiredOnPool(newSize, null, newDiskOffering.getEncrypt()); } - validateVolumeResizeWithSize(volume, currentSize, newSize, shrinkOk, existingDiskOffering, newDiskOffering); + List reservations = new ArrayList<>(); + try { + validateVolumeResizeWithSize(volume, currentSize, newSize, shrinkOk, existingDiskOffering, newDiskOffering, reservations); /* If this volume has never been beyond allocated state, short circuit everything and simply update the database. */ // We need to publish this event to usage_volume table @@ -2175,6 +2206,10 @@ public Volume changeDiskOfferingForVolumeInternal(Long volumeId, Long newDiskOff } return volume; + + } finally { + ReservationHelper.closeAll(reservations); + } } private void updateStorageWithTheNewDiskOffering(VolumeVO volume, DiskOfferingVO newDiskOffering) { @@ -2380,7 +2415,7 @@ private void checkIfVolumeCanResizeWithNewDiskOffering(VolumeVO volume, DiskOffe } private void validateVolumeResizeWithSize(VolumeVO volume, long currentSize, Long newSize, boolean shrinkOk, - DiskOfferingVO existingDiskOffering, DiskOfferingVO newDiskOffering) throws ResourceAllocationException { + DiskOfferingVO existingDiskOffering, DiskOfferingVO newDiskOffering, List reservations) throws ResourceAllocationException { // if the caller is looking to change the size of the volume if (newSize != null && currentSize != newSize) { @@ -2450,7 +2485,7 @@ private void validateVolumeResizeWithSize(VolumeVO volume, long currentSize, Lon /* Check resource limit for this account */ _resourceLimitMgr.checkVolumeResourceLimitForDiskOfferingChange(_accountMgr.getAccount(volume.getAccountId()), volume.isDisplayVolume(), currentSize, newSize != null ? newSize : currentSize, - existingDiskOffering, newDiskOffering); + existingDiskOffering, newDiskOffering, reservations); } @Override @@ -2622,7 +2657,7 @@ public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean checkForBackups(vm, true); - checkRightsToAttach(caller, volumeToAttach, vm); + _accountMgr.checkAccess(caller, null, true, volumeToAttach, vm); HypervisorType rootDiskHyperType = vm.getHypervisorType(); HypervisorType volumeToAttachHyperType = _volsDao.getHypervisorType(volumeToAttach.getId()); @@ -2649,6 +2684,12 @@ public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean throw new InvalidParameterValueException("Volume's disk offering has encryption enabled, but volume encryption is not supported for hypervisor type " + rootDiskHyperType); } + Account owner = _accountDao.findById(volumeToAttach.getAccountId()); + List resourceLimitStorageTags = _resourceLimitMgr.getResourceLimitStorageTagsForResourceCountOperation(true, diskOffering); + Long requiredPrimaryStorageSpace = getRequiredPrimaryStorageSizeForVolumeAttach(resourceLimitStorageTags, volumeToAttach); + + try (CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, resourceLimitStorageTags, requiredPrimaryStorageSpace, reservationDao, _resourceLimitMgr)) { + _jobMgr.updateAsyncJobAttachment(job.getId(), "Volume", volumeId); if (asyncExecutionContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) { @@ -2656,9 +2697,21 @@ public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean } else { return getVolumeAttachJobResult(vmId, volumeId, deviceId); } + + } catch (ResourceAllocationException e) { + logger.error("primary storage resource limit check failed", e); + throw new InvalidParameterValueException(e.getMessage()); + } + } + + protected Long getRequiredPrimaryStorageSizeForVolumeAttach(List resourceLimitStorageTags, VolumeInfo volumeToAttach) { + if (CollectionUtils.isEmpty(resourceLimitStorageTags) || Arrays.asList(Volume.State.Allocated, Volume.State.Ready).contains(volumeToAttach.getState())) { + return 0L; + } + return volumeToAttach.getSize(); } - @Nullable private Volume getVolumeAttachJobResult(Long vmId, Long volumeId, Long deviceId) { + @Nullable protected Volume getVolumeAttachJobResult(Long vmId, Long volumeId, Long deviceId) { Outcome outcome = attachVolumeToVmThroughJobQueue(vmId, volumeId, deviceId); Volume vol = null; @@ -2707,21 +2760,6 @@ private void checkForMatchingHypervisorTypesIf(boolean checkNeeded, HypervisorTy } } - private void checkRightsToAttach(Account caller, VolumeInfo volumeToAttach, UserVmVO vm) { - _accountMgr.checkAccess(caller, null, true, volumeToAttach, vm); - - Account owner = _accountDao.findById(volumeToAttach.getAccountId()); - - if (!Arrays.asList(Volume.State.Allocated, Volume.State.Ready).contains(volumeToAttach.getState())) { - try { - _resourceLimitMgr.checkResourceLimit(owner, ResourceType.primary_storage, volumeToAttach.getSize()); - } catch (ResourceAllocationException e) { - logger.error("primary storage resource limit check failed", e); - throw new InvalidParameterValueException(e.getMessage()); - } - } - } - private void checkForVMSnapshots(Long vmId, UserVmVO vm) { // if target VM has associated VM snapshots List vmSnapshots = _vmSnapshotDao.findByVm(vmId); @@ -4207,7 +4245,9 @@ public Volume assignVolumeToAccount(AssignVolumeCmd command) throws ResourceAllo _accountMgr.checkAccess(caller, null, true, oldAccount); _accountMgr.checkAccess(caller, null, true, newAccount); - _resourceLimitMgr.checkVolumeResourceLimit(newAccount, true, volume.getSize(), _diskOfferingDao.findById(volume.getDiskOfferingId())); + List reservations = new ArrayList<>(); + try { + _resourceLimitMgr.checkVolumeResourceLimit(newAccount, true, volume.getSize(), _diskOfferingDao.findById(volume.getDiskOfferingId()), reservations); Transaction.execute(new TransactionCallbackNoReturn() { @Override @@ -4217,6 +4257,10 @@ public void doInTransactionWithoutResult(TransactionStatus status) { }); return volume; + + } finally { + ReservationHelper.closeAll(reservations); + } } protected void updateVolumeAccount(Account oldAccount, VolumeVO volume, Account newAccount) { diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index d5475948c59b..4a4a7544ce74 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -1759,16 +1759,13 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), ResourceType.snapshot); _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), storeResourceType, volume.getSize()); return snapshot; - } catch (Exception e) { - if (e instanceof ResourceAllocationException) { - if (snapshotType != Type.MANUAL) { - String msg = String.format("Snapshot resource limit exceeded for account id : %s. Failed to create recurring snapshots", owner.getId()); - logger.warn(msg); - _alertMgr.sendAlert(AlertManager.AlertType.ALERT_TYPE_UPDATE_RESOURCE_COUNT, 0L, 0L, msg, msg + ". Please, use updateResourceLimit to increase the limit"); - } - throw (ResourceAllocationException) e; + } catch (ResourceAllocationException e) { + if (snapshotType != Type.MANUAL) { + String msg = String.format("Snapshot resource limit exceeded for account id : %s. Failed to create recurring snapshots", owner.getId()); + logger.warn(msg); + _alertMgr.sendAlert(AlertManager.AlertType.ALERT_TYPE_UPDATE_RESOURCE_COUNT, 0L, 0L, msg, msg + ". Please, use updateResourceLimit to increase the limit"); } - throw new CloudRuntimeException(e); + throw e; } } diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 749be7aab8bc..d0fdcbfde8f8 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -358,6 +358,7 @@ public VirtualMachineTemplate registerIso(RegisterIsoCmd cmd) throws ResourceAll // Secondary storage resource count is not incremented for BareMetalTemplateAdapter // Note: checking the file size before registering will require the Management Server host to have access to the Internet and a DNS server + // If it does not, UriUtils.getRemoteSize will return 0L. long secondaryStorageUsage = adapter instanceof HypervisorTemplateAdapter && !cmd.isDirectDownload() ? UriUtils.getRemoteSize(cmd.getUrl(), StorageManager.DataStoreDownloadFollowRedirects.value()) : 0L; @@ -366,7 +367,7 @@ public VirtualMachineTemplate registerIso(RegisterIsoCmd cmd) throws ResourceAll TemplateProfile profile = adapter.prepare(cmd); VMTemplateVO template = adapter.create(profile); - // Secondary storage resource usage will be recalculated in com.cloud.template.HypervisorTemplateAdapter.createTemplateAsyncCallBack + // Secondary storage resource usage will be incremented in com.cloud.template.HypervisorTemplateAdapter.createTemplateAsyncCallBack _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); if (secondaryStorageUsage > 0) { _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.secondary_storage, secondaryStorageUsage); @@ -406,7 +407,7 @@ public VirtualMachineTemplate registerTemplate(RegisterTemplateCmd cmd) throws U TemplateProfile profile = adapter.prepare(cmd); VMTemplateVO template = adapter.create(profile); - // Secondary storage resource usage will be recalculated in com.cloud.template.HypervisorTemplateAdapter.createTemplateAsyncCallBack + // Secondary storage resource usage will be incremented in com.cloud.template.HypervisorTemplateAdapter.createTemplateAsyncCallBack // for HypervisorTemplateAdapter _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); if (secondaryStorageUsage > 0) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 7fcf242ea20f..8b77cb506a8e 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -54,6 +54,7 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; +import com.cloud.resourcelimit.ReservationHelper; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -122,6 +123,7 @@ import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.snapshot.SnapshotHelper; import org.apache.cloudstack.storage.command.DeleteCommand; import org.apache.cloudstack.storage.command.DettachCommand; @@ -1351,9 +1353,12 @@ private UserVm upgradeStoppedVirtualMachine(Long vmId, Long svcOffId, Map reservations = new ArrayList<>(); + try { if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { _resourceLimitMgr.checkVmResourceLimitsForServiceOfferingChange(owner, vmInstance.isDisplay(), (long) currentCpu, (long) newCpu, - (long) currentMemory, (long) newMemory, currentServiceOffering, newServiceOffering, template); + (long) currentMemory, (long) newMemory, currentServiceOffering, newServiceOffering, template, reservations); } // Check that the specified service offering ID is valid @@ -1376,6 +1381,9 @@ private UserVm upgradeStoppedVirtualMachine(Long vmId, Long svcOffId, Map reservations = new ArrayList<>(); + try { // Check resource limits _resourceLimitMgr.checkVmResourceLimitsForServiceOfferingChange(owner, vmInstance.isDisplay(), (long) currentCpu, (long) newCpu, - (long) currentMemory, (long) newMemory, currentServiceOffering, newServiceOffering, template); + (long) currentMemory, (long) newMemory, currentServiceOffering, newServiceOffering, template, reservations); // Dynamically upgrade the running vms boolean success = false; @@ -2137,6 +2147,10 @@ private boolean upgradeRunningVirtualMachine(Long vmId, Long newServiceOfferingI } } return success; + + } finally { + ReservationHelper.closeAll(reservations); + } } protected void validateDiskOfferingChecks(ServiceOfferingVO currentServiceOffering, ServiceOfferingVO newServiceOffering) { @@ -2322,10 +2336,12 @@ public UserVm recoverVirtualMachine(RecoverVMCmd cmd) throws ResourceAllocationE ServiceOfferingVO serviceOffering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); + List reservations = new ArrayList<>(); + try { // First check that the maximum number of UserVMs, CPU and Memory limit for the given // accountId will not be exceeded if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { - resourceLimitService.checkVmResourceLimit(account, vm.isDisplayVm(), serviceOffering, template); + resourceLimitService.checkVmResourceLimit(account, vm.isDisplayVm(), serviceOffering, template, reservations); } _haMgr.cancelDestroy(vm, vm.getHostId()); @@ -2350,6 +2366,10 @@ public UserVm recoverVirtualMachine(RecoverVMCmd cmd) throws ResourceAllocationE //Update Resource Count for the given account resourceCountIncrement(account.getId(), vm.isDisplayVm(), serviceOffering, template); + + } finally { + ReservationHelper.closeAll(reservations); + } } }); @@ -2776,27 +2796,25 @@ protected void verifyVmLimits(UserVmVO vmInstance, Map details) long currentCpu = currentServiceOffering.getCpu(); long currentMemory = currentServiceOffering.getRamSize(); VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vmInstance.getTemplateId()); + List reservations = new ArrayList<>(); try { + _resourceLimitMgr.checkVmResourceLimitsForServiceOfferingChange(owner, vmInstance.isDisplay(), currentCpu, newCpu, + currentMemory, newMemory, currentServiceOffering, svcOffering, template, reservations); if (newCpu > currentCpu) { - _resourceLimitMgr.checkVmCpuResourceLimit(owner, vmInstance.isDisplay(), svcOffering, template, newCpu - currentCpu); + _resourceLimitMgr.incrementVmCpuResourceCount(owner.getAccountId(), vmInstance.isDisplay(), svcOffering, template, newCpu - currentCpu); + } else if (newCpu > 0 && currentCpu > newCpu){ + _resourceLimitMgr.decrementVmCpuResourceCount(owner.getAccountId(), vmInstance.isDisplay(), svcOffering, template, currentCpu - newCpu); } if (newMemory > currentMemory) { - _resourceLimitMgr.checkVmMemoryResourceLimit(owner, vmInstance.isDisplay(), svcOffering, template, newMemory - currentMemory); + _resourceLimitMgr.incrementVmMemoryResourceCount(owner.getAccountId(), vmInstance.isDisplay(), svcOffering, template, newMemory - currentMemory); + } else if (newMemory > 0 && currentMemory > newMemory){ + _resourceLimitMgr.decrementVmMemoryResourceCount(owner.getAccountId(), vmInstance.isDisplay(), svcOffering, template, currentMemory - newMemory); } } catch (ResourceAllocationException e) { logger.error(String.format("Failed to updated VM due to: %s", e.getLocalizedMessage())); throw new InvalidParameterValueException(e.getLocalizedMessage()); - } - - if (newCpu > currentCpu) { - _resourceLimitMgr.incrementVmCpuResourceCount(owner.getAccountId(), vmInstance.isDisplay(), svcOffering, template, newCpu - currentCpu); - } else if (newCpu > 0 && currentCpu > newCpu){ - _resourceLimitMgr.decrementVmCpuResourceCount(owner.getAccountId(), vmInstance.isDisplay(), svcOffering, template, currentCpu - newCpu); - } - if (newMemory > currentMemory) { - _resourceLimitMgr.incrementVmMemoryResourceCount(owner.getAccountId(), vmInstance.isDisplay(), svcOffering, template, newMemory - currentMemory); - } else if (newMemory > 0 && currentMemory > newMemory){ - _resourceLimitMgr.decrementVmMemoryResourceCount(owner.getAccountId(), vmInstance.isDisplay(), svcOffering, template, currentMemory - newMemory); + } finally { + ReservationHelper.closeAll(reservations); } } @@ -4236,7 +4254,6 @@ private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, Stri throw new InvalidParameterValueException(String.format("Invalid disk offering %s specified for datadisk Template %s. Disk offering size should be greater than or equal to the Template size", dataDiskOffering, dataDiskTemplate)); } _templateDao.loadDetails(dataDiskTemplate); - resourceLimitService.checkVolumeResourceLimit(owner, true, dataDiskOffering.getDiskSize(), dataDiskOffering); } } @@ -5660,11 +5677,6 @@ public Pair> startVirtualMach CheckedReservation memReservation = new CheckedReservation(owner, ResourceType.memory, resourceLimitHostTags, Long.valueOf(offering.getRamSize()), reservationDao, resourceLimitService); ) { return startVirtualMachineUnchecked(vm, template, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, isExplicitHost, isRootAdmin); - } catch (ResourceAllocationException | CloudRuntimeException e) { - throw e; - } catch (Exception e) { - logger.error("Failed to start VM {} : error during resource reservation and allocation", e); - throw new CloudRuntimeException(e); } } else { return startVirtualMachineUnchecked(vm, template, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, isExplicitHost, isRootAdmin); @@ -7387,38 +7399,29 @@ public VirtualMachine migrateVirtualMachineWithVolume(Long vmId, Host destinatio return findMigratedVm(vm.getId(), vm.getType()); } - protected void checkVolumesLimits(Account account, List volumes) throws ResourceAllocationException { - Long totalVolumes = 0L; - Long totalVolumesSize = 0L; + protected void checkVolumesLimits(Account account, List volumes, List reservations) throws ResourceAllocationException { Map> diskOfferingTagsMap = new HashMap<>(); - Map tagVolumeCountMap = new HashMap<>(); - Map tagSizeMap = new HashMap<>(); + for (VolumeVO volume : volumes) { if (!volume.isDisplay()) { continue; } - totalVolumes++; - totalVolumesSize += volume.getSize(); - if (!diskOfferingTagsMap.containsKey(volume.getDiskOfferingId())) { - diskOfferingTagsMap.put(volume.getDiskOfferingId(), _resourceLimitMgr.getResourceLimitStorageTags( - _diskOfferingDao.findById(volume.getDiskOfferingId()))); - } - List tags = diskOfferingTagsMap.get(volume.getDiskOfferingId()); - for (String tag : tags) { - if (tagVolumeCountMap.containsKey(tag)) { - tagVolumeCountMap.put(tag, tagVolumeCountMap.get(tag) + 1); - tagSizeMap.put(tag, tagSizeMap.get(tag) + volume.getSize()); - } else { - tagVolumeCountMap.put(tag, 1L); - tagSizeMap.put(tag, volume.getSize()); - } + + Long diskOfferingId = volume.getDiskOfferingId(); + if (!diskOfferingTagsMap.containsKey(diskOfferingId)) { + DiskOffering diskOffering = _diskOfferingDao.findById(diskOfferingId); + List tagsForDiskOffering = _resourceLimitMgr.getResourceLimitStorageTags(diskOffering); + diskOfferingTagsMap.put(diskOfferingId, tagsForDiskOffering); } - } - _resourceLimitMgr.checkResourceLimit(account, ResourceType.volume, totalVolumes); - _resourceLimitMgr.checkResourceLimit(account, ResourceType.primary_storage, totalVolumesSize); - for (String tag : tagVolumeCountMap.keySet()) { - resourceLimitService.checkResourceLimitWithTag(account, ResourceType.volume, tag, tagVolumeCountMap.get(tag)); - resourceLimitService.checkResourceLimitWithTag(account, ResourceType.primary_storage, tag, tagSizeMap.get(tag)); + + List tags = diskOfferingTagsMap.get(diskOfferingId); + + CheckedReservation volumeReservation = new CheckedReservation(account, ResourceType.volume, tags, 1L, reservationDao, resourceLimitService); + reservations.add(volumeReservation); + + long size = ObjectUtils.defaultIfNull(volume.getSize(), 0L); + CheckedReservation primaryStorageReservation = new CheckedReservation(account, ResourceType.primary_storage, tags, size, reservationDao, resourceLimitService); + reservations.add(primaryStorageReservation); } } @@ -7460,14 +7463,16 @@ public UserVm moveVmToUser(final AssignVMCmd cmd) throws ResourceAllocationExcep final ServiceOfferingVO offering = serviceOfferingDao.findByIdIncludingRemoved(vm.getId(), vm.getServiceOfferingId()); VirtualMachineTemplate template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); - verifyResourceLimitsForAccountAndStorage(newAccount, vm, offering, volumes, template); - validateIfNewOwnerHasAccessToTemplate(vm, newAccount, template); DomainVO domain = _domainDao.findById(domainId); logger.trace("Verifying if the new account [{}] has access to the specified domain [{}].", newAccount, domain); _accountMgr.checkAccess(newAccount, domain); + List reservations = new ArrayList<>(); + try { + verifyResourceLimitsForAccountAndStorage(newAccount, vm, offering, volumes, template, reservations); + Network newNetwork = ensureDestinationNetwork(cmd, vm, newAccount); try { Transaction.execute(new TransactionCallbackNoReturn() { @@ -7484,6 +7489,10 @@ public void doInTransactionWithoutResult(TransactionStatus status) { throw e; } + } finally { + ReservationHelper.closeAll(reservations); + } + logger.info("VM [{}] now belongs to account [{}].", vm.getInstanceName(), newAccountName); return vm; } @@ -7554,18 +7563,18 @@ protected void validateIfVolumesHaveNoSnapshots(List volumes) throws I * @param volumes The volumes whose total size can exceed resource limits. * @throws ResourceAllocationException */ - protected void verifyResourceLimitsForAccountAndStorage(Account account, UserVmVO vm, ServiceOfferingVO offering, List volumes, VirtualMachineTemplate template) + protected void verifyResourceLimitsForAccountAndStorage(Account account, UserVmVO vm, ServiceOfferingVO offering, List volumes, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException { logger.trace("Verifying if CPU and RAM for VM [{}] do not exceed account [{}] limit.", vm, account); if (!countOnlyRunningVmsInResourceLimitation()) { - resourceLimitService.checkVmResourceLimit(account, vm.isDisplayVm(), offering, template); + resourceLimitService.checkVmResourceLimit(account, vm.isDisplayVm(), offering, template, reservations); } logger.trace("Verifying if volume size for VM [{}] does not exceed account [{}] limit.", vm, account); - checkVolumesLimits(account, volumes); + checkVolumesLimits(account, volumes, reservations); } protected boolean countOnlyRunningVmsInResourceLimitation() { @@ -8415,12 +8424,10 @@ public UserVm restoreVirtualMachine(final Account caller, final long vmId, final VMTemplateVO template = getRestoreVirtualMachineTemplate(caller, newTemplateId, rootVols, vm); DiskOffering diskOffering = rootDiskOfferingId != null ? _diskOfferingDao.findById(rootDiskOfferingId) : null; + + List reservations = new ArrayList<>(); try { - checkRestoreVmFromTemplate(vm, template, rootVols, diskOffering, details); - } catch (ResourceAllocationException e) { - logger.error("Failed to restore VM {} due to {}", vm, e.getMessage(), e); - throw new CloudRuntimeException("Failed to restore VM " + vm.getUuid() + " due to " + e.getMessage(), e); - } + checkRestoreVmFromTemplate(vm, template, rootVols, diskOffering, details, reservations); if (needRestart) { try { @@ -8565,6 +8572,12 @@ public Pair doInTransaction(final TransactionStatus status) th logger.debug("Restore VM {} done successfully", vm); return vm; + } catch (ResourceAllocationException e) { + logger.error("Failed to restore VM {} due to {}", vm, e.getMessage(), e); + throw new CloudRuntimeException("Failed to restore VM " + vm.getUuid() + " due to " + e.getMessage(), e); + } finally { + ReservationHelper.closeAll(reservations); + } } Long getRootVolumeSizeForVmRestore(Volume vol, VMTemplateVO template, UserVmVO userVm, DiskOffering diskOffering, Map details, boolean update) { @@ -8664,7 +8677,7 @@ private void updateVMDynamicallyScalabilityUsingTemplate(UserVmVO vm, Long newTe * @param template template * @throws InvalidParameterValueException if restore is not possible */ - private void checkRestoreVmFromTemplate(UserVmVO vm, VMTemplateVO template, List rootVolumes, DiskOffering newDiskOffering, Map details) throws ResourceAllocationException { + private void checkRestoreVmFromTemplate(UserVmVO vm, VMTemplateVO template, List rootVolumes, DiskOffering newDiskOffering, Map details, List reservations) throws ResourceAllocationException { TemplateDataStoreVO tmplStore; if (!template.isDirectDownload()) { tmplStore = _templateStoreDao.findByTemplateZoneReady(template.getId(), vm.getDataCenterId()); @@ -8682,7 +8695,7 @@ private void checkRestoreVmFromTemplate(UserVmVO vm, VMTemplateVO template, List if (vm.getTemplateId() != template.getId()) { ServiceOfferingVO serviceOffering = serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); VMTemplateVO currentTemplate = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); - _resourceLimitMgr.checkVmResourceLimitsForTemplateChange(owner, vm.isDisplay(), serviceOffering, currentTemplate, template); + _resourceLimitMgr.checkVmResourceLimitsForTemplateChange(owner, vm.isDisplay(), serviceOffering, currentTemplate, template, reservations); } for (Volume vol : rootVolumes) { @@ -8693,7 +8706,7 @@ private void checkRestoreVmFromTemplate(UserVmVO vm, VMTemplateVO template, List if (newDiskOffering != null || !vol.getSize().equals(newSize)) { DiskOffering currentOffering = _diskOfferingDao.findById(vol.getDiskOfferingId()); _resourceLimitMgr.checkVolumeResourceLimitForDiskOfferingChange(owner, vol.isDisplay(), - vol.getSize(), newSize, currentOffering, newDiskOffering); + vol.getSize(), newSize, currentOffering, newDiskOffering, reservations); } } } diff --git a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java index cdbb7119e9e8..8e7ce351246a 100644 --- a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java @@ -811,25 +811,31 @@ else if (jobResult instanceof Throwable) } /** - * If snapshot was taken with a different service offering than actual used in vm, should change it back to it - * @param userVm vm to change service offering (if necessary) + * If snapshot was taken with a different service offering than actual used in vm, should change it back to it. + * We also call changeUserVmServiceOffering in case the service offering is dynamic in order to + * perform resource limit validation, as the amount of CPUs or memory may have been changed. * @param vmSnapshotVo vm snapshot */ protected void updateUserVmServiceOffering(UserVm userVm, VMSnapshotVO vmSnapshotVo) { if (vmSnapshotVo.getServiceOfferingId() != userVm.getServiceOfferingId()) { changeUserVmServiceOffering(userVm, vmSnapshotVo); + return; + } + ServiceOfferingVO serviceOffering = _serviceOfferingDao.findById(userVm.getServiceOfferingId()); + if (serviceOffering.isDynamic()) { + changeUserVmServiceOffering(userVm, vmSnapshotVo); } } /** * Get user vm details as a map - * @param userVm user vm + * @param vmSnapshotVo snapshot to get the details from * @return map */ - protected Map getVmMapDetails(UserVm userVm) { - List userVmDetails = _userVmDetailsDao.listDetails(userVm.getId()); + protected Map getVmMapDetails(VMSnapshotVO vmSnapshotVo) { + List vmSnapshotDetails = _vmSnapshotDetailsDao.listDetails(vmSnapshotVo.getId()); Map details = new HashMap(); - for (UserVmDetailVO detail : userVmDetails) { + for (VMSnapshotDetailsVO detail : vmSnapshotDetails) { details.put(detail.getName(), detail.getValue()); } return details; @@ -841,7 +847,7 @@ protected Map getVmMapDetails(UserVm userVm) { * @param vmSnapshotVo vm snapshot */ protected void changeUserVmServiceOffering(UserVm userVm, VMSnapshotVO vmSnapshotVo) { - Map vmDetails = getVmMapDetails(userVm); + Map vmDetails = getVmMapDetails(vmSnapshotVo); boolean result = upgradeUserVmServiceOffering(userVm, vmSnapshotVo.getServiceOfferingId(), vmDetails); if (! result){ throw new CloudRuntimeException("Instance Snapshot reverting failed because the Instance service offering couldn't be changed to the one used when Snapshot was taken"); @@ -938,8 +944,8 @@ private UserVm orchestrateRevertToVMSnapshot(Long vmSnapshotId) throws Insuffici Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) throws CloudRuntimeException { - revertCustomServiceOfferingDetailsFromVmSnapshot(userVm, vmSnapshotVo); updateUserVmServiceOffering(userVm, vmSnapshotVo); + revertCustomServiceOfferingDetailsFromVmSnapshot(userVm, vmSnapshotVo); } }); return userVm; diff --git a/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java index 9e1fc46e02eb..47aa57f728cd 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java @@ -35,6 +35,7 @@ import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.offering.DiskOffering; +import com.cloud.resourcelimit.ReservationHelper; import com.cloud.storage.DataStoreRole; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.Storage; @@ -68,6 +69,7 @@ import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; @@ -198,10 +200,7 @@ public VolumeResponse importVolume(ImportVolumeCmd cmd) { logFailureAndThrowException("Volume is a reference of snapshot on primary: " + volume.getFullPath()); } - // 5. check resource limitation - checkResourceLimitForImportVolume(owner, volume); - - // 6. get disk offering + // 5. get disk offering DiskOfferingVO diskOffering = getOrCreateDiskOffering(owner, cmd.getDiskOfferingId(), pool.getDataCenterId(), pool.isLocal()); if (diskOffering.isCustomized()) { volumeApiService.validateCustomDiskOfferingSizeRange(volume.getVirtualSize() / ByteScaleUtils.GiB); @@ -210,6 +209,11 @@ public VolumeResponse importVolume(ImportVolumeCmd cmd) { logFailureAndThrowException(String.format("Disk offering: %s storage tags are not compatible with selected storage pool: %s", diskOffering, pool)); } + List reservations = new ArrayList<>(); + try { + // 6. check resource limitation + checkResourceLimitForImportVolume(owner, volume, diskOffering, reservations); + // 7. create records String volumeName = StringUtils.isNotBlank(cmd.getName()) ? cmd.getName().trim() : volumePath; VolumeVO volumeVO = importVolumeInternal(volume, diskOffering, owner, pool, volumeName); @@ -221,6 +225,10 @@ public VolumeResponse importVolume(ImportVolumeCmd cmd) { publicUsageEventForVolumeImportAndUnmanage(volumeVO, true); return responseGenerator.createVolumeResponse(ResponseObject.ResponseView.Full, volumeVO); + + } finally { + ReservationHelper.closeAll(reservations); + } } protected VolumeOnStorageTO getVolumeOnStorageAndCheck(StoragePoolVO pool, String volumePath) { @@ -456,11 +464,10 @@ private VolumeVO importVolumeInternal(VolumeOnStorageTO volume, DiskOfferingVO d return volumeDao.findById(diskProfile.getVolumeId()); } - protected void checkResourceLimitForImportVolume(Account owner, VolumeOnStorageTO volume) { + protected void checkResourceLimitForImportVolume(Account owner, VolumeOnStorageTO volume, DiskOfferingVO diskOffering, List reservations) { Long volumeSize = volume.getVirtualSize(); try { - resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.volume); - resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.primary_storage, volumeSize); + resourceLimitService.checkVolumeResourceLimit(owner, true, volumeSize, diskOffering, reservations); } catch (ResourceAllocationException e) { logger.error("VM resource allocation error for account: {}", owner, e); throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM resource allocation error for account: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage()))); diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 0cf921f36bed..1917804fb8d5 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -86,6 +86,8 @@ import com.cloud.org.Cluster; import com.cloud.resource.ResourceManager; import com.cloud.resource.ResourceState; +import com.cloud.resourcelimit.CheckedReservation; +import com.cloud.resourcelimit.ReservationHelper; import com.cloud.serializer.GsonHelper; import com.cloud.server.ManagementService; import com.cloud.service.ServiceOfferingVO; @@ -164,6 +166,8 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -222,6 +226,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { @Inject private ResourceLimitService resourceLimitService; @Inject + private ReservationDao reservationDao; + @Inject private UserVmDetailsDao userVmDetailsDao; @Inject private UserVmManager userVmManager; @@ -604,7 +610,7 @@ private Pair> getRootAn return new Pair<>(rootDisk, dataDisks); } - private void checkUnmanagedDiskAndOfferingForImport(String instanceName, UnmanagedInstanceTO.Disk disk, DiskOffering diskOffering, ServiceOffering serviceOffering, final Account owner, final DataCenter zone, final Cluster cluster, final boolean migrateAllowed) + private void checkUnmanagedDiskAndOfferingForImport(String instanceName, UnmanagedInstanceTO.Disk disk, DiskOffering diskOffering, ServiceOffering serviceOffering, final Account owner, final DataCenter zone, final Cluster cluster, final boolean migrateAllowed, List reservations) throws ServerApiException, PermissionDeniedException, ResourceAllocationException { if (serviceOffering == null && diskOffering == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Disk offering for disk ID [%s] not found during VM [%s] import.", disk.getDiskId(), instanceName)); @@ -612,7 +618,6 @@ private void checkUnmanagedDiskAndOfferingForImport(String instanceName, Unmanag if (diskOffering != null) { accountService.checkAccess(owner, diskOffering, zone); } - resourceLimitService.checkVolumeResourceLimit(owner, true, null, diskOffering); if (disk.getCapacity() == null || disk.getCapacity() == 0) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Size of disk(ID: %s) is found invalid during VM import", disk.getDiskId())); } @@ -627,9 +632,10 @@ private void checkUnmanagedDiskAndOfferingForImport(String instanceName, Unmanag if (diskOffering != null && !migrateAllowed && !storagePoolSupportsDiskOffering(storagePool, diskOffering)) { throw new InvalidParameterValueException(String.format("Disk offering: %s is not compatible with storage pool: %s of unmanaged disk: %s", diskOffering.getUuid(), storagePool.getUuid(), disk.getDiskId())); } + resourceLimitService.checkVolumeResourceLimit(owner, true, disk.getCapacity(), diskOffering, reservations); } - private void checkUnmanagedDiskAndOfferingForImport(String intanceName, List disks, final Map diskOfferingMap, final Account owner, final DataCenter zone, final Cluster cluster, final boolean migrateAllowed) + private void checkUnmanagedDiskAndOfferingForImport(String intanceName, List disks, final Map diskOfferingMap, final Account owner, final DataCenter zone, final Cluster cluster, final boolean migrateAllowed, List reservations) throws ServerApiException, PermissionDeniedException, ResourceAllocationException { String diskController = null; for (UnmanagedInstanceTO.Disk disk : disks) { @@ -646,7 +652,7 @@ private void checkUnmanagedDiskAndOfferingForImport(String intanceName, List dataDisks, Map dataDiskOfferingMap) throws ResourceAllocationException { - Long totalVolumes = 0L; - Long totalVolumesSize = 0L; - List disks = new ArrayList<>(); - disks.add(rootDisk); - disks.addAll(dataDisks); - Map diskOfferingMap = new HashMap<>(dataDiskOfferingMap); - diskOfferingMap.put(rootDisk.getDiskId(), serviceOffering.getDiskOfferingId()); - Map diskOfferingVolumeCountMap = new HashMap<>(); - Map diskOfferingSizeMap = new HashMap<>(); - for (UnmanagedInstanceTO.Disk disk : disks) { - totalVolumes++; - totalVolumesSize += disk.getCapacity(); - Long diskOfferingId = diskOfferingMap.get(disk.getDiskId()); - if (diskOfferingVolumeCountMap.containsKey(diskOfferingId)) { - diskOfferingVolumeCountMap.put(diskOfferingId, diskOfferingVolumeCountMap.get(diskOfferingId) + 1); - diskOfferingSizeMap.put(diskOfferingId, diskOfferingSizeMap.get(diskOfferingId) + disk.getCapacity()); - } else { - diskOfferingVolumeCountMap.put(diskOfferingId, 1L); - diskOfferingSizeMap.put(diskOfferingId, disk.getCapacity()); - } - } - resourceLimitService.checkResourceLimit(account, Resource.ResourceType.volume, totalVolumes); - resourceLimitService.checkResourceLimit(account, Resource.ResourceType.primary_storage, totalVolumesSize); - for (Long diskOfferingId : diskOfferingVolumeCountMap.keySet()) { - List tags = resourceLimitService.getResourceLimitStorageTags(diskOfferingDao.findById(diskOfferingId)); - for (String tag : tags) { - resourceLimitService.checkResourceLimitWithTag(account, Resource.ResourceType.volume, tag, diskOfferingVolumeCountMap.get(diskOfferingId)); - resourceLimitService.checkResourceLimitWithTag(account, Resource.ResourceType.primary_storage, tag, diskOfferingSizeMap.get(diskOfferingId)); - } - } - } - private UserVm importVirtualMachineInternal(final UnmanagedInstanceTO unmanagedInstance, final String instanceNameInternal, final DataCenter zone, final Cluster cluster, final HostVO host, final VirtualMachineTemplate template, final String displayName, final String hostName, final Account caller, final Account owner, final Long userId, final ServiceOfferingVO serviceOffering, final Map dataDiskOfferingMap, @@ -1164,17 +1136,14 @@ private UserVm importVirtualMachineInternal(final UnmanagedInstanceTO unmanagedI allDetails.put(VmDetailConstants.ROOT_DISK_SIZE, String.valueOf(size)); } + List reservations = new ArrayList<>(); try { - checkUnmanagedDiskAndOfferingForImport(unmanagedInstance.getName(), rootDisk, null, validatedServiceOffering, owner, zone, cluster, migrateAllowed); - if (CollectionUtils.isNotEmpty(dataDisks)) { // Data disk(s) present - checkUnmanagedDiskAndOfferingForImport(unmanagedInstance.getName(), dataDisks, dataDiskOfferingMap, owner, zone, cluster, migrateAllowed); - allDetails.put(VmDetailConstants.DATA_DISK_CONTROLLER, dataDisks.get(0).getController()); - } - checkUnmanagedDiskLimits(owner, rootDisk, serviceOffering, dataDisks, dataDiskOfferingMap); - } catch (ResourceAllocationException e) { - logger.error("Volume resource allocation error for owner: {}", owner, e); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume resource allocation error for owner: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage()))); + checkUnmanagedDiskAndOfferingForImport(unmanagedInstance.getName(), rootDisk, null, validatedServiceOffering, owner, zone, cluster, migrateAllowed, reservations); + if (CollectionUtils.isNotEmpty(dataDisks)) { // Data disk(s) present + checkUnmanagedDiskAndOfferingForImport(unmanagedInstance.getName(), dataDisks, dataDiskOfferingMap, owner, zone, cluster, migrateAllowed, reservations); + allDetails.put(VmDetailConstants.DATA_DISK_CONTROLLER, dataDisks.get(0).getController()); } + // Check NICs and supplied networks Map nicIpAddressMap = getNicIpAddresses(unmanagedInstance.getNics(), callerNicIpAddressMap); Map allNicNetworkMap = getUnmanagedNicNetworkMap(unmanagedInstance.getName(), unmanagedInstance.getNics(), nicNetworkMap, nicIpAddressMap, zone, hostName, owner, cluster.getHypervisorType()); @@ -1257,6 +1226,13 @@ private UserVm importVirtualMachineInternal(final UnmanagedInstanceTO unmanagedI } publishVMUsageUpdateResourceCount(userVm, validatedServiceOffering, template); return userVm; + + } catch (ResourceAllocationException e) { // This will be thrown by checkUnmanagedDiskAndOfferingForImport, so the VM was not imported yet + logger.error("Volume resource allocation error for owner: {}", owner, e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume resource allocation error for owner: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage()))); + } finally { + ReservationHelper.closeAll(reservations); + } } private void addImportingVMBootTypeAndModeDetails(String bootType, String bootMode, Map allDetails) { @@ -1361,8 +1337,6 @@ private UserVmResponse baseImportInstance(ImportUnmanagedInstanceCmd cmd) { VMTemplateVO template = getTemplateForImportInstance(cmd.getTemplateId(), cluster.getHypervisorType()); ServiceOfferingVO serviceOffering = getServiceOfferingForImportInstance(cmd.getServiceOfferingId(), owner, zone); - checkResourceLimitForImportInstance(owner); - String displayName = getDisplayNameForImportInstance(cmd.getDisplayName(), instanceName); String hostName = getHostNameForImportInstance(cmd.getHostName(), cluster.getHypervisorType(), instanceName, displayName); @@ -1378,6 +1352,11 @@ private UserVmResponse baseImportInstance(ImportUnmanagedInstanceCmd cmd) { List managedVms = new ArrayList<>(additionalNameFilters); managedVms.addAll(getHostsManagedVms(hosts)); + List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(serviceOffering, template); + try (CheckedReservation vmReservation = new CheckedReservation(owner, Resource.ResourceType.user_vm, resourceLimitHostTags, 1L, reservationDao, resourceLimitService); + CheckedReservation cpuReservation = new CheckedReservation(owner, Resource.ResourceType.cpu, resourceLimitHostTags, Long.valueOf(serviceOffering.getCpu()), reservationDao, resourceLimitService); + CheckedReservation memReservation = new CheckedReservation(owner, Resource.ResourceType.memory, resourceLimitHostTags, Long.valueOf(serviceOffering.getRamSize()), reservationDao, resourceLimitService)) { + ActionEventUtils.onStartedActionEvent(userId, owner.getId(), EventTypes.EVENT_VM_IMPORT, cmd.getEventDescription(), null, null, true, 0); @@ -1405,6 +1384,11 @@ private UserVmResponse baseImportInstance(ImportUnmanagedInstanceCmd cmd) { } } + } catch (ResourceAllocationException e) { + logger.error(String.format("VM resource allocation error for account: %s", owner.getUuid()), e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM resource allocation error for account: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage()))); + } + if (userVm == null) { ActionEventUtils.onCompletedActionEvent(userId, owner.getId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_IMPORT, cmd.getEventDescription(), null, null, 0); @@ -1464,15 +1448,6 @@ private String getDisplayNameForImportInstance(String displayName, String instan return StringUtils.isEmpty(displayName) ? instanceName : displayName; } - private void checkResourceLimitForImportInstance(Account owner) { - try { - resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.user_vm, 1); - } catch (ResourceAllocationException e) { - logger.error("VM resource allocation error for account: {}", owner, e); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM resource allocation error for account: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage()))); - } - } - private ServiceOfferingVO getServiceOfferingForImportInstance(Long serviceOfferingId, Account owner, DataCenter zone) { if (serviceOfferingId == null) { throw new InvalidParameterValueException("Service offering ID cannot be null"); @@ -2338,12 +2313,6 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) { throw new InvalidParameterValueException(String.format("Service offering ID: %d cannot be found", serviceOfferingId)); } accountService.checkAccess(owner, serviceOffering, zone); - try { - resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.user_vm, 1); - } catch (ResourceAllocationException e) { - logger.error("VM resource allocation error for account: {}", owner, e); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM resource allocation error for account: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage()))); - } String displayName = cmd.getDisplayName(); if (StringUtils.isEmpty(displayName)) { displayName = instanceName; @@ -2431,6 +2400,11 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) { UserVm userVm = null; + List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(serviceOffering, template); + try (CheckedReservation vmReservation = new CheckedReservation(owner, Resource.ResourceType.user_vm, resourceLimitHostTags, 1L, reservationDao, resourceLimitService); + CheckedReservation cpuReservation = new CheckedReservation(owner, Resource.ResourceType.cpu, resourceLimitHostTags, Long.valueOf(serviceOffering.getCpu()), reservationDao, resourceLimitService); + CheckedReservation memReservation = new CheckedReservation(owner, Resource.ResourceType.memory, resourceLimitHostTags, Long.valueOf(serviceOffering.getRamSize()), reservationDao, resourceLimitService)) { + if (ImportSource.EXTERNAL == importSource) { String username = cmd.getUsername(); String password = cmd.getPassword(); @@ -2451,6 +2425,12 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) { throw new RuntimeException(e); } } + + } catch (ResourceAllocationException e) { + logger.error(String.format("VM resource allocation error for account: %s", owner.getUuid()), e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM resource allocation error for account: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage()))); + } + if (userVm == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import Vm with name: %s ", instanceName)); } @@ -2464,7 +2444,7 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag final VirtualMachineTemplate template, final String displayName, final String hostName, final Account caller, final Account owner, final Long userId, final ServiceOfferingVO serviceOffering, final Map dataDiskOfferingMap, final Map nicNetworkMap, final Map callerNicIpAddressMap, - final String remoteUrl, String username, String password, String tmpPath, final Map details) { + final String remoteUrl, String username, String password, String tmpPath, final Map details) throws ResourceAllocationException { UserVm userVm = null; Map allDetails = new HashMap<>(details); @@ -2475,6 +2455,7 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("No attached disks found for the unmanaged VM: %s", instanceName)); } + DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); Pair> rootAndDataDisksPair = getRootAndDataDisks(unmanagedInstanceDisks, dataDiskOfferingMap); final UnmanagedInstanceTO.Disk rootDisk = rootAndDataDisksPair.first(); final List dataDisks = rootAndDataDisksPair.second(); @@ -2483,6 +2464,10 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag } allDetails.put(VmDetailConstants.ROOT_DISK_CONTROLLER, rootDisk.getController()); + List reservations = new ArrayList<>(); + try { + checkVolumeResourceLimitsForExternalKvmVmImport(owner, rootDisk, dataDisks, diskOffering, dataDiskOfferingMap, reservations); + // Check NICs and supplied networks Map nicIpAddressMap = getNicIpAddresses(unmanagedInstance.getNics(), callerNicIpAddressMap); Map allNicNetworkMap = getUnmanagedNicNetworkMap(unmanagedInstance.getName(), unmanagedInstance.getNics(), nicNetworkMap, nicIpAddressMap, zone, hostName, owner, Hypervisor.HypervisorType.KVM); @@ -2503,16 +2488,12 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag if (userVm == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); } - DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); String rootVolumeName = String.format("ROOT-%s", userVm.getId()); DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null); DiskProfile[] dataDiskProfiles = new DiskProfile[dataDisks.size()]; int diskSeq = 0; for (UnmanagedInstanceTO.Disk disk : dataDisks) { - if (disk.getCapacity() == null || disk.getCapacity() == 0) { - throw new InvalidParameterValueException(String.format("Disk ID: %s size is invalid", disk.getDiskId())); - } DiskOffering offering = diskOfferingDao.findById(dataDiskOfferingMap.get(disk.getDiskId())); DiskProfile dataDiskProfile = volumeManager.allocateRawVolume(Volume.Type.DATADISK, String.format("DATA-%d-%s", userVm.getId(), disk.getDiskId()), offering, null, null, null, userVm, template, owner, null); dataDiskProfiles[diskSeq++] = dataDiskProfile; @@ -2537,10 +2518,6 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag List> diskProfileStoragePoolList = new ArrayList<>(); try { - if (rootDisk.getCapacity() == null || rootDisk.getCapacity() == 0) { - throw new InvalidParameterValueException(String.format("Root disk ID: %s size is invalid", rootDisk.getDiskId())); - } - diskProfileStoragePoolList.add(importExternalDisk(rootDisk, userVm, dest, diskOffering, Volume.Type.ROOT, template, null, remoteUrl, username, password, tmpPath, diskProfile)); @@ -2574,6 +2551,30 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag } publishVMUsageUpdateResourceCount(userVm, dummyOffering, template); return userVm; + + } finally { + ReservationHelper.closeAll(reservations); + } + } + + protected void checkVolumeResourceLimitsForExternalKvmVmImport(Account owner, UnmanagedInstanceTO.Disk rootDisk, + List dataDisks, DiskOfferingVO rootDiskOffering, + Map dataDiskOfferingMap, List reservations) throws ResourceAllocationException { + if (rootDisk.getCapacity() == null || rootDisk.getCapacity() == 0) { + throw new InvalidParameterValueException(String.format("Root disk ID: %s size is invalid", rootDisk.getDiskId())); + } + resourceLimitService.checkVolumeResourceLimit(owner, true, rootDisk.getCapacity(), rootDiskOffering, reservations); + + if (CollectionUtils.isEmpty(dataDisks)) { + return; + } + for (UnmanagedInstanceTO.Disk disk : dataDisks) { + if (disk.getCapacity() == null || disk.getCapacity() == 0) { + throw new InvalidParameterValueException(String.format("Data disk ID: %s size is invalid", disk.getDiskId())); + } + DiskOffering offering = diskOfferingDao.findById(dataDiskOfferingMap.get(disk.getDiskId())); + resourceLimitService.checkVolumeResourceLimit(owner, true, disk.getCapacity(), offering, reservations); + } } private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, final String instanceName, final DataCenter zone, @@ -2641,7 +2642,16 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, if (userVm == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); } + DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); + + List reservations = new ArrayList<>(); + List resourceLimitStorageTags = resourceLimitService.getResourceLimitStorageTagsForResourceCountOperation(true, diskOffering); + try { + CheckedReservation volumeReservation = new CheckedReservation(owner, Resource.ResourceType.volume, resourceLimitStorageTags, + CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? 1L : 0L, reservationDao, resourceLimitService); + reservations.add(volumeReservation); + String rootVolumeName = String.format("ROOT-%s", userVm.getId()); DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null); @@ -2686,6 +2696,14 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, throw new CloudRuntimeException("Disk not found or is invalid"); } diskProfile.setSize(checkVolumeAnswer.getSize()); + try { + CheckedReservation primaryStorageReservation = new CheckedReservation(owner, Resource.ResourceType.primary_storage, resourceLimitStorageTags, + CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? diskProfile.getSize() : 0L, reservationDao, resourceLimitService); + reservations.add(primaryStorageReservation); + } catch (ResourceAllocationException e) { + cleanupFailedImportVM(userVm); + throw e; + } List> diskProfileStoragePoolList = new ArrayList<>(); try { @@ -2705,6 +2723,10 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, networkOrchestrationService.importNic(macAddress, 0, network, true, userVm, requestedIpPair, zone, true); publishVMUsageUpdateResourceCount(userVm, dummyOffering, template); return userVm; + + } finally { + ReservationHelper.closeAll(reservations); + } } private void checkVolume(Map volumeDetails) { diff --git a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java index e3bfdc636358..bdc6620c4904 100644 --- a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java +++ b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.api.response.TaggedResourceLimitAndCountResponse; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -40,6 +41,7 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; @@ -264,6 +266,7 @@ public void testGetResourceLimitStorageTags1() { @Test public void testCheckVmResourceLimit() { + List reservations = new ArrayList<>(); ServiceOffering serviceOffering = Mockito.mock(ServiceOffering.class); VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); Mockito.when(serviceOffering.getHostTag()).thenReturn(hostTags.get(0)); @@ -271,53 +274,12 @@ public void testCheckVmResourceLimit() { Mockito.when(serviceOffering.getRamSize()).thenReturn(256); Mockito.when(template.getTemplateTag()).thenReturn(hostTags.get(0)); Account account = Mockito.mock(Account.class); - try { - Mockito.doNothing().when(resourceLimitManager).checkResourceLimitWithTag(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - resourceLimitManager.checkVmResourceLimit(account, true, serviceOffering, template); + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { + resourceLimitManager.checkVmResourceLimit(account, true, serviceOffering, template, reservations); List tags = new ArrayList<>(); tags.add(null); tags.add(hostTags.get(0)); - for (String tag: tags) { - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag(account, Resource.ResourceType.user_vm, tag); - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag(account, Resource.ResourceType.cpu, tag, 2L); - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag(account, Resource.ResourceType.memory, tag, 256L); - } - } catch (ResourceAllocationException e) { - Assert.fail("Exception encountered: " + e.getMessage()); - } - } - - @Test - public void testCheckVmCpuResourceLimit() { - ServiceOffering serviceOffering = Mockito.mock(ServiceOffering.class); - VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); - Mockito.when(serviceOffering.getHostTag()).thenReturn(hostTags.get(0)); - Mockito.when(template.getTemplateTag()).thenReturn(hostTags.get(0)); - Account account = Mockito.mock(Account.class); - long cpu = 2L; - try { - Mockito.doNothing().when(resourceLimitManager).checkResourceLimitWithTag(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - resourceLimitManager.checkVmCpuResourceLimit(account, true, serviceOffering, template, cpu); - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag(account, Resource.ResourceType.cpu, null, cpu); - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag(account, Resource.ResourceType.cpu, hostTags.get(0), cpu); - } catch (ResourceAllocationException e) { - Assert.fail("Exception encountered: " + e.getMessage()); - } - } - - @Test - public void testCheckVmMemoryResourceLimit() { - ServiceOffering serviceOffering = Mockito.mock(ServiceOffering.class); - VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); - Mockito.when(serviceOffering.getHostTag()).thenReturn(hostTags.get(0)); - Mockito.when(template.getTemplateTag()).thenReturn(hostTags.get(0)); - Account account = Mockito.mock(Account.class); - long delta = 256L; - try { - Mockito.doNothing().when(resourceLimitManager).checkResourceLimitWithTag(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - resourceLimitManager.checkVmMemoryResourceLimit(account, true, serviceOffering, template, delta); - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag(account, Resource.ResourceType.memory, null, delta); - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag(account, Resource.ResourceType.memory, hostTags.get(0), delta); + Assert.assertEquals(3, mockCheckedReservation.constructed().size()); } catch (ResourceAllocationException e) { Assert.fail("Exception encountered: " + e.getMessage()); } @@ -325,21 +287,15 @@ public void testCheckVmMemoryResourceLimit() { @Test public void testCheckVolumeResourceLimit() { + List reservations = new ArrayList<>(); String checkTag = storageTags.get(0); DiskOffering diskOffering = Mockito.mock(DiskOffering.class); Mockito.when(diskOffering.getTags()).thenReturn(checkTag); Mockito.when(diskOffering.getTagsArray()).thenReturn(new String[]{checkTag}); Account account = Mockito.mock(Account.class); - try { - Mockito.doNothing().when(resourceLimitManager).checkResourceLimitWithTag(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - resourceLimitManager.checkVolumeResourceLimit(account, true, 100L, diskOffering); - List tags = new ArrayList<>(); - tags.add(null); - tags.add(checkTag); - for (String tag: tags) { - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag(account, Resource.ResourceType.volume, tag); - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag(account, Resource.ResourceType.primary_storage, tag, 100L); - } + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { + resourceLimitManager.checkVolumeResourceLimit(account, true, 100L, diskOffering, reservations); + Assert.assertEquals(2, reservations.size()); } catch (ResourceAllocationException e) { Assert.fail("Exception encountered: " + e.getMessage()); } @@ -934,13 +890,6 @@ public void updateTaggedResourceLimitsAndCountsForDomains() { Mockito.anyList(), Mockito.eq(tag)); } - private void mockCheckResourceLimitWithTag() throws ResourceAllocationException { - Mockito.doNothing().when(resourceLimitManager).checkResourceLimitWithTag( - Mockito.any(Account.class), Mockito.any(Resource.ResourceType.class), Mockito.anyString()); - Mockito.doNothing().when(resourceLimitManager).checkResourceLimitWithTag( - Mockito.any(Account.class), Mockito.any(Resource.ResourceType.class), Mockito.anyString(), Mockito.anyLong()); - } - private void mockIncrementResourceCountWithTag() { Mockito.doNothing().when(resourceLimitManager).incrementResourceCountWithTag( Mockito.anyLong(), Mockito.any(Resource.ResourceType.class), Mockito.anyString()); @@ -957,6 +906,7 @@ private void mockDecrementResourceCountWithTag() { @Test public void testCheckVolumeResourceCount() throws ResourceAllocationException { + List reservations = new ArrayList<>(); Account account = Mockito.mock(Account.class); String tag = "tag"; long delta = 10L; @@ -970,12 +920,11 @@ public void testCheckVolumeResourceCount() throws ResourceAllocationException { Mockito.doReturn(List.of(tag)).when(resourceLimitManager) .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); - mockCheckResourceLimitWithTag(); - resourceLimitManager.checkVolumeResourceLimit(account, false, delta, Mockito.mock(DiskOffering.class)); - Mockito.verify(resourceLimitManager, Mockito.times(1)).checkResourceLimitWithTag( - account, Resource.ResourceType.volume, tag); - Mockito.verify(resourceLimitManager, Mockito.times(1)) - .checkResourceLimitWithTag(account, Resource.ResourceType.primary_storage, tag, 10L); + + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { + resourceLimitManager.checkVolumeResourceLimit(account, false, delta, Mockito.mock(DiskOffering.class), reservations); + Assert.assertEquals(2, reservations.size()); + } } @Test diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index 6c25e69876fe..5a81d7d8ce32 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -26,7 +26,6 @@ import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -41,6 +40,7 @@ import java.util.UUID; import java.util.concurrent.ExecutionException; +import com.cloud.resourcelimit.CheckedReservation; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd; @@ -83,6 +83,7 @@ import org.mockito.InOrder; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.Spy; @@ -91,7 +92,6 @@ import com.cloud.api.query.dao.ServiceOfferingJoinDao; import com.cloud.configuration.ConfigurationManager; -import com.cloud.configuration.Resource; import com.cloud.configuration.Resource.ResourceType; import com.cloud.dc.ClusterVO; import com.cloud.dc.DataCenterVO; @@ -545,7 +545,9 @@ public void attachRootInUploadedState() throws NoSuchFieldException, IllegalAcce @Test public void attachRootVolumePositive() throws NoSuchFieldException, IllegalAccessException { thrown.expect(NullPointerException.class); - volumeApiServiceImpl.attachVolumeToVM(2L, 6L, 0L, false); + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { + volumeApiServiceImpl.attachVolumeToVM(2L, 6L, 0L, false); + } } // Negative test - attach data volume, to the vm on non-kvm hypervisor @@ -564,7 +566,9 @@ public void attachDiskWithEncryptEnabledOfferingOnKVM() throws NoSuchFieldExcept DiskOfferingVO diskOffering = Mockito.mock(DiskOfferingVO.class); when(diskOffering.getEncrypt()).thenReturn(true); when(_diskOfferingDao.findById(anyLong())).thenReturn(diskOffering); - volumeApiServiceImpl.attachVolumeToVM(4L, 10L, 1L, false); + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { + volumeApiServiceImpl.attachVolumeToVM(4L, 10L, 1L, false); + } } // volume not Ready @@ -649,9 +653,7 @@ public void testAllocSnapshotNonManagedStorageArchive() { * The resource limit check for primary storage should not be skipped for Volume in 'Uploaded' state. */ @Test - public void testResourceLimitCheckForUploadedVolume() throws NoSuchFieldException, IllegalAccessException, ResourceAllocationException { - doThrow(new ResourceAllocationException("primary storage resource limit check failed", Resource.ResourceType.primary_storage)).when(resourceLimitServiceMock) - .checkResourceLimit(any(AccountVO.class), any(Resource.ResourceType.class), any(Long.class)); + public void testAttachVolumeToVMPerformsResourceReservation() throws NoSuchFieldException, IllegalAccessException, ResourceAllocationException { UserVmVO vm = Mockito.mock(UserVmVO.class); AccountVO acc = Mockito.mock(AccountVO.class); VolumeInfo volumeToAttach = Mockito.mock(VolumeInfo.class); @@ -672,10 +674,10 @@ public void testResourceLimitCheckForUploadedVolume() throws NoSuchFieldExceptio DataCenterVO zoneWithDisabledLocalStorage = Mockito.mock(DataCenterVO.class); when(_dcDao.findById(anyLong())).thenReturn(zoneWithDisabledLocalStorage); when(zoneWithDisabledLocalStorage.isLocalStorageEnabled()).thenReturn(true); - try { + doReturn(volumeVoMock).when(volumeApiServiceImpl).getVolumeAttachJobResult(Mockito.any(), Mockito.any(), Mockito.any()); + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { volumeApiServiceImpl.attachVolumeToVM(2L, 9L, null, false); - } catch (InvalidParameterValueException e) { - Assert.assertEquals(e.getMessage(), ("primary storage resource limit check failed")); + Assert.assertEquals(1, mockCheckedReservation.constructed().size()); } } @@ -2199,4 +2201,34 @@ public void testCreateVolumeOnSecondaryForAttachIfNeeded_NoSuitablePool_ReturnSa Assert.fail(); } } + + @Test + public void getRequiredPrimaryStorageSizeForVolumeAttachTestTagsAreEmptyReturnsZero() { + List tags = new ArrayList<>(); + + Long result = volumeApiServiceImpl.getRequiredPrimaryStorageSizeForVolumeAttach(tags, volumeInfoMock); + + Assert.assertEquals(0L, (long) result); + } + + @Test + public void getRequiredPrimaryStorageSizeForVolumeAttachTestVolumeIsReadyReturnsZero() { + List tags = List.of("tag1", "tag2"); + Mockito.doReturn(Volume.State.Ready).when(volumeInfoMock).getState(); + + Long result = volumeApiServiceImpl.getRequiredPrimaryStorageSizeForVolumeAttach(tags, volumeInfoMock); + + Assert.assertEquals(0L, (long) result); + } + + @Test + public void getRequiredPrimaryStorageSizeForVolumeAttachTestTagsAreNotEmptyAndVolumeIsUploadedReturnsVolumeSize() { + List tags = List.of("tag1", "tag2"); + Mockito.doReturn(Volume.State.Uploaded).when(volumeInfoMock).getState(); + Mockito.doReturn(2L).when(volumeInfoMock).getSize(); + + Long result = volumeApiServiceImpl.getRequiredPrimaryStorageSizeForVolumeAttach(tags, volumeInfoMock); + + Assert.assertEquals(2L, (long) result); + } } diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index 06fb65921c3a..3ef304f4ec70 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -42,6 +42,7 @@ import java.util.Map; import com.cloud.network.NetworkService; +import com.cloud.resourcelimit.CheckedReservation; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.BaseCmd.HTTPMethod; @@ -54,6 +55,7 @@ import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.template.VnfTemplateManager; @@ -65,6 +67,7 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; @@ -609,7 +612,6 @@ private void configureDoNothingForMethodsThatWeDoNotWantToTest() throws Resource Mockito.doNothing().when(userVmManagerImpl).validateOldAndNewAccounts(Mockito.nullable(Account.class), Mockito.nullable(Account.class), Mockito.anyLong(), Mockito.nullable(String.class), Mockito.nullable(Long.class)); Mockito.doNothing().when(userVmManagerImpl).validateIfVmHasNoRules(Mockito.any(), Mockito.anyLong()); Mockito.doNothing().when(userVmManagerImpl).removeInstanceFromInstanceGroup(Mockito.anyLong()); - Mockito.doNothing().when(userVmManagerImpl).verifyResourceLimitsForAccountAndStorage(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyList(), Mockito.any()); Mockito.doNothing().when(userVmManagerImpl).validateIfNewOwnerHasAccessToTemplate(Mockito.any(), Mockito.any(), Mockito.any()); Mockito.doNothing().when(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); @@ -1615,23 +1617,11 @@ public void testCheckVolumesLimits() { Mockito.when(vol5.isDisplay()).thenReturn(true); List volumes = List.of(vol1, undisplayedVolume, vol3, vol4, vol5); - Long size = volumes.stream().filter(VolumeVO::isDisplay).mapToLong(VolumeVO::getSize).sum(); - try { - userVmManagerImpl.checkVolumesLimits(account, volumes); - Mockito.verify(resourceLimitMgr, Mockito.times(1)) - .checkResourceLimit(account, Resource.ResourceType.volume, 4); - Mockito.verify(resourceLimitMgr, Mockito.times(1)) - .checkResourceLimit(account, Resource.ResourceType.primary_storage, size); - Mockito.verify(resourceLimitMgr, Mockito.times(1)) - .checkResourceLimitWithTag(account, Resource.ResourceType.volume, "tag1", 2); - Mockito.verify(resourceLimitMgr, Mockito.times(1)) - .checkResourceLimitWithTag(account, Resource.ResourceType.volume, "tag2", 3); - Mockito.verify(resourceLimitMgr, Mockito.times(1)) - .checkResourceLimitWithTag(account, Resource.ResourceType.primary_storage, "tag1", - vol1.getSize() + vol5.getSize()); - Mockito.verify(resourceLimitMgr, Mockito.times(1)) - .checkResourceLimitWithTag(account, Resource.ResourceType.primary_storage, "tag2", - vol1.getSize() + vol3.getSize() + vol5.getSize()); + List reservations = new ArrayList<>(); + + try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { + userVmManagerImpl.checkVolumesLimits(account, volumes, reservations); + Assert.assertEquals(8, reservations.size()); } catch (ResourceAllocationException e) { Assert.fail(e.getMessage()); } @@ -1922,26 +1912,26 @@ public void validateIfVmHasNoRulesTestOneToOneNatRulesDoNotExistDoesNotThrowInva @Test public void verifyResourceLimitsForAccountAndStorageTestCountOnlyRunningVmsInResourceLimitationIsTrueDoesNotCallVmResourceLimitCheck() throws ResourceAllocationException { + List reservations = new ArrayList<>(); LinkedList volumeVoList = new LinkedList(); Mockito.doReturn(true).when(userVmManagerImpl).countOnlyRunningVmsInResourceLimitation(); - userVmManagerImpl.verifyResourceLimitsForAccountAndStorage(accountMock, userVmVoMock, serviceOfferingVoMock, volumeVoList, virtualMachineTemplateMock); + userVmManagerImpl.verifyResourceLimitsForAccountAndStorage(accountMock, userVmVoMock, serviceOfferingVoMock, volumeVoList, virtualMachineTemplateMock, reservations); - Mockito.verify(resourceLimitMgr, Mockito.never()).checkVmResourceLimit(Mockito.any(), Mockito.anyBoolean(), Mockito.any(), Mockito.any()); - Mockito.verify(resourceLimitMgr).checkResourceLimit(accountMock, Resource.ResourceType.volume, 0l); - Mockito.verify(resourceLimitMgr).checkResourceLimit(accountMock, Resource.ResourceType.primary_storage, 0l); + Mockito.verify(resourceLimitMgr, Mockito.never()).checkVmResourceLimit(Mockito.any(), Mockito.anyBoolean(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.verify(resourceLimitMgr, Mockito.never()).checkVolumeResourceLimit(Mockito.any(), Mockito.anyBoolean(), Mockito.any(), Mockito.any(), Mockito.any()); } @Test public void verifyResourceLimitsForAccountAndStorageTestCountOnlyRunningVmsInResourceLimitationIsFalseCallsVmResourceLimitCheck() throws ResourceAllocationException { + List reservations = new ArrayList<>(); LinkedList volumeVoList = new LinkedList(); Mockito.doReturn(false).when(userVmManagerImpl).countOnlyRunningVmsInResourceLimitation(); - userVmManagerImpl.verifyResourceLimitsForAccountAndStorage(accountMock, userVmVoMock, serviceOfferingVoMock, volumeVoList, virtualMachineTemplateMock); + userVmManagerImpl.verifyResourceLimitsForAccountAndStorage(accountMock, userVmVoMock, serviceOfferingVoMock, volumeVoList, virtualMachineTemplateMock, reservations); - Mockito.verify(resourceLimitMgr).checkVmResourceLimit(Mockito.any(), Mockito.anyBoolean(), Mockito.any(), Mockito.any()); - Mockito.verify(resourceLimitMgr).checkResourceLimit(accountMock, Resource.ResourceType.volume, 0l); - Mockito.verify(resourceLimitMgr).checkResourceLimit(accountMock, Resource.ResourceType.primary_storage, 0l); + Mockito.verify(resourceLimitMgr).checkVmResourceLimit(Mockito.any(), Mockito.anyBoolean(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.verify(userVmManagerImpl).checkVolumesLimits(Mockito.any(), Mockito.any(), Mockito.any()); } @Test @@ -2986,7 +2976,7 @@ public void moveVmToUserTestVerifyResourceLimitsForAccountAndStorageThrowsResour configureDoNothingForMethodsThatWeDoNotWantToTest(); Mockito.doThrow(ResourceAllocationException.class).when(userVmManagerImpl).verifyResourceLimitsForAccountAndStorage(Mockito.any(), Mockito.any(), - Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Assert.assertThrows(ResourceAllocationException.class, () -> userVmManagerImpl.moveVmToUser(assignVmCmdMock)); } diff --git a/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java b/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java index 8d48fc4dac5b..01512b448b25 100644 --- a/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java +++ b/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java @@ -357,13 +357,13 @@ public void testUpdateUserVmServiceOfferingDifferentServiceOffering() throws Con _vmSnapshotMgr.updateUserVmServiceOffering(userVm, vmSnapshotVO); verify(_vmSnapshotMgr).changeUserVmServiceOffering(userVm, vmSnapshotVO); - verify(_vmSnapshotMgr).getVmMapDetails(userVm); + verify(_vmSnapshotMgr).getVmMapDetails(vmSnapshotVO); verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(eq(userVm), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); } @Test public void testGetVmMapDetails() { - Map result = _vmSnapshotMgr.getVmMapDetails(userVm); + Map result = _vmSnapshotMgr.getVmMapDetails(vmSnapshotVO); assert(result.containsKey(userVmDetailCpuNumber.getName())); assert(result.containsKey(userVmDetailMemory.getName())); assertEquals(userVmDetails.size(), result.size()); @@ -375,7 +375,7 @@ public void testGetVmMapDetails() { public void testChangeUserVmServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { when(_userVmManager.upgradeVirtualMachine(eq(TEST_VM_ID), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); _vmSnapshotMgr.changeUserVmServiceOffering(userVm, vmSnapshotVO); - verify(_vmSnapshotMgr).getVmMapDetails(userVm); + verify(_vmSnapshotMgr).getVmMapDetails(vmSnapshotVO); verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(eq(userVm), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); } @@ -383,7 +383,7 @@ public void testChangeUserVmServiceOffering() throws ConcurrentOperationExceptio public void testChangeUserVmServiceOfferingFailOnUpgradeVMServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { when(_userVmManager.upgradeVirtualMachine(eq(TEST_VM_ID), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(false); _vmSnapshotMgr.changeUserVmServiceOffering(userVm, vmSnapshotVO); - verify(_vmSnapshotMgr).getVmMapDetails(userVm); + verify(_vmSnapshotMgr).getVmMapDetails(vmSnapshotVO); verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(eq(userVm), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); } diff --git a/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java index 045f21785295..2ac4d6c862ce 100644 --- a/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java @@ -24,6 +24,7 @@ import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.resourcelimit.Reserver; import org.springframework.stereotype.Component; import com.cloud.configuration.Resource.ResourceType; @@ -273,7 +274,7 @@ public List getResourceLimitStorageTags(DiskOffering diskOffering) { } @Override - public void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering) throws ResourceAllocationException { + public void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException { } @@ -284,13 +285,13 @@ public List getResourceLimitStorageTagsForResourceCountOperation(Boolean @Override public void checkVolumeResourceLimitForDiskOfferingChange(Account owner, Boolean display, Long currentSize, Long newSize, - DiskOffering currentOffering, DiskOffering newOffering) throws ResourceAllocationException { + DiskOffering currentOffering, DiskOffering newOffering, List reservations) throws ResourceAllocationException { } @Override public void checkPrimaryStorageResourceLimit(Account owner, Boolean display, Long size, - DiskOffering diskOffering) { + DiskOffering diskOffering, List reservations) { } @@ -334,7 +335,7 @@ public void decrementVolumePrimaryStorageResourceCount(long accountId, Boolean d } @Override - public void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template) throws ResourceAllocationException { + public void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException { } @@ -351,19 +352,14 @@ public void decrementVmResourceCount(long accountId, Boolean display, ServiceOff @Override public void checkVmResourceLimitsForServiceOfferingChange(Account owner, Boolean display, Long currentCpu, Long newCpu, Long currentMemory, Long newMemory, ServiceOffering currentOffering, ServiceOffering newOffering, - VirtualMachineTemplate template) throws ResourceAllocationException { + VirtualMachineTemplate template, List reservations) throws ResourceAllocationException { } @Override public void checkVmResourceLimitsForTemplateChange(Account owner, Boolean display, ServiceOffering offering, VirtualMachineTemplate currentTemplate, - VirtualMachineTemplate newTemplate) throws ResourceAllocationException { - - } - - @Override - public void checkVmCpuResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu) throws ResourceAllocationException { + VirtualMachineTemplate newTemplate, List reservations) throws ResourceAllocationException { } @@ -377,11 +373,6 @@ public void decrementVmCpuResourceCount(long accountId, Boolean display, Service } - @Override - public void checkVmMemoryResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory) throws ResourceAllocationException { - - } - @Override public void incrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory) { diff --git a/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java index 419acc0ca0b6..f3947a75e6e1 100644 --- a/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java @@ -268,9 +268,6 @@ public void testImportVolumeAllGood() throws ResourceAllocationException { doNothing().when(volumeImportUnmanageManager).checkIfVolumeIsEncrypted(volumeOnStorageTO); doNothing().when(volumeImportUnmanageManager).checkIfVolumeHasBackingFile(volumeOnStorageTO); - doNothing().when(resourceLimitService).checkResourceLimit(account, Resource.ResourceType.volume); - doNothing().when(resourceLimitService).checkResourceLimit(account, Resource.ResourceType.primary_storage, virtualSize); - DiskOfferingVO diskOffering = mock(DiskOfferingVO.class); when(diskOffering.isCustomized()).thenReturn(true); doReturn(diskOffering).when(volumeImportUnmanageManager).getOrCreateDiskOffering(account, diskOfferingId, zoneId, isLocal); diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index 09f62f7a049a..282f3fb3a700 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -38,6 +38,7 @@ import java.util.UUID; import com.cloud.offering.DiskOffering; +import com.cloud.resourcelimit.CheckedReservation; import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ServerApiException; @@ -67,6 +68,7 @@ import org.mockito.BDDMockito; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -89,7 +91,6 @@ import com.cloud.agent.api.ImportConvertedInstanceAnswer; import com.cloud.agent.api.ImportConvertedInstanceCommand; import com.cloud.agent.api.to.DataStoreTO; -import com.cloud.configuration.Resource; import com.cloud.dc.ClusterVO; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; @@ -106,7 +107,6 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.PermissionDeniedException; -import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.UnsupportedServiceException; import com.cloud.host.Host; import com.cloud.host.HostVO; @@ -282,7 +282,6 @@ public void setUp() throws Exception { clusterVO.setHypervisorType(Hypervisor.HypervisorType.VMware.toString()); when(clusterDao.findById(anyLong())).thenReturn(clusterVO); when(configurationDao.getValue(Mockito.anyString())).thenReturn(null); - doNothing().when(resourceLimitService).checkResourceLimit(any(Account.class), any(Resource.ResourceType.class), anyLong()); List hosts = new ArrayList<>(); HostVO hostVO = Mockito.mock(HostVO.class); when(hostVO.isInMaintenanceStates()).thenReturn(false); @@ -422,7 +421,8 @@ public void importUnmanagedInstanceTest() { when(importUnmanageInstanceCmd.getName()).thenReturn("TestInstance"); when(importUnmanageInstanceCmd.getDomainId()).thenReturn(null); when(volumeApiService.doesStoragePoolSupportDiskOffering(any(StoragePool.class), any())).thenReturn(true); - try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class)) { + try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class); + MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { unmanagedVMsManager.importUnmanagedInstance(importUnmanageInstanceCmd); } } @@ -520,7 +520,8 @@ public void testImportFromExternalTest() throws InsufficientServerCapacityExcept CopyRemoteVolumeAnswer copyAnswer = Mockito.mock(CopyRemoteVolumeAnswer.class); when(copyAnswer.getResult()).thenReturn(true); when(agentManager.easySend(anyLong(), any(CopyRemoteVolumeCommand.class))).thenReturn(copyAnswer); - try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class)) { + try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class); + MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { unmanagedVMsManager.importVm(cmd); } } @@ -707,7 +708,8 @@ private void baseTestImportVmFromVmwareToKvm(VcenterParameter vcenterParameter, Mockito.lenient().when(agentManager.send(Mockito.eq(convertHostId), Mockito.any(ImportConvertedInstanceCommand.class))).thenReturn(convertImportedInstanceAnswer); } - try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class)) { + try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class); + MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { unmanagedVMsManager.importVm(importVmCmd); verify(vmwareGuru).getHypervisorVMOutOfBandAndCloneIfRequired(Mockito.eq(host), Mockito.eq(vmName), anyMap()); verify(vmwareGuru).createVMTemplateOutOfBand(Mockito.eq(host), Mockito.eq(vmName), anyMap(), any(DataStoreTO.class), anyInt()); @@ -760,7 +762,8 @@ private void importFromDisk(String source) throws InsufficientServerCapacityExce when(volumeApiService.doesStoragePoolSupportDiskOffering(any(StoragePool.class), any())).thenReturn(true); StoragePoolHostVO storagePoolHost = Mockito.mock(StoragePoolHostVO.class); when(storagePoolHostDao.findByPoolHost(anyLong(), anyLong())).thenReturn(storagePoolHost); - try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class)) { + try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class); + MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { unmanagedVMsManager.importVm(cmd); } } @@ -1107,40 +1110,4 @@ public void testSelectKVMHostForConversionInClusterWithImportInstanceIdInvalidHo unmanagedVMsManager.selectKVMHostForConversionInCluster(cluster, hostId); } - - @Test - public void testCheckUnmanagedDiskLimits() { - Account owner = Mockito.mock(Account.class); - UnmanagedInstanceTO.Disk disk = Mockito.mock(UnmanagedInstanceTO.Disk.class); - Mockito.when(disk.getDiskId()).thenReturn("disk1"); - Mockito.when(disk.getCapacity()).thenReturn(100L); - ServiceOffering serviceOffering = Mockito.mock(ServiceOffering.class); - Mockito.when(serviceOffering.getDiskOfferingId()).thenReturn(1L); - UnmanagedInstanceTO.Disk dataDisk = Mockito.mock(UnmanagedInstanceTO.Disk.class); - Mockito.when(dataDisk.getDiskId()).thenReturn("disk2"); - Mockito.when(dataDisk.getCapacity()).thenReturn(1000L); - Map dataDiskMap = new HashMap<>(); - dataDiskMap.put("disk2", 2L); - DiskOfferingVO offering1 = Mockito.mock(DiskOfferingVO.class); - Mockito.when(diskOfferingDao.findById(1L)).thenReturn(offering1); - String tag1 = "tag1"; - Mockito.when(resourceLimitService.getResourceLimitStorageTags(offering1)).thenReturn(List.of(tag1)); - DiskOfferingVO offering2 = Mockito.mock(DiskOfferingVO.class); - Mockito.when(diskOfferingDao.findById(2L)).thenReturn(offering2); - String tag2 = "tag2"; - Mockito.when(resourceLimitService.getResourceLimitStorageTags(offering2)).thenReturn(List.of(tag2)); - try { - Mockito.doNothing().when(resourceLimitService).checkResourceLimit(any(), any(), any()); - Mockito.doNothing().when(resourceLimitService).checkResourceLimitWithTag(any(), any(), any(), any()); - unmanagedVMsManager.checkUnmanagedDiskLimits(owner, disk, serviceOffering, List.of(dataDisk), dataDiskMap); - Mockito.verify(resourceLimitService, Mockito.times(1)).checkResourceLimit(owner, Resource.ResourceType.volume, 2); - Mockito.verify(resourceLimitService, Mockito.times(1)).checkResourceLimit(owner, Resource.ResourceType.primary_storage, 1100L); - Mockito.verify(resourceLimitService, Mockito.times(1)).checkResourceLimitWithTag(owner, Resource.ResourceType.volume, tag1,1); - Mockito.verify(resourceLimitService, Mockito.times(1)).checkResourceLimitWithTag(owner, Resource.ResourceType.volume, tag2,1); - Mockito.verify(resourceLimitService, Mockito.times(1)).checkResourceLimitWithTag(owner, Resource.ResourceType.primary_storage, tag1,100L); - Mockito.verify(resourceLimitService, Mockito.times(1)).checkResourceLimitWithTag(owner, Resource.ResourceType.primary_storage, tag2,1000L); - } catch (ResourceAllocationException e) { - Assert.fail("Exception encountered: " + e.getMessage()); - } - } } From 08bb37a566eab9c9eda1de3f2235290413851df8 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Sun, 8 Mar 2026 13:25:27 -0300 Subject: [PATCH 016/205] Cleanup imported VM from disk on failure due to volume allocation + prevent duplicate volume and primary storage increment on import --- .../service/VolumeOrchestrationService.java | 2 +- .../cloud/vm/VirtualMachineManagerImpl.java | 6 +++--- .../orchestration/VolumeOrchestrator.java | 4 ++-- .../vm/UnmanagedVMsManagerImpl.java | 21 +++++++++---------- .../vm/UnmanagedVMsManagerImplTest.java | 4 ++-- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java index 7950dda4d68e..84e110a8940e 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java @@ -107,7 +107,7 @@ VolumeInfo moveVolume(VolumeInfo volume, long destPoolDcId, Long destPoolPodId, void destroyVolume(Volume volume); DiskProfile allocateRawVolume(Type type, String name, DiskOffering offering, Long size, Long minIops, Long maxIops, VirtualMachine vm, VirtualMachineTemplate template, - Account owner, Long deviceId); + Account owner, Long deviceId, boolean incrementResourceCount); VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo volume, HypervisorType rootDiskHyperType, StoragePool storagePool) throws NoTransitionException; diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 2dcb8fa20594..74ff3801b6a3 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -536,7 +536,7 @@ public void allocate(final String vmInstanceName, final VirtualMachineTemplate t if (dataDiskOfferings != null) { for (final DiskOfferingInfo dataDiskOfferingInfo : dataDiskOfferings) { volumeMgr.allocateRawVolume(Type.DATADISK, "DATA-" + persistedVm.getId(), dataDiskOfferingInfo.getDiskOffering(), dataDiskOfferingInfo.getSize(), - dataDiskOfferingInfo.getMinIops(), dataDiskOfferingInfo.getMaxIops(), persistedVm, template, owner, null); + dataDiskOfferingInfo.getMinIops(), dataDiskOfferingInfo.getMaxIops(), persistedVm, template, owner, null, true); } } if (datadiskTemplateToDiskOfferingMap != null && !datadiskTemplateToDiskOfferingMap.isEmpty()) { @@ -546,7 +546,7 @@ public void allocate(final String vmInstanceName, final VirtualMachineTemplate t long diskOfferingSize = diskOffering.getDiskSize() / (1024 * 1024 * 1024); VMTemplateVO dataDiskTemplate = _templateDao.findById(dataDiskTemplateToDiskOfferingMap.getKey()); volumeMgr.allocateRawVolume(Type.DATADISK, "DATA-" + persistedVm.getId() + "-" + String.valueOf(diskNumber), diskOffering, diskOfferingSize, null, null, - persistedVm, dataDiskTemplate, owner, Long.valueOf(diskNumber)); + persistedVm, dataDiskTemplate, owner, Long.valueOf(diskNumber), true); diskNumber++; } } @@ -576,7 +576,7 @@ private void allocateRootVolume(VMInstanceVO vm, VirtualMachineTemplate template String rootVolumeName = String.format("ROOT-%s", vm.getId()); if (template.getFormat() == ImageFormat.ISO) { volumeMgr.allocateRawVolume(Type.ROOT, rootVolumeName, rootDiskOfferingInfo.getDiskOffering(), rootDiskOfferingInfo.getSize(), - rootDiskOfferingInfo.getMinIops(), rootDiskOfferingInfo.getMaxIops(), vm, template, owner, null); + rootDiskOfferingInfo.getMinIops(), rootDiskOfferingInfo.getMaxIops(), vm, template, owner, null, true); } else if (template.getFormat() == ImageFormat.BAREMETAL) { logger.debug("%s has format [{}]. Skipping ROOT volume [{}] allocation.", template.toString(), ImageFormat.BAREMETAL, rootVolumeName); } else { diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index 0fc61d815882..5eef84e354cb 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -835,7 +835,7 @@ protected DiskProfile toDiskProfile(Volume vol, DiskOffering offering) { @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating volume", create = true) @Override public DiskProfile allocateRawVolume(Type type, String name, DiskOffering offering, Long size, Long minIops, Long maxIops, VirtualMachine vm, VirtualMachineTemplate template, Account owner, - Long deviceId) { + Long deviceId, boolean incrementResourceCount) { if (size == null) { size = offering.getDiskSize(); } else { @@ -874,7 +874,7 @@ public DiskProfile allocateRawVolume(Type type, String name, DiskOffering offeri saveVolumeDetails(offering.getId(), vol.getId()); // Save usage event and update resource count for user vm volumes - if (vm.getType() == VirtualMachine.Type.User) { + if (vm.getType() == VirtualMachine.Type.User && incrementResourceCount) { UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_CREATE, vol.getAccountId(), vol.getDataCenterId(), vol.getId(), vol.getName(), offering.getId(), null, size, Volume.class.getName(), vol.getUuid(), vol.isDisplayVolume()); _resourceLimitMgr.incrementVolumeResourceCount(vm.getAccountId(), vol.isDisplayVolume(), vol.getSize(), offering); diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 1917804fb8d5..a32459ed059f 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -2489,13 +2489,13 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); } String rootVolumeName = String.format("ROOT-%s", userVm.getId()); - DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null); + DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null, false); DiskProfile[] dataDiskProfiles = new DiskProfile[dataDisks.size()]; int diskSeq = 0; for (UnmanagedInstanceTO.Disk disk : dataDisks) { DiskOffering offering = diskOfferingDao.findById(dataDiskOfferingMap.get(disk.getDiskId())); - DiskProfile dataDiskProfile = volumeManager.allocateRawVolume(Volume.Type.DATADISK, String.format("DATA-%d-%s", userVm.getId(), disk.getDiskId()), offering, null, null, null, userVm, template, owner, null); + DiskProfile dataDiskProfile = volumeManager.allocateRawVolume(Volume.Type.DATADISK, String.format("DATA-%d-%s", userVm.getId(), disk.getDiskId()), offering, null, null, null, userVm, template, owner, null, false); dataDiskProfiles[diskSeq++] = dataDiskProfile; } @@ -2653,7 +2653,7 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, reservations.add(volumeReservation); String rootVolumeName = String.format("ROOT-%s", userVm.getId()); - DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null); + DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null, false); final VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm, template, serviceOffering, owner, null); ServiceOfferingVO dummyOffering = serviceOfferingDao.findById(userVm.getId(), serviceOffering.getId()); @@ -2696,14 +2696,10 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, throw new CloudRuntimeException("Disk not found or is invalid"); } diskProfile.setSize(checkVolumeAnswer.getSize()); - try { - CheckedReservation primaryStorageReservation = new CheckedReservation(owner, Resource.ResourceType.primary_storage, resourceLimitStorageTags, - CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? diskProfile.getSize() : 0L, reservationDao, resourceLimitService); - reservations.add(primaryStorageReservation); - } catch (ResourceAllocationException e) { - cleanupFailedImportVM(userVm); - throw e; - } + + CheckedReservation primaryStorageReservation = new CheckedReservation(owner, Resource.ResourceType.primary_storage, resourceLimitStorageTags, + CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? diskProfile.getSize() : 0L, reservationDao, resourceLimitService); + reservations.add(primaryStorageReservation); List> diskProfileStoragePoolList = new ArrayList<>(); try { @@ -2724,6 +2720,9 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, publishVMUsageUpdateResourceCount(userVm, dummyOffering, template); return userVm; + } catch (ResourceAllocationException e) { + cleanupFailedImportVM(userVm); + throw e; } finally { ReservationHelper.closeAll(reservations); } diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index 282f3fb3a700..d98eebebd62e 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -508,7 +508,7 @@ public void testImportFromExternalTest() throws InsufficientServerCapacityExcept DeployDestination mockDest = Mockito.mock(DeployDestination.class); when(deploymentPlanningManager.planDeployment(any(), any(), any(), any())).thenReturn(mockDest); DiskProfile diskProfile = Mockito.mock(DiskProfile.class); - when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenReturn(diskProfile); Map storage = new HashMap<>(); VolumeVO volume = Mockito.mock(VolumeVO.class); @@ -743,7 +743,7 @@ private void importFromDisk(String source) throws InsufficientServerCapacityExce DeployDestination mockDest = Mockito.mock(DeployDestination.class); when(deploymentPlanningManager.planDeployment(any(), any(), any(), any())).thenReturn(mockDest); DiskProfile diskProfile = Mockito.mock(DiskProfile.class); - when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenReturn(diskProfile); Map storage = new HashMap<>(); VolumeVO volume = Mockito.mock(VolumeVO.class); From 928dc7dfc0adf2bba07240cb1c10073fc3b2f654 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Sun, 8 Mar 2026 14:08:05 -0300 Subject: [PATCH 017/205] Fix failing tests --- .../org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index d98eebebd62e..d56299126a52 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -508,7 +508,7 @@ public void testImportFromExternalTest() throws InsufficientServerCapacityExcept DeployDestination mockDest = Mockito.mock(DeployDestination.class); when(deploymentPlanningManager.planDeployment(any(), any(), any(), any())).thenReturn(mockDest); DiskProfile diskProfile = Mockito.mock(DiskProfile.class); - when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean())) .thenReturn(diskProfile); Map storage = new HashMap<>(); VolumeVO volume = Mockito.mock(VolumeVO.class); @@ -743,7 +743,7 @@ private void importFromDisk(String source) throws InsufficientServerCapacityExce DeployDestination mockDest = Mockito.mock(DeployDestination.class); when(deploymentPlanningManager.planDeployment(any(), any(), any(), any())).thenReturn(mockDest); DiskProfile diskProfile = Mockito.mock(DiskProfile.class); - when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean())) .thenReturn(diskProfile); Map storage = new HashMap<>(); VolumeVO volume = Mockito.mock(VolumeVO.class); From 3f4f574e5c95349dbf0c3328ea1e737d69374d03 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Tue, 10 Mar 2026 18:33:00 -0300 Subject: [PATCH 018/205] Address public IP limit validations --- .../com/cloud/dc/dao/AccountVlanMapDao.java | 2 +- .../cloud/dc/dao/AccountVlanMapDaoImpl.java | 4 +- .../com/cloud/dc/dao/DomainVlanMapDao.java | 2 +- .../cloud/dc/dao/DomainVlanMapDaoImpl.java | 4 +- .../main/java/com/cloud/api/ApiDBUtils.java | 4 + .../ConfigurationManagerImpl.java | 74 +++++++++++++------ .../com/cloud/network/NetworkServiceImpl.java | 24 +++--- .../resourcelimit/CheckedReservation.java | 6 ++ 8 files changed, 81 insertions(+), 39 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDao.java index 01afd0780f7e..e4047cf7973d 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDao.java @@ -27,6 +27,6 @@ public interface AccountVlanMapDao extends GenericDao { public List listAccountVlanMapsByVlan(long vlanDbId); - public AccountVlanMapVO findAccountVlanMap(long accountId, long vlanDbId); + public AccountVlanMapVO findAccountVlanMap(Long accountId, long vlanDbId); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDaoImpl.java index 12114770f112..0844bb77caa2 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDaoImpl.java @@ -48,9 +48,9 @@ public List listAccountVlanMapsByVlan(long vlanDbId) { } @Override - public AccountVlanMapVO findAccountVlanMap(long accountId, long vlanDbId) { + public AccountVlanMapVO findAccountVlanMap(Long accountId, long vlanDbId) { SearchCriteria sc = AccountVlanSearch.create(); - sc.setParameters("accountId", accountId); + sc.setParametersIfNotNull("accountId", accountId); sc.setParameters("vlanDbId", vlanDbId); return findOneIncludingRemovedBy(sc); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDao.java index 6af16bbace99..d14ccbe86ca6 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDao.java @@ -24,5 +24,5 @@ public interface DomainVlanMapDao extends GenericDao { public List listDomainVlanMapsByDomain(long domainId); public List listDomainVlanMapsByVlan(long vlanDbId); - public DomainVlanMapVO findDomainVlanMap(long domainId, long vlanDbId); + public DomainVlanMapVO findDomainVlanMap(Long domainId, long vlanDbId); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDaoImpl.java index f789721d5fd6..0b4c781349fd 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDaoImpl.java @@ -46,9 +46,9 @@ public List listDomainVlanMapsByVlan(long vlanDbId) { } @Override - public DomainVlanMapVO findDomainVlanMap(long domainId, long vlanDbId) { + public DomainVlanMapVO findDomainVlanMap(Long domainId, long vlanDbId) { SearchCriteria sc = DomainVlanSearch.create(); - sc.setParameters("domainId", domainId); + sc.setParametersIfNotNull("domainId", domainId); sc.setParameters("vlanDbId", vlanDbId); return findOneIncludingRemovedBy(sc); } diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 4ef1b28b9c0e..4ff25472efe9 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -2244,6 +2244,10 @@ public static boolean isAdmin(Account account) { return s_accountService.isAdmin(account.getId()); } + public static Account getSystemAccount() { + return s_accountService.getSystemAccount(); + } + public static List listResourceTagViewByResourceUUID(String resourceUUID, ResourceObjectType resourceType) { return s_tagJoinDao.listBy(resourceUUID, resourceType); } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index eb138bb10b00..8ba8234c1ba8 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -52,6 +52,7 @@ import com.cloud.exception.UnsupportedServiceException; import com.cloud.network.as.AutoScaleManager; +import com.cloud.resourcelimit.CheckedReservation; import com.cloud.user.AccountManagerImpl; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker; @@ -128,6 +129,7 @@ import org.apache.cloudstack.region.Region; import org.apache.cloudstack.region.RegionVO; import org.apache.cloudstack.region.dao.RegionDao; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.cloudstack.resourcedetail.DiskOfferingDetailVO; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; @@ -395,6 +397,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati @Inject ResourceLimitService _resourceLimitMgr; @Inject + ReservationDao reservationDao; + @Inject ProjectManager _projectMgr; @Inject DataStoreManager _dataStoreMgr; @@ -4833,22 +4837,20 @@ public Vlan createVlanAndPublicIpRange(final CreateVlanIpRangeCmd cmd) throws In throw new InvalidParameterValueException("Gateway, netmask and zoneId have to be passed in for virtual and direct untagged networks"); } - if (forVirtualNetwork) { - if (vlanOwner != null) { - - final long accountIpRange = NetUtils.ip2Long(endIP) - NetUtils.ip2Long(startIP) + 1; - - // check resource limits - _resourceLimitMgr.checkResourceLimit(vlanOwner, ResourceType.public_ip, accountIpRange); - } - } // Check if the IP range overlaps with the private ip if (ipv4) { checkOverlapPrivateIpRange(zoneId, startIP, endIP); } - return commitVlan(zoneId, podId, startIP, endIP, newVlanGateway, newVlanNetmask, vlanId, forVirtualNetwork, forSystemVms, networkId, physicalNetworkId, startIPv6, endIPv6, ip6Gateway, - ip6Cidr, domain, vlanOwner, network, sameSubnet, cmd.isForNsx()); + long reservedIpAddressesAmount = 0L; + if (forVirtualNetwork && vlanOwner != null) { + reservedIpAddressesAmount = NetUtils.ip2Long(endIP) - NetUtils.ip2Long(startIP) + 1; + } + + try (CheckedReservation publicIpReservation = new CheckedReservation(vlanOwner, ResourceType.public_ip, null, null, null, reservedIpAddressesAmount, null, reservationDao, _resourceLimitMgr)) { + return commitVlan(zoneId, podId, startIP, endIP, newVlanGateway, newVlanNetmask, vlanId, forVirtualNetwork, forSystemVms, networkId, physicalNetworkId, startIPv6, endIPv6, ip6Gateway, + ip6Cidr, domain, vlanOwner, network, sameSubnet, cmd.isForNsx()); + } } private Network getNetwork(Long networkId) { @@ -5377,7 +5379,7 @@ public Vlan updateVlanAndPublicIpRange(final long id, String startIp, String endIpv6, String ip6Gateway, String ip6Cidr, - Boolean forSystemVms) throws ConcurrentOperationException { + Boolean forSystemVms) throws ConcurrentOperationException, ResourceAllocationException { VlanVO vlanRange = _vlanDao.findById(id); if (vlanRange == null) { @@ -5397,24 +5399,50 @@ public Vlan updateVlanAndPublicIpRange(final long id, String startIp, } } + AccountVlanMapVO accountMap = _accountVlanMapDao.findAccountVlanMap(null, id); + Account account = accountMap != null ? _accountDao.findById(accountMap.getAccountId()) : null; + + DomainVlanMapVO domainMap = _domainVlanMapDao.findDomainVlanMap(null, id); + Long domainId = domainMap != null ? domainMap.getDomainId() : null; + final Boolean isRangeForSystemVM = checkIfVlanRangeIsForSystemVM(id); if (forSystemVms != null && isRangeForSystemVM != forSystemVms) { if (VlanType.DirectAttached.equals(vlanRange.getVlanType())) { throw new InvalidParameterValueException("forSystemVms is not available for this IP range with vlan type: " + VlanType.DirectAttached); } // Check if range has already been dedicated - final List maps = _accountVlanMapDao.listAccountVlanMapsByVlan(id); - if (maps != null && !maps.isEmpty()) { + if (account != null) { throw new InvalidParameterValueException("Specified Public IP range has already been dedicated to an account"); } - - List domainmaps = _domainVlanMapDao.listDomainVlanMapsByVlan(id); - if (domainmaps != null && !domainmaps.isEmpty()) { + if (domainId != null) { throw new InvalidParameterValueException("Specified Public IP range has already been dedicated to a domain"); } } if (ipv4) { + long existingIpAddressAmount = 0L; + long newIpAddressAmount = 0L; + + if (account != null) { + // IPv4 public range is dedicated to an account (IPv6 cannot be dedicated at the moment). + // We need to update the resource count. + existingIpAddressAmount = _publicIpAddressDao.countIPs(vlanRange.getDataCenterId(), id, false); + newIpAddressAmount = NetUtils.ip2Long(endIp) - NetUtils.ip2Long(startIp) + 1; + } + + try (CheckedReservation publicIpReservation = new CheckedReservation(account, ResourceType.public_ip, null, null, null, newIpAddressAmount, existingIpAddressAmount, reservationDao, _resourceLimitMgr)) { + updateVlanAndIpv4Range(id, vlanRange, startIp, endIp, gateway, netmask, isRangeForSystemVM, forSystemVms); + + if (account != null) { + long countDiff = newIpAddressAmount - existingIpAddressAmount; + if (countDiff > 0) { + _resourceLimitMgr.incrementResourceCount(account.getId(), ResourceType.public_ip, countDiff); + } else if (countDiff < 0) { + _resourceLimitMgr.decrementResourceCount(account.getId(), ResourceType.public_ip, Math.abs(countDiff)); + } + } + + } } if (ipv6) { updateVlanAndIpv6Range(id, vlanRange, startIpv6, endIpv6, ip6Gateway, ip6Cidr, isRangeForSystemVM, forSystemVms); @@ -5801,12 +5829,6 @@ public Vlan dedicatePublicIpRange(final DedicatePublicIpRangeCmd cmd) throws Res throw new InvalidParameterValueException("Public IP range can be dedicated to an account only in the zone of type " + NetworkType.Advanced); } - // Check Public IP resource limits - if (vlanOwner != null) { - final int accountPublicIpRange = _publicIpAddressDao.countIPs(zoneId, vlanDbId, false); - _resourceLimitMgr.checkResourceLimit(vlanOwner, ResourceType.public_ip, accountPublicIpRange); - } - // Check if any of the Public IP addresses is allocated to another // account final List ips = _publicIpAddressDao.listByVlanId(vlanDbId); @@ -5827,6 +5849,10 @@ public Vlan dedicatePublicIpRange(final DedicatePublicIpRangeCmd cmd) throws Res } } + // Check Public IP resource limits + long reservedIpAddressesAmount = vlanOwner != null ? _publicIpAddressDao.countIPs(zoneId, vlanDbId, false) : 0L; + try (CheckedReservation publicIpReservation = new CheckedReservation(vlanOwner, ResourceType.public_ip, null, null, null, reservedIpAddressesAmount, null, reservationDao, _resourceLimitMgr)) { + if (vlanOwner != null) { // Create an AccountVlanMapVO entry final AccountVlanMapVO accountVlanMapVO = new AccountVlanMapVO(vlanOwner.getId(), vlan.getId()); @@ -5850,6 +5876,8 @@ public Vlan dedicatePublicIpRange(final DedicatePublicIpRangeCmd cmd) throws Res } return vlan; + + } } @Override diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index 54c1d1339434..c75fe4efbccd 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -40,6 +40,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.resourcelimit.CheckedReservation; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.alert.AlertService; @@ -75,6 +76,7 @@ import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.dao.NetworkPermissionDao; import org.apache.cloudstack.network.element.InternalLoadBalancerElementService; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.BooleanUtils; @@ -328,6 +330,8 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C @Inject ResourceLimitService _resourceLimitMgr; @Inject + ReservationDao reservationDao; + @Inject DomainManager _domainMgr; @Inject ProjectManager _projectMgr; @@ -1143,15 +1147,10 @@ public IpAddress reserveIpAddress(Account account, Boolean displayIp, Long ipAdd if (ipDedicatedAccountId != null && !ipDedicatedAccountId.equals(account.getAccountId())) { throw new InvalidParameterValueException("Unable to reserve a IP because it is dedicated to another Account."); } - if (ipDedicatedAccountId == null) { - // Check that the maximum number of public IPs for the given accountId will not be exceeded - try { - _resourceLimitMgr.checkResourceLimit(account, Resource.ResourceType.public_ip); - } catch (ResourceAllocationException ex) { - logger.warn("Failed to allocate resource of type " + ex.getResourceType() + " for account " + account); - throw new AccountLimitException("Maximum number of public IP addresses for account: " + account.getAccountName() + " has been exceeded."); - } - } + + long reservedIpAddressesAmount = ipDedicatedAccountId == null ? 1L : 0L; + try (CheckedReservation publicIpAddressReservation = new CheckedReservation(account, Resource.ResourceType.public_ip, reservedIpAddressesAmount, reservationDao, _resourceLimitMgr)) { + List maps = _accountVlanMapDao.listAccountVlanMapsByVlan(ipVO.getVlanId()); ipVO.setAllocatedTime(new Date()); ipVO.setAllocatedToAccountId(account.getAccountId()); @@ -1161,10 +1160,15 @@ public IpAddress reserveIpAddress(Account account, Boolean displayIp, Long ipAdd ipVO.setDisplay(displayIp); } ipVO = _ipAddressDao.persist(ipVO); - if (ipDedicatedAccountId == null) { + if (reservedIpAddressesAmount > 0) { _resourceLimitMgr.incrementResourceCount(account.getId(), Resource.ResourceType.public_ip); } return ipVO; + + } catch (ResourceAllocationException ex) { + logger.warn("Failed to allocate resource of type " + ex.getResourceType() + " for account " + account); + throw new AccountLimitException("Maximum number of public IP addresses for account: " + account.getAccountName() + " has been exceeded."); + } } @Override diff --git a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java index 5f9913e2ee5f..cab77ccf16ac 100644 --- a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java +++ b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java @@ -23,6 +23,7 @@ import java.util.Objects; import java.util.stream.Collectors; +import com.cloud.api.ApiDBUtils; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.reservation.ReservationVO; import org.apache.cloudstack.reservation.dao.ReservationDao; @@ -146,6 +147,11 @@ public CheckedReservation(Account account, Long domainId, ResourceType resourceT this.reservationDao = reservationDao; this.resourceLimitService = resourceLimitService; + + // When allocating to a domain instead of a specific account, consider the system account as the owner for the validations here. + if (account == null) { + account = ApiDBUtils.getSystemAccount(); + } this.account = account; if (domainId == null) { From a09835b1f4cea559aa93a49ea16ac37eddda6a2e Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:28:30 +0530 Subject: [PATCH 019/205] review comments --- .../cloud/projects/ProjectManagerImpl.java | 41 +++++++++---------- .../storage/ImageStoreUploadMonitorImpl.java | 2 +- .../storage/download/DownloadListener.java | 7 +++- .../cloud/template/TemplateManagerImpl.java | 14 ++++--- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java index 43efccd04f98..6942400c82c6 100644 --- a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java +++ b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java @@ -604,16 +604,16 @@ public boolean addUserToProject(Long projectId, String username, String email, L boolean shouldIncrementResourceCount = projectRole != null && Role.Admin == projectRole; try (CheckedReservation cr = new CheckedReservation(userAccount, ResourceType.project, shouldIncrementResourceCount ? 1L : 0L, reservationDao, _resourceLimitMgr)) { - if (assignUserToProject(project, user.getId(), user.getAccountId(), projectRole, - Optional.ofNullable(role).map(ProjectRole::getId).orElse(null)) != null) { - if (shouldIncrementResourceCount) { - _resourceLimitMgr.incrementResourceCount(userAccount.getId(), ResourceType.project); + if (assignUserToProject(project, user.getId(), user.getAccountId(), projectRole, + Optional.ofNullable(role).map(ProjectRole::getId).orElse(null)) != null) { + if (shouldIncrementResourceCount) { + _resourceLimitMgr.incrementResourceCount(userAccount.getId(), ResourceType.project); + } + return true; + } else { + logger.warn("Failed to add user to project: {}", project); + return false; } - return true; - } else { - logger.warn("Failed to add user to project: {}", project); - return false; - } } } } @@ -721,19 +721,16 @@ public void doInTransactionWithoutResult(TransactionStatus status) throws Resour } try (CheckedReservation checkedReservation = new CheckedReservation(futureOwnerAccount, ResourceType.project, null, null, 1L, reservationDao, _resourceLimitMgr)) { - - _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(futureOwnerAccount.getId()), ResourceType.project); - - //unset the role for the old owner - ProjectAccountVO currentOwner = _projectAccountDao.findByProjectIdAccountId(projectId, currentOwnerAccount.getId()); - currentOwner.setAccountRole(Role.Regular); - _projectAccountDao.update(currentOwner.getId(), currentOwner); - _resourceLimitMgr.decrementResourceCount(currentOwnerAccount.getId(), ResourceType.project); - - //set new owner - futureOwner.setAccountRole(Role.Admin); - _projectAccountDao.update(futureOwner.getId(), futureOwner); - _resourceLimitMgr.incrementResourceCount(futureOwnerAccount.getId(), ResourceType.project); + //unset the role for the old owner + ProjectAccountVO currentOwner = _projectAccountDao.findByProjectIdAccountId(projectId, currentOwnerAccount.getId()); + currentOwner.setAccountRole(Role.Regular); + _projectAccountDao.update(currentOwner.getId(), currentOwner); + _resourceLimitMgr.decrementResourceCount(currentOwnerAccount.getId(), ResourceType.project); + + //set new owner + futureOwner.setAccountRole(Role.Admin); + _projectAccountDao.update(futureOwner.getId(), futureOwner); + _resourceLimitMgr.incrementResourceCount(futureOwnerAccount.getId(), ResourceType.project); } } else { logger.trace("Future owner {}is already the owner of the project {}", newOwnerName, project); diff --git a/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java b/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java index b56e5b562136..a63b0a6f5ec4 100755 --- a/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java +++ b/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java @@ -360,7 +360,7 @@ private Boolean checkAndUpdateVolumeResourceLimit(VolumeVO volume, VolumeDataSto boolean success = true; Long currentSize = answer.getVirtualSize() != 0 ? answer.getVirtualSize() : answer.getPhysicalSize(); Long lastSize = volume.getSize() != null ? volume.getSize() : 0L; - if (!checkAndUpdateSecondaryStorageResourceLimit(volume.getAccountId(), volume.getSize(), currentSize)) { + if (!checkAndUpdateSecondaryStorageResourceLimit(volume.getAccountId(), lastSize, currentSize)) { volumeDataStore.setDownloadState(VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR); volumeDataStore.setState(State.Failed); volumeDataStore.setErrorString("Storage Limit Reached"); diff --git a/server/src/main/java/com/cloud/storage/download/DownloadListener.java b/server/src/main/java/com/cloud/storage/download/DownloadListener.java index 058881fdb54a..695b1c060e41 100644 --- a/server/src/main/java/com/cloud/storage/download/DownloadListener.java +++ b/server/src/main/java/com/cloud/storage/download/DownloadListener.java @@ -280,7 +280,7 @@ private Long getAccountIdForDataObject() { } private Long getSizeFromDB() { - Long lastSize = 0L; + Long lastSize = null; if (DataObjectType.TEMPLATE.equals(object.getType())) { TemplateDataStoreVO t = _templateDataStoreDao.findByStoreTemplate(object.getDataStore().getId(), object.getId()); lastSize = t.getSize(); @@ -288,7 +288,7 @@ private Long getSizeFromDB() { VolumeVO v = _volumeDao.findById(object.getId()); lastSize = v.getSize(); } - return lastSize; + return lastSize == null ? 0L : lastSize; } private Boolean checkAndUpdateResourceLimits(DownloadAnswer answer) { @@ -297,6 +297,9 @@ private Boolean checkAndUpdateResourceLimits(DownloadAnswer answer) { if (currentSize > lastSize) { Long accountId = getAccountIdForDataObject(); + if (accountId == null) { + return true; + } Account account = _accountMgr.getAccount(accountId); Long usage = currentSize - lastSize; try (CheckedReservation secStorageReservation = new CheckedReservation(account, Resource.ResourceType.secondary_storage, usage, _reservationDao, _resourceLimitMgr)) { diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index d0fdcbfde8f8..13144893f52f 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -1036,12 +1036,14 @@ public VirtualMachineTemplate copyTemplate(CopyTemplateCmd cmd) throws StorageUn logger.debug("There is Template {} in secondary storage {} in zone {} , don't need to copy", template, dstSecStore, dataCenterVOs.get(destZoneId)); continue; } - try (CheckedReservation secondaryStorageReservation = new CheckedReservation(templateOwner, ResourceType.secondary_storage, null, null, template.getSize(), reservationDao, _resourceLimitMgr)) { - if (!copy(userId, template, srcSecStore, dataCenterVOs.get(destZoneId))) { - failedZones.add(dataCenterVOs.get(destZoneId).getName()); - continue; - } - _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.secondary_storage, template.getSize()); + if (template.getSize() != null) { + try (CheckedReservation secondaryStorageReservation = new CheckedReservation(templateOwner, ResourceType.secondary_storage, null, null, template.getSize(), reservationDao, _resourceLimitMgr)) { + if (!copy(userId, template, srcSecStore, dataCenterVOs.get(destZoneId))) { + failedZones.add(dataCenterVOs.get(destZoneId).getName()); + continue; + } + _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.secondary_storage, template.getSize()); + } } } } From 4a691f43df625cb32cd09f62ed71a5f60f236d2a Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:26:43 +0530 Subject: [PATCH 020/205] fix identation --- .../orchestration/NetworkOrchestrator.java | 579 +++++++++--------- .../ConfigurationManagerImpl.java | 60 +- .../com/cloud/network/NetworkServiceImpl.java | 28 +- .../com/cloud/network/vpc/VpcManagerImpl.java | 34 +- .../cloud/projects/ProjectManagerImpl.java | 81 ++- .../cloud/storage/VolumeApiServiceImpl.java | 515 ++++++++-------- .../cloud/template/TemplateManagerImpl.java | 188 +++--- .../java/com/cloud/vm/UserVmManagerImpl.java | 78 +-- .../VolumeImportUnmanageManagerImpl.java | 20 +- .../vm/UnmanagedVMsManagerImpl.java | 286 ++++----- 10 files changed, 930 insertions(+), 939 deletions(-) diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 4fd5cbd1949a..2da0c837a84e 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -2745,358 +2745,357 @@ private Network createGuestNetwork(final long networkOfferingId, final String na boolean ipv6 = false; try (CheckedReservation networkReservation = new CheckedReservation(owner, domainId, Resource.ResourceType.network, null, null, 1L, reservationDao, _resourceLimitMgr)) { + if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) { + ipv6 = true; + } + // Validate zone + if (zone.getNetworkType() == NetworkType.Basic) { + // In Basic zone the network should have aclType=Domain, domainId=1, subdomainAccess=true + if (aclType == null || aclType != ACLType.Domain) { + throw new InvalidParameterValueException("Only AclType=Domain can be specified for network creation in Basic zone"); + } - if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) { - ipv6 = true; - } - // Validate zone - if (zone.getNetworkType() == NetworkType.Basic) { - // In Basic zone the network should have aclType=Domain, domainId=1, subdomainAccess=true - if (aclType == null || aclType != ACLType.Domain) { - throw new InvalidParameterValueException("Only AclType=Domain can be specified for network creation in Basic zone"); - } - - // Only one guest network is supported in Basic zone - final List guestNetworks = _networksDao.listByZoneAndTrafficType(zone.getId(), TrafficType.Guest); - if (!guestNetworks.isEmpty()) { - throw new InvalidParameterValueException("Can't have more than one Guest network in zone with network type " + NetworkType.Basic); - } - - // if zone is basic, only Shared network offerings w/o source nat service are allowed - if (!(ntwkOff.getGuestType() == GuestType.Shared && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat))) { - throw new InvalidParameterValueException("For zone of type " + NetworkType.Basic + " only offerings of " + "guestType " + GuestType.Shared + " with disabled " - + Service.SourceNat.getName() + " service are allowed"); - } + // Only one guest network is supported in Basic zone + final List guestNetworks = _networksDao.listByZoneAndTrafficType(zone.getId(), TrafficType.Guest); + if (!guestNetworks.isEmpty()) { + throw new InvalidParameterValueException("Can't have more than one Guest network in zone with network type " + NetworkType.Basic); + } - if (domainId == null || domainId != Domain.ROOT_DOMAIN) { - throw new InvalidParameterValueException("Guest network in Basic zone should be dedicated to ROOT domain"); - } + // if zone is basic, only Shared network offerings w/o source nat service are allowed + if (!(ntwkOff.getGuestType() == GuestType.Shared && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat))) { + throw new InvalidParameterValueException("For zone of type " + NetworkType.Basic + " only offerings of " + "guestType " + GuestType.Shared + " with disabled " + + Service.SourceNat.getName() + " service are allowed"); + } - if (subdomainAccess == null) { - subdomainAccess = true; - } else if (!subdomainAccess) { - throw new InvalidParameterValueException("Subdomain access should be set to true for the" + " guest network in the Basic zone"); - } + if (domainId == null || domainId != Domain.ROOT_DOMAIN) { + throw new InvalidParameterValueException("Guest network in Basic zone should be dedicated to ROOT domain"); + } - if (vlanId == null) { - vlanId = Vlan.UNTAGGED; - } else { - if (!vlanId.equalsIgnoreCase(Vlan.UNTAGGED)) { - throw new InvalidParameterValueException("Only vlan " + Vlan.UNTAGGED + " can be created in " + "the zone of type " + NetworkType.Basic); + if (subdomainAccess == null) { + subdomainAccess = true; + } else if (!subdomainAccess) { + throw new InvalidParameterValueException("Subdomain access should be set to true for the" + " guest network in the Basic zone"); } - } - } else if (zone.getNetworkType() == NetworkType.Advanced) { - if (zone.isSecurityGroupEnabled()) { - if (isolatedPvlan != null) { - throw new InvalidParameterValueException("Isolated Private VLAN is not supported with security group!"); + if (vlanId == null) { + vlanId = Vlan.UNTAGGED; + } else { + if (!vlanId.equalsIgnoreCase(Vlan.UNTAGGED)) { + throw new InvalidParameterValueException("Only vlan " + Vlan.UNTAGGED + " can be created in " + "the zone of type " + NetworkType.Basic); + } } - // Only Account specific Isolated network with sourceNat service disabled are allowed in security group - // enabled zone - if ((ntwkOff.getGuestType() != GuestType.Shared) && (ntwkOff.getGuestType() != GuestType.L2)) { - throw new InvalidParameterValueException("Only shared or L2 guest network can be created in security group enabled zone"); + + } else if (zone.getNetworkType() == NetworkType.Advanced) { + if (zone.isSecurityGroupEnabled()) { + if (isolatedPvlan != null) { + throw new InvalidParameterValueException("Isolated Private VLAN is not supported with security group!"); + } + // Only Account specific Isolated network with sourceNat service disabled are allowed in security group + // enabled zone + if ((ntwkOff.getGuestType() != GuestType.Shared) && (ntwkOff.getGuestType() != GuestType.L2)) { + throw new InvalidParameterValueException("Only shared or L2 guest network can be created in security group enabled zone"); + } + if (_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat)) { + throw new InvalidParameterValueException("Service SourceNat is not allowed in security group enabled zone"); + } } - if (_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat)) { - throw new InvalidParameterValueException("Service SourceNat is not allowed in security group enabled zone"); + + //don't allow eip/elb networks in Advance zone + if (ntwkOff.isElasticIp() || ntwkOff.isElasticLb()) { + throw new InvalidParameterValueException("Elastic IP and Elastic LB services are supported in zone of type " + NetworkType.Basic); } } - //don't allow eip/elb networks in Advance zone - if (ntwkOff.isElasticIp() || ntwkOff.isElasticLb()) { - throw new InvalidParameterValueException("Elastic IP and Elastic LB services are supported in zone of type " + NetworkType.Basic); + if (ipv6 && !GuestType.Shared.equals(ntwkOff.getGuestType())) { + _networkModel.checkIp6CidrSizeEqualTo64(ip6Cidr); } - } - - if (ipv6 && !GuestType.Shared.equals(ntwkOff.getGuestType())) { - _networkModel.checkIp6CidrSizeEqualTo64(ip6Cidr); - } - //TODO(VXLAN): Support VNI specified - // VlanId can be specified only when network offering supports it - final boolean vlanSpecified = vlanId != null; - if (vlanSpecified != ntwkOff.isSpecifyVlan()) { - if (vlanSpecified) { - if (!isSharedNetworkWithoutSpecifyVlan(ntwkOff) && !isPrivateGatewayWithoutSpecifyVlan(ntwkOff)) { - throw new InvalidParameterValueException("Can't specify vlan; corresponding offering says specifyVlan=false"); + //TODO(VXLAN): Support VNI specified + // VlanId can be specified only when network offering supports it + final boolean vlanSpecified = vlanId != null; + if (vlanSpecified != ntwkOff.isSpecifyVlan()) { + if (vlanSpecified) { + if (!isSharedNetworkWithoutSpecifyVlan(ntwkOff) && !isPrivateGatewayWithoutSpecifyVlan(ntwkOff)) { + throw new InvalidParameterValueException("Can't specify vlan; corresponding offering says specifyVlan=false"); + } + } else { + throw new InvalidParameterValueException("Vlan has to be specified; corresponding offering says specifyVlan=true"); } - } else { - throw new InvalidParameterValueException("Vlan has to be specified; corresponding offering says specifyVlan=true"); } - } - if (vlanSpecified) { - URI uri = encodeVlanIdIntoBroadcastUri(vlanId, pNtwk); - // Aux: generate secondary URI for secondary VLAN ID (if provided) for performing checks - URI secondaryUri = StringUtils.isNotBlank(isolatedPvlan) ? BroadcastDomainType.fromString(isolatedPvlan) : null; - if (isSharedNetworkWithoutSpecifyVlan(ntwkOff) || isPrivateGatewayWithoutSpecifyVlan(ntwkOff)) { - bypassVlanOverlapCheck = true; - } - //don't allow to specify vlan tag used by physical network for dynamic vlan allocation - if (!(bypassVlanOverlapCheck && (ntwkOff.getGuestType() == GuestType.Shared || isPrivateNetwork)) - && _dcDao.findVnet(zoneId, pNtwk.getId(), BroadcastDomainType.getValue(uri)).size() > 0) { - throw new InvalidParameterValueException("The VLAN tag to use for new guest network, " + vlanId + " is already being used for dynamic vlan allocation for the guest network in zone " - + zone.getName()); - } - if (secondaryUri != null && !(bypassVlanOverlapCheck && ntwkOff.getGuestType() == GuestType.Shared) && - _dcDao.findVnet(zoneId, pNtwk.getId(), BroadcastDomainType.getValue(secondaryUri)).size() > 0) { - throw new InvalidParameterValueException(String.format( - "The VLAN tag for isolated PVLAN %s is already being used for dynamic vlan allocation for the guest network in zone %s", - isolatedPvlan, zone)); - } - if (!UuidUtils.isUuid(vlanId)) { - // For Isolated and L2 networks, don't allow to create network with vlan that already exists in the zone - if (!hasGuestBypassVlanOverlapCheck(bypassVlanOverlapCheck, ntwkOff, isPrivateNetwork)) { - if (_networksDao.listByZoneAndUriAndGuestType(zoneId, uri.toString(), null).size() > 0) { - throw new InvalidParameterValueException(String.format( - "Network with vlan %s already exists or overlaps with other network vlans in zone %s", - vlanId, zone)); - } else if (secondaryUri != null && _networksDao.listByZoneAndUriAndGuestType(zoneId, secondaryUri.toString(), null).size() > 0) { - throw new InvalidParameterValueException(String.format( - "Network with vlan %s already exists or overlaps with other network vlans in zone %s", - isolatedPvlan, zone)); - } else { - final List dcVnets = _datacenterVnetDao.findVnet(zoneId, BroadcastDomainType.getValue(uri)); - //for the network that is created as part of private gateway, - //the vnet is not coming from the data center vnet table, so the list can be empty - if (!dcVnets.isEmpty()) { - final DataCenterVnetVO dcVnet = dcVnets.get(0); - // Fail network creation if specified vlan is dedicated to a different account - if (dcVnet.getAccountGuestVlanMapId() != null) { - final Long accountGuestVlanMapId = dcVnet.getAccountGuestVlanMapId(); - final AccountGuestVlanMapVO map = _accountGuestVlanMapDao.findById(accountGuestVlanMapId); - if (map.getAccountId() != owner.getAccountId()) { - throw new InvalidParameterValueException("Vlan " + vlanId + " is dedicated to a different account"); - } - // Fail network creation if owner has a dedicated range of vlans but the specified vlan belongs to the system pool - } else { - final List maps = _accountGuestVlanMapDao.listAccountGuestVlanMapsByAccount(owner.getAccountId()); - if (maps != null && !maps.isEmpty()) { - final int vnetsAllocatedToAccount = _datacenterVnetDao.countVnetsAllocatedToAccount(zoneId, owner.getAccountId()); - final int vnetsDedicatedToAccount = _datacenterVnetDao.countVnetsDedicatedToAccount(zoneId, owner.getAccountId()); - if (vnetsAllocatedToAccount < vnetsDedicatedToAccount) { - throw new InvalidParameterValueException("Specified vlan " + vlanId + " doesn't belong" + " to the vlan range dedicated to the owner " - + owner.getAccountName()); + if (vlanSpecified) { + URI uri = encodeVlanIdIntoBroadcastUri(vlanId, pNtwk); + // Aux: generate secondary URI for secondary VLAN ID (if provided) for performing checks + URI secondaryUri = StringUtils.isNotBlank(isolatedPvlan) ? BroadcastDomainType.fromString(isolatedPvlan) : null; + if (isSharedNetworkWithoutSpecifyVlan(ntwkOff) || isPrivateGatewayWithoutSpecifyVlan(ntwkOff)) { + bypassVlanOverlapCheck = true; + } + //don't allow to specify vlan tag used by physical network for dynamic vlan allocation + if (!(bypassVlanOverlapCheck && (ntwkOff.getGuestType() == GuestType.Shared || isPrivateNetwork)) + && _dcDao.findVnet(zoneId, pNtwk.getId(), BroadcastDomainType.getValue(uri)).size() > 0) { + throw new InvalidParameterValueException("The VLAN tag to use for new guest network, " + vlanId + " is already being used for dynamic vlan allocation for the guest network in zone " + + zone.getName()); + } + if (secondaryUri != null && !(bypassVlanOverlapCheck && ntwkOff.getGuestType() == GuestType.Shared) && + _dcDao.findVnet(zoneId, pNtwk.getId(), BroadcastDomainType.getValue(secondaryUri)).size() > 0) { + throw new InvalidParameterValueException(String.format( + "The VLAN tag for isolated PVLAN %s is already being used for dynamic vlan allocation for the guest network in zone %s", + isolatedPvlan, zone)); + } + if (!UuidUtils.isUuid(vlanId)) { + // For Isolated and L2 networks, don't allow to create network with vlan that already exists in the zone + if (!hasGuestBypassVlanOverlapCheck(bypassVlanOverlapCheck, ntwkOff, isPrivateNetwork)) { + if (_networksDao.listByZoneAndUriAndGuestType(zoneId, uri.toString(), null).size() > 0) { + throw new InvalidParameterValueException(String.format( + "Network with vlan %s already exists or overlaps with other network vlans in zone %s", + vlanId, zone)); + } else if (secondaryUri != null && _networksDao.listByZoneAndUriAndGuestType(zoneId, secondaryUri.toString(), null).size() > 0) { + throw new InvalidParameterValueException(String.format( + "Network with vlan %s already exists or overlaps with other network vlans in zone %s", + isolatedPvlan, zone)); + } else { + final List dcVnets = _datacenterVnetDao.findVnet(zoneId, BroadcastDomainType.getValue(uri)); + //for the network that is created as part of private gateway, + //the vnet is not coming from the data center vnet table, so the list can be empty + if (!dcVnets.isEmpty()) { + final DataCenterVnetVO dcVnet = dcVnets.get(0); + // Fail network creation if specified vlan is dedicated to a different account + if (dcVnet.getAccountGuestVlanMapId() != null) { + final Long accountGuestVlanMapId = dcVnet.getAccountGuestVlanMapId(); + final AccountGuestVlanMapVO map = _accountGuestVlanMapDao.findById(accountGuestVlanMapId); + if (map.getAccountId() != owner.getAccountId()) { + throw new InvalidParameterValueException("Vlan " + vlanId + " is dedicated to a different account"); + } + // Fail network creation if owner has a dedicated range of vlans but the specified vlan belongs to the system pool + } else { + final List maps = _accountGuestVlanMapDao.listAccountGuestVlanMapsByAccount(owner.getAccountId()); + if (maps != null && !maps.isEmpty()) { + final int vnetsAllocatedToAccount = _datacenterVnetDao.countVnetsAllocatedToAccount(zoneId, owner.getAccountId()); + final int vnetsDedicatedToAccount = _datacenterVnetDao.countVnetsDedicatedToAccount(zoneId, owner.getAccountId()); + if (vnetsAllocatedToAccount < vnetsDedicatedToAccount) { + throw new InvalidParameterValueException("Specified vlan " + vlanId + " doesn't belong" + " to the vlan range dedicated to the owner " + + owner.getAccountName()); + } } } } } - } - } else { - // don't allow to creating shared network with given Vlan ID, if there already exists a isolated network or - // shared network with same Vlan ID in the zone - if (!bypassVlanOverlapCheck && _networksDao.listByZoneAndUriAndGuestType(zoneId, uri.toString(), GuestType.Isolated).size() > 0) { - throw new InvalidParameterValueException(String.format( - "There is an existing isolated/shared network that overlaps with vlan id:%s in zone %s", vlanId, zone)); + } else { + // don't allow to creating shared network with given Vlan ID, if there already exists a isolated network or + // shared network with same Vlan ID in the zone + if (!bypassVlanOverlapCheck && _networksDao.listByZoneAndUriAndGuestType(zoneId, uri.toString(), GuestType.Isolated).size() > 0) { + throw new InvalidParameterValueException(String.format( + "There is an existing isolated/shared network that overlaps with vlan id:%s in zone %s", vlanId, zone)); + } } } - } - } + } - // If networkDomain is not specified, take it from the global configuration - if (_networkModel.areServicesSupportedByNetworkOffering(networkOfferingId, Service.Dns)) { - final Map dnsCapabilities = _networkModel.getNetworkOfferingServiceCapabilities(_entityMgr.findById(NetworkOffering.class, networkOfferingId), - Service.Dns); - final String isUpdateDnsSupported = dnsCapabilities.get(Capability.AllowDnsSuffixModification); - if (isUpdateDnsSupported == null || !Boolean.valueOf(isUpdateDnsSupported)) { - if (networkDomain != null) { - // TBD: NetworkOfferingId and zoneId. Send uuids instead. - throw new InvalidParameterValueException(String.format( - "Domain name change is not supported by network offering id=%d in zone %s", - networkOfferingId, zone)); - } - } else { - if (networkDomain == null) { - // 1) Get networkDomain from the corresponding account/domain/zone - if (aclType == ACLType.Domain) { - networkDomain = _networkModel.getDomainNetworkDomain(domainId, zoneId); - } else if (aclType == ACLType.Account) { - networkDomain = _networkModel.getAccountNetworkDomain(owner.getId(), zoneId); + // If networkDomain is not specified, take it from the global configuration + if (_networkModel.areServicesSupportedByNetworkOffering(networkOfferingId, Service.Dns)) { + final Map dnsCapabilities = _networkModel.getNetworkOfferingServiceCapabilities(_entityMgr.findById(NetworkOffering.class, networkOfferingId), + Service.Dns); + final String isUpdateDnsSupported = dnsCapabilities.get(Capability.AllowDnsSuffixModification); + if (isUpdateDnsSupported == null || !Boolean.valueOf(isUpdateDnsSupported)) { + if (networkDomain != null) { + // TBD: NetworkOfferingId and zoneId. Send uuids instead. + throw new InvalidParameterValueException(String.format( + "Domain name change is not supported by network offering id=%d in zone %s", + networkOfferingId, zone)); } - - // 2) If null, generate networkDomain using domain suffix from the global config variables + } else { if (networkDomain == null) { - networkDomain = "cs" + Long.toHexString(owner.getId()) + GuestDomainSuffix.valueIn(zoneId); - } + // 1) Get networkDomain from the corresponding account/domain/zone + if (aclType == ACLType.Domain) { + networkDomain = _networkModel.getDomainNetworkDomain(domainId, zoneId); + } else if (aclType == ACLType.Account) { + networkDomain = _networkModel.getAccountNetworkDomain(owner.getId(), zoneId); + } - } else { - // validate network domain - if (!NetUtils.verifyDomainName(networkDomain)) { - throw new InvalidParameterValueException("Invalid network domain. Total length shouldn't exceed 190 chars. Each domain " - + "label must be between 1 and 63 characters long, can contain ASCII letters 'a' through 'z', the digits '0' through '9', " - + "and the hyphen ('-'); can't start or end with \"-\""); + // 2) If null, generate networkDomain using domain suffix from the global config variables + if (networkDomain == null) { + networkDomain = "cs" + Long.toHexString(owner.getId()) + GuestDomainSuffix.valueIn(zoneId); + } + + } else { + // validate network domain + if (!NetUtils.verifyDomainName(networkDomain)) { + throw new InvalidParameterValueException("Invalid network domain. Total length shouldn't exceed 190 chars. Each domain " + + "label must be between 1 and 63 characters long, can contain ASCII letters 'a' through 'z', the digits '0' through '9', " + + "and the hyphen ('-'); can't start or end with \"-\""); + } } } } - } - // In Advance zone Cidr for Shared networks and Isolated networks w/o source nat service can't be NULL - 2.2.x - // limitation, remove after we introduce support for multiple ip ranges - // with different Cidrs for the same Shared network - final boolean cidrRequired = zone.getNetworkType() == NetworkType.Advanced - && ntwkOff.getTrafficType() == TrafficType.Guest - && (ntwkOff.getGuestType() == GuestType.Shared || (ntwkOff.getGuestType() == GuestType.Isolated - && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat) - && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.Gateway))); - if (cidr == null && ip6Cidr == null && cidrRequired) { - if (ntwkOff.getGuestType() == GuestType.Shared) { - throw new InvalidParameterValueException(String.format("Gateway/netmask are required when creating %s networks.", Network.GuestType.Shared)); - } else { - throw new InvalidParameterValueException("gateway/netmask are required when create network of" + " type " + GuestType.Isolated + " with service " + Service.SourceNat.getName() + " disabled"); + // In Advance zone Cidr for Shared networks and Isolated networks w/o source nat service can't be NULL - 2.2.x + // limitation, remove after we introduce support for multiple ip ranges + // with different Cidrs for the same Shared network + final boolean cidrRequired = zone.getNetworkType() == NetworkType.Advanced + && ntwkOff.getTrafficType() == TrafficType.Guest + && (ntwkOff.getGuestType() == GuestType.Shared || (ntwkOff.getGuestType() == GuestType.Isolated + && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat) + && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.Gateway))); + if (cidr == null && ip6Cidr == null && cidrRequired) { + if (ntwkOff.getGuestType() == GuestType.Shared) { + throw new InvalidParameterValueException(String.format("Gateway/netmask are required when creating %s networks.", Network.GuestType.Shared)); + } else { + throw new InvalidParameterValueException("gateway/netmask are required when create network of" + " type " + GuestType.Isolated + " with service " + Service.SourceNat.getName() + " disabled"); + } } - } - checkL2OfferingServices(ntwkOff); + checkL2OfferingServices(ntwkOff); - // No cidr can be specified in Basic zone - if (zone.getNetworkType() == NetworkType.Basic && cidr != null) { - throw new InvalidParameterValueException("StartIp/endIp/gateway/netmask can't be specified for zone of type " + NetworkType.Basic); - } + // No cidr can be specified in Basic zone + if (zone.getNetworkType() == NetworkType.Basic && cidr != null) { + throw new InvalidParameterValueException("StartIp/endIp/gateway/netmask can't be specified for zone of type " + NetworkType.Basic); + } - // Check if cidr is RFC1918 compliant if the network is Guest Isolated for IPv4 - if (cidr != null && (ntwkOff.getGuestType() == Network.GuestType.Isolated && ntwkOff.getTrafficType() == TrafficType.Guest) && - !NetUtils.validateGuestCidr(cidr, !ConfigurationManager.AllowNonRFC1918CompliantIPs.value())) { + // Check if cidr is RFC1918 compliant if the network is Guest Isolated for IPv4 + if (cidr != null && (ntwkOff.getGuestType() == Network.GuestType.Isolated && ntwkOff.getTrafficType() == TrafficType.Guest) && + !NetUtils.validateGuestCidr(cidr, !ConfigurationManager.AllowNonRFC1918CompliantIPs.value())) { throw new InvalidParameterValueException("Virtual Guest Cidr " + cidr + " is not RFC 1918 or 6598 compliant"); - } - - final String networkDomainFinal = networkDomain; - final String vlanIdFinal = vlanId; - final Boolean subdomainAccessFinal = subdomainAccess; - final Network network = Transaction.execute(new TransactionCallback() { - @Override - public Network doInTransaction(final TransactionStatus status) { - Long physicalNetworkId = null; - if (pNtwk != null) { - physicalNetworkId = pNtwk.getId(); - } - final DataCenterDeployment plan = new DataCenterDeployment(zoneId, null, null, null, null, physicalNetworkId); - final NetworkVO userNetwork = new NetworkVO(); - userNetwork.setNetworkDomain(networkDomainFinal); - - if (cidr != null && gateway != null) { - userNetwork.setCidr(cidr); - userNetwork.setGateway(gateway); - } - - if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) { - userNetwork.setIp6Cidr(ip6Cidr); - userNetwork.setIp6Gateway(ip6Gateway); - } - - if (externalId != null) { - userNetwork.setExternalId(externalId); - } - - if (StringUtils.isNotBlank(routerIp)) { - userNetwork.setRouterIp(routerIp); - } - - if (StringUtils.isNotBlank(routerIpv6)) { - userNetwork.setRouterIpv6(routerIpv6); - } + } - if (vrIfaceMTUs != null) { - if (vrIfaceMTUs.first() != null && vrIfaceMTUs.first() > 0) { - userNetwork.setPublicMtu(vrIfaceMTUs.first()); - } else { - userNetwork.setPublicMtu(Integer.valueOf(NetworkService.VRPublicInterfaceMtu.defaultValue())); + final String networkDomainFinal = networkDomain; + final String vlanIdFinal = vlanId; + final Boolean subdomainAccessFinal = subdomainAccess; + final Network network = Transaction.execute(new TransactionCallback() { + @Override + public Network doInTransaction(final TransactionStatus status) { + Long physicalNetworkId = null; + if (pNtwk != null) { + physicalNetworkId = pNtwk.getId(); } + final DataCenterDeployment plan = new DataCenterDeployment(zoneId, null, null, null, null, physicalNetworkId); + final NetworkVO userNetwork = new NetworkVO(); + userNetwork.setNetworkDomain(networkDomainFinal); - if (vrIfaceMTUs.second() != null && vrIfaceMTUs.second() > 0) { - userNetwork.setPrivateMtu(vrIfaceMTUs.second()); - } else { - userNetwork.setPrivateMtu(Integer.valueOf(NetworkService.VRPrivateInterfaceMtu.defaultValue())); + if (cidr != null && gateway != null) { + userNetwork.setCidr(cidr); + userNetwork.setGateway(gateway); } - } else { - userNetwork.setPublicMtu(Integer.valueOf(NetworkService.VRPublicInterfaceMtu.defaultValue())); - userNetwork.setPrivateMtu(Integer.valueOf(NetworkService.VRPrivateInterfaceMtu.defaultValue())); - } - if (!GuestType.L2.equals(userNetwork.getGuestType())) { - if (StringUtils.isNotBlank(ip4Dns1)) { - userNetwork.setDns1(ip4Dns1); + if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) { + userNetwork.setIp6Cidr(ip6Cidr); + userNetwork.setIp6Gateway(ip6Gateway); } - if (StringUtils.isNotBlank(ip4Dns2)) { - userNetwork.setDns2(ip4Dns2); + + if (externalId != null) { + userNetwork.setExternalId(externalId); } - if (StringUtils.isNotBlank(ip6Dns1)) { - userNetwork.setIp6Dns1(ip6Dns1); + + if (StringUtils.isNotBlank(routerIp)) { + userNetwork.setRouterIp(routerIp); } - if (StringUtils.isNotBlank(ip6Dns2)) { - userNetwork.setIp6Dns2(ip6Dns2); + + if (StringUtils.isNotBlank(routerIpv6)) { + userNetwork.setRouterIpv6(routerIpv6); } - } - if (vlanIdFinal != null) { - if (isolatedPvlan == null) { - URI uri = null; - if (UuidUtils.isUuid(vlanIdFinal)) { - //Logical router's UUID provided as VLAN_ID - userNetwork.setVlanIdAsUUID(vlanIdFinal); //Set transient field + if (vrIfaceMTUs != null) { + if (vrIfaceMTUs.first() != null && vrIfaceMTUs.first() > 0) { + userNetwork.setPublicMtu(vrIfaceMTUs.first()); } else { - uri = encodeVlanIdIntoBroadcastUri(vlanIdFinal, pNtwk); + userNetwork.setPublicMtu(Integer.valueOf(NetworkService.VRPublicInterfaceMtu.defaultValue())); } - if (_networksDao.listByPhysicalNetworkPvlan(physicalNetworkId, uri.toString()).size() > 0) { - throw new InvalidParameterValueException(String.format( - "Network with vlan %s already exists or overlaps with other network pvlans in zone %s", - vlanIdFinal, zone)); - } - - userNetwork.setBroadcastUri(uri); - if (!vlanIdFinal.equalsIgnoreCase(Vlan.UNTAGGED)) { - userNetwork.setBroadcastDomainType(BroadcastDomainType.Vlan); + if (vrIfaceMTUs.second() != null && vrIfaceMTUs.second() > 0) { + userNetwork.setPrivateMtu(vrIfaceMTUs.second()); } else { - userNetwork.setBroadcastDomainType(BroadcastDomainType.Native); + userNetwork.setPrivateMtu(Integer.valueOf(NetworkService.VRPrivateInterfaceMtu.defaultValue())); } } else { - if (vlanIdFinal.equalsIgnoreCase(Vlan.UNTAGGED)) { - throw new InvalidParameterValueException("Cannot support pvlan with untagged primary vlan!"); + userNetwork.setPublicMtu(Integer.valueOf(NetworkService.VRPublicInterfaceMtu.defaultValue())); + userNetwork.setPrivateMtu(Integer.valueOf(NetworkService.VRPrivateInterfaceMtu.defaultValue())); + } + + if (!GuestType.L2.equals(userNetwork.getGuestType())) { + if (StringUtils.isNotBlank(ip4Dns1)) { + userNetwork.setDns1(ip4Dns1); } - URI uri = NetUtils.generateUriForPvlan(vlanIdFinal, isolatedPvlan, isolatedPvlanType.toString()); - if (_networksDao.listByPhysicalNetworkPvlan(physicalNetworkId, uri.toString(), isolatedPvlanType).size() > 0) { - throw new InvalidParameterValueException(String.format( - "Network with primary vlan %s and secondary vlan %s type %s already exists or overlaps with other network pvlans in zone %s", - vlanIdFinal, isolatedPvlan, isolatedPvlanType, zone)); + if (StringUtils.isNotBlank(ip4Dns2)) { + userNetwork.setDns2(ip4Dns2); + } + if (StringUtils.isNotBlank(ip6Dns1)) { + userNetwork.setIp6Dns1(ip6Dns1); + } + if (StringUtils.isNotBlank(ip6Dns2)) { + userNetwork.setIp6Dns2(ip6Dns2); } - userNetwork.setBroadcastUri(uri); - userNetwork.setBroadcastDomainType(BroadcastDomainType.Pvlan); - userNetwork.setPvlanType(isolatedPvlanType); } - } - userNetwork.setNetworkCidrSize(networkCidrSize); - final List networks = setupNetwork(owner, ntwkOff, userNetwork, plan, name, displayText, true, domainId, aclType, subdomainAccessFinal, vpcId, - isDisplayNetworkEnabled); - Network network = null; - if (networks == null || networks.isEmpty()) { - throw new CloudRuntimeException("Fail to create a network"); - } else { - if (networks.size() > 0 && networks.get(0).getGuestType() == Network.GuestType.Isolated && networks.get(0).getTrafficType() == TrafficType.Guest) { - Network defaultGuestNetwork = networks.get(0); - for (final Network nw : networks) { - if (nw.getCidr() != null && nw.getCidr().equals(zone.getGuestNetworkCidr())) { - defaultGuestNetwork = nw; + + if (vlanIdFinal != null) { + if (isolatedPvlan == null) { + URI uri = null; + if (UuidUtils.isUuid(vlanIdFinal)) { + //Logical router's UUID provided as VLAN_ID + userNetwork.setVlanIdAsUUID(vlanIdFinal); //Set transient field + } else { + uri = encodeVlanIdIntoBroadcastUri(vlanIdFinal, pNtwk); + } + + if (_networksDao.listByPhysicalNetworkPvlan(physicalNetworkId, uri.toString()).size() > 0) { + throw new InvalidParameterValueException(String.format( + "Network with vlan %s already exists or overlaps with other network pvlans in zone %s", + vlanIdFinal, zone)); + } + + userNetwork.setBroadcastUri(uri); + if (!vlanIdFinal.equalsIgnoreCase(Vlan.UNTAGGED)) { + userNetwork.setBroadcastDomainType(BroadcastDomainType.Vlan); + } else { + userNetwork.setBroadcastDomainType(BroadcastDomainType.Native); + } + } else { + if (vlanIdFinal.equalsIgnoreCase(Vlan.UNTAGGED)) { + throw new InvalidParameterValueException("Cannot support pvlan with untagged primary vlan!"); + } + URI uri = NetUtils.generateUriForPvlan(vlanIdFinal, isolatedPvlan, isolatedPvlanType.toString()); + if (_networksDao.listByPhysicalNetworkPvlan(physicalNetworkId, uri.toString(), isolatedPvlanType).size() > 0) { + throw new InvalidParameterValueException(String.format( + "Network with primary vlan %s and secondary vlan %s type %s already exists or overlaps with other network pvlans in zone %s", + vlanIdFinal, isolatedPvlan, isolatedPvlanType, zone)); } + userNetwork.setBroadcastUri(uri); + userNetwork.setBroadcastDomainType(BroadcastDomainType.Pvlan); + userNetwork.setPvlanType(isolatedPvlanType); } - network = defaultGuestNetwork; + } + userNetwork.setNetworkCidrSize(networkCidrSize); + final List networks = setupNetwork(owner, ntwkOff, userNetwork, plan, name, displayText, true, domainId, aclType, subdomainAccessFinal, vpcId, + isDisplayNetworkEnabled); + Network network = null; + if (networks == null || networks.isEmpty()) { + throw new CloudRuntimeException("Fail to create a network"); } else { - // For shared network - network = networks.get(0); + if (networks.size() > 0 && networks.get(0).getGuestType() == Network.GuestType.Isolated && networks.get(0).getTrafficType() == TrafficType.Guest) { + Network defaultGuestNetwork = networks.get(0); + for (final Network nw : networks) { + if (nw.getCidr() != null && nw.getCidr().equals(zone.getGuestNetworkCidr())) { + defaultGuestNetwork = nw; + } + } + network = defaultGuestNetwork; + } else { + // For shared network + network = networks.get(0); + } } - } - if (isResourceCountUpdateNeeded(ntwkOff)) { - changeAccountResourceCountOrRecalculateDomainResourceCount(owner.getAccountId(), domainId, isDisplayNetworkEnabled, true); - } - UsageEventUtils.publishNetworkCreation(network); + if (isResourceCountUpdateNeeded(ntwkOff)) { + changeAccountResourceCountOrRecalculateDomainResourceCount(owner.getAccountId(), domainId, isDisplayNetworkEnabled, true); + } + UsageEventUtils.publishNetworkCreation(network); - return network; - } - }); + return network; + } + }); - CallContext.current().setEventDetails("Network Id: " + network.getId()); - CallContext.current().putContextParameter(Network.class, network.getUuid()); - return network; + CallContext.current().setEventDetails("Network Id: " + network.getId()); + CallContext.current().putContextParameter(Network.class, network.getUuid()); + return network; } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 8ba8234c1ba8..ac8e53caddf9 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -5430,19 +5430,17 @@ public Vlan updateVlanAndPublicIpRange(final long id, String startIp, } try (CheckedReservation publicIpReservation = new CheckedReservation(account, ResourceType.public_ip, null, null, null, newIpAddressAmount, existingIpAddressAmount, reservationDao, _resourceLimitMgr)) { - - updateVlanAndIpv4Range(id, vlanRange, startIp, endIp, gateway, netmask, isRangeForSystemVM, forSystemVms); - - if (account != null) { - long countDiff = newIpAddressAmount - existingIpAddressAmount; - if (countDiff > 0) { - _resourceLimitMgr.incrementResourceCount(account.getId(), ResourceType.public_ip, countDiff); - } else if (countDiff < 0) { - _resourceLimitMgr.decrementResourceCount(account.getId(), ResourceType.public_ip, Math.abs(countDiff)); + updateVlanAndIpv4Range(id, vlanRange, startIp, endIp, gateway, netmask, isRangeForSystemVM, forSystemVms); + + if (account != null) { + long countDiff = newIpAddressAmount - existingIpAddressAmount; + if (countDiff > 0) { + _resourceLimitMgr.incrementResourceCount(account.getId(), ResourceType.public_ip, countDiff); + } else if (countDiff < 0) { + _resourceLimitMgr.decrementResourceCount(account.getId(), ResourceType.public_ip, Math.abs(countDiff)); + } } } - - } } if (ipv6) { updateVlanAndIpv6Range(id, vlanRange, startIpv6, endIpv6, ip6Gateway, ip6Cidr, isRangeForSystemVM, forSystemVms); @@ -5852,31 +5850,29 @@ public Vlan dedicatePublicIpRange(final DedicatePublicIpRangeCmd cmd) throws Res // Check Public IP resource limits long reservedIpAddressesAmount = vlanOwner != null ? _publicIpAddressDao.countIPs(zoneId, vlanDbId, false) : 0L; try (CheckedReservation publicIpReservation = new CheckedReservation(vlanOwner, ResourceType.public_ip, null, null, null, reservedIpAddressesAmount, null, reservationDao, _resourceLimitMgr)) { + if (vlanOwner != null) { + // Create an AccountVlanMapVO entry + final AccountVlanMapVO accountVlanMapVO = new AccountVlanMapVO(vlanOwner.getId(), vlan.getId()); + _accountVlanMapDao.persist(accountVlanMapVO); - if (vlanOwner != null) { - // Create an AccountVlanMapVO entry - final AccountVlanMapVO accountVlanMapVO = new AccountVlanMapVO(vlanOwner.getId(), vlan.getId()); - _accountVlanMapDao.persist(accountVlanMapVO); - - // generate usage event for dedication of every ip address in the range - for (final IPAddressVO ip : ips) { - final boolean usageHidden = _ipAddrMgr.isUsageHidden(ip); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NET_IP_ASSIGN, vlanOwner.getId(), ip.getDataCenterId(), ip.getId(), ip.getAddress().toString(), ip.isSourceNat(), - vlan.getVlanType().toString(), ip.getSystem(), usageHidden, ip.getClass().getName(), ip.getUuid()); + // generate usage event for dedication of every ip address in the range + for (final IPAddressVO ip : ips) { + final boolean usageHidden = _ipAddrMgr.isUsageHidden(ip); + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NET_IP_ASSIGN, vlanOwner.getId(), ip.getDataCenterId(), ip.getId(), ip.getAddress().toString(), ip.isSourceNat(), + vlan.getVlanType().toString(), ip.getSystem(), usageHidden, ip.getClass().getName(), ip.getUuid()); + } + } else if (domain != null) { + // Create an DomainVlanMapVO entry + DomainVlanMapVO domainVlanMapVO = new DomainVlanMapVO(domain.getId(), vlan.getId()); + _domainVlanMapDao.persist(domainVlanMapVO); } - } else if (domain != null) { - // Create an DomainVlanMapVO entry - DomainVlanMapVO domainVlanMapVO = new DomainVlanMapVO(domain.getId(), vlan.getId()); - _domainVlanMapDao.persist(domainVlanMapVO); - } - - // increment resource count for dedicated public ip's - if (vlanOwner != null) { - _resourceLimitMgr.incrementResourceCount(vlanOwner.getId(), ResourceType.public_ip, new Long(ips.size())); - } - return vlan; + // increment resource count for dedicated public ip's + if (vlanOwner != null) { + _resourceLimitMgr.incrementResourceCount(vlanOwner.getId(), ResourceType.public_ip, new Long(ips.size())); + } + return vlan; } } diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index c75fe4efbccd..9744961e0377 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -1150,21 +1150,19 @@ public IpAddress reserveIpAddress(Account account, Boolean displayIp, Long ipAdd long reservedIpAddressesAmount = ipDedicatedAccountId == null ? 1L : 0L; try (CheckedReservation publicIpAddressReservation = new CheckedReservation(account, Resource.ResourceType.public_ip, reservedIpAddressesAmount, reservationDao, _resourceLimitMgr)) { - - List maps = _accountVlanMapDao.listAccountVlanMapsByVlan(ipVO.getVlanId()); - ipVO.setAllocatedTime(new Date()); - ipVO.setAllocatedToAccountId(account.getAccountId()); - ipVO.setAllocatedInDomainId(account.getDomainId()); - ipVO.setState(State.Reserved); - if (displayIp != null) { - ipVO.setDisplay(displayIp); - } - ipVO = _ipAddressDao.persist(ipVO); - if (reservedIpAddressesAmount > 0) { - _resourceLimitMgr.incrementResourceCount(account.getId(), Resource.ResourceType.public_ip); - } - return ipVO; - + List maps = _accountVlanMapDao.listAccountVlanMapsByVlan(ipVO.getVlanId()); + ipVO.setAllocatedTime(new Date()); + ipVO.setAllocatedToAccountId(account.getAccountId()); + ipVO.setAllocatedInDomainId(account.getDomainId()); + ipVO.setState(State.Reserved); + if (displayIp != null) { + ipVO.setDisplay(displayIp); + } + ipVO = _ipAddressDao.persist(ipVO); + if (reservedIpAddressesAmount > 0) { + _resourceLimitMgr.incrementResourceCount(account.getId(), Resource.ResourceType.public_ip); + } + return ipVO; } catch (ResourceAllocationException ex) { logger.warn("Failed to allocate resource of type " + ex.getResourceType() + " for account " + account); throw new AccountLimitException("Maximum number of public IP addresses for account: " + account.getAccountName() + " has been exceeded."); diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java index 4a952bb582dc..74fb4160848f 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -1248,25 +1248,25 @@ public Vpc createVpc(final long zoneId, final long vpcOffId, final long vpcOwner vpc.setDisplay(Boolean.TRUE.equals(displayVpc)); try (CheckedReservation vpcReservation = new CheckedReservation(owner, ResourceType.vpc, null, null, 1L, reservationDao, _resourceLimitMgr)) { - if (vpc.getCidr() == null && cidrSize != null) { - // Allocate a CIDR for VPC - Ipv4GuestSubnetNetworkMap subnet = routedIpv4Manager.getOrCreateIpv4SubnetForVpc(vpc, cidrSize); - if (subnet != null) { - vpc.setCidr(subnet.getSubnet()); - } else { - throw new CloudRuntimeException("Failed to allocate a CIDR with requested size for VPC."); + if (vpc.getCidr() == null && cidrSize != null) { + // Allocate a CIDR for VPC + Ipv4GuestSubnetNetworkMap subnet = routedIpv4Manager.getOrCreateIpv4SubnetForVpc(vpc, cidrSize); + if (subnet != null) { + vpc.setCidr(subnet.getSubnet()); + } else { + throw new CloudRuntimeException("Failed to allocate a CIDR with requested size for VPC."); + } } - } - Vpc newVpc = createVpc(displayVpc, vpc); - // assign Ipv4 subnet to Routed VPC - if (routedIpv4Manager.isRoutedVpc(vpc)) { - routedIpv4Manager.assignIpv4SubnetToVpc(newVpc); - } - if (CollectionUtils.isNotEmpty(bgpPeerIds)) { - routedIpv4Manager.persistBgpPeersForVpc(newVpc.getId(), bgpPeerIds); - } - return newVpc; + Vpc newVpc = createVpc(displayVpc, vpc); + // assign Ipv4 subnet to Routed VPC + if (routedIpv4Manager.isRoutedVpc(vpc)) { + routedIpv4Manager.assignIpv4SubnetToVpc(newVpc); + } + if (CollectionUtils.isNotEmpty(bgpPeerIds)) { + routedIpv4Manager.persistBgpPeersForVpc(newVpc.getId(), bgpPeerIds); + } + return newVpc; } } diff --git a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java index 6942400c82c6..d165f0cd1b6b 100644 --- a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java +++ b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java @@ -277,40 +277,39 @@ public Project createProject(final String name, final String displayText, String } try (CheckedReservation projectReservation = new CheckedReservation(owner, ResourceType.project, null, null, 1L, reservationDao, _resourceLimitMgr)) { + final Account ownerFinal = owner; + User finalUser = user; + Project project = Transaction.execute(new TransactionCallback() { + @Override + public Project doInTransaction(TransactionStatus status) { - final Account ownerFinal = owner; - User finalUser = user; - Project project = Transaction.execute(new TransactionCallback() { - @Override - public Project doInTransaction(TransactionStatus status) { - - //Create an account associated with the project - StringBuilder acctNm = new StringBuilder("PrjAcct-"); - acctNm.append(name).append("-").append(ownerFinal.getDomainId()); + //Create an account associated with the project + StringBuilder acctNm = new StringBuilder("PrjAcct-"); + acctNm.append(name).append("-").append(ownerFinal.getDomainId()); - Account projectAccount = _accountMgr.createAccount(acctNm.toString(), Account.Type.PROJECT, null, domainId, null, null, UUID.randomUUID().toString()); + Account projectAccount = _accountMgr.createAccount(acctNm.toString(), Account.Type.PROJECT, null, domainId, null, null, UUID.randomUUID().toString()); - Project project = _projectDao.persist(new ProjectVO(name, displayText, ownerFinal.getDomainId(), projectAccount.getId())); + Project project = _projectDao.persist(new ProjectVO(name, displayText, ownerFinal.getDomainId(), projectAccount.getId())); - //assign owner to the project - assignAccountToProject(project, ownerFinal.getId(), ProjectAccount.Role.Admin, - Optional.ofNullable(finalUser).map(User::getId).orElse(null), null); + //assign owner to the project + assignAccountToProject(project, ownerFinal.getId(), ProjectAccount.Role.Admin, + Optional.ofNullable(finalUser).map(User::getId).orElse(null), null); - if (project != null) { - CallContext.current().setEventDetails("Project id=" + project.getId()); - CallContext.current().putContextParameter(Project.class, project.getUuid()); - } + if (project != null) { + CallContext.current().setEventDetails("Project id=" + project.getId()); + CallContext.current().putContextParameter(Project.class, project.getUuid()); + } - //Increment resource count - _resourceLimitMgr.incrementResourceCount(ownerFinal.getId(), ResourceType.project); + //Increment resource count + _resourceLimitMgr.incrementResourceCount(ownerFinal.getId(), ResourceType.project); - return project; - } - }); + return project; + } + }); - messageBus.publish(_name, ProjectManager.MESSAGE_CREATE_TUNGSTEN_PROJECT_EVENT, PublishScope.LOCAL, project); + messageBus.publish(_name, ProjectManager.MESSAGE_CREATE_TUNGSTEN_PROJECT_EVENT, PublishScope.LOCAL, project); - return project; + return project; } } @@ -671,13 +670,13 @@ private void updateProjectAccount(ProjectAccountVO futureOwner, Role newAccRole, boolean shouldIncrementResourceCount = Role.Admin == newAccRole; try (CheckedReservation checkedReservation = new CheckedReservation(account, ResourceType.project, shouldIncrementResourceCount ? 1L : 0L, reservationDao, _resourceLimitMgr)) { - futureOwner.setAccountRole(newAccRole); - _projectAccountDao.update(futureOwner.getId(), futureOwner); - if (shouldIncrementResourceCount) { - _resourceLimitMgr.incrementResourceCount(accountId, ResourceType.project); - } else { - _resourceLimitMgr.decrementResourceCount(accountId, ResourceType.project); - } + futureOwner.setAccountRole(newAccRole); + _projectAccountDao.update(futureOwner.getId(), futureOwner); + if (shouldIncrementResourceCount) { + _resourceLimitMgr.incrementResourceCount(accountId, ResourceType.project); + } else { + _resourceLimitMgr.decrementResourceCount(accountId, ResourceType.project); + } } } @@ -877,16 +876,16 @@ public boolean addAccountToProject(long projectId, String accountName, String em boolean shouldIncrementResourceCount = projectRoleType != null && Role.Admin == projectRoleType; try (CheckedReservation cr = new CheckedReservation(account, ResourceType.project, shouldIncrementResourceCount ? 1L : 0L, reservationDao, _resourceLimitMgr)) { - if (assignAccountToProject(project, account.getId(), projectRoleType, null, - Optional.ofNullable(projectRole).map(ProjectRole::getId).orElse(null)) != null) { - if (shouldIncrementResourceCount) { - _resourceLimitMgr.incrementResourceCount(account.getId(), ResourceType.project); + if (assignAccountToProject(project, account.getId(), projectRoleType, null, + Optional.ofNullable(projectRole).map(ProjectRole::getId).orElse(null)) != null) { + if (shouldIncrementResourceCount) { + _resourceLimitMgr.incrementResourceCount(account.getId(), ResourceType.project); + } + return true; + } else { + logger.warn("Failed to add account {} to project {}", accountName, project); + return false; } - return true; - } else { - logger.warn("Failed to add account {} to project {}", accountName, project); - return false; - } } } } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 7186b07334df..026a9ae1dd97 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -431,9 +431,8 @@ public VolumeVO uploadVolume(UploadVolumeCmd cmd) throws ResourceAllocationExcep List reservations = new ArrayList<>(); try { - - validateVolume(caller, ownerId, zoneId, volumeName, url, format, diskOfferingId, reservations); - volume = persistVolume(owner, zoneId, volumeName, url, format, diskOfferingId, Volume.State.Allocated); + validateVolume(caller, ownerId, zoneId, volumeName, url, format, diskOfferingId, reservations); + volume = persistVolume(owner, zoneId, volumeName, url, format, diskOfferingId, Volume.State.Allocated); } finally { ReservationHelper.closeAll(reservations); @@ -479,74 +478,74 @@ public GetUploadParamsResponse uploadVolume(final GetUploadParamsForVolumeCmd cm List reservations = new ArrayList<>(); try { - validateVolume(caller, ownerId, zoneId, volumeName, null, format, diskOfferingId, reservations); + validateVolume(caller, ownerId, zoneId, volumeName, null, format, diskOfferingId, reservations); - return Transaction.execute(new TransactionCallbackWithException() { - @Override - public GetUploadParamsResponse doInTransaction(TransactionStatus status) throws MalformedURLException { + return Transaction.execute(new TransactionCallbackWithException() { + @Override + public GetUploadParamsResponse doInTransaction(TransactionStatus status) throws MalformedURLException { - VolumeVO volume = persistVolume(owner, zoneId, volumeName, null, format, diskOfferingId, Volume.State.NotUploaded); + VolumeVO volume = persistVolume(owner, zoneId, volumeName, null, format, diskOfferingId, Volume.State.NotUploaded); - final DataStore store = _tmpltMgr.getImageStore(imageStoreUuid, zoneId, volume); + final DataStore store = _tmpltMgr.getImageStore(imageStoreUuid, zoneId, volume); - VolumeInfo vol = volFactory.getVolume(volume.getId()); + VolumeInfo vol = volFactory.getVolume(volume.getId()); - RegisterVolumePayload payload = new RegisterVolumePayload(null, cmd.getChecksum(), format); - vol.addPayload(payload); + RegisterVolumePayload payload = new RegisterVolumePayload(null, cmd.getChecksum(), format); + vol.addPayload(payload); - Pair pair = volService.registerVolumeForPostUpload(vol, store); - EndPoint ep = pair.first(); - DataObject dataObject = pair.second(); + Pair pair = volService.registerVolumeForPostUpload(vol, store); + EndPoint ep = pair.first(); + DataObject dataObject = pair.second(); - GetUploadParamsResponse response = new GetUploadParamsResponse(); + GetUploadParamsResponse response = new GetUploadParamsResponse(); - String ssvmUrlDomain = _configDao.getValue(Config.SecStorageSecureCopyCert.key()); - String protocol = UseHttpsToUpload.value() ? "https" : "http"; + String ssvmUrlDomain = _configDao.getValue(Config.SecStorageSecureCopyCert.key()); + String protocol = UseHttpsToUpload.value() ? "https" : "http"; - String url = ImageStoreUtil.generatePostUploadUrl(ssvmUrlDomain, ep.getPublicAddr(), vol.getUuid(), protocol); - response.setPostURL(new URL(url)); + String url = ImageStoreUtil.generatePostUploadUrl(ssvmUrlDomain, ep.getPublicAddr(), vol.getUuid(), protocol); + response.setPostURL(new URL(url)); - // set the post url, this is used in the monitoring thread to determine the SSVM - VolumeDataStoreVO volumeStore = _volumeStoreDao.findByVolume(vol.getId()); - assert (volumeStore != null) : "sincle volume is registered, volumestore cannot be null at this stage"; - volumeStore.setExtractUrl(url); - _volumeStoreDao.persist(volumeStore); + // set the post url, this is used in the monitoring thread to determine the SSVM + VolumeDataStoreVO volumeStore = _volumeStoreDao.findByVolume(vol.getId()); + assert (volumeStore != null) : "sincle volume is registered, volumestore cannot be null at this stage"; + volumeStore.setExtractUrl(url); + _volumeStoreDao.persist(volumeStore); - response.setId(UUID.fromString(vol.getUuid())); + response.setId(UUID.fromString(vol.getUuid())); - int timeout = ImageStoreUploadMonitorImpl.getUploadOperationTimeout(); - DateTime currentDateTime = new DateTime(DateTimeZone.UTC); - String expires = currentDateTime.plusMinutes(timeout).toString(); - response.setTimeout(expires); + int timeout = ImageStoreUploadMonitorImpl.getUploadOperationTimeout(); + DateTime currentDateTime = new DateTime(DateTimeZone.UTC); + String expires = currentDateTime.plusMinutes(timeout).toString(); + response.setTimeout(expires); - String key = _configDao.getValue(Config.SSVMPSK.key()); - /* - * encoded metadata using the post upload config key - */ - TemplateOrVolumePostUploadCommand command = new TemplateOrVolumePostUploadCommand(vol.getId(), vol.getUuid(), volumeStore.getInstallPath(), cmd.getChecksum(), vol.getType().toString(), - vol.getName(), vol.getFormat().toString(), dataObject.getDataStore().getUri(), dataObject.getDataStore().getRole().toString()); - command.setLocalPath(volumeStore.getLocalDownloadPath()); - //using the existing max upload size configuration - command.setProcessTimeout(NumbersUtil.parseLong(_configDao.getValue("vmware.package.ova.timeout"), 3600)); - command.setMaxUploadSize(_configDao.getValue(Config.MaxUploadVolumeSize.key())); + String key = _configDao.getValue(Config.SSVMPSK.key()); + /* + * encoded metadata using the post upload config key + */ + TemplateOrVolumePostUploadCommand command = new TemplateOrVolumePostUploadCommand(vol.getId(), vol.getUuid(), volumeStore.getInstallPath(), cmd.getChecksum(), vol.getType().toString(), + vol.getName(), vol.getFormat().toString(), dataObject.getDataStore().getUri(), dataObject.getDataStore().getRole().toString()); + command.setLocalPath(volumeStore.getLocalDownloadPath()); + //using the existing max upload size configuration + command.setProcessTimeout(NumbersUtil.parseLong(_configDao.getValue("vmware.package.ova.timeout"), 3600)); + command.setMaxUploadSize(_configDao.getValue(Config.MaxUploadVolumeSize.key())); - long accountId = vol.getAccountId(); - Account account = _accountDao.findById(accountId); - Domain domain = domainDao.findById(account.getDomainId()); + long accountId = vol.getAccountId(); + Account account = _accountDao.findById(accountId); + Domain domain = domainDao.findById(account.getDomainId()); - command.setDefaultMaxSecondaryStorageInBytes(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null)); - command.setAccountId(accountId); - Gson gson = new GsonBuilder().create(); - String metadata = EncryptionUtil.encodeData(gson.toJson(command), key); - response.setMetadata(metadata); + command.setDefaultMaxSecondaryStorageInBytes(_resourceLimitMgr.findCorrectResourceLimitForAccountAndDomain(account, domain, ResourceType.secondary_storage, null)); + command.setAccountId(accountId); + Gson gson = new GsonBuilder().create(); + String metadata = EncryptionUtil.encodeData(gson.toJson(command), key); + response.setMetadata(metadata); - /* - * signature calculated on the url, expiry, metadata. - */ - response.setSignature(EncryptionUtil.generateSignature(metadata + url + expires, key)); - return response; - } - }); + /* + * signature calculated on the url, expiry, metadata. + */ + response.setSignature(EncryptionUtil.generateSignature(metadata + url + expires, key)); + return response; + } + }); } finally { ReservationHelper.closeAll(reservations); @@ -945,29 +944,29 @@ public VolumeVO allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationExcept List reservations = new ArrayList<>(); try { - _resourceLimitMgr.checkVolumeResourceLimit(owner, displayVolume, size, diskOffering, reservations); + _resourceLimitMgr.checkVolumeResourceLimit(owner, displayVolume, size, diskOffering, reservations); - // Verify that zone exists - DataCenterVO zone = _dcDao.findById(zoneId); - if (zone == null) { - throw new InvalidParameterValueException("Unable to find zone by id " + zoneId); - } + // Verify that zone exists + DataCenterVO zone = _dcDao.findById(zoneId); + if (zone == null) { + throw new InvalidParameterValueException("Unable to find zone by id " + zoneId); + } - // Check if zone is disabled - if (Grouping.AllocationState.Disabled == zone.getAllocationState() && !_accountMgr.isRootAdmin(caller.getId())) { - throw new PermissionDeniedException(String.format("Cannot perform this operation, Zone: %s is currently disabled", zone)); - } + // Check if zone is disabled + if (Grouping.AllocationState.Disabled == zone.getAllocationState() && !_accountMgr.isRootAdmin(caller.getId())) { + throw new PermissionDeniedException(String.format("Cannot perform this operation, Zone: %s is currently disabled", zone)); + } - // If local storage is disabled then creation of volume with local disk - // offering not allowed - if (!zone.isLocalStorageEnabled() && diskOffering.isUseLocalStorage()) { - throw new InvalidParameterValueException("Zone is not configured to use local storage but volume's disk offering " + diskOffering.getName() + " uses it"); - } + // If local storage is disabled then creation of volume with local disk + // offering not allowed + if (!zone.isLocalStorageEnabled() && diskOffering.isUseLocalStorage()) { + 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(cmd); - return commitVolume(cmd, caller, owner, displayVolume, zoneId, diskOfferingId, provisioningType, size, minIops, maxIops, parentVolume, userSpecifiedName, - _uuidMgr.generateUuid(Volume.class, cmd.getCustomId()), details); + return commitVolume(cmd, caller, owner, displayVolume, zoneId, diskOfferingId, provisioningType, size, minIops, maxIops, parentVolume, userSpecifiedName, + _uuidMgr.generateUuid(Volume.class, cmd.getCustomId()), details); } finally { ReservationHelper.closeAll(reservations); } @@ -1298,134 +1297,134 @@ public VolumeVO resizeVolume(ResizeVolumeCmd cmd) throws ResourceAllocationExcep List reservations = new ArrayList<>(); try { - validateVolumeResizeWithSize(volume, currentSize, newSize, shrinkOk, diskOffering, newDiskOffering, reservations); + validateVolumeResizeWithSize(volume, currentSize, newSize, shrinkOk, diskOffering, newDiskOffering, reservations); - // Note: The storage plug-in in question should perform validation on the IOPS to check if a sufficient number of IOPS is available to perform - // the requested change + // Note: The storage plug-in in question should perform validation on the IOPS to check if a sufficient number of IOPS is available to perform + // the requested change - /* If this volume has never been beyond allocated state, short circuit everything and simply update the database. */ - // We need to publish this event to usage_volume table - if (volume.getState() == Volume.State.Allocated) { - logger.debug("Volume is in the allocated state, but has never been created. Simply updating database with new size and IOPS."); + /* If this volume has never been beyond allocated state, short circuit everything and simply update the database. */ + // We need to publish this event to usage_volume table + if (volume.getState() == Volume.State.Allocated) { + logger.debug("Volume is in the allocated state, but has never been created. Simply updating database with new size and IOPS."); - volume.setSize(newSize); - volume.setMinIops(newMinIops); - volume.setMaxIops(newMaxIops); - volume.setHypervisorSnapshotReserve(newHypervisorSnapshotReserve); + volume.setSize(newSize); + volume.setMinIops(newMinIops); + volume.setMaxIops(newMaxIops); + volume.setHypervisorSnapshotReserve(newHypervisorSnapshotReserve); - if (newDiskOffering != null) { - volume.setDiskOfferingId(cmd.getNewDiskOfferingId()); - } + if (newDiskOffering != null) { + volume.setDiskOfferingId(cmd.getNewDiskOfferingId()); + } - _volsDao.update(volume.getId(), volume); - _resourceLimitMgr.updateVolumeResourceCountForDiskOfferingChange(volume.getAccountId(), volume.isDisplayVolume(), currentSize, newSize, - diskOffering, newDiskOffering); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_RESIZE, volume.getAccountId(), volume.getDataCenterId(), volume.getId(), volume.getName(), - volume.getDiskOfferingId(), volume.getTemplateId(), volume.getSize(), Volume.class.getName(), volume.getUuid()); - return volume; - } + _volsDao.update(volume.getId(), volume); + _resourceLimitMgr.updateVolumeResourceCountForDiskOfferingChange(volume.getAccountId(), volume.isDisplayVolume(), currentSize, newSize, + diskOffering, newDiskOffering); + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_RESIZE, volume.getAccountId(), volume.getDataCenterId(), volume.getId(), volume.getName(), + volume.getDiskOfferingId(), volume.getTemplateId(), volume.getSize(), Volume.class.getName(), volume.getUuid()); + return volume; + } - Long newDiskOfferingId = newDiskOffering != null ? newDiskOffering.getId() : diskOffering.getId(); + Long newDiskOfferingId = newDiskOffering != null ? newDiskOffering.getId() : diskOffering.getId(); - boolean volumeMigrateRequired = false; - List suitableStoragePoolsWithEnoughSpace = null; - StoragePoolVO storagePool = _storagePoolDao.findById(volume.getPoolId()); - if (!storageMgr.storagePoolHasEnoughSpaceForResize(storagePool, currentSize, newSize)) { - if (!autoMigrateVolume) { - throw new CloudRuntimeException(String.format("Failed to resize volume %s since the storage pool does not have enough space to accommodate new size for the volume %s, try with automigrate set to true in order to check in the other suitable pools for the new size and then migrate & resize volume there.", volume.getUuid(), volume.getName())); - } - Pair, List> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOfferingId, currentSize, newMinIops, newMaxIops, true, false); - List suitableStoragePools = poolsPair.second(); - if (CollectionUtils.isEmpty(poolsPair.first()) && CollectionUtils.isEmpty(poolsPair.second())) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume resize failed for volume ID: %s as no suitable pool(s) found for migrating to support new disk offering or new size", volume.getUuid())); - } - final Long newSizeFinal = newSize; - suitableStoragePoolsWithEnoughSpace = suitableStoragePools.stream().filter(pool -> storageMgr.storagePoolHasEnoughSpaceForResize(pool, 0L, newSizeFinal)).collect(Collectors.toList()); - if (CollectionUtils.isEmpty(suitableStoragePoolsWithEnoughSpace)) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume resize failed for volume ID: %s as no suitable pool(s) with enough space found.", volume.getUuid())); + boolean volumeMigrateRequired = false; + List suitableStoragePoolsWithEnoughSpace = null; + StoragePoolVO storagePool = _storagePoolDao.findById(volume.getPoolId()); + if (!storageMgr.storagePoolHasEnoughSpaceForResize(storagePool, currentSize, newSize)) { + if (!autoMigrateVolume) { + throw new CloudRuntimeException(String.format("Failed to resize volume %s since the storage pool does not have enough space to accommodate new size for the volume %s, try with automigrate set to true in order to check in the other suitable pools for the new size and then migrate & resize volume there.", volume.getUuid(), volume.getName())); + } + Pair, List> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOfferingId, currentSize, newMinIops, newMaxIops, true, false); + List suitableStoragePools = poolsPair.second(); + if (CollectionUtils.isEmpty(poolsPair.first()) && CollectionUtils.isEmpty(poolsPair.second())) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume resize failed for volume ID: %s as no suitable pool(s) found for migrating to support new disk offering or new size", volume.getUuid())); + } + final Long newSizeFinal = newSize; + suitableStoragePoolsWithEnoughSpace = suitableStoragePools.stream().filter(pool -> storageMgr.storagePoolHasEnoughSpaceForResize(pool, 0L, newSizeFinal)).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(suitableStoragePoolsWithEnoughSpace)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume resize failed for volume ID: %s as no suitable pool(s) with enough space found.", volume.getUuid())); + } + Collections.shuffle(suitableStoragePoolsWithEnoughSpace); + volumeMigrateRequired = true; } - Collections.shuffle(suitableStoragePoolsWithEnoughSpace); - volumeMigrateRequired = true; - } - boolean volumeResizeRequired = false; - if (currentSize != newSize || !compareEqualsIncludingNullOrZero(newMaxIops, volume.getMaxIops()) || !compareEqualsIncludingNullOrZero(newMinIops, volume.getMinIops())) { - volumeResizeRequired = true; - } - if (!volumeMigrateRequired && !volumeResizeRequired && newDiskOffering != null) { - _volsDao.updateDiskOffering(volume.getId(), newDiskOffering.getId()); - volume = _volsDao.findById(volume.getId()); - updateStorageWithTheNewDiskOffering(volume, newDiskOffering); + boolean volumeResizeRequired = false; + if (currentSize != newSize || !compareEqualsIncludingNullOrZero(newMaxIops, volume.getMaxIops()) || !compareEqualsIncludingNullOrZero(newMinIops, volume.getMinIops())) { + volumeResizeRequired = true; + } + if (!volumeMigrateRequired && !volumeResizeRequired && newDiskOffering != null) { + _volsDao.updateDiskOffering(volume.getId(), newDiskOffering.getId()); + volume = _volsDao.findById(volume.getId()); + updateStorageWithTheNewDiskOffering(volume, newDiskOffering); - return volume; - } + return volume; + } - if (volumeMigrateRequired) { - MigrateVolumeCmd migrateVolumeCmd = new MigrateVolumeCmd(volume.getId(), suitableStoragePoolsWithEnoughSpace.get(0).getId(), newDiskOfferingId, true); - try { - Volume result = migrateVolume(migrateVolumeCmd); - volume = (result != null) ? _volsDao.findById(result.getId()) : null; - if (volume == null) { + if (volumeMigrateRequired) { + MigrateVolumeCmd migrateVolumeCmd = new MigrateVolumeCmd(volume.getId(), suitableStoragePoolsWithEnoughSpace.get(0).getId(), newDiskOfferingId, true); + try { + Volume result = migrateVolume(migrateVolumeCmd); + volume = (result != null) ? _volsDao.findById(result.getId()) : null; + if (volume == null) { + throw new CloudRuntimeException(String.format("Volume resize operation failed for volume ID: %s as migration failed to storage pool %s accommodating new size", volume.getUuid(), suitableStoragePoolsWithEnoughSpace.get(0).getId())); + } + } catch (Exception e) { throw new CloudRuntimeException(String.format("Volume resize operation failed for volume ID: %s as migration failed to storage pool %s accommodating new size", volume.getUuid(), suitableStoragePoolsWithEnoughSpace.get(0).getId())); } - } catch (Exception e) { - throw new CloudRuntimeException(String.format("Volume resize operation failed for volume ID: %s as migration failed to storage pool %s accommodating new size", volume.getUuid(), suitableStoragePoolsWithEnoughSpace.get(0).getId())); } - } - UserVmVO userVm = _userVmDao.findById(volume.getInstanceId()); + UserVmVO userVm = _userVmDao.findById(volume.getInstanceId()); - if (userVm != null) { - // serialize VM operation - AsyncJobExecutionContext jobContext = AsyncJobExecutionContext.getCurrentExecutionContext(); + if (userVm != null) { + // serialize VM operation + AsyncJobExecutionContext jobContext = AsyncJobExecutionContext.getCurrentExecutionContext(); - if (jobContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) { - // avoid re-entrance + if (jobContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) { + // avoid re-entrance - VmWorkJobVO placeHolder = null; + VmWorkJobVO placeHolder = null; - placeHolder = createPlaceHolderWork(userVm.getId()); + placeHolder = createPlaceHolderWork(userVm.getId()); - try { - return orchestrateResizeVolume(volume.getId(), currentSize, newSize, newMinIops, newMaxIops, newHypervisorSnapshotReserve, + try { + return orchestrateResizeVolume(volume.getId(), currentSize, newSize, newMinIops, newMaxIops, newHypervisorSnapshotReserve, + newDiskOffering != null ? cmd.getNewDiskOfferingId() : null, shrinkOk); + } finally { + _workJobDao.expunge(placeHolder.getId()); + } + } else { + Outcome outcome = resizeVolumeThroughJobQueue(userVm.getId(), volume.getId(), currentSize, newSize, newMinIops, newMaxIops, newHypervisorSnapshotReserve, newDiskOffering != null ? cmd.getNewDiskOfferingId() : null, shrinkOk); - } finally { - _workJobDao.expunge(placeHolder.getId()); - } - } else { - Outcome outcome = resizeVolumeThroughJobQueue(userVm.getId(), volume.getId(), currentSize, newSize, newMinIops, newMaxIops, newHypervisorSnapshotReserve, - newDiskOffering != null ? cmd.getNewDiskOfferingId() : null, shrinkOk); - - try { - outcome.get(); - } catch (InterruptedException e) { - throw new RuntimeException("Operation was interrupted", e); - } catch (ExecutionException e) { - throw new RuntimeException("Execution exception", e); - } - Object jobResult = _jobMgr.unmarshallResultObject(outcome.getJob()); + try { + outcome.get(); + } catch (InterruptedException e) { + throw new RuntimeException("Operation was interrupted", e); + } catch (ExecutionException e) { + throw new RuntimeException("Execution exception", e); + } - if (jobResult != null) { - if (jobResult instanceof ConcurrentOperationException) { - throw (ConcurrentOperationException) jobResult; - } else if (jobResult instanceof ResourceAllocationException) { - throw (ResourceAllocationException) jobResult; - } else if (jobResult instanceof RuntimeException) { - throw (RuntimeException) jobResult; - } else if (jobResult instanceof Throwable) { - throw new RuntimeException("Unexpected exception", (Throwable) jobResult); - } else if (jobResult instanceof Long) { - return _volsDao.findById((Long) jobResult); + Object jobResult = _jobMgr.unmarshallResultObject(outcome.getJob()); + + if (jobResult != null) { + if (jobResult instanceof ConcurrentOperationException) { + throw (ConcurrentOperationException) jobResult; + } else if (jobResult instanceof ResourceAllocationException) { + throw (ResourceAllocationException) jobResult; + } else if (jobResult instanceof RuntimeException) { + throw (RuntimeException) jobResult; + } else if (jobResult instanceof Throwable) { + throw new RuntimeException("Unexpected exception", (Throwable) jobResult); + } else if (jobResult instanceof Long) { + return _volsDao.findById((Long) jobResult); + } } - } - return volume; + return volume; + } } - } - return orchestrateResizeVolume(volume.getId(), currentSize, newSize, newMinIops, newMaxIops, newHypervisorSnapshotReserve, newDiskOffering != null ? cmd.getNewDiskOfferingId() : null, - shrinkOk); + return orchestrateResizeVolume(volume.getId(), currentSize, newSize, newMinIops, newMaxIops,newHypervisorSnapshotReserve, + newDiskOffering != null ? cmd.getNewDiskOfferingId() : null, shrinkOk); } finally { ReservationHelper.closeAll(reservations); @@ -2116,96 +2115,96 @@ public Volume changeDiskOfferingForVolumeInternal(Long volumeId, Long newDiskOff List reservations = new ArrayList<>(); try { - validateVolumeResizeWithSize(volume, currentSize, newSize, shrinkOk, existingDiskOffering, newDiskOffering, reservations); + validateVolumeResizeWithSize(volume, currentSize, newSize, shrinkOk, existingDiskOffering, newDiskOffering, reservations); - /* If this volume has never been beyond allocated state, short circuit everything and simply update the database. */ - // We need to publish this event to usage_volume table - if (volume.getState() == Volume.State.Allocated) { - logger.debug("Volume {} is in the allocated state, but has never been created. Simply updating database with new size and IOPS.", volume); + /* If this volume has never been beyond allocated state, short circuit everything and simply update the database. */ + // We need to publish this event to usage_volume table + if (volume.getState() == Volume.State.Allocated) { + logger.debug("Volume {} is in the allocated state, but has never been created. Simply updating database with new size and IOPS.", volume); - volume.setSize(newSize); - volume.setMinIops(newMinIops); - volume.setMaxIops(newMaxIops); - volume.setHypervisorSnapshotReserve(newHypervisorSnapshotReserve); + volume.setSize(newSize); + volume.setMinIops(newMinIops); + volume.setMaxIops(newMaxIops); + volume.setHypervisorSnapshotReserve(newHypervisorSnapshotReserve); - if (newDiskOffering != null) { - volume.setDiskOfferingId(newDiskOfferingId); - _volumeMgr.saveVolumeDetails(newDiskOfferingId, volume.getId()); - } + if (newDiskOffering != null) { + volume.setDiskOfferingId(newDiskOfferingId); + _volumeMgr.saveVolumeDetails(newDiskOfferingId, volume.getId()); + } - _volsDao.update(volume.getId(), volume); - _resourceLimitMgr.updateVolumeResourceCountForDiskOfferingChange(volume.getAccountId(), volume.isDisplayVolume(), currentSize, newSize, - existingDiskOffering, newDiskOffering); + _volsDao.update(volume.getId(), volume); + _resourceLimitMgr.updateVolumeResourceCountForDiskOfferingChange(volume.getAccountId(), volume.isDisplayVolume(), currentSize, newSize, + existingDiskOffering, newDiskOffering); - if (currentSize != newSize) { - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_RESIZE, volume.getAccountId(), volume.getDataCenterId(), volume.getId(), volume.getName(), - volume.getDiskOfferingId(), volume.getTemplateId(), volume.getSize(), Volume.class.getName(), volume.getUuid()); + if (currentSize != newSize) { + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_RESIZE, volume.getAccountId(), volume.getDataCenterId(), volume.getId(), volume.getName(), + volume.getDiskOfferingId(), volume.getTemplateId(), volume.getSize(), Volume.class.getName(), volume.getUuid()); + } + return volume; } - return volume; - } - if (currentSize != newSize || !compareEqualsIncludingNullOrZero(newMaxIops, volume.getMaxIops()) || !compareEqualsIncludingNullOrZero(newMinIops, volume.getMinIops())) { - volumeResizeRequired = true; - validateVolumeReadyStateAndHypervisorChecks(volume, currentSize, newSize); - } + if (currentSize != newSize || !compareEqualsIncludingNullOrZero(newMaxIops, volume.getMaxIops()) || !compareEqualsIncludingNullOrZero(newMinIops, volume.getMinIops())) { + volumeResizeRequired = true; + validateVolumeReadyStateAndHypervisorChecks(volume, currentSize, newSize); + } - StoragePoolVO existingStoragePool = _storagePoolDao.findById(volume.getPoolId()); + StoragePoolVO existingStoragePool = _storagePoolDao.findById(volume.getPoolId()); - Pair, List> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOffering.getId(), currentSize, newMinIops, newMaxIops, true, false); - List suitableStoragePools = poolsPair.second(); + Pair, List> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOffering.getId(), currentSize, newMinIops, newMaxIops, true, false); + List suitableStoragePools = poolsPair.second(); - if (!suitableStoragePools.stream().anyMatch(p -> (p.getId() == existingStoragePool.getId()))) { - volumeMigrateRequired = true; - if (!autoMigrateVolume) { - throw new InvalidParameterValueException(String.format("Failed to change offering for volume %s since automigrate is set to false but volume needs to migrated", volume.getUuid())); + if (!suitableStoragePools.stream().anyMatch(p -> (p.getId() == existingStoragePool.getId()))) { + volumeMigrateRequired = true; + if (!autoMigrateVolume) { + throw new InvalidParameterValueException(String.format("Failed to change offering for volume %s since automigrate is set to false but volume needs to migrated", volume.getUuid())); + } } - } - if (!volumeMigrateRequired && !volumeResizeRequired) { - _volsDao.updateDiskOffering(volume.getId(), newDiskOffering.getId()); - volume = _volsDao.findById(volume.getId()); - updateStorageWithTheNewDiskOffering(volume, newDiskOffering); - - return volume; - } + if (!volumeMigrateRequired && !volumeResizeRequired) { + _volsDao.updateDiskOffering(volume.getId(), newDiskOffering.getId()); + volume = _volsDao.findById(volume.getId()); + updateStorageWithTheNewDiskOffering(volume, newDiskOffering); - if (volumeMigrateRequired) { - if (CollectionUtils.isEmpty(poolsPair.first()) && CollectionUtils.isEmpty(poolsPair.second())) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume change offering operation failed for volume: %s as no suitable pool(s) found for migrating to support new disk offering", volume)); - } - final Long newSizeFinal = newSize; - List suitableStoragePoolsWithEnoughSpace = suitableStoragePools.stream().filter(pool -> storageMgr.storagePoolHasEnoughSpaceForResize(pool, 0L, newSizeFinal)).collect(Collectors.toList()); - if (CollectionUtils.isEmpty(suitableStoragePoolsWithEnoughSpace)) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume change offering operation failed for volume: %s as no suitable pool(s) with enough space found for volume migration.", volume)); + return volume; } - Collections.shuffle(suitableStoragePoolsWithEnoughSpace); - MigrateVolumeCmd migrateVolumeCmd = new MigrateVolumeCmd(volume.getId(), suitableStoragePoolsWithEnoughSpace.get(0).getId(), newDiskOffering.getId(), true); - try { - Volume result = migrateVolume(migrateVolumeCmd); - volume = (result != null) ? _volsDao.findById(result.getId()) : null; - if (volume == null) { - throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume: %s migration failed to storage pool %s", volume, suitableStoragePools.get(0))); + + if (volumeMigrateRequired) { + if (CollectionUtils.isEmpty(poolsPair.first()) && CollectionUtils.isEmpty(poolsPair.second())) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume change offering operation failed for volume: %s as no suitable pool(s) found for migrating to support new disk offering", volume)); + } + final Long newSizeFinal = newSize; + List suitableStoragePoolsWithEnoughSpace = suitableStoragePools.stream().filter(pool -> storageMgr.storagePoolHasEnoughSpaceForResize(pool, 0L, newSizeFinal)).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(suitableStoragePoolsWithEnoughSpace)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume change offering operation failed for volume: %s as no suitable pool(s) with enough space found for volume migration.", volume)); + } + Collections.shuffle(suitableStoragePoolsWithEnoughSpace); + MigrateVolumeCmd migrateVolumeCmd = new MigrateVolumeCmd(volume.getId(), suitableStoragePoolsWithEnoughSpace.get(0).getId(), newDiskOffering.getId(), true); + try { + Volume result = migrateVolume(migrateVolumeCmd); + volume = (result != null) ? _volsDao.findById(result.getId()) : null; + if (volume == null) { + throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume: %s migration failed to storage pool %s", volume, suitableStoragePools.get(0))); + } + } catch (Exception e) { + throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume: %s migration failed to storage pool %s due to %s", volume, suitableStoragePools.get(0), e.getMessage())); } - } catch (Exception e) { - throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume: %s migration failed to storage pool %s due to %s", volume, suitableStoragePools.get(0), e.getMessage())); } - } - if (volumeResizeRequired) { - // refresh volume data - volume = _volsDao.findById(volume.getId()); - try { - volume = resizeVolumeInternal(volume, newDiskOffering, currentSize, newSize, newMinIops, newMaxIops, newHypervisorSnapshotReserve, shrinkOk); - } catch (Exception e) { - if (volumeMigrateRequired) { - logger.warn(String.format("Volume change offering operation succeeded for volume ID: %s but volume resize operation failed, so please try resize volume operation separately", volume.getUuid())); - } else { - throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume ID: %s due to resize volume operation failed", volume.getUuid())); + if (volumeResizeRequired) { + // refresh volume data + volume = _volsDao.findById(volume.getId()); + try { + volume = resizeVolumeInternal(volume, newDiskOffering, currentSize, newSize, newMinIops, newMaxIops, newHypervisorSnapshotReserve, shrinkOk); + } catch (Exception e) { + if (volumeMigrateRequired) { + logger.warn(String.format("Volume change offering operation succeeded for volume ID: %s but volume resize operation failed, so please try resize volume operation separately", volume.getUuid())); + } else { + throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume ID: %s due to resize volume operation failed", volume.getUuid())); + } } } - } - return volume; + return volume; } finally { ReservationHelper.closeAll(reservations); @@ -2690,13 +2689,13 @@ public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean try (CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, resourceLimitStorageTags, requiredPrimaryStorageSpace, reservationDao, _resourceLimitMgr)) { - _jobMgr.updateAsyncJobAttachment(job.getId(), "Volume", volumeId); + _jobMgr.updateAsyncJobAttachment(job.getId(), "Volume", volumeId); - if (asyncExecutionContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) { - return safelyOrchestrateAttachVolume(vmId, volumeId, deviceId); - } else { - return getVolumeAttachJobResult(vmId, volumeId, deviceId); - } + if (asyncExecutionContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) { + return safelyOrchestrateAttachVolume(vmId, volumeId, deviceId); + } else { + return getVolumeAttachJobResult(vmId, volumeId, deviceId); + } } catch (ResourceAllocationException e) { logger.error("primary storage resource limit check failed", e); @@ -4247,16 +4246,16 @@ public Volume assignVolumeToAccount(AssignVolumeCmd command) throws ResourceAllo List reservations = new ArrayList<>(); try { - _resourceLimitMgr.checkVolumeResourceLimit(newAccount, true, volume.getSize(), _diskOfferingDao.findById(volume.getDiskOfferingId()), reservations); + _resourceLimitMgr.checkVolumeResourceLimit(newAccount, true, volume.getSize(), _diskOfferingDao.findById(volume.getDiskOfferingId()), reservations); - Transaction.execute(new TransactionCallbackNoReturn() { - @Override - public void doInTransactionWithoutResult(TransactionStatus status) { - updateVolumeAccount(oldAccount, volume, newAccount); - } - }); + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + updateVolumeAccount(oldAccount, volume, newAccount); + } + }); - return volume; + return volume; } finally { ReservationHelper.closeAll(reservations); diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 13144893f52f..2f397de2bb9e 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -404,23 +404,23 @@ public VirtualMachineTemplate registerTemplate(RegisterTemplateCmd cmd) throws U try (CheckedReservation templateReservation = new CheckedReservation(owner, ResourceType.template, null, null, 1L, reservationDao, _resourceLimitMgr); CheckedReservation secondaryStorageReservation = new CheckedReservation(owner, ResourceType.secondary_storage, null, null, secondaryStorageUsage, reservationDao, _resourceLimitMgr)) { - TemplateProfile profile = adapter.prepare(cmd); - VMTemplateVO template = adapter.create(profile); + TemplateProfile profile = adapter.prepare(cmd); + VMTemplateVO template = adapter.create(profile); - // Secondary storage resource usage will be incremented in com.cloud.template.HypervisorTemplateAdapter.createTemplateAsyncCallBack - // for HypervisorTemplateAdapter - _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); - if (secondaryStorageUsage > 0) { - _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.secondary_storage, secondaryStorageUsage); - } + // Secondary storage resource usage will be incremented in com.cloud.template.HypervisorTemplateAdapter.createTemplateAsyncCallBack + // for HypervisorTemplateAdapter + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); + if (secondaryStorageUsage > 0) { + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.secondary_storage, secondaryStorageUsage); + } - if (template != null) { - CallContext.current().putContextParameter(VirtualMachineTemplate.class, template.getUuid()); - if (cmd instanceof RegisterVnfTemplateCmd) { - vnfTemplateManager.persistVnfTemplate(template.getId(), (RegisterVnfTemplateCmd) cmd); + if (template != null) { + CallContext.current().putContextParameter(VirtualMachineTemplate.class, template.getUuid()); + if (cmd instanceof RegisterVnfTemplateCmd) { + vnfTemplateManager.persistVnfTemplate(template.getId(), (RegisterVnfTemplateCmd) cmd); + } + return template; } - return template; - } } throw new CloudRuntimeException("Failed to create a Template"); } @@ -1994,102 +1994,102 @@ public VMTemplateVO createPrivateTemplateRecord(CreateTemplateCmd cmd, Account t try (CheckedReservation templateReservation = new CheckedReservation(templateOwner, ResourceType.template, null, null, 1L, reservationDao, _resourceLimitMgr); CheckedReservation secondaryStorageReservation = new CheckedReservation(templateOwner, ResourceType.secondary_storage, null, null, templateSize, reservationDao, _resourceLimitMgr)) { - if (!isAdmin || featured == null) { - featured = Boolean.FALSE; - } - Long guestOSId = cmd.getOsTypeId(); - GuestOSVO guestOS = _guestOSDao.findById(guestOSId); - if (guestOS == null) { - throw new InvalidParameterValueException("GuestOS with ID: " + guestOSId + " does not exist."); - } - - Long nextTemplateId = _tmpltDao.getNextInSequence(Long.class, "id"); - String description = cmd.getDisplayText(); - boolean isExtractable = false; - Long sourceTemplateId = null; - if (volume != null) { - VMTemplateVO template = ApiDBUtils.findTemplateById(volume.getTemplateId()); - isExtractable = template != null && template.isExtractable() && template.getTemplateType() != Storage.TemplateType.SYSTEM; - if (template != null) { - arch = template.getArch(); + if (!isAdmin || featured == null) { + featured = Boolean.FALSE; } - if (volume.getIsoId() != null && volume.getIsoId() != 0) { - sourceTemplateId = volume.getIsoId(); - } else if (volume.getTemplateId() != null) { - sourceTemplateId = volume.getTemplateId(); + Long guestOSId = cmd.getOsTypeId(); + GuestOSVO guestOS = _guestOSDao.findById(guestOSId); + if (guestOS == null) { + throw new InvalidParameterValueException("GuestOS with ID: " + guestOSId + " does not exist."); } - } - String templateTag = cmd.getTemplateTag(); - if (templateTag != null) { - if (logger.isDebugEnabled()) { - logger.debug("Adding Template tag: " + templateTag); + + Long nextTemplateId = _tmpltDao.getNextInSequence(Long.class, "id"); + String description = cmd.getDisplayText(); + boolean isExtractable = false; + Long sourceTemplateId = null; + if (volume != null) { + VMTemplateVO template = ApiDBUtils.findTemplateById(volume.getTemplateId()); + isExtractable = template != null && template.isExtractable() && template.getTemplateType() != Storage.TemplateType.SYSTEM; + if (template != null) { + arch = template.getArch(); + } + if (volume.getIsoId() != null && volume.getIsoId() != 0) { + sourceTemplateId = volume.getIsoId(); + } else if (volume.getTemplateId() != null) { + sourceTemplateId = volume.getTemplateId(); + } } - } - privateTemplate = new VMTemplateVO(nextTemplateId, name, ImageFormat.RAW, isPublic, featured, isExtractable, - TemplateType.USER, null, requiresHvmValue, bitsValue, templateOwner.getId(), null, description, - passwordEnabledValue, guestOS.getId(), true, hyperType, templateTag, cmd.getDetails(), sshKeyEnabledValue, isDynamicScalingEnabled, false, false, arch); + String templateTag = cmd.getTemplateTag(); + if (templateTag != null) { + if (logger.isDebugEnabled()) { + logger.debug("Adding Template tag: " + templateTag); + } + } + privateTemplate = new VMTemplateVO(nextTemplateId, name, ImageFormat.RAW, isPublic, featured, isExtractable, + TemplateType.USER, null, requiresHvmValue, bitsValue, templateOwner.getId(), null, description, + passwordEnabledValue, guestOS.getId(), true, hyperType, templateTag, cmd.getDetails(), sshKeyEnabledValue, isDynamicScalingEnabled, false, false, arch); - if (sourceTemplateId != null) { - if (logger.isDebugEnabled()) { - logger.debug("This Template is getting created from other Template, setting source Template ID to: " + sourceTemplateId); + if (sourceTemplateId != null) { + if (logger.isDebugEnabled()) { + logger.debug("This Template is getting created from other Template, setting source Template ID to: " + sourceTemplateId); + } } - } - // for region wide storage, set cross zones flag - List stores = _imgStoreDao.findRegionImageStores(); - if (!CollectionUtils.isEmpty(stores)) { - privateTemplate.setCrossZones(true); - } + // for region wide storage, set cross zones flag + List stores = _imgStoreDao.findRegionImageStores(); + if (!CollectionUtils.isEmpty(stores)) { + privateTemplate.setCrossZones(true); + } - privateTemplate.setSourceTemplateId(sourceTemplateId); + privateTemplate.setSourceTemplateId(sourceTemplateId); - VMTemplateVO template = _tmpltDao.persist(privateTemplate); - // Increment the number of templates - if (template != null) { - Map details = new HashMap(); + VMTemplateVO template = _tmpltDao.persist(privateTemplate); + // Increment the number of templates + if (template != null) { + Map details = new HashMap(); - if (sourceTemplateId != null) { - VMTemplateVO sourceTemplate = _tmpltDao.findById(sourceTemplateId); - if (sourceTemplate != null && sourceTemplate.getDetails() != null) { - details.putAll(sourceTemplate.getDetails()); + if (sourceTemplateId != null) { + VMTemplateVO sourceTemplate = _tmpltDao.findById(sourceTemplateId); + if (sourceTemplate != null && sourceTemplate.getDetails() != null) { + details.putAll(sourceTemplate.getDetails()); + } } - } - if (volume != null) { - Long vmId = volume.getInstanceId(); - if (vmId != null) { - UserVmVO userVm = _userVmDao.findById(vmId); - if (userVm != null) { - _userVmDao.loadDetails(userVm); - Map vmDetails = userVm.getDetails(); - vmDetails = vmDetails.entrySet() - .stream() - .filter(map -> map.getValue() != null) - .collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue())); - details.putAll(vmDetails); + if (volume != null) { + Long vmId = volume.getInstanceId(); + if (vmId != null) { + UserVmVO userVm = _userVmDao.findById(vmId); + if (userVm != null) { + _userVmDao.loadDetails(userVm); + Map vmDetails = userVm.getDetails(); + vmDetails = vmDetails.entrySet() + .stream() + .filter(map -> map.getValue() != null) + .collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue())); + details.putAll(vmDetails); + } } } - } - if (cmd.getDetails() != null) { - details.remove(VmDetailConstants.ENCRYPTED_PASSWORD); // new password will be generated during vm deployment from password enabled template - details.putAll(cmd.getDetails()); - } - if (!details.isEmpty()) { - privateTemplate.setDetails(details); - _tmpltDao.saveDetails(privateTemplate); - } + if (cmd.getDetails() != null) { + details.remove(VmDetailConstants.ENCRYPTED_PASSWORD); // new password will be generated during vm deployment from password enabled template + details.putAll(cmd.getDetails()); + } + if (!details.isEmpty()) { + privateTemplate.setDetails(details); + _tmpltDao.saveDetails(privateTemplate); + } - _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.template); - _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.secondary_storage, templateSize); - } + _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.template); + _resourceLimitMgr.incrementResourceCount(templateOwner.getId(), ResourceType.secondary_storage, templateSize); + } - if (template != null) { - CallContext.current().putContextParameter(VirtualMachineTemplate.class, template.getUuid()); - return template; - } else { - throw new CloudRuntimeException("Failed to create a Template"); - } + if (template != null) { + CallContext.current().putContextParameter(VirtualMachineTemplate.class, template.getUuid()); + return template; + } else { + throw new CloudRuntimeException("Failed to create a Template"); + } } } diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 8b77cb506a8e..c68a86180376 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -1356,30 +1356,30 @@ private UserVm upgradeStoppedVirtualMachine(Long vmId, Long svcOffId, Map reservations = new ArrayList<>(); try { - if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { - _resourceLimitMgr.checkVmResourceLimitsForServiceOfferingChange(owner, vmInstance.isDisplay(), (long) currentCpu, (long) newCpu, - (long) currentMemory, (long) newMemory, currentServiceOffering, newServiceOffering, template, reservations); - } + if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { + _resourceLimitMgr.checkVmResourceLimitsForServiceOfferingChange(owner, vmInstance.isDisplay(), (long) currentCpu, (long) newCpu, + (long) currentMemory, (long) newMemory, currentServiceOffering, newServiceOffering, template, reservations); + } - // Check that the specified service offering ID is valid - _itMgr.checkIfCanUpgrade(vmInstance, newServiceOffering); + // Check that the specified service offering ID is valid + _itMgr.checkIfCanUpgrade(vmInstance, newServiceOffering); - // Check if the new service offering can be applied to vm instance - _accountMgr.checkAccess(owner, newServiceOffering, _dcDao.findById(vmInstance.getDataCenterId())); + // Check if the new service offering can be applied to vm instance + _accountMgr.checkAccess(owner, newServiceOffering, _dcDao.findById(vmInstance.getDataCenterId())); - // resize and migrate the root volume if required - DiskOfferingVO newDiskOffering = _diskOfferingDao.findById(newServiceOffering.getDiskOfferingId()); - changeDiskOfferingForRootVolume(vmId, newDiskOffering, customParameters, vmInstance.getDataCenterId()); + // resize and migrate the root volume if required + DiskOfferingVO newDiskOffering = _diskOfferingDao.findById(newServiceOffering.getDiskOfferingId()); + changeDiskOfferingForRootVolume(vmId, newDiskOffering, customParameters, vmInstance.getDataCenterId()); - _itMgr.upgradeVmDb(vmId, newServiceOffering, currentServiceOffering); + _itMgr.upgradeVmDb(vmId, newServiceOffering, currentServiceOffering); - // Increment or decrement CPU and Memory count accordingly. - if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { - _resourceLimitMgr.updateVmResourceCountForServiceOfferingChange(owner.getAccountId(), vmInstance.isDisplay(), (long) currentCpu, (long) newCpu, - (long) currentMemory, (long) newMemory, currentServiceOffering, newServiceOffering, template); - } + // Increment or decrement CPU and Memory count accordingly. + if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { + _resourceLimitMgr.updateVmResourceCountForServiceOfferingChange(owner.getAccountId(), vmInstance.isDisplay(), (long) currentCpu, (long) newCpu, + (long) currentMemory, (long) newMemory, currentServiceOffering, newServiceOffering, template); + } - return _vmDao.findById(vmInstance.getId()); + return _vmDao.findById(vmInstance.getId()); } finally { ReservationHelper.closeAll(reservations); @@ -2338,34 +2338,34 @@ public UserVm recoverVirtualMachine(RecoverVMCmd cmd) throws ResourceAllocationE List reservations = new ArrayList<>(); try { - // First check that the maximum number of UserVMs, CPU and Memory limit for the given - // accountId will not be exceeded - if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { - resourceLimitService.checkVmResourceLimit(account, vm.isDisplayVm(), serviceOffering, template, reservations); - } + // First check that the maximum number of UserVMs, CPU and Memory limit for the given + // accountId will not be exceeded + if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { + resourceLimitService.checkVmResourceLimit(account, vm.isDisplayVm(), serviceOffering, template, reservations); + } - _haMgr.cancelDestroy(vm, vm.getHostId()); + _haMgr.cancelDestroy(vm, vm.getHostId()); - try { - if (!_itMgr.stateTransitTo(vm, VirtualMachine.Event.RecoveryRequested, null)) { - logger.debug("Unable to recover the vm {} because it is not in the correct state. current state: {}", vm, vm.getState()); + try { + if (!_itMgr.stateTransitTo(vm, VirtualMachine.Event.RecoveryRequested, null)) { + logger.debug("Unable to recover the vm {} because it is not in the correct state. current state: {}", vm, vm.getState()); + throw new InvalidParameterValueException(String.format("Unable to recover the vm %s because it is not in the correct state. current state: %s", vm, vm.getState())); + } + } catch (NoTransitionException e) { throw new InvalidParameterValueException(String.format("Unable to recover the vm %s because it is not in the correct state. current state: %s", vm, vm.getState())); } - } catch (NoTransitionException e) { - throw new InvalidParameterValueException(String.format("Unable to recover the vm %s because it is not in the correct state. current state: %s", vm, vm.getState())); - } - // Recover the VM's disks - List volumes = _volsDao.findByInstance(vmId); - for (VolumeVO volume : volumes) { - if (volume.getVolumeType().equals(Volume.Type.ROOT)) { - recoverRootVolume(volume, vmId); - break; + // Recover the VM's disks + List volumes = _volsDao.findByInstance(vmId); + for (VolumeVO volume : volumes) { + if (volume.getVolumeType().equals(Volume.Type.ROOT)) { + recoverRootVolume(volume, vmId); + break; + } } - } - //Update Resource Count for the given account - resourceCountIncrement(account.getId(), vm.isDisplayVm(), serviceOffering, template); + //Update Resource Count for the given account + resourceCountIncrement(account.getId(), vm.isDisplayVm(), serviceOffering, template); } finally { ReservationHelper.closeAll(reservations); diff --git a/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java index 47aa57f728cd..0fc1953dfefc 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java @@ -211,20 +211,20 @@ public VolumeResponse importVolume(ImportVolumeCmd cmd) { List reservations = new ArrayList<>(); try { - // 6. check resource limitation - checkResourceLimitForImportVolume(owner, volume, diskOffering, reservations); + // 6. check resource limitation + checkResourceLimitForImportVolume(owner, volume, diskOffering, reservations); - // 7. create records - String volumeName = StringUtils.isNotBlank(cmd.getName()) ? cmd.getName().trim() : volumePath; - VolumeVO volumeVO = importVolumeInternal(volume, diskOffering, owner, pool, volumeName); + // 7. create records + String volumeName = StringUtils.isNotBlank(cmd.getName()) ? cmd.getName().trim() : volumePath; + VolumeVO volumeVO = importVolumeInternal(volume, diskOffering, owner, pool, volumeName); - // 8. Update resource count - updateResourceLimitForVolumeImport(volumeVO); + // 8. Update resource count + updateResourceLimitForVolumeImport(volumeVO); - // 9. Publish event - publicUsageEventForVolumeImportAndUnmanage(volumeVO, true); + // 9. Publish event + publicUsageEventForVolumeImportAndUnmanage(volumeVO, true); - return responseGenerator.createVolumeResponse(ResponseObject.ResponseView.Full, volumeVO); + return responseGenerator.createVolumeResponse(ResponseObject.ResponseView.Full, volumeVO); } finally { ReservationHelper.closeAll(reservations); diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index a32459ed059f..1b588b042105 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -2466,91 +2466,91 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag List reservations = new ArrayList<>(); try { - checkVolumeResourceLimitsForExternalKvmVmImport(owner, rootDisk, dataDisks, diskOffering, dataDiskOfferingMap, reservations); + checkVolumeResourceLimitsForExternalKvmVmImport(owner, rootDisk, dataDisks, diskOffering, dataDiskOfferingMap, reservations); - // Check NICs and supplied networks - Map nicIpAddressMap = getNicIpAddresses(unmanagedInstance.getNics(), callerNicIpAddressMap); - Map allNicNetworkMap = getUnmanagedNicNetworkMap(unmanagedInstance.getName(), unmanagedInstance.getNics(), nicNetworkMap, nicIpAddressMap, zone, hostName, owner, Hypervisor.HypervisorType.KVM); - if (!CollectionUtils.isEmpty(unmanagedInstance.getNics())) { - allDetails.put(VmDetailConstants.NIC_ADAPTER, unmanagedInstance.getNics().get(0).getAdapterType()); - } - VirtualMachine.PowerState powerState = VirtualMachine.PowerState.PowerOff; - - try { - userVm = userVmManager.importVM(zone, null, template, null, displayName, owner, - null, caller, true, null, owner.getAccountId(), userId, - serviceOffering, null, hostName, - Hypervisor.HypervisorType.KVM, allDetails, powerState, null); - } catch (InsufficientCapacityException ice) { - logger.error(String.format("Failed to import vm name: %s", instanceName), ice); - throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, ice.getMessage()); - } - if (userVm == null) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); - } - String rootVolumeName = String.format("ROOT-%s", userVm.getId()); - DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null, false); - - DiskProfile[] dataDiskProfiles = new DiskProfile[dataDisks.size()]; - int diskSeq = 0; - for (UnmanagedInstanceTO.Disk disk : dataDisks) { - DiskOffering offering = diskOfferingDao.findById(dataDiskOfferingMap.get(disk.getDiskId())); - DiskProfile dataDiskProfile = volumeManager.allocateRawVolume(Volume.Type.DATADISK, String.format("DATA-%d-%s", userVm.getId(), disk.getDiskId()), offering, null, null, null, userVm, template, owner, null, false); - dataDiskProfiles[diskSeq++] = dataDiskProfile; - } - - final VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm, template, serviceOffering, owner, null); - ServiceOfferingVO dummyOffering = serviceOfferingDao.findById(userVm.getId(), serviceOffering.getId()); - profile.setServiceOffering(dummyOffering); - DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList(); - final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), null, null, null, null, null); - DeployDestination dest = null; - try { - dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null); - } catch (Exception e) { - logger.warn("Import failed for Vm: {} while finding deployment destination", userVm, e); - cleanupFailedImportVM(userVm); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName())); - } - if(dest == null) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s. Suitable deployment destination not found", userVm.getInstanceName())); - } - - List> diskProfileStoragePoolList = new ArrayList<>(); - try { - diskProfileStoragePoolList.add(importExternalDisk(rootDisk, userVm, dest, diskOffering, Volume.Type.ROOT, - template, null, remoteUrl, username, password, tmpPath, diskProfile)); + // Check NICs and supplied networks + Map nicIpAddressMap = getNicIpAddresses(unmanagedInstance.getNics(), callerNicIpAddressMap); + Map allNicNetworkMap = getUnmanagedNicNetworkMap(unmanagedInstance.getName(), unmanagedInstance.getNics(), nicNetworkMap, nicIpAddressMap, zone, hostName, owner, Hypervisor.HypervisorType.KVM); + if (!CollectionUtils.isEmpty(unmanagedInstance.getNics())) { + allDetails.put(VmDetailConstants.NIC_ADAPTER, unmanagedInstance.getNics().get(0).getAdapterType()); + } + VirtualMachine.PowerState powerState = VirtualMachine.PowerState.PowerOff; - long deviceId = 1L; - diskSeq = 0; + try { + userVm = userVmManager.importVM(zone, null, template, null, displayName, owner, + null, caller, true, null, owner.getAccountId(), userId, + serviceOffering, null, hostName, + Hypervisor.HypervisorType.KVM, allDetails, powerState, null); + } catch (InsufficientCapacityException ice) { + logger.error(String.format("Failed to import vm name: %s", instanceName), ice); + throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, ice.getMessage()); + } + if (userVm == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); + } + String rootVolumeName = String.format("ROOT-%s", userVm.getId()); + DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null, false); + + DiskProfile[] dataDiskProfiles = new DiskProfile[dataDisks.size()]; + int diskSeq = 0; for (UnmanagedInstanceTO.Disk disk : dataDisks) { - DiskProfile dataDiskProfile = dataDiskProfiles[diskSeq++]; DiskOffering offering = diskOfferingDao.findById(dataDiskOfferingMap.get(disk.getDiskId())); + DiskProfile dataDiskProfile = volumeManager.allocateRawVolume(Volume.Type.DATADISK, String.format("DATA-%d-%s", userVm.getId(), disk.getDiskId()), offering, null, null, null, userVm, template, owner, null, false); + dataDiskProfiles[diskSeq++] = dataDiskProfile; + } - diskProfileStoragePoolList.add(importExternalDisk(disk, userVm, dest, offering, Volume.Type.DATADISK, - template, deviceId, remoteUrl, username, password, tmpPath, dataDiskProfile)); - deviceId++; + final VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm, template, serviceOffering, owner, null); + ServiceOfferingVO dummyOffering = serviceOfferingDao.findById(userVm.getId(), serviceOffering.getId()); + profile.setServiceOffering(dummyOffering); + DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList(); + final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), null, null, null, null, null); + DeployDestination dest = null; + try { + dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null); + } catch (Exception e) { + logger.warn("Import failed for Vm: {} while finding deployment destination", userVm, e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName())); } - } catch (Exception e) { - logger.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e); - cleanupFailedImportVM(userVm); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import volumes while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); - } - try { - int nicIndex = 0; - for (UnmanagedInstanceTO.Nic nic : unmanagedInstance.getNics()) { - Network network = networkDao.findById(allNicNetworkMap.get(nic.getNicId())); - Network.IpAddresses ipAddresses = nicIpAddressMap.get(nic.getNicId()); - importNic(nic, userVm, network, ipAddresses, nicIndex, nicIndex==0, true); - nicIndex++; + if(dest == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s. Suitable deployment destination not found", userVm.getInstanceName())); } - } catch (Exception e) { - logger.error(String.format("Failed to import NICs while importing vm: %s", instanceName), e); - cleanupFailedImportVM(userVm); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import NICs while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); - } - publishVMUsageUpdateResourceCount(userVm, dummyOffering, template); - return userVm; + + List> diskProfileStoragePoolList = new ArrayList<>(); + try { + diskProfileStoragePoolList.add(importExternalDisk(rootDisk, userVm, dest, diskOffering, Volume.Type.ROOT, + template, null, remoteUrl, username, password, tmpPath, diskProfile)); + + long deviceId = 1L; + diskSeq = 0; + for (UnmanagedInstanceTO.Disk disk : dataDisks) { + DiskProfile dataDiskProfile = dataDiskProfiles[diskSeq++]; + DiskOffering offering = diskOfferingDao.findById(dataDiskOfferingMap.get(disk.getDiskId())); + + diskProfileStoragePoolList.add(importExternalDisk(disk, userVm, dest, offering, Volume.Type.DATADISK, + template, deviceId, remoteUrl, username, password, tmpPath, dataDiskProfile)); + deviceId++; + } + } catch (Exception e) { + logger.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import volumes while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); + } + try { + int nicIndex = 0; + for (UnmanagedInstanceTO.Nic nic : unmanagedInstance.getNics()) { + Network network = networkDao.findById(allNicNetworkMap.get(nic.getNicId())); + Network.IpAddresses ipAddresses = nicIpAddressMap.get(nic.getNicId()); + importNic(nic, userVm, network, ipAddresses, nicIndex, nicIndex==0, true); + nicIndex++; + } + } catch (Exception e) { + logger.error(String.format("Failed to import NICs while importing vm: %s", instanceName), e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import NICs while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); + } + publishVMUsageUpdateResourceCount(userVm, dummyOffering, template); + return userVm; } finally { ReservationHelper.closeAll(reservations); @@ -2648,77 +2648,77 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, List reservations = new ArrayList<>(); List resourceLimitStorageTags = resourceLimitService.getResourceLimitStorageTagsForResourceCountOperation(true, diskOffering); try { - CheckedReservation volumeReservation = new CheckedReservation(owner, Resource.ResourceType.volume, resourceLimitStorageTags, + CheckedReservation volumeReservation = new CheckedReservation(owner, Resource.ResourceType.volume, resourceLimitStorageTags, CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? 1L : 0L, reservationDao, resourceLimitService); - reservations.add(volumeReservation); + reservations.add(volumeReservation); - String rootVolumeName = String.format("ROOT-%s", userVm.getId()); - DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null, false); + String rootVolumeName = String.format("ROOT-%s", userVm.getId()); + DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null, false); - final VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm, template, serviceOffering, owner, null); - ServiceOfferingVO dummyOffering = serviceOfferingDao.findById(userVm.getId(), serviceOffering.getId()); - profile.setServiceOffering(dummyOffering); - DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList(); - final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), null, null, hostId, poolId, null); - DeployDestination dest = null; - try { - dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null); - } catch (Exception e) { - logger.warn("Import failed for Vm: {} while finding deployment destination", userVm, e); - cleanupFailedImportVM(userVm); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName())); - } - if(dest == null) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s. Suitable deployment destination not found", userVm.getInstanceName())); - } - - Map storage = dest.getStorageForDisks(); - Volume volume = volumeDao.findById(diskProfile.getVolumeId()); - StoragePool storagePool = storage.get(volume); - CheckVolumeCommand checkVolumeCommand = new CheckVolumeCommand(); - checkVolumeCommand.setSrcFile(diskPath); - StorageFilerTO storageTO = new StorageFilerTO(storagePool); - checkVolumeCommand.setStorageFilerTO(storageTO); - Answer answer = agentManager.easySend(dest.getHost().getId(), checkVolumeCommand); - if (!(answer instanceof CheckVolumeAnswer)) { - cleanupFailedImportVM(userVm); - throw new CloudRuntimeException("Disk not found or is invalid"); - } - CheckVolumeAnswer checkVolumeAnswer = (CheckVolumeAnswer) answer; - try { - checkVolume(checkVolumeAnswer.getVolumeDetails()); - } catch (CloudRuntimeException e) { - cleanupFailedImportVM(userVm); - throw e; - } - if (!checkVolumeAnswer.getResult()) { - cleanupFailedImportVM(userVm); - throw new CloudRuntimeException("Disk not found or is invalid"); - } - diskProfile.setSize(checkVolumeAnswer.getSize()); + final VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm, template, serviceOffering, owner, null); + ServiceOfferingVO dummyOffering = serviceOfferingDao.findById(userVm.getId(), serviceOffering.getId()); + profile.setServiceOffering(dummyOffering); + DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList(); + final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), null, null, hostId, poolId, null); + DeployDestination dest = null; + try { + dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null); + } catch (Exception e) { + logger.warn("Import failed for Vm: {} while finding deployment destination", userVm, e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName())); + } + if(dest == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s. Suitable deployment destination not found", userVm.getInstanceName())); + } + + Map storage = dest.getStorageForDisks(); + Volume volume = volumeDao.findById(diskProfile.getVolumeId()); + StoragePool storagePool = storage.get(volume); + CheckVolumeCommand checkVolumeCommand = new CheckVolumeCommand(); + checkVolumeCommand.setSrcFile(diskPath); + StorageFilerTO storageTO = new StorageFilerTO(storagePool); + checkVolumeCommand.setStorageFilerTO(storageTO); + Answer answer = agentManager.easySend(dest.getHost().getId(), checkVolumeCommand); + if (!(answer instanceof CheckVolumeAnswer)) { + cleanupFailedImportVM(userVm); + throw new CloudRuntimeException("Disk not found or is invalid"); + } + CheckVolumeAnswer checkVolumeAnswer = (CheckVolumeAnswer) answer; + try { + checkVolume(checkVolumeAnswer.getVolumeDetails()); + } catch (CloudRuntimeException e) { + cleanupFailedImportVM(userVm); + throw e; + } + if (!checkVolumeAnswer.getResult()) { + cleanupFailedImportVM(userVm); + throw new CloudRuntimeException("Disk not found or is invalid"); + } + diskProfile.setSize(checkVolumeAnswer.getSize()); - CheckedReservation primaryStorageReservation = new CheckedReservation(owner, Resource.ResourceType.primary_storage, resourceLimitStorageTags, - CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? diskProfile.getSize() : 0L, reservationDao, resourceLimitService); - reservations.add(primaryStorageReservation); + CheckedReservation primaryStorageReservation = new CheckedReservation(owner, Resource.ResourceType.primary_storage, resourceLimitStorageTags, + CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? diskProfile.getSize() : 0L, reservationDao, resourceLimitService); + reservations.add(primaryStorageReservation); - List> diskProfileStoragePoolList = new ArrayList<>(); - try { - long deviceId = 1L; - if(ImportSource.SHARED == importSource) { - diskProfileStoragePoolList.add(importKVMSharedDisk(userVm, diskOffering, Volume.Type.ROOT, - template, deviceId, poolId, diskPath, diskProfile)); - } else if(ImportSource.LOCAL == importSource) { - diskProfileStoragePoolList.add(importKVMLocalDisk(userVm, diskOffering, Volume.Type.ROOT, - template, deviceId, hostId, diskPath, diskProfile)); + List> diskProfileStoragePoolList = new ArrayList<>(); + try { + long deviceId = 1L; + if(ImportSource.SHARED == importSource) { + diskProfileStoragePoolList.add(importKVMSharedDisk(userVm, diskOffering, Volume.Type.ROOT, + template, deviceId, poolId, diskPath, diskProfile)); + } else if(ImportSource.LOCAL == importSource) { + diskProfileStoragePoolList.add(importKVMLocalDisk(userVm, diskOffering, Volume.Type.ROOT, + template, deviceId, hostId, diskPath, diskProfile)); + } + } catch (Exception e) { + logger.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import volumes while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); } - } catch (Exception e) { - logger.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e); - cleanupFailedImportVM(userVm); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import volumes while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); - } - networkOrchestrationService.importNic(macAddress, 0, network, true, userVm, requestedIpPair, zone, true); - publishVMUsageUpdateResourceCount(userVm, dummyOffering, template); - return userVm; + networkOrchestrationService.importNic(macAddress, 0, network, true, userVm, requestedIpPair, zone, true); + publishVMUsageUpdateResourceCount(userVm, dummyOffering, template); + return userVm; } catch (ResourceAllocationException e) { cleanupFailedImportVM(userVm); From f6cad87586f6079f14984873ba8839d054254e46 Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Tue, 17 Mar 2026 06:56:09 +0530 Subject: [PATCH 021/205] Fix copy snapshot resource limit --- .../storage/snapshot/SnapshotManagerImpl.java | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index 4a4a7544ce74..f9057a3434f0 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -1748,17 +1748,17 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, try (CheckedReservation volumeSnapshotReservation = new CheckedReservation(owner, ResourceType.snapshot, null, null, 1L, reservationDao, _resourceLimitMgr); CheckedReservation storageReservation = new CheckedReservation(owner, storeResourceType, null, null, volume.getSize(), reservationDao, _resourceLimitMgr)) { - SnapshotVO snapshotVO = new SnapshotVO(volume.getDataCenterId(), volume.getAccountId(), volume.getDomainId(), volume.getId(), volume.getDiskOfferingId(), snapshotName, - (short)snapshotType.ordinal(), snapshotType.name(), volume.getSize(), volume.getMinIops(), volume.getMaxIops(), hypervisorType, locationType); + SnapshotVO snapshotVO = new SnapshotVO(volume.getDataCenterId(), volume.getAccountId(), volume.getDomainId(), volume.getId(), volume.getDiskOfferingId(), snapshotName, + (short)snapshotType.ordinal(), snapshotType.name(), volume.getSize(), volume.getMinIops(), volume.getMaxIops(), hypervisorType, locationType); - SnapshotVO snapshot = _snapshotDao.persist(snapshotVO); - if (snapshot == null) { - throw new CloudRuntimeException(String.format("Failed to create snapshot for volume: %s", volume)); - } - CallContext.current().putContextParameter(Snapshot.class, snapshot.getUuid()); - _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), ResourceType.snapshot); - _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), storeResourceType, volume.getSize()); - return snapshot; + SnapshotVO snapshot = _snapshotDao.persist(snapshotVO); + if (snapshot == null) { + throw new CloudRuntimeException(String.format("Failed to create snapshot for volume: %s", volume)); + } + CallContext.current().putContextParameter(Snapshot.class, snapshot.getUuid()); + _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), ResourceType.snapshot); + _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), storeResourceType, volume.getSize()); + return snapshot; } catch (ResourceAllocationException e) { if (snapshotType != Type.MANUAL) { String msg = String.format("Snapshot resource limit exceeded for account id : %s. Failed to create recurring snapshots", owner.getId()); @@ -1814,43 +1814,44 @@ private boolean copySnapshotToZone(SnapshotDataStoreVO snapshotDataStoreVO, Data if (checkAndProcessSnapshotAlreadyExistInStore(snapshotId, dstSecStore)) { return true; } - _resourceLimitMgr.checkResourceLimit(account, ResourceType.secondary_storage, snapshotDataStoreVO.getSize()); - // snapshotId may refer to ID of a removed parent snapshot - SnapshotInfo snapshotOnSecondary = snapshotFactory.getSnapshot(snapshotId, srcSecStore); - String copyUrl = null; - try { - AsyncCallFuture future = snapshotSrv.queryCopySnapshot(snapshotOnSecondary); - CreateCmdResult result = future.get(); - if (!result.isFailed()) { - copyUrl = result.getPath(); + try (CheckedReservation secStorageReservation = new CheckedReservation(account, ResourceType.secondary_storage, + snapshotDataStoreVO.getSize(), reservationDao, _resourceLimitMgr)) { + SnapshotInfo snapshotOnSecondary = snapshotFactory.getSnapshot(snapshotId, srcSecStore); + String copyUrl = null; + try { + AsyncCallFuture future = snapshotSrv.queryCopySnapshot(snapshotOnSecondary); + CreateCmdResult result = future.get(); + if (!result.isFailed()) { + copyUrl = result.getPath(); + } + } catch (InterruptedException | ExecutionException | ResourceUnavailableException ex) { + logger.error("Failed to prepare URL for copy for snapshot ID: {} on store: {}", snapshotId, srcSecStore, ex); } - } catch (InterruptedException | ExecutionException | ResourceUnavailableException ex) { - logger.error("Failed to prepare URL for copy for snapshot ID: {} on store: {}", snapshotId, srcSecStore, ex); - } - if (StringUtils.isEmpty(copyUrl)) { - logger.error("Unable to prepare URL for copy for snapshot ID: {} on store: {}", snapshotId, srcSecStore); - return false; - } - logger.debug(String.format("Copying snapshot ID: %d to destination zones using download URL: %s", snapshotId, copyUrl)); - try { - AsyncCallFuture future = snapshotSrv.copySnapshot(snapshotOnSecondary, copyUrl, dstSecStore); - SnapshotResult result = future.get(); - if (result.isFailed()) { - logger.debug("Copy snapshot ID: {} failed for image store {}: {}", snapshotId, dstSecStore, result.getResult()); + if (StringUtils.isEmpty(copyUrl)) { + logger.error("Unable to prepare URL for copy for snapshot ID: {} on store: {}", snapshotId, srcSecStore); return false; } - snapshotZoneDao.addSnapshotToZone(snapshotId, dstZoneId); - _resourceLimitMgr.incrementResourceCount(account.getId(), ResourceType.secondary_storage, snapshotDataStoreVO.getSize()); - if (account.getId() != Account.ACCOUNT_ID_SYSTEM) { - SnapshotVO snapshotVO = _snapshotDao.findByIdIncludingRemoved(snapshotId); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_SNAPSHOT_COPY, account.getId(), dstZoneId, snapshotId, null, null, null, snapshotVO.getSize(), - snapshotVO.getSize(), snapshotVO.getClass().getName(), snapshotVO.getUuid()); + logger.debug(String.format("Copying snapshot ID: %d to destination zones using download URL: %s", snapshotId, copyUrl)); + try { + AsyncCallFuture future = snapshotSrv.copySnapshot(snapshotOnSecondary, copyUrl, dstSecStore); + SnapshotResult result = future.get(); + if (result.isFailed()) { + logger.debug("Copy snapshot ID: {} failed for image store {}: {}", snapshotId, dstSecStore, result.getResult()); + return false; + } + snapshotZoneDao.addSnapshotToZone(snapshotId, dstZoneId); + _resourceLimitMgr.incrementResourceCount(account.getId(), ResourceType.secondary_storage, snapshotDataStoreVO.getSize()); + if (account.getId() != Account.ACCOUNT_ID_SYSTEM) { + SnapshotVO snapshotVO = _snapshotDao.findByIdIncludingRemoved(snapshotId); + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_SNAPSHOT_COPY, account.getId(), dstZoneId, snapshotId, null, null, null, snapshotVO.getSize(), + snapshotVO.getSize(), snapshotVO.getClass().getName(), snapshotVO.getUuid()); + } + return true; + } catch (InterruptedException | ExecutionException | ResourceUnavailableException ex) { + logger.debug("Failed to copy snapshot ID: {} to image store: {}", snapshotId, dstSecStore); } - return true; - } catch (InterruptedException | ExecutionException | ResourceUnavailableException ex) { - logger.debug("Failed to copy snapshot ID: {} to image store: {}", snapshotId, dstSecStore); + return false; } - return false; } @DB From 639ceeea619701831a89f0ab95b1c362549bf3bc Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Wed, 18 Mar 2026 06:57:02 +0530 Subject: [PATCH 022/205] Fix snapshot copy resource limit concurrency --- .../storage/snapshot/SnapshotManagerImpl.java | 36 ++++++++++--------- .../snapshot/SnapshotManagerImplTest.java | 4 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index f9057a3434f0..0d6e9de509fa 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -1807,15 +1807,17 @@ private boolean checkAndProcessSnapshotAlreadyExistInStore(long snapshotId, Data @DB private boolean copySnapshotToZone(SnapshotDataStoreVO snapshotDataStoreVO, DataStore srcSecStore, - DataCenterVO dstZone, DataStore dstSecStore, Account account) + DataCenterVO dstZone, DataStore dstSecStore, Account account, boolean shouldCheckResourceLimits) throws ResourceAllocationException { final long snapshotId = snapshotDataStoreVO.getSnapshotId(); final long dstZoneId = dstZone.getId(); if (checkAndProcessSnapshotAlreadyExistInStore(snapshotId, dstSecStore)) { return true; } - try (CheckedReservation secStorageReservation = new CheckedReservation(account, ResourceType.secondary_storage, - snapshotDataStoreVO.getSize(), reservationDao, _resourceLimitMgr)) { + // Resource limit checks are not performed here at the moment, but they were added in case this method is used + // in the future to copy a standalone snapshot + long requiredSecondaryStorageSpace = shouldCheckResourceLimits ? snapshotDataStoreVO.getSize() : 0L; + try (CheckedReservation secondaryStorageReservation = new CheckedReservation(account, ResourceType.secondary_storage, null, null, null, requiredSecondaryStorageSpace, null, reservationDao, _resourceLimitMgr)) { SnapshotInfo snapshotOnSecondary = snapshotFactory.getSnapshot(snapshotId, srcSecStore); String copyUrl = null; try { @@ -1846,6 +1848,7 @@ private boolean copySnapshotToZone(SnapshotDataStoreVO snapshotDataStoreVO, Data UsageEventUtils.publishUsageEvent(EventTypes.EVENT_SNAPSHOT_COPY, account.getId(), dstZoneId, snapshotId, null, null, null, snapshotVO.getSize(), snapshotVO.getSize(), snapshotVO.getClass().getName(), snapshotVO.getUuid()); } + return true; } catch (InterruptedException | ExecutionException | ResourceUnavailableException ex) { logger.debug("Failed to copy snapshot ID: {} to image store: {}", snapshotId, dstSecStore); @@ -1882,13 +1885,6 @@ private boolean copySnapshotChainToZone(SnapshotVO snapshotVO, DataStore srcSecS if (CollectionUtils.isEmpty(snapshotChain)) { return true; } - try { - _resourceLimitMgr.checkResourceLimit(account, ResourceType.secondary_storage, size); - } catch (ResourceAllocationException e) { - logger.error(String.format("Unable to allocate secondary storage resources for snapshot chain for %s with size: %d", snapshotVO, size), e); - return false; - } - Collections.reverse(snapshotChain); if (dstSecStore == null) { // find all eligible image stores for the destination zone List dstSecStores = dataStoreMgr.getImageStoresByScopeExcludingReadOnly(new ZoneScope(destZoneId)); @@ -1900,15 +1896,21 @@ private boolean copySnapshotChainToZone(SnapshotVO snapshotVO, DataStore srcSecS throw new StorageUnavailableException("Destination zone is not ready, no image store with free capacity", DataCenter.class, destZoneId); } } - logger.debug("Copying snapshot chain for snapshot ID: {} on secondary store: {} of zone ID: {}", snapshotVO, dstSecStore, destZone); - for (SnapshotDataStoreVO snapshotDataStoreVO : snapshotChain) { - if (!copySnapshotToZone(snapshotDataStoreVO, srcSecStore, destZone, dstSecStore, account)) { - logger.error("Failed to copy snapshot: {} to zone: {} due to failure to copy snapshot ID: {} from snapshot chain", - snapshotVO, destZone, snapshotDataStoreVO.getSnapshotId()); - return false; + try (CheckedReservation secondaryStorageReservation = new CheckedReservation(account, ResourceType.secondary_storage, null, null, null, size, null, reservationDao, _resourceLimitMgr)) { + logger.debug("Copying snapshot chain for snapshot ID: {} on secondary store: {} of zone ID: {}", snapshotVO, dstSecStore, destZone); + Collections.reverse(snapshotChain); + for (SnapshotDataStoreVO snapshotDataStoreVO : snapshotChain) { + if (!copySnapshotToZone(snapshotDataStoreVO, srcSecStore, destZone, dstSecStore, account, false)) { + logger.error("Failed to copy snapshot: {} to zone: {} due to failure to copy snapshot ID: {} from snapshot chain", + snapshotVO, destZone, snapshotDataStoreVO.getSnapshotId()); + return false; + } } + return true; + } catch (ResourceAllocationException e) { + logger.error(String.format("Unable to allocate secondary storage resources for snapshot chain for %s with size: %d", snapshotVO, size), e); + return false; } - return true; } @DB diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java index 86fdcfecc137..32b103f39d3f 100644 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java @@ -46,7 +46,6 @@ import com.cloud.event.ActionEventUtils; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; -import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.org.Grouping; import com.cloud.storage.DataStoreRole; @@ -284,12 +283,11 @@ public void testCopyNewSnapshotToZones() { Mockito.when(result1.isFailed()).thenReturn(false); AsyncCallFuture future1 = Mockito.mock(AsyncCallFuture.class); try { - Mockito.doNothing().when(resourceLimitService).checkResourceLimit(Mockito.any(), Mockito.any(), Mockito.anyLong()); Mockito.when(future.get()).thenReturn(result); Mockito.when(snapshotService.queryCopySnapshot(Mockito.any())).thenReturn(future); Mockito.when(future1.get()).thenReturn(result1); Mockito.when(snapshotService.copySnapshot(Mockito.any(SnapshotInfo.class), Mockito.anyString(), Mockito.any(DataStore.class))).thenReturn(future1); - } catch (ResourceAllocationException | ResourceUnavailableException | ExecutionException | InterruptedException e) { + } catch (ResourceUnavailableException | ExecutionException | InterruptedException e) { Assert.fail(e.getMessage()); } List addedZone = new ArrayList<>(); From 4df32ae79fb21e9f11df5c1235bc504a8e284ae7 Mon Sep 17 00:00:00 2001 From: Daniil Zhyliaiev Date: Mon, 6 Apr 2026 21:50:17 +0300 Subject: [PATCH 023/205] fix: NsxResource.executeRequest DeleteNsxNatRuleCommand comparison bug (#12833) Fixes an issue in NsxResource.executeRequest where Network.Service comparison failed when DeleteNsxNatRuleCommand was executed in a different process. Due to serialization/deserialization, the deserialized Network.Service instance was not equal to the static instances Network.Service.StaticNat and Network.Service.PortForwarding, causing the comparison to always return false. Co-authored-by: Andrey Volchkov (cherry picked from commit 30dd234b000845706c7f79f44ed498fd4eab01f2) --- .../cloudstack/agent/api/DeleteNsxNatRuleCommand.java | 7 +++++++ .../java/org/apache/cloudstack/resource/NsxResource.java | 6 +++--- .../org/apache/cloudstack/resource/NsxResourceTest.java | 6 ++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/agent/api/DeleteNsxNatRuleCommand.java b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/agent/api/DeleteNsxNatRuleCommand.java index c5231b19ac40..b642df856185 100644 --- a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/agent/api/DeleteNsxNatRuleCommand.java +++ b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/agent/api/DeleteNsxNatRuleCommand.java @@ -54,6 +54,13 @@ public String getProtocol() { return protocol; } + public String getNetworkServiceName() { + if (service != null) { + return service.getName(); + } + return null; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/resource/NsxResource.java b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/resource/NsxResource.java index 76815b0deebe..78a9363a5e49 100644 --- a/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/resource/NsxResource.java +++ b/plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/resource/NsxResource.java @@ -415,10 +415,10 @@ private NsxAnswer executeRequest(CreateNsxPortForwardRuleCommand cmd) { private NsxAnswer executeRequest(DeleteNsxNatRuleCommand cmd) { String ruleName = null; - if (cmd.getService() == Network.Service.StaticNat) { + if (Network.Service.StaticNat.getName().equals(cmd.getNetworkServiceName())) { ruleName = NsxControllerUtils.getStaticNatRuleName(cmd.getDomainId(), cmd.getAccountId(), cmd.getZoneId(), cmd.getNetworkResourceId(), cmd.isResourceVpc()); - } else if (cmd.getService() == Network.Service.PortForwarding) { + } else if (Network.Service.PortForwarding.getName().equals(cmd.getNetworkServiceName())) { ruleName = NsxControllerUtils.getPortForwardRuleName(cmd.getDomainId(), cmd.getAccountId(), cmd.getZoneId(), cmd.getNetworkResourceId(), cmd.getRuleId(), cmd.isResourceVpc()); } @@ -456,7 +456,7 @@ private NsxAnswer executeRequest(DeleteNsxLoadBalancerRuleCommand cmd) { try { nsxApiClient.deleteNsxLbResources(tier1GatewayName, cmd.getLbId()); } catch (Exception e) { - logger.error(String.format("Failed to add NSX load balancer rule %s for network: %s", ruleName, cmd.getNetworkResourceName())); + logger.error(String.format("Failed to delete NSX load balancer rule %s for network: %s", ruleName, cmd.getNetworkResourceName())); return new NsxAnswer(cmd, new CloudRuntimeException(e.getMessage())); } return new NsxAnswer(cmd, true, null); diff --git a/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/resource/NsxResourceTest.java b/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/resource/NsxResourceTest.java index ee4f4fb64c20..0d74bb8a3b3d 100644 --- a/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/resource/NsxResourceTest.java +++ b/plugins/network-elements/nsx/src/test/java/org/apache/cloudstack/resource/NsxResourceTest.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.resource; +import com.cloud.network.Network; import com.cloud.network.dao.NetworkVO; import com.cloud.utils.exception.CloudRuntimeException; import com.vmware.nsx.model.TransportZone; @@ -61,6 +62,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -247,8 +249,12 @@ public void testCreatePortForwardRule() { @Test public void testDeleteNsxNatRule() { DeleteNsxNatRuleCommand cmd = new DeleteNsxNatRuleCommand(domainId, accountId, zoneId, 3L, "VPC01", true, 2L, 5L, "22", "tcp"); + Network.Service service = mock(Network.Service.class); + when(service.getName()).thenReturn("PortForwarding"); + cmd.setService(service); NsxAnswer answer = (NsxAnswer) nsxResource.executeRequest(cmd); assertTrue(answer.getResult()); + verify(nsxApi).deleteNatRule(service, "22", "tcp", "VPC01", "D1-A2-Z1-V3", "D1-A2-Z1-V3-PF5"); } @Test From 6c1437b7ddd8fc590c96d1aac6f8df7709ac4625 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 17 Apr 2026 13:56:17 +0200 Subject: [PATCH 024/205] fix end of file schema-42200to42210.sql --- .../src/main/resources/META-INF/db/schema-42200to42210.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql index 0300e5554636..baa20e9f0ad5 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql @@ -59,4 +59,3 @@ UPDATE `cloud`.`configuration` SET value = CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', '{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team') WHERE name = 'user.password.reset.mail.template' AND value IN (CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', 'http://{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team'), CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', '{{{domainUrl}}}{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team')); - From be89e6f7c3784672cd0a9f8d0a0101c670162dd2 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Fri, 17 Apr 2026 23:39:19 -0300 Subject: [PATCH 025/205] [KVM] Reorder migration logs to prevent populating agent logs on migrations (#12883) * Move logs for values of the migration settings out of the loop * Apply suggestions from code review Co-authored-by: Suresh Kumar Anaparti --------- Co-authored-by: Suresh Kumar Anaparti --- .../resource/wrapper/LibvirtMigrateCommandWrapper.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java index 43607edc53a5..f54918bbc228 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java @@ -259,6 +259,12 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. final int migrateDowntime = libvirtComputingResource.getMigrateDowntime(); boolean isMigrateDowntimeSet = false; + final int migrateWait = libvirtComputingResource.getMigrateWait(); + logger.info("vm.migrate.wait value set to: {} secs for VM: {}", migrateWait, vmName); + + final int migratePauseAfter = libvirtComputingResource.getMigratePauseAfter(); + logger.info("vm.migrate.pauseafter value set to: {} ms for VM: {}", migratePauseAfter, vmName); + while (!executor.isTerminated()) { Thread.sleep(100); sleeptime += 100; @@ -278,8 +284,6 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. } // abort the vm migration if the job is executed more than vm.migrate.wait - final int migrateWait = libvirtComputingResource.getMigrateWait(); - logger.info("vm.migrate.wait value set to: {}for VM: {}", migrateWait, vmName); if (migrateWait > 0 && sleeptime > migrateWait * 1000) { DomainState state = null; try { @@ -306,8 +310,6 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. } // pause vm if we meet the vm.migrate.pauseafter threshold and not already paused - final int migratePauseAfter = libvirtComputingResource.getMigratePauseAfter(); - logger.info("vm.migrate.pauseafter value set to: {} for VM: {}", migratePauseAfter, vmName); if (migratePauseAfter > 0 && sleeptime > migratePauseAfter) { DomainState state = null; try { From 89d915493fb83c5e0abe8db0fc1800bedc5be42b Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Fri, 27 Mar 2026 01:55:16 -0300 Subject: [PATCH 026/205] Fix NPE on external/unmanaged instance import using custom offerings (#12884) * Fix NPE on external/unmanaged instance import using custom offerings --- .../vm/UnmanagedVMsManagerImpl.java | 254 ++++++++++++------ .../vm/UnmanagedVMsManagerImplTest.java | 118 +++++++- 2 files changed, 280 insertions(+), 92 deletions(-) diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 75f879315917..18ba2d171798 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -1387,10 +1387,7 @@ private UserVmResponse baseImportInstance(ImportUnmanagedInstanceCmd cmd) { List managedVms = new ArrayList<>(additionalNameFilters); managedVms.addAll(getHostsManagedVms(hosts)); - List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(serviceOffering, template); - try (CheckedReservation vmReservation = new CheckedReservation(owner, Resource.ResourceType.user_vm, resourceLimitHostTags, 1L, reservationDao, resourceLimitService); - CheckedReservation cpuReservation = new CheckedReservation(owner, Resource.ResourceType.cpu, resourceLimitHostTags, Long.valueOf(serviceOffering.getCpu()), reservationDao, resourceLimitService); - CheckedReservation memReservation = new CheckedReservation(owner, Resource.ResourceType.memory, resourceLimitHostTags, Long.valueOf(serviceOffering.getRamSize()), reservationDao, resourceLimitService)) { + try { ActionEventUtils.onStartedActionEvent(userId, owner.getId(), EventTypes.EVENT_VM_IMPORT, cmd.getEventDescription(), null, null, true, 0); @@ -1560,7 +1557,7 @@ private UserVm importUnmanagedInstanceFromHypervisor(DataCenter zone, Cluster cl String hostName, Account caller, Account owner, long userId, ServiceOfferingVO serviceOffering, Map dataDiskOfferingMap, Map nicNetworkMap, Map nicIpAddressMap, - Map details, Boolean migrateAllowed, List managedVms, boolean forced) { + Map details, Boolean migrateAllowed, List managedVms, boolean forced) throws ResourceAllocationException { UserVm userVm = null; for (HostVO host : hosts) { HashMap unmanagedInstances = getUnmanagedInstancesForHost(host, instanceName, managedVms); @@ -1604,11 +1601,18 @@ private UserVm importUnmanagedInstanceFromHypervisor(DataCenter zone, Cluster cl template.setGuestOSId(guestOSHypervisor.getGuestOsId()); } - userVm = importVirtualMachineInternal(unmanagedInstance, instanceName, zone, cluster, host, - template, displayName, hostName, CallContext.current().getCallingAccount(), owner, userId, - serviceOffering, dataDiskOfferingMap, - nicNetworkMap, nicIpAddressMap, - details, migrateAllowed, forced, true); + + List reservations = new ArrayList<>(); + try { + checkVmResourceLimitsForUnmanagedInstanceImport(owner, unmanagedInstance, serviceOffering, template, reservations); + userVm = importVirtualMachineInternal(unmanagedInstance, instanceName, zone, cluster, host, + template, displayName, hostName, CallContext.current().getCallingAccount(), owner, userId, + serviceOffering, dataDiskOfferingMap, + nicNetworkMap, nicIpAddressMap, + details, migrateAllowed, forced, true); + } finally { + ReservationHelper.closeAll(reservations); + } break; } if (userVm != null) { @@ -1618,6 +1622,36 @@ private UserVm importUnmanagedInstanceFromHypervisor(DataCenter zone, Cluster cl return userVm; } + protected void checkVmResourceLimitsForUnmanagedInstanceImport(Account owner, UnmanagedInstanceTO unmanagedInstance, ServiceOfferingVO serviceOffering, VMTemplateVO template, List reservations) throws ResourceAllocationException { + // When importing an unmanaged instance, the amount of CPUs and memory is obtained from the hypervisor unless powered off + // and not using a dynamic offering, unlike the external VM import that always obtains it from the compute offering + Integer cpu = serviceOffering.getCpu(); + Integer memory = serviceOffering.getRamSize(); + + if (serviceOffering.isDynamic() || !UnmanagedInstanceTO.PowerState.PowerOff.equals(unmanagedInstance.getPowerState())) { + cpu = unmanagedInstance.getCpuCores(); + memory = unmanagedInstance.getMemory(); + } + + if (cpu == null || cpu == 0) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("CPU cores [%s] is not valid for importing VM [%s].", cpu, unmanagedInstance.getName())); + } + if (memory == null || memory == 0) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Memory [%s] is not valid for importing VM [%s].", memory, unmanagedInstance.getName())); + } + + List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(serviceOffering, template); + + CheckedReservation vmReservation = new CheckedReservation(owner, Resource.ResourceType.user_vm, resourceLimitHostTags, 1L, reservationDao, resourceLimitService); + reservations.add(vmReservation); + + CheckedReservation cpuReservation = new CheckedReservation(owner, Resource.ResourceType.cpu, resourceLimitHostTags, cpu.longValue(), reservationDao, resourceLimitService); + reservations.add(cpuReservation); + + CheckedReservation memReservation = new CheckedReservation(owner, Resource.ResourceType.memory, resourceLimitHostTags, memory.longValue(), reservationDao, resourceLimitService); + reservations.add(memReservation); + } + private Pair getSourceVmwareUnmanagedInstance(String vcenter, String datacenterName, String username, String password, String clusterName, String sourceHostName, String sourceVM, ServiceOfferingVO serviceOffering) { @@ -1674,7 +1708,7 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster Account caller, Account owner, long userId, ServiceOfferingVO serviceOffering, Map dataDiskOfferingMap, Map nicNetworkMap, Map nicIpAddressMap, - Map details, ImportVmCmd cmd, boolean forced) { + Map details, ImportVmCmd cmd, boolean forced) throws ResourceAllocationException { Long existingVcenterId = cmd.getExistingVcenterId(); String vcenter = cmd.getVcenter(); String datacenterName = cmd.getDatacenterName(); @@ -1719,6 +1753,8 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster DataStoreTO temporaryConvertLocation = null; String ovfTemplateOnConvertLocation = null; ImportVmTask importVMTask = null; + List reservations = new ArrayList<>(); + try { HostVO convertHost = selectKVMHostForConversionInCluster(destinationCluster, convertInstanceHostId); HostVO importHost = selectKVMHostForImportingInCluster(destinationCluster, importInstanceHostId); @@ -1741,6 +1777,10 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster Pair sourceInstanceDetails = getSourceVmwareUnmanagedInstance(vcenter, datacenterName, username, password, clusterName, sourceHostName, sourceVMName, serviceOffering); sourceVMwareInstance = sourceInstanceDetails.first(); isClonedInstance = sourceInstanceDetails.second(); + + // Ensure that the configured resource limits will not be exceeded before beginning the conversion process + checkVmResourceLimitsForUnmanagedInstanceImport(owner, sourceVMwareInstance, serviceOffering, template, reservations); + boolean isWindowsVm = sourceVMwareInstance.getOperatingSystem().toLowerCase().contains("windows"); if (isWindowsVm) { checkConversionSupportOnHost(convertHost, sourceVMName, true); @@ -1793,6 +1833,7 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster if (temporaryConvertLocation != null && StringUtils.isNotBlank(ovfTemplateOnConvertLocation)) { removeTemplate(temporaryConvertLocation, ovfTemplateOnConvertLocation); } + ReservationHelper.closeAll(reservations); } } @@ -2581,10 +2622,7 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) { UserVm userVm = null; - List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(serviceOffering, template); - try (CheckedReservation vmReservation = new CheckedReservation(owner, Resource.ResourceType.user_vm, resourceLimitHostTags, 1L, reservationDao, resourceLimitService); - CheckedReservation cpuReservation = new CheckedReservation(owner, Resource.ResourceType.cpu, resourceLimitHostTags, Long.valueOf(serviceOffering.getCpu()), reservationDao, resourceLimitService); - CheckedReservation memReservation = new CheckedReservation(owner, Resource.ResourceType.memory, resourceLimitHostTags, Long.valueOf(serviceOffering.getRamSize()), reservationDao, resourceLimitService)) { + try { if (ImportSource.EXTERNAL == importSource) { String username = cmd.getUsername(); @@ -2647,6 +2685,7 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag List reservations = new ArrayList<>(); try { + checkVmResourceLimitsForExternalKvmVmImport(owner, serviceOffering, (VMTemplateVO) template, details, reservations); checkVolumeResourceLimitsForExternalKvmVmImport(owner, rootDisk, dataDisks, diskOffering, dataDiskOfferingMap, reservations); // Check NICs and supplied networks @@ -2811,98 +2850,135 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, profiles.add(nicProfile); networkNicMap.put(network.getUuid(), profiles); + List reservations = new ArrayList<>(); try { + checkVmResourceLimitsForExternalKvmVmImport(owner, serviceOffering, (VMTemplateVO) template, details, reservations); userVm = userVmManager.importVM(zone, null, template, null, displayName, owner, null, caller, true, null, owner.getAccountId(), userId, serviceOffering, null, hostName, Hypervisor.HypervisorType.KVM, allDetails, powerState, networkNicMap); - } catch (InsufficientCapacityException ice) { - logger.error(String.format("Failed to import vm name: %s", instanceName), ice); - throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, ice.getMessage()); - } - if (userVm == null) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); - } - DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); + if (userVm == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); + } - List reservations = new ArrayList<>(); - List resourceLimitStorageTags = resourceLimitService.getResourceLimitStorageTagsForResourceCountOperation(true, diskOffering); - try { - CheckedReservation volumeReservation = new CheckedReservation(owner, Resource.ResourceType.volume, resourceLimitStorageTags, + DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); + List resourceLimitStorageTags = resourceLimitService.getResourceLimitStorageTagsForResourceCountOperation(true, diskOffering); + CheckedReservation volumeReservation = new CheckedReservation(owner, Resource.ResourceType.volume, resourceLimitStorageTags, CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? 1L : 0L, reservationDao, resourceLimitService); - reservations.add(volumeReservation); + reservations.add(volumeReservation); - String rootVolumeName = String.format("ROOT-%s", userVm.getId()); - DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null, false); + String rootVolumeName = String.format("ROOT-%s", userVm.getId()); + DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null, false); - final VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm, template, serviceOffering, owner, null); - ServiceOfferingVO dummyOffering = serviceOfferingDao.findById(userVm.getId(), serviceOffering.getId()); - profile.setServiceOffering(dummyOffering); - DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList(); - final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), null, null, hostId, poolId, null); - DeployDestination dest = null; - try { - dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null); - } catch (Exception e) { - logger.warn("Import failed for Vm: {} while finding deployment destination", userVm, e); - cleanupFailedImportVM(userVm); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName())); - } - if(dest == null) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s. Suitable deployment destination not found", userVm.getInstanceName())); - } + final VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm, template, serviceOffering, owner, null); + ServiceOfferingVO dummyOffering = serviceOfferingDao.findById(userVm.getId(), serviceOffering.getId()); + profile.setServiceOffering(dummyOffering); + DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList(); + final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), null, null, hostId, poolId, null); + DeployDestination dest = null; + try { + dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null); + } catch (Exception e) { + logger.warn("Import failed for Vm: {} while finding deployment destination", userVm, e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName())); + } + if(dest == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s. Suitable deployment destination not found", userVm.getInstanceName())); + } - Map storage = dest.getStorageForDisks(); - Volume volume = volumeDao.findById(diskProfile.getVolumeId()); - StoragePool storagePool = storage.get(volume); - CheckVolumeCommand checkVolumeCommand = new CheckVolumeCommand(); - checkVolumeCommand.setSrcFile(diskPath); - StorageFilerTO storageTO = new StorageFilerTO(storagePool); - checkVolumeCommand.setStorageFilerTO(storageTO); - Answer answer = agentManager.easySend(dest.getHost().getId(), checkVolumeCommand); - if (!(answer instanceof CheckVolumeAnswer)) { - cleanupFailedImportVM(userVm); - throw new CloudRuntimeException("Disk not found or is invalid"); - } - CheckVolumeAnswer checkVolumeAnswer = (CheckVolumeAnswer) answer; - try { - checkVolume(checkVolumeAnswer.getVolumeDetails()); - } catch (CloudRuntimeException e) { + Map storage = dest.getStorageForDisks(); + Volume volume = volumeDao.findById(diskProfile.getVolumeId()); + StoragePool storagePool = storage.get(volume); + CheckVolumeCommand checkVolumeCommand = new CheckVolumeCommand(); + checkVolumeCommand.setSrcFile(diskPath); + StorageFilerTO storageTO = new StorageFilerTO(storagePool); + checkVolumeCommand.setStorageFilerTO(storageTO); + Answer answer = agentManager.easySend(dest.getHost().getId(), checkVolumeCommand); + if (!(answer instanceof CheckVolumeAnswer)) { + cleanupFailedImportVM(userVm); + throw new CloudRuntimeException("Disk not found or is invalid"); + } + CheckVolumeAnswer checkVolumeAnswer = (CheckVolumeAnswer) answer; + try { + checkVolume(checkVolumeAnswer.getVolumeDetails()); + } catch (CloudRuntimeException e) { + cleanupFailedImportVM(userVm); + throw e; + } + if (!checkVolumeAnswer.getResult()) { + cleanupFailedImportVM(userVm); + throw new CloudRuntimeException("Disk not found or is invalid"); + } + diskProfile.setSize(checkVolumeAnswer.getSize()); + + CheckedReservation primaryStorageReservation = new CheckedReservation(owner, Resource.ResourceType.primary_storage, resourceLimitStorageTags, + CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? diskProfile.getSize() : 0L, reservationDao, resourceLimitService); + reservations.add(primaryStorageReservation); + + List> diskProfileStoragePoolList = new ArrayList<>(); + try { + long deviceId = 1L; + if(ImportSource.SHARED == importSource) { + diskProfileStoragePoolList.add(importKVMSharedDisk(userVm, diskOffering, Volume.Type.ROOT, + template, deviceId, poolId, diskPath, diskProfile)); + } else if(ImportSource.LOCAL == importSource) { + diskProfileStoragePoolList.add(importKVMLocalDisk(userVm, diskOffering, Volume.Type.ROOT, + template, deviceId, hostId, diskPath, diskProfile)); + } + } catch (Exception e) { + logger.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import volumes while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); + } + networkOrchestrationService.importNic(macAddress, 0, network, true, userVm, requestedIpPair, zone, true); + publishVMUsageUpdateResourceCount(userVm, dummyOffering, template); + return userVm; + + } catch (InsufficientCapacityException ice) { // This will be thrown by com.cloud.vm.UserVmService.importVM + logger.error(String.format("Failed to import vm name: %s", instanceName), ice); + throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, ice.getMessage()); + } catch (ResourceAllocationException e) { cleanupFailedImportVM(userVm); throw e; + } finally { + ReservationHelper.closeAll(reservations); } - if (!checkVolumeAnswer.getResult()) { - cleanupFailedImportVM(userVm); - throw new CloudRuntimeException("Disk not found or is invalid"); - } - diskProfile.setSize(checkVolumeAnswer.getSize()); + } - CheckedReservation primaryStorageReservation = new CheckedReservation(owner, Resource.ResourceType.primary_storage, resourceLimitStorageTags, - CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? diskProfile.getSize() : 0L, reservationDao, resourceLimitService); - reservations.add(primaryStorageReservation); + protected void checkVmResourceLimitsForExternalKvmVmImport(Account owner, ServiceOfferingVO serviceOffering, VMTemplateVO template, Map details, List reservations) throws ResourceAllocationException { + // When importing an external VM, the amount of CPUs and memory is always obtained from the compute offering, + // unlike the unmanaged instance import that obtains it from the hypervisor unless the VM is powered off and the offering is fixed + Integer cpu = serviceOffering.getCpu(); + Integer memory = serviceOffering.getRamSize(); - List> diskProfileStoragePoolList = new ArrayList<>(); - try { - long deviceId = 1L; - if(ImportSource.SHARED == importSource) { - diskProfileStoragePoolList.add(importKVMSharedDisk(userVm, diskOffering, Volume.Type.ROOT, - template, deviceId, poolId, diskPath, diskProfile)); - } else if(ImportSource.LOCAL == importSource) { - diskProfileStoragePoolList.add(importKVMLocalDisk(userVm, diskOffering, Volume.Type.ROOT, - template, deviceId, hostId, diskPath, diskProfile)); - } - } catch (Exception e) { - logger.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e); - cleanupFailedImportVM(userVm); - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import volumes while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); + if (serviceOffering.isDynamic()) { + cpu = getDetailAsInteger(VmDetailConstants.CPU_NUMBER, details); + memory = getDetailAsInteger(VmDetailConstants.MEMORY, details); } - networkOrchestrationService.importNic(macAddress, 0, network, true, userVm, requestedIpPair, zone, true); - publishVMUsageUpdateResourceCount(userVm, dummyOffering, template); - return userVm; - } finally { - ReservationHelper.closeAll(reservations); + List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(serviceOffering, template); + + CheckedReservation vmReservation = new CheckedReservation(owner, Resource.ResourceType.user_vm, resourceLimitHostTags, 1L, reservationDao, resourceLimitService); + reservations.add(vmReservation); + + CheckedReservation cpuReservation = new CheckedReservation(owner, Resource.ResourceType.cpu, resourceLimitHostTags, cpu.longValue(), reservationDao, resourceLimitService); + reservations.add(cpuReservation); + + CheckedReservation memReservation = new CheckedReservation(owner, Resource.ResourceType.memory, resourceLimitHostTags, memory.longValue(), reservationDao, resourceLimitService); + reservations.add(memReservation); + } + + protected Integer getDetailAsInteger(String key, Map details) { + String detail = details.get(key); + if (detail == null) { + throw new InvalidParameterValueException(String.format("Detail '%s' must be provided.", key)); + } + try { + return Integer.valueOf(detail); + } catch (NumberFormatException e) { + throw new InvalidParameterValueException(String.format("Please provide a valid integer value for detail '%s'.", key)); } } diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index 9935a228c5b4..544c37d57e92 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -38,9 +38,11 @@ import java.util.Map; import java.util.UUID; +import com.cloud.exception.ResourceAllocationException; import com.cloud.offering.DiskOffering; import com.cloud.resourcelimit.CheckedReservation; import com.cloud.vm.ImportVMTaskVO; +import com.cloud.vm.VmDetailConstants; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.ResponseObject; @@ -60,6 +62,7 @@ import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -167,7 +170,6 @@ import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; -import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; @@ -178,7 +180,7 @@ public class UnmanagedVMsManagerImplTest { @Spy @InjectMocks - private UnmanagedVMsManagerImpl unmanagedVMsManager = new UnmanagedVMsManagerImpl(); + private UnmanagedVMsManagerImpl unmanagedVMsManager; @Mock private UserVmManager userVmManager; @@ -255,11 +257,18 @@ public class UnmanagedVMsManagerImplTest { ImportVMTaskVO importVMTaskVO; @Mock private VMInstanceDetailsDao vmInstanceDetailsDao; - @Mock private ConfigKey configKeyMockParamsAllowed; @Mock private ConfigKey configKeyMockParamsAllowedList; + @Mock + private Account accountMock; + @Mock + private ServiceOfferingVO serviceOfferingMock; + @Mock + private VMTemplateVO templateMock; + @Mock + private UnmanagedInstanceTO unmanagedInstanceMock; private static final long virtualMachineId = 1L; @@ -386,6 +395,11 @@ public void setUp() throws Exception { when(vmDao.findById(virtualMachineId)).thenReturn(virtualMachine); when(virtualMachine.getState()).thenReturn(VirtualMachine.State.Running); + + when(unmanagedInstanceMock.getCpuCores()).thenReturn(8); + when(unmanagedInstanceMock.getMemory()).thenReturn(4096); + when(serviceOfferingMock.getCpu()).thenReturn(4); + when(serviceOfferingMock.getRamSize()).thenReturn(2048); } @NotNull @@ -1321,4 +1335,102 @@ public void testAddServiceOfferingDetailsToParamsCustomUnconstrainedOffering() { Assert.assertFalse(params.containsKey(VmDetailConstants.CPU_SPEED)); Assert.assertFalse(params.containsKey(VmDetailConstants.MEMORY)); } + + @Test + public void checkVmResourceLimitsForUnmanagedInstanceImportTestUsesInformationFromHypervisorWhenOfferingIsDynamic() throws Exception { + when(serviceOfferingMock.isDynamic()).thenReturn(true); + List reservations = new ArrayList<>(); + + try (MockedConstruction mockedConstruction = Mockito.mockConstruction(CheckedReservation.class)) { + unmanagedVMsManager.checkVmResourceLimitsForUnmanagedInstanceImport(accountMock, unmanagedInstanceMock, serviceOfferingMock, templateMock, reservations); + + Assert.assertEquals(3, mockedConstruction.constructed().size()); + Assert.assertEquals(3, reservations.size()); + verify(unmanagedInstanceMock).getCpuCores(); + verify(unmanagedInstanceMock).getMemory(); + } + } + + @Test + public void checkVmResourceLimitsForUnmanagedInstanceImportTestUsesInformationFromHypervisorWhenVmIsPoweredOn() throws Exception { + when(unmanagedInstanceMock.getPowerState()).thenReturn(UnmanagedInstanceTO.PowerState.PowerOn); + when(serviceOfferingMock.isDynamic()).thenReturn(false); + List reservations = new ArrayList<>(); + + try (MockedConstruction mockedConstruction = Mockito.mockConstruction(CheckedReservation.class)) { + unmanagedVMsManager.checkVmResourceLimitsForUnmanagedInstanceImport(accountMock, unmanagedInstanceMock, serviceOfferingMock, templateMock, reservations); + + Assert.assertEquals(3, mockedConstruction.constructed().size()); + Assert.assertEquals(3, reservations.size()); + verify(unmanagedInstanceMock).getCpuCores(); + verify(unmanagedInstanceMock).getMemory(); + } + } + + @Test + public void checkVmResourceLimitsForUnmanagedInstanceImportTestUsesInformationFromOfferingWhenOfferingIsNotDynamicAndVmIsPoweredOff() throws Exception { + when(unmanagedInstanceMock.getPowerState()).thenReturn(UnmanagedInstanceTO.PowerState.PowerOff); + when(serviceOfferingMock.isDynamic()).thenReturn(false); + List reservations = new ArrayList<>(); + + try (MockedConstruction mockedConstruction = Mockito.mockConstruction(CheckedReservation.class)) { + unmanagedVMsManager.checkVmResourceLimitsForUnmanagedInstanceImport(accountMock, unmanagedInstanceMock, serviceOfferingMock, templateMock, reservations); + + Assert.assertEquals(3, mockedConstruction.constructed().size()); + Assert.assertEquals(3, reservations.size()); + verify(serviceOfferingMock).getCpu(); + verify(serviceOfferingMock).getRamSize(); + verify(unmanagedInstanceMock, Mockito.never()).getCpuCores(); + verify(unmanagedInstanceMock, Mockito.never()).getMemory(); + } + } + + @Test + public void checkVmResourceLimitsForExternalKvmVmImportTestUsesInformationFromOfferingWhenOfferingIsNotDynamic() throws ResourceAllocationException { + when(serviceOfferingMock.isDynamic()).thenReturn(false); + Map details = new HashMap<>(); + List reservations = new ArrayList<>(); + + try (MockedConstruction mockedConstruction = Mockito.mockConstruction(CheckedReservation.class)) { + unmanagedVMsManager.checkVmResourceLimitsForExternalKvmVmImport(accountMock, serviceOfferingMock, templateMock, details, reservations); + + Assert.assertEquals(3, mockedConstruction.constructed().size()); + Assert.assertEquals(3, reservations.size()); + verify(serviceOfferingMock).getCpu(); + verify(serviceOfferingMock).getRamSize(); + verify(unmanagedVMsManager, Mockito.never()).getDetailAsInteger(VmDetailConstants.CPU_NUMBER, details); + verify(unmanagedVMsManager, Mockito.never()).getDetailAsInteger(VmDetailConstants.MEMORY, details); + } + } + + @Test + public void checkVmResourceLimitsForExternalKvmVmImportTestUsesInformationFromDetailsWhenOfferingIsDynamic() throws ResourceAllocationException { + when(serviceOfferingMock.isDynamic()).thenReturn(true); + Map details = new HashMap<>(); + details.put(VmDetailConstants.CPU_NUMBER, "8"); + details.put(VmDetailConstants.MEMORY, "4096"); + List reservations = new ArrayList<>(); + + try (MockedConstruction mockedConstruction = Mockito.mockConstruction(CheckedReservation.class)) { + unmanagedVMsManager.checkVmResourceLimitsForExternalKvmVmImport(accountMock, serviceOfferingMock, templateMock, details, reservations); + + Assert.assertEquals(3, mockedConstruction.constructed().size()); + Assert.assertEquals(3, reservations.size()); + verify(unmanagedVMsManager).getDetailAsInteger(VmDetailConstants.CPU_NUMBER, details); + verify(unmanagedVMsManager).getDetailAsInteger(VmDetailConstants.MEMORY, details); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void getDetailAsIntegerTestThrowsInvalidParameterValueExceptionWhenDetailIsNull() { + Map details = new HashMap<>(); + unmanagedVMsManager.getDetailAsInteger("non-existent", details); + } + + @Test(expected = InvalidParameterValueException.class) + public void getDetailAsIntegerTestThrowsInvalidParameterValueExceptionWhenValueIsInvalid() { + Map details = new HashMap<>(); + details.put("key", "not-a-number"); + unmanagedVMsManager.getDetailAsInteger("key", details); + } } From a127a26ebd3cb2b3cdfed3e8a6b5f1a2c855a5cd Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:19:52 +0530 Subject: [PATCH 027/205] Fix Revert Instance to Snapshot with custom service offering (#12885) * Fix revertVM with custom svc offering --- .../vm/snapshot/VMSnapshotManagerImpl.java | 54 +++++++++++----- .../vm/snapshot/VMSnapshotManagerTest.java | 62 ++++++++++++++----- 2 files changed, 87 insertions(+), 29 deletions(-) diff --git a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java index b2e820bce946..fe91e1d9caae 100644 --- a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java @@ -751,11 +751,17 @@ public UserVm revertToSnapshot(Long vmSnapshotId) throws InsufficientCapacityExc "VM Snapshot reverting failed due to vm is not in the state of Running or Stopped."); } - if (userVm.getState() == VirtualMachine.State.Running && vmSnapshotVo.getType() == VMSnapshot.Type.Disk || userVm.getState() == VirtualMachine.State.Stopped - && vmSnapshotVo.getType() == VMSnapshot.Type.DiskAndMemory) { + if (userVm.getState() == VirtualMachine.State.Running && vmSnapshotVo.getType() == VMSnapshot.Type.Disk) { throw new InvalidParameterValueException( - "VM Snapshot revert not allowed. This will result in VM state change. You can revert running VM to disk and memory type snapshot and stopped VM to disk type" - + " snapshot"); + "Reverting to the Instance Snapshot is not allowed for running Instances as this would result in an Instance state change. " + + "For running Instances only Snapshots with memory can be reverted. " + + "In order to revert to a Snapshot without memory you need to first stop the Instance."); + } + + if (userVm.getState() == VirtualMachine.State.Stopped && vmSnapshotVo.getType() == VMSnapshot.Type.DiskAndMemory) { + throw new InvalidParameterValueException( + "Reverting to the Instance Snapshot is not allowed for stopped Instances when the Snapshot contains memory as this would result in an Instance state change. " + + "In order to revert to a Snapshot with memory you need to first start the Instance."); } // if snapshot is not created, error out @@ -808,20 +814,36 @@ else if (jobResult instanceof Throwable) } /** - * If snapshot was taken with a different service offering than actual used in vm, should change it back to it. - * We also call changeUserVmServiceOffering in case the service offering is dynamic in order to - * perform resource limit validation, as the amount of CPUs or memory may have been changed. - * @param vmSnapshotVo vm snapshot + * Check if service offering change is needed for user vm when reverting to vm snapshot. + * Service offering change is needed when snapshot was taken with a different service offering than actual used in vm. + * Service offering change is also needed when service offering is dynamic and the amount of cpu, memory or cpu speed + * has been changed since snapshot was taken. + * @param userVm + * @param vmSnapshotVo + * @return true if service offering change is needed; false otherwise */ - protected void updateUserVmServiceOffering(UserVm userVm, VMSnapshotVO vmSnapshotVo) { + protected boolean userVmServiceOfferingNeedsChange(UserVm userVm, VMSnapshotVO vmSnapshotVo) { if (vmSnapshotVo.getServiceOfferingId() != userVm.getServiceOfferingId()) { - changeUserVmServiceOffering(userVm, vmSnapshotVo); - return; + return true; } - ServiceOfferingVO serviceOffering = _serviceOfferingDao.findById(userVm.getServiceOfferingId()); - if (serviceOffering.isDynamic()) { - changeUserVmServiceOffering(userVm, vmSnapshotVo); + + ServiceOfferingVO currentServiceOffering = _serviceOfferingDao.findByIdIncludingRemoved(userVm.getId(), userVm.getServiceOfferingId()); + if (currentServiceOffering.isDynamic()) { + Map vmDetails = getVmMapDetails(vmSnapshotVo); + ServiceOfferingVO newServiceOffering = _serviceOfferingDao.getComputeOffering(currentServiceOffering, vmDetails); + + int newCpu = newServiceOffering.getCpu(); + int newMemory = newServiceOffering.getRamSize(); + int newSpeed = newServiceOffering.getSpeed(); + int currentCpu = currentServiceOffering.getCpu(); + int currentMemory = currentServiceOffering.getRamSize(); + int currentSpeed = currentServiceOffering.getSpeed(); + + if (newCpu != currentCpu || newMemory != currentMemory || newSpeed != currentSpeed) { + return true; + } } + return false; } /** @@ -941,7 +963,9 @@ private UserVm orchestrateRevertToVMSnapshot(Long vmSnapshotId) throws Insuffici Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) throws CloudRuntimeException { - updateUserVmServiceOffering(userVm, vmSnapshotVo); + if (userVmServiceOfferingNeedsChange(userVm, vmSnapshotVo)) { + changeUserVmServiceOffering(userVm, vmSnapshotVo); + } revertUserVmDetailsFromVmSnapshot(userVm, vmSnapshotVo); } }); diff --git a/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java b/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java index 7b12fc4a99c0..88af1a6325d3 100644 --- a/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java +++ b/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java @@ -67,7 +67,6 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; @@ -79,9 +78,12 @@ import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -223,6 +225,7 @@ public void setup() { when(vmSnapshotVO.getId()).thenReturn(VM_SNAPSHOT_ID); when(serviceOffering.isDynamic()).thenReturn(false); when(_serviceOfferingDao.findById(SERVICE_OFFERING_ID)).thenReturn(serviceOffering); + when(_serviceOfferingDao.findByIdIncludingRemoved(TEST_VM_ID, SERVICE_OFFERING_ID)).thenReturn(serviceOffering); for (ResourceDetail detail : Arrays.asList(userVmDetailCpuNumber, vmSnapshotDetailCpuNumber)) { when(detail.getName()).thenReturn("cpuNumber"); @@ -340,20 +343,51 @@ public void testAddSupportForCustomServiceOfferingDynamicServiceOffering() { } @Test - public void testUpdateUserVmServiceOfferingSameServiceOffering() { - _vmSnapshotMgr.updateUserVmServiceOffering(userVm, vmSnapshotVO); - verify(_vmSnapshotMgr, never()).changeUserVmServiceOffering(userVm, vmSnapshotVO); + public void testUserVmServiceOfferingNeedsChangeWhenSnapshotOfferingDiffers() { + when(userVm.getServiceOfferingId()).thenReturn(SERVICE_OFFERING_DIFFERENT_ID); + when(vmSnapshotVO.getServiceOfferingId()).thenReturn(SERVICE_OFFERING_ID); + + assertTrue(_vmSnapshotMgr.userVmServiceOfferingNeedsChange(userVm, vmSnapshotVO)); + + verify(_serviceOfferingDao, never()).findByIdIncludingRemoved(anyLong(), anyLong()); + verify(_serviceOfferingDao, never()).getComputeOffering(any(ServiceOfferingVO.class), any()); } @Test - public void testUpdateUserVmServiceOfferingDifferentServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { - when(userVm.getServiceOfferingId()).thenReturn(SERVICE_OFFERING_DIFFERENT_ID); - when(_userVmManager.upgradeVirtualMachine(ArgumentMatchers.eq(TEST_VM_ID), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); - _vmSnapshotMgr.updateUserVmServiceOffering(userVm, vmSnapshotVO); + public void testUserVmServiceOfferingNeedsChangeWhenSameNonDynamicOffering() { + assertFalse(_vmSnapshotMgr.userVmServiceOfferingNeedsChange(userVm, vmSnapshotVO)); + + verify(_serviceOfferingDao).findByIdIncludingRemoved(TEST_VM_ID, SERVICE_OFFERING_ID); + verify(_serviceOfferingDao, never()).getComputeOffering(any(ServiceOfferingVO.class), any()); + } - verify(_vmSnapshotMgr).changeUserVmServiceOffering(userVm, vmSnapshotVO); + @Test + public void testUserVmServiceOfferingNeedsChangeWhenDynamicOfferingMatchesSnapshot() { + when(serviceOffering.isDynamic()).thenReturn(true); + when(serviceOffering.getCpu()).thenReturn(2); + when(serviceOffering.getRamSize()).thenReturn(2048); + when(serviceOffering.getSpeed()).thenReturn(1000); + when(_serviceOfferingDao.getComputeOffering(eq(serviceOffering), any())).thenReturn(serviceOffering); + + assertFalse(_vmSnapshotMgr.userVmServiceOfferingNeedsChange(userVm, vmSnapshotVO)); + + verify(_serviceOfferingDao).getComputeOffering(eq(serviceOffering), any()); verify(_vmSnapshotMgr).getVmMapDetails(vmSnapshotVO); - verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(ArgumentMatchers.eq(userVm), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); + } + + @Test + public void testUserVmServiceOfferingNeedsChangeWhenDynamicCpuDiffersFromSnapshot() { + when(serviceOffering.isDynamic()).thenReturn(true); + when(serviceOffering.getCpu()).thenReturn(2); + when(serviceOffering.getRamSize()).thenReturn(2048); + when(serviceOffering.getSpeed()).thenReturn(1000); + ServiceOfferingVO fromSnapshot = mock(ServiceOfferingVO.class); + when(fromSnapshot.getCpu()).thenReturn(4); + when(fromSnapshot.getRamSize()).thenReturn(2048); + when(fromSnapshot.getSpeed()).thenReturn(1000); + when(_serviceOfferingDao.getComputeOffering(eq(serviceOffering), any())).thenReturn(fromSnapshot); + + assertTrue(_vmSnapshotMgr.userVmServiceOfferingNeedsChange(userVm, vmSnapshotVO)); } @Test @@ -368,18 +402,18 @@ public void testGetVmMapDetails() { @Test public void testChangeUserVmServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { - when(_userVmManager.upgradeVirtualMachine(ArgumentMatchers.eq(TEST_VM_ID), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); + when(_userVmManager.upgradeVirtualMachine(eq(TEST_VM_ID), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); _vmSnapshotMgr.changeUserVmServiceOffering(userVm, vmSnapshotVO); verify(_vmSnapshotMgr).getVmMapDetails(vmSnapshotVO); - verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(ArgumentMatchers.eq(userVm), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); + verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(eq(userVm), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); } @Test(expected=CloudRuntimeException.class) public void testChangeUserVmServiceOfferingFailOnUpgradeVMServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { - when(_userVmManager.upgradeVirtualMachine(ArgumentMatchers.eq(TEST_VM_ID), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(false); + when(_userVmManager.upgradeVirtualMachine(eq(TEST_VM_ID), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(false); _vmSnapshotMgr.changeUserVmServiceOffering(userVm, vmSnapshotVO); verify(_vmSnapshotMgr).getVmMapDetails(vmSnapshotVO); - verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(ArgumentMatchers.eq(userVm), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); + verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(eq(userVm), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); } @Test From 9f96c9d5eb97f1836103918cf520d72cc3395c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= <89930804+erikbocks@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:53:11 -0300 Subject: [PATCH 028/205] Flexibilize public IP selection (#11076) --- .../network/dao/PublicIpQuarantineDao.java | 12 +++ .../dao/PublicIpQuarantineDaoImpl.java | 22 +++++ .../cloud/network/IpAddressManagerImpl.java | 91 +++++++++++-------- .../cloud/network/IpAddressManagerTest.java | 1 + .../integration/smoke/test_quarantined_ips.py | 8 +- 5 files changed, 92 insertions(+), 42 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java index ccba6bb18893..606bdaaaa7a7 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java @@ -19,9 +19,21 @@ import com.cloud.network.vo.PublicIpQuarantineVO; import com.cloud.utils.db.GenericDao; +import java.util.Date; +import java.util.List; + public interface PublicIpQuarantineDao extends GenericDao { PublicIpQuarantineVO findByPublicIpAddressId(long publicIpAddressId); PublicIpQuarantineVO findByIpAddress(String publicIpAddress); + + /** + * Returns a list of public IP addresses that are actively quarantined at the specified date and the previous owner differs from the specified user. + * + * @param userId used to check against the IP's previous owner; + * @param date used to check if the quarantine is active; + * @return a list of PublicIpQuarantineVOs. + */ + List listQuarantinedIpAddressesToUser(Long userId, Date date); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java index a1b789b8a46b..0c47a0d36e30 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java @@ -26,6 +26,8 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; +import java.util.Date; +import java.util.List; @Component public class PublicIpQuarantineDaoImpl extends GenericDaoBase implements PublicIpQuarantineDao { @@ -33,6 +35,8 @@ public class PublicIpQuarantineDaoImpl extends GenericDaoBase ipAddressSearchBuilder; + private SearchBuilder quarantinedIpAddressesSearch; + @Inject IPAddressDao ipAddressDao; @@ -47,8 +51,16 @@ public void init() { publicIpAddressByIdSearch.join("quarantineJoin", ipAddressSearchBuilder, ipAddressSearchBuilder.entity().getId(), publicIpAddressByIdSearch.entity().getPublicIpAddressId(), JoinBuilder.JoinType.INNER); + quarantinedIpAddressesSearch = createSearchBuilder(); + quarantinedIpAddressesSearch.and("previousOwnerId", quarantinedIpAddressesSearch.entity().getPreviousOwnerId(), SearchCriteria.Op.NEQ); + quarantinedIpAddressesSearch.and(); + quarantinedIpAddressesSearch.op("removedIsNull", quarantinedIpAddressesSearch.entity().getRemoved(), SearchCriteria.Op.NULL); + quarantinedIpAddressesSearch.and("endDate", quarantinedIpAddressesSearch.entity().getEndDate(), SearchCriteria.Op.GT); + quarantinedIpAddressesSearch.cp(); + ipAddressSearchBuilder.done(); publicIpAddressByIdSearch.done(); + quarantinedIpAddressesSearch.done(); } @Override @@ -68,4 +80,14 @@ public PublicIpQuarantineVO findByIpAddress(String publicIpAddress) { return findOneBy(sc, filter); } + + @Override + public List listQuarantinedIpAddressesToUser(Long userId, Date date) { + SearchCriteria sc = quarantinedIpAddressesSearch.create(); + + sc.setParameters("previousOwnerId", userId); + sc.setParameters("endDate", date); + + return searchIncludingRemoved(sc, null, false, false); + } } diff --git a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java index fe555af9d50f..c5ca392a4375 100644 --- a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java +++ b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java @@ -514,6 +514,9 @@ public boolean configure(String name, Map params) { AssignIpAddressSearch.and("allocated", AssignIpAddressSearch.entity().getAllocatedTime(), Op.NULL); AssignIpAddressSearch.and("vlanId", AssignIpAddressSearch.entity().getVlanId(), Op.IN); AssignIpAddressSearch.and("forSystemVms", AssignIpAddressSearch.entity().isForSystemVms(), Op.EQ); + AssignIpAddressSearch.and("id", AssignIpAddressSearch.entity().getId(), Op.NIN); + AssignIpAddressSearch.and("requestedAddress", AssignIpAddressSearch.entity().getAddress(), Op.EQ); + AssignIpAddressSearch.and("routerAddress", AssignIpAddressSearch.entity().getAddress(), Op.NEQ); SearchBuilder vlanSearch = _vlanDao.createSearchBuilder(); vlanSearch.and("type", vlanSearch.entity().getVlanType(), Op.EQ); @@ -883,10 +886,23 @@ public List listAvailablePublicIps(final long dcId, final Long podI if (podId != null) { sc = AssignIpAddressFromPodVlanSearch.create(); sc.setJoinParameters("podVlanMapSB", "podId", podId); - errorMessage.append(" pod id=" + podId); + errorMessage.append(" pod id=").append(podId); } else { sc = AssignIpAddressSearch.create(); - errorMessage.append(" zone id=" + dcId); + errorMessage.append(" zone id=").append(dcId); + } + + if (lockOneRow) { + logger.debug("Listing quarantined public IPs to ignore on search for public IP for system VM. The IPs ignored will be the ones that: were not associated to account [{}]; were not removed yet; and with quarantine end dates after [{}].", owner.getUuid(), new Date()); + + List quarantinedAddresses = publicIpQuarantineDao.listQuarantinedIpAddressesToUser(owner.getId(), new Date()); + List quarantinedAddressesIDs = quarantinedAddresses.stream().map(PublicIpQuarantineVO::getPublicIpAddressId).collect(Collectors.toList()); + + logger.debug("Found addresses with the following IDs: [{}] that will be ignored when searching for available public IPs.", quarantinedAddressesIDs); + + if (CollectionUtils.isNotEmpty(quarantinedAddressesIDs)) { + sc.setParameters("id", quarantinedAddressesIDs.toArray()); + } } sc.setParameters("dc", dcId); @@ -894,11 +910,11 @@ public List listAvailablePublicIps(final long dcId, final Long podI // for direct network take ip addresses only from the vlans belonging to the network if (vlanUse == VlanType.DirectAttached) { sc.setJoinParameters("vlan", "networkId", guestNetworkId); - errorMessage.append(", network id=" + guestNetworkId); + errorMessage.append(", network id=").append(guestNetworkId); } if (requestedGateway != null) { sc.setJoinParameters("vlan", "vlanGateway", requestedGateway); - errorMessage.append(", requested gateway=" + requestedGateway); + errorMessage.append(", requested gateway=").append(requestedGateway); } sc.setJoinParameters("vlan", "type", vlanUse); @@ -908,38 +924,39 @@ public List listAvailablePublicIps(final long dcId, final Long podI NetworkDetailVO routerIpDetail = _networkDetailsDao.findDetail(network.getId(), ApiConstants.ROUTER_IP); routerIpAddress = routerIpDetail != null ? routerIpDetail.getValue() : null; } + if (requestedIp != null) { - sc.addAnd("address", SearchCriteria.Op.EQ, requestedIp); - errorMessage.append(": requested ip " + requestedIp + " is not available"); + sc.setParameters("requestedAddress", requestedIp); + errorMessage.append(": requested ip ").append(requestedIp).append(" is not available"); } else if (routerIpAddress != null) { - sc.addAnd("address", Op.NEQ, routerIpAddress); + sc.setParameters("routerAddress", routerIpAddress); } boolean ascOrder = ! forSystemVms; - Filter filter = new Filter(IPAddressVO.class, "forSystemVms", ascOrder, 0l, 1l); + Filter filter = new Filter(IPAddressVO.class, "forSystemVms", ascOrder, 0L, 1L); filter.addOrderBy(IPAddressVO.class,"vlanId", true); - List addrs = new ArrayList<>(); + List addresses = new ArrayList<>(); if (forSystemVms) { // Get Public IPs for system vms in dedicated ranges sc.setParameters("forSystemVms", true); if (lockOneRow) { - addrs = _ipAddressDao.lockRows(sc, filter, true); + addresses = _ipAddressDao.lockRows(sc, filter, true); } else { - addrs = new ArrayList<>(_ipAddressDao.search(sc, null)); + addresses = new ArrayList<>(_ipAddressDao.search(sc, null)); } } - if ((!lockOneRow || (lockOneRow && CollectionUtils.isEmpty(addrs))) && + if ((!lockOneRow || (lockOneRow && CollectionUtils.isEmpty(addresses))) && !(forSystemVms && SystemVmPublicIpReservationModeStrictness.value())) { sc.setParameters("forSystemVms", false); // If owner has dedicated Public IP ranges, fetch IP from the dedicated range // Otherwise fetch IP from the system pool // Checking if network is null in the case of system VM's. At the time of allocation of IP address to systemVm, no network is present. if (network == null || !(network.getGuestType() == GuestType.Shared && zone.getNetworkType() == NetworkType.Advanced)) { - List maps = _accountVlanMapDao.listAccountVlanMapsByAccount(owner.getId()); - for (AccountVlanMapVO map : maps) { + List accountVlanMaps = _accountVlanMapDao.listAccountVlanMapsByAccount(owner.getId()); + for (AccountVlanMapVO map : accountVlanMaps) { if (vlanDbIds == null || vlanDbIds.contains(map.getVlanDbId())) dedicatedVlanDbIds.add(map.getVlanDbId()); } @@ -958,10 +975,10 @@ public List listAvailablePublicIps(final long dcId, final Long podI if (!dedicatedVlanDbIds.isEmpty()) { fetchFromDedicatedRange = true; sc.setParameters("vlanId", dedicatedVlanDbIds.toArray()); - errorMessage.append(", vlanId id=" + Arrays.toString(dedicatedVlanDbIds.toArray())); + errorMessage.append(", vlanId id=").append(Arrays.toString(dedicatedVlanDbIds.toArray())); } else if (!nonDedicatedVlanDbIds.isEmpty()) { sc.setParameters("vlanId", nonDedicatedVlanDbIds.toArray()); - errorMessage.append(", vlanId id=" + Arrays.toString(nonDedicatedVlanDbIds.toArray())); + errorMessage.append(", vlanId id=").append(Arrays.toString(nonDedicatedVlanDbIds.toArray())); } else { if (podId != null) { InsufficientAddressCapacityException ex = new InsufficientAddressCapacityException("Insufficient address capacity", Pod.class, podId); @@ -975,13 +992,13 @@ public List listAvailablePublicIps(final long dcId, final Long podI } } if (lockOneRow) { - addrs = _ipAddressDao.lockRows(sc, filter, true); + addresses = _ipAddressDao.lockRows(sc, filter, true); } else { - addrs = new ArrayList<>(_ipAddressDao.search(sc, null)); + addresses = new ArrayList<>(_ipAddressDao.search(sc, null)); } // If all the dedicated IPs of the owner are in use fetch an IP from the system pool - if ((!lockOneRow || (lockOneRow && addrs.size() == 0)) && fetchFromDedicatedRange && vlanUse == VlanType.VirtualNetwork) { + if ((!lockOneRow || (lockOneRow && addresses.isEmpty())) && fetchFromDedicatedRange && vlanUse == VlanType.VirtualNetwork) { // Verify if account is allowed to acquire IPs from the system boolean useSystemIps = UseSystemPublicIps.valueIn(owner.getId()); if (useSystemIps && !nonDedicatedVlanDbIds.isEmpty()) { @@ -989,15 +1006,15 @@ public List listAvailablePublicIps(final long dcId, final Long podI sc.setParameters("vlanId", nonDedicatedVlanDbIds.toArray()); errorMessage.append(", vlanId id=" + Arrays.toString(nonDedicatedVlanDbIds.toArray())); if (lockOneRow) { - addrs = _ipAddressDao.lockRows(sc, filter, true); + addresses = _ipAddressDao.lockRows(sc, filter, true); } else { - addrs.addAll(_ipAddressDao.search(sc, null)); + addresses.addAll(_ipAddressDao.search(sc, null)); } } } } - if (lockOneRow && addrs.size() == 0) { + if (lockOneRow && addresses.isEmpty()) { if (podId != null) { InsufficientAddressCapacityException ex = new InsufficientAddressCapacityException("Insufficient address capacity", Pod.class, podId); // for now, we hardcode the table names, but we should ideally do a lookup for the tablename from the VO object. @@ -1011,13 +1028,12 @@ public List listAvailablePublicIps(final long dcId, final Long podI } if (lockOneRow) { - assert (addrs.size() == 1) : "Return size is incorrect: " + addrs.size(); - IpAddress ipAddress = addrs.get(0); - boolean ipCanBeAllocated = canPublicIpAddressBeAllocated(ipAddress, owner); + IPAddressVO allocatableIp = addresses.get(0); + + boolean isPublicIpAllocatable = canPublicIpAddressBeAllocated(allocatableIp, owner); - if (!ipCanBeAllocated) { - throw new InsufficientAddressCapacityException(String.format("Failed to allocate public IP address [%s] as it is in quarantine.", ipAddress.getAddress()), - DataCenter.class, dcId); + if (!isPublicIpAllocatable) { + throw new InsufficientAddressCapacityException(String.format("Failed to allocate public IP [%s] as it is in quarantine.", allocatableIp.getAddress()), DataCenter.class, dcId); } } @@ -1026,12 +1042,12 @@ public List listAvailablePublicIps(final long dcId, final Long podI try { _resourceLimitMgr.checkResourceLimit(owner, ResourceType.public_ip); } catch (ResourceAllocationException ex) { - logger.warn("Failed to allocate resource of type " + ex.getResourceType() + " for account " + owner); + logger.warn("Failed to allocate resource of type {} for account {}", ex.getResourceType(), owner); throw new AccountLimitException("Maximum number of public IP addresses for account: " + owner.getAccountName() + " has been exceeded."); } } - return addrs; + return addresses; } @DB @@ -2458,26 +2474,27 @@ public boolean canPublicIpAddressBeAllocated(IpAddress ip, Account newOwner) { PublicIpQuarantineVO publicIpQuarantineVO = publicIpQuarantineDao.findByPublicIpAddressId(ip.getId()); if (publicIpQuarantineVO == null) { - logger.debug(String.format("Public IP address [%s] is not in quarantine; therefore, it is allowed to be allocated.", ip)); + logger.debug("Public IP address [{}] is not in quarantine; therefore, it is allowed to be allocated.", ip); return true; } if (!isPublicIpAddressStillInQuarantine(publicIpQuarantineVO, new Date())) { - logger.debug(String.format("Public IP address [%s] is no longer in quarantine; therefore, it is allowed to be allocated.", ip)); + logger.debug("Public IP address [{}] is no longer in quarantine; therefore, it is allowed to be allocated.", ip); + removePublicIpAddressFromQuarantine(publicIpQuarantineVO.getId(), "IP was removed from quarantine because it was no longer in quarantine."); return true; } Account previousOwner = _accountMgr.getAccount(publicIpQuarantineVO.getPreviousOwnerId()); if (Objects.equals(previousOwner.getUuid(), newOwner.getUuid())) { - logger.debug(String.format("Public IP address [%s] is in quarantine; however, the Public IP previous owner [%s] is the same as the new owner [%s]; therefore the IP" + - " can be allocated. The public IP address will be removed from quarantine.", ip, previousOwner, newOwner)); + logger.debug("Public IP address [{}] is in quarantine; however, the Public IP previous owner [{}] is the same as the new owner [{}]; therefore the IP" + + " can be allocated. The public IP address will be removed from quarantine.", ip, previousOwner, newOwner); removePublicIpAddressFromQuarantine(publicIpQuarantineVO.getId(), "IP was removed from quarantine because it has been allocated by the previous owner"); return true; } - logger.error(String.format("Public IP address [%s] is in quarantine and the previous owner [%s] is different than the new owner [%s]; therefore, the IP cannot be " + - "allocated.", ip, previousOwner, newOwner)); + logger.error("Public IP address [{}] is in quarantine and the previous owner [{}] is different than the new owner [{}]; therefore, the IP cannot be " + + "allocated.", ip, previousOwner, newOwner); return false; } @@ -2528,7 +2545,7 @@ public void removePublicIpAddressFromQuarantine(Long quarantineProcessId, String publicIpQuarantineVO.setRemovalReason(removalReason); publicIpQuarantineVO.setRemoverAccountId(removerAccountId); - logger.debug(String.format("Removing public IP Address [%s] from quarantine by updating the removed date to [%s].", ipAddress, removedDate)); + logger.debug("Removing public IP Address [{}] from quarantine by updating the removed date to [{}].", ipAddress, removedDate); publicIpQuarantineDao.persist(publicIpQuarantineVO); } diff --git a/server/src/test/java/com/cloud/network/IpAddressManagerTest.java b/server/src/test/java/com/cloud/network/IpAddressManagerTest.java index 824d4ee47019..cf3a886ce99f 100644 --- a/server/src/test/java/com/cloud/network/IpAddressManagerTest.java +++ b/server/src/test/java/com/cloud/network/IpAddressManagerTest.java @@ -356,6 +356,7 @@ public void checkIfPublicIpAddressIsNotInQuarantineAndCanBeAllocatedTestIpIsNoLo Mockito.when(ipAddressMock.getId()).thenReturn(dummyID); Mockito.when(publicIpQuarantineDaoMock.findByPublicIpAddressId(Mockito.anyLong())).thenReturn(publicIpQuarantineVOMock); Mockito.doReturn(false).when(ipAddressManager).isPublicIpAddressStillInQuarantine(Mockito.any(PublicIpQuarantineVO.class), Mockito.any(Date.class)); + Mockito.doNothing().when(ipAddressManager).removePublicIpAddressFromQuarantine(Mockito.anyLong(), Mockito.anyString()); boolean result = ipAddressManager.canPublicIpAddressBeAllocated(ipAddressMock, newOwnerMock); diff --git a/test/integration/smoke/test_quarantined_ips.py b/test/integration/smoke/test_quarantined_ips.py index 42349fd2a530..2469760da130 100644 --- a/test/integration/smoke/test_quarantined_ips.py +++ b/test/integration/smoke/test_quarantined_ips.py @@ -85,7 +85,7 @@ def setUp(self): self.services["root_admin"]["roletype"]) """ - Set public.ip.address.quarantine.duration to 60 minutes + Set public.ip.address.quarantine.duration to 1 minute """ update_configuration_cmd = updateConfiguration.updateConfigurationCmd() update_configuration_cmd.name = "public.ip.address.quarantine.duration" @@ -168,8 +168,7 @@ def test_only_owner_can_allocate_ip_in_quarantine_vpc(self): zoneid=self.zone.id, vpcid=root_vpc.id, ipaddress=ip_address) - self.assertIn(f"Failed to allocate public IP address [{ip_address}] as it is in quarantine.", - exception.exception.errorMsg) + self.assertIn("errorCode: 533", exception.exception.errorMsg) # Owner should be able to allocate its IP in quarantine public_ip = PublicIPAddress.create(self.domain_admin_apiclient, @@ -267,8 +266,7 @@ def test_only_owner_can_allocate_ip_in_quarantine_network(self): zoneid=self.zone.id, networkid=root_network.id, ipaddress=ip_address) - self.assertIn(f"Failed to allocate public IP address [{ip_address}] as it is in quarantine.", - exception.exception.errorMsg) + self.assertIn("errorCode: 533", exception.exception.errorMsg) # Owner should be able to allocate its IP in quarantine public_ip = PublicIPAddress.create(self.domain_admin_apiclient, From 64ac0822b439d71796108f0f41485b61e3b8b395 Mon Sep 17 00:00:00 2001 From: dahn Date: Thu, 23 Apr 2026 19:16:54 +0100 Subject: [PATCH 029/205] merge conflict fixes (#13046) * merge conflict fixes * fix pre-commit issue Co-authored-by: Daan Hoogland Co-authored-by: Suresh Kumar Anaparti --- agent/conf/agent.properties | 1 - .../wrapper/LibvirtConvertInstanceCommandWrapper.java | 2 +- .../apache/cloudstack/vm/UnmanagedVMsManagerImpl.java | 10 ++-------- .../cloudstack/vm/UnmanagedVMsManagerImplTest.java | 1 - 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties index 013c2e5bf2af..ba4a3874664a 100644 --- a/agent/conf/agent.properties +++ b/agent/conf/agent.properties @@ -472,4 +472,3 @@ iscsi.session.cleanup.enabled=false # Optional vCenter SHA1 thumbprint for VMware to KVM conversion via VDDK, passed as # -io vddk-thumbprint=. If unset, CloudStack computes it on the KVM host via openssl. #vddk.thumbprint= - diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java index eaa8b7904397..f28b5eda4a63 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java @@ -107,7 +107,7 @@ public Answer execute(ConvertInstanceCommand cmd, LibvirtComputingResource serve String vddkLibDir = resolveVddkSetting(cmd.getVddkLibDir(), serverResource.getVddkLibDir()); if (StringUtils.isBlank(vddkLibDir)) { String err = String.format("VDDK lib dir is not configured on the host. " + - "Set '%s' in agent.properties or in details parameter of the import api calll to use VDDK-based conversion.", "vddk.lib.dir"); + "Set '%s' in agent.properties or in details parameter of the import api call to use VDDK-based conversion.", "vddk.lib.dir"); logger.error("({}) {}", originalVMName, err); return new Answer(cmd, false, err); } diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 56998325ba78..846eab599fd1 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -1325,10 +1325,7 @@ private UserVmResponse baseImportInstance(ImportUnmanagedInstanceCmd cmd) { List managedVms = new ArrayList<>(additionalNameFilters); managedVms.addAll(getHostsManagedVms(hosts)); - List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(serviceOffering, template); - try (CheckedReservation vmReservation = new CheckedReservation(owner, Resource.ResourceType.user_vm, resourceLimitHostTags, 1L, reservationDao, resourceLimitService); - CheckedReservation cpuReservation = new CheckedReservation(owner, Resource.ResourceType.cpu, resourceLimitHostTags, Long.valueOf(serviceOffering.getCpu()), reservationDao, resourceLimitService); - CheckedReservation memReservation = new CheckedReservation(owner, Resource.ResourceType.memory, resourceLimitHostTags, Long.valueOf(serviceOffering.getRamSize()), reservationDao, resourceLimitService)) { + try { ActionEventUtils.onStartedActionEvent(userId, owner.getId(), EventTypes.EVENT_VM_IMPORT, cmd.getEventDescription(), null, null, true, 0); @@ -2673,10 +2670,7 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) { UserVm userVm = null; - List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(serviceOffering, template); - try (CheckedReservation vmReservation = new CheckedReservation(owner, Resource.ResourceType.user_vm, resourceLimitHostTags, 1L, reservationDao, resourceLimitService); - CheckedReservation cpuReservation = new CheckedReservation(owner, Resource.ResourceType.cpu, resourceLimitHostTags, Long.valueOf(serviceOffering.getCpu()), reservationDao, resourceLimitService); - CheckedReservation memReservation = new CheckedReservation(owner, Resource.ResourceType.memory, resourceLimitHostTags, Long.valueOf(serviceOffering.getRamSize()), reservationDao, resourceLimitService)) { + try { if (ImportSource.EXTERNAL == importSource) { String username = cmd.getUsername(); diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index 9655b797ae96..874bce0f95ef 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -260,7 +260,6 @@ public class UnmanagedVMsManagerImplTest { ImportVMTaskVO importVMTaskVO; @Mock private VMInstanceDetailsDao vmInstanceDetailsDao; - @Mock private ConfigKey configKeyMockParamsAllowed; @Mock From 0b169920f34c815fc169d5a105f330949c8e5259 Mon Sep 17 00:00:00 2001 From: dahn Date: Mon, 27 Apr 2026 09:13:58 +0100 Subject: [PATCH 030/205] make dh group 31 default, support 22-24+31 (#12764) --- .../views/network/CreateVpnCustomerGateway.vue | 16 ++++++++++------ .../main/java/com/cloud/utils/net/NetUtils.java | 2 +- .../java/com/cloud/utils/net/NetUtilsTest.java | 4 ++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/ui/src/views/network/CreateVpnCustomerGateway.vue b/ui/src/views/network/CreateVpnCustomerGateway.vue index f71fc4709e8d..bacc04cae311 100644 --- a/ui/src/views/network/CreateVpnCustomerGateway.vue +++ b/ui/src/views/network/CreateVpnCustomerGateway.vue @@ -258,9 +258,13 @@ export default { 'Group 15': 'modp3072', 'Group 16': 'modp4096', 'Group 17': 'modp6144', - 'Group 18': 'modp8192' + 'Group 18': 'modp8192', + 'Group 22': 'modp1024s160', + 'Group 23': 'modp2048s224', + 'Group 24': 'modp2048s256', + 'Group 31': 'curve25519' }, - ikeDhGroupInitialValue: 'Group 5(modp1536)', + ikeDhGroupInitialValue: 'Group 31(curve25519)', isSubmitted: false, ikeversion: 'ike' } @@ -275,12 +279,12 @@ export default { initForm () { this.formRef = ref() this.form = reactive({ - ikeEncryption: 'aes128', + ikeEncryption: 'aes256', ikeHash: 'sha1', ikeversion: 'ike', - ikeDh: 'Group 5(modp1536)', - espEncryption: 'aes128', - espHash: 'sha1', + ikeDh: 'Group 31(curve 25519)', + espEncryption: 'aes256', + espHash: 'sha256', perfectForwardSecrecy: 'None', ikelifetime: '86400', esplifetime: '3600', diff --git a/utils/src/main/java/com/cloud/utils/net/NetUtils.java b/utils/src/main/java/com/cloud/utils/net/NetUtils.java index 65878e055e73..d89d9fa2d93c 100644 --- a/utils/src/main/java/com/cloud/utils/net/NetUtils.java +++ b/utils/src/main/java/com/cloud/utils/net/NetUtils.java @@ -1265,7 +1265,7 @@ public static boolean isValidS2SVpnPolicy(final String policyType, final String if (group == null && policyType.toLowerCase().matches("ike")) { return false; // StrongSwan requires a DH group for the IKE policy } - if (group != null && !group.matches("modp1024|modp1536|modp2048|modp3072|modp4096|modp6144|modp8192")) { + if (group != null && !group.matches("modp1024|modp1536|modp2048|modp3072|modp4096|modp6144|modp8192|modp1024s160|modp2048s224|modp2048s256|curve25519")) { return false; } } diff --git a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java index 4495a123b07e..5c9d41f90a25 100644 --- a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java +++ b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java @@ -131,6 +131,10 @@ public void testIsValidS2SVpnPolicy() { assertTrue(NetUtils.isValidS2SVpnPolicy("ike", "3des-md5;modp1024")); assertTrue(NetUtils.isValidS2SVpnPolicy("ike", "3des-sha1;modp3072,aes128-sha1;modp1536")); assertTrue(NetUtils.isValidS2SVpnPolicy("ike", "3des-sha256;modp3072,aes128-sha512;modp1536")); + assertTrue(NetUtils.isValidS2SVpnPolicy("ike", "aes256-sha256;modp1024s160")); + assertTrue(NetUtils.isValidS2SVpnPolicy("ike", "aes256-sha256;modp2048s224")); + assertTrue(NetUtils.isValidS2SVpnPolicy("ike", "aes256-sha256;modp2048s256")); + assertTrue(NetUtils.isValidS2SVpnPolicy("ike", "aes256-sha256;curve25519")); assertFalse(NetUtils.isValidS2SVpnPolicy("ike", "aes128-sha1")); assertFalse(NetUtils.isValidS2SVpnPolicy("ike", "3des-sha1")); assertFalse(NetUtils.isValidS2SVpnPolicy("ike", "3des-sha1,aes256-sha1")); From ffebe8eaa64607e4294ee2d072e53b0347de5e9e Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 27 Apr 2026 15:38:52 +0530 Subject: [PATCH 031/205] Fix bulk power state query missing VM lifecycle state field (#13027) * Fix bulk power state query missing VM lifecycle state field The IdsPowerStateSelectSearch partial select did not include the VM lifecycle state, causing isPowerStateInSyncWithInstanceState to always return true when state was null. This prevented retry of failed StopCommands on subsequent ping cycles. * Add defensive check for instance host ID to prevent NPE Co-authored-by: Sachin R Doddaguni Co-authored-by: nvazquez --- .../main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index a38b6af3aa0b..d8c9b9253c89 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -358,7 +358,8 @@ protected void init() { IdsPowerStateSelectSearch.entity().getPowerHostId(), IdsPowerStateSelectSearch.entity().getPowerState(), IdsPowerStateSelectSearch.entity().getPowerStateUpdateCount(), - IdsPowerStateSelectSearch.entity().getPowerStateUpdateTime()); + IdsPowerStateSelectSearch.entity().getPowerStateUpdateTime(), + IdsPowerStateSelectSearch.entity().getState()); IdsPowerStateSelectSearch.done(); CountByOfferingId = createSearchBuilder(Integer.class); @@ -1105,10 +1106,14 @@ public Map updatePowerState( private boolean isPowerStateInSyncWithInstanceState(final VirtualMachine.PowerState powerState, final long powerHostId, final VMInstanceVO instance) { State instanceState = instance.getState(); + if (instanceState == null) { + logger.warn("VM {} has null instance state during power state sync check, treating as out of sync", instance); + return false; + } if ((powerState == VirtualMachine.PowerState.PowerOff && instanceState == State.Running) || (powerState == VirtualMachine.PowerState.PowerOn && instanceState == State.Stopped)) { HostVO instanceHost = hostDao.findById(instance.getHostId()); - HostVO powerHost = powerHostId == instance.getHostId() ? instanceHost : hostDao.findById(powerHostId); + HostVO powerHost = instance.getHostId() != null && powerHostId == instance.getHostId() ? instanceHost : hostDao.findById(powerHostId); logger.debug("VM: {} on host: {} and power host : {} is in {} state, but power state is {}", instance, instanceHost, powerHost, instanceState, powerState); return false; From 6f4445c5c1b510a47b12dec1b61dc41dde02979b Mon Sep 17 00:00:00 2001 From: Henrique Sato Date: Mon, 27 Apr 2026 09:36:37 -0300 Subject: [PATCH 032/205] Add offering preset variables for `Network` and `VPC` Quota tariffs (#11810) * Add offering preset variable to Network and VPC tariffs * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Fabricio Duarte * Add tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Fabricio Duarte --- ...s-between-management-and-usage-context.xml | 2 + ...spring-engine-schema-core-daos-context.xml | 2 - .../presetvariables/PresetVariableHelper.java | 32 ++++- .../activationrule/presetvariables/Value.java | 22 +++ .../PresetVariableHelperTest.java | 127 ++++++++++++++++++ 5 files changed, 182 insertions(+), 3 deletions(-) diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml index 1846c3c62a0e..2c6869bd81e3 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml @@ -73,5 +73,7 @@ + + diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index edc14d9fa0cc..ad3722577c27 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -236,13 +236,11 @@ - - diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java index a2fca7b80fd5..cd87052e878c 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java @@ -28,11 +28,12 @@ import com.cloud.dc.ClusterDetailsDao; import com.cloud.dc.ClusterDetailsVO; import com.cloud.host.HostTagVO; +import com.cloud.hypervisor.Hypervisor; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.vpc.VpcOfferingVO; import com.cloud.network.vpc.VpcVO; import javax.inject.Inject; -import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.StoragePoolTagVO; import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.acl.dao.RoleDao; @@ -66,6 +67,7 @@ import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostTagsDao; +import com.cloud.network.vpc.dao.VpcOfferingDao; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.server.ResourceTag; @@ -191,6 +193,9 @@ public class PresetVariableHelper { @Inject ClusterDetailsDao clusterDetailsDao; + @Inject + VpcOfferingDao vpcOfferingDao; + protected boolean backupSnapshotAfterTakingSnapshot = SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value(); private List runningAndAllocatedVmUsageTypes = Arrays.asList(UsageTypes.RUNNING_VM, UsageTypes.ALLOCATED_VM); @@ -778,6 +783,19 @@ protected void loadPresetVariableValueForNetwork(UsageVO usageRecord, Value valu value.setId(network.getUuid()); value.setName(network.getName()); value.setState(usageRecord.getState()); + + value.setNetworkOffering(getPresetVariableValueNetworkOffering(network.getNetworkOfferingId())); + } + + protected GenericPresetVariable getPresetVariableValueNetworkOffering(Long networkOfferingId) { + NetworkOfferingVO networkOfferingVo = networkOfferingDao.findByIdIncludingRemoved(networkOfferingId); + validateIfObjectIsNull(networkOfferingVo, networkOfferingId, "network offering"); + + GenericPresetVariable networkOffering = new GenericPresetVariable(); + networkOffering.setId(networkOfferingVo.getUuid()); + networkOffering.setName(networkOfferingVo.getName()); + + return networkOffering; } protected void loadPresetVariableValueForVpc(UsageVO usageRecord, Value value) { @@ -793,6 +811,18 @@ protected void loadPresetVariableValueForVpc(UsageVO usageRecord, Value value) { value.setId(vpc.getUuid()); value.setName(vpc.getName()); + value.setVpcOffering(getPresetVariableValueVpcOffering(vpc.getVpcOfferingId())); + } + + protected GenericPresetVariable getPresetVariableValueVpcOffering(Long vpcOfferingId) { + VpcOfferingVO vpcOfferingVo = vpcOfferingDao.findByIdIncludingRemoved(vpcOfferingId); + validateIfObjectIsNull(vpcOfferingVo, vpcOfferingId, "vpc offering"); + + GenericPresetVariable vpcOffering = new GenericPresetVariable(); + vpcOffering.setId(vpcOfferingVo.getUuid()); + vpcOffering.setName(vpcOfferingVo.getName()); + + return vpcOffering; } /** diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java index ac776d13c578..aff2c040e742 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java @@ -96,6 +96,12 @@ public class Value extends GenericPresetVariable { private String state; + @PresetVariableDefinition(description = "Network offering of the network.", supportedTypes = {QuotaTypes.NETWORK}) + private GenericPresetVariable networkOffering; + + @PresetVariableDefinition(description = "VPC offering of the VPC.", supportedTypes = {QuotaTypes.VPC}) + private GenericPresetVariable vpcOffering; + public Host getHost() { return host; } @@ -255,4 +261,20 @@ public String getState() { public void setState(String state) { this.state = state; } + + public GenericPresetVariable getNetworkOffering() { + return networkOffering; + } + + public void setNetworkOffering(GenericPresetVariable networkOffering) { + this.networkOffering = networkOffering; + } + + public GenericPresetVariable getVpcOffering() { + return vpcOffering; + } + + public void setVpcOffering(GenericPresetVariable vpcOffering) { + this.vpcOffering = vpcOffering; + } } diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java index 85397503587e..fb093a456790 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java @@ -31,6 +31,10 @@ import com.cloud.dc.ClusterDetailsVO; import com.cloud.host.HostTagVO; import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.vpc.VpcOfferingVO; +import com.cloud.network.vpc.VpcVO; +import com.cloud.network.vpc.dao.VpcOfferingDao; import com.cloud.storage.StoragePoolTagVO; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.RoleVO; @@ -38,7 +42,9 @@ import org.apache.cloudstack.backup.BackupOfferingVO; import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.quota.constant.QuotaTypes; +import org.apache.cloudstack.quota.dao.NetworkDao; import org.apache.cloudstack.quota.dao.VmTemplateDao; +import org.apache.cloudstack.quota.dao.VpcDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -188,6 +194,15 @@ public class PresetVariableHelperTest { @Mock BackupOfferingDao backupOfferingDaoMock; + @Mock + NetworkDao networkDaoMock; + + @Mock + VpcDao vpcDaoMock; + + @Mock + VpcOfferingDao vpcOfferingDaoMock; + List runningAndAllocatedVmUsageTypes = Arrays.asList(UsageTypes.RUNNING_VM, UsageTypes.ALLOCATED_VM); List templateAndIsoUsageTypes = Arrays.asList(UsageTypes.TEMPLATE, UsageTypes.ISO); @@ -223,6 +238,8 @@ private Value getValueForTests() { value.setVmSnapshotType(VMSnapshot.Type.Disk.toString()); value.setComputingResources(getComputingResourcesForTests()); value.setVolumeType(Volume.Type.DATADISK.toString()); + value.setNetworkOffering(getNetworkOfferingForTests()); + value.setVpcOffering(getVpcOfferingForTests()); return value; } @@ -324,6 +341,20 @@ private DiskOfferingPresetVariables getDiskOfferingForTests() { return diskOffering; } + private GenericPresetVariable getNetworkOfferingForTests() { + GenericPresetVariable networkOffering = new GenericPresetVariable(); + networkOffering.setId("network_offering_id"); + networkOffering.setName("network_offering_name"); + return networkOffering; + } + + private GenericPresetVariable getVpcOfferingForTests() { + GenericPresetVariable vpcOffering = new GenericPresetVariable(); + vpcOffering.setId("vpc_offering_id"); + vpcOffering.setName("vpc_offering_name"); + return vpcOffering; + } + private void mockMethodValidateIfObjectIsNull() { Mockito.doNothing().when(presetVariableHelperSpy).validateIfObjectIsNull(Mockito.any(), Mockito.anyLong(), Mockito.anyString()); } @@ -1289,4 +1320,100 @@ public void testGetSnapshotImageStoreRefNotNull() { Mockito.when(imageStoreDaoMock.findById(1L)).thenReturn(store); Assert.assertNotNull(presetVariableHelperSpy.getSnapshotImageStoreRef(1L, 1L)); } + + @Test + public void loadPresetVariableValueForNetworkTestRecordIsNotANetworkDoNothing() { + getQuotaTypesForTests(UsageTypes.NETWORK).forEach(type -> { + Mockito.doReturn(type.getKey()).when(usageVoMock).getUsageType(); + presetVariableHelperSpy.loadPresetVariableValueForNetwork(usageVoMock, null); + }); + + Mockito.verifyNoInteractions(networkDaoMock); + } + + @Test + public void loadPresetVariableValueForNetworkTestRecordIsNetworkSetFields() { + Value expected = getValueForTests(); + + NetworkVO networkVoMock = Mockito.mock(NetworkVO.class); + Mockito.doReturn(networkVoMock).when(networkDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + + mockMethodValidateIfObjectIsNull(); + + Mockito.doReturn(expected.getId()).when(networkVoMock).getUuid(); + Mockito.doReturn(expected.getName()).when(networkVoMock).getName(); + Mockito.doReturn(expected.getState()).when(usageVoMock).getState(); + Mockito.doReturn(expected.getNetworkOffering()).when(presetVariableHelperSpy).getPresetVariableValueNetworkOffering(Mockito.anyLong()); + + Mockito.doReturn(UsageTypes.NETWORK).when(usageVoMock).getUsageType(); + + Value result = new Value(); + presetVariableHelperSpy.loadPresetVariableValueForNetwork(usageVoMock, result); + + assertPresetVariableIdAndName(expected, result); + Assert.assertEquals(expected.getState(), result.getState()); + Assert.assertEquals(expected.getNetworkOffering(), result.getNetworkOffering()); + } + + @Test + public void loadPresetVariableValueForVpcTestRecordIsNotAVpcDoNothing() { + getQuotaTypesForTests(UsageTypes.VPC).forEach(type -> { + Mockito.doReturn(type.getKey()).when(usageVoMock).getUsageType(); + presetVariableHelperSpy.loadPresetVariableValueForVpc(usageVoMock, null); + }); + + Mockito.verifyNoInteractions(networkDaoMock); + } + + @Test + public void loadPresetVariableValueForVpcTestRecordIsVpcSetFields() { + Value expected = getValueForTests(); + + VpcVO networkVoMock = Mockito.mock(VpcVO.class); + Mockito.doReturn(networkVoMock).when(vpcDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + + mockMethodValidateIfObjectIsNull(); + + Mockito.doReturn(expected.getId()).when(networkVoMock).getUuid(); + Mockito.doReturn(expected.getName()).when(networkVoMock).getName(); + Mockito.doReturn(expected.getVpcOffering()).when(presetVariableHelperSpy).getPresetVariableValueVpcOffering(Mockito.anyLong()); + + Mockito.doReturn(UsageTypes.VPC).when(usageVoMock).getUsageType(); + + Value result = new Value(); + presetVariableHelperSpy.loadPresetVariableValueForVpc(usageVoMock, result); + + assertPresetVariableIdAndName(expected, result); + Assert.assertEquals(expected.getVpcOffering(), result.getVpcOffering()); + } + + @Test + public void getPresetVariableValueNetworkOfferingTestSetValuesAndReturnObject() { + NetworkOfferingVO networkOfferingVoMock = Mockito.mock(NetworkOfferingVO.class); + Mockito.doReturn(networkOfferingVoMock).when(networkOfferingDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + mockMethodValidateIfObjectIsNull(); + + GenericPresetVariable expected = getGenericPresetVariableForTests(); + Mockito.doReturn(expected.getId()).when(networkOfferingVoMock).getUuid(); + Mockito.doReturn(expected.getName()).when(networkOfferingVoMock).getName(); + + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueNetworkOffering(1L); + + assertPresetVariableIdAndName(expected, result); + } + + @Test + public void getPresetVariableValueVpcOfferingTestSetValuesAndReturnObject() { + VpcOfferingVO vpcOfferingVoMock = Mockito.mock(VpcOfferingVO.class); + Mockito.doReturn(vpcOfferingVoMock).when(vpcOfferingDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + mockMethodValidateIfObjectIsNull(); + + GenericPresetVariable expected = getGenericPresetVariableForTests(); + Mockito.doReturn(expected.getId()).when(vpcOfferingVoMock).getUuid(); + Mockito.doReturn(expected.getName()).when(vpcOfferingVoMock).getName(); + + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueVpcOffering(1L); + + assertPresetVariableIdAndName(expected, result); + } } From e2c13da419705bfd61b031142cbd46b69c5ccff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= <89930804+erikbocks@users.noreply.github.com> Date: Tue, 28 Apr 2026 04:13:05 -0300 Subject: [PATCH 033/205] Remove UUID parse from param processing workflow (#13065) --- api/src/main/java/org/apache/cloudstack/api/BaseCmd.java | 9 +-------- .../java/org/apache/cloudstack/context/CallContext.java | 6 +++--- .../network/CreateIpv4SubnetForGuestNetworkCmdTest.java | 2 +- .../admin/network/CreateIpv4SubnetForZoneCmdTest.java | 2 +- .../admin/network/DedicateIpv4SubnetForZoneCmdTest.java | 2 +- .../network/DeleteIpv4SubnetForGuestNetworkCmdTest.java | 2 +- .../admin/network/DeleteIpv4SubnetForZoneCmdTest.java | 2 +- .../ReleaseDedicatedIpv4SubnetForZoneCmdTest.java | 2 +- .../admin/network/UpdateIpv4SubnetForZoneCmdTest.java | 2 +- .../network/bgp/ChangeBgpPeersForNetworkCmdTest.java | 2 +- .../admin/network/bgp/ChangeBgpPeersForVpcCmdTest.java | 2 +- .../command/admin/network/bgp/CreateBgpPeerCmdTest.java | 2 +- .../admin/network/bgp/DedicateBgpPeerCmdTest.java | 2 +- .../command/admin/network/bgp/DeleteBgpPeerCmdTest.java | 2 +- .../network/bgp/ReleaseDedicatedBgpPeerCmdTest.java | 2 +- .../command/admin/network/bgp/UpdateBgpPeerCmdTest.java | 2 +- .../admin/storage/DownloadImageStoreObjectCmdTest.java | 2 +- .../api/command/admin/volume/UnmanageVolumeCmdTest.java | 2 +- .../api/command/test/CreateSnapshotCmdTest.java | 2 +- .../api/command/test/UpdateConditionCmdTest.java | 2 +- .../routing/DeleteRoutingFirewallRuleCmdTest.java | 2 +- .../java/com/cloud/api/dispatch/ParamProcessWorker.java | 5 ++--- 22 files changed, 25 insertions(+), 33 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java index e495cf284130..00b1bc310d5a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -27,7 +27,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.regex.Pattern; import javax.inject.Inject; @@ -504,12 +503,6 @@ public Map convertExternalDetailsToMap(Map externalDetails) { } public String getResourceUuid(String parameterName) { - UUID resourceUuid = CallContext.current().getApiResourceUuid(parameterName); - - if (resourceUuid != null) { - return resourceUuid.toString(); - } - - return null; + return CallContext.current().getApiResourceUuid(parameterName); } } diff --git a/api/src/main/java/org/apache/cloudstack/context/CallContext.java b/api/src/main/java/org/apache/cloudstack/context/CallContext.java index 5e0c60184f4f..fcfb5b6b1e00 100644 --- a/api/src/main/java/org/apache/cloudstack/context/CallContext.java +++ b/api/src/main/java/org/apache/cloudstack/context/CallContext.java @@ -63,7 +63,7 @@ protected Stack initialValue() { private User user; private long userId; private final Map context = new HashMap(); - private final Map apiResourcesUuids = new HashMap<>(); + private final Map apiResourcesUuids = new HashMap<>(); private Project project; private String apiName; @@ -389,11 +389,11 @@ public void setEventDisplayEnabled(boolean eventDisplayEnabled) { isEventDisplayEnabled = eventDisplayEnabled; } - public UUID getApiResourceUuid(String paramName) { + public String getApiResourceUuid(String paramName) { return apiResourcesUuids.get(paramName); } - public void putApiResourceUuid(String paramName, UUID uuid) { + public void putApiResourceUuid(String paramName, String uuid) { apiResourcesUuids.put(paramName, uuid); } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmdTest.java index 4039ca6dc948..38d0df2e8b82 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmdTest.java @@ -40,7 +40,7 @@ public class CreateIpv4SubnetForGuestNetworkCmdTest { @Test public void testCreateIpv4SubnetForGuestNetworkCmd() { Long parentId = 1L; - UUID parentUuid = UUID.randomUUID(); + String parentUuid = UUID.randomUUID().toString(); String subnet = "192.168.1.0/24"; Integer cidrSize = 26; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmdTest.java index bb324aca0e70..560ecdc3b296 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmdTest.java @@ -40,7 +40,7 @@ public class CreateIpv4SubnetForZoneCmdTest { @Test public void testCreateIpv4SubnetForZoneCmd() { Long zoneId = 1L; - UUID zoneUuid = UUID.randomUUID(); + String zoneUuid = UUID.randomUUID().toString(); String subnet = "192.168.1.0/24"; String accountName = "user"; Long projectId = 10L; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmdTest.java index 31458d2833fa..4640510ccdab 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmdTest.java @@ -39,7 +39,7 @@ public class DedicateIpv4SubnetForZoneCmdTest { @Test public void testDedicateIpv4SubnetForZoneCmd() { Long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); String accountName = "user"; Long projectId = 10L; Long domainId = 11L; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmdTest.java index 48aceeaaeec1..cd25d8d24014 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmdTest.java @@ -39,7 +39,7 @@ public class DeleteIpv4SubnetForGuestNetworkCmdTest { @Test public void testDeleteIpv4SubnetForGuestNetworkCmd() { Long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); DeleteIpv4SubnetForGuestNetworkCmd cmd = new DeleteIpv4SubnetForGuestNetworkCmd(); ReflectionTestUtils.setField(cmd, "id", id); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmdTest.java index 5c3593a8f1b6..269fb3f3c192 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmdTest.java @@ -39,7 +39,7 @@ public class DeleteIpv4SubnetForZoneCmdTest { @Test public void testDeleteIpv4SubnetForZoneCmd() { Long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); DeleteIpv4SubnetForZoneCmd cmd = new DeleteIpv4SubnetForZoneCmd(); ReflectionTestUtils.setField(cmd, "id", id); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmdTest.java index 29d6d8e735bb..6e5d2a95f3ab 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmdTest.java @@ -39,7 +39,7 @@ public class ReleaseDedicatedIpv4SubnetForZoneCmdTest { @Test public void testReleaseDedicatedIpv4SubnetForZoneCmd() { Long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); ReleaseDedicatedIpv4SubnetForZoneCmd cmd = new ReleaseDedicatedIpv4SubnetForZoneCmd(); ReflectionTestUtils.setField(cmd, "id", id); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmdTest.java index 399b77de6e85..af37006eafd2 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmdTest.java @@ -40,7 +40,7 @@ public class UpdateIpv4SubnetForZoneCmdTest { @Test public void testUpdateIpv4SubnetForZoneCmd() { Long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); String subnet = "192.168.1.0/24"; UpdateIpv4SubnetForZoneCmd cmd = new UpdateIpv4SubnetForZoneCmd(); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmdTest.java index 9cd403ddd1de..3db1fab466f6 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmdTest.java @@ -46,7 +46,7 @@ public class ChangeBgpPeersForNetworkCmdTest { @Test public void testChangeBgpPeersForNetworkCmd() { Long networkId = 10L; - UUID networkUuid = UUID.randomUUID(); + String networkUuid = UUID.randomUUID().toString(); List bgpPeerIds = Arrays.asList(20L, 21L); ChangeBgpPeersForNetworkCmd cmd = new ChangeBgpPeersForNetworkCmd(); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmdTest.java index 545523e3ab97..fb85f7060684 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmdTest.java @@ -46,7 +46,7 @@ public class ChangeBgpPeersForVpcCmdTest { @Test public void testChangeBgpPeersForVpcCmd() { Long VpcId = 10L; - UUID vpcUuid = UUID.randomUUID(); + String vpcUuid = UUID.randomUUID().toString(); List bgpPeerIds = Arrays.asList(20L, 21L); ChangeBgpPeersForVpcCmd cmd = new ChangeBgpPeersForVpcCmd(); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmdTest.java index 866824f62936..3abc5f57d017 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmdTest.java @@ -40,7 +40,7 @@ public class CreateBgpPeerCmdTest { @Test public void testCreateBgpPeerCmd() { Long zoneId = 1L; - UUID zoneUuid = UUID.randomUUID(); + String zoneUuid = UUID.randomUUID().toString(); String accountName = "user"; Long projectId = 10L; Long domainId = 11L; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmdTest.java index a8046d3d745c..c5edb1b8f53f 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmdTest.java @@ -39,7 +39,7 @@ public class DedicateBgpPeerCmdTest { @Test public void testDedicateBgpPeerCmd() { Long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); String accountName = "user"; Long projectId = 10L; Long domainId = 11L; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmdTest.java index d54be9e859e4..5228a63dc92d 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmdTest.java @@ -39,7 +39,7 @@ public class DeleteBgpPeerCmdTest { @Test public void testDeleteBgpPeerCmd() { Long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); DeleteBgpPeerCmd cmd = new DeleteBgpPeerCmd(); ReflectionTestUtils.setField(cmd, "id", id); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmdTest.java index 1cf8ead706d1..60a814d63050 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmdTest.java @@ -39,7 +39,7 @@ public class ReleaseDedicatedBgpPeerCmdTest { @Test public void testReleaseDedicatedBgpPeerCmd() { Long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); ReleaseDedicatedBgpPeerCmd cmd = new ReleaseDedicatedBgpPeerCmd(); ReflectionTestUtils.setField(cmd, "id", id); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmdTest.java index 1601fcb4c5a5..d594bc5718b7 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmdTest.java @@ -40,7 +40,7 @@ public class UpdateBgpPeerCmdTest { @Test public void testUpdateBgpPeerCmd() { Long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); String ip4Address = "ip4-address"; String ip6Address = "ip6-address"; Long peerAsNumber = 15000L; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmdTest.java index 3b2b49cae4e1..8d2771c969b7 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmdTest.java @@ -97,7 +97,7 @@ public void testGetEventType() { @Test public void testGetEventDescription() { - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); ReflectionTestUtils.setField(cmd, "storeId", 1L); ReflectionTestUtils.setField(cmd, "path", "path/to/object"); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmdTest.java index ecca507a6b95..59a61806e867 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmdTest.java @@ -44,7 +44,7 @@ public class UnmanageVolumeCmdTest { public void testUnmanageVolumeCmd() { long accountId = 2L; Long volumeId = 3L; - UUID volumeUuid = UUID.randomUUID(); + String volumeUuid = UUID.randomUUID().toString(); Volume volume = Mockito.mock(Volume.class); Mockito.when(responseGenerator.findVolumeById(volumeId)).thenReturn(volume); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java index b70efaf9a6c5..032dca8d8007 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java @@ -118,7 +118,7 @@ public void testCreateFailure() { AccountService accountService = Mockito.mock(AccountService.class); Account account = Mockito.mock(Account.class); Mockito.when(accountService.getAccount(anyLong())).thenReturn(account); - UUID volumeUuid = UUID.randomUUID(); + String volumeUuid = UUID.randomUUID().toString(); CallContext.current().putApiResourceUuid("volumeid", volumeUuid); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateConditionCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateConditionCmdTest.java index 3dfb29dadd3e..c78dbe9b56bc 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateConditionCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateConditionCmdTest.java @@ -56,7 +56,7 @@ public class UpdateConditionCmdTest { private static final Long threshold = 100L; private static final long accountId = 5L; - private static final UUID conditionUuid = UUID.randomUUID(); + private static final String conditionUuid = UUID.randomUUID().toString(); @Before public void setUp() { diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmdTest.java index d3cf5dd6cd68..dbe7669431d8 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmdTest.java @@ -49,7 +49,7 @@ public void testProperties() { ReflectionTestUtils.setField(cmd, "_firewallService", _firewallService); long id = 1L; - UUID uuid = UUID.randomUUID(); + String uuid = UUID.randomUUID().toString(); long accountId = 2L; long networkId = 3L; diff --git a/server/src/main/java/com/cloud/api/dispatch/ParamProcessWorker.java b/server/src/main/java/com/cloud/api/dispatch/ParamProcessWorker.java index 0205bfa9d8fd..2d17fd029a14 100644 --- a/server/src/main/java/com/cloud/api/dispatch/ParamProcessWorker.java +++ b/server/src/main/java/com/cloud/api/dispatch/ParamProcessWorker.java @@ -28,7 +28,6 @@ import java.util.List; import java.util.Map; import java.util.StringTokenizer; -import java.util.UUID; import java.util.regex.Matcher; import javax.inject.Inject; @@ -526,7 +525,7 @@ private Long translateUuidToInternalId(final String uuid, final Parameter annota continue; } String entityUuid = ((Identity) objVO).getUuid(); - CallContext.current().putApiResourceUuid(annotation.name(), UUID.fromString(entityUuid)); + CallContext.current().putApiResourceUuid(annotation.name(), entityUuid); } validateNaturalNumber(internalId, annotation.name()); return internalId; @@ -551,7 +550,7 @@ private Long translateUuidToInternalId(final String uuid, final Parameter annota } // Return on first non-null Id for the uuid entity if (internalId != null){ - CallContext.current().putApiResourceUuid(annotation.name(), UUID.fromString(uuid)); + CallContext.current().putApiResourceUuid(annotation.name(), uuid); CallContext.current().putContextParameter(entity, uuid); break; } From 089eb36e471c263ff1a90376cadaba7a595fc9e8 Mon Sep 17 00:00:00 2001 From: Sergiy Kukunin Date: Tue, 28 Apr 2026 22:53:08 -0700 Subject: [PATCH 034/205] Linstor: fix create volume from snapshot on primary storage (#13043) * Linstor: fix create volume from snapshot on primary storage When creating a volume from a snapshot on Linstor primary storage (with lin.backup.snapshots=false), the operation fails with: "Only the following image types are currently supported: VHD, OVA, QCOW2, RAW (for PowerFlex and FiberChannel)" Root cause: the Linstor driver does not handle SNAPSHOT -> VOLUME in its canCopy()/copyAsync() methods. This causes DataMotionServiceImpl to fall through to StorageSystemDataMotionStrategy (selected because Linstor advertises STORAGE_SYSTEM_SNAPSHOT=true). That strategy's verifyFormatWithPoolType() rejects RAW format for Linstor pools, since RAW is only allowed for PowerFlex and FiberChannel. Additionally, VolumeOrchestrator.createVolumeFromSnapshot() attempts to back up the snapshot to secondary storage when the storage plugin does not advertise CAN_CREATE_TEMPLATE_FROM_SNAPSHOT. This backup fails because the snapshot only exists on Linstor primary storage. Fix: - Add CAN_CREATE_TEMPLATE_FROM_SNAPSHOT capability so the orchestrator skips the backup-to-secondary path - Add canCopySnapshotToVolumeCond() to match SNAPSHOT -> VOLUME when both are on the same Linstor primary store - Wire it into canCopy() to intercept at DataMotionServiceImpl before strategy selection, bypassing StorageSystemDataMotionStrategy - Implement copySnapshotToVolume() which delegates to the existing createResourceFromSnapshot() for native Linstor snapshot restore This follows the same pattern used by the StorPool plugin, which handles SNAPSHOT -> VOLUME directly in its driver rather than going through StorageSystemDataMotionStrategy. Tested on CloudStack 4.22 with Linstor LVM_THIN storage, creating a volume from a 1TB CNPG Postgres database snapshot. Volume creates successfully with correct path and deletes cleanly. * Let CloudRuntimeException propagate from copySnapshotToVolume Remove try/catch in copySnapshotToVolume so that CloudRuntimeException from createResourceFromSnapshot propagates to the caller, ensuring CloudStack properly notices and reports the failure. * Fix CAN_CREATE_TEMPLATE_FROM_SNAPSHOT breaking template creation Setting CAN_CREATE_TEMPLATE_FROM_SNAPSHOT unconditionally to true caused createTemplate from snapshot to take the StorPool-specific code path in TemplateManagerImpl, which sends a CopyCommand to a system VM that Linstor cannot handle. Fix: make CAN_CREATE_TEMPLATE_FROM_SNAPSHOT conditional on the same flag as STORAGE_SYSTEM_SNAPSHOT (!BackupSnapshots). When snapshots are backed up to secondary (the default), the old template creation flow works. When snapshots stay on primary, the direct path is used. Also fix checkstyle: remove unused DataObject import in test. Co-authored-by: Claude Opus 4.6 (1M context) --- .../LinstorPrimaryDataStoreDriverImpl.java | 30 ++++- ...LinstorPrimaryDataStoreDriverImplTest.java | 109 ++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java index 3f06bee8ac83..83dacf74e8df 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java @@ -153,6 +153,8 @@ public Map getCapabilities() // CAN_CREATE_VOLUME_FROM_SNAPSHOT see note from CAN_CREATE_VOLUME_FROM_VOLUME mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString()); mapCapabilities.put(DataStoreCapabilities.CAN_REVERT_VOLUME_TO_SNAPSHOT.toString(), Boolean.TRUE.toString()); + mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_TEMPLATE_FROM_SNAPSHOT.toString(), + Boolean.toString(system_snapshot)); return mapCapabilities; } @@ -720,6 +722,13 @@ public void revertSnapshot( } } + private static boolean canCopySnapshotToVolumeCond(DataObject srcData, DataObject dstData) { + return srcData.getType() == DataObjectType.SNAPSHOT && dstData.getType() == DataObjectType.VOLUME + && srcData.getDataStore().getRole() == DataStoreRole.Primary + && dstData.getDataStore().getRole() == DataStoreRole.Primary + && srcData.getDataStore().getId() == dstData.getDataStore().getId(); + } + private static boolean canCopySnapshotCond(DataObject srcData, DataObject dstData) { return srcData.getType() == DataObjectType.SNAPSHOT && dstData.getType() == DataObjectType.SNAPSHOT && (dstData.getDataStore().getRole() == DataStoreRole.Image @@ -747,7 +756,10 @@ public boolean canCopy(DataObject srcData, DataObject dstData) { logger.debug("LinstorPrimaryDataStoreDriverImpl.canCopy: " + srcData.getType() + " -> " + dstData.getType()); - if (canCopySnapshotCond(srcData, dstData)) { + if (canCopySnapshotToVolumeCond(srcData, dstData)) { + StoragePoolVO storagePool = _storagePoolDao.findById(srcData.getDataStore().getId()); + return storagePool.getStorageProviderName().equals(LinstorUtil.PROVIDER_NAME); + } else if (canCopySnapshotCond(srcData, dstData)) { SnapshotInfo sinfo = (SnapshotInfo) srcData; VolumeInfo volume = sinfo.getBaseVolume(); StoragePoolVO storagePool = _storagePoolDao.findById(volume.getPoolId()); @@ -766,6 +778,18 @@ public boolean canCopy(DataObject srcData, DataObject dstData) return false; } + private CopyCommandResult copySnapshotToVolume(SnapshotInfo snapshotInfo, VolumeInfo volumeInfo) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(snapshotInfo.getDataStore().getId()); + String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getUuid(); + createResourceFromSnapshot(snapshotInfo.getId(), rscName, storagePoolVO); + + VolumeObjectTO volumeTO = (VolumeObjectTO) volumeInfo.getTO(); + volumeTO.setPath(volumeInfo.getUuid()); + volumeTO.setSize(volumeInfo.getSize()); + + return new CopyCommandResult(null, new CopyCmdAnswer(volumeTO)); + } + @Override public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCallback callback) { @@ -773,7 +797,9 @@ public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCal + srcData.getType() + " -> " + dstData.getType()); final CopyCommandResult res; - if (canCopySnapshotCond(srcData, dstData)) { + if (canCopySnapshotToVolumeCond(srcData, dstData)) { + res = copySnapshotToVolume((SnapshotInfo) srcData, (VolumeInfo) dstData); + } else if (canCopySnapshotCond(srcData, dstData)) { String errMsg = null; Answer answer = copySnapshot(srcData, dstData); if (answer != null && !answer.getResult()) { diff --git a/plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImplTest.java b/plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImplTest.java index 4653cfa358b0..23266e17f4f0 100644 --- a/plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImplTest.java +++ b/plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImplTest.java @@ -25,13 +25,23 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.storage.DataStoreRole; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.mockito.Mockito.mock; @@ -42,6 +52,9 @@ public class LinstorPrimaryDataStoreDriverImplTest { private DevelopersApi api; + @Mock + private PrimaryDataStoreDao _storagePoolDao; + @InjectMocks private LinstorPrimaryDataStoreDriverImpl linstorPrimaryDataStoreDriver; @@ -85,4 +98,100 @@ public void testGetEncryptedLayerList() throws ApiException { layers = LinstorUtil.getEncryptedLayerList(api, "EncryptedGrp"); Assert.assertEquals(Arrays.asList(LayerType.DRBD, LayerType.LUKS, LayerType.STORAGE), layers); } + + @Test + public void testGetCapabilitiesIncludesCreateTemplateFromSnapshotMatchesSystemSnapshot() { + Map caps = linstorPrimaryDataStoreDriver.getCapabilities(); + + Assert.assertEquals( + "CAN_CREATE_TEMPLATE_FROM_SNAPSHOT should match STORAGE_SYSTEM_SNAPSHOT", + caps.get(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString()), + caps.get(DataStoreCapabilities.CAN_CREATE_TEMPLATE_FROM_SNAPSHOT.toString())); + } + + @Test + public void testCanCopySnapshotToVolumeOnSamePrimary() { + DataStore primaryStore = mock(DataStore.class); + when(primaryStore.getRole()).thenReturn(DataStoreRole.Primary); + when(primaryStore.getId()).thenReturn(1L); + + SnapshotInfo snapshot = mock(SnapshotInfo.class); + when(snapshot.getType()).thenReturn(DataObjectType.SNAPSHOT); + when(snapshot.getDataStore()).thenReturn(primaryStore); + + VolumeInfo volume = mock(VolumeInfo.class); + when(volume.getType()).thenReturn(DataObjectType.VOLUME); + when(volume.getDataStore()).thenReturn(primaryStore); + + StoragePoolVO pool = mock(StoragePoolVO.class); + when(pool.getStorageProviderName()).thenReturn(LinstorUtil.PROVIDER_NAME); + when(_storagePoolDao.findById(1L)).thenReturn(pool); + + Assert.assertTrue("canCopy should return true for SNAPSHOT -> VOLUME on same Linstor primary", + linstorPrimaryDataStoreDriver.canCopy(snapshot, volume)); + } + + @Test + public void testCanCopySnapshotToVolumeRejectsNonLinstor() { + DataStore primaryStore = mock(DataStore.class); + when(primaryStore.getRole()).thenReturn(DataStoreRole.Primary); + when(primaryStore.getId()).thenReturn(1L); + + SnapshotInfo snapshot = mock(SnapshotInfo.class); + when(snapshot.getType()).thenReturn(DataObjectType.SNAPSHOT); + when(snapshot.getDataStore()).thenReturn(primaryStore); + + VolumeInfo volume = mock(VolumeInfo.class); + when(volume.getType()).thenReturn(DataObjectType.VOLUME); + when(volume.getDataStore()).thenReturn(primaryStore); + + StoragePoolVO pool = mock(StoragePoolVO.class); + when(pool.getStorageProviderName()).thenReturn("SomeOtherProvider"); + when(_storagePoolDao.findById(1L)).thenReturn(pool); + + Assert.assertFalse("canCopy should return false for non-Linstor storage", + linstorPrimaryDataStoreDriver.canCopy(snapshot, volume)); + } + + @Test + public void testCanCopySnapshotToVolumeRejectsCrossPrimary() { + DataStore srcStore = mock(DataStore.class); + when(srcStore.getRole()).thenReturn(DataStoreRole.Primary); + when(srcStore.getId()).thenReturn(1L); + + DataStore destStore = mock(DataStore.class); + when(destStore.getRole()).thenReturn(DataStoreRole.Primary); + when(destStore.getId()).thenReturn(2L); + + SnapshotInfo snapshot = mock(SnapshotInfo.class); + when(snapshot.getType()).thenReturn(DataObjectType.SNAPSHOT); + when(snapshot.getDataStore()).thenReturn(srcStore); + + VolumeInfo volume = mock(VolumeInfo.class); + when(volume.getType()).thenReturn(DataObjectType.VOLUME); + when(volume.getDataStore()).thenReturn(destStore); + + Assert.assertFalse("canCopy should return false for SNAPSHOT -> VOLUME across different primary stores", + linstorPrimaryDataStoreDriver.canCopy(snapshot, volume)); + } + + @Test + public void testCanCopySnapshotToVolumeRejectsImageDest() { + DataStore primaryStore = mock(DataStore.class); + when(primaryStore.getRole()).thenReturn(DataStoreRole.Primary); + + DataStore imageStore = mock(DataStore.class); + when(imageStore.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotInfo snapshot = mock(SnapshotInfo.class); + when(snapshot.getType()).thenReturn(DataObjectType.SNAPSHOT); + when(snapshot.getDataStore()).thenReturn(primaryStore); + + VolumeInfo volume = mock(VolumeInfo.class); + when(volume.getType()).thenReturn(DataObjectType.VOLUME); + when(volume.getDataStore()).thenReturn(imageStore); + + Assert.assertFalse("canCopy should return false when destination is Image store", + linstorPrimaryDataStoreDriver.canCopy(snapshot, volume)); + } } From a73cc9a22c09954c1631c36d2a11064764cc8355 Mon Sep 17 00:00:00 2001 From: julien-vaz <54545601+julien-vaz@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:09:13 -0300 Subject: [PATCH 035/205] Improve Quota Statement (#10506) * Improve Quota Statement * Removes unused import * Fix QuotaUsageJoinDao, QuotaResponseBuilderImpl, QuotaServiceImpl e QuotaServiceImplTest * Reorganize imports * Updates QuotaStatementCmd responseBuilder scope to default * Fix log4j syntax * Address reviews + other improvements * Add missing SQL scripts and injections * Change accountid and domainid logic + add unit tests * Rename QuotaUsageDetail to QuotaTariffUsage * Fix out of bounds exception --------- Co-authored-by: Julien Hervot de Mattos Vaz Co-authored-by: Fabricio Duarte --- .../apache/cloudstack/api/ApiConstants.java | 6 + .../META-INF/db/schema-42210to42300.sql | 10 + .../db/views/cloud_usage.quota_usage_view.sql | 35 ++ .../com/cloud/utils/db/SearchCriteria.java | 6 + .../quota/dao/QuotaTariffUsageDao.java | 29 ++ .../quota/dao/QuotaTariffUsageDaoImpl.java | 56 +++ .../quota/dao/QuotaUsageJoinDao.java | 31 ++ .../quota/dao/QuotaUsageJoinDaoImpl.java | 94 +++++ .../quota/vo/QuotaTariffUsageVO.java | 86 +++++ .../cloudstack/quota/vo/QuotaUsageJoinVO.java | 179 ++++++++++ .../quota/vo/QuotaUsageResourceVO.java | 62 ++++ .../quota/spring-framework-quota-context.xml | 4 +- .../api/command/QuotaStatementCmd.java | 74 ++-- .../api/response/QuotaResponseBuilder.java | 5 +- .../response/QuotaResponseBuilderImpl.java | 322 +++++++++++++----- .../QuotaStatementItemResourceResponse.java | 61 ++++ .../response/QuotaStatementItemResponse.java | 69 ++-- .../api/response/QuotaStatementResponse.java | 65 ++-- .../apache/cloudstack/quota/QuotaService.java | 4 +- .../cloudstack/quota/QuotaServiceImpl.java | 30 +- .../api/command/QuotaStatementCmdTest.java | 23 +- .../QuotaResponseBuilderImplTest.java | 199 +++++++++++ .../quota/QuotaServiceImplTest.java | 10 +- 23 files changed, 1212 insertions(+), 248 deletions(-) create mode 100644 engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_usage_view.sql create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDao.java create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDaoImpl.java create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaTariffUsageVO.java create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageJoinVO.java create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageResourceVO.java create mode 100644 plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 7eae16a2a376..6fb7cb0612c0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -157,6 +157,7 @@ public class ApiConstants { public static final String CUSTOM_ID = "customid"; public static final String CUSTOM_ACTION_ID = "customactionid"; public static final String CUSTOM_JOB_ID = "customjobid"; + public static final String CURRENCY = "currency"; public static final String CURRENT_START_IP = "currentstartip"; public static final String CURRENT_END_IP = "currentendip"; public static final String ENCRYPT = "encrypt"; @@ -541,6 +542,7 @@ public class ApiConstants { public static final String SESSIONKEY = "sessionkey"; public static final String SHOW_CAPACITIES = "showcapacities"; public static final String SHOW_REMOVED = "showremoved"; + public static final String SHOW_RESOURCES = "showresources"; public static final String SHOW_RESOURCE_ICON = "showicon"; public static final String SHOW_INACTIVE = "showinactive"; public static final String SHOW_UNIQUE = "showunique"; @@ -606,9 +608,11 @@ public class ApiConstants { public static final String TENANT_NAME = "tenantname"; public static final String TOTAL = "total"; public static final String TOTAL_SUBNETS = "totalsubnets"; + public static final String TOTAL_QUOTA = "totalquota"; public static final String TYPE = "type"; public static final String TRUST_STORE = "truststore"; public static final String TRUST_STORE_PASSWORD = "truststorepass"; + public static final String UNIT = "unit"; public static final String URL = "url"; public static final String USAGE_INTERFACE = "usageinterface"; public static final String USED = "used"; @@ -1300,6 +1304,8 @@ public class ApiConstants { public static final String OBJECT_LOCKING = "objectlocking"; public static final String ENCRYPTION = "encryption"; public static final String QUOTA = "quota"; + public static final String QUOTA_CONSUMED = "quotaconsumed"; + public static final String QUOTA_USAGE = "quotausage"; public static final String ACCESS_KEY = "accesskey"; public static final String SOURCE_NAT_IP = "sourcenatipaddress"; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 4cb9eb7cb2c4..c0feb06e76ac 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -117,3 +117,13 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tin --- Disable/enable NICs CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Indicates whether the NIC is enabled or not'' '); + +--- Quota tariff/usage mapping +CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `tariff_id` bigint(20) unsigned NOT NULL COMMENT 'ID of the tariff of the Quota usage detail calculated, foreign key to quota_tariff table', + `quota_usage_id` bigint(20) unsigned NOT NULL COMMENT 'ID of the aggregation of Quota usage details, foreign key to quota_usage table', + `quota_used` decimal(20,8) NOT NULL COMMENT 'Amount of quota used', + PRIMARY KEY (`id`), + CONSTRAINT `fk_quota_tariff_usage__tariff_id` FOREIGN KEY (`tariff_id`) REFERENCES `cloud_usage`.`quota_tariff` (`id`), + CONSTRAINT `fk_quota_tariff_usage__quota_usage_id` FOREIGN KEY (`quota_usage_id`) REFERENCES `cloud_usage`.`quota_usage` (`id`)); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_usage_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_usage_view.sql new file mode 100644 index 000000000000..7ac001384e8e --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_usage_view.sql @@ -0,0 +1,35 @@ +-- 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. + +-- VIEW `cloud_usage`.`quota_usage_view`; + +DROP VIEW IF EXISTS `cloud_usage`.`quota_usage_view`; +CREATE VIEW `cloud_usage`.`quota_usage_view` AS +SELECT qu.id, + qu.usage_item_id, + qu.zone_id, + qu.account_id, + qu.domain_id, + qu.usage_type, + qu.quota_used, + qu.start_date, + qu.end_date, + cu.usage_id AS resource_id, + cu.network_id as network_id, + cu.offering_id as offering_id +FROM `cloud_usage`.`quota_usage` qu +INNER JOIN `cloud_usage`.`cloud_usage` cu ON (cu.id = qu.usage_item_id); diff --git a/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java b/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java index 15807eb26d42..3323d5c4d829 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java +++ b/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java @@ -205,6 +205,12 @@ public void setJoinParameters(String joinName, String conditionName, Object... p } + public void setJoinParametersIfNotNull(String joinName, String conditionName, Object... params) { + if (ArrayUtils.isNotEmpty(params) && (params.length > 1 || params[0] != null)) { + setJoinParameters(joinName, conditionName, params); + } + } + public SearchCriteria getJoin(String joinName) { return _joins.get(joinName).getT(); } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDao.java new file mode 100644 index 000000000000..9684ca117b56 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDao.java @@ -0,0 +1,29 @@ +//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.quota.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; +import java.util.List; + +public interface QuotaTariffUsageDao extends GenericDao { + + void persistQuotaTariffUsage(QuotaTariffUsageVO quotaTariffUsage); + + List listQuotaTariffUsages(Long quotaUsageId); + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDaoImpl.java new file mode 100644 index 000000000000..556f552fed69 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDaoImpl.java @@ -0,0 +1,56 @@ +//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.quota.dao; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; + +import javax.annotation.PostConstruct; +import java.util.List; + +@Component +public class QuotaTariffUsageDaoImpl extends GenericDaoBase implements QuotaTariffUsageDao { + private SearchBuilder searchQuotaTariffUsages; + + @PostConstruct + public void init() { + searchQuotaTariffUsages = createSearchBuilder(); + searchQuotaTariffUsages.and("quotaUsageId", searchQuotaTariffUsages.entity().getQuotaUsageId(), SearchCriteria.Op.EQ); + searchQuotaTariffUsages.done(); + } + + @Override + public void persistQuotaTariffUsage(final QuotaTariffUsageVO quotaTariffUsage) { + logger.trace("Persisting quota tariff usage [{}].", quotaTariffUsage); + Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback) status -> persist(quotaTariffUsage)); + } + + @Override + public List listQuotaTariffUsages(Long quotaUsageId) { + SearchCriteria sc = searchQuotaTariffUsages.create(); + sc.setParameters("quotaUsageId", quotaUsageId); + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> listBy(sc)); + } + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java new file mode 100644 index 000000000000..126fa11413f7 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java @@ -0,0 +1,31 @@ +// 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.quota.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; + +import java.util.Date; +import java.util.List; + +public interface QuotaUsageJoinDao extends GenericDao { + + List findQuotaUsage(Long accountId, Long domainId, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId); + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java new file mode 100644 index 000000000000..b98ea2b3a5d2 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java @@ -0,0 +1,94 @@ +// 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.quota.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.JoinBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Date; +import java.util.List; + +@Component +public class QuotaUsageJoinDaoImpl extends GenericDaoBase implements QuotaUsageJoinDao { + + private SearchBuilder searchQuotaUsages; + + private SearchBuilder searchQuotaUsagesJoinTariffUsages; + + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; + + @PostConstruct + public void init() { + searchQuotaUsages = createSearchBuilder(); + prepareQuotaUsageSearchBuilder(searchQuotaUsages); + searchQuotaUsages.done(); + + SearchBuilder searchQuotaTariffUsages = quotaTariffUsageDao.createSearchBuilder(); + searchQuotaTariffUsages.and("tariffId", searchQuotaTariffUsages.entity().getTariffId(), SearchCriteria.Op.EQ); + searchQuotaUsagesJoinTariffUsages = createSearchBuilder(); + prepareQuotaUsageSearchBuilder(searchQuotaUsagesJoinTariffUsages); + searchQuotaUsagesJoinTariffUsages.join("searchQuotaTariffUsages", searchQuotaTariffUsages, searchQuotaUsagesJoinTariffUsages.entity().getId(), + searchQuotaTariffUsages.entity().getQuotaUsageId(), JoinBuilder.JoinType.INNER); + searchQuotaUsagesJoinTariffUsages.done(); + } + + private void prepareQuotaUsageSearchBuilder(SearchBuilder searchBuilder) { + searchBuilder.and("accountId", searchBuilder.entity().getAccountId(), SearchCriteria.Op.EQ); + searchBuilder.and("domainId", searchBuilder.entity().getDomainId(), SearchCriteria.Op.EQ); + searchBuilder.and("usageType", searchBuilder.entity().getUsageType(), SearchCriteria.Op.EQ); + searchBuilder.and("resourceId", searchBuilder.entity().getResourceId(), SearchCriteria.Op.EQ); + searchBuilder.and("networkId", searchBuilder.entity().getNetworkId(), SearchCriteria.Op.EQ); + searchBuilder.and("offeringId", searchBuilder.entity().getOfferingId(), SearchCriteria.Op.EQ); + searchBuilder.and("startDate", searchBuilder.entity().getStartDate(), SearchCriteria.Op.BETWEEN); + searchBuilder.and("endDate", searchBuilder.entity().getEndDate(), SearchCriteria.Op.BETWEEN); + } + + @Override + public List findQuotaUsage(Long accountId, Long domainId, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId) { + SearchCriteria sc = tariffId == null ? searchQuotaUsages.create() : searchQuotaUsagesJoinTariffUsages.create(); + + sc.setParametersIfNotNull("accountId", accountId); + sc.setParametersIfNotNull("domainId", domainId); + sc.setParametersIfNotNull("usageType", usageType); + sc.setParametersIfNotNull("resourceId", resourceId); + sc.setParametersIfNotNull("networkId", networkId); + sc.setParametersIfNotNull("offeringId", offeringId); + + if (ObjectUtils.allNotNull(startDate, endDate)) { + sc.setParameters("startDate", startDate, endDate); + sc.setParameters("endDate", startDate, endDate); + } + + sc.setJoinParametersIfNotNull("searchQuotaTariffUsages", "tariffId", tariffId); + + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> listBy(sc)); + } + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaTariffUsageVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaTariffUsageVO.java new file mode 100644 index 000000000000..4fa9e771713e --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaTariffUsageVO.java @@ -0,0 +1,86 @@ +//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.quota.vo; + +import java.math.BigDecimal; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +@Entity +@Table(name = "quota_tariff_usage") +public class QuotaTariffUsageVO implements InternalIdentity { + @Id + @Column(name = "id") + private Long id; + + @Column(name = "tariff_id") + private Long tariffId; + + @Column(name = "quota_usage_id") + private Long quotaUsageId; + + @Column(name = "quota_used") + private BigDecimal quotaUsed; + + public QuotaTariffUsageVO() { + quotaUsed = new BigDecimal(0); + } + + @Override + public long getId() { + return id; + } + + public Long getTariffId() { + return tariffId; + } + + public Long getQuotaUsageId() { + return quotaUsageId; + } + + public BigDecimal getQuotaUsed() { + return quotaUsed; + } + + public void setId(Long id) { + this.id = id; + } + + public void setTariffId(Long tariffId) { + this.tariffId = tariffId; + } + + public void setQuotaUsageId(Long quotaUsageId) { + this.quotaUsageId = quotaUsageId; + } + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + @Override + public String toString() { + return new ReflectionToStringBuilder(this, ToStringStyle.JSON_STYLE).toString(); + } +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageJoinVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageJoinVO.java new file mode 100644 index 000000000000..df9577e23c3e --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageJoinVO.java @@ -0,0 +1,179 @@ +//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.quota.vo; + +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.math.BigDecimal; +import java.util.Date; + +@Entity +@Table(name = "quota_usage_view") +public class QuotaUsageJoinVO implements InternalIdentity { + + @Id + @Column(name = "id", updatable = false, nullable = false) + private Long id; + + @Column(name = "zone_id") + private Long zoneId = null; + + @Column(name = "account_id") + private Long accountId = null; + + @Column(name = "domain_id") + private Long domainId = null; + + @Column(name = "usage_item_id") + private Long usageItemId; + + @Column(name = "usage_type") + private int usageType; + + @Column(name = "quota_used") + private BigDecimal quotaUsed; + + @Column(name = "start_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date startDate = null; + + @Column(name = "end_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date endDate = null; + + @Column(name = "resource_id") + private Long resourceId = null; + + @Column(name = "network_id") + private Long networkId = null; + + @Column(name = "offering_id") + private Long offeringId = null; + + @Override + public long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getZoneId() { + return zoneId; + } + + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getDomainId() { + return domainId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public Long getUsageItemId() { + return usageItemId; + } + + public void setUsageItemId(Long usageItemId) { + this.usageItemId = usageItemId; + } + + public int getUsageType() { + return usageType; + } + + public void setUsageType(int usageType) { + this.usageType = usageType; + } + + public BigDecimal getQuotaUsed() { + return quotaUsed; + } + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public Long getResourceId() { + return resourceId; + } + + public void setResourceId(Long resourceId) { + this.resourceId = resourceId; + } + + public Long getNetworkId() { + return networkId; + } + + public void setNetworkId(Long networkId) { + this.networkId = networkId; + } + + public Long getOfferingId() { + return offeringId; + } + + public void setOfferingId(Long offeringId) { + this.offeringId = offeringId; + } + + public QuotaUsageJoinVO () { + } + + @Override + public String toString() { + return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "zoneId", "accountId", "domainId", "usageItemId", "usageType", "quotaUsed", "startDate", + "endDate", "resourceId"); + } +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageResourceVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageResourceVO.java new file mode 100644 index 000000000000..924824231005 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageResourceVO.java @@ -0,0 +1,62 @@ +// +// 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.quota.vo; + +import java.util.Date; + +public class QuotaUsageResourceVO { + private String uuid; + private String name; + private Date removed; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public boolean isRemoved() { + return this.removed != null; + } + + public QuotaUsageResourceVO(String uuid, String name, Date removed) { + this.uuid = uuid; + this.name = name; + this.removed = removed; + } +} diff --git a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml index 304b23b7220f..5ca2679c388c 100644 --- a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml +++ b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml @@ -26,7 +26,9 @@ - + + + diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java index d3bd3868ed16..bfe26a9f4250 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java @@ -17,7 +17,6 @@ package org.apache.cloudstack.api.command; import java.util.Date; -import java.util.List; import javax.inject.Inject; @@ -28,24 +27,25 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaStatementItemResponse; import org.apache.cloudstack.api.response.QuotaStatementResponse; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; -import com.cloud.user.Account; +import org.apache.commons.lang3.ObjectUtils; -@APICommand(name = "quotaStatement", responseObject = QuotaStatementItemResponse.class, description = "Create a quota statement", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - httpMethod = "GET") +@APICommand(name = "quotaStatement", responseObject = QuotaStatementItemResponse.class, description = "Create a Quota statement for the provided Account, Project, or Domain.", + since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, httpMethod = "GET") public class QuotaStatementCmd extends BaseCmd { - - - @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Optional, Account Id for which statement needs to be generated") + @ACL + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, + description = "Name of the Account for which the Quota statement will be generated. Deprecated, please use accountid instead.") private String accountName; @ACL - @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "Optional, If domain Id is given and the caller is domain admin then the statement is generated for domain.") + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "ID of the Domain for which the Quota statement will be generated. May be used individually or with account.") private Long domainId; @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = true, description = "End of the period of the Quota statement. " + @@ -56,15 +56,25 @@ public class QuotaStatementCmd extends BaseCmd { ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) private Date startDate; - @Parameter(name = ApiConstants.TYPE, type = CommandType.INTEGER, description = "List quota usage records for the specified usage type") + @Parameter(name = ApiConstants.TYPE, type = CommandType.INTEGER, + description = "Consider only Quota usage records for the specified usage type in the statement.") private Integer usageType; @ACL - @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "List usage records for the specified Account") + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "ID of the Account for which the Quota statement will be generated. Can not be specified with projectid.") private Long accountId; + @ACL + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "ID of the Project for which the Quota statement will be generated. Can not be specified with accountid.", since = "4.23.0") + private Long projectId; + + @Parameter(name = ApiConstants.SHOW_RESOURCES, type = CommandType.BOOLEAN, description = "List the resources of each Quota type in the period.", since = "4.23.0") + private boolean showResources; + @Inject - private QuotaResponseBuilder _responseBuilder; + QuotaResponseBuilder responseBuilder; public Long getAccountId() { return accountId; @@ -99,43 +109,47 @@ public void setDomainId(Long domainId) { } public Date getEndDate() { - return _responseBuilder.startOfNextDay(endDate == null ? new Date() : new Date(endDate.getTime())); + return endDate; } public void setEndDate(Date endDate) { - this.endDate = endDate == null ? null : new Date(endDate.getTime()); + this.endDate = endDate; } public Date getStartDate() { - return startDate == null ? null : new Date(startDate.getTime()); + return startDate; } public void setStartDate(Date startDate) { - this.startDate = startDate == null ? null : new Date(startDate.getTime()); + this.startDate = startDate; + } + + public boolean isShowResources() { + return showResources; + } + + public void setShowResources(boolean showResources) { + this.showResources = showResources; + } + + public Long getProjectId() { + return projectId; } @Override public long getEntityOwnerId() { - if (accountId != null) { - return accountId; + if (ObjectUtils.allNull(accountId, accountName, projectId)) { + return -1; } - Account activeAccountByName = _accountService.getActiveAccountByName(accountName, domainId); - if (activeAccountByName != null) { - return activeAccountByName.getAccountId(); - } - return Account.ACCOUNT_ID_SYSTEM; + return _accountService.finalizeAccountId(accountId, accountName, domainId, projectId); } @Override public void execute() { - List quotaUsage = _responseBuilder.getQuotaUsage(this); - - QuotaStatementResponse response = _responseBuilder.createQuotaStatementResponse(quotaUsage); - response.setStartDate(startDate == null ? null : new Date(startDate.getTime())); - response.setEndDate(endDate == null ? null : new Date(endDate.getTime())); - + QuotaStatementResponse response = responseBuilder.createQuotaStatementResponse(this); + response.setStartDate(startDate); + response.setEndDate(endDate); response.setResponseName(getCommandName()); setResponseObject(response); } - } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java index 177fb00d4b55..bde905c487b7 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java @@ -32,7 +32,6 @@ import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; import java.util.Date; import java.util.List; @@ -49,7 +48,7 @@ public interface QuotaResponseBuilder { boolean isUserAllowedToSeeActivationRules(User user); - QuotaStatementResponse createQuotaStatementResponse(List quotaUsage); + QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd); QuotaBalanceResponse createQuotaBalanceResponse(List quotaUsage, Date startDate, Date endDate); @@ -57,8 +56,6 @@ public interface QuotaResponseBuilder { QuotaBalanceResponse createQuotaLastBalanceResponse(List quotaBalance, Date startDate); - List getQuotaUsage(QuotaStatementCmd cmd); - List getQuotaBalance(QuotaBalanceCmd cmd); QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 2d6ec3255f4a..173c0723731b 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -21,13 +21,11 @@ import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.math.BigDecimal; -import java.math.RoundingMode; import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; @@ -36,6 +34,7 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -43,12 +42,36 @@ import javax.inject.Inject; import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; -import com.cloud.projects.dao.ProjectDao; +import com.cloud.network.dao.IPAddressDao; +import com.cloud.network.dao.IPAddressVO; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.offerings.NetworkOfferingVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.SnapshotVO; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.AccountVO; +import com.cloud.user.dao.AccountDao; +import com.cloud.user.dao.UserDao; import com.cloud.user.User; import com.cloud.user.UserVO; import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ServerApiException; @@ -94,7 +117,8 @@ import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; import org.apache.cloudstack.quota.vo.QuotaSummaryVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; +import org.apache.cloudstack.quota.vo.QuotaUsageResourceVO; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections4.CollectionUtils; @@ -106,18 +130,6 @@ import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; -import com.cloud.domain.DomainVO; -import com.cloud.domain.dao.DomainDao; -import com.cloud.event.ActionEvent; -import com.cloud.event.EventTypes; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.user.Account; -import com.cloud.user.AccountManager; -import com.cloud.user.AccountVO; -import com.cloud.user.dao.AccountDao; -import com.cloud.user.dao.UserDao; -import com.cloud.utils.Pair; - @Component public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { protected Logger logger = LogManager.getLogger(getClass()); @@ -140,8 +152,6 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Inject private AccountDao _accountDao; @Inject - private ProjectDao projectDao; - @Inject private QuotaAccountDao quotaAccountDao; @Inject private DomainDao domainDao; @@ -159,6 +169,21 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { private JsInterpreterHelper jsInterpreterHelper; @Inject private ApiDiscoveryService apiDiscoveryService; + @Inject + private IPAddressDao ipAddressDao; + @Inject + private NetworkDao networkDao; + @Inject + private NetworkOfferingDao networkOfferingDao; + @Inject + private SnapshotDao snapshotDao; + @Inject + private VMInstanceDao vmInstanceDao; + @Inject + private VMTemplateDao vmTemplateDao; + @Inject + private VolumeDao volumeDao; + private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class}; @@ -393,76 +418,210 @@ public int compare(QuotaBalanceVO o1, QuotaBalanceVO o2) { } @Override - public QuotaStatementResponse createQuotaStatementResponse(final List quotaUsage) { - if (quotaUsage == null || quotaUsage.isEmpty()) { - throw new InvalidParameterValueException("There is no usage data found for period mentioned."); - } + public QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd) { + Long accountId = getAccountIdForQuotaStatement(cmd); + Long domainId = getDomainIdForQuotaStatement(cmd, accountId); + List quotaUsages = _quotaService.getQuotaUsage(accountId, null, domainId, cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); + + logger.debug("Creating quota statement from [{}] usage records for parameters [{}].", quotaUsages.size(), + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "accountName", "accountId", "projectId", "domainId", "startDate", "endDate", "usageType", "showResources")); + createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(quotaUsages, cmd.getUsageType()); + + Map> recordsPerUsageTypes = quotaUsages.stream() + .sorted(Comparator.comparingInt(QuotaUsageJoinVO::getUsageType)) + .collect(Collectors.groupingBy(QuotaUsageJoinVO::getUsageType)); + + List items = new ArrayList<>(); + recordsPerUsageTypes.forEach((key, value) -> items.add(createStatementItem(key, value, cmd.isShowResources()))); QuotaStatementResponse statement = new QuotaStatementResponse(); + statement.setLineItem(items); + statement.setTotalQuota(items.stream().map(QuotaStatementItemResponse::getQuotaUsed).reduce(BigDecimal.ZERO, BigDecimal::add)); + statement.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); + statement.setObjectName("statement"); - HashMap quotaTariffMap = new HashMap(); - Collection result = QuotaTypes.listQuotaTypes().values(); + if (accountId != null) { + Account account = _accountDao.findByIdIncludingRemoved(accountId); + statement.setAccountId(account.getUuid()); + statement.setAccountName(account.getAccountName()); + domainId = account.getDomainId(); + } + if (domainId != null) { + DomainVO domain = domainDao.findByIdIncludingRemoved(domainId); + statement.setDomainId(domain.getUuid()); + } + + return statement; + } - for (QuotaTypes quotaTariff : result) { - quotaTariffMap.put(quotaTariff.getQuotaType(), quotaTariff); - // add dummy record for each usage type - QuotaUsageVO dummy = new QuotaUsageVO(quotaUsage.get(0)); - dummy.setUsageType(quotaTariff.getQuotaType()); - dummy.setQuotaUsed(new BigDecimal(0)); - quotaUsage.add(dummy); + protected Long getAccountIdForQuotaStatement(QuotaStatementCmd cmd) { + if (Account.Type.NORMAL.equals(CallContext.current().getCallingAccount().getType())) { + logger.debug("Limiting the Quota statement for the calling Account, as they are a User Account."); + return CallContext.current().getCallingAccountId(); } - if (logger.isDebugEnabled()) { - logger.debug( - "createQuotaStatementResponse Type=" + quotaUsage.get(0).getUsageType() + " usage=" + quotaUsage.get(0).getQuotaUsed().setScale(2, RoundingMode.HALF_EVEN) - + " rec.id=" + quotaUsage.get(0).getUsageItemId() + " SD=" + quotaUsage.get(0).getStartDate() + " ED=" + quotaUsage.get(0).getEndDate()); + long accountId = cmd.getEntityOwnerId(); + if (accountId != -1) { + return accountId; } - Collections.sort(quotaUsage, new Comparator() { - @Override - public int compare(QuotaUsageVO o1, QuotaUsageVO o2) { - if (o1.getUsageType() == o2.getUsageType()) { - return 0; - } - return o1.getUsageType() < o2.getUsageType() ? -1 : 1; - } - }); + if (cmd.getDomainId() == null) { + logger.debug("Limiting the Quota statement for the calling Account, as 'domainid' was not informed."); + return CallContext.current().getCallingAccountId(); + } + + logger.debug("Allowing admin/domain admin to generate the Quota statement for the provided Domain."); + return null; + } + + protected Long getDomainIdForQuotaStatement(QuotaStatementCmd cmd, Long accountId) { + if (accountId != null) { + logger.debug("Quota statement is already limited to Account [{}].", accountId); + Account account = _accountDao.findByIdIncludingRemoved(accountId); + return account.getDomainId(); + } + + Long domainId = cmd.getDomainId(); + if (domainId != null) { + return domainId; + } + + logger.debug("Limiting the Quota statement for the calling Account's Domain."); + return CallContext.current().getCallingAccount().getDomainId(); + } + + protected void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(List quotaUsages, Integer usageType) { + if (usageType != null) { + logger.debug("As the usage type [{}] was informed as parameter of the API quotaStatement, we will not create dummy records.", usageType); + return; + + } + + for (Integer quotaType : QuotaTypes.listQuotaTypes().keySet()) { + QuotaUsageJoinVO dummy = new QuotaUsageJoinVO(); + dummy.setUsageType(quotaType); + dummy.setQuotaUsed(BigDecimal.ZERO); + quotaUsages.add(dummy); + } + } + + protected QuotaStatementItemResponse createStatementItem(int usageType, List usageRecords, boolean showResources) { + QuotaUsageJoinVO firstRecord = usageRecords.get(0); + int type = firstRecord.getUsageType(); + + QuotaTypes quotaType = QuotaTypes.listQuotaTypes().get(type); + + QuotaStatementItemResponse item = new QuotaStatementItemResponse(type); + item.setQuotaUsed(usageRecords.stream().map(QuotaUsageJoinVO::getQuotaUsed).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + item.setUsageUnit(quotaType.getQuotaUnit()); + item.setUsageName(quotaType.getQuotaName()); + + setStatementItemResources(item, usageType, usageRecords, showResources); + return item; + } + + protected void setStatementItemResources(QuotaStatementItemResponse statementItem, int usageType, List quotaUsageRecords, boolean showResources) { + if (!showResources) { + return; + } + + List itemDetails = new ArrayList<>(); + + Map quotaUsagesValuesAggregatedById = quotaUsageRecords + .stream() + .filter(quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType) != null) + .collect(Collectors.groupingBy( + quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType), + Collectors.reducing(new BigDecimal(0), QuotaUsageJoinVO::getQuotaUsed, BigDecimal::add) + )); + + for (Map.Entry entry : quotaUsagesValuesAggregatedById.entrySet()) { + QuotaStatementItemResourceResponse detail = new QuotaStatementItemResourceResponse(); + + detail.setQuotaUsed(entry.getValue()); + + QuotaUsageResourceVO resource = getResourceFromIdAndType(entry.getKey(), usageType); + if (resource != null) { + detail.setResourceId(resource.getUuid()); + detail.setDisplayName(resource.getName()); + detail.setRemoved(resource.isRemoved()); + } else { + detail.setDisplayName(""); - List items = new ArrayList(); - QuotaStatementItemResponse lineitem; - int type = -1; - BigDecimal usage = new BigDecimal(0); - BigDecimal totalUsage = new BigDecimal(0); - quotaUsage.add(new QuotaUsageVO());// boundary - QuotaUsageVO prev = quotaUsage.get(0); - if (logger.isDebugEnabled()) { - logger.debug("createQuotaStatementResponse record count=" + quotaUsage.size()); - } - for (final QuotaUsageVO quotaRecord : quotaUsage) { - if (type != quotaRecord.getUsageType()) { - if (type != -1) { - lineitem = new QuotaStatementItemResponse(type); - lineitem.setQuotaUsed(usage); - lineitem.setAccountId(prev.getAccountId()); - lineitem.setDomainId(prev.getDomainId()); - lineitem.setUsageUnit(quotaTariffMap.get(type).getQuotaUnit()); - lineitem.setUsageName(quotaTariffMap.get(type).getQuotaName()); - lineitem.setObjectName("quotausage"); - items.add(lineitem); - totalUsage = totalUsage.add(usage); - usage = new BigDecimal(0); - } - type = quotaRecord.getUsageType(); } - prev = quotaRecord; - usage = usage.add(quotaRecord.getQuotaUsed()); + itemDetails.add(detail); } + statementItem.setResources(itemDetails); + } - statement.setLineItem(items); - statement.setTotalQuota(totalUsage); - statement.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); - statement.setObjectName("statement"); - return statement; + protected Long getResourceIdByUsageType(QuotaUsageJoinVO quotaUsageJoinVo, int usageType) { + switch (usageType) { + case QuotaTypes.NETWORK_BYTES_SENT: + case QuotaTypes.NETWORK_BYTES_RECEIVED: + return quotaUsageJoinVo.getNetworkId(); + case QuotaTypes.NETWORK_OFFERING: + return quotaUsageJoinVo.getOfferingId(); + default: + return quotaUsageJoinVo.getResourceId(); + } + } + + protected QuotaUsageResourceVO getResourceFromIdAndType(long resourceId, int usageType) { + switch (usageType) { + case QuotaTypes.ALLOCATED_VM: + case QuotaTypes.RUNNING_VM: + VMInstanceVO vmInstance = vmInstanceDao.findByIdIncludingRemoved(resourceId); + if (vmInstance != null) { + return new QuotaUsageResourceVO(vmInstance.getUuid(), vmInstance.getHostName(), vmInstance.getRemoved()); + } + break; + case QuotaTypes.VOLUME: + case QuotaTypes.VOLUME_SECONDARY: + case QuotaTypes.VM_DISK_BYTES_READ: + case QuotaTypes.VM_DISK_BYTES_WRITE: + case QuotaTypes.VM_DISK_IO_READ: + case QuotaTypes.VM_DISK_IO_WRITE: + VolumeVO volume = volumeDao.findByIdIncludingRemoved(resourceId); + if (volume != null) { + return new QuotaUsageResourceVO(volume.getUuid(), volume.getName(), volume.getRemoved()); + } + break; + case QuotaTypes.VM_SNAPSHOT_ON_PRIMARY: + case QuotaTypes.VM_SNAPSHOT: + case QuotaTypes.SNAPSHOT: + SnapshotVO snapshot = snapshotDao.findByIdIncludingRemoved(resourceId); + if (snapshot != null) { + return new QuotaUsageResourceVO(snapshot.getUuid(), snapshot.getName(), snapshot.getRemoved()); + } + break; + case QuotaTypes.NETWORK_BYTES_SENT: + case QuotaTypes.NETWORK_BYTES_RECEIVED: + NetworkVO network = networkDao.findByIdIncludingRemoved(resourceId); + if (network != null) { + return new QuotaUsageResourceVO(network.getUuid(), network.getName(), network.getRemoved()); + } + break; + case QuotaTypes.TEMPLATE: + case QuotaTypes.ISO: + VMTemplateVO vmTemplate = vmTemplateDao.findByIdIncludingRemoved(resourceId); + if (vmTemplate != null) { + return new QuotaUsageResourceVO(vmTemplate.getUuid(), vmTemplate.getName(), vmTemplate.getRemoved()); + } + break; + case QuotaTypes.NETWORK_OFFERING: + NetworkOfferingVO networkOffering = networkOfferingDao.findByIdIncludingRemoved(resourceId); + if (networkOffering != null) { + return new QuotaUsageResourceVO(networkOffering.getUuid(), networkOffering.getName(), networkOffering.getRemoved()); + } + break; + case QuotaTypes.IP_ADDRESS: + IPAddressVO ipAddress = ipAddressDao.findByIdIncludingRemoved(resourceId); + if (ipAddress != null) { + return new QuotaUsageResourceVO(ipAddress.getUuid(), ipAddress.getName(), ipAddress.getRemoved()); + } + break; + } + return null; } @Override @@ -518,14 +677,14 @@ public QuotaTariffVO updateQuotaTariffPlan(QuotaTariffUpdateCmd cmd) { } protected void warnQuotaTariffUpdateDeprecatedFields(QuotaTariffUpdateCmd cmd) { - String warnMessage = "The parameter 's%s' for API 'quotaTariffUpdate' is no longer needed and it will be removed in future releases."; + String warnMessage = "The parameter '{}' for API 'quotaTariffUpdate' is no longer needed and it will be removed in future releases."; if (cmd.getStartDate() != null) { - logger.warn(String.format(warnMessage,"startdate")); + logger.warn(warnMessage, "startdate"); } if (cmd.getUsageType() != null) { - logger.warn(String.format(warnMessage,"usagetype")); + logger.warn(warnMessage, "usagetype"); } } @@ -712,11 +871,6 @@ public QuotaBalanceResponse createQuotaLastBalanceResponse(List return resp; } - @Override - public List getQuotaUsage(QuotaStatementCmd cmd) { - return _quotaService.getQuotaUsage(cmd.getAccountId(), cmd.getAccountName(), cmd.getDomainId(), cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); - } - @Override public List getQuotaBalance(QuotaBalanceCmd cmd) { return _quotaService.findQuotaBalanceVO(cmd.getAccountId(), cmd.getAccountName(), cmd.getDomainId(), cmd.getStartDate(), cmd.getEndDate()); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java new file mode 100644 index 000000000000..3e052f733391 --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java @@ -0,0 +1,61 @@ +//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.api.response; + +import java.math.BigDecimal; + +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; + +public class QuotaStatementItemResourceResponse extends BaseResponse { + + @SerializedName(ApiConstants.QUOTA_CONSUMED) + @Param(description = "Quota consumed.") + private BigDecimal quotaUsed; + + @SerializedName(ApiConstants.RESOURCE_ID) + @Param(description = "Resources's ID.") + private String resourceId; + + @SerializedName(ApiConstants.DISPLAY_NAME) + @Param(description = "Resource's display name.") + private String displayName; + + @SerializedName(ApiConstants.REMOVED) + @Param(description = "Indicates whether the resource is removed or active.") + private boolean removed; + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public void setRemoved(boolean removed) { + this.removed = removed; + } +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java index c370d82b3cc4..0747c5a9172d 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java @@ -17,72 +17,41 @@ package org.apache.cloudstack.api.response; import java.math.BigDecimal; -import java.math.RoundingMode; +import java.util.List; import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import com.cloud.serializer.Param; public class QuotaStatementItemResponse extends BaseResponse { - @SerializedName("type") - @Param(description = "Usage type") + @SerializedName(ApiConstants.TYPE) + @Param(description = "Usage type.") private int usageType; - @SerializedName("accountid") - @Param(description = "Account id") - private Long accountId; - - @SerializedName("account") - @Param(description = "Account name") - private String accountName; - - @SerializedName("domain") - @Param(description = "Domain id") - private Long domainId; - - @SerializedName("name") - @Param(description = "Usage type name") + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the Usage type.") private String usageName; - @SerializedName("unit") - @Param(description = "Usage unit") + @SerializedName(ApiConstants.UNIT) + @Param(description = "Unit of the Usage type.") private String usageUnit; - @SerializedName("quota") - @Param(description = "Quota consumed") + @SerializedName(ApiConstants.QUOTA) + @Param(description = "Quota consumed.") private BigDecimal quotaUsed; + @SerializedName(ApiConstants.RESOURCES) + @Param(description = "Item's resources.") + private List resources; + public QuotaStatementItemResponse(final int usageType) { this.usageType = usageType; } - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - public String getAccountName() { - return accountName; - } - - public void setAccountName(String accountName) { - this.accountName = accountName; - } - - public Long getDomainId() { - return domainId; - } - - public void setDomainId(Long domainId) { - this.domainId = domainId; - } - public String getUsageName() { return usageName; } @@ -112,7 +81,15 @@ public BigDecimal getQuotaUsed() { } public void setQuotaUsed(BigDecimal quotaUsed) { - this.quotaUsed = quotaUsed.setScale(2, RoundingMode.HALF_EVEN); + this.quotaUsed = quotaUsed; + } + + public List getResources() { + return resources; + } + + public void setResources(List resources) { + this.resources = resources; } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java index 0a7ba4dbeb94..81cb1011182d 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java @@ -18,56 +18,56 @@ import com.cloud.serializer.Param; import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Date; import java.util.List; public class QuotaStatementResponse extends BaseResponse { - @SerializedName("accountid") - @Param(description = "Account ID") - private Long accountId; + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "ID of the Account.") + private String accountId; - @SerializedName("account") - @Param(description = "Account name") + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "Name of the Account.") private String accountName; - @SerializedName("domain") - @Param(description = "Domain ID") - private Long domainId; + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "ID of the Domain.") + private String domainId; - @SerializedName("quotausage") - @Param(description = "List of quota usage under various types", responseObject = QuotaStatementItemResponse.class) + @SerializedName(ApiConstants.QUOTA_USAGE) + @Param(description = "List of Quota usage under various types.", responseObject = QuotaStatementItemResponse.class) private List lineItem; - @SerializedName("totalquota") - @Param(description = "Total quota used during this period") + @SerializedName(ApiConstants.TOTAL_QUOTA) + @Param(description = "Total Quota consumed during this period.") private BigDecimal totalQuota; - @SerializedName("startdate") - @Param(description = "Start date") + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Start date of the Quota statement.") private Date startDate = null; - @SerializedName("enddate") - @Param(description = "End date") + @SerializedName(ApiConstants.END_DATE) + @Param(description = "End date of the Quota statement.") private Date endDate = null; - @SerializedName("currency") - @Param(description = "Currency") + @SerializedName(ApiConstants.CURRENCY) + @Param(description = "Currency of the Quota statement.") private String currency; public QuotaStatementResponse() { super(); } - public Long getAccountId() { + public String getAccountId() { return accountId; } - public void setAccountId(Long accountId) { + public void setAccountId(String accountId) { this.accountId = accountId; } @@ -79,45 +79,36 @@ public void setAccountName(String accountName) { this.accountName = accountName; } - public Long getDomainId() { + public String getDomainId() { return domainId; } - public void setDomainId(Long domainId) { + public void setDomainId(String domainId) { this.domainId = domainId; } - public List getLineItem() { - return lineItem; - } - public void setLineItem(List lineItem) { this.lineItem = lineItem; } public Date getStartDate() { - return startDate == null ? null : new Date(startDate.getTime()); + return startDate; } public void setStartDate(Date startDate) { - this.startDate = startDate == null ? null : new Date(startDate.getTime()); + this.startDate = startDate; } public Date getEndDate() { - return endDate == null ? null : new Date(endDate.getTime()); + return endDate; } public void setEndDate(Date endDate) { - this.endDate = endDate == null ? null : new Date(endDate.getTime()); - } - - - public BigDecimal getTotalQuota() { - return totalQuota; + this.endDate = endDate; } public void setTotalQuota(BigDecimal totalQuota) { - this.totalQuota = totalQuota.setScale(2, RoundingMode.HALF_EVEN); + this.totalQuota = totalQuota; } public String getCurrency() { diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java index a421d0f066a0..78acfc11682e 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java @@ -21,14 +21,14 @@ import java.util.List; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; import com.cloud.user.AccountVO; import com.cloud.utils.component.PluggableService; public interface QuotaService extends PluggableService { - List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate); + List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate); List findQuotaBalanceVO(Long accountId, String accountName, Long domainId, Date startDate, Date endDate); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java index 2d7d623d1d98..a0ba2fbc751d 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java @@ -53,10 +53,10 @@ import org.apache.cloudstack.quota.constant.QuotaConfig; import org.apache.cloudstack.quota.dao.QuotaAccountDao; import org.apache.cloudstack.quota.dao.QuotaBalanceDao; -import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; import org.apache.commons.lang3.ObjectUtils; import org.springframework.stereotype.Component; @@ -80,7 +80,7 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi @Inject private QuotaAccountDao _quotaAcc; @Inject - private QuotaUsageDao _quotaUsageDao; + private QuotaUsageJoinDao quotaUsageJoinDao; @Inject private DomainDao _domainDao; @Inject @@ -213,27 +213,7 @@ public List findQuotaBalanceVO(Long accountId, String accountNam } @Override - public List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate) { - // if accountId is not specified, use accountName and domainId - if ((accountId == null) && (accountName != null) && (domainId != null)) { - Account userAccount = null; - Account caller = CallContext.current().getCallingAccount(); - if (_domainDao.isChildDomain(caller.getDomainId(), domainId)) { - Filter filter = new Filter(AccountVO.class, "id", Boolean.FALSE, null, null); - List accounts = _accountDao.listAccounts(accountName, domainId, filter); - if (!accounts.isEmpty()) { - userAccount = accounts.get(0); - } - if (userAccount != null) { - accountId = userAccount.getId(); - } else { - throw new InvalidParameterValueException("Unable to find account " + accountName + " in domain " + domainId); - } - } else { - throw new PermissionDeniedException("Invalid Domain Id or Account"); - } - } - + public List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate) { if (startDate.after(endDate)) { throw new InvalidParameterValueException("Incorrect Date Range. Start date: " + startDate + " is after end date:" + endDate); } @@ -241,7 +221,7 @@ public List getQuotaUsage(Long accountId, String accountName, Long logger.debug("Getting quota records of type [{}] for account [{}] in domain [{}], between [{}] and [{}].", usageType, accountId, domainId, startDate, endDate); - return _quotaUsageDao.findQuotaUsage(accountId, domainId, usageType, startDate, endDate); + return quotaUsageJoinDao.findQuotaUsage(accountId, domainId, usageType, null, null, null, startDate, endDate, null); } @Override diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaStatementCmdTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaStatementCmdTest.java index d6f9f747fa84..e67638db632e 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaStatementCmdTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaStatementCmdTest.java @@ -16,38 +16,29 @@ // under the License. package org.apache.cloudstack.api.command; -import junit.framework.TestCase; import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaStatementResponse; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; - @RunWith(MockitoJUnitRunner.class) -public class QuotaStatementCmdTest extends TestCase { +public class QuotaStatementCmdTest { @Mock - QuotaResponseBuilder responseBuilder; + QuotaResponseBuilder responseBuilderMock; @Test - public void testQuotaStatementCmd() throws NoSuchFieldException, IllegalAccessException { + public void executeTestVerifyCalls() { QuotaStatementCmd cmd = new QuotaStatementCmd(); cmd.setAccountName("admin"); + cmd.responseBuilder = responseBuilderMock; - Field rbField = QuotaStatementCmd.class.getDeclaredField("_responseBuilder"); - rbField.setAccessible(true); - rbField.set(cmd, responseBuilder); + Mockito.doReturn(new QuotaStatementResponse()).when(responseBuilderMock).createQuotaStatementResponse(Mockito.any()); - List quotaUsageVOList = new ArrayList(); - Mockito.when(responseBuilder.getQuotaUsage(Mockito.eq(cmd))).thenReturn(quotaUsageVOList); - Mockito.when(responseBuilder.createQuotaStatementResponse(Mockito.eq(quotaUsageVOList))).thenReturn(new QuotaStatementResponse()); cmd.execute(); - Mockito.verify(responseBuilder, Mockito.times(1)).getQuotaUsage(Mockito.eq(cmd)); + + Mockito.verify(responseBuilderMock).createQuotaStatementResponse(cmd); } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index ea88a106b846..81b4992082d9 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -17,6 +17,8 @@ package org.apache.cloudstack.api.response; import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; @@ -42,6 +44,7 @@ import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; +import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd; import org.apache.cloudstack.context.CallContext; @@ -67,6 +70,7 @@ import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.commons.lang3.time.DateUtils; @@ -914,4 +918,199 @@ public void injectUsageTypeVariablesTestReturnInjectedVariables() { Assert.assertTrue(formattedVariables.containsValue("accountname")); Assert.assertTrue(formattedVariables.containsValue("zonename")); } + + @Test + public void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformedTestUsageTypeDifferentFromNullDoNothing() { + List listUsage = new ArrayList<>(); + + quotaResponseBuilderSpy.createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(listUsage, 1); + + Assert.assertTrue(listUsage.isEmpty()); + } + + @Test + public void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformedTestUsageTypeIsNullAddDummyForAllQuotaTypes() { + List listUsage = new ArrayList<>(); + listUsage.add(new QuotaUsageJoinVO()); + + quotaResponseBuilderSpy.createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(listUsage, null); + + Assert.assertEquals(QuotaTypes.listQuotaTypes().size() + 1, listUsage.size()); + + QuotaTypes.listQuotaTypes().entrySet().forEach(entry -> { + Assert.assertTrue(listUsage.stream().anyMatch(usage -> usage.getUsageType() == entry.getKey() && usage.getQuotaUsed().equals(BigDecimal.ZERO))); + }); + } + + private List getQuotaUsagesForTest() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + List quotaUsages = new ArrayList<>(); + + QuotaUsageJoinVO quotaUsage = new QuotaUsageJoinVO(); + quotaUsage.setAccountId(1l); + quotaUsage.setDomainId(2l); + quotaUsage.setUsageType(3); + quotaUsage.setQuotaUsed(BigDecimal.valueOf(10)); + try { + quotaUsage.setStartDate(sdf.parse("2022-01-01")); + quotaUsage.setEndDate(sdf.parse("2022-01-02")); + } catch (ParseException ignored) { + } + quotaUsages.add(quotaUsage); + + quotaUsage = new QuotaUsageJoinVO(); + quotaUsage.setAccountId(4l); + quotaUsage.setDomainId(5l); + quotaUsage.setUsageType(3); + quotaUsage.setQuotaUsed(null); + try { + quotaUsage.setStartDate(sdf.parse("2022-01-03")); + quotaUsage.setEndDate(sdf.parse("2022-01-04")); + } catch (ParseException ignored) { + } + quotaUsages.add(quotaUsage); + + quotaUsage = new QuotaUsageJoinVO(); + quotaUsage.setAccountId(6l); + quotaUsage.setDomainId(7l); + quotaUsage.setUsageType(3); + quotaUsage.setQuotaUsed(BigDecimal.valueOf(5)); + try { + quotaUsage.setStartDate(sdf.parse("2022-01-05")); + quotaUsage.setEndDate(sdf.parse("2022-01-06")); + } catch (ParseException ignored) { + } + quotaUsages.add(quotaUsage); + + return quotaUsages; + } + + @Test + public void createStatementItemTestReturnItem() { + List quotaUsages = getQuotaUsagesForTest(); + Mockito.doNothing().when(quotaResponseBuilderSpy).setStatementItemResources(Mockito.any(), Mockito.anyInt(), Mockito.any(), Mockito.anyBoolean()); + + QuotaStatementItemResponse result = quotaResponseBuilderSpy.createStatementItem(0, quotaUsages, false); + + QuotaUsageJoinVO expected = quotaUsages.get(0); + QuotaTypes quotaTypeExpected = QuotaTypes.listQuotaTypes().get(expected.getUsageType()); + Assert.assertEquals(BigDecimal.valueOf(15), result.getQuotaUsed()); + Assert.assertEquals(quotaTypeExpected.getQuotaUnit(), result.getUsageUnit()); + Assert.assertEquals(quotaTypeExpected.getQuotaName(), result.getUsageName()); + } + + @Test + public void setStatementItemResourcesTestDoNotShowResourcesDoNothing() { + QuotaStatementItemResponse item = new QuotaStatementItemResponse(1); + + quotaResponseBuilderSpy.setStatementItemResources(item, 0, getQuotaUsagesForTest(), false); + + Assert.assertNull(item.getResources()); + } + + @Test + public void getAccountIdForQuotaStatementTestLimitsToCallingAccountForNormalUser() { + QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + + Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); + Mockito.doReturn(Account.Type.NORMAL).when(accountMock).getType(); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + + Assert.assertEquals(Long.valueOf(callerAccountMock.getAccountId()), result); + } + } + + @Test + public void getAccountIdForQuotaStatementTestReturnsEntityOwnerIdWhenProvided() { + QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + + Mockito.doReturn(42L).when(cmd).getEntityOwnerId(); + + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + + Assert.assertEquals(Long.valueOf(42L), result); + } + + @Test + public void getAccountIdForQuotaStatementTestLimitsToCallingAccountWhenCallerIsAdminAndDomainIsNotProvided() { + QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + + Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); + Mockito.doReturn(Account.Type.ADMIN).when(accountMock).getType(); + Mockito.doReturn(-1L).when(cmd).getEntityOwnerId(); + Mockito.doReturn(null).when(cmd).getDomainId(); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + + Assert.assertEquals(Long.valueOf(callerAccountMock.getAccountId()), result); + } + } + + @Test + public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsAdminAndDomainIsProvided() { + QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + + Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); + Mockito.doReturn(Account.Type.ADMIN).when(accountMock).getType(); + Mockito.doReturn(-1L).when(cmd).getEntityOwnerId(); + Mockito.doReturn(10L).when(cmd).getDomainId(); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + + Assert.assertNull(result); + } + } + + @Test + public void getDomainIdForQuotaStatementTestReturnsAccountDomainIdWhenAccountIdIsProvided() { + QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + AccountVO account = Mockito.mock(AccountVO.class); + + Mockito.doReturn(account).when(accountDaoMock).findByIdIncludingRemoved(55L); + Mockito.doReturn(77L).when(account).getDomainId(); + + Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, 55L); + + Assert.assertEquals(Long.valueOf(77L), result); + } + + @Test + public void getDomainIdForQuotaStatementTestReturnsProvidedDomainIdWhenAccountIdIsNull() { + QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + + Mockito.doReturn(99L).when(cmd).getDomainId(); + + Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, null); + + Assert.assertEquals(Long.valueOf(99L), result); + } + + @Test + public void getDomainIdForQuotaStatementTestFallsBackToCallingAccountDomainIdWhenNeitherAccountNorDomainIsProvided() { + QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + Account account = Mockito.mock(Account.class); + + Mockito.doReturn(null).when(cmd).getDomainId(); + Mockito.doReturn(123L).when(account).getDomainId(); + Mockito.doReturn(account).when(callContextMock).getCallingAccount(); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, null); + + Assert.assertEquals(123L, result.longValue()); + } + } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java index 259264f3b0e0..a0fe63de8518 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.quota.dao.QuotaAccountDao; import org.apache.cloudstack.quota.dao.QuotaBalanceDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.joda.time.DateTime; @@ -63,6 +64,8 @@ public class QuotaServiceImplTest extends TestCase { @Mock QuotaBalanceDao quotaBalanceDao; @Mock + QuotaUsageJoinDao quotaUsageJoinDaoMock; + @Mock QuotaResponseBuilder respBldr; @Mock private AccountVO accountVoMock; @@ -85,9 +88,9 @@ public void setup() throws IllegalAccessException, NoSuchFieldException, Configu quotaAccountDaoField.setAccessible(true); quotaAccountDaoField.set(quotaServiceImplSpy, quotaAcc); - Field quotaUsageDaoField = QuotaServiceImpl.class.getDeclaredField("_quotaUsageDao"); + Field quotaUsageDaoField = QuotaServiceImpl.class.getDeclaredField("quotaUsageJoinDao"); quotaUsageDaoField.setAccessible(true); - quotaUsageDaoField.set(quotaServiceImplSpy, quotaUsageDao); + quotaUsageDaoField.set(quotaServiceImplSpy, quotaUsageJoinDaoMock); Field domainDaoField = QuotaServiceImpl.class.getDeclaredField("_domainDao"); domainDaoField.setAccessible(true); @@ -142,7 +145,8 @@ public void testGetQuotaUsage() { final Date endDate = new Date(); quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainId, QuotaTypes.IP_ADDRESS, startDate, endDate); - Mockito.verify(quotaUsageDao, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(Date.class), Mockito.any(Date.class)); + Mockito.verify(quotaUsageJoinDaoMock, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(Date.class), Mockito.any(Date.class), Mockito.any()); } @Test From 92d82989e327470c48d3ad2123a93011c0fb5ae3 Mon Sep 17 00:00:00 2001 From: Gean Jair Silva <89494158+GeanJS@users.noreply.github.com> Date: Thu, 30 Apr 2026 05:46:26 -0300 Subject: [PATCH 036/205] Correction of the user responsible for the event (#13066) Co-authored-by: gean.silva --- .../java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 9e7c644cf655..fad2da89cf28 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -1037,7 +1037,8 @@ public ResourceLimitVO updateResourceLimit(Long accountId, Long domainId, Intege ResourceLimitVO limit = _resourceLimitDao.findByOwnerIdAndTypeAndTag(ownerId, ownerType, resourceType, tag); - ActionEventUtils.onActionEvent(caller.getId(), caller.getAccountId(), + Long callingUserId = CallContext.current().getCallingUserId(); + ActionEventUtils.onActionEvent(callingUserId, caller.getAccountId(), caller.getDomainId(), EventTypes.EVENT_RESOURCE_LIMIT_UPDATE, "Resource limit updated. Resource Type: " + resourceType + ", New Value: " + max, ownerResourceId, ownerResourceType.toString()); From 2eef7aa9a272a1346e5caea3c3a84f3f5264afed Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 30 Apr 2026 13:52:39 +0200 Subject: [PATCH 037/205] adding default deny keys also when there are no other keys --- .../extensions/manager/ExtensionsManagerImpl.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java index 1422338ddc99..f6fd08b6da2c 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java @@ -1663,14 +1663,14 @@ public Extension getExtensionForCluster(long clusterId) { public List getExtensionReservedResourceDetails(long extensionId) { ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS); - if (detailsVO == null || !StringUtils.isNotBlank(detailsVO.getValue())) { - return Collections.emptyList(); - } List reservedDetails = new ArrayList<>(); - String[] parts = detailsVO.getValue().split(","); - for (String part : parts) { - if (StringUtils.isNotBlank(part)) { - reservedDetails.add(part.trim()); + if (detailsVO != null && StringUtils.isNotBlank(detailsVO.getValue())) { + String[] parts = detailsVO.getValue().split(","); + for (String part : parts) { + String trimmedPart = part.trim(); + if (StringUtils.isNotBlank(trimmedPart)) { + reservedDetails.add(trimmedPart); + } } } addInbuiltExtensionReservedResourceDetails(extensionId, reservedDetails); From c45596cca3ec5d3c98fd3bdc1fb3bca53a3352a1 Mon Sep 17 00:00:00 2001 From: Bryan Lima <42067040+BryanMLima@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:30:02 +0200 Subject: [PATCH 038/205] Refactor of Allocator classes (#9074) * Refactoring Allocator classes * Break into smaller methods random and firfit allocators. * Added unit tests for random and firstfit allocators * Move random allocator from cloud-plugins to cloud-server * Add BaseAllocator abstract class for duplicate code * Add missing license * Add missing license to unit test file * Remove host allocator random dependency * Change exception message on smoke tests * Remove conditional as it was never actually reached in the original flow * Fix tests * Fix flipped parameters * Fix NPE while listing hosts for migration when suitableHosts is null * Remove unnecessary stubbings * Fix checkstyle * Remove unnecessary file * Rename exception error messages * Apply suggestions from code review Co-authored-by: Fabricio Duarte * Rename UserVmDetailVO references to VMInstanceDetailVO * Remove unused imports * Add new line at EOF * Remove unnecessary random allocator pom * Fix GPU allocation mistake * Fix failing tests --------- Co-authored-by: Fabricio Duarte Co-authored-by: Fabricio Duarte --- .../manager/allocator/HostAllocator.java | 33 - .../com/cloud/deploy/DeploymentPlanner.java | 2 +- .../admin/host/FindHostsForMigrationCmd.java | 2 +- .../api/command/admin/host/ListHostsCmd.java | 2 +- client/pom.xml | 5 - .../com/cloud/vm/VirtualMachineManager.java | 9 - .../cloud/vm/VirtualMachineManagerImpl.java | 18 - .../main/java/com/cloud/host/dao/HostDao.java | 2 +- .../java/com/cloud/host/dao/HostDaoImpl.java | 4 +- plugins/host-allocators/random/pom.xml | 30 - .../allocator/impl/RandomAllocator.java | 196 ----- .../host-allocator-random/module.properties | 18 - .../spring-host-allocator-random-context.xml | 34 - .../allocator/impl/RandomAllocatorTest.java | 80 -- plugins/pom.xml | 2 - .../manager/allocator/impl/BaseAllocator.java | 90 +++ .../allocator/impl/FirstFitAllocator.java | 546 +++++-------- .../allocator/impl/RandomAllocator.java | 132 +++ .../allocator/impl/TestingAllocator.java | 21 +- .../deploy/DeploymentPlanningManagerImpl.java | 22 +- .../cloud/server/ManagementServerImpl.java | 10 +- .../spring-server-allocator-context.xml | 3 + .../allocator/impl/BaseAllocatorTest.java | 219 +++++ .../allocator/impl/FirstFitAllocatorTest.java | 760 +++++++++++++----- .../allocator/impl/RandomAllocatorTest.java | 332 ++++++++ .../smoke/test_vm_strict_host_tags.py | 2 +- 26 files changed, 1593 insertions(+), 981 deletions(-) delete mode 100644 plugins/host-allocators/random/pom.xml delete mode 100644 plugins/host-allocators/random/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java delete mode 100644 plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/module.properties delete mode 100644 plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/spring-host-allocator-random-context.xml delete mode 100644 plugins/host-allocators/random/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java create mode 100644 server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java create mode 100644 server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java create mode 100644 server/src/test/java/com/cloud/agent/manager/allocator/impl/BaseAllocatorTest.java create mode 100644 server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java diff --git a/api/src/main/java/com/cloud/agent/manager/allocator/HostAllocator.java b/api/src/main/java/com/cloud/agent/manager/allocator/HostAllocator.java index 604720aaa290..5d028d31d5b6 100644 --- a/api/src/main/java/com/cloud/agent/manager/allocator/HostAllocator.java +++ b/api/src/main/java/com/cloud/agent/manager/allocator/HostAllocator.java @@ -22,19 +22,11 @@ import com.cloud.deploy.DeploymentPlanner.ExcludeList; import com.cloud.host.Host; import com.cloud.host.Host.Type; -import com.cloud.offering.ServiceOffering; import com.cloud.utils.component.Adapter; -import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; public interface HostAllocator extends Adapter { - /** - * @param UserVm vm - * @param ServiceOffering offering - **/ - boolean isVirtualMachineUpgradable(final VirtualMachine vm, final ServiceOffering offering); - /** * Determines which physical hosts are suitable to * allocate the guest virtual machines on @@ -49,31 +41,6 @@ public interface HostAllocator extends Adapter { public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo); - /** - * Determines which physical hosts are suitable to allocate the guest - * virtual machines on - * - * Allocators must set any other hosts not considered for allocation in the - * ExcludeList avoid. Thus the avoid set and the list of hosts suitable, - * together must cover the entire host set in the cluster. - * - * @param VirtualMachineProfile - * vmProfile - * @param DeploymentPlan - * plan - * @param GuestType - * type - * @param ExcludeList - * avoid - * @param int returnUpTo (use -1 to return all possible hosts) - * @param boolean considerReservedCapacity (default should be true, set to - * false if host capacity calculation should not look at reserved - * capacity) - * @return List List of hosts that are suitable for VM allocation - **/ - - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity); - /** * Determines which physical hosts are suitable to allocate the guest * virtual machines on diff --git a/api/src/main/java/com/cloud/deploy/DeploymentPlanner.java b/api/src/main/java/com/cloud/deploy/DeploymentPlanner.java index 8f7e773070f0..22d796d4a775 100644 --- a/api/src/main/java/com/cloud/deploy/DeploymentPlanner.java +++ b/api/src/main/java/com/cloud/deploy/DeploymentPlanner.java @@ -70,7 +70,7 @@ public interface DeploymentPlanner extends Adapter { boolean canHandle(VirtualMachineProfile vm, DeploymentPlan plan, ExcludeList avoid); public enum AllocationAlgorithm { - random, firstfit, userdispersing; + random, firstfit, userdispersing, firstfitleastconsumed; } public enum PlannerResourceUsage { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java index abca619f82a7..4d6ef7419616 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java @@ -78,7 +78,7 @@ public void execute() { for (Host host : result.first()) { HostForMigrationResponse hostResponse = _responseGenerator.createHostForMigrationResponse(host); Boolean suitableForMigration = false; - if (hostsWithCapacity.contains(host)) { + if (hostsWithCapacity != null && hostsWithCapacity.contains(host)) { suitableForMigration = true; } hostResponse.setSuitableForMigration(suitableForMigration); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java index e202dfad77ba..8f5e6c784d6e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java @@ -252,7 +252,7 @@ protected ListResponse getHostResponses() { for (Host host : result.first()) { HostResponse hostResponse = _responseGenerator.createHostResponse(host, getDetails()); Boolean suitableForMigration = false; - if (hostsWithCapacity.contains(host)) { + if (hostsWithCapacity != null && hostsWithCapacity.contains(host)) { suitableForMigration = true; } hostResponse.setSuitableForMigration(suitableForMigration); diff --git a/client/pom.xml b/client/pom.xml index 7118f455ab5f..55123de0f98e 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -377,11 +377,6 @@ cloud-plugin-explicit-dedication ${project.version} - - org.apache.cloudstack - cloud-plugin-host-allocator-random - ${project.version} - org.apache.cloudstack cloud-plugin-outofbandmanagement-driver-ipmitool diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index 57dc1b7bf728..e871bd8672ff 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -181,15 +181,6 @@ void orchestrateStart(String vmUuid, Map pa void advanceReboot(String vmUuid, Map params) throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException, OperationTimedoutException; - /** - * Check to see if a virtual machine can be upgraded to the given service offering - * - * @param vm - * @param offering - * @return true if the host can handle the upgrade, false otherwise - */ - boolean isVirtualMachineUpgradable(final VirtualMachine vm, final ServiceOffering offering); - VirtualMachine findById(long vmId); void storageMigration(String vmUuid, Map volumeToPool); diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 71ecc73f325f..17ddf8706702 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -4017,19 +4017,6 @@ protected void runInContext() { } } - @Override - public boolean isVirtualMachineUpgradable(final VirtualMachine vm, final ServiceOffering offering) { - boolean isMachineUpgradable = true; - for (final HostAllocator allocator : hostAllocators) { - isMachineUpgradable = allocator.isVirtualMachineUpgradable(vm, offering); - if (!isMachineUpgradable) { - break; - } - } - - return isMachineUpgradable; - } - @Override public void reboot(final String vmUuid, final Map params) throws InsufficientCapacityException, ResourceUnavailableException { try { @@ -4469,11 +4456,6 @@ public void checkIfCanUpgrade(final VirtualMachine vmInstance, final ServiceOffe throw new InvalidParameterValueException("isSystem property is different for current service offering and new service offering"); } - if (!isVirtualMachineUpgradable(vmInstance, newServiceOffering)) { - throw new InvalidParameterValueException("Unable to upgrade virtual machine, not enough resources available " + "for an offering of " + - newServiceOffering.getCpu() + " cpu(s) at " + newServiceOffering.getSpeed() + " Mhz, and " + newServiceOffering.getRamSize() + " MB of memory"); - } - final List currentTags = StringUtils.csvTagsToList(currentDiskOffering.getTags()); final List newTags = StringUtils.csvTagsToList(newDiskOffering.getTags()); if (VolumeApiServiceImpl.MatchStoragePoolTagsWithDiskOffering.valueIn(vmInstance.getDataCenterId())) { diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java index 090b019334f4..5f5b2affee08 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java @@ -218,7 +218,7 @@ int countHostsByMsResourceStateTypeAndHypervisorType(long msId, List listOrderedHostsHypervisorVersionsInDatacenter(long datacenterId, HypervisorType hypervisorType); - List findHostsWithTagRuleThatMatchComputeOferringTags(String computeOfferingTags); + List findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags); List findClustersThatMatchHostTagRule(String computeOfferingTags); diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java index 2d8fcca6cdb7..99c9a979c3bf 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java @@ -1520,7 +1520,7 @@ private List findHostIdsByHostTags(String hostTags){ } } - public List findHostsWithTagRuleThatMatchComputeOferringTags(String computeOfferingTags) { + public List findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags) { List hostTagVOList = _hostTagsDao.findHostRuleTags(); List result = new ArrayList<>(); for (HostTagVO rule: hostTagVOList) { @@ -1534,7 +1534,7 @@ public List findHostsWithTagRuleThatMatchComputeOferringTags(String comp public List findClustersThatMatchHostTagRule(String computeOfferingTags) { Set result = new HashSet<>(); - List hosts = findHostsWithTagRuleThatMatchComputeOferringTags(computeOfferingTags); + List hosts = findHostsWithTagRuleThatMatchComputeOfferingTags(computeOfferingTags); for (HostVO host: hosts) { result.add(host.getClusterId()); } diff --git a/plugins/host-allocators/random/pom.xml b/plugins/host-allocators/random/pom.xml deleted file mode 100644 index 061caf1573ff..000000000000 --- a/plugins/host-allocators/random/pom.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - 4.0.0 - cloud-plugin-host-allocator-random - Apache CloudStack Plugin - Host Allocator Random - - org.apache.cloudstack - cloudstack-plugins - 4.23.0.0-SNAPSHOT - ../../pom.xml - - diff --git a/plugins/host-allocators/random/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java b/plugins/host-allocators/random/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java deleted file mode 100644 index 42129944a194..000000000000 --- a/plugins/host-allocators/random/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java +++ /dev/null @@ -1,196 +0,0 @@ -// 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 com.cloud.agent.manager.allocator.impl; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import javax.inject.Inject; - -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.collections.ListUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; - -import com.cloud.agent.manager.allocator.HostAllocator; -import com.cloud.capacity.CapacityManager; -import com.cloud.dc.ClusterDetailsDao; -import com.cloud.dc.dao.ClusterDao; -import com.cloud.deploy.DeploymentPlan; -import com.cloud.deploy.DeploymentPlanner.ExcludeList; -import com.cloud.host.Host; -import com.cloud.host.Host.Type; -import com.cloud.host.HostVO; -import com.cloud.host.dao.HostDao; -import com.cloud.offering.ServiceOffering; -import com.cloud.resource.ResourceManager; -import com.cloud.storage.VMTemplateVO; -import com.cloud.utils.Pair; -import com.cloud.utils.component.AdapterBase; -import com.cloud.vm.VirtualMachine; -import com.cloud.vm.VirtualMachineProfile; - -@Component -public class RandomAllocator extends AdapterBase implements HostAllocator { - @Inject - private HostDao _hostDao; - @Inject - private ResourceManager _resourceMgr; - @Inject - private ClusterDao clusterDao; - @Inject - private ClusterDetailsDao clusterDetailsDao; - @Inject - private CapacityManager capacityManager; - - protected List listHostsByTags(Host.Type type, long dcId, Long podId, Long clusterId, String offeringHostTag, String templateTag) { - List taggedHosts = new ArrayList<>(); - if (offeringHostTag != null) { - taggedHosts.addAll(_hostDao.listByHostTag(type, clusterId, podId, dcId, offeringHostTag)); - } - if (templateTag != null) { - List templateTaggedHosts = _hostDao.listByHostTag(type, clusterId, podId, dcId, templateTag); - if (taggedHosts.isEmpty()) { - taggedHosts = templateTaggedHosts; - } else { - taggedHosts.retainAll(templateTaggedHosts); - } - } - if (logger.isDebugEnabled()) { - logger.debug(String.format("Found %d hosts %s with type: %s, zone ID: %d, pod ID: %d, cluster ID: %s, offering host tag(s): %s, template tag: %s", - taggedHosts.size(), - (taggedHosts.isEmpty() ? "" : String.format("(%s)", StringUtils.join(taggedHosts.stream().map(HostVO::toString).toArray(), ","))), - type.name(), dcId, podId, clusterId, offeringHostTag, templateTag)); - } - return taggedHosts; - } - private List findSuitableHosts(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, - ExcludeList avoid, List hosts, int returnUpTo, - boolean considerReservedCapacity) { - long dcId = plan.getDataCenterId(); - Long podId = plan.getPodId(); - Long clusterId = plan.getClusterId(); - ServiceOffering offering = vmProfile.getServiceOffering(); - List hostsCopy = null; - List suitableHosts = new ArrayList<>(); - - if (type == Host.Type.Storage) { - return suitableHosts; - } - String offeringHostTag = offering.getHostTag(); - - VMTemplateVO template = (VMTemplateVO)vmProfile.getTemplate(); - String templateTag = template.getTemplateTag(); - String hostTag = null; - if (ObjectUtils.anyNotNull(offeringHostTag, templateTag)) { - hostTag = ObjectUtils.allNotNull(offeringHostTag, templateTag) ? - String.format("%s, %s", offeringHostTag, templateTag) : - ObjectUtils.firstNonNull(offeringHostTag, templateTag); - logger.debug("Looking for hosts in dc [{}], pod [{}], cluster [{}] and complying with host tag(s): [{}]", dcId, podId, clusterId, hostTag); - } else { - logger.debug("Looking for hosts in dc: {} pod: {} cluster: {}", dcId , podId, clusterId); - } - if (hosts != null) { - // retain all computing hosts, regardless of whether they support routing...it's random after all - hostsCopy = new ArrayList<>(hosts); - if (ObjectUtils.anyNotNull(offeringHostTag, templateTag)) { - hostsCopy.retainAll(listHostsByTags(type, dcId, podId, clusterId, offeringHostTag, templateTag)); - } else { - hostsCopy.retainAll(_hostDao.listAllHostsThatHaveNoRuleTag(type, clusterId, podId, dcId)); - } - } else { - // list all computing hosts, regardless of whether they support routing...it's random after all - if (offeringHostTag != null) { - hostsCopy = listHostsByTags(type, dcId, podId, clusterId, offeringHostTag, templateTag); - } else { - hostsCopy = _hostDao.listAllHostsThatHaveNoRuleTag(type, clusterId, podId, dcId); - } - } - hostsCopy = ListUtils.union(hostsCopy, _hostDao.findHostsWithTagRuleThatMatchComputeOferringTags(offeringHostTag)); - - if (hostsCopy.isEmpty()) { - logger.info("No suitable host found for VM [{}] in {}.", vmProfile, hostTag); - return null; - } - - logger.debug("Random Allocator found {} hosts", hostsCopy.size()); - if (hostsCopy.isEmpty()) { - return suitableHosts; - } - - Collections.shuffle(hostsCopy); - for (Host host : hostsCopy) { - if (suitableHosts.size() == returnUpTo) { - break; - } - if (avoid.shouldAvoid(host)) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Host %s is in avoid set, skipping this and trying other available hosts", host)); - } - continue; - } - Pair cpuCapabilityAndCapacity = capacityManager.checkIfHostHasCpuCapabilityAndCapacity(host, offering, considerReservedCapacity); - if (!cpuCapabilityAndCapacity.first() || !cpuCapabilityAndCapacity.second()) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Not using host %s; host has cpu capability? %s, host has capacity? %s", host, cpuCapabilityAndCapacity.first(), cpuCapabilityAndCapacity.second())); - } - continue; - } - if (logger.isDebugEnabled()) { - logger.debug(String.format("Found a suitable host, adding to list: %s", host)); - } - suitableHosts.add(host); - } - if (logger.isDebugEnabled()) { - logger.debug("Random Host Allocator returning " + suitableHosts.size() + " suitable hosts"); - } - return suitableHosts; - } - - @Override - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo) { - return allocateTo(vmProfile, plan, type, avoid, returnUpTo, true); - } - - @Override - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, - ExcludeList avoid, List hosts, int returnUpTo, - boolean considerReservedCapacity) { - if (CollectionUtils.isEmpty(hosts)) { - if (logger.isDebugEnabled()) { - logger.debug("Random Allocator found 0 hosts as given host list is empty"); - } - return new ArrayList<>(); - } - return findSuitableHosts(vmProfile, plan, type, avoid, hosts, returnUpTo, considerReservedCapacity); - } - - @Override - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, - Type type, ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity) { - return findSuitableHosts(vmProfile, plan, type, avoid, null, returnUpTo, considerReservedCapacity); - } - - @Override - public boolean isVirtualMachineUpgradable(VirtualMachine vm, ServiceOffering offering) { - // currently we do no special checks to rule out a VM being upgradable to an offering, so - // return true - return true; - } -} diff --git a/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/module.properties b/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/module.properties deleted file mode 100644 index dcfe8d3537ff..000000000000 --- a/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/module.properties +++ /dev/null @@ -1,18 +0,0 @@ -# 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. -name=host-allocator-random -parent=allocator diff --git a/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/spring-host-allocator-random-context.xml b/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/spring-host-allocator-random-context.xml deleted file mode 100644 index d84eaafaa5a7..000000000000 --- a/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/spring-host-allocator-random-context.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - diff --git a/plugins/host-allocators/random/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java b/plugins/host-allocators/random/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java deleted file mode 100644 index 538d7157184a..000000000000 --- a/plugins/host-allocators/random/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java +++ /dev/null @@ -1,80 +0,0 @@ -// 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 com.cloud.agent.manager.allocator.impl; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.collections.CollectionUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -import com.cloud.host.Host; -import com.cloud.host.HostVO; -import com.cloud.host.dao.HostDao; - -@RunWith(MockitoJUnitRunner.class) -public class RandomAllocatorTest { - - @Mock - HostDao hostDao; - @InjectMocks - RandomAllocator randomAllocator; - - @Test - public void testListHostsByTags() { - Host.Type type = Host.Type.Routing; - Long id = 1L; - String templateTag = "tag1"; - String offeringTag = "tag2"; - HostVO host1 = Mockito.mock(HostVO.class); - HostVO host2 = Mockito.mock(HostVO.class); - Mockito.when(hostDao.listByHostTag(type, id, id, id, offeringTag)).thenReturn(List.of(host1, host2)); - - // No template tagged host - Mockito.when(hostDao.listByHostTag(type, id, id, id, templateTag)).thenReturn(new ArrayList<>()); - List result = randomAllocator.listHostsByTags(type, id, id, id, offeringTag, templateTag); - Assert.assertTrue(CollectionUtils.isEmpty(result)); - - // Different template tagged host - HostVO host3 = Mockito.mock(HostVO.class); - Mockito.when(hostDao.listByHostTag(type, id, id, id, templateTag)).thenReturn(List.of(host3)); - result = randomAllocator.listHostsByTags(type, id, id, id, offeringTag, templateTag); - Assert.assertTrue(CollectionUtils.isEmpty(result)); - - // Matching template tagged host - Mockito.when(hostDao.listByHostTag(type, id, id, id, templateTag)).thenReturn(List.of(host1)); - result = randomAllocator.listHostsByTags(type, id, id, id, offeringTag, templateTag); - Assert.assertFalse(CollectionUtils.isEmpty(result)); - Assert.assertEquals(1, result.size()); - - // No template tag - result = randomAllocator.listHostsByTags(type, id, id, id, offeringTag, null); - Assert.assertFalse(CollectionUtils.isEmpty(result)); - Assert.assertEquals(2, result.size()); - - // No offering tag - result = randomAllocator.listHostsByTags(type, id, id, id, null, templateTag); - Assert.assertFalse(CollectionUtils.isEmpty(result)); - Assert.assertEquals(1, result.size()); - } -} diff --git a/plugins/pom.xml b/plugins/pom.xml index e4904ccdf40b..3262404032fa 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -84,8 +84,6 @@ ha-planners/skip-heurestics - host-allocators/random - hypervisors/baremetal hypervisors/external hypervisors/hyperv diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java new file mode 100644 index 000000000000..58fcc62cdc31 --- /dev/null +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java @@ -0,0 +1,90 @@ +// 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 com.cloud.agent.manager.allocator.impl; + +import com.cloud.agent.manager.allocator.HostAllocator; +import com.cloud.capacity.CapacityManager; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.offering.ServiceOffering; +import com.cloud.utils.Pair; +import com.cloud.utils.component.AdapterBase; +import org.apache.commons.collections.CollectionUtils; + +import javax.inject.Inject; +import java.util.List; + +public abstract class BaseAllocator extends AdapterBase implements HostAllocator { + + @Inject + protected HostDao hostDao; + + @Inject + protected CapacityManager capacityManager; + + protected void retainHostsMatchingServiceOfferingAndTemplateTags(List availableHosts, Host.Type type, long dcId, Long podId, Long clusterId, String offeringHostTag, String templateTag) { + logger.debug("Hosts {} will be checked for template and host tags compatibility.", availableHosts); + + if (offeringHostTag != null) { + logger.debug("Looking for hosts having the tag [{}] specified in the Service Offering.", offeringHostTag); + List hostsWithHostTag = hostDao.listByHostTag(type, clusterId, podId, dcId, offeringHostTag); + logger.debug("Retaining hosts {} because they match the offering host tag {}.", hostsWithHostTag, offeringHostTag); + availableHosts.retainAll(hostsWithHostTag); + } + + if (templateTag != null) { + logger.debug("Looking for hosts having the tag [{}] specified in the Template.", templateTag); + List hostsWithTemplateTag = hostDao.listByHostTag(type, clusterId, podId, dcId, templateTag); + logger.debug("Retaining hosts {} because they match the template tag {}.", hostsWithTemplateTag, templateTag); + availableHosts.retainAll(hostsWithTemplateTag); + } + + logger.debug("Remaining hosts after template tag and host tags validations are {}.", availableHosts); + } + + protected void addHostsBasedOnTagRules(String hostTagOnOffering, List clusterHosts) { + List hostsWithTagRules = hostDao.findHostsWithTagRuleThatMatchComputeOfferingTags(hostTagOnOffering); + + if (CollectionUtils.isEmpty(hostsWithTagRules)) { + logger.info("No hosts found with tag rules matching the compute offering tag [{}].", hostTagOnOffering); + return; + } + + logger.info("Found hosts {} with tag rules matching the compute offering tag [{}].", hostsWithTagRules, hostTagOnOffering); + clusterHosts.addAll(hostsWithTagRules); + } + + /** + * Adds hosts with enough CPU capability and enough CPU capacity to the suitable hosts list. + */ + protected boolean hostHasCpuCapabilityAndCapacity(boolean considerReservedCapacity, ServiceOffering offering, Host host) { + logger.debug("Looking for CPU frequency {} MHz and RAM {} MB.", () -> offering.getCpu() * offering.getSpeed(), offering::getRamSize); + Pair cpuCapabilityAndCapacity = capacityManager.checkIfHostHasCpuCapabilityAndCapacity(host, offering, considerReservedCapacity); + Boolean hasCpuCapability = cpuCapabilityAndCapacity.first(); + Boolean hasCpuCapacity = cpuCapabilityAndCapacity.second(); + + if (hasCpuCapability && hasCpuCapacity) { + logger.debug("Host {} is a suitable host as it has enough CPU capability and CPU capacity.", () -> host); + return true; + } + + logger.debug("Cannot use host {}. Does the host have CPU capability? {}. Does the host have CPU capacity? {}..", () -> host, () -> hasCpuCapability, () -> hasCpuCapacity); + return false; + } + +} diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java index 1110804959ea..8b4ca318bf78 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java @@ -16,36 +16,17 @@ // under the License. package com.cloud.agent.manager.allocator.impl; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import static com.cloud.deploy.DeploymentPlanner.AllocationAlgorithm.firstfitleastconsumed; +import static com.cloud.deploy.DeploymentPlanner.AllocationAlgorithm.random; +import static com.cloud.deploy.DeploymentPlanner.AllocationAlgorithm.userdispersing; -import javax.inject.Inject; -import javax.naming.ConfigurationException; - -import com.cloud.gpu.dao.VgpuProfileDao; - -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.springframework.stereotype.Component; - -import com.cloud.agent.manager.allocator.HostAllocator; import com.cloud.capacity.Capacity; -import com.cloud.capacity.CapacityManager; import com.cloud.capacity.CapacityVO; import com.cloud.capacity.dao.CapacityDao; import com.cloud.configuration.Config; import com.cloud.configuration.ConfigurationManager; -import com.cloud.dc.ClusterDetailsDao; -import com.cloud.dc.dao.ClusterDao; -import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentClusterPlanner; +import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentPlanner.ExcludeList; import com.cloud.deploy.FirstFitPlanner; import com.cloud.gpu.GPU; @@ -53,11 +34,9 @@ import com.cloud.host.Host; import com.cloud.host.Host.Type; import com.cloud.host.HostVO; -import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDetailsDao; import com.cloud.offering.ServiceOffering; import com.cloud.resource.ResourceManager; -import com.cloud.service.ServiceOfferingDetailsVO; import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.storage.GuestOSCategoryVO; import com.cloud.storage.GuestOSVO; @@ -66,22 +45,38 @@ import com.cloud.storage.dao.GuestOSDao; import com.cloud.user.Account; import com.cloud.utils.Pair; -import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.StringUtils; import com.cloud.vm.VMInstanceDetailVO; -import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; -import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; /** * An allocator that tries to find a fit on a computing host. This allocator does not care whether or not the host supports routing. */ @Component -public class FirstFitAllocator extends AdapterBase implements HostAllocator { - @Inject - protected HostDao _hostDao = null; +public class FirstFitAllocator extends BaseAllocator { @Inject HostDetailsDao _hostDetailsDao = null; @Inject @@ -95,289 +90,183 @@ public class FirstFitAllocator extends AdapterBase implements HostAllocator { @Inject protected ResourceManager _resourceMgr; @Inject - ClusterDao _clusterDao; - @Inject - ClusterDetailsDao _clusterDetailsDao; - @Inject ServiceOfferingDetailsDao _serviceOfferingDetailsDao; @Inject - CapacityManager _capacityMgr; - @Inject CapacityDao _capacityDao; @Inject - VMInstanceDetailsDao _vmInstanceDetailsDao; - @Inject - private VgpuProfileDao vgpuProfileDao; + VMInstanceDetailsDao vmInstanceDetailsDao; boolean _checkHvm = true; + static DecimalFormat decimalFormat = new DecimalFormat("#.##"); @Override public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo) { - return allocateTo(vmProfile, plan, type, avoid, returnUpTo, true); + return allocateTo(vmProfile, plan, type, avoid, null, returnUpTo, true); } @Override - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity) { + public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, List hosts, int returnUpTo, + boolean considerReservedCapacity) { + if (type == Host.Type.Storage) { + return null; + } long dcId = plan.getDataCenterId(); Long podId = plan.getPodId(); Long clusterId = plan.getClusterId(); ServiceOffering offering = vmProfile.getServiceOffering(); - VMTemplateVO template = (VMTemplateVO)vmProfile.getTemplate(); + VMTemplateVO template = (VMTemplateVO) vmProfile.getTemplate(); Account account = vmProfile.getOwner(); - boolean isVMDeployedWithUefi = false; - VMInstanceDetailVO vmInstanceDetailVO = _vmInstanceDetailsDao.findDetail(vmProfile.getId(), "UEFI"); - if(vmInstanceDetailVO != null){ - if ("secure".equalsIgnoreCase(vmInstanceDetailVO.getValue()) || "legacy".equalsIgnoreCase(vmInstanceDetailVO.getValue())) { - isVMDeployedWithUefi = true; - } - } - logger.info(" Guest VM is requested with Custom[UEFI] Boot Type "+ isVMDeployedWithUefi); + String hostTagOnOffering = offering.getHostTag(); + String hostTagOnTemplate = template.getTemplateTag(); + String paramAsStringToLog = String.format("zone [%s], pod [%s], cluster [%s]", dcId, podId, clusterId); + List suitableHosts = retrieveHosts(vmProfile, type, (List) hosts, clusterId, podId, dcId, hostTagOnOffering, hostTagOnTemplate); - if (type == Host.Type.Storage) { - // FirstFitAllocator should be used for user VMs only since it won't care whether the host is capable of routing or not - return new ArrayList<>(); + if (suitableHosts.isEmpty()) { + logger.info("No suitable host found for VM [{}] in {}.", vmProfile, paramAsStringToLog); + return null; } - String paramAsStringToLog = String.format("zone [%s], pod [%s], cluster [%s]", dcId, podId, clusterId); - logger.debug("Looking for hosts in {}", paramAsStringToLog); - String hostTagOnOffering = offering.getHostTag(); - String hostTagOnTemplate = template.getTemplateTag(); - String hostTagUefi = "UEFI"; + if (CollectionUtils.isEmpty(hosts)) { + addHostsToAvoidSet(type, avoid, clusterId, podId, dcId, suitableHosts); + } - boolean hasSvcOfferingTag = hostTagOnOffering != null ? true : false; - boolean hasTemplateTag = hostTagOnTemplate != null ? true : false; + return allocateTo(vmProfile, plan, offering, template, avoid, suitableHosts, returnUpTo, considerReservedCapacity, account); + } - List clusterHosts = new ArrayList<>(); - List hostsMatchingUefiTag = new ArrayList<>(); - if(isVMDeployedWithUefi){ - hostsMatchingUefiTag = _hostDao.listByHostCapability(type, clusterId, podId, dcId, Host.HOST_UEFI_ENABLE); - if (logger.isDebugEnabled()) { - logger.debug("Hosts with tag '" + hostTagUefi + "' are:" + hostsMatchingUefiTag); - } - } + protected List retrieveHosts(VirtualMachineProfile vmProfile, Type type, List hostsToFilter, Long clusterId, Long podId, long dcId, String hostTagOnOffering, + String hostTagOnTemplate) { + String haVmTag = (String) vmProfile.getParameter(VirtualMachineProfile.Param.HaTag); + List clusterHosts; + if (CollectionUtils.isNotEmpty(hostsToFilter)) { + clusterHosts = new ArrayList<>(hostsToFilter); + } else { + clusterHosts = _resourceMgr.listAllUpAndEnabledHosts(type, clusterId, podId, dcId); + } - String haVmTag = (String)vmProfile.getParameter(VirtualMachineProfile.Param.HaTag); if (haVmTag != null) { - clusterHosts = _hostDao.listByHostTag(type, clusterId, podId, dcId, haVmTag); + clusterHosts.retainAll(hostDao.listByHostTag(type, clusterId, podId, dcId, haVmTag)); + } else if (ObjectUtils.allNull(hostTagOnOffering, hostTagOnTemplate)) { + clusterHosts.retainAll(_resourceMgr.listAllUpAndEnabledNonHAHosts(type, clusterId, podId, dcId)); } else { - if (hostTagOnOffering == null && hostTagOnTemplate == null) { - clusterHosts = _resourceMgr.listAllUpAndEnabledNonHAHosts(type, clusterId, podId, dcId); - } else { - List hostsMatchingOfferingTag = new ArrayList<>(); - List hostsMatchingTemplateTag = new ArrayList<>(); - if (hasSvcOfferingTag) { - if (logger.isDebugEnabled()) { - logger.debug("Looking for hosts having tag specified on SvcOffering:" + hostTagOnOffering); - } - hostsMatchingOfferingTag = _hostDao.listByHostTag(type, clusterId, podId, dcId, hostTagOnOffering); - if (logger.isDebugEnabled()) { - logger.debug("Hosts with tag '" + hostTagOnOffering + "' are:" + hostsMatchingOfferingTag); - } - } - if (hasTemplateTag) { - if (logger.isDebugEnabled()) { - logger.debug("Looking for hosts having tag specified on Template:" + hostTagOnTemplate); - } - hostsMatchingTemplateTag = _hostDao.listByHostTag(type, clusterId, podId, dcId, hostTagOnTemplate); - if (logger.isDebugEnabled()) { - logger.debug("Hosts with tag '" + hostTagOnTemplate + "' are:" + hostsMatchingTemplateTag); - } - } - - if (hasSvcOfferingTag && hasTemplateTag) { - hostsMatchingOfferingTag.retainAll(hostsMatchingTemplateTag); - if (logger.isDebugEnabled()) { - logger.debug("Found " + hostsMatchingOfferingTag.size() + " Hosts satisfying both tags, host ids are:" + hostsMatchingOfferingTag); - } - - clusterHosts = hostsMatchingOfferingTag; - } else { - if (hasSvcOfferingTag) { - clusterHosts = hostsMatchingOfferingTag; - } else { - clusterHosts = hostsMatchingTemplateTag; - } - } - } + retainHostsMatchingServiceOfferingAndTemplateTags(clusterHosts, type, dcId, podId, clusterId, hostTagOnOffering, hostTagOnTemplate); } - if (isVMDeployedWithUefi) { - clusterHosts.retainAll(hostsMatchingUefiTag); - } + filterHostsWithUefiEnabled(type, vmProfile, clusterId, podId, dcId, clusterHosts); - clusterHosts.addAll(_hostDao.findHostsWithTagRuleThatMatchComputeOferringTags(hostTagOnOffering)); + addHostsBasedOnTagRules(hostTagOnOffering, clusterHosts); + return clusterHosts; - if (clusterHosts.isEmpty()) { - logger.warn("No suitable host found for VM [{}] with tags {} in {}.", vmProfile, hostTagOnOffering, paramAsStringToLog); - return null; - } - // add all hosts that we are not considering to the avoid list - List allhostsInCluster = _hostDao.listAllUpAndEnabledNonHAHosts(type, clusterId, podId, dcId, null); - allhostsInCluster.removeAll(clusterHosts); + } - logger.debug(() -> String.format("Adding hosts [%s] to the avoid set because these hosts do not support HA.", - ReflectionToStringBuilderUtils.reflectOnlySelectedFields(allhostsInCluster, "uuid", "name"))); + /** + * Add all hosts to the avoid set that were not considered during the allocation + */ + protected void addHostsToAvoidSet(Type type, ExcludeList avoid, Long clusterId, Long podId, long dcId, List suitableHosts) { + List allHostsInCluster = hostDao.listAllUpAndEnabledNonHAHosts(type, clusterId, podId, dcId, null); - for (HostVO host : allhostsInCluster) { + allHostsInCluster.removeAll(suitableHosts); + + logger.debug("Adding hosts [{}] to the avoid set because these hosts were not considered for allocation.", + () -> ReflectionToStringBuilderUtils.reflectOnlySelectedFields(allHostsInCluster, "uuid", "name")); + + for (HostVO host : allHostsInCluster) { avoid.addHost(host.getId()); } - - return allocateTo(vmProfile, plan, offering, template, avoid, clusterHosts, returnUpTo, considerReservedCapacity, account); } - @Override - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, List hosts, int returnUpTo, - boolean considerReservedCapacity) { - long dcId = plan.getDataCenterId(); - Long podId = plan.getPodId(); - Long clusterId = plan.getClusterId(); - ServiceOffering offering = vmProfile.getServiceOffering(); - VMTemplateVO template = (VMTemplateVO)vmProfile.getTemplate(); - Account account = vmProfile.getOwner(); - List suitableHosts = new ArrayList<>(); - List hostsCopy = new ArrayList<>(hosts); + protected void filterHostsWithUefiEnabled(Type type, VirtualMachineProfile vmProfile, Long clusterId, Long podId, long dcId, List clusterHosts) { + VMInstanceDetailVO vmInstanceDetailVO = vmInstanceDetailsDao.findDetail(vmProfile.getId(), "UEFI"); - if (type == Host.Type.Storage) { - // FirstFitAllocator should be used for user VMs only since it won't care whether the host is capable of - // routing or not. - return suitableHosts; + if (vmInstanceDetailVO == null) { + return; } - String hostTagOnOffering = offering.getHostTag(); - String hostTagOnTemplate = template.getTemplateTag(); - boolean hasSvcOfferingTag = hostTagOnOffering != null ? true : false; - boolean hasTemplateTag = hostTagOnTemplate != null ? true : false; - - String haVmTag = (String)vmProfile.getParameter(VirtualMachineProfile.Param.HaTag); - if (haVmTag != null) { - hostsCopy.retainAll(_hostDao.listByHostTag(type, clusterId, podId, dcId, haVmTag)); - } else { - if (hostTagOnOffering == null && hostTagOnTemplate == null) { - hostsCopy.retainAll(_resourceMgr.listAllUpAndEnabledNonHAHosts(type, clusterId, podId, dcId)); - } else { - if (hasSvcOfferingTag) { - if (logger.isDebugEnabled()) { - logger.debug("Looking for hosts having tag specified on SvcOffering:" + hostTagOnOffering); - } - hostsCopy.retainAll(_hostDao.listByHostTag(type, clusterId, podId, dcId, hostTagOnOffering)); - - if (logger.isDebugEnabled()) { - logger.debug("Hosts with tag '" + hostTagOnOffering + "' are:" + hostsCopy); - } - } - - if (hasTemplateTag) { - if (logger.isDebugEnabled()) { - logger.debug("Looking for hosts having tag specified on Template:" + hostTagOnTemplate); - } - - hostsCopy.retainAll(_hostDao.listByHostTag(type, clusterId, podId, dcId, hostTagOnTemplate)); - - if (logger.isDebugEnabled()) { - logger.debug("Hosts with tag '" + hostTagOnTemplate + "' are:" + hostsCopy); - } - } - } + if (!StringUtils.equalsAnyIgnoreCase(vmInstanceDetailVO.getValue(), ApiConstants.BootMode.SECURE.toString(), ApiConstants.BootMode.LEGACY.toString())) { + return; } - hostsCopy.addAll(_hostDao.findHostsWithTagRuleThatMatchComputeOferringTags(hostTagOnOffering)); + logger.info("Guest VM is requested with Custom[UEFI] Boot Type enabled."); - if (!hostsCopy.isEmpty()) { - suitableHosts = allocateTo(vmProfile, plan, offering, template, avoid, hostsCopy, returnUpTo, considerReservedCapacity, account); - } + List hostsMatchingUefiTag = hostDao.listByHostCapability(type, clusterId, podId, dcId, Host.HOST_UEFI_ENABLE); - return suitableHosts; + logger.debug("Hosts with UEFI enabled are {}.", hostsMatchingUefiTag); + clusterHosts.retainAll(hostsMatchingUefiTag); } protected List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, ServiceOffering offering, VMTemplateVO template, ExcludeList avoid, List hosts, int returnUpTo, - boolean considerReservedCapacity, Account account) { + boolean considerReservedCapacity, Account account) { String vmAllocationAlgorithm = DeploymentClusterPlanner.VmAllocationAlgorithm.value(); - if (vmAllocationAlgorithm.equals("random")) { - // Shuffle this so that we don't check the hosts in the same order. + if (random.toString().equals(vmAllocationAlgorithm)) { Collections.shuffle(hosts); - } else if (vmAllocationAlgorithm.equals("userdispersing")) { + } else if (userdispersing.toString().equals(vmAllocationAlgorithm)) { hosts = reorderHostsByNumberOfVms(plan, hosts, account); - } else if(vmAllocationAlgorithm.equals("firstfitleastconsumed")){ + } else if (firstfitleastconsumed.toString().equals(vmAllocationAlgorithm)) { hosts = reorderHostsByCapacity(plan, hosts); } - if (logger.isDebugEnabled()) { - logger.debug("FirstFitAllocator has " + hosts.size() + " hosts to check for allocation: " + hosts); - } - - // We will try to reorder the host lists such that we give priority to hosts that have - // the minimums to support a VM's requirements + logger.debug("FirstFitAllocator has {} hosts to check for allocation {}.", hosts.size(), hosts); hosts = prioritizeHosts(template, offering, hosts); + logger.debug("Found {} hosts for allocation after prioritization: {}.", hosts.size(), hosts); - if (logger.isDebugEnabled()) { - logger.debug("Found " + hosts.size() + " hosts for allocation after prioritization: " + hosts); - } + List suitableHosts = checkHostsCompatibilities(offering, vmProfile, avoid, hosts, returnUpTo, considerReservedCapacity); + logger.debug("Host Allocator returning {} suitable hosts.", suitableHosts.size()); - if (logger.isDebugEnabled()) { - logger.debug("Looking for speed=" + (offering.getCpu() * offering.getSpeed()) + "Mhz, Ram=" + offering.getRamSize() + " MB"); - } + return suitableHosts; + } - long serviceOfferingId = offering.getId(); + protected List checkHostsCompatibilities(ServiceOffering offering, VirtualMachineProfile vmProfile, ExcludeList avoid, List hosts, int returnUpTo, boolean considerReservedCapacity) { List suitableHosts = new ArrayList<>(); - ServiceOfferingDetailsVO offeringDetails = null; + logger.debug("Checking compatibility for the following hosts {}.", suitableHosts); for (Host host : hosts) { if (suitableHosts.size() == returnUpTo) { break; } + if (avoid.shouldAvoid(host)) { - if (logger.isDebugEnabled()) { - logger.debug("Host: {} is in avoid set, skipping this and trying other available hosts", host); - } + logger.debug("Host [{}] is in avoid set, skipping this and trying other available hosts", host); continue; } - //find number of guest VMs occupying capacity on this host. - if (_capacityMgr.checkIfHostReachMaxGuestLimit(host)) { + if (capacityManager.checkIfHostReachMaxGuestLimit(host)) { logger.debug("Adding host [{}] to the avoid set because this host already has the max number of running (user and/or system) VMs.", host); avoid.addHost(host.getId()); continue; } - // Check if GPU device is required by offering and host has the availability - if (_resourceMgr.isGPUDeviceAvailable(offering, host, vmProfile.getId())) { - logger.debug("Host [{}] has required GPU devices available.", host); - } else { - // If GPU is not available, skip this host - logger.debug("Adding host [{}] to avoid set, because this host does not have required GPU devices available.", host); - avoid.addHost(host.getId()); + if (offeringRequestedVGpuAndHostDoesNotHaveIt(offering, vmProfile, avoid, host)) { continue; } - Pair cpuCapabilityAndCapacity = _capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(host, offering, considerReservedCapacity); - if (cpuCapabilityAndCapacity.first() && cpuCapabilityAndCapacity.second()) { - if (logger.isDebugEnabled()) { - logger.debug("Found a suitable host, adding to list: {}", host); - } + if (hostHasCpuCapabilityAndCapacity(considerReservedCapacity, offering, host)) { suitableHosts.add(host); - } else { - if (logger.isDebugEnabled()) { - logger.debug("Not using host {}; host has cpu capability? {}, host has capacity?{}", - host, cpuCapabilityAndCapacity.first(), cpuCapabilityAndCapacity.second()); - } - avoid.addHost(host.getId()); + continue; } + avoid.addHost(host.getId()); } + return suitableHosts; + } - if (logger.isDebugEnabled()) { - logger.debug("Host Allocator returning " + suitableHosts.size() + " suitable hosts"); - } + protected boolean offeringRequestedVGpuAndHostDoesNotHaveIt(ServiceOffering offering, VirtualMachineProfile vmProfile, ExcludeList avoid, Host host) { + if (_resourceMgr.isGPUDeviceAvailable(offering, host, vmProfile.getId())) { + logger.debug("Host [{}] has required GPU devices available.", () -> host); + return false; + } - return suitableHosts; + logger.debug("Adding host [{}] to avoid set, because this host does not have required GPU devices available.", () -> host); + avoid.addHost(host.getId()); + return true; } - // Reorder hosts in the decreasing order of free capacity. + /** + * Reorder hosts in the decreasing order of free capacity. + */ private List reorderHostsByCapacity(DeploymentPlan plan, List hosts) { Long zoneId = plan.getDataCenterId(); Long clusterId = plan.getClusterId(); @@ -388,26 +277,10 @@ private List reorderHostsByCapacity(DeploymentPlan plan, List decimalFormat.format(entry.getValue() * 100) + "%", (e1, e2) -> e1, LinkedHashMap::new)); - if (logger.isDebugEnabled()) { - logger.debug("List of hosts: [{}] in descending order of free capacity (percentage) in the cluster: {}", - hostIdsByFreeCapacity, sortedHostByCapacity); - } - - //now filter the given list of Hosts by this ordered list - Map hostMap = new HashMap<>(); - for (Host host : hosts) { - hostMap.put(host.getId(), host); - } - List matchingHostIds = new ArrayList<>(hostMap.keySet()); - - hostIdsByFreeCapacity.retainAll(matchingHostIds); - - List reorderedHosts = new ArrayList<>(); - for(Long id: hostIdsByFreeCapacity){ - reorderedHosts.add(hostMap.get(id)); - } + logger.debug("List of hosts: [{}] in descending order of free capacity (percentage) in the cluster: {}.", + hostIdsByFreeCapacity, sortedHostByCapacity); - return reorderedHosts; + return filterHosts(hosts, hostIdsByFreeCapacity); } private Pair, Map> getOrderedHostsByCapacity(Long zoneId, Long clusterId) { @@ -450,116 +323,154 @@ private List reorderHostsByNumberOfVms(DeploymentPlan plan, List Long clusterId = plan.getClusterId(); List hostIdsByVmCount = _vmInstanceDao.listHostIdsByVmCount(dcId, podId, clusterId, account.getAccountId()); - if (logger.isDebugEnabled()) { - logger.debug("List of hosts in ascending order of number of VMs: " + hostIdsByVmCount); - } + logger.debug("List of hosts in ascending order of number of VMs: {}.", hostIdsByVmCount); - //now filter the given list of Hosts by this ordered list + return filterHosts(hosts, hostIdsByVmCount); + } + + /** + * Filter the given list of Hosts considering the ordered list + */ + private List filterHosts(List hosts, List orderedHostIdsList) { Map hostMap = new HashMap<>(); + for (Host host : hosts) { hostMap.put(host.getId(), host); } List matchingHostIds = new ArrayList<>(hostMap.keySet()); - - hostIdsByVmCount.retainAll(matchingHostIds); + orderedHostIdsList.retainAll(matchingHostIds); List reorderedHosts = new ArrayList<>(); - for (Long id : hostIdsByVmCount) { + for(Long id: orderedHostIdsList){ reorderedHosts.add(hostMap.get(id)); } return reorderedHosts; } - @Override - public boolean isVirtualMachineUpgradable(VirtualMachine vm, ServiceOffering offering) { - // currently we do no special checks to rule out a VM being upgradable to an offering, so - // return true - return true; - } - + /** + * Reorder the host list giving priority to hosts that have the minimum to support the VM's requirements. + */ protected List prioritizeHosts(VMTemplateVO template, ServiceOffering offering, List hosts) { if (template == null) { return hosts; } - // Determine the guest OS category of the template - String templateGuestOSCategory = getTemplateGuestOSCategory(template); + List hostsToCheck = filterHostWithNoHvmIfTemplateRequested(template, hosts); List prioritizedHosts = new ArrayList<>(); - List noHvmHosts = new ArrayList<>(); + List highPriorityHosts = new ArrayList<>(); + List lowPriorityHosts = new ArrayList<>(); + + prioritizeHostsWithMatchingGuestOs(template, hostsToCheck, highPriorityHosts, lowPriorityHosts); + hostsToCheck.removeAll(highPriorityHosts); + hostsToCheck.removeAll(lowPriorityHosts); + + prioritizeHostsByHvmCapability(template, hostsToCheck, prioritizedHosts); + prioritizedHosts.addAll(0, highPriorityHosts); + prioritizedHosts.addAll(lowPriorityHosts); + + prioritizeHostsByGpuEnabled(offering, prioritizedHosts); + + return prioritizedHosts; + } + - // If a template requires HVM and a host doesn't support HVM, remove it from consideration + /** + * If a template requires HVM and a host doesn't support HVM, remove it from consideration. + */ + protected List filterHostWithNoHvmIfTemplateRequested(VMTemplateVO template, List hosts) { List hostsToCheck = new ArrayList<>(); - if (template.isRequiresHvm()) { - for (Host host : hosts) { - if (hostSupportsHVM(host)) { - hostsToCheck.add(host); - } else { - noHvmHosts.add(host); - } - } - } else { + + if (!template.isRequiresHvm()) { + logger.debug("Template [{}] does not require HVM, therefore, the hosts {} will not be checked for HVM compatibility.", template, hostsToCheck); hostsToCheck.addAll(hosts); + return hostsToCheck; } - if (logger.isDebugEnabled()) { - if (noHvmHosts.size() > 0) { - logger.debug("Not considering hosts: " + noHvmHosts + " to deploy template: " + template + " as they are not HVM enabled"); + List noHvmHosts = new ArrayList<>(); + logger.debug("Template [{}] requires HVM, therefore, the hosts %s will be checked for HVM compatibility.", template, hostsToCheck); + + for (Host host : hosts) { + if (hostSupportsHVM(host)) { + hostsToCheck.add(host); + } else { + noHvmHosts.add(host); } } - // If a host is tagged with the same guest OS category as the template, move it to a high priority list - // If a host is tagged with a different guest OS category than the template, move it to a low priority list - List highPriorityHosts = new ArrayList<>(); - List lowPriorityHosts = new ArrayList<>(); - for (Host host : hostsToCheck) { - String hostGuestOSCategory = getHostGuestOSCategory(host); - if (hostGuestOSCategory == null) { - continue; - } else if (templateGuestOSCategory != null && templateGuestOSCategory.equals(hostGuestOSCategory)) { - highPriorityHosts.add(host); - } else { - lowPriorityHosts.add(host); + + if (!noHvmHosts.isEmpty()) { + logger.debug("Not considering hosts {} to deploy VM using template {} as they are not HVM enabled.", noHvmHosts, template); + } + + return hostsToCheck; + } + + + /** + * If service offering did not request for vGPU, then move all host with GPU to the end of the host priority list. + */ + protected void prioritizeHostsByGpuEnabled(ServiceOffering offering, List prioritizedHosts) { + boolean serviceOfferingRequestedVGpu = _serviceOfferingDetailsDao.findDetail(offering.getId(), GPU.Keys.vgpuType.toString()) != null || offering.getVgpuProfileId() != null; + + if (serviceOfferingRequestedVGpu) { + return; + } + + List gpuEnabledHosts = new ArrayList<>(); + + for (Host host : prioritizedHosts) { + if (_resourceMgr.isHostGpuEnabled(host.getId())) { + gpuEnabledHosts.add(host); } } - hostsToCheck.removeAll(highPriorityHosts); - hostsToCheck.removeAll(lowPriorityHosts); + if (!gpuEnabledHosts.isEmpty()) { + prioritizedHosts.removeAll(gpuEnabledHosts); + prioritizedHosts.addAll(gpuEnabledHosts); + } + } - // Prioritize the remaining hosts by HVM capability + /** + * Prioritize remaining host by HVM capability. + * + *
    + *
  • If host and template both do not support HVM, put it at the start of the list.
  • + *
  • If the template doesn't require HVM, but the machine supports it, append it to the list.
  • + *
+ */ + protected void prioritizeHostsByHvmCapability(VMTemplateVO template, List hostsToCheck, List prioritizedHosts) { for (Host host : hostsToCheck) { if (!template.isRequiresHvm() && !hostSupportsHVM(host)) { - // Host and template both do not support hvm, put it as first consideration prioritizedHosts.add(0, host); } else { - // Template doesn't require hvm, but the machine supports it, make it last for consideration prioritizedHosts.add(host); } } + } - // Merge the lists - prioritizedHosts.addAll(0, highPriorityHosts); - prioritizedHosts.addAll(lowPriorityHosts); - // if service offering is not GPU enabled then move all the GPU enabled hosts to the end of priority list. - if (_serviceOfferingDetailsDao.findDetail(offering.getId(), GPU.Keys.vgpuType.toString()) == null && offering.getVgpuProfileId() == null) { + /** + *
    + *
  • If a host is tagged with the same guest OS category as the template, move it to a high priority list.
  • + *
  • If a host is tagged with a different guest OS category than the template, move it to a low priority list.
  • + *
+ */ + protected void prioritizeHostsWithMatchingGuestOs(VMTemplateVO template, List hostsToCheck, List highPriorityHosts, List lowPriorityHosts) { + String templateGuestOSCategory = getTemplateGuestOSCategory(template); - List gpuEnabledHosts = new ArrayList<>(); - // Check for GPU enabled hosts. - for (Host host : prioritizedHosts) { - if (_resourceMgr.isHostGpuEnabled(host.getId())) { - gpuEnabledHosts.add(host); - } - } - // Move GPU enabled hosts to the end of list - if(!gpuEnabledHosts.isEmpty()) { - prioritizedHosts.removeAll(gpuEnabledHosts); - prioritizedHosts.addAll(gpuEnabledHosts); + for (Host host : hostsToCheck) { + String hostGuestOSCategory = getHostGuestOSCategory(host); + + if (StringUtils.equals(templateGuestOSCategory, hostGuestOSCategory)) { + highPriorityHosts.add(host); + } else if (hostGuestOSCategory != null) { + lowPriorityHosts.add(host); } } - return prioritizedHosts; } + protected boolean hostSupportsHVM(Host host) { if (!_checkHvm) { return true; @@ -621,19 +532,8 @@ public boolean configure(String name, Map params) throws Configu if (_configDao != null) { Map configs = _configDao.getConfiguration(params); String value = configs.get("xenserver.check.hvm"); - _checkHvm = value == null ? true : Boolean.parseBoolean(value); + _checkHvm = value == null || Boolean.parseBoolean(value); } return true; } - - @Override - public boolean start() { - return true; - } - - @Override - public boolean stop() { - return true; - } - } diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java new file mode 100644 index 000000000000..96b4255b876d --- /dev/null +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java @@ -0,0 +1,132 @@ +// 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 com.cloud.agent.manager.allocator.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.stereotype.Component; + +import com.cloud.deploy.DeploymentPlan; +import com.cloud.deploy.DeploymentPlanner.ExcludeList; +import com.cloud.host.Host; +import com.cloud.host.Host.Type; +import com.cloud.host.HostVO; +import com.cloud.offering.ServiceOffering; +import com.cloud.resource.ResourceManager; +import com.cloud.storage.VMTemplateVO; +import com.cloud.vm.VirtualMachineProfile; + +@Component +public class RandomAllocator extends BaseAllocator { + @Inject + private ResourceManager _resourceMgr; + + protected List findSuitableHosts(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, List hosts, int returnUpTo, + boolean considerReservedCapacity) { + if (type == Host.Type.Storage) { + return null; + } + + long dcId = plan.getDataCenterId(); + Long podId = plan.getPodId(); + Long clusterId = plan.getClusterId(); + ServiceOffering offering = vmProfile.getServiceOffering(); + + String offeringHostTag = offering.getHostTag(); + VMTemplateVO template = (VMTemplateVO) vmProfile.getTemplate(); + logger.debug("Looking for hosts in zone [{}], pod [{}], cluster [{}].", dcId, podId, clusterId); + + List availableHosts = retrieveHosts(type, (List) hosts, template, offeringHostTag, clusterId, podId, dcId); + + if (availableHosts.isEmpty()) { + logger.info("No suitable host found for VM [{}] in zone [{}], pod [{}], cluster [{}].", vmProfile, dcId, podId, clusterId); + return null; + } + + return filterAvailableHosts(avoid, returnUpTo, considerReservedCapacity, availableHosts, offering); + } + + protected List filterAvailableHosts(ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity, List availableHosts, ServiceOffering offering) { + logger.debug("Random Allocator found [{}] available hosts. They will be checked if they are in the avoid set and for CPU capability and capacity.", availableHosts::size); + List suitableHosts = new ArrayList<>(); + + Collections.shuffle(availableHosts); + for (Host host : availableHosts) { + if (suitableHosts.size() == returnUpTo) { + break; + } + + if (avoid.shouldAvoid(host)) { + logger.debug("Host [{}] is in the avoid set, skipping it and trying other available hosts.", () -> host); + continue; + } + + if (!hostHasCpuCapabilityAndCapacity(considerReservedCapacity, offering, host)) { + continue; + } + + logger.debug("Found the suitable host [{}], adding to list.", () -> host); + suitableHosts.add(host); + } + + logger.debug("Random Host Allocator returning {} suitable hosts.", suitableHosts::size); + return suitableHosts; + } + + /** + * @return all computing hosts, regardless of whether they support routing. + */ + protected List retrieveHosts(Type type, List hosts, VMTemplateVO template, String offeringHostTag, Long clusterId, Long podId, long dcId) { + List availableHosts; + String templateTag = template.getTemplateTag(); + + if (CollectionUtils.isNotEmpty(hosts)) { + availableHosts = new ArrayList<>(hosts); + } else { + availableHosts = _resourceMgr.listAllUpAndEnabledHosts(type, clusterId, podId, dcId); + } + + if (ObjectUtils.anyNotNull(offeringHostTag, templateTag)) { + retainHostsMatchingServiceOfferingAndTemplateTags(availableHosts, type, dcId, podId, clusterId, offeringHostTag, templateTag); + } else { + List hostsWithNoRuleTag = hostDao.listAllHostsThatHaveNoRuleTag(type, clusterId, podId, dcId); + logger.debug("Retaining hosts {} because they do not have rule tags.", hostsWithNoRuleTag); + availableHosts.retainAll(hostsWithNoRuleTag); + } + + addHostsBasedOnTagRules(offeringHostTag, availableHosts); + + return availableHosts; + } + + @Override + public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo) { + return allocateTo(vmProfile, plan, type, avoid, null, returnUpTo, true); + } + + @Override + public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, List hosts, int returnUpTo, + boolean considerReservedCapacity) { + return findSuitableHosts(vmProfile, plan, type, avoid, hosts, returnUpTo, considerReservedCapacity); + } +} diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/TestingAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/TestingAllocator.java index fd8e65e24761..04a0b182b7b1 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/TestingAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/TestingAllocator.java @@ -28,32 +28,24 @@ import com.cloud.host.Host; import com.cloud.host.Host.Type; import com.cloud.host.dao.HostDao; -import com.cloud.offering.ServiceOffering; import com.cloud.utils.component.AdapterBase; -import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; public class TestingAllocator extends AdapterBase implements HostAllocator { @Inject HostDao _hostDao; - Long _computingHost; Long _storageHost; Long _routingHost; @Override public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo) { - return allocateTo(vmProfile, plan, type, avoid, returnUpTo, true); + return allocateTo(vmProfile, plan, type, avoid, null, returnUpTo, true); } @Override public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, List hosts, int returnUpTo, - boolean considerReservedCapacity) { - return allocateTo(vmProfile, plan, type, avoid, returnUpTo, considerReservedCapacity); - } - - @Override - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity) { - List availableHosts = new ArrayList(); + boolean considerReservedCapacity) { + List availableHosts = new ArrayList<>(); Host host = null; if (type == Host.Type.Routing && _routingHost != null) { host = _hostDao.findById(_routingHost); @@ -66,13 +58,6 @@ public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan pla return availableHosts; } - @Override - public boolean isVirtualMachineUpgradable(VirtualMachine vm, ServiceOffering offering) { - // currently we do no special checks to rule out a VM being upgradable to an offering, so - // return true - return true; - } - @Override public boolean configure(String name, Map params) { String value = (String)params.get(Host.Type.Routing.toString()); diff --git a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java index f163a3d52a5a..7fb4a97e149b 100644 --- a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java +++ b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -1714,20 +1713,19 @@ protected List findSuitableHosts(VirtualMachineProfile vmProfile, Deployme @Override public void reorderHostsByPriority(Map priorities, List hosts) { - logger.debug("Re-ordering hosts {} by priorities {}", hosts, priorities); + if (CollectionUtils.isEmpty(hosts)){ + logger.debug("Hosts list is empty; therefore, there is nothing to reorder."); + return; + } + logger.info("Re-ordering hosts [{}] by priorities [{}].", hosts, priorities); hosts.removeIf(host -> DataCenterDeployment.PROHIBITED_HOST_PRIORITY.equals(getHostPriority(priorities, host.getId()))); + hosts.sort((host1, host2) -> { + int res = getHostPriority(priorities, host1.getId()).compareTo(getHostPriority(priorities, host2.getId())); + return -res; + }); - Collections.sort(hosts, new Comparator<>() { - @Override - public int compare(Host host1, Host host2) { - int res = getHostPriority(priorities, host1.getId()).compareTo(getHostPriority(priorities, host2.getId())); - return -res; - } - } - ); - - logger.debug("Hosts after re-ordering are: {}", hosts); + logger.info("Hosts after re-ordering are: [{}].", hosts); } private Integer getHostPriority(Map priorities, Long hostId) { diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index d4266496e982..337bdbbbc639 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -1696,11 +1696,7 @@ protected List getCapableSuitableHosts( List suitableHosts = new ArrayList<>(); for (final HostAllocator allocator : hostAllocators) { - if (CollectionUtils.isNotEmpty(compatibleHosts)) { - suitableHosts = allocator.allocateTo(vmProfile, plan, Host.Type.Routing, excludes, compatibleHosts, HostAllocator.RETURN_UPTO_ALL, false); - } else { - suitableHosts = allocator.allocateTo(vmProfile, plan, Host.Type.Routing, excludes, HostAllocator.RETURN_UPTO_ALL, false); - } + suitableHosts = allocator.allocateTo(vmProfile, plan, Host.Type.Routing, excludes, compatibleHosts, HostAllocator.RETURN_UPTO_ALL, false); if (CollectionUtils.isNotEmpty(suitableHosts)) { break; @@ -1709,10 +1705,10 @@ protected List getCapableSuitableHosts( _dpMgr.reorderHostsByPriority(plan.getHostPriorities(), suitableHosts); - if (suitableHosts.isEmpty()) { + if (CollectionUtils.isEmpty(suitableHosts)) { logger.warn("No suitable hosts found."); } else { - logger.debug("Hosts having capacity and suitable for migration: {}", suitableHosts); + logger.debug("Hosts having capacity and are suitable for migration: {}", suitableHosts); } // Only list hosts of the same architecture as the source Host in a multi-arch zone diff --git a/server/src/main/resources/META-INF/cloudstack/server-allocator/spring-server-allocator-context.xml b/server/src/main/resources/META-INF/cloudstack/server-allocator/spring-server-allocator-context.xml index d3781649845b..28b96b8d194b 100644 --- a/server/src/main/resources/META-INF/cloudstack/server-allocator/spring-server-allocator-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/server-allocator/spring-server-allocator-context.xml @@ -34,6 +34,9 @@ + + diff --git a/server/src/test/java/com/cloud/agent/manager/allocator/impl/BaseAllocatorTest.java b/server/src/test/java/com/cloud/agent/manager/allocator/impl/BaseAllocatorTest.java new file mode 100644 index 000000000000..ddd1e3959d51 --- /dev/null +++ b/server/src/test/java/com/cloud/agent/manager/allocator/impl/BaseAllocatorTest.java @@ -0,0 +1,219 @@ +// 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 com.cloud.agent.manager.allocator.impl; + +import com.cloud.capacity.CapacityManager; +import com.cloud.deploy.DeploymentPlan; +import com.cloud.deploy.DeploymentPlanner; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.offering.ServiceOffering; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.utils.Pair; +import com.cloud.vm.VirtualMachineProfile; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(MockitoJUnitRunner.class) +public class BaseAllocatorTest { + @Mock + HostDao hostDaoMock; + + @Mock + CapacityManager capacityManagerMock; + + @InjectMocks + @Spy + BaseAllocator baseAllocator = new MockBaseAllocator(); + + private final Host.Type type = Host.Type.Routing; + + private final Long clusterId = 1L; + + private final Long podId = 2L; + + private final Long dcId = 3L; + + private final HostVO host1 = Mockito.mock(HostVO.class); + + private final HostVO host2 = Mockito.mock(HostVO.class); + + private final HostVO host3 = Mockito.mock(HostVO.class); + + private final ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class); + + private final String hostTag = "hostTag"; + + @Test + public void retainHostsMatchingServiceOfferingAndTemplateTagsTestHasServiceOfferingTagShouldRetainHostsWithServiceOfferingTag() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List hostsWithMathingTags = new ArrayList<>(Arrays.asList(host1, host3)); + String hostTagOnTemplate = "hostTagOnTemplate"; + String hostTagOnOffering = null; + + Mockito.doReturn(hostsWithMathingTags).when(hostDaoMock).listByHostTag(type, clusterId, podId, dcId, hostTagOnTemplate); + baseAllocator.retainHostsMatchingServiceOfferingAndTemplateTags(suitableHosts, type, dcId, podId, clusterId, hostTagOnOffering, hostTagOnTemplate); + + Assert.assertEquals(2, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + Assert.assertEquals(host3, suitableHosts.get(1)); + } + + @Test + public void retainHostsMatchingServiceOfferingAndTemplateTagsTestHasServiceOfferingTagAndHasHostTagOnTemplateShouldRetainHostsWithServiceOfferingTagAndTemplateTag() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List hostsWithMathingServiceTags = new ArrayList<>(Arrays.asList(host1, host3)); + List hostsWithMathingTemplateTags = new ArrayList<>(Arrays.asList(host1, host2)); + String hostTagOnTemplate = "hostTagOnTemplate"; + String hostTagOnOffering = "hostTagOnOffering"; + + Mockito.doReturn(hostsWithMathingTemplateTags).when(hostDaoMock).listByHostTag(type, clusterId, podId, dcId, hostTagOnTemplate); + Mockito.doReturn(hostsWithMathingServiceTags).when(hostDaoMock).listByHostTag(type, clusterId, podId, dcId, hostTagOnOffering); + baseAllocator.retainHostsMatchingServiceOfferingAndTemplateTags(suitableHosts, type, dcId, podId, clusterId, hostTagOnOffering, hostTagOnTemplate); + + Assert.assertEquals(1, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + } + + @Test + public void retainHostsMatchingServiceOfferingAndTemplateTagsTestHasHostTagOnTemplateShouldRetainHostsWithTemplateTag() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List hostsWithMathingServiceTags = new ArrayList<>(Arrays.asList(host1, host3)); + String hostTagOnTemplate = null; + String hostTagOnOffering = "hostTagOnOffering"; + + Mockito.doReturn(hostsWithMathingServiceTags).when(hostDaoMock).listByHostTag(type, clusterId, podId, dcId, hostTagOnOffering); + baseAllocator.retainHostsMatchingServiceOfferingAndTemplateTags(suitableHosts, type, dcId, podId, clusterId, hostTagOnOffering, hostTagOnTemplate); + + Assert.assertEquals(2, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + Assert.assertEquals(host3, suitableHosts.get(1)); + } + + @Test + public void retainHostsMatchingServiceOfferingAndTemplateTagsTestNoServiceTagAndNoTemplateTagShouldHaveAllSuitableHosts() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + String hostTagOnTemplate = null; + String hostTagOnOffering = null; + + baseAllocator.retainHostsMatchingServiceOfferingAndTemplateTags(suitableHosts, type, dcId, podId, clusterId, hostTagOnOffering, hostTagOnTemplate); + + Assert.assertEquals(3, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + Assert.assertEquals(host2, suitableHosts.get(1)); + Assert.assertEquals(host3, suitableHosts.get(2)); + } + + @Test + public void addHostsBasedOnTagRulesTestHostsWithTagRuleIsEmptyShouldNotAddToSuitableHosts() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List emptyList = new ArrayList<>(); + + Mockito.doReturn(emptyList).when(hostDaoMock).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.anyString()); + baseAllocator.addHostsBasedOnTagRules(hostTag, suitableHosts); + + Assert.assertEquals(2, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + Assert.assertEquals(host2, suitableHosts.get(1)); + } + + @Test + public void addHostsBasedOnTagRulesTestHostsWithTagRuleIsNotEmptyShouldAddToSuitableHosts() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List hostsMatchingRuleTag = new ArrayList<>(Arrays.asList(host3)); + + Mockito.doReturn(hostsMatchingRuleTag).when(hostDaoMock).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.anyString()); + baseAllocator.addHostsBasedOnTagRules(hostTag, suitableHosts); + + Assert.assertEquals(3, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + Assert.assertEquals(host2, suitableHosts.get(1)); + Assert.assertEquals(host3, suitableHosts.get(2)); + } + + @Test + public void hostHasCpuCapabilityAndCapacityTestHostHasCpuCapabilityAndCpuCapacityShouldReturnTrue() { + Boolean hasCpuCapability = true; + Boolean hasCpuCapacity = true; + Pair pair = new Pair<>(hasCpuCapability, hasCpuCapacity); + + Mockito.doReturn(pair).when(capacityManagerMock).checkIfHostHasCpuCapabilityAndCapacity(Mockito.any(Host.class), Mockito.any(ServiceOffering.class), Mockito.anyBoolean()); + boolean result = baseAllocator.hostHasCpuCapabilityAndCapacity(true, serviceOffering, host1); + + Assert.assertTrue(result); + } + + @Test + public void hostHasCpuCapabilityAndCapacityTestHostHasCpuCapabilityButNoCpuCapacityShouldReturnFalse() { + Boolean hasCpuCapability = true; + Boolean hasCpuCapacity = false; + Pair pair = new Pair<>(hasCpuCapability, hasCpuCapacity); + + Mockito.doReturn(pair).when(capacityManagerMock).checkIfHostHasCpuCapabilityAndCapacity(Mockito.any(Host.class), Mockito.any(ServiceOffering.class), Mockito.anyBoolean()); + boolean result = baseAllocator.hostHasCpuCapabilityAndCapacity(true, serviceOffering, host1); + + Assert.assertFalse(result); + } + + @Test + public void hostHasCpuCapabilityAndCapacityTestHostDoesNotHaveCpuCapabilityButHasCpuCapacityShouldReturnFalse() { + Boolean hasCpuCapability = false; + Boolean hasCpuCapacity = true; + Pair pair = new Pair<>(hasCpuCapability, hasCpuCapacity); + + Mockito.doReturn(pair).when(capacityManagerMock).checkIfHostHasCpuCapabilityAndCapacity(Mockito.any(Host.class), Mockito.any(ServiceOffering.class), Mockito.anyBoolean()); + boolean result = baseAllocator.hostHasCpuCapabilityAndCapacity(true, serviceOffering, host1); + + Assert.assertFalse(result); + } + + @Test + public void hostHasCpuCapabilityAndCapacityTestHostDoesNotHaveCpuCapabilityAndCpuCapacityShouldReturnFalse() { + Boolean hasCpuCapability = false; + Boolean hasCpuCapacity = false; + Pair pair = new Pair<>(hasCpuCapability, hasCpuCapacity); + + Mockito.doReturn(pair).when(capacityManagerMock).checkIfHostHasCpuCapabilityAndCapacity(Mockito.any(Host.class), Mockito.any(ServiceOffering.class), Mockito.anyBoolean()); + boolean result = baseAllocator.hostHasCpuCapabilityAndCapacity(true, serviceOffering, host1); + + Assert.assertFalse(result); + } + + class MockBaseAllocator extends BaseAllocator { + + @Override + public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Host.Type type, DeploymentPlanner.ExcludeList avoid, int returnUpTo) { + return null; + } + + @Override + public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Host.Type type, DeploymentPlanner.ExcludeList avoid, List hosts, int returnUpTo, boolean considerReservedCapacity) { + return null; + } + } +} diff --git a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java index 0d6a6fa8ff92..00cac5bbd52c 100644 --- a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java +++ b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java @@ -18,214 +18,596 @@ package com.cloud.agent.manager.allocator.impl; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.cloud.agent.manager.allocator.HostAllocator; import com.cloud.capacity.CapacityManager; import com.cloud.capacity.CapacityVO; import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentPlanner; import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; import com.cloud.offering.ServiceOffering; import com.cloud.resource.ResourceManager; +import com.cloud.service.ServiceOfferingDetailsVO; +import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDetailsDao; +import com.cloud.storage.VMTemplateVO; import com.cloud.user.Account; import com.cloud.utils.Pair; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.dao.VMInstanceDetailsDao; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; - +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) +public class FirstFitAllocatorTest { + private static final double TOLERANCE = 0.0001; + @Mock + HostDao hostDaoMock; -public class FirstFitAllocatorTest { - private static final double TOLERANCE = 0.0001; - private FirstFitAllocator allocator; - private CapacityManager capacityMgr; - private ServiceOfferingDetailsDao offeringDetailsDao; - private ResourceManager resourceMgr; - - private DeploymentPlan plan; - private ServiceOffering offering; - private DeploymentPlanner.ExcludeList avoid; - private Account account; - - private Host host1; - private Host host2; - private VirtualMachineProfile vmProfile; - ConfigurationDao configDao; - - @Before - public void setUp() { - allocator = new FirstFitAllocator(); - capacityMgr = mock(CapacityManager.class); - offeringDetailsDao = mock(ServiceOfferingDetailsDao.class); - resourceMgr = mock(ResourceManager.class); - configDao = mock(ConfigurationDao.class); - - allocator._capacityMgr = capacityMgr; - allocator._serviceOfferingDetailsDao = offeringDetailsDao; - allocator._resourceMgr = resourceMgr; - allocator._configDao = configDao; - - plan = mock(DeploymentPlan.class); - offering = mock(ServiceOffering.class); - avoid = mock(DeploymentPlanner.ExcludeList.class); - account = mock(Account.class); - - host1 = mock(Host.class); - host2 = mock(Host.class); - - vmProfile = mock(VirtualMachineProfile.class); - when(vmProfile.getId()).thenReturn(1L); - - when(plan.getDataCenterId()).thenReturn(1L); - when(offering.getCpu()).thenReturn(2); - when(offering.getSpeed()).thenReturn(1000); - when(offering.getRamSize()).thenReturn(2048); - when(offering.getId()).thenReturn(123L); - when(offering.getHostTag()).thenReturn(null); - when(offering.getVgpuProfileId()).thenReturn(null); - } - - @Test - public void testConfigure() throws Exception { - when(configDao.getConfiguration(anyMap())).thenReturn(new HashMap<>()); - assertTrue(allocator._checkHvm); - assertTrue(allocator.configure("test", new HashMap<>())); - } - - @Test - public void testAllocateTo_SuccessfulMatch() { - List inputHosts = Arrays.asList(host1, host2); - - // All hosts are allowed - when(avoid.shouldAvoid(host1)).thenReturn(false); - when(avoid.shouldAvoid(host2)).thenReturn(false); - - // No GPU requirement - when(offeringDetailsDao.findDetail(eq(123L), anyString())).thenReturn(null); - - // CPU capability and capacity is met - when(capacityMgr.checkIfHostReachMaxGuestLimit(any())).thenReturn(false); - when(capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(eq(host1), eq(offering), eq(true))) - .thenReturn(new Pair<>(true, true)); - when(capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(eq(host2), eq(offering), eq(true))) - .thenReturn(new Pair<>(true, false)); - - - when(resourceMgr.isGPUDeviceAvailable(offering, host1, vmProfile.getId())).thenReturn(true); - when(resourceMgr.isGPUDeviceAvailable(offering, host2, vmProfile.getId())).thenReturn(true); - - List result = allocator.allocateTo(vmProfile, plan, offering, null, avoid, inputHosts, 2, true, account); - - // Only host1 should be returned - assertEquals(1, result.size()); - assertTrue(result.contains(host1)); - assertFalse(result.contains(host2)); - } - - @Test - public void testAllocateTo_AvoidSetAndGuestLimit() { - List inputHosts = Arrays.asList(host1, host2); - - when(avoid.shouldAvoid(host1)).thenReturn(true); // Avoided - when(avoid.shouldAvoid(host2)).thenReturn(false); - - when(capacityMgr.checkIfHostReachMaxGuestLimit(host2)).thenReturn(true); // Reached limit - - List result = allocator.allocateTo(vmProfile, plan, offering, null, avoid, inputHosts, 2, true, account); - - assertTrue(result.isEmpty()); - } - - @Test - public void testAllocateTo_GPUNotAvailable() { - List inputHosts = Arrays.asList(host1); - when(avoid.shouldAvoid(host1)).thenReturn(false); - - // GPU required but not available - var vgpuDetail = mock(com.cloud.service.ServiceOfferingDetailsVO.class); - var pciDetail = mock(com.cloud.service.ServiceOfferingDetailsVO.class); - when(offeringDetailsDao.findDetail(eq(123L), eq("vgpuType"))).thenReturn(vgpuDetail); - when(offeringDetailsDao.findDetail(eq(123L), eq("pciDevice"))).thenReturn(pciDetail); - when(pciDetail.getValue()).thenReturn("NVIDIA"); - when(vgpuDetail.getValue()).thenReturn("GRID"); - - when(resourceMgr.isGPUDeviceAvailable(offering, host1, vmProfile.getId())).thenReturn(false); - - List result = allocator.allocateTo(vmProfile, plan, offering, null, avoid, inputHosts, 1, true, account); - - assertTrue(result.isEmpty()); - } - - @Test - public void testHostByCombinedCapacityOrder() { - // Test scenario 1: Default capacity usage (0.5 weight) - List mockCapacity = getHostCapacities(); - Map hostByCombinedCapacity = FirstFitAllocator.getHostByCombinedCapacities(mockCapacity, 0.5); - - // Verify host ordering and capacity values - Long firstHostId = hostByCombinedCapacity.keySet().iterator().next(); - Assert.assertEquals("Host with ID 1 should be first in ordering", Long.valueOf(1L), firstHostId); - Assert.assertEquals("Host 1 combined capacity should match expected value", - 0.9609375, hostByCombinedCapacity.get(1L), TOLERANCE); - Assert.assertEquals("Host 2 combined capacity should match expected value", - 0.9296875, hostByCombinedCapacity.get(2L), TOLERANCE); - - // Test scenario 2: Modified capacity usage (0.7 weight) - when(mockCapacity.get(0).getUsedCapacity()).thenReturn(1500L); - hostByCombinedCapacity = FirstFitAllocator.getHostByCombinedCapacities(mockCapacity, 0.7); - - // Verify new ordering after capacity change - firstHostId = hostByCombinedCapacity.keySet().iterator().next(); - Assert.assertEquals("Host with ID 2 should be first after capacity change", Long.valueOf(2L), firstHostId); - Assert.assertEquals("Host 2 combined capacity should match expected value after change", - 0.9515625, hostByCombinedCapacity.get(2L), TOLERANCE); - Assert.assertEquals("Host 1 combined capacity should match expected value after change", - 0.9484375, hostByCombinedCapacity.get(1L), TOLERANCE); - } - - List getHostCapacities() { - CapacityVO cpuCapacity1 = mock(CapacityVO.class); - when(cpuCapacity1.getHostOrPoolId()).thenReturn(1L); - when(cpuCapacity1.getTotalCapacity()).thenReturn(32000L); - when(cpuCapacity1.getReservedCapacity()).thenReturn(0L); - when(cpuCapacity1.getUsedCapacity()).thenReturn(500L); - when(cpuCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); - - CapacityVO cpuCapacity2 = mock(CapacityVO.class); - when(cpuCapacity2.getHostOrPoolId()).thenReturn(2L); - when(cpuCapacity2.getTotalCapacity()).thenReturn(32000L); - when(cpuCapacity2.getReservedCapacity()).thenReturn(0L); - when(cpuCapacity2.getUsedCapacity()).thenReturn(500L); - when(cpuCapacity2.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); - - CapacityVO memCapacity1 = mock(CapacityVO.class); - when(memCapacity1.getHostOrPoolId()).thenReturn(1L); - when(memCapacity1.getTotalCapacity()).thenReturn(8589934592L); - when(memCapacity1.getReservedCapacity()).thenReturn(0L); - when(memCapacity1.getUsedCapacity()).thenReturn(536870912L); - when(memCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); - - CapacityVO memCapacity2 = mock(CapacityVO.class); - when(memCapacity2.getHostOrPoolId()).thenReturn(2L); - when(memCapacity2.getTotalCapacity()).thenReturn(8589934592L); - when(memCapacity2.getReservedCapacity()).thenReturn(0L); - when(memCapacity2.getUsedCapacity()).thenReturn(1073741824L); - when(memCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); - return Arrays.asList(cpuCapacity1, memCapacity1, cpuCapacity2, memCapacity2); - } + @Mock + ResourceManager resourceManagerMock; + + @Mock + VMInstanceDetailsDao userVmDetailsDaoMock; + + @Mock + ServiceOfferingDetailsDao serviceOfferingDetailsDao; + + @Mock + CapacityManager capacityMgr; + + @Mock + ConfigurationDao configDao; + + @Spy + @InjectMocks + FirstFitAllocator firstFitAllocatorSpy; + + private final Host.Type type = Host.Type.Routing; + + private final Long clusterId = 1L; + + private final Long podId = 2L; + + private final Long dcId = 3L; + + private final List emptyList = new ArrayList<>(); + + private final String hostTag = "hostTag"; + + private final String templateTag = "templateTag"; + + private final HostVO host1 = mock(HostVO.class); + + private final HostVO host2 = mock(HostVO.class); + + private final HostVO host3 = mock(HostVO.class); + + private final ServiceOfferingVO serviceOffering = mock(ServiceOfferingVO.class); + + private final DeploymentPlanner.ExcludeList excludeList = mock(DeploymentPlanner.ExcludeList.class); + + private final VirtualMachineProfile virtualMachineProfile = mock(VirtualMachineProfile.class); + + private final VMTemplateVO vmTemplateVO = mock(VMTemplateVO.class); + + private final Account account = mock(Account.class); + + private final DeploymentPlan deploymentPlan = mock(DeploymentPlan.class); + + private final DeploymentPlanner.ExcludeList avoid = mock(DeploymentPlanner.ExcludeList.class); + + private final ServiceOffering offering = mock(ServiceOffering.class); + + private final boolean considerReservedCapacity = true; + + @Test + public void testConfigure() throws Exception { + when(configDao.getConfiguration(Mockito.anyMap())).thenReturn(new HashMap<>()); + Assert.assertTrue(firstFitAllocatorSpy._checkHvm); + Assert.assertTrue(firstFitAllocatorSpy.configure("test", new HashMap<>())); + } + + @Test + public void testAllocateTo_SuccessfulMatch() { + List inputHosts = Arrays.asList(host1, host2); + + // All hosts are allowed + when(avoid.shouldAvoid(host1)).thenReturn(false); + when(avoid.shouldAvoid(host2)).thenReturn(false); + + // CPU capability and capacity is met + when(capacityMgr.checkIfHostReachMaxGuestLimit(Mockito.any(Host.class))).thenReturn(false); + when(capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(host1, offering, true)) + .thenReturn(new Pair<>(true, true)); + when(capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(host2, offering,true)) + .thenReturn(new Pair<>(true, false)); + + when(resourceManagerMock.isGPUDeviceAvailable(offering, host1, virtualMachineProfile.getId())).thenReturn(true); + when(resourceManagerMock.isGPUDeviceAvailable(offering, host2, virtualMachineProfile.getId())).thenReturn(true); + + List result = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, offering, null, avoid, inputHosts, 2, true, account); + + // Only host1 should be returned + Assert.assertEquals(1, result.size()); + Assert.assertTrue(result.contains(host1)); + Assert.assertFalse(result.contains(host2)); + } + + @Test + public void testAllocateTo_AvoidSetAndGuestLimit() { + List inputHosts = Arrays.asList(host1, host2); + + when(avoid.shouldAvoid(host1)).thenReturn(true); // Avoided + when(avoid.shouldAvoid(host2)).thenReturn(false); + + when(capacityMgr.checkIfHostReachMaxGuestLimit(host2)).thenReturn(true); // Reached limit + + List result = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, offering, null, avoid, inputHosts, 2, true, account); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testAllocateTo_GPUNotAvailable() { + List inputHosts = Arrays.asList(host1); + when(avoid.shouldAvoid(host1)).thenReturn(false); + when(resourceManagerMock.isGPUDeviceAvailable(offering, host1, virtualMachineProfile.getId())).thenReturn(false); + + List result = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, offering, null, avoid, inputHosts, 1, true, account); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void allocateToTestHostTypeStorageShouldReturnNull() { + List suitableHosts = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, Host.Type.Storage, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); + + Assert.assertNull(suitableHosts); + } + + @Test + public void allocateToTestSuitableHostsEmptyShouldReturnNull() { + Mockito.doReturn(serviceOffering).when(virtualMachineProfile).getServiceOffering(); + Mockito.doReturn(vmTemplateVO).when(virtualMachineProfile).getTemplate(); + Mockito.doReturn(account).when(virtualMachineProfile).getOwner(); + Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); + Mockito.doReturn(templateTag).when(vmTemplateVO).getTemplateTag(); + Mockito.doReturn(emptyList).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + List suitableHosts = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); + + Assert.assertNull(suitableHosts); + } + + @Test + public void allocateToTestSuitableHostsNotEmptyShouldCallAllocateToMethod() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2)); + + Mockito.doReturn(serviceOffering).when(virtualMachineProfile).getServiceOffering(); + Mockito.doReturn(vmTemplateVO).when(virtualMachineProfile).getTemplate(); + Mockito.doReturn(account).when(virtualMachineProfile).getOwner(); + Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); + Mockito.doReturn(templateTag).when(vmTemplateVO).getTemplateTag(); + Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(hosts).when(firstFitAllocatorSpy).allocateTo(Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class), Mockito.any(ServiceOffering.class), Mockito.any(VMTemplateVO.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(firstFitAllocatorSpy).addHostsToAvoidSet(Mockito.any(Host.Type.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); + List suitableHosts = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); + + Mockito.verify(firstFitAllocatorSpy, Mockito.times(1)).allocateTo(virtualMachineProfile, deploymentPlan, serviceOffering, vmTemplateVO, excludeList, hosts, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity, account); + Assert.assertEquals(2, suitableHosts.size()); + } + + @Test + public void allocateToTestProvidedHostsNotNullShouldCallAddHostsToAvoidSetMethod() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2)); + + Mockito.doReturn(serviceOffering).when(virtualMachineProfile).getServiceOffering(); + Mockito.doReturn(vmTemplateVO).when(virtualMachineProfile).getTemplate(); + Mockito.doReturn(account).when(virtualMachineProfile).getOwner(); + Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); + Mockito.doReturn(templateTag).when(vmTemplateVO).getTemplateTag(); + Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(hosts).when(firstFitAllocatorSpy).allocateTo(Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class), Mockito.any(ServiceOffering.class), Mockito.any(VMTemplateVO.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean(), Mockito.any(Account.class)); + Mockito.doNothing().when(firstFitAllocatorSpy).addHostsToAvoidSet(Mockito.any(Host.Type.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); + firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); + + Mockito.verify(firstFitAllocatorSpy, Mockito.times(1)).addHostsToAvoidSet(Mockito.any(Host.Type.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); + } + + @Test + public void retrieveHostsTestHostsToFilterIsNullAndHaTagNotNullShouldReturnOnlyHostsWithHaTag() { + List allUpAndEnabledHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List hostsWithHaTag = new ArrayList<>(Arrays.asList(host1, host2)); + String hostVmTag = "haVmTag"; + + Mockito.doReturn(hostVmTag).when(virtualMachineProfile).getParameter(Mockito.any(VirtualMachineProfile.Param.class)); + Mockito.doReturn(allUpAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(hostsWithHaTag).when(hostDaoMock).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); + Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); + List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, clusterId, podId, dcId, hostTag, templateTag); + + Assert.assertEquals(2, resultHosts.size()); + Assert.assertEquals(host1, resultHosts.get(0)); + Assert.assertEquals(host2, resultHosts.get(1)); + } + + @Test + public void retrieveHostsTestHostsToFilterIsNotNullAndHaTagNotNullShouldReturnOnlyHostsToFilterWithHaTag() { + List hostsToFilter = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List hostsWithHaTag = new ArrayList<>(Arrays.asList(host1, host2)); + String hostVmTag = "haVmTag"; + + Mockito.doReturn(hostVmTag).when(virtualMachineProfile).getParameter(Mockito.any(VirtualMachineProfile.Param.class)); + Mockito.doReturn(hostsWithHaTag).when(hostDaoMock).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); + Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); + List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, hostsToFilter, clusterId, podId, dcId, hostTag, templateTag); + + Assert.assertEquals(2, resultHosts.size()); + Assert.assertEquals(host1, resultHosts.get(0)); + Assert.assertEquals(host2, resultHosts.get(1)); + } + + @Test + public void retrieveHostsTestHostsToFilterIsNullAndNoHaTagAndNoHostTagShouldReturnOnlyAllUpAndEnabledNonHaHosts() { + List allUpAndEnabledHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List upAndEnabledHostsWithNoHa = new ArrayList<>(Arrays.asList(host1, host2)); + + Mockito.doReturn(allUpAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(upAndEnabledHostsWithNoHa).when(resourceManagerMock).listAllUpAndEnabledNonHAHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); + Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.nullable(String.class), Mockito.anyList()); + List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, clusterId, podId, dcId, null, null); + + Assert.assertEquals(2, resultHosts.size()); + Assert.assertEquals(host1, resultHosts.get(0)); + Assert.assertEquals(host2, resultHosts.get(1)); + } + + @Test + public void retrieveHostsTestHostsToFilterIsNullAndNoHaTagWithHostTagShouldCallRetainHostsMatchingServiceOfferingAndTemplateTags() { + List allUpAndEnabledHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + + Mockito.doReturn(allUpAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doNothing().when(firstFitAllocatorSpy).retainHostsMatchingServiceOfferingAndTemplateTags(Mockito.anyList(), Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); + Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); + firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, clusterId, podId, dcId, hostTag, templateTag); + + Mockito.verify(firstFitAllocatorSpy, Mockito.times(1)).retainHostsMatchingServiceOfferingAndTemplateTags(Mockito.anyList(), Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void addHostsToAvoidSetTestAllHostsWereConsideredForAllocationShouldNotAddAnyHostToTheAvoidSet() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + + Mockito.doReturn(suitableHosts).when(hostDaoMock).listAllUpAndEnabledNonHAHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.nullable(String.class)); + firstFitAllocatorSpy.addHostsToAvoidSet(type, excludeList, clusterId, podId, dcId, suitableHosts); + + Assert.assertTrue(excludeList.getHostsToAvoid().isEmpty()); + } + + @Test + public void addHostsToAvoidSetTestNotAllHostsWereConsideredForAllocationShouldAddHostToTheAvoidSet() { + List allUpAndEnabledNonHAHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List consideredHosts = new ArrayList<>(Arrays.asList(host2, host3)); + + Mockito.doReturn(1L).when(host1).getId(); + Mockito.doCallRealMethod().when(excludeList).addHost(Mockito.anyLong()); + Mockito.doCallRealMethod().when(excludeList).getHostsToAvoid(); + Mockito.doReturn(allUpAndEnabledNonHAHosts).when(hostDaoMock).listAllUpAndEnabledNonHAHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.nullable(String.class)); + firstFitAllocatorSpy.addHostsToAvoidSet(type, excludeList, clusterId, podId, dcId, consideredHosts); + + Assert.assertEquals(1, excludeList.getHostsToAvoid().size()); + Assert.assertTrue(excludeList.getHostsToAvoid().contains(1L)); + } + + @Test + public void filterHostsWithUefiEnabledTestNoDetailWithUefiShouldNotFilterAnyHost() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + VMInstanceDetailVO userVmDetailVO = null; + + Mockito.doReturn(userVmDetailVO).when(userVmDetailsDaoMock).findDetail(Mockito.anyLong(), Mockito.anyString()); + firstFitAllocatorSpy.filterHostsWithUefiEnabled(type, virtualMachineProfile, clusterId, podId, dcId, suitableHosts); + + Assert.assertEquals(3, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + Assert.assertEquals(host2, suitableHosts.get(1)); + Assert.assertEquals(host3, suitableHosts.get(2)); + } + + @Test + public void filterHostsWithUefiEnabledTestDetailWithUefiWithInvalidModeShouldNotFilterAnyHost() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + VMInstanceDetailVO userVmDetailVO = mock(VMInstanceDetailVO.class); + String bootMode = "Invalid mode"; + + Mockito.doReturn(bootMode).when(userVmDetailVO).getValue(); + Mockito.doReturn(userVmDetailVO).when(userVmDetailsDaoMock).findDetail(Mockito.anyLong(), Mockito.anyString()); + firstFitAllocatorSpy.filterHostsWithUefiEnabled(type, virtualMachineProfile, clusterId, podId, dcId, suitableHosts); + + Assert.assertEquals(3, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + Assert.assertEquals(host2, suitableHosts.get(1)); + Assert.assertEquals(host3, suitableHosts.get(2)); + } + + @Test + public void filterHostsWithUefiEnabledTestDetailWithUefiWithLegacyModeShouldFilterHost() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List uefiHosts = new ArrayList<>(Arrays.asList(host2, host3)); + VMInstanceDetailVO userVmDetailVO = mock(VMInstanceDetailVO.class); + String bootMode = ApiConstants.BootMode.LEGACY.toString(); + + Mockito.doReturn(bootMode).when(userVmDetailVO).getValue(); + Mockito.doReturn(userVmDetailVO).when(userVmDetailsDaoMock).findDetail(Mockito.anyLong(), Mockito.anyString()); + Mockito.doReturn(uefiHosts).when(hostDaoMock).listByHostCapability(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); + firstFitAllocatorSpy.filterHostsWithUefiEnabled(type, virtualMachineProfile, clusterId, podId, dcId, suitableHosts); + + Assert.assertEquals(2, suitableHosts.size()); + Assert.assertEquals(host2, suitableHosts.get(0)); + Assert.assertEquals(host3, suitableHosts.get(1)); + } + + @Test + public void filterHostsWithUefiEnabledTestDetailWithUefiWithSecureModeShouldFilterHost() { + List suitableHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List uefiHosts = new ArrayList<>(Arrays.asList(host2, host3)); + VMInstanceDetailVO userVmDetailVO = mock(VMInstanceDetailVO.class); + String bootMode = ApiConstants.BootMode.SECURE.toString(); + + Mockito.doReturn(bootMode).when(userVmDetailVO).getValue(); + Mockito.doReturn(userVmDetailVO).when(userVmDetailsDaoMock).findDetail(Mockito.anyLong(), Mockito.anyString()); + Mockito.doReturn(uefiHosts).when(hostDaoMock).listByHostCapability(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); + firstFitAllocatorSpy.filterHostsWithUefiEnabled(type, virtualMachineProfile, clusterId, podId, dcId, suitableHosts); + + Assert.assertEquals(2, suitableHosts.size()); + Assert.assertEquals(host2, suitableHosts.get(0)); + Assert.assertEquals(host3, suitableHosts.get(1)); + } + + @Test + public void offeringRequestedVGpuAndHostDoesNotHaveItTestVGpuRequestedButHostDoesNotHaveItShouldReturnTrue() { + Mockito.doReturn(1L).when(host1).getId(); + Mockito.doCallRealMethod().when(excludeList).addHost(Mockito.anyLong()); + Mockito.doCallRealMethod().when(excludeList).getHostsToAvoid(); + Mockito.doReturn(false).when(resourceManagerMock).isGPUDeviceAvailable(Mockito.any(ServiceOffering.class), Mockito.any(Host.class), Mockito.any(Long.class)); + boolean result = firstFitAllocatorSpy.offeringRequestedVGpuAndHostDoesNotHaveIt(serviceOffering, virtualMachineProfile, excludeList, host1); + + Assert.assertTrue(result); + Assert.assertEquals(1, excludeList.getHostsToAvoid().size()); + Assert.assertTrue(excludeList.getHostsToAvoid().contains(1L)); + } + + @Test + public void offeringRequestedVGpuAndHostDoesNotHaveItTestVGpuRequestedAndHostDoesHaveItShouldReturnFalse() { + Mockito.doReturn(true).when(resourceManagerMock).isGPUDeviceAvailable(Mockito.any(ServiceOffering.class), Mockito.any(Host.class), Mockito.any(Long.class)); + boolean result = firstFitAllocatorSpy.offeringRequestedVGpuAndHostDoesNotHaveIt(serviceOffering, virtualMachineProfile, excludeList, host1); + + Assert.assertFalse(result); + Mockito.verify(excludeList, Mockito.never()).addHost(Mockito.anyLong()); + } + + @Test + public void filterHostWithNoHvmIfTemplateRequestedTestTemplateDoesNotRequireHvm() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + + Mockito.doReturn(false).when(vmTemplateVO).isRequiresHvm(); + List suitableHosts = firstFitAllocatorSpy.filterHostWithNoHvmIfTemplateRequested(vmTemplateVO, hosts); + + Assert.assertEquals(3, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + Assert.assertEquals(host2, suitableHosts.get(1)); + Assert.assertEquals(host3, suitableHosts.get(2)); + } + + @Test + public void filterHostWithNoHvmIfTemplateRequestedTestTemplateRequiresHvmShouldReturnOnlyHvmHosts() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + + Mockito.doReturn(true).when(vmTemplateVO).isRequiresHvm(); + Mockito.doReturn(true).when(firstFitAllocatorSpy).hostSupportsHVM(host1); + Mockito.doReturn(false).when(firstFitAllocatorSpy).hostSupportsHVM(host2); + Mockito.doReturn(true).when(firstFitAllocatorSpy).hostSupportsHVM(host3); + List suitableHosts = firstFitAllocatorSpy.filterHostWithNoHvmIfTemplateRequested(vmTemplateVO, hosts); + + Assert.assertEquals(2, suitableHosts.size()); + Assert.assertEquals(host1, suitableHosts.get(0)); + Assert.assertEquals(host3, suitableHosts.get(1)); + } + + @Test + public void prioritizeHostsByGpuEnabledTestServiceOfferingRequestedVGpuViaDetailShouldDoNothing() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + ServiceOfferingDetailsVO requestedVGpuType = mock(ServiceOfferingDetailsVO.class); + + Mockito.doReturn(requestedVGpuType).when(serviceOfferingDetailsDao).findDetail(Mockito.anyLong(), Mockito.anyString()); + firstFitAllocatorSpy.prioritizeHostsByGpuEnabled(serviceOffering, hosts); + + Assert.assertEquals(3, hosts.size()); + Assert.assertEquals(host1, hosts.get(0)); + Assert.assertEquals(host2, hosts.get(1)); + Assert.assertEquals(host3, hosts.get(2)); + } + + @Test + public void prioritizeHostsByGpuEnabledTestServiceOfferingRequestedVGpuViaProfileIdShouldDoNothing() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + + Mockito.doReturn(1L).when(serviceOffering).getVgpuProfileId(); + firstFitAllocatorSpy.prioritizeHostsByGpuEnabled(serviceOffering, hosts); + + Assert.assertEquals(3, hosts.size()); + Assert.assertEquals(host1, hosts.get(0)); + Assert.assertEquals(host2, hosts.get(1)); + Assert.assertEquals(host3, hosts.get(2)); + } + + @Test + public void prioritizeHostsByGpuEnabledTestServiceOfferingDidNotRequestVGpuShouldReorderList() { + List allHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + + Mockito.doReturn(null).when(serviceOfferingDetailsDao).findDetail(Mockito.anyLong(), Mockito.anyString()); + Mockito.doReturn(null).when(serviceOffering).getVgpuProfileId(); + Mockito.doReturn(1L).when(host1).getId(); + Mockito.doReturn(2L).when(host2).getId(); + Mockito.doReturn(3L).when(host3).getId(); + Mockito.doReturn(true).when(resourceManagerMock).isHostGpuEnabled(1L); + Mockito.doReturn(false).when(resourceManagerMock).isHostGpuEnabled(2L); + Mockito.doReturn(false).when(resourceManagerMock).isHostGpuEnabled(3L); + firstFitAllocatorSpy.prioritizeHostsByGpuEnabled(serviceOffering, allHosts); + + Assert.assertEquals(3, allHosts.size()); + Assert.assertEquals(host2, allHosts.get(0)); + Assert.assertEquals(host3, allHosts.get(1)); + Assert.assertEquals(host1, allHosts.get(2)); + } + + @Test + public void prioritizeHostsByGpuEnabledTestServiceOfferingDidNotRequestVGpuShouldNotReorderListIfThereIsNoHostWithVGpu() { + List allHosts = new ArrayList<>(Arrays.asList(host1, host2, host3)); + + Mockito.doReturn(null).when(serviceOfferingDetailsDao).findDetail(Mockito.anyLong(), Mockito.anyString()); + Mockito.doReturn(null).when(serviceOffering).getVgpuProfileId(); + Mockito.doReturn(1L).when(host1).getId(); + Mockito.doReturn(2L).when(host2).getId(); + Mockito.doReturn(3L).when(host3).getId(); + Mockito.doReturn(false).when(resourceManagerMock).isHostGpuEnabled(Mockito.anyLong()); + firstFitAllocatorSpy.prioritizeHostsByGpuEnabled(serviceOffering, allHosts); + + Assert.assertEquals(3, allHosts.size()); + Assert.assertEquals(host1, allHosts.get(0)); + Assert.assertEquals(host2, allHosts.get(1)); + Assert.assertEquals(host3, allHosts.get(2)); + } + + @Test + public void prioritizeHostsByHvmCapabilityTestTemplateDidNotRequestedHvmShouldPutHostThatDoesNotSupportHvmInStartOfThePriorityList() { + List hostsToCheck = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List prioritizedHosts = new ArrayList<>(); + + Mockito.doReturn(false).when(vmTemplateVO).isRequiresHvm(); + Mockito.doReturn(true).when(firstFitAllocatorSpy).hostSupportsHVM(host1); + Mockito.doReturn(false).when(firstFitAllocatorSpy).hostSupportsHVM(host2); + Mockito.doReturn(true).when(firstFitAllocatorSpy).hostSupportsHVM(host3); + firstFitAllocatorSpy.prioritizeHostsByHvmCapability(vmTemplateVO, hostsToCheck, prioritizedHosts); + + Assert.assertEquals(3, prioritizedHosts.size()); + Assert.assertEquals(host2, prioritizedHosts.get(0)); + Assert.assertEquals(host1, prioritizedHosts.get(1)); + Assert.assertEquals(host3, prioritizedHosts.get(2)); + } + + @Test + public void prioritizeHostsByHvmCapabilityTestTemplateRequiresHvmShouldNotReorderList() { + List hostsToCheck = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List prioritizedHosts = new ArrayList<>(); + + Mockito.doReturn(true).when(vmTemplateVO).isRequiresHvm(); + firstFitAllocatorSpy.prioritizeHostsByHvmCapability(vmTemplateVO, hostsToCheck, prioritizedHosts); + + Assert.assertEquals(3, prioritizedHosts.size()); + Assert.assertEquals(host1, prioritizedHosts.get(0)); + Assert.assertEquals(host2, prioritizedHosts.get(1)); + Assert.assertEquals(host3, prioritizedHosts.get(2)); + } + + @Test + public void prioritizeHostsWithMatchingGuestOsTestShouldPutMatchingHostInHighPriorityAndHostsThatDoesNotMatchInLowPriorityList() { + List hostsToCheck = new ArrayList<>(Arrays.asList(host1, host2, host3)); + List highPriorityHosts = new ArrayList<>(); + List lowPriorityHosts = new ArrayList<>(); + String guestOsCategory1 = "guestOsCategory1"; + String guestOsCategory2 = "guestOsCategory2"; + + Mockito.doReturn(guestOsCategory1).when(firstFitAllocatorSpy).getTemplateGuestOSCategory(vmTemplateVO); + Mockito.doReturn(guestOsCategory1).when(firstFitAllocatorSpy).getHostGuestOSCategory(host1); + Mockito.doReturn(guestOsCategory2).when(firstFitAllocatorSpy).getHostGuestOSCategory(host2); + Mockito.doReturn(null).when(firstFitAllocatorSpy).getHostGuestOSCategory(host3); + firstFitAllocatorSpy.prioritizeHostsWithMatchingGuestOs(vmTemplateVO,hostsToCheck, highPriorityHosts, lowPriorityHosts); + + Assert.assertEquals(1, highPriorityHosts.size()); + Assert.assertEquals(host1, highPriorityHosts.get(0)); + Assert.assertEquals(1, lowPriorityHosts.size()); + Assert.assertEquals(host2, lowPriorityHosts.get(0)); + } + + @Test + public void testHostByCombinedCapacityOrder() { + // Test scenario 1: Default capacity usage (0.5 weight) + List mockCapacity = getHostCapacities(); + Map hostByCombinedCapacity = FirstFitAllocator.getHostByCombinedCapacities(mockCapacity, 0.5); + + // Verify host ordering and capacity values + Long firstHostId = hostByCombinedCapacity.keySet().iterator().next(); + Assert.assertEquals("Host with ID 1 should be first in ordering", Long.valueOf(1L), firstHostId); + Assert.assertEquals("Host 1 combined capacity should match expected value", + 0.9609375, hostByCombinedCapacity.get(1L), TOLERANCE); + Assert.assertEquals("Host 2 combined capacity should match expected value", + 0.9296875, hostByCombinedCapacity.get(2L), TOLERANCE); + + // Test scenario 2: Modified capacity usage (0.7 weight) + when(mockCapacity.get(0).getUsedCapacity()).thenReturn(1500L); + hostByCombinedCapacity = FirstFitAllocator.getHostByCombinedCapacities(mockCapacity, 0.7); + + // Verify new ordering after capacity change + firstHostId = hostByCombinedCapacity.keySet().iterator().next(); + Assert.assertEquals("Host with ID 2 should be first after capacity change", Long.valueOf(2L), firstHostId); + Assert.assertEquals("Host 2 combined capacity should match expected value after change", + 0.9515625, hostByCombinedCapacity.get(2L), TOLERANCE); + Assert.assertEquals("Host 1 combined capacity should match expected value after change", + 0.9484375, hostByCombinedCapacity.get(1L), TOLERANCE); + } + + List getHostCapacities() { + CapacityVO cpuCapacity1 = mock(CapacityVO.class); + when(cpuCapacity1.getHostOrPoolId()).thenReturn(1L); + when(cpuCapacity1.getTotalCapacity()).thenReturn(32000L); + when(cpuCapacity1.getReservedCapacity()).thenReturn(0L); + when(cpuCapacity1.getUsedCapacity()).thenReturn(500L); + when(cpuCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); + + CapacityVO cpuCapacity2 = mock(CapacityVO.class); + when(cpuCapacity2.getHostOrPoolId()).thenReturn(2L); + when(cpuCapacity2.getTotalCapacity()).thenReturn(32000L); + when(cpuCapacity2.getReservedCapacity()).thenReturn(0L); + when(cpuCapacity2.getUsedCapacity()).thenReturn(500L); + when(cpuCapacity2.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); + + CapacityVO memCapacity1 = mock(CapacityVO.class); + when(memCapacity1.getHostOrPoolId()).thenReturn(1L); + when(memCapacity1.getTotalCapacity()).thenReturn(8589934592L); + when(memCapacity1.getReservedCapacity()).thenReturn(0L); + when(memCapacity1.getUsedCapacity()).thenReturn(536870912L); + when(memCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); + + CapacityVO memCapacity2 = mock(CapacityVO.class); + when(memCapacity2.getHostOrPoolId()).thenReturn(2L); + when(memCapacity2.getTotalCapacity()).thenReturn(8589934592L); + when(memCapacity2.getReservedCapacity()).thenReturn(0L); + when(memCapacity2.getUsedCapacity()).thenReturn(1073741824L); + when(memCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); + return Arrays.asList(cpuCapacity1, memCapacity1, cpuCapacity2, memCapacity2); + } } diff --git a/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java b/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java new file mode 100644 index 000000000000..ab9b322eb843 --- /dev/null +++ b/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java @@ -0,0 +1,332 @@ +// 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 com.cloud.agent.manager.allocator.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.cloud.agent.manager.allocator.HostAllocator; +import com.cloud.deploy.DeploymentPlan; +import com.cloud.deploy.DeploymentPlanner; +import com.cloud.offering.ServiceOffering; +import com.cloud.resource.ResourceManager; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.storage.VMTemplateVO; +import com.cloud.vm.VirtualMachineProfile; +import org.apache.commons.collections.CollectionUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; + +@RunWith(MockitoJUnitRunner.class) +public class RandomAllocatorTest { + + @Mock + HostDao hostDao; + + @Spy + @InjectMocks + RandomAllocator randomAllocator; + + @Mock + ResourceManager resourceManagerMock; + + private final Host.Type type = Host.Type.Routing; + + private final Long clusterId = 1L; + + private final Long podId = 2L; + + private final Long zoneId = 3L; + + private final List emptyList = new ArrayList<>(); + + private final String hostTag = "hostTag"; + + private final HostVO host1 = Mockito.mock(HostVO.class); + + private final HostVO host2 = Mockito.mock(HostVO.class); + + private final HostVO host3 = Mockito.mock(HostVO.class); + + private final VMTemplateVO vmTemplateVO = Mockito.mock(VMTemplateVO.class); + + private final ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class); + + private final DeploymentPlanner.ExcludeList excludeList = Mockito.mock(DeploymentPlanner.ExcludeList.class); + + private final VirtualMachineProfile virtualMachineProfile = Mockito.mock(VirtualMachineProfile.class); + + private final DeploymentPlan deploymentPlan = Mockito.mock(DeploymentPlan.class); + + private final boolean considerReservedCapacity = true; + + + @Test + public void testListHostsByTags() { + Host.Type type = Host.Type.Routing; + Long id = 1L; + String templateTag = "tag1"; + String offeringTag = "tag2"; + HostVO host1 = Mockito.mock(HostVO.class); + HostVO host2 = Mockito.mock(HostVO.class); + Mockito.when(hostDao.listByHostTag(type, clusterId, podId, zoneId, offeringTag)).thenReturn(List.of(host1, host2)); + + // No template tagged host + ArrayList noTemplateTaggedHosts = new ArrayList<>(Arrays.asList(host1, host2)); + Mockito.when(hostDao.listByHostTag(type, clusterId, podId, zoneId, templateTag)).thenReturn(new ArrayList<>()); + randomAllocator.retainHostsMatchingServiceOfferingAndTemplateTags(noTemplateTaggedHosts, type, zoneId, podId, clusterId, offeringTag, templateTag); + Assert.assertTrue(CollectionUtils.isEmpty(noTemplateTaggedHosts)); + + // Different template tagged host + ArrayList differentTemplateTaggedHost = new ArrayList<>(Arrays.asList(host1, host2)); + HostVO host3 = Mockito.mock(HostVO.class); + Mockito.when(hostDao.listByHostTag(type, clusterId, podId, zoneId, templateTag)).thenReturn(List.of(host3)); + randomAllocator.retainHostsMatchingServiceOfferingAndTemplateTags(differentTemplateTaggedHost, type, zoneId, podId, clusterId, offeringTag, templateTag); + Assert.assertTrue(CollectionUtils.isEmpty(differentTemplateTaggedHost)); + + // Matching template tagged host + ArrayList matchingTemplateTaggedHost = new ArrayList<>(Arrays.asList(host1, host2)); + Mockito.when(hostDao.listByHostTag(type, clusterId, podId, zoneId, templateTag)).thenReturn(List.of(host1)); + randomAllocator.retainHostsMatchingServiceOfferingAndTemplateTags(matchingTemplateTaggedHost, type, zoneId, podId, clusterId, offeringTag, templateTag); + Assert.assertFalse(CollectionUtils.isEmpty(matchingTemplateTaggedHost)); + Assert.assertEquals(1, matchingTemplateTaggedHost.size()); + + // No template tag + ArrayList noTemplateTag = new ArrayList<>(Arrays.asList(host1, host2)); + randomAllocator.retainHostsMatchingServiceOfferingAndTemplateTags(noTemplateTag, type, zoneId, podId, clusterId, offeringTag, null); + Assert.assertFalse(CollectionUtils.isEmpty(noTemplateTag)); + Assert.assertEquals(2, noTemplateTag.size()); + + // No offering tag + ArrayList noOfferingTag = new ArrayList<>(Arrays.asList(host1, host2)); + randomAllocator.retainHostsMatchingServiceOfferingAndTemplateTags(noOfferingTag, type, zoneId, podId, clusterId, null, templateTag); + Assert.assertFalse(CollectionUtils.isEmpty(noOfferingTag)); + Assert.assertEquals(1, noOfferingTag.size()); + } + + @Test + public void findSuitableHostsTestHostTypeStorageShouldReturnNull() { + List suitableHosts = randomAllocator.findSuitableHosts(virtualMachineProfile, deploymentPlan, Host.Type.Storage, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); + + Assert.assertNull(suitableHosts); + } + + @Test + public void findSuitableHostsTestNoAvailableHostsShouldReturnNull() { + Mockito.doReturn(serviceOffering).when(virtualMachineProfile).getServiceOffering(); + Mockito.doReturn(vmTemplateVO).when(virtualMachineProfile).getTemplate(); + Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); + Mockito.doReturn(emptyList).when(randomAllocator).retrieveHosts(Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.any(VMTemplateVO.class), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + List suitableHosts = randomAllocator.findSuitableHosts(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); + + Assert.assertNull(suitableHosts); + } + + @Test + public void findSuitableHostsTestAvailableHostsShouldCallFilterAvailableHostsOnce() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2)); + + Mockito.doReturn(serviceOffering).when(virtualMachineProfile).getServiceOffering(); + Mockito.doReturn(vmTemplateVO).when(virtualMachineProfile).getTemplate(); + Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); + Mockito.doReturn(hosts).when(randomAllocator).retrieveHosts(Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.any(VMTemplateVO.class), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(hosts).when(randomAllocator).filterAvailableHosts(Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyInt(), Mockito.anyBoolean(), Mockito.anyList(), Mockito.any(ServiceOffering.class)); + List suitableHosts = randomAllocator.findSuitableHosts(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); + + Mockito.verify(randomAllocator, Mockito.times(1)).filterAvailableHosts(Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyInt(), Mockito.anyBoolean(), Mockito.anyList(), Mockito.any(ServiceOffering.class)); + Assert.assertEquals(2, suitableHosts.size()); + } + + @Test + public void filterAvailableHostsTestAvailableHostsReachedReturnUpToLimitShouldReturnOnlyHostsWithinLimit() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2)); + int returnUpTo = 1; + + Mockito.doReturn(false).when(excludeList).shouldAvoid(Mockito.any(Host.class)); + Mockito.doReturn(true).when(randomAllocator).hostHasCpuCapabilityAndCapacity(Mockito.anyBoolean(), Mockito.any(ServiceOffering.class), Mockito.any(Host.class)); + List suitableHosts = randomAllocator.filterAvailableHosts(excludeList, returnUpTo, considerReservedCapacity, hosts, serviceOffering); + + Assert.assertEquals(1, suitableHosts.size()); + } + + @Test + public void filterAvailableHostsTestReturnUpToAllShouldReturnAllAvailableHosts() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2)); + int returnUpTo = HostAllocator.RETURN_UPTO_ALL; + + Mockito.doReturn(false).when(excludeList).shouldAvoid(Mockito.any(Host.class)); + Mockito.doReturn(true).when(randomAllocator).hostHasCpuCapabilityAndCapacity(Mockito.anyBoolean(), Mockito.any(ServiceOffering.class), Mockito.any(Host.class)); + List suitableHosts = randomAllocator.filterAvailableHosts(excludeList, returnUpTo, considerReservedCapacity, hosts, serviceOffering); + + Assert.assertEquals(2, suitableHosts.size()); + } + + @Test + public void filterAvailableHostsTestHost1InAvoidShouldOnlyReturnHost2() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2)); + int returnUpTo = HostAllocator.RETURN_UPTO_ALL; + + Mockito.doReturn(true).when(excludeList).shouldAvoid(host1); + Mockito.doReturn(false).when(excludeList).shouldAvoid(host2); + Mockito.doReturn(true).when(randomAllocator).hostHasCpuCapabilityAndCapacity(Mockito.anyBoolean(), Mockito.any(ServiceOffering.class), Mockito.any(Host.class)); + List suitableHosts = randomAllocator.filterAvailableHosts(excludeList, returnUpTo, considerReservedCapacity, hosts, serviceOffering); + + Assert.assertEquals(1, suitableHosts.size()); + Assert.assertEquals(host2, suitableHosts.get(0)); + } + + @Test + public void filterAvailableHostsTestOnlyHost2HasCpuCapacityAndCapabilityShouldReturnOnlyHost2() { + List hosts = new ArrayList<>(Arrays.asList(host1, host2)); + int returnUpTo = HostAllocator.RETURN_UPTO_ALL; + + Mockito.doReturn(false).when(excludeList).shouldAvoid(Mockito.any(Host.class)); + Mockito.doReturn(false).when(randomAllocator).hostHasCpuCapabilityAndCapacity(considerReservedCapacity, serviceOffering, host1); + Mockito.doReturn(true).when(randomAllocator).hostHasCpuCapabilityAndCapacity(considerReservedCapacity, serviceOffering, host2); + List suitableHosts = randomAllocator.filterAvailableHosts(excludeList, returnUpTo, considerReservedCapacity, hosts, serviceOffering); + + Assert.assertEquals(1, suitableHosts.size()); + Assert.assertEquals(host2, suitableHosts.get(0)); + } + + @Test + public void retrieveHostsTestProvidedHostsNullAndNoHostTagAndNoTagRuleShouldOnlyReturnHostsWithNoTags() { + List upAndEnabledHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List hostsWithNoRuleTagsAndHostTags = new ArrayList<>(Arrays.asList(host1)); + + Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, null, clusterId, podId, zoneId); + + Assert.assertEquals(1, availableHosts.size()); + Assert.assertEquals(host1, availableHosts.get(0)); + } + + @Test + public void retrieveHostsTestProvidedHostsNullAndOnlyHostTagsRulesShouldReturnHostsThatMatchRuleTagsAndHostsWithNoTags() { + List upAndEnabledHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List hostsWithNoRuleTagsAndHostTags = new ArrayList<>(Arrays.asList(host1)); + List hostsMatchingRuleTags = new ArrayList<>(Arrays.asList(host2)); + + Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, null, clusterId, podId, zoneId); + + Assert.assertEquals(2, availableHosts.size()); + Assert.assertEquals(host1, availableHosts.get(0)); + Assert.assertEquals(host2, availableHosts.get(1)); + } + + @Test + public void retrieveHostsTestProvidedHostsNullProvidedHostTagsNotNullAndNoHostWithMatchingRuleTagsShouldReturnHostWithMatchingTags() { + List upAndEnabledHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List hostsWithMatchingTags = new ArrayList<>(Arrays.asList(host1)); + + Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); + Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, hostTag, clusterId, podId, zoneId); + + Assert.assertEquals(1, availableHosts.size()); + Assert.assertEquals(host1, availableHosts.get(0)); + } + + @Test + public void retrieveHostsTestProvidedHostsNullProvidedHostTagsNotNullAndHostWithMatchingRuleTagsShouldReturnHostWithHostMatchingTagsAndRuleTags() { + List upAndEnabledHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List hostsWithMatchingTags = new ArrayList<>(Arrays.asList(host1)); + List hostsMatchingRuleTags = new ArrayList<>(Arrays.asList(host3)); + + Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); + Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, hostTag, clusterId, podId, zoneId); + + Assert.assertEquals(2, availableHosts.size()); + Assert.assertEquals(host1, availableHosts.get(0)); + Assert.assertEquals(host3, availableHosts.get(1)); + } + + @Test + public void retrieveHostsTestProvidedHostsNotNullAndNoHostTagAndNoTagRuleShouldOnlyReturnHostsWithNoTags() { + List providedHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List hostsWithNoRuleTagsAndHostTags = new ArrayList<>(Arrays.asList(host1)); + + Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, null, clusterId, podId, zoneId); + + Assert.assertEquals(1, availableHosts.size()); + Assert.assertEquals(host1, availableHosts.get(0)); + } + + @Test + public void retrieveHostsTestProvidedHostsNotNullAndOnlyHostTagsRulesShouldReturnHostsThatMatchRuleTagsAndHostsWithNoTags() { + List providedHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List hostsWithNoRuleTagsAndHostTags = new ArrayList<>(Arrays.asList(host1)); + List hostsMatchingRuleTags = new ArrayList<>(Arrays.asList(host2)); + + Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, null, clusterId, podId, zoneId); + + Assert.assertEquals(2, availableHosts.size()); + Assert.assertEquals(host1, availableHosts.get(0)); + Assert.assertEquals(host2, availableHosts.get(1)); + } + + @Test + public void retrieveHostsTestProvidedHostsNotNullProvidedHostTagsNotNullAndNoHostWithMatchingRuleTagsShouldReturnHostWithMatchingTags() { + List providedHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List hostsWithMatchingTags = new ArrayList<>(Arrays.asList(host1)); + + Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); + Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, hostTag, clusterId, podId, zoneId); + + Assert.assertEquals(1, availableHosts.size()); + Assert.assertEquals(host1, availableHosts.get(0)); + } + + @Test + public void retrieveHostsTestProvidedHostsNullNotProvidedHostTagsNotNullAndHostWithMatchingRuleTagsShouldReturnHostWithHostMatchingTagsAndRuleTags() { + List providedHosts = new ArrayList<>(Arrays.asList(host1, host2)); + List hostsWithMatchingTags = new ArrayList<>(Arrays.asList(host1)); + List hostsMatchingRuleTags = new ArrayList<>(Arrays.asList(host3)); + + Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); + Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, hostTag, clusterId, podId, zoneId); + + Assert.assertEquals(2, availableHosts.size()); + Assert.assertEquals(host1, availableHosts.get(0)); + Assert.assertEquals(host3, availableHosts.get(1)); + } +} diff --git a/test/integration/smoke/test_vm_strict_host_tags.py b/test/integration/smoke/test_vm_strict_host_tags.py index 2377e9a76185..aac3e1ea65f2 100644 --- a/test/integration/smoke/test_vm_strict_host_tags.py +++ b/test/integration/smoke/test_vm_strict_host_tags.py @@ -423,7 +423,7 @@ def test_02_restore_vm_strict_tags_failure(self): vm.restore(self.apiclient, templateid=self.template_t2.id, expunge=True) self.fail("VM should not be restored") except Exception as e: - self.assertTrue("Unable to start VM with specified id" in str(e)) + self.assertTrue("Unable to create a deployment for " in str(e)) class TestMigrateVMStrictTags(cloudstackTestCase): From 1f5dba9bd2d33e95c63cc04b57df51b5c7921630 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Thu, 30 Apr 2026 12:22:35 -0300 Subject: [PATCH 039/205] Release reserved storage resources on VM deployment failure (#13048) --- .../db/schema-42200to42210-cleanup.sql | 6 +++++ .../java/com/cloud/vm/UserVmManagerImpl.java | 24 ++++++------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210-cleanup.sql index 54baf226ac43..2f104568c140 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210-cleanup.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210-cleanup.sql @@ -18,3 +18,9 @@ --; -- Schema upgrade cleanup from 4.22.0.0 to 4.22.1.0 --; + +-- Entries remaining on `cloud`.`resource_reservation` during the upgrade process are stale, so delete them. +-- This script was added to normalize volume/primary storage reservations that got stuck due to a bug on VM deployment, +-- but it is more interesting to introduce a smarter logic to clean these stale reservations in the future without the need +-- for upgrades (for instance, by having a heartbeat_time column for the reservations and automatically cleaning old entries). +DELETE FROM `cloud`.`resource_reservation`; diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index fe18263fd3bf..b23291586ef3 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -4301,9 +4301,7 @@ protected List getResourceLimitStorageTags(long diskOfferingId) { return resourceLimitService.getResourceLimitStorageTags(diskOfferingVO); } - private List reserveStorageResourcesForVm(Account owner, Long diskOfferingId, Long diskSize, List dataDiskInfoList, Long rootDiskOfferingId, ServiceOfferingVO offering, Long rootDiskSize) throws ResourceAllocationException { - List checkedReservations = new ArrayList<>(); - + private void reserveStorageResourcesForVm(List checkedReservations, Account owner, Long diskOfferingId, Long diskSize, List dataDiskInfoList, Long rootDiskOfferingId, ServiceOfferingVO offering, Long rootDiskSize) throws ResourceAllocationException { List rootResourceLimitStorageTags = getResourceLimitStorageTags(rootDiskOfferingId != null ? rootDiskOfferingId : offering.getDiskOfferingId()); CheckedReservation rootVolumeReservation = new CheckedReservation(owner, ResourceType.volume, rootResourceLimitStorageTags, 1L, reservationDao, resourceLimitService); checkedReservations.add(rootVolumeReservation); @@ -4311,12 +4309,12 @@ private List reserveStorageResourcesForVm(Account owner, Lon checkedReservations.add(rootPrimaryStorageReservation); if (diskOfferingId != null) { - List additionalResourceLimitStorageTags = diskOfferingId != null ? getResourceLimitStorageTags(diskOfferingId) : null; + List additionalResourceLimitStorageTags = getResourceLimitStorageTags(diskOfferingId); DiskOfferingVO diskOffering = _diskOfferingDao.findById(diskOfferingId); Long size = verifyAndGetDiskSize(diskOffering, diskSize); - CheckedReservation additionalVolumeReservation = diskOfferingId != null ? new CheckedReservation(owner, ResourceType.volume, additionalResourceLimitStorageTags, 1L, reservationDao, resourceLimitService) : null; + CheckedReservation additionalVolumeReservation = new CheckedReservation(owner, ResourceType.volume, additionalResourceLimitStorageTags, 1L, reservationDao, resourceLimitService); checkedReservations.add(additionalVolumeReservation); - CheckedReservation additionalPrimaryStorageReservation = diskOfferingId != null ? new CheckedReservation(owner, ResourceType.primary_storage, additionalResourceLimitStorageTags, size, reservationDao, resourceLimitService) : null; + CheckedReservation additionalPrimaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, additionalResourceLimitStorageTags, size, reservationDao, resourceLimitService); checkedReservations.add(additionalPrimaryStorageReservation); } @@ -4332,7 +4330,6 @@ private List reserveStorageResourcesForVm(Account owner, Lon checkedReservations.add(additionalPrimaryStorageReservation); } } - return checkedReservations; } private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, String displayName, Account owner, @@ -4344,10 +4341,10 @@ private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, Stri Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, Long rootDiskOfferingId, long volumesSize, Volume volume, Snapshot snapshot) throws ResourceAllocationException { - List checkedReservations = new ArrayList<>(); + List checkedReservations = new ArrayList<>(); try { - checkedReservations = reserveStorageResourcesForVm(owner, diskOfferingId, diskSize, dataDiskInfoList, rootDiskOfferingId, offering, volumesSize); + reserveStorageResourcesForVm(checkedReservations, owner, diskOfferingId, diskSize, dataDiskInfoList, rootDiskOfferingId, offering, volumesSize); // verify security group ids if (securityGroupIdList != null) { @@ -4638,14 +4635,7 @@ private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, Stri logger.error("error during resource reservation and allocation", e); throw new CloudRuntimeException(e); } finally { - for (CheckedReservation checkedReservation : checkedReservations) { - try { - checkedReservation.close(); - } catch (Exception e) { - logger.error("error during resource reservation and allocation", e); - throw new CloudRuntimeException(e); - } - } + ReservationHelper.closeAll(checkedReservations); } } From c07f1fd5d29ab2c6ba3a419dbf6d21b0a4f3900b Mon Sep 17 00:00:00 2001 From: Henrique Sato Date: Fri, 1 May 2026 06:54:40 -0300 Subject: [PATCH 040/205] Number of running and stopped VMs as preset variables for `Network` type Quota tariffs (#11689) Co-authored-by: Fabricio Duarte --- .../presetvariables/PresetVariableHelper.java | 14 ++++- .../presetvariables/ResourceCounting.java | 52 +++++++++++++++++++ .../activationrule/presetvariables/Value.java | 11 ++++ .../PresetVariableHelperTest.java | 37 +++++++++++-- .../response/QuotaResponseBuilderImpl.java | 3 +- 5 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceCounting.java diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java index cd87052e878c..23020292027c 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java @@ -35,6 +35,7 @@ import javax.inject.Inject; import com.cloud.storage.StoragePoolTagVO; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.acl.dao.RoleDao; import org.apache.cloudstack.backup.BackupOfferingVO; @@ -783,10 +784,21 @@ protected void loadPresetVariableValueForNetwork(UsageVO usageRecord, Value valu value.setId(network.getUuid()); value.setName(network.getName()); value.setState(usageRecord.getState()); - + value.setResourceCounting(getPresetVariableValueNetworkResourceCounting(networkId)); value.setNetworkOffering(getPresetVariableValueNetworkOffering(network.getNetworkOfferingId())); } + protected ResourceCounting getPresetVariableValueNetworkResourceCounting(Long networkId) { + ResourceCounting resourceCounting = new ResourceCounting(); + List vmInstancesVO = vmInstanceDao.listNonRemovedVmsByTypeAndNetwork(networkId, VirtualMachine.Type.User); + int runningVms = (int) vmInstancesVO.stream().filter(vm -> vm.getState().equals(VirtualMachine.State.Running)).count(); + int stoppedVms = (int) vmInstancesVO.stream().filter(vm -> vm.getState().equals(VirtualMachine.State.Stopped)).count(); + + resourceCounting.setRunningVms(runningVms); + resourceCounting.setStoppedVms(stoppedVms); + return resourceCounting; + } + protected GenericPresetVariable getPresetVariableValueNetworkOffering(Long networkOfferingId) { NetworkOfferingVO networkOfferingVo = networkOfferingDao.findByIdIncludingRemoved(networkOfferingId); validateIfObjectIsNull(networkOfferingVo, networkOfferingId, "network offering"); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceCounting.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceCounting.java new file mode 100644 index 000000000000..75049c3486a3 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceCounting.java @@ -0,0 +1,52 @@ +// 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.quota.activationrule.presetvariables; + + +import org.apache.cloudstack.quota.constant.QuotaTypes; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class ResourceCounting { + + @PresetVariableDefinition(description = "The number of running user instances.", supportedTypes = {QuotaTypes.NETWORK}) + private int runningVms; + @PresetVariableDefinition(description = "The number of stopped user instances.", supportedTypes = {QuotaTypes.NETWORK}) + private int stoppedVms; + + public int getRunningVms() { + return runningVms; + } + + public void setRunningVms(int runningVms) { + this.runningVms = runningVms; + } + + public int getStoppedVms() { + return stoppedVms; + } + + public void setStoppedVms(int stoppedVms) { + this.stoppedVms = stoppedVms; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java index aff2c040e742..286fe5c60fd5 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java @@ -84,6 +84,9 @@ public class Value extends GenericPresetVariable { @PresetVariableDefinition(description = "Backup offering of the backup.", supportedTypes = {QuotaTypes.BACKUP}) private BackupOffering backupOffering; + @PresetVariableDefinition(description = "The amount of resources of the usage type.") + private ResourceCounting resourceCounting; + @PresetVariableDefinition(description = "The hypervisor where the resource was deployed. Values can be: XenServer, KVM, VMware, Hyperv, BareMetal, Ovm, Ovm3 and LXC.", supportedTypes = {QuotaTypes.RUNNING_VM, QuotaTypes.ALLOCATED_VM, QuotaTypes.VM_SNAPSHOT, QuotaTypes.SNAPSHOT}) private String hypervisorType; @@ -262,6 +265,14 @@ public void setState(String state) { this.state = state; } + public ResourceCounting getResourceCounting() { + return resourceCounting; + } + + public void setResourceCounting(ResourceCounting resourceCounting) { + this.resourceCounting = resourceCounting; + } + public GenericPresetVariable getNetworkOffering() { return networkOffering; } diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java index fb093a456790..bcdbc3b46cec 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java @@ -31,11 +31,13 @@ import com.cloud.dc.ClusterDetailsVO; import com.cloud.host.HostTagVO; import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.Network; import com.cloud.network.dao.NetworkVO; import com.cloud.network.vpc.VpcOfferingVO; import com.cloud.network.vpc.VpcVO; import com.cloud.network.vpc.dao.VpcOfferingDao; import com.cloud.storage.StoragePoolTagVO; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.acl.dao.RoleDao; @@ -238,6 +240,8 @@ private Value getValueForTests() { value.setVmSnapshotType(VMSnapshot.Type.Disk.toString()); value.setComputingResources(getComputingResourcesForTests()); value.setVolumeType(Volume.Type.DATADISK.toString()); + value.setState(Network.State.Implemented.toString()); + value.setResourceCounting(getResourceCountingForTests()); value.setNetworkOffering(getNetworkOfferingForTests()); value.setVpcOffering(getVpcOfferingForTests()); return value; @@ -276,6 +280,13 @@ private Configuration getConfigurationForTests() { return configuration; } + private ResourceCounting getResourceCountingForTests() { + ResourceCounting resourceCounting = new ResourceCounting(); + resourceCounting.setRunningVms(1); + resourceCounting.setStoppedVms(1); + return resourceCounting; + } + private List getHostTagsForTests() { return Arrays.asList(new HostTagVO(1, "tag1", false), new HostTagVO(1, "tag2", false)); } @@ -1343,8 +1354,8 @@ public void loadPresetVariableValueForNetworkTestRecordIsNetworkSetFields() { Mockito.doReturn(expected.getId()).when(networkVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(networkVoMock).getName(); Mockito.doReturn(expected.getState()).when(usageVoMock).getState(); + Mockito.doReturn(expected.getResourceCounting()).when(presetVariableHelperSpy).getPresetVariableValueNetworkResourceCounting(Mockito.anyLong()); Mockito.doReturn(expected.getNetworkOffering()).when(presetVariableHelperSpy).getPresetVariableValueNetworkOffering(Mockito.anyLong()); - Mockito.doReturn(UsageTypes.NETWORK).when(usageVoMock).getUsageType(); Value result = new Value(); @@ -1352,7 +1363,27 @@ public void loadPresetVariableValueForNetworkTestRecordIsNetworkSetFields() { assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getState(), result.getState()); - Assert.assertEquals(expected.getNetworkOffering(), result.getNetworkOffering()); + Assert.assertEquals(expected.getResourceCounting(), result.getResourceCounting()); + } + + @Test + public void getPresetVariableValueNetworkResourceCountingTestSetValueAndReturnObject() { + VMInstanceVO vmInstanceVoMock1 = Mockito.spy(VMInstanceVO.class); + vmInstanceVoMock1.setState(VirtualMachine.State.Stopped); + + VMInstanceVO vmInstanceVoMock2 = Mockito.spy(VMInstanceVO.class); + vmInstanceVoMock2.setState(VirtualMachine.State.Running); + + Mockito.doReturn(List.of(vmInstanceVoMock1, vmInstanceVoMock2)).when(vmInstanceDaoMock).listNonRemovedVmsByTypeAndNetwork(Mockito.anyLong(), Mockito.any()); + + mockMethodValidateIfObjectIsNull(); + + ResourceCounting expected = getResourceCountingForTests(); + + ResourceCounting result = presetVariableHelperSpy.getPresetVariableValueNetworkResourceCounting(1L); + + Assert.assertEquals(expected.getRunningVms(), result.getRunningVms()); + Assert.assertEquals(expected.getStoppedVms(), result.getStoppedVms()); } @Test @@ -1362,7 +1393,7 @@ public void loadPresetVariableValueForVpcTestRecordIsNotAVpcDoNothing() { presetVariableHelperSpy.loadPresetVariableValueForVpc(usageVoMock, null); }); - Mockito.verifyNoInteractions(networkDaoMock); + Mockito.verifyNoInteractions(vpcDaoMock); } @Test diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 173c0723731b..c919bb5887c1 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -98,6 +98,7 @@ import org.apache.cloudstack.quota.activationrule.presetvariables.GenericPresetVariable; import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariableDefinition; import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariables; +import org.apache.cloudstack.quota.activationrule.presetvariables.ResourceCounting; import org.apache.cloudstack.quota.activationrule.presetvariables.Value; import org.apache.cloudstack.quota.constant.QuotaConfig; import org.apache.cloudstack.quota.constant.QuotaTypes; @@ -185,7 +186,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { private VolumeDao volumeDao; - private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class}; + private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class, ResourceCounting.class}; private Set accountTypesThatCanListAllQuotaSummaries = Sets.newHashSet(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN); From 08b1d38755727b615fda6a668ba9ac035f3b8002 Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Sat, 2 May 2026 11:21:13 +0100 Subject: [PATCH 041/205] adaptive: honor user-provided capacityBytes when provider stats are unavailable (#13059) Signed-off-by: Eugenio Grosso Co-authored-by: Eugenio Grosso --- .../lifecycle/AdaptiveDataStoreLifeCycleImpl.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/storage/volume/adaptive/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/AdaptiveDataStoreLifeCycleImpl.java b/plugins/storage/volume/adaptive/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/AdaptiveDataStoreLifeCycleImpl.java index 771f79887e0f..c8efc08c2892 100644 --- a/plugins/storage/volume/adaptive/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/AdaptiveDataStoreLifeCycleImpl.java +++ b/plugins/storage/volume/adaptive/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/AdaptiveDataStoreLifeCycleImpl.java @@ -217,13 +217,14 @@ public DataStore initialize(Map dsInfos) { // validate the provided details are correct/valid for the provider api.validate(); - // if we have user-provided capacity bytes, validate they do not exceed the manaaged storage capacity bytes + // User-provided capacityBytes always wins; validate against storage stats only when + // the provider could actually report them. If the provider cannot (empty pod with no + // footprint, no quota set, transient probe failure), fall through and use what the + // user supplied rather than failing the whole registration. ProviderVolumeStorageStats stats = api.getManagedStorageStats(); - if (capacityBytes != null && capacityBytes != 0 && stats != null) { - if (stats.getCapacityInBytes() > 0) { - if (stats.getCapacityInBytes() < capacityBytes) { - throw new InvalidParameterValueException("Capacity bytes provided exceeds the capacity of the storage endpoint: provided by user: " + capacityBytes + ", storage capacity from storage provider: " + stats.getCapacityInBytes()); - } + if (capacityBytes != null && capacityBytes > 0) { + if (stats != null && stats.getCapacityInBytes() > 0 && stats.getCapacityInBytes() < capacityBytes) { + throw new InvalidParameterValueException("Provided capacity bytes exceed the capacity of the storage endpoint: provided by user: " + capacityBytes + ", storage capacity from storage provider: " + stats.getCapacityInBytes()); } parameters.setCapacityBytes(capacityBytes); } From a17bff9ba8770349c3b8344cb45778e89d4379f4 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sun, 3 May 2026 22:09:41 +0530 Subject: [PATCH 042/205] ui: fix webhook filters listing (#13068) --- ui/public/locales/en.json | 1 + ui/src/components/view/ListView.vue | 2 +- ui/src/components/view/WebhookFiltersTab.vue | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 4a23b454252a..e2045eba8dfe 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -4167,6 +4167,7 @@ "message.warn.select.template": "Please select a Template for Registration.", "message.warn.zone.mtu.update": "Please note that this limit won't affect pre-existing Network's MTU settings", "message.webhook.deliveries.time.filter": "Webhook deliveries list can be filtered based on date-time. Select 'Custom' for specifying start and end date range.", +"message.webhook.filter.add": "Webhook deliveries can be controlled using filters (currently by Event type). Please select the parameters to add to the applied filters list.", "message.zone.creation.complete": "Zone creation complete.", "message.zone.detail.description": "Populate Zone details.", "message.zone.detail.hint": "A Zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more Pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 66dd6b3db9e6..b03293efacae 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -949,7 +949,7 @@ style="margin-left: 5px" :actions="actions" :resource="record" - :enabled="quickViewEnabled() && actions.length > 0" + :enabled="quickViewEnabled(actions, columns, column.key)" @exec-action="$parent.execAction" /> diff --git a/ui/src/components/view/WebhookFiltersTab.vue b/ui/src/components/view/WebhookFiltersTab.vue index 2d276e2a3cf0..513720a86258 100644 --- a/ui/src/components/view/WebhookFiltersTab.vue +++ b/ui/src/components/view/WebhookFiltersTab.vue @@ -18,6 +18,7 @@
@@ -1369,9 +1369,11 @@ export default { this.currentAction.paramFilters = [] if ('message' in action) { if (typeof action.message === 'function') { - action.message = action.message(action.resource) + action.messageString = action.message(action.resource) + } else { + action.messageString = action.message } - action.message = Array.isArray(action.message) ? this.$t(...action.message) : this.$t(action.message) + action.messageString = Array.isArray(action.messageString) ? this.$t(...action.messageString) : this.$t(action.messageString) } this.getArgs(action, isGroupAction, paramFields) this.getFilters(action, isGroupAction, paramFields) From e1521f139b026e1459a4993b75d9425ac508956d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 11 May 2026 16:52:54 +0530 Subject: [PATCH 055/205] systemvmtemplate-register: correctly update existing template name in config (#12703) --- .../upgrade/SystemVmTemplateRegistration.java | 2 +- .../upgrade/SystemVmTemplateRegistrationTest.java | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java index 292bafefbb65..2acb4138d234 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java @@ -1073,7 +1073,7 @@ protected void updateRegisteredTemplateDetails(Long templateId, MetadataTemplate } Hypervisor.HypervisorType hypervisorType = templateDetails.getHypervisorType(); updateSystemVMEntries(templateId, hypervisorType); - updateConfigurationParams(hypervisorType, templateDetails.getName(), zoneId); + updateConfigurationParams(hypervisorType, templateVO.getName(), zoneId); } protected void updateTemplateUrlChecksumAndGuestOsId(VMTemplateVO templateVO, diff --git a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java index 8028e78c9073..51db952eb613 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java @@ -570,18 +570,19 @@ public void updateRegisteredTemplateDetails_UpdatesTemplateSuccessfully() { SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class); + String templateName = "templateName"; + when(templateVO.getName()).thenReturn(templateName); GuestOSVO guestOS = Mockito.mock(GuestOSVO.class); when(templateDetails.getGuestOs()).thenReturn("Debian"); when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); - when(templateDetails.getName()).thenReturn("templateName"); when(vmTemplateDao.findById(templateId)).thenReturn(templateVO); when(guestOSDao.findOneByDisplayName("Debian")).thenReturn(guestOS); when(guestOS.getId()).thenReturn(10L); when(vmTemplateDao.update(templateVO.getId(), templateVO)).thenReturn(true); doNothing().when(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); doNothing().when(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, - "templateName", zoneId); + templateName, zoneId); systemVmTemplateRegistration.updateRegisteredTemplateDetails(templateId, templateDetails, zoneId); @@ -590,7 +591,7 @@ public void updateRegisteredTemplateDetails_UpdatesTemplateSuccessfully() { verify(vmTemplateDao).update(templateVO.getId(), templateVO); verify(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); verify(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, - "templateName", zoneId); + templateName, zoneId); } @Test @@ -620,16 +621,17 @@ public void updateRegisteredTemplateDetails_SkipsGuestOSUpdateWhenNotFound() { SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class); + String templateName = "templateName"; + when(templateVO.getName()).thenReturn(templateName); when(templateDetails.getGuestOs()).thenReturn("NonExistentOS"); when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); - when(templateDetails.getName()).thenReturn("templateName"); when(vmTemplateDao.findById(templateId)).thenReturn(templateVO); when(guestOSDao.findOneByDisplayName("NonExistentOS")).thenReturn(null); when(vmTemplateDao.update(templateVO.getId(), templateVO)).thenReturn(true); doNothing().when(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); doNothing().when(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, - "templateName", zoneId); + templateName, zoneId); systemVmTemplateRegistration.updateRegisteredTemplateDetails(templateId, templateDetails, zoneId); @@ -637,7 +639,7 @@ public void updateRegisteredTemplateDetails_SkipsGuestOSUpdateWhenNotFound() { verify(vmTemplateDao).update(templateVO.getId(), templateVO); verify(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); verify(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, - "templateName", zoneId); + templateName, zoneId); } @Test From a289bb0e6c0dafbcb3e6dec60e5d3933f5b61a95 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 11 May 2026 20:15:16 +0530 Subject: [PATCH 056/205] Update suse15 packaging spec, use qemu-ovmf-x86_64 package instead of edk2-ovmf for agent (#13133) * Update qemu-ovmf-x86_64 package in agent for suse instead of edk2-ovmf for agent * Maintain separate properties file for suse15 (UEFI properties are different for suse15) * Maintain separate packaging spec for suse15 --- packaging/el8/cloud.spec | 2 +- packaging/suse15 | 1 - packaging/suse15/cloud-ipallocator.rc | 1 + packaging/suse15/cloud.limits | 1 + packaging/suse15/cloud.spec | 750 ++++++++++++++++++++++++++ packaging/suse15/cloudstack-agent.te | 1 + packaging/suse15/cloudstack-sccs | 1 + packaging/suse15/filelimit.conf | 1 + packaging/suse15/replace.properties | 65 +++ 9 files changed, 821 insertions(+), 2 deletions(-) delete mode 120000 packaging/suse15 create mode 120000 packaging/suse15/cloud-ipallocator.rc create mode 120000 packaging/suse15/cloud.limits create mode 100644 packaging/suse15/cloud.spec create mode 120000 packaging/suse15/cloudstack-agent.te create mode 120000 packaging/suse15/cloudstack-sccs create mode 120000 packaging/suse15/filelimit.conf create mode 100644 packaging/suse15/replace.properties diff --git a/packaging/el8/cloud.spec b/packaging/el8/cloud.spec index 3dee161bf274..13c0a36cb106 100644 --- a/packaging/el8/cloud.spec +++ b/packaging/el8/cloud.spec @@ -117,7 +117,7 @@ Requires: ipset Requires: perl Requires: rsync Requires: cifs-utils -Requires: edk2-ovmf +Requires: (edk2-ovmf or qemu-ovmf-x86_64) Requires: swtpm Requires: (python3-libvirt or python3-libvirt-python) Requires: (qemu-img or qemu-tools) diff --git a/packaging/suse15 b/packaging/suse15 deleted file mode 120000 index 4dad90d45e0c..000000000000 --- a/packaging/suse15 +++ /dev/null @@ -1 +0,0 @@ -el8 \ No newline at end of file diff --git a/packaging/suse15/cloud-ipallocator.rc b/packaging/suse15/cloud-ipallocator.rc new file mode 120000 index 000000000000..647598e6dc41 --- /dev/null +++ b/packaging/suse15/cloud-ipallocator.rc @@ -0,0 +1 @@ +../el8/cloud-ipallocator.rc \ No newline at end of file diff --git a/packaging/suse15/cloud.limits b/packaging/suse15/cloud.limits new file mode 120000 index 000000000000..37be77e3acf2 --- /dev/null +++ b/packaging/suse15/cloud.limits @@ -0,0 +1 @@ +../el8/cloud.limits \ No newline at end of file diff --git a/packaging/suse15/cloud.spec b/packaging/suse15/cloud.spec new file mode 100644 index 000000000000..cdfc5a72a34e --- /dev/null +++ b/packaging/suse15/cloud.spec @@ -0,0 +1,750 @@ +# 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. + +%define __os_install_post %{nil} +%global debug_package %{nil} +%global __requires_exclude libc\\.so\\..*|libc\\.so\\.6\\(GLIBC_.*\\) +%define _binaries_in_noarch_packages_terminate_build 0 + +# DISABLE the post-percentinstall java repacking and line number stripping +# we need to find a way to just disable the java repacking and line number stripping, but not the autodeps + +Name: cloudstack +Summary: CloudStack IaaS Platform +#http://fedoraproject.org/wiki/PackageNamingGuidelines#Pre-Release_packages +%define _maventag %{_fullver} +Release: %{_rel} + +Version: %{_ver} +License: ASL 2.0 +Vendor: Apache CloudStack +Packager: Apache CloudStack +Group: System Environment/Libraries +# FIXME do groups for every single one of the subpackages +Source0: %{name}-%{_maventag}.tgz +BuildRoot: %{_tmppath}/%{name}-%{_maventag}-%{release}-build +BuildArch: noarch + +BuildRequires: (java-11-openjdk-devel or java-17-openjdk-devel or java-21-openjdk-devel) +#BuildRequires: ws-commons-util +BuildRequires: jpackage-utils +BuildRequires: gcc +BuildRequires: glibc-devel +BuildRequires: /usr/bin/mkisofs +BuildRequires: python3-setuptools +BuildRequires: wget +BuildRequires: nodejs + +%description +CloudStack is a highly-scalable elastic, open source, +intelligent IaaS cloud implementation. + +%package management +Summary: CloudStack management server UI +Requires: (java-17-openjdk or java-21-openjdk) +Requires: (tzdata-java or timezone-java) +Requires: python3 +Requires: bash +Requires: gawk +Requires: which +Requires: file +Requires: tar +Requires: bzip2 +Requires: gzip +Requires: unzip +Requires: (/sbin/mount.nfs or /usr/sbin/mount.nfs) +Requires: (openssh-clients or openssh) +Requires: (nfs-utils or nfs-client) +Requires: iproute +Requires: wget +Requires: (mysql or mariadb or mysql8.4) +Requires: sudo +Requires: /sbin/service +Requires: /sbin/chkconfig +Requires: /usr/bin/ssh-keygen +Requires: (genisoimage or mkisofs or xorrisofs) +Requires: ipmitool +Requires: %{name}-common = %{_ver} +Requires: (iptables-services or iptables) +Requires: rng-tools +Requires: (qemu-img or qemu-tools) +Requires: python3-pip +Requires: python3-six +Requires: python3-protobuf +Requires: python3-setuptools +Requires: (libgcrypt > 1.8.3 or libgcrypt20) +Group: System Environment/Libraries +%description management +The CloudStack management server is the central point of coordination, +management, and intelligence in CloudStack. + +%package common +Summary: Apache CloudStack common files and scripts +Requires: python3 +Group: System Environment/Libraries +%description common +The Apache CloudStack files shared between agent and management server +%global __requires_exclude libc\\.so\\..*|libc\\.so\\.6\\(GLIBC_.*\\)|^(libuuid\\.so\\.1|/usr/bin/python)$ + +%package agent +Summary: CloudStack Agent for KVM hypervisors +Requires: (openssh-clients or openssh) +Requires: (java-17-openjdk or java-21-openjdk) +Requires: (tzdata-java or timezone-java) +Requires: %{name}-common = %{_ver} +Requires: libvirt +Requires: libvirt-daemon-driver-storage-rbd +Requires: ebtables +Requires: iptables +Requires: ethtool +Requires: (net-tools or net-tools-deprecated) +Requires: iproute +Requires: ipset +Requires: perl +Requires: rsync +Requires: cifs-utils +Requires: (edk2-ovmf or qemu-ovmf-x86_64) +Requires: swtpm +Requires: (python3-libvirt or python3-libvirt-python) +Requires: (qemu-img or qemu-tools) +Requires: qemu-kvm +Requires: cryptsetup +Requires: rng-tools +Requires: (libgcrypt > 1.8.3 or libgcrypt20) +Requires: (selinux-tools if selinux-tools) +Requires: sysstat +Provides: cloud-agent +Group: System Environment/Libraries +%description agent +The CloudStack agent for KVM hypervisors + +%package baremetal-agent +Summary: CloudStack baremetal agent +Requires: tftp-server +Requires: xinetd +Requires: syslinux +Requires: chkconfig +Requires: dhcp +Requires: httpd +Group: System Environment/Libraries +%description baremetal-agent +The CloudStack baremetal agent + +%package usage +Summary: CloudStack Usage calculation server +Requires: (java-17-openjdk or java-21-openjdk) +Requires: (tzdata-java or timezone-java) +Group: System Environment/Libraries +%description usage +The CloudStack usage calculation service + +%package ui +Summary: CloudStack UI +Group: System Environment/Libraries +%description ui +The CloudStack UI + +%package marvin +Summary: Apache CloudStack Marvin library +Requires: python3-pip +Requires: gcc +Requires: python3-devel +Requires: libffi-devel +Requires: openssl-devel +Group: System Environment/Libraries +%description marvin +Apache CloudStack Marvin library + +%package integration-tests +Summary: Apache CloudStack Marvin integration tests +Requires: %{name}-marvin = %{_ver} +Group: System Environment/Libraries +%description integration-tests +Apache CloudStack Marvin integration tests + +%if "%{_ossnoss}" == "noredist" +%package mysql-ha +Summary: Apache CloudStack Balancing Strategy for MySQL +Group: System Environmnet/Libraries +%description mysql-ha +Apache CloudStack Balancing Strategy for MySQL + +%endif + +%prep +echo Doing CloudStack build + +%setup -q -n %{name}-%{_maventag} + +%build + +cp packaging/suse15/replace.properties build/replace.properties +echo VERSION=%{_maventag} >> build/replace.properties +echo PACKAGE=%{name} >> build/replace.properties +touch build/gitrev.txt +echo $(git rev-parse HEAD) > build/gitrev.txt + +if [ "%{_ossnoss}" == "NOREDIST" -o "%{_ossnoss}" == "noredist" ] ; then + echo "Adding noredist flag to the maven build" + FLAGS="$FLAGS -Dnoredist" +fi + +if [ "%{_sim}" == "SIMULATOR" -o "%{_sim}" == "simulator" ] ; then + echo "Adding simulator flag to the maven build" + FLAGS="$FLAGS -Dsimulator" +fi + +if [ \"%{_temp}\" != "" ]; then + echo "Adding flags to package requested templates" + FLAGS="$FLAGS `rpm --eval %{?_temp}`" +fi + +mvn -Psystemvm,developer $FLAGS clean package +cd ui && npm install && npm run build && cd .. + +%install +[ ${RPM_BUILD_ROOT} != "/" ] && rm -rf ${RPM_BUILD_ROOT} +# Common directories +mkdir -p ${RPM_BUILD_ROOT}%{_bindir} +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/agent +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/ipallocator +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/cache/%{name}/management/work +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/cache/%{name}/management/temp +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/%{name}/mnt +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/%{name}/management +mkdir -p ${RPM_BUILD_ROOT}%{_initrddir} +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/default +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/profile.d +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/sudoers.d + +# Common +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/vms +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site +mkdir -p ${RPM_BUILD_ROOT}/usr/bin +cp -r scripts/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts +install -D systemvm/dist/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/vms/ +install python/lib/cloud_utils.py ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site/cloud_utils.py +cp -r python/lib/cloudutils ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site/ +python3 -m py_compile ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site/cloud_utils.py +python3 -m compileall ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site/cloudutils +cp build/gitrev.txt ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts +cp packaging/suse15/cloudstack-sccs ${RPM_BUILD_ROOT}/usr/bin + +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts/network/cisco +cp -r plugins/network-elements/cisco-vnmc/src/main/scripts/network/cisco/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts/network/cisco + +# Management +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/ +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/lib +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/management +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/systemd/system/%{name}-management.service.d +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/run +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel + +# Setup Jetty +ln -sf /etc/%{name}/management ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/conf +ln -sf /var/log/%{name}/management ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/logs + +install -D client/target/utilities/bin/cloud-migrate-databases ${RPM_BUILD_ROOT}%{_bindir}/%{name}-migrate-databases +install -D client/target/utilities/bin/cloud-set-guest-password ${RPM_BUILD_ROOT}%{_bindir}/%{name}-set-guest-password +install -D client/target/utilities/bin/cloud-set-guest-sshkey ${RPM_BUILD_ROOT}%{_bindir}/%{name}-set-guest-sshkey +install -D client/target/utilities/bin/cloud-setup-databases ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-databases +install -D client/target/utilities/bin/cloud-setup-encryption ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-encryption +install -D client/target/utilities/bin/cloud-setup-management ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-management +install -D client/target/utilities/bin/cloud-setup-baremetal ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-baremetal +install -D client/target/utilities/bin/cloud-sysvmadm ${RPM_BUILD_ROOT}%{_bindir}/%{name}-sysvmadm +install -D client/target/utilities/bin/cloud-update-xenserver-licenses ${RPM_BUILD_ROOT}%{_bindir}/%{name}-update-xenserver-licenses +# Bundle cmk in cloudstack-management +wget https://github.com/apache/cloudstack-cloudmonkey/releases/latest/download/cmk.linux.x86-64 -O ${RPM_BUILD_ROOT}%{_bindir}/cmk +chmod +x ${RPM_BUILD_ROOT}%{_bindir}/cmk + +cp -r client/target/utilities/scripts/db/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup +cp -r plugins/integrations/kubernetes-service/src/main/resources/conf/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf +cp -r client/target/cloud-client-ui-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/ +cp -r client/target/classes/META-INF/webapp ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapp +cp ui/dist/config.json ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management/ +cp -r ui/dist/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapp/ +rm -f ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapp/config.json +ln -sf /etc/%{name}/management/config.json ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapp/config.json +mv ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cloud-client-ui-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/lib/cloudstack-%{_maventag}.jar +cp client/target/lib/*jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/lib/ + +# Don't package the scripts in the management webapp +rm -rf ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/scripts +rm -rf ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/vms + +for name in db.properties server.properties log4j-cloud.xml environment.properties java.security.ciphers +do + cp client/target/conf/$name ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management/$name +done + +ln -sf log4j-cloud.xml ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management/log4j2.xml + +install python/bindir/cloud-external-ipallocator.py ${RPM_BUILD_ROOT}%{_bindir}/%{name}-external-ipallocator.py +install -D client/target/pythonlibs/jasypt-1.9.3.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/lib/jasypt-1.9.3.jar +install -D utils/target/cloud-utils-%{_maventag}-bundled.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/lib/%{name}-utils.jar + +install -D packaging/suse15/cloud-ipallocator.rc ${RPM_BUILD_ROOT}%{_initrddir}/%{name}-ipallocator +install -D packaging/suse15/cloud.limits ${RPM_BUILD_ROOT}%{_sysconfdir}/security/limits.d/cloud +install -D packaging/suse15/filelimit.conf ${RPM_BUILD_ROOT}%{_sysconfdir}/systemd/system/%{name}-management.service.d +install -D packaging/systemd/cloudstack-management.service ${RPM_BUILD_ROOT}%{_unitdir}/%{name}-management.service +install -D packaging/systemd/cloudstack-management.default ${RPM_BUILD_ROOT}%{_sysconfdir}/default/%{name}-management +install -D server/target/conf/cloudstack-sudoers ${RPM_BUILD_ROOT}%{_sysconfdir}/sudoers.d/%{name}-management +touch ${RPM_BUILD_ROOT}%{_localstatedir}/run/%{name}-management.pid +#install -D server/target/conf/cloudstack-catalina.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-catalina +install -D server/target/conf/cloudstack-management.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-management + +install -D plugins/integrations/kubernetes-service/src/main/resources/conf/etcd-node.yml ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf/etcd-node.yml +install -D plugins/integrations/kubernetes-service/src/main/resources/conf/k8s-control-node.yml ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf/k8s-control-node.yml +install -D plugins/integrations/kubernetes-service/src/main/resources/conf/k8s-control-node-add.yml ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf/k8s-control-node-add.yml +install -D plugins/integrations/kubernetes-service/src/main/resources/conf/k8s-node.yml ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf/k8s-node.yml + +# SystemVM template +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/templates/systemvm +cp -r engine/schema/dist/systemvm-templates/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/templates/systemvm +rm -rf ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/templates/systemvm/sha512sum.txt + +# Sample Extensions +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/extensions +cp -r extensions/* ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/extensions +ln -sf %{_sysconfdir}/%{name}/extensions ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/extensions + +# UI +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/ui +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/ +cp -r client/target/classes/META-INF/webapp/WEB-INF ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui +cp ui/dist/config.json ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/ui/ +cp -r ui/dist/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/ +rm -f ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/config.json +ln -sf /etc/%{name}/ui/config.json ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/config.json + +# Package mysql-connector-python (bundled to avoid dependency on external community repo) +# Version 8.0.31 is the last version supporting Python 3.6 (EL8) +wget -P ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel https://files.pythonhosted.org/packages/08/1f/42d74bae9dd6dcfec67c9ed0f3fa482b1ae5ac5f117ca82ab589ecb3ca19/mysql_connector_python-8.0.31-py2.py3-none-any.whl +# Version 8.3.0 supports Python 3.8 to 3.12 (EL9, EL10) +wget -P ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel https://files.pythonhosted.org/packages/53/ed/26a4b8cacb8852c6fd97d2d58a7f2591c41989807ea82bd8d9725a4e6937/mysql_connector_python-8.3.0-py2.py3-none-any.whl + +chmod 440 ${RPM_BUILD_ROOT}%{_sysconfdir}/sudoers.d/%{name}-management +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/%{name}/mnt +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/%{name}/management +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/cache/%{name}/management/work +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/cache/%{name}/management/temp +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/management +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/agent + +# KVM Agent +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/agent +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/plugins +install -D packaging/systemd/cloudstack-agent.service ${RPM_BUILD_ROOT}%{_unitdir}/%{name}-agent.service +install -D packaging/systemd/cloudstack-rolling-maintenance@.service ${RPM_BUILD_ROOT}%{_unitdir}/%{name}-rolling-maintenance@.service +install -D packaging/systemd/cloudstack-agent.default ${RPM_BUILD_ROOT}%{_sysconfdir}/default/%{name}-agent +install -D agent/target/transformed/agent.properties ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent/agent.properties +install -D agent/target/transformed/uefi.properties ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent/uefi.properties +install -D agent/target/transformed/environment.properties ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent/environment.properties +install -D agent/target/transformed/log4j-cloud.xml ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent/log4j-cloud.xml +install -D agent/target/transformed/cloud-setup-agent ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-agent +install -D agent/target/transformed/cloudstack-agent-upgrade ${RPM_BUILD_ROOT}%{_bindir}/%{name}-agent-upgrade +install -D agent/target/transformed/cloud-guest-tool ${RPM_BUILD_ROOT}%{_bindir}/%{name}-guest-tool +install -D agent/target/transformed/libvirtqemuhook ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib/libvirtqemuhook +install -D agent/target/transformed/rolling-maintenance ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib/rolling-maintenance +install -D agent/target/transformed/cloud-ssh ${RPM_BUILD_ROOT}%{_bindir}/%{name}-ssh +install -D agent/target/transformed/cloudstack-agent-profile.sh ${RPM_BUILD_ROOT}%{_sysconfdir}/profile.d/%{name}-agent-profile.sh +install -D agent/target/transformed/cloudstack-agent.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-agent +install -D plugins/hypervisors/kvm/target/cloud-plugin-hypervisor-kvm-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%name-agent/lib/cloud-plugin-hypervisor-kvm-%{_maventag}.jar +cp plugins/hypervisors/kvm/target/dependencies/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib +cp plugins/storage/volume/storpool/target/*.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib +cp plugins/storage/volume/linstor/target/*.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib + +# Usage server +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/usage +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-usage/lib +install -D usage/target/cloud-usage-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-usage/cloud-usage-%{_maventag}.jar +install -D usage/target/transformed/db.properties ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/usage/db.properties +install -D usage/target/transformed/log4j-cloud_usage.xml ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/usage/log4j-cloud.xml +cp usage/target/dependencies/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-usage/lib/ +cp client/target/lib/mysql*jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-usage/lib/ +install -D packaging/systemd/cloudstack-usage.service ${RPM_BUILD_ROOT}%{_unitdir}/%{name}-usage.service +install -D packaging/systemd/cloudstack-usage.default ${RPM_BUILD_ROOT}%{_sysconfdir}/default/%{name}-usage +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/usage/ +install -D usage/target/transformed/cloudstack-usage.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-usage + +# Marvin +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-marvin +cp tools/marvin/dist/Marvin-*.tar.gz ${RPM_BUILD_ROOT}%{_datadir}/%{name}-marvin/ + +# integration-tests +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-integration-tests +cp -r test/integration/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-integration-tests/ + +# MYSQL HA +if [ "x%{_ossnoss}" == "xnoredist" ] ; then + mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-mysql-ha/lib + cp -r plugins/database/mysql-ha/target/cloud-plugin-database-mysqlha-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-mysql-ha/lib +fi + +#License files from whisker +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-management-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-management-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-common-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-common-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-agent-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-agent-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-usage-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-usage-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-ui-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-ui-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-marvin-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-marvin-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-integration-tests-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-integration-tests-%{version}/LICENSE + +%clean +[ ${RPM_BUILD_ROOT} != "/" ] && rm -rf ${RPM_BUILD_ROOT} + +%posttrans common + +unalias cp +python_dir=$(python3 -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))") +if [ ! -z $python_dir ];then + cp -f -r /usr/share/cloudstack-common/python-site/* $python_dir/ +fi + +%preun management +/usr/bin/systemctl stop cloudstack-management || true +/usr/bin/systemctl disable cloudstack-management || true + +%pre management +id cloud > /dev/null 2>&1 || /usr/sbin/useradd -M -U -c "CloudStack unprivileged user" \ + -r -s /bin/sh -d %{_localstatedir}/cloudstack/management cloud || true + +rm -rf %{_localstatedir}/cache/cloudstack + +# in case of upgrade to 4.9+ copy commands.properties if not exists in /etc/cloudstack/management/ +if [ "$1" == "2" ] ; then + if [ -f "%{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/commands.properties" ] && [ ! -f "%{_sysconfdir}/%{name}/management/commands.properties" ] ; then + cp -p %{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/commands.properties %{_sysconfdir}/%{name}/management/commands.properties + fi +fi + +# Remove old tomcat symlinks and env config file +if [ -L "%{_datadir}/%{name}-management/lib" ] +then + rm -f %{_datadir}/%{name}-management/bin + rm -f %{_datadir}/%{name}-management/lib + rm -f %{_datadir}/%{name}-management/temp + rm -f %{_datadir}/%{name}-management/work + rm -f %{_sysconfdir}/default/%{name}-management +fi + +%post management +# Install mysql-connector-python wheel +# Detect Python version to install compatible wheel +if python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 7) else 1)'; then + pip3 install %{_datadir}/%{name}-management/setup/wheel/mysql_connector_python-8.3.0-py2.py3-none-any.whl +else + pip3 install %{_datadir}/%{name}-management/setup/wheel/mysql_connector_python-8.0.31-py2.py3-none-any.whl +fi + +/usr/bin/systemctl enable cloudstack-management > /dev/null 2>&1 || true +/usr/bin/systemctl enable --now rngd > /dev/null 2>&1 || true + +grep -s -q "db.cloud.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" || sed -i -e "\$adb.cloud.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "db.usage.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" || sed -i -e "\$adb.usage.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "db.simulator.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" || sed -i -e "\$adb.simulator.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" + +# Update DB properties having master and slave(s), with source and replica(s) respectively (for inclusiveness) +grep -s -q "^db.cloud.slaves=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.cloud.slaves=/db.cloud.replicas=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.cloud.secondsBeforeRetryMaster=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.cloud.secondsBeforeRetryMaster=/db.cloud.secondsBeforeRetrySource=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.cloud.queriesBeforeRetryMaster=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.cloud.queriesBeforeRetryMaster=/db.cloud.queriesBeforeRetrySource=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.usage.slaves=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.usage.slaves=/db.usage.replicas=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.usage.secondsBeforeRetryMaster=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.usage.secondsBeforeRetryMaster=/db.usage.secondsBeforeRetrySource=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.usage.queriesBeforeRetryMaster=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.usage.queriesBeforeRetryMaster=/db.usage.queriesBeforeRetrySource=/g" "%{_sysconfdir}/%{name}/management/db.properties" + +if [ ! -f %{_datadir}/cloudstack-common/scripts/vm/hypervisor/xenserver/vhd-util ] ; then + echo Please download vhd-util from http://download.cloudstack.org/tools/vhd-util and put it in + echo %{_datadir}/cloudstack-common/scripts/vm/hypervisor/xenserver/ +fi + +if [ -f %{_sysconfdir}/sysconfig/%{name}-management ] ; then + rm -f %{_sysconfdir}/sysconfig/%{name}-management +fi + +chown -R cloud:cloud /var/log/cloudstack/management +chown -R cloud:cloud /usr/share/cloudstack-management/templates +find /usr/share/cloudstack-management/templates -type d -exec chmod 0770 {} \; + +systemctl daemon-reload + +%posttrans management +# Print help message +if [ -f "/usr/share/cloudstack-common/scripts/installer/cloudstack-help-text" ];then + sed -i "s,^ACS_VERSION=.*,ACS_VERSION=%{_maventag},g" /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text + /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text management +fi + +%preun agent +/sbin/service cloudstack-agent stop || true +if [ "$1" == "0" ] ; then + /sbin/chkconfig --del cloudstack-agent > /dev/null 2>&1 || true +fi + +%pre agent + +# save old configs if they exist (for upgrade). Otherwise we may lose them +# when the old packages are erased. There are a lot of properties files here. +if [ -d "%{_sysconfdir}/cloud" ] ; then + mv %{_sysconfdir}/cloud %{_sysconfdir}/cloud.rpmsave +fi + +%posttrans agent + +if [ "$1" == "2" ] ; then + echo "Running %{_bindir}/%{name}-agent-upgrade to update bridge name for upgrade from CloudStack 4.0.x (and before) to CloudStack 4.1 (and later)" + %{_bindir}/%{name}-agent-upgrade +fi +if [ ! -d %{_sysconfdir}/libvirt/hooks ] ; then + mkdir %{_sysconfdir}/libvirt/hooks +fi +cp -a ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib/libvirtqemuhook %{_sysconfdir}/libvirt/hooks/qemu +mkdir -m 0755 -p /usr/share/cloudstack-agent/tmp +/usr/bin/systemctl restart libvirtd +/usr/bin/systemctl enable cloudstack-agent > /dev/null 2>&1 || true +/usr/bin/systemctl enable cloudstack-rolling-maintenance@p > /dev/null 2>&1 || true +/usr/bin/systemctl enable --now rngd > /dev/null 2>&1 || true + +# if saved agent.properties from upgrade exist, copy them over +if [ -f "%{_sysconfdir}/cloud.rpmsave/agent/agent.properties" ]; then + mv %{_sysconfdir}/%{name}/agent/agent.properties %{_sysconfdir}/%{name}/agent/agent.properties.rpmnew + cp -p %{_sysconfdir}/cloud.rpmsave/agent/agent.properties %{_sysconfdir}/%{name}/agent + # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall + mv %{_sysconfdir}/cloud.rpmsave/agent/agent.properties %{_sysconfdir}/cloud.rpmsave/agent/agent.properties.rpmsave +fi + +# if saved uefi.properties from upgrade exist, copy them over +if [ -f "%{_sysconfdir}/cloud.rpmsave/agent/uefi.properties" ]; then + mv %{_sysconfdir}/%{name}/agent/uefi.properties %{_sysconfdir}/%{name}/agent/uefi.properties.rpmnew + cp -p %{_sysconfdir}/cloud.rpmsave/agent/uefi.properties %{_sysconfdir}/%{name}/agent + # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall + mv %{_sysconfdir}/cloud.rpmsave/agent/uefi.properties %{_sysconfdir}/cloud.rpmsave/agent/uefi.properties.rpmsave +fi + +systemctl daemon-reload + +# Print help message +if [ -f "/usr/share/cloudstack-common/scripts/installer/cloudstack-help-text" ];then + sed -i "s,^ACS_VERSION=.*,ACS_VERSION=%{_maventag},g" /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text + /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text agent +fi + +%pre usage +id cloud > /dev/null 2>&1 || /usr/sbin/useradd -M -U -c "CloudStack unprivileged user" \ + -r -s /bin/sh -d %{_localstatedir}/cloudstack/management cloud|| true + +%preun usage +/sbin/service cloudstack-usage stop || true +if [ "$1" == "0" ] ; then + /sbin/chkconfig --del cloudstack-usage > /dev/null 2>&1 || true +fi + +%post usage +if [ -f "%{_sysconfdir}/%{name}/management/db.properties" ]; then + echo "Replacing usage server's db.properties with a link to the management server's db.properties" + rm -f %{_sysconfdir}/%{name}/usage/db.properties + ln -s %{_sysconfdir}/%{name}/management/db.properties %{_sysconfdir}/%{name}/usage/db.properties + /usr/bin/systemctl enable cloudstack-usage > /dev/null 2>&1 || true +fi + +if [ -f "%{_sysconfdir}/%{name}/management/key" ]; then + echo "Replacing usage server's key with a link to the management server's key" + rm -f %{_sysconfdir}/%{name}/usage/key + ln -s %{_sysconfdir}/%{name}/management/key %{_sysconfdir}/%{name}/usage/key +fi + +if [ ! -f "%{_sysconfdir}/%{name}/usage/key" ]; then + ln -s %{_sysconfdir}/%{name}/management/key %{_sysconfdir}/%{name}/usage/key +fi + +mkdir -p /usr/local/libexec +if [ ! -f "/usr/local/libexec/sanity-check-last-id" ]; then + echo 1 > /usr/local/libexec/sanity-check-last-id +fi +chown cloud:cloud /usr/local/libexec/sanity-check-last-id + +%posttrans usage +# Print help message +if [ -f "/usr/share/cloudstack-common/scripts/installer/cloudstack-help-text" ];then + sed -i "s,^ACS_VERSION=.*,ACS_VERSION=%{_maventag},g" /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text + /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text usage +fi + +%post marvin +pip3 install --upgrade https://files.pythonhosted.org/packages/08/1f/42d74bae9dd6dcfec67c9ed0f3fa482b1ae5ac5f117ca82ab589ecb3ca19/mysql_connector_python-8.0.31-py2.py3-none-any.whl +pip3 install --upgrade /usr/share/cloudstack-marvin/Marvin-*.tar.gz + +#No default permission as the permission setup is complex +%files management +%defattr(-,root,root,-) +%dir %{_datadir}/%{name}-management +%dir %attr(0770,root,cloud) %{_localstatedir}/%{name}/mnt +%dir %attr(0770,cloud,cloud) %{_localstatedir}/%{name}/management +%dir %attr(0770,root,cloud) %{_localstatedir}/cache/%{name}/management +%dir %attr(0770,root,cloud) %{_localstatedir}/log/%{name}/management +%config(noreplace) %{_sysconfdir}/default/%{name}-management +%config(noreplace) %{_sysconfdir}/sudoers.d/%{name}-management +%config(noreplace) %{_sysconfdir}/security/limits.d/cloud +%config(noreplace) %{_sysconfdir}/systemd/system/%{name}-management.service.d +%config(noreplace) %attr(0640,root,cloud) %{_sysconfdir}/%{name}/management/db.properties +%config(noreplace) %attr(0640,root,cloud) %{_sysconfdir}/%{name}/management/server.properties +%config(noreplace) %attr(0640,root,cloud) %{_sysconfdir}/%{name}/management/config.json +%config(noreplace) %{_sysconfdir}/%{name}/management/log4j-cloud.xml +%config(noreplace) %{_sysconfdir}/%{name}/management/log4j2.xml +%config(noreplace) %{_sysconfdir}/%{name}/management/environment.properties +%config(noreplace) %{_sysconfdir}/%{name}/management/java.security.ciphers +%config(noreplace) %attr(0644,root,root) %{_sysconfdir}/logrotate.d/%{name}-management +%attr(0644,root,root) %{_unitdir}/%{name}-management.service +%attr(0755,cloud,cloud) %{_localstatedir}/run/%{name}-management.pid +%attr(0755,root,root) %{_bindir}/%{name}-setup-management +%attr(0755,root,root) %{_bindir}/%{name}-update-xenserver-licenses +%{_datadir}/%{name}-management/conf +%{_datadir}/%{name}-management/lib/*.jar +%{_datadir}/%{name}-management/logs +%{_datadir}/%{name}-management/templates +%{_datadir}/%{name}-management/extensions +%attr(0755,root,root) %{_bindir}/%{name}-setup-databases +%attr(0755,root,root) %{_bindir}/%{name}-migrate-databases +%attr(0755,root,root) %{_bindir}/%{name}-set-guest-password +%attr(0755,root,root) %{_bindir}/%{name}-set-guest-sshkey +%attr(0755,root,root) %{_bindir}/%{name}-sysvmadm +%attr(0755,root,root) %{_bindir}/%{name}-setup-encryption +%attr(0755,root,root) %{_bindir}/cmk +%{_datadir}/%{name}-management/cks/conf/*.yml +%{_datadir}/%{name}-management/setup/*.sql +%{_datadir}/%{name}-management/setup/*.sh +%{_datadir}/%{name}-management/setup/server-setup.xml +%{_datadir}/%{name}-management/webapp/* +%dir %attr(0770, cloud, cloud) %{_datadir}/%{name}-management/templates +%dir %attr(0770, cloud, cloud) %{_datadir}/%{name}-management/templates/systemvm +%attr(0644, cloud, cloud) %{_datadir}/%{name}-management/templates/systemvm/* +%attr(0755,root,root) %{_bindir}/%{name}-external-ipallocator.py +%attr(0755,root,root) %{_initrddir}/%{name}-ipallocator +%dir %attr(0770,root,root) %{_localstatedir}/log/%{name}/ipallocator +%{_defaultdocdir}/%{name}-management-%{version}/LICENSE +%{_defaultdocdir}/%{name}-management-%{version}/NOTICE +%{_datadir}/%{name}-management/setup/wheel/*.whl +%dir %attr(0755,cloud,cloud) %{_sysconfdir}/%{name}/extensions +%attr(0755,cloud,cloud) %{_sysconfdir}/%{name}/extensions/* + +%files agent +%attr(0755,root,root) %{_bindir}/%{name}-setup-agent +%attr(0755,root,root) %{_bindir}/%{name}-agent-upgrade +%attr(0755,root,root) %{_bindir}/%{name}-guest-tool +%attr(0755,root,root) %{_bindir}/%{name}-ssh +%attr(0644,root,root) %{_unitdir}/%{name}-agent.service +%attr(0644,root,root) %{_unitdir}/%{name}-rolling-maintenance@.service +%config(noreplace) %{_sysconfdir}/default/%{name}-agent +%attr(0644,root,root) %{_sysconfdir}/profile.d/%{name}-agent-profile.sh +%config(noreplace) %attr(0644,root,root) %{_sysconfdir}/logrotate.d/%{name}-agent +%attr(0755,root,root) %{_datadir}/%{name}-common/scripts/network/cisco +%config(noreplace) %{_sysconfdir}/%{name}/agent +%dir %{_localstatedir}/log/%{name}/agent +%attr(0644,root,root) %{_datadir}/%{name}-agent/lib/*.jar +%attr(0755,root,root) %{_datadir}/%{name}-agent/lib/libvirtqemuhook +%attr(0755,root,root) %{_datadir}/%{name}-agent/lib/rolling-maintenance +%dir %{_datadir}/%{name}-agent/plugins +%{_defaultdocdir}/%{name}-agent-%{version}/LICENSE +%{_defaultdocdir}/%{name}-agent-%{version}/NOTICE + +%files common +%dir %attr(0755,root,root) %{_datadir}/%{name}-common/python-site/cloudutils +%dir %attr(0755,root,root) %{_datadir}/%{name}-common/vms +%attr(0755,root,root) %{_datadir}/%{name}-common/scripts +%attr(0755,root,root) /usr/bin/cloudstack-sccs +%attr(0644, root, root) %{_datadir}/%{name}-common/vms/agent.zip +%attr(0644, root, root) %{_datadir}/%{name}-common/vms/cloud-scripts.tgz +%attr(0644, root, root) %{_datadir}/%{name}-common/vms/patch-sysvms.sh +%attr(0644,root,root) %{_datadir}/%{name}-common/python-site/cloud_utils.py +%attr(0644,root,root) %{_datadir}/%{name}-common/python-site/__pycache__/* +%attr(0644,root,root) %{_datadir}/%{name}-common/python-site/cloudutils/* +%attr(0644, root, root) %{_datadir}/%{name}-common/lib/jasypt-1.9.3.jar +%attr(0644, root, root) %{_datadir}/%{name}-common/lib/%{name}-utils.jar +%{_defaultdocdir}/%{name}-common-%{version}/LICENSE +%{_defaultdocdir}/%{name}-common-%{version}/NOTICE + +%files ui +%config(noreplace) %attr(0640,root,cloud) %{_sysconfdir}/%{name}/ui/config.json +%{_datadir}/%{name}-ui/* +%{_defaultdocdir}/%{name}-ui-%{version}/LICENSE +%{_defaultdocdir}/%{name}-ui-%{version}/NOTICE + +%files usage +%attr(0644,root,root) %{_unitdir}/%{name}-usage.service +%config(noreplace) %{_sysconfdir}/default/%{name}-usage +%config(noreplace) %attr(0644,root,root) %{_sysconfdir}/logrotate.d/%{name}-usage +%attr(0644,root,root) %{_datadir}/%{name}-usage/*.jar +%attr(0644,root,root) %{_datadir}/%{name}-usage/lib/*.jar +%dir %attr(0770,root,cloud) %{_localstatedir}/log/%{name}/usage +%attr(0644,root,root) %{_sysconfdir}/%{name}/usage/db.properties +%attr(0644,root,root) %{_sysconfdir}/%{name}/usage/log4j-cloud.xml +%{_defaultdocdir}/%{name}-usage-%{version}/LICENSE +%{_defaultdocdir}/%{name}-usage-%{version}/NOTICE + +%files marvin +%attr(0644,root,root) %{_datadir}/%{name}-marvin/Marvin*.tar.gz +%{_defaultdocdir}/%{name}-marvin-%{version}/LICENSE +%{_defaultdocdir}/%{name}-marvin-%{version}/NOTICE + +%files integration-tests +%attr(0755,root,root) %{_datadir}/%{name}-integration-tests/* +%{_defaultdocdir}/%{name}-integration-tests-%{version}/LICENSE +%{_defaultdocdir}/%{name}-integration-tests-%{version}/NOTICE + +%if "%{_ossnoss}" == "noredist" +%files mysql-ha +%defattr(0644,cloud,cloud,0755) +%attr(0644,root,root) %{_datadir}/%{name}-mysql-ha/lib/* +%endif + +%files baremetal-agent +%attr(0755,root,root) %{_bindir}/cloudstack-setup-baremetal + +%changelog +* Thu Dec 22 2022 Rohit Yadav 4.18.0 +- Add support for EL9 + +* Fri Oct 14 2022 Daan Hoogland 4.18.0 +- initialising sanity check pointer file + +* Tue Jun 29 2021 David Jumani 4.16.0 +- Adding SUSE 15 support + +* Thu Apr 30 2015 Rohit Yadav 4.6.0 +- Remove awsapi package + +* Wed Nov 19 2014 Hugo Trippaers 4.6.0 +- Create a specific spec for CentOS 7 + +* Fri Jul 4 2014 Hugo Trippaers 4.5.0 +- Add a package for the mysql ha module + +* Fri Oct 5 2012 Hugo Trippaers 4.1.0 +- new style spec file diff --git a/packaging/suse15/cloudstack-agent.te b/packaging/suse15/cloudstack-agent.te new file mode 120000 index 000000000000..30e123f6cba5 --- /dev/null +++ b/packaging/suse15/cloudstack-agent.te @@ -0,0 +1 @@ +../el8/cloudstack-agent.te \ No newline at end of file diff --git a/packaging/suse15/cloudstack-sccs b/packaging/suse15/cloudstack-sccs new file mode 120000 index 000000000000..b9e6ed9dc085 --- /dev/null +++ b/packaging/suse15/cloudstack-sccs @@ -0,0 +1 @@ +../el8/cloudstack-sccs \ No newline at end of file diff --git a/packaging/suse15/filelimit.conf b/packaging/suse15/filelimit.conf new file mode 120000 index 000000000000..c71688ea640a --- /dev/null +++ b/packaging/suse15/filelimit.conf @@ -0,0 +1 @@ +../el8/filelimit.conf \ No newline at end of file diff --git a/packaging/suse15/replace.properties b/packaging/suse15/replace.properties new file mode 100644 index 000000000000..b1900af83406 --- /dev/null +++ b/packaging/suse15/replace.properties @@ -0,0 +1,65 @@ +# 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. + +DBUSER=cloud +DBPW=cloud +DBROOTPW= +MSLOG=vmops.log +APISERVERLOG=api.log +DBHOST=localhost +DBDRIVER=jdbc:mysql +COMPONENTS-SPEC=components-premium.xml +REMOTEHOST=localhost +AGENTCLASSPATH= +AGENTLOG=/var/log/cloudstack/agent/agent.log +AGENTLOGDIR=/var/log/cloudstack/agent/ +AGENTSYSCONFDIR=/etc/cloudstack/agent +APISERVERLOG=/var/log/cloudstack/management/apilog.log +BINDIR=/usr/bin +COMMONLIBDIR=/usr/share/cloudstack-common +CONFIGUREVARS= +DEPSCLASSPATH= +DOCDIR= +IPALOCATORLOG=/var/log/cloudstack/management/ipallocator.log +JAVADIR=/usr/share/java +LIBEXECDIR=/usr/libexec +LOCKDIR=/var/lock +MSCLASSPATH= +MSCONF=/etc/cloudstack/management +MSENVIRON=/usr/share/cloudstack-management +MSLOG=/var/log/cloudstack/management/management-server.log +MSLOGDIR=/var/log/cloudstack/management/ +MSMNTDIR=/var/cloudstack/mnt +MSUSER=cloud +PIDDIR=/var/run +PLUGINJAVADIR=/usr/share/cloudstack-management/plugin +PREMIUMJAVADIR=/usr/share/cloudstack-management/premium +PYTHONDIR=/usr/share/cloudstack-common/python-site/ +SERVERSYSCONFDIR=/etc/sysconfig +SETUPDATADIR=/usr/share/cloudstack-management/setup +SYSCONFDIR=/etc/sysconfig +SYSTEMCLASSPATH= +SYSTEMJARS= +USAGECLASSPATH= +USAGELOG=/var/log/cloudstack/usage/usage.log +USAGESYSCONFDIR=/etc/sysconfig +EXTENSIONSDEPLOYMENTMODE=production +GUESTNVRAMTEMPLATELEGACY=/usr/share/qemu/ovmf-x86_64-vars.bin +GUESTLOADERLEGACY=/usr/share/qemu/ovmf-x86_64-code.bin +GUESTNVRAMTEMPLATESECURE=/usr/share/qemu/ovmf-x86_64-ms-vars.bin +GUESTLOADERSECURE=/usr/share/qemu/ovmf-x86_64-ms-code.bin +GUESTNVRAMPATH=/var/lib/libvirt/qemu/nvram/ From 348ce953a99246a756b527994f7745a7be038234 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 11 May 2026 20:26:07 +0530 Subject: [PATCH 057/205] Updating pom.xml version numbers for release 4.22.1.0 Signed-off-by: Suresh Kumar Anaparti --- agent/pom.xml | 2 +- api/pom.xml | 2 +- client/pom.xml | 2 +- core/pom.xml | 2 +- debian/changelog | 6 ++++++ developer/pom.xml | 2 +- engine/api/pom.xml | 2 +- engine/components-api/pom.xml | 2 +- engine/orchestration/pom.xml | 2 +- engine/pom.xml | 2 +- engine/schema/pom.xml | 2 +- engine/service/pom.xml | 2 +- engine/storage/cache/pom.xml | 2 +- engine/storage/configdrive/pom.xml | 2 +- engine/storage/datamotion/pom.xml | 2 +- engine/storage/image/pom.xml | 2 +- engine/storage/integration-test/pom.xml | 2 +- engine/storage/object/pom.xml | 2 +- engine/storage/pom.xml | 2 +- engine/storage/snapshot/pom.xml | 2 +- engine/storage/volume/pom.xml | 2 +- engine/userdata/cloud-init/pom.xml | 2 +- engine/userdata/pom.xml | 2 +- framework/agent-lb/pom.xml | 2 +- framework/ca/pom.xml | 2 +- framework/cluster/pom.xml | 2 +- framework/config/pom.xml | 2 +- framework/db/pom.xml | 2 +- framework/direct-download/pom.xml | 2 +- framework/events/pom.xml | 2 +- framework/extensions/pom.xml | 6 +++--- framework/ipc/pom.xml | 2 +- framework/jobs/pom.xml | 2 +- framework/managed-context/pom.xml | 2 +- framework/pom.xml | 2 +- framework/quota/pom.xml | 2 +- framework/rest/pom.xml | 2 +- framework/security/pom.xml | 2 +- framework/spring/lifecycle/pom.xml | 2 +- framework/spring/module/pom.xml | 2 +- plugins/acl/dynamic-role-based/pom.xml | 2 +- plugins/acl/project-role-based/pom.xml | 2 +- plugins/acl/static-role-based/pom.xml | 2 +- .../affinity-group-processors/explicit-dedication/pom.xml | 2 +- plugins/affinity-group-processors/host-affinity/pom.xml | 2 +- .../affinity-group-processors/host-anti-affinity/pom.xml | 2 +- .../non-strict-host-affinity/pom.xml | 2 +- .../non-strict-host-anti-affinity/pom.xml | 2 +- plugins/alert-handlers/snmp-alerts/pom.xml | 2 +- plugins/alert-handlers/syslog-alerts/pom.xml | 2 +- plugins/api/discovery/pom.xml | 2 +- plugins/api/rate-limit/pom.xml | 2 +- plugins/api/solidfire-intg-test/pom.xml | 2 +- plugins/api/vmware-sioc/pom.xml | 2 +- plugins/backup/dummy/pom.xml | 2 +- plugins/backup/nas/pom.xml | 2 +- plugins/backup/networker/pom.xml | 2 +- plugins/backup/veeam/pom.xml | 2 +- plugins/ca/root-ca/pom.xml | 2 +- plugins/database/mysql-ha/pom.xml | 2 +- plugins/database/quota/pom.xml | 2 +- plugins/dedicated-resources/pom.xml | 2 +- plugins/deployment-planners/implicit-dedication/pom.xml | 2 +- plugins/deployment-planners/user-concentrated-pod/pom.xml | 2 +- plugins/deployment-planners/user-dispersing/pom.xml | 2 +- plugins/drs/cluster/balanced/pom.xml | 2 +- plugins/drs/cluster/condensed/pom.xml | 2 +- plugins/event-bus/inmemory/pom.xml | 2 +- plugins/event-bus/kafka/pom.xml | 2 +- plugins/event-bus/rabbitmq/pom.xml | 2 +- plugins/event-bus/webhook/pom.xml | 2 +- plugins/ha-planners/skip-heurestics/pom.xml | 2 +- plugins/host-allocators/random/pom.xml | 2 +- plugins/hypervisors/baremetal/pom.xml | 2 +- plugins/hypervisors/external/pom.xml | 2 +- plugins/hypervisors/hyperv/pom.xml | 2 +- plugins/hypervisors/kvm/pom.xml | 2 +- plugins/hypervisors/simulator/pom.xml | 2 +- plugins/hypervisors/ucs/pom.xml | 2 +- plugins/hypervisors/vmware/pom.xml | 2 +- plugins/hypervisors/xenserver/pom.xml | 2 +- plugins/integrations/cloudian/pom.xml | 2 +- plugins/integrations/kubernetes-service/pom.xml | 2 +- plugins/integrations/prometheus/pom.xml | 2 +- plugins/maintenance/pom.xml | 2 +- plugins/metrics/pom.xml | 2 +- plugins/network-elements/bigswitch/pom.xml | 2 +- plugins/network-elements/brocade-vcs/pom.xml | 2 +- plugins/network-elements/cisco-vnmc/pom.xml | 2 +- plugins/network-elements/dns-notifier/pom.xml | 2 +- plugins/network-elements/elastic-loadbalancer/pom.xml | 2 +- plugins/network-elements/globodns/pom.xml | 2 +- plugins/network-elements/internal-loadbalancer/pom.xml | 2 +- plugins/network-elements/juniper-contrail/pom.xml | 2 +- plugins/network-elements/netris/pom.xml | 2 +- plugins/network-elements/netscaler/pom.xml | 2 +- plugins/network-elements/nicira-nvp/pom.xml | 2 +- plugins/network-elements/nsx/pom.xml | 2 +- plugins/network-elements/opendaylight/pom.xml | 2 +- plugins/network-elements/ovs/pom.xml | 2 +- plugins/network-elements/palo-alto/pom.xml | 2 +- plugins/network-elements/stratosphere-ssp/pom.xml | 2 +- plugins/network-elements/tungsten/pom.xml | 2 +- plugins/network-elements/vxlan/pom.xml | 2 +- plugins/outofbandmanagement-drivers/ipmitool/pom.xml | 2 +- .../outofbandmanagement-drivers/nested-cloudstack/pom.xml | 2 +- plugins/outofbandmanagement-drivers/redfish/pom.xml | 2 +- plugins/pom.xml | 2 +- plugins/storage-allocators/random/pom.xml | 2 +- plugins/storage/image/default/pom.xml | 2 +- plugins/storage/image/s3/pom.xml | 2 +- plugins/storage/image/sample/pom.xml | 2 +- plugins/storage/image/swift/pom.xml | 2 +- plugins/storage/object/ceph/pom.xml | 2 +- plugins/storage/object/cloudian/pom.xml | 2 +- plugins/storage/object/minio/pom.xml | 2 +- plugins/storage/object/simulator/pom.xml | 2 +- plugins/storage/sharedfs/storagevm/pom.xml | 2 +- plugins/storage/volume/adaptive/pom.xml | 2 +- plugins/storage/volume/cloudbyte/pom.xml | 2 +- plugins/storage/volume/datera/pom.xml | 2 +- plugins/storage/volume/default/pom.xml | 2 +- plugins/storage/volume/flasharray/pom.xml | 2 +- plugins/storage/volume/linstor/pom.xml | 2 +- plugins/storage/volume/nexenta/pom.xml | 2 +- plugins/storage/volume/primera/pom.xml | 2 +- plugins/storage/volume/sample/pom.xml | 2 +- plugins/storage/volume/scaleio/pom.xml | 2 +- plugins/storage/volume/solidfire/pom.xml | 2 +- plugins/storage/volume/storpool/pom.xml | 2 +- plugins/user-authenticators/ldap/pom.xml | 2 +- plugins/user-authenticators/md5/pom.xml | 2 +- plugins/user-authenticators/oauth2/pom.xml | 2 +- plugins/user-authenticators/pbkdf2/pom.xml | 2 +- plugins/user-authenticators/plain-text/pom.xml | 2 +- plugins/user-authenticators/saml2/pom.xml | 2 +- plugins/user-authenticators/sha256salted/pom.xml | 2 +- plugins/user-two-factor-authenticators/static-pin/pom.xml | 2 +- plugins/user-two-factor-authenticators/totp/pom.xml | 2 +- pom.xml | 2 +- quickcloud/pom.xml | 2 +- server/pom.xml | 4 ++-- services/console-proxy/pom.xml | 2 +- services/console-proxy/rdpconsole/pom.xml | 2 +- services/console-proxy/server/pom.xml | 2 +- services/pom.xml | 2 +- services/secondary-storage/controller/pom.xml | 2 +- services/secondary-storage/pom.xml | 2 +- services/secondary-storage/server/pom.xml | 2 +- systemvm/pom.xml | 2 +- test/pom.xml | 2 +- tools/apidoc/pom.xml | 2 +- tools/checkstyle/pom.xml | 2 +- tools/devcloud-kvm/pom.xml | 2 +- tools/devcloud4/pom.xml | 2 +- tools/docker/Dockerfile | 2 +- tools/docker/Dockerfile.marvin | 4 ++-- tools/marvin/pom.xml | 2 +- tools/marvin/setup.py | 2 +- tools/pom.xml | 2 +- usage/pom.xml | 2 +- utils/pom.xml | 2 +- vmware-base/pom.xml | 2 +- 163 files changed, 172 insertions(+), 166 deletions(-) diff --git a/agent/pom.xml b/agent/pom.xml index 4fa30e4f78e2..70fdea6d0c59 100644 --- a/agent/pom.xml +++ b/agent/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 diff --git a/api/pom.xml b/api/pom.xml index 405365451c6f..03abe9ee30bf 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 diff --git a/client/pom.xml b/client/pom.xml index 94d844be3c42..e97b5474d733 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 diff --git a/core/pom.xml b/core/pom.xml index 1bf7c28674d6..8012f3747ada 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 diff --git a/debian/changelog b/debian/changelog index 6d288afc4db0..bdd67f8f91e1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +cloudstack (4.22.1.0) unstable; urgency=low + + * Update the version to 4.22.1.0 + + -- the Apache CloudStack project Mon, 11 May 2026 20:26:07 +0530 + cloudstack (4.22.1.0-SNAPSHOT) unstable; urgency=low * Update the version to 4.22.1.0-SNAPSHOT diff --git a/developer/pom.xml b/developer/pom.xml index de6a8ef3d10a..b22140ea70e1 100644 --- a/developer/pom.xml +++ b/developer/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 diff --git a/engine/api/pom.xml b/engine/api/pom.xml index 2f7e15aaab05..e23593f34ff7 100644 --- a/engine/api/pom.xml +++ b/engine/api/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/engine/components-api/pom.xml b/engine/components-api/pom.xml index 8caf8ccbff69..940fd8050f3e 100644 --- a/engine/components-api/pom.xml +++ b/engine/components-api/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/engine/orchestration/pom.xml b/engine/orchestration/pom.xml index cd5578d245ca..44b57a821479 100755 --- a/engine/orchestration/pom.xml +++ b/engine/orchestration/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/engine/pom.xml b/engine/pom.xml index 821a4f8f54ce..dcd65661f57b 100644 --- a/engine/pom.xml +++ b/engine/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/engine/schema/pom.xml b/engine/schema/pom.xml index 7d88f649245c..ceedce8a140d 100644 --- a/engine/schema/pom.xml +++ b/engine/schema/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/engine/service/pom.xml b/engine/service/pom.xml index c429ebf5da5a..fa324fb9398d 100644 --- a/engine/service/pom.xml +++ b/engine/service/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 cloud-engine-service war diff --git a/engine/storage/cache/pom.xml b/engine/storage/cache/pom.xml index 2e736ad42e55..1eda5b66aaf2 100644 --- a/engine/storage/cache/pom.xml +++ b/engine/storage/cache/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/engine/storage/configdrive/pom.xml b/engine/storage/configdrive/pom.xml index d5ef49d7ef78..d176d00568d6 100644 --- a/engine/storage/configdrive/pom.xml +++ b/engine/storage/configdrive/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/engine/storage/datamotion/pom.xml b/engine/storage/datamotion/pom.xml index aa5a6d1f8f87..ff4bfa5877d1 100644 --- a/engine/storage/datamotion/pom.xml +++ b/engine/storage/datamotion/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/engine/storage/image/pom.xml b/engine/storage/image/pom.xml index 0232e404f5a7..bdb5f9e9bf87 100644 --- a/engine/storage/image/pom.xml +++ b/engine/storage/image/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/engine/storage/integration-test/pom.xml b/engine/storage/integration-test/pom.xml index 2ca31465e08d..6fccacdcb41e 100644 --- a/engine/storage/integration-test/pom.xml +++ b/engine/storage/integration-test/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/engine/storage/object/pom.xml b/engine/storage/object/pom.xml index a9e3aeac4674..6fee3d658fdc 100644 --- a/engine/storage/object/pom.xml +++ b/engine/storage/object/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/engine/storage/pom.xml b/engine/storage/pom.xml index 72e9def90a3d..7529adfd0dd4 100644 --- a/engine/storage/pom.xml +++ b/engine/storage/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/engine/storage/snapshot/pom.xml b/engine/storage/snapshot/pom.xml index 14b0ce92f8b0..7fa2d4056043 100644 --- a/engine/storage/snapshot/pom.xml +++ b/engine/storage/snapshot/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/engine/storage/volume/pom.xml b/engine/storage/volume/pom.xml index 42399bc75622..220989ddcd95 100644 --- a/engine/storage/volume/pom.xml +++ b/engine/storage/volume/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/engine/userdata/cloud-init/pom.xml b/engine/userdata/cloud-init/pom.xml index 2b5d55948ede..460a18a8f206 100644 --- a/engine/userdata/cloud-init/pom.xml +++ b/engine/userdata/cloud-init/pom.xml @@ -23,7 +23,7 @@ cloud-engine org.apache.cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/engine/userdata/pom.xml b/engine/userdata/pom.xml index 001825841565..7b052903d170 100644 --- a/engine/userdata/pom.xml +++ b/engine/userdata/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/agent-lb/pom.xml b/framework/agent-lb/pom.xml index cba750d3c7fb..c36a01aaf45d 100644 --- a/framework/agent-lb/pom.xml +++ b/framework/agent-lb/pom.xml @@ -24,7 +24,7 @@ cloudstack-framework org.apache.cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml
diff --git a/framework/ca/pom.xml b/framework/ca/pom.xml index e7d85c089bb7..49637c6f2b6b 100644 --- a/framework/ca/pom.xml +++ b/framework/ca/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml
diff --git a/framework/cluster/pom.xml b/framework/cluster/pom.xml index da3de5ebf751..bd6bb84f9443 100644 --- a/framework/cluster/pom.xml +++ b/framework/cluster/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/config/pom.xml b/framework/config/pom.xml index 9fbbc4a0cd72..38f45870305c 100644 --- a/framework/config/pom.xml +++ b/framework/config/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/db/pom.xml b/framework/db/pom.xml index c03fb0654bd8..1d53484a6a90 100644 --- a/framework/db/pom.xml +++ b/framework/db/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/direct-download/pom.xml b/framework/direct-download/pom.xml index 913bef072a04..688f23a25cea 100644 --- a/framework/direct-download/pom.xml +++ b/framework/direct-download/pom.xml @@ -32,7 +32,7 @@ cloudstack-framework org.apache.cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/events/pom.xml b/framework/events/pom.xml index 173b18a4df2e..5bf3cfeea21a 100644 --- a/framework/events/pom.xml +++ b/framework/events/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/extensions/pom.xml b/framework/extensions/pom.xml index 76952555698a..5186d7e95b5f 100644 --- a/framework/extensions/pom.xml +++ b/framework/extensions/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml @@ -41,13 +41,13 @@ org.apache.cloudstack cloud-engine-schema - 4.22.1.0-SNAPSHOT + 4.22.1.0 compile org.apache.cloudstack cloud-engine-components-api - 4.22.1.0-SNAPSHOT + 4.22.1.0 compile diff --git a/framework/ipc/pom.xml b/framework/ipc/pom.xml index 0d13898f3f84..b05854df03a4 100644 --- a/framework/ipc/pom.xml +++ b/framework/ipc/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/jobs/pom.xml b/framework/jobs/pom.xml index f3249727c3a4..99206fc1a446 100644 --- a/framework/jobs/pom.xml +++ b/framework/jobs/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/managed-context/pom.xml b/framework/managed-context/pom.xml index c0950855ef45..dbee01e43087 100644 --- a/framework/managed-context/pom.xml +++ b/framework/managed-context/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/framework/pom.xml b/framework/pom.xml index 02ce0a09b817..e2c52f643cc2 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 diff --git a/framework/quota/pom.xml b/framework/quota/pom.xml index 52f0d658f009..3e0ec54790f2 100644 --- a/framework/quota/pom.xml +++ b/framework/quota/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/rest/pom.xml b/framework/rest/pom.xml index bb69f909ba22..b7f2e526fd38 100644 --- a/framework/rest/pom.xml +++ b/framework/rest/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml cloud-framework-rest diff --git a/framework/security/pom.xml b/framework/security/pom.xml index a36978e4ef64..4f4b7af07e41 100644 --- a/framework/security/pom.xml +++ b/framework/security/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/framework/spring/lifecycle/pom.xml b/framework/spring/lifecycle/pom.xml index ff6b59567dad..1844b803809b 100644 --- a/framework/spring/lifecycle/pom.xml +++ b/framework/spring/lifecycle/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/framework/spring/module/pom.xml b/framework/spring/module/pom.xml index 77a9646a984e..6a1dbade3bf4 100644 --- a/framework/spring/module/pom.xml +++ b/framework/spring/module/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/acl/dynamic-role-based/pom.xml b/plugins/acl/dynamic-role-based/pom.xml index f975afc6e5fa..029fcb5e97b8 100644 --- a/plugins/acl/dynamic-role-based/pom.xml +++ b/plugins/acl/dynamic-role-based/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/acl/project-role-based/pom.xml b/plugins/acl/project-role-based/pom.xml index ae00e850aa7e..dd0b621b395f 100644 --- a/plugins/acl/project-role-based/pom.xml +++ b/plugins/acl/project-role-based/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/acl/static-role-based/pom.xml b/plugins/acl/static-role-based/pom.xml index ee088e3bd432..06996d471b59 100644 --- a/plugins/acl/static-role-based/pom.xml +++ b/plugins/acl/static-role-based/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/affinity-group-processors/explicit-dedication/pom.xml b/plugins/affinity-group-processors/explicit-dedication/pom.xml index 1d5b5303c7e5..e134d687c892 100644 --- a/plugins/affinity-group-processors/explicit-dedication/pom.xml +++ b/plugins/affinity-group-processors/explicit-dedication/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/affinity-group-processors/host-affinity/pom.xml b/plugins/affinity-group-processors/host-affinity/pom.xml index f0c13e050361..75aec801bee8 100644 --- a/plugins/affinity-group-processors/host-affinity/pom.xml +++ b/plugins/affinity-group-processors/host-affinity/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/affinity-group-processors/host-anti-affinity/pom.xml b/plugins/affinity-group-processors/host-anti-affinity/pom.xml index 00895373ca94..d3d2e0b85630 100644 --- a/plugins/affinity-group-processors/host-anti-affinity/pom.xml +++ b/plugins/affinity-group-processors/host-anti-affinity/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml b/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml index fc626289a35d..f6841ecce9c1 100644 --- a/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml +++ b/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml b/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml index b474f7647a9b..7222e80c3dcb 100644 --- a/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml +++ b/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml @@ -32,7 +32,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/alert-handlers/snmp-alerts/pom.xml b/plugins/alert-handlers/snmp-alerts/pom.xml index 3beb2ce8d914..147e81506ec9 100644 --- a/plugins/alert-handlers/snmp-alerts/pom.xml +++ b/plugins/alert-handlers/snmp-alerts/pom.xml @@ -24,7 +24,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/alert-handlers/syslog-alerts/pom.xml b/plugins/alert-handlers/syslog-alerts/pom.xml index 5241ec011436..97f72217933d 100644 --- a/plugins/alert-handlers/syslog-alerts/pom.xml +++ b/plugins/alert-handlers/syslog-alerts/pom.xml @@ -24,7 +24,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/api/discovery/pom.xml b/plugins/api/discovery/pom.xml index 525a5583e51e..655568138cb8 100644 --- a/plugins/api/discovery/pom.xml +++ b/plugins/api/discovery/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/api/rate-limit/pom.xml b/plugins/api/rate-limit/pom.xml index 4d802cabac4f..4e524f32e4e9 100644 --- a/plugins/api/rate-limit/pom.xml +++ b/plugins/api/rate-limit/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/api/solidfire-intg-test/pom.xml b/plugins/api/solidfire-intg-test/pom.xml index ca2a0328cade..01ed3e70362c 100644 --- a/plugins/api/solidfire-intg-test/pom.xml +++ b/plugins/api/solidfire-intg-test/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/api/vmware-sioc/pom.xml b/plugins/api/vmware-sioc/pom.xml index 10a1853fedc1..9c6e783fb331 100644 --- a/plugins/api/vmware-sioc/pom.xml +++ b/plugins/api/vmware-sioc/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/backup/dummy/pom.xml b/plugins/backup/dummy/pom.xml index c15b5dcab63b..2006afc95e74 100644 --- a/plugins/backup/dummy/pom.xml +++ b/plugins/backup/dummy/pom.xml @@ -23,7 +23,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/backup/nas/pom.xml b/plugins/backup/nas/pom.xml index aae1e61970cc..f0c170c2aadd 100644 --- a/plugins/backup/nas/pom.xml +++ b/plugins/backup/nas/pom.xml @@ -25,7 +25,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/backup/networker/pom.xml b/plugins/backup/networker/pom.xml index 55b827e29d14..b52863e89ea8 100644 --- a/plugins/backup/networker/pom.xml +++ b/plugins/backup/networker/pom.xml @@ -25,7 +25,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/backup/veeam/pom.xml b/plugins/backup/veeam/pom.xml index dd145bc5e113..b64a68387ab1 100644 --- a/plugins/backup/veeam/pom.xml +++ b/plugins/backup/veeam/pom.xml @@ -23,7 +23,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/ca/root-ca/pom.xml b/plugins/ca/root-ca/pom.xml index 501ba480b80e..6d9315f701a1 100644 --- a/plugins/ca/root-ca/pom.xml +++ b/plugins/ca/root-ca/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/database/mysql-ha/pom.xml b/plugins/database/mysql-ha/pom.xml index 518443c91e72..9ac00d1a5079 100644 --- a/plugins/database/mysql-ha/pom.xml +++ b/plugins/database/mysql-ha/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/database/quota/pom.xml b/plugins/database/quota/pom.xml index 5ecbefbcf2b6..56b8c0ab4ed6 100644 --- a/plugins/database/quota/pom.xml +++ b/plugins/database/quota/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/dedicated-resources/pom.xml b/plugins/dedicated-resources/pom.xml index 8d4c21e9cb26..a299dc77eb3e 100644 --- a/plugins/dedicated-resources/pom.xml +++ b/plugins/dedicated-resources/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/plugins/deployment-planners/implicit-dedication/pom.xml b/plugins/deployment-planners/implicit-dedication/pom.xml index bfc0e24a8ac8..162b3d2c84c4 100644 --- a/plugins/deployment-planners/implicit-dedication/pom.xml +++ b/plugins/deployment-planners/implicit-dedication/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/deployment-planners/user-concentrated-pod/pom.xml b/plugins/deployment-planners/user-concentrated-pod/pom.xml index ebbd879076c3..bba781a774a6 100644 --- a/plugins/deployment-planners/user-concentrated-pod/pom.xml +++ b/plugins/deployment-planners/user-concentrated-pod/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/deployment-planners/user-dispersing/pom.xml b/plugins/deployment-planners/user-dispersing/pom.xml index d811b5e88d1b..915dbede4f04 100644 --- a/plugins/deployment-planners/user-dispersing/pom.xml +++ b/plugins/deployment-planners/user-dispersing/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/drs/cluster/balanced/pom.xml b/plugins/drs/cluster/balanced/pom.xml index 5267d991b846..3d10f190270f 100644 --- a/plugins/drs/cluster/balanced/pom.xml +++ b/plugins/drs/cluster/balanced/pom.xml @@ -27,7 +27,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/drs/cluster/condensed/pom.xml b/plugins/drs/cluster/condensed/pom.xml index d34f3564db51..a568064537c9 100644 --- a/plugins/drs/cluster/condensed/pom.xml +++ b/plugins/drs/cluster/condensed/pom.xml @@ -27,7 +27,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/event-bus/inmemory/pom.xml b/plugins/event-bus/inmemory/pom.xml index 7704867bc708..e497f6dad9a9 100644 --- a/plugins/event-bus/inmemory/pom.xml +++ b/plugins/event-bus/inmemory/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/event-bus/kafka/pom.xml b/plugins/event-bus/kafka/pom.xml index 5231a723baab..91a78a260d24 100644 --- a/plugins/event-bus/kafka/pom.xml +++ b/plugins/event-bus/kafka/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/event-bus/rabbitmq/pom.xml b/plugins/event-bus/rabbitmq/pom.xml index 738cc7fce6dc..5a16ab17fe4d 100644 --- a/plugins/event-bus/rabbitmq/pom.xml +++ b/plugins/event-bus/rabbitmq/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/event-bus/webhook/pom.xml b/plugins/event-bus/webhook/pom.xml index 2bbd5b55c01b..83b4b77efbc0 100644 --- a/plugins/event-bus/webhook/pom.xml +++ b/plugins/event-bus/webhook/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/ha-planners/skip-heurestics/pom.xml b/plugins/ha-planners/skip-heurestics/pom.xml index e010e7108fc6..272a26bd3a14 100644 --- a/plugins/ha-planners/skip-heurestics/pom.xml +++ b/plugins/ha-planners/skip-heurestics/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/host-allocators/random/pom.xml b/plugins/host-allocators/random/pom.xml index d55436e846d3..bc1fdc55d6ed 100644 --- a/plugins/host-allocators/random/pom.xml +++ b/plugins/host-allocators/random/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/hypervisors/baremetal/pom.xml b/plugins/hypervisors/baremetal/pom.xml index bfdf58884a26..f5914e9b7730 100755 --- a/plugins/hypervisors/baremetal/pom.xml +++ b/plugins/hypervisors/baremetal/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml cloud-plugin-hypervisor-baremetal diff --git a/plugins/hypervisors/external/pom.xml b/plugins/hypervisors/external/pom.xml index 8359b1de3ca3..8cc3a870221e 100644 --- a/plugins/hypervisors/external/pom.xml +++ b/plugins/hypervisors/external/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml cloud-plugin-hypervisor-external diff --git a/plugins/hypervisors/hyperv/pom.xml b/plugins/hypervisors/hyperv/pom.xml index 758cee0ee63f..28f6c412b729 100644 --- a/plugins/hypervisors/hyperv/pom.xml +++ b/plugins/hypervisors/hyperv/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/hypervisors/kvm/pom.xml b/plugins/hypervisors/kvm/pom.xml index 7e3bd3847b19..883012d4cc54 100644 --- a/plugins/hypervisors/kvm/pom.xml +++ b/plugins/hypervisors/kvm/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/hypervisors/simulator/pom.xml b/plugins/hypervisors/simulator/pom.xml index f5942328e3cf..76d989edee13 100644 --- a/plugins/hypervisors/simulator/pom.xml +++ b/plugins/hypervisors/simulator/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml cloud-plugin-hypervisor-simulator diff --git a/plugins/hypervisors/ucs/pom.xml b/plugins/hypervisors/ucs/pom.xml index 521dd7416b5c..8febcda74e0e 100644 --- a/plugins/hypervisors/ucs/pom.xml +++ b/plugins/hypervisors/ucs/pom.xml @@ -23,7 +23,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml cloud-plugin-hypervisor-ucs diff --git a/plugins/hypervisors/vmware/pom.xml b/plugins/hypervisors/vmware/pom.xml index 355d4488ba90..3c3a38da45b4 100644 --- a/plugins/hypervisors/vmware/pom.xml +++ b/plugins/hypervisors/vmware/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/hypervisors/xenserver/pom.xml b/plugins/hypervisors/xenserver/pom.xml index cafa999d42dc..5eee2e47fc98 100644 --- a/plugins/hypervisors/xenserver/pom.xml +++ b/plugins/hypervisors/xenserver/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/integrations/cloudian/pom.xml b/plugins/integrations/cloudian/pom.xml index 69321f9e558c..7799a3e5a348 100644 --- a/plugins/integrations/cloudian/pom.xml +++ b/plugins/integrations/cloudian/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/integrations/kubernetes-service/pom.xml b/plugins/integrations/kubernetes-service/pom.xml index 9aad4d6d4304..24e42e15be8e 100644 --- a/plugins/integrations/kubernetes-service/pom.xml +++ b/plugins/integrations/kubernetes-service/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/integrations/prometheus/pom.xml b/plugins/integrations/prometheus/pom.xml index 378eab52471f..77a584c25dc7 100644 --- a/plugins/integrations/prometheus/pom.xml +++ b/plugins/integrations/prometheus/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/maintenance/pom.xml b/plugins/maintenance/pom.xml index 5290b5277674..65363e33b078 100644 --- a/plugins/maintenance/pom.xml +++ b/plugins/maintenance/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/plugins/metrics/pom.xml b/plugins/metrics/pom.xml index 6cba7d8bd043..25102cd6da5f 100644 --- a/plugins/metrics/pom.xml +++ b/plugins/metrics/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/plugins/network-elements/bigswitch/pom.xml b/plugins/network-elements/bigswitch/pom.xml index b1a163eb3ee3..8729e12782da 100644 --- a/plugins/network-elements/bigswitch/pom.xml +++ b/plugins/network-elements/bigswitch/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/brocade-vcs/pom.xml b/plugins/network-elements/brocade-vcs/pom.xml index e3d7a6cf3adc..e4d29023fe98 100644 --- a/plugins/network-elements/brocade-vcs/pom.xml +++ b/plugins/network-elements/brocade-vcs/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/cisco-vnmc/pom.xml b/plugins/network-elements/cisco-vnmc/pom.xml index 56ff642f549a..f29ed0a4b798 100644 --- a/plugins/network-elements/cisco-vnmc/pom.xml +++ b/plugins/network-elements/cisco-vnmc/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/dns-notifier/pom.xml b/plugins/network-elements/dns-notifier/pom.xml index 7f87c49a0d48..99451699621c 100644 --- a/plugins/network-elements/dns-notifier/pom.xml +++ b/plugins/network-elements/dns-notifier/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml cloud-plugin-example-dns-notifier diff --git a/plugins/network-elements/elastic-loadbalancer/pom.xml b/plugins/network-elements/elastic-loadbalancer/pom.xml index 21ed6e32429b..1283c19bc45b 100644 --- a/plugins/network-elements/elastic-loadbalancer/pom.xml +++ b/plugins/network-elements/elastic-loadbalancer/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/globodns/pom.xml b/plugins/network-elements/globodns/pom.xml index a03bb1e3cd26..1f705b259278 100644 --- a/plugins/network-elements/globodns/pom.xml +++ b/plugins/network-elements/globodns/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/internal-loadbalancer/pom.xml b/plugins/network-elements/internal-loadbalancer/pom.xml index beb5a54f933b..fd426ae6f878 100644 --- a/plugins/network-elements/internal-loadbalancer/pom.xml +++ b/plugins/network-elements/internal-loadbalancer/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/juniper-contrail/pom.xml b/plugins/network-elements/juniper-contrail/pom.xml index 0b3f8bf0d203..c13c5be79121 100644 --- a/plugins/network-elements/juniper-contrail/pom.xml +++ b/plugins/network-elements/juniper-contrail/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/netris/pom.xml b/plugins/network-elements/netris/pom.xml index 1a3fe5d7e29b..fd3fb3fa9402 100644 --- a/plugins/network-elements/netris/pom.xml +++ b/plugins/network-elements/netris/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/netscaler/pom.xml b/plugins/network-elements/netscaler/pom.xml index ab51db5b4809..596e0f49ea78 100644 --- a/plugins/network-elements/netscaler/pom.xml +++ b/plugins/network-elements/netscaler/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/nicira-nvp/pom.xml b/plugins/network-elements/nicira-nvp/pom.xml index 40d4863decdf..cffdc20a3497 100644 --- a/plugins/network-elements/nicira-nvp/pom.xml +++ b/plugins/network-elements/nicira-nvp/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/nsx/pom.xml b/plugins/network-elements/nsx/pom.xml index 7dc31d625f3c..21297e7ce645 100644 --- a/plugins/network-elements/nsx/pom.xml +++ b/plugins/network-elements/nsx/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/opendaylight/pom.xml b/plugins/network-elements/opendaylight/pom.xml index a71b86dfecbf..a6d40c66ab42 100644 --- a/plugins/network-elements/opendaylight/pom.xml +++ b/plugins/network-elements/opendaylight/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/ovs/pom.xml b/plugins/network-elements/ovs/pom.xml index 24f622548c23..455078e985a2 100644 --- a/plugins/network-elements/ovs/pom.xml +++ b/plugins/network-elements/ovs/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/palo-alto/pom.xml b/plugins/network-elements/palo-alto/pom.xml index 3e821ec8109a..be8324a9b700 100644 --- a/plugins/network-elements/palo-alto/pom.xml +++ b/plugins/network-elements/palo-alto/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/stratosphere-ssp/pom.xml b/plugins/network-elements/stratosphere-ssp/pom.xml index 1703012eff36..6a6f10c49943 100644 --- a/plugins/network-elements/stratosphere-ssp/pom.xml +++ b/plugins/network-elements/stratosphere-ssp/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/tungsten/pom.xml b/plugins/network-elements/tungsten/pom.xml index 793d754baffc..51e8133a7832 100644 --- a/plugins/network-elements/tungsten/pom.xml +++ b/plugins/network-elements/tungsten/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/network-elements/vxlan/pom.xml b/plugins/network-elements/vxlan/pom.xml index 1192c98acafa..26f32d65be2f 100644 --- a/plugins/network-elements/vxlan/pom.xml +++ b/plugins/network-elements/vxlan/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/outofbandmanagement-drivers/ipmitool/pom.xml b/plugins/outofbandmanagement-drivers/ipmitool/pom.xml index debeb7daab29..78ab611bda05 100644 --- a/plugins/outofbandmanagement-drivers/ipmitool/pom.xml +++ b/plugins/outofbandmanagement-drivers/ipmitool/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/outofbandmanagement-drivers/nested-cloudstack/pom.xml b/plugins/outofbandmanagement-drivers/nested-cloudstack/pom.xml index 9c5e44195abd..67cb45c70191 100644 --- a/plugins/outofbandmanagement-drivers/nested-cloudstack/pom.xml +++ b/plugins/outofbandmanagement-drivers/nested-cloudstack/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/outofbandmanagement-drivers/redfish/pom.xml b/plugins/outofbandmanagement-drivers/redfish/pom.xml index 0d1b292812d7..d5f1cc19ca64 100644 --- a/plugins/outofbandmanagement-drivers/redfish/pom.xml +++ b/plugins/outofbandmanagement-drivers/redfish/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/pom.xml b/plugins/pom.xml index 16e5ed1d8f59..6d7f9665ed66 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 diff --git a/plugins/storage-allocators/random/pom.xml b/plugins/storage-allocators/random/pom.xml index b1a1c2604a74..6281ba6f76a9 100644 --- a/plugins/storage-allocators/random/pom.xml +++ b/plugins/storage-allocators/random/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/storage/image/default/pom.xml b/plugins/storage/image/default/pom.xml index 1bac038c6a97..2756b865aafe 100644 --- a/plugins/storage/image/default/pom.xml +++ b/plugins/storage/image/default/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/image/s3/pom.xml b/plugins/storage/image/s3/pom.xml index 9c3ea94130dd..0a5f5212939f 100644 --- a/plugins/storage/image/s3/pom.xml +++ b/plugins/storage/image/s3/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/image/sample/pom.xml b/plugins/storage/image/sample/pom.xml index 586eb10f28f8..0ba1d96507ff 100644 --- a/plugins/storage/image/sample/pom.xml +++ b/plugins/storage/image/sample/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/image/swift/pom.xml b/plugins/storage/image/swift/pom.xml index 407106ce935e..85319d7a81a9 100644 --- a/plugins/storage/image/swift/pom.xml +++ b/plugins/storage/image/swift/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/object/ceph/pom.xml b/plugins/storage/object/ceph/pom.xml index f6138add95d3..31468200641f 100644 --- a/plugins/storage/object/ceph/pom.xml +++ b/plugins/storage/object/ceph/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/object/cloudian/pom.xml b/plugins/storage/object/cloudian/pom.xml index 81c78a8d346a..ac08a7a7d849 100644 --- a/plugins/storage/object/cloudian/pom.xml +++ b/plugins/storage/object/cloudian/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/object/minio/pom.xml b/plugins/storage/object/minio/pom.xml index 61b9838de49a..15e6f44db616 100644 --- a/plugins/storage/object/minio/pom.xml +++ b/plugins/storage/object/minio/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/object/simulator/pom.xml b/plugins/storage/object/simulator/pom.xml index 6efbd1c41aa3..71778dffcf4b 100644 --- a/plugins/storage/object/simulator/pom.xml +++ b/plugins/storage/object/simulator/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/sharedfs/storagevm/pom.xml b/plugins/storage/sharedfs/storagevm/pom.xml index 34594898cc05..555b61508ade 100644 --- a/plugins/storage/sharedfs/storagevm/pom.xml +++ b/plugins/storage/sharedfs/storagevm/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/adaptive/pom.xml b/plugins/storage/volume/adaptive/pom.xml index e625cd66f53b..8cc14a192d4b 100644 --- a/plugins/storage/volume/adaptive/pom.xml +++ b/plugins/storage/volume/adaptive/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/cloudbyte/pom.xml b/plugins/storage/volume/cloudbyte/pom.xml index 77dadb4ec8fe..aff11e4f9712 100644 --- a/plugins/storage/volume/cloudbyte/pom.xml +++ b/plugins/storage/volume/cloudbyte/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/datera/pom.xml b/plugins/storage/volume/datera/pom.xml index 84a5e4c27480..91d26224a563 100644 --- a/plugins/storage/volume/datera/pom.xml +++ b/plugins/storage/volume/datera/pom.xml @@ -16,7 +16,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/default/pom.xml b/plugins/storage/volume/default/pom.xml index 02e2fb88b229..c79103a54d9e 100644 --- a/plugins/storage/volume/default/pom.xml +++ b/plugins/storage/volume/default/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/flasharray/pom.xml b/plugins/storage/volume/flasharray/pom.xml index a6da4cf9d582..fc730cd595ff 100644 --- a/plugins/storage/volume/flasharray/pom.xml +++ b/plugins/storage/volume/flasharray/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/linstor/pom.xml b/plugins/storage/volume/linstor/pom.xml index ee72aab66a38..a04810b91c6c 100644 --- a/plugins/storage/volume/linstor/pom.xml +++ b/plugins/storage/volume/linstor/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/nexenta/pom.xml b/plugins/storage/volume/nexenta/pom.xml index d9ed6fd64a49..c802d5b1e988 100644 --- a/plugins/storage/volume/nexenta/pom.xml +++ b/plugins/storage/volume/nexenta/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/primera/pom.xml b/plugins/storage/volume/primera/pom.xml index 4f1583a8a7b3..a55d1cda6dc3 100644 --- a/plugins/storage/volume/primera/pom.xml +++ b/plugins/storage/volume/primera/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/sample/pom.xml b/plugins/storage/volume/sample/pom.xml index 0a9de06ba623..5d9be3fd6eb8 100644 --- a/plugins/storage/volume/sample/pom.xml +++ b/plugins/storage/volume/sample/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/scaleio/pom.xml b/plugins/storage/volume/scaleio/pom.xml index 79a707ba7d54..c2ebfba29b9c 100644 --- a/plugins/storage/volume/scaleio/pom.xml +++ b/plugins/storage/volume/scaleio/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/solidfire/pom.xml b/plugins/storage/volume/solidfire/pom.xml index 65a91afc10e3..27cd8cd29511 100644 --- a/plugins/storage/volume/solidfire/pom.xml +++ b/plugins/storage/volume/solidfire/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/storage/volume/storpool/pom.xml b/plugins/storage/volume/storpool/pom.xml index 0829575b2295..a3913800f453 100644 --- a/plugins/storage/volume/storpool/pom.xml +++ b/plugins/storage/volume/storpool/pom.xml @@ -17,7 +17,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../../pom.xml diff --git a/plugins/user-authenticators/ldap/pom.xml b/plugins/user-authenticators/ldap/pom.xml index 7f58d8fa28a7..28da0df580e6 100644 --- a/plugins/user-authenticators/ldap/pom.xml +++ b/plugins/user-authenticators/ldap/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/user-authenticators/md5/pom.xml b/plugins/user-authenticators/md5/pom.xml index 11f0a4272307..9c0548a7c9cc 100644 --- a/plugins/user-authenticators/md5/pom.xml +++ b/plugins/user-authenticators/md5/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/user-authenticators/oauth2/pom.xml b/plugins/user-authenticators/oauth2/pom.xml index 8caf06ff8143..ddd4b6ecd5ee 100644 --- a/plugins/user-authenticators/oauth2/pom.xml +++ b/plugins/user-authenticators/oauth2/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/user-authenticators/pbkdf2/pom.xml b/plugins/user-authenticators/pbkdf2/pom.xml index 75351f20f3f4..89bb93ad29cc 100644 --- a/plugins/user-authenticators/pbkdf2/pom.xml +++ b/plugins/user-authenticators/pbkdf2/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/user-authenticators/plain-text/pom.xml b/plugins/user-authenticators/plain-text/pom.xml index ceddc54e34d4..4eee43ee4010 100644 --- a/plugins/user-authenticators/plain-text/pom.xml +++ b/plugins/user-authenticators/plain-text/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/user-authenticators/saml2/pom.xml b/plugins/user-authenticators/saml2/pom.xml index 8802ab9260f0..9c56d11b55b3 100644 --- a/plugins/user-authenticators/saml2/pom.xml +++ b/plugins/user-authenticators/saml2/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/user-authenticators/sha256salted/pom.xml b/plugins/user-authenticators/sha256salted/pom.xml index 2938e21e2094..b59e1b6120d6 100644 --- a/plugins/user-authenticators/sha256salted/pom.xml +++ b/plugins/user-authenticators/sha256salted/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/user-two-factor-authenticators/static-pin/pom.xml b/plugins/user-two-factor-authenticators/static-pin/pom.xml index 1fe95fa3913d..781a1d97a706 100644 --- a/plugins/user-two-factor-authenticators/static-pin/pom.xml +++ b/plugins/user-two-factor-authenticators/static-pin/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/plugins/user-two-factor-authenticators/totp/pom.xml b/plugins/user-two-factor-authenticators/totp/pom.xml index dcf051f92170..87f8d9b3bf42 100644 --- a/plugins/user-two-factor-authenticators/totp/pom.xml +++ b/plugins/user-two-factor-authenticators/totp/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../../pom.xml diff --git a/pom.xml b/pom.xml index b4e2ec57f81b..4cc6b5914836 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 pom Apache CloudStack Apache CloudStack is an IaaS ("Infrastructure as a Service") cloud orchestration platform. diff --git a/quickcloud/pom.xml b/quickcloud/pom.xml index d4d759f8986a..2130fd87aa7e 100644 --- a/quickcloud/pom.xml +++ b/quickcloud/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/server/pom.xml b/server/pom.xml index 3324cdb2e619..3e91e5a29061 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 @@ -194,7 +194,7 @@ org.apache.cloudstack cloud-framework-extensions - 4.22.1.0-SNAPSHOT + 4.22.1.0 compile diff --git a/services/console-proxy/pom.xml b/services/console-proxy/pom.xml index 43d32fdf61b9..7ccbe9a1a1f8 100644 --- a/services/console-proxy/pom.xml +++ b/services/console-proxy/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-services - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/services/console-proxy/rdpconsole/pom.xml b/services/console-proxy/rdpconsole/pom.xml index 541648827c66..ce8f587f1f1a 100644 --- a/services/console-proxy/rdpconsole/pom.xml +++ b/services/console-proxy/rdpconsole/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-service-console-proxy - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/services/console-proxy/server/pom.xml b/services/console-proxy/server/pom.xml index cff9ae3cfcb6..8da4335bd235 100644 --- a/services/console-proxy/server/pom.xml +++ b/services/console-proxy/server/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-service-console-proxy - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/services/pom.xml b/services/pom.xml index 2205e460ee0c..5e7315e347d5 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/services/secondary-storage/controller/pom.xml b/services/secondary-storage/controller/pom.xml index e3847e294fc1..62c6c098ed09 100644 --- a/services/secondary-storage/controller/pom.xml +++ b/services/secondary-storage/controller/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-service-secondary-storage - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/services/secondary-storage/pom.xml b/services/secondary-storage/pom.xml index c1d3f9016151..f96826f02d4a 100644 --- a/services/secondary-storage/pom.xml +++ b/services/secondary-storage/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-services - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/services/secondary-storage/server/pom.xml b/services/secondary-storage/server/pom.xml index ca878ce9ba61..1ac2f00e8d44 100644 --- a/services/secondary-storage/server/pom.xml +++ b/services/secondary-storage/server/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-service-secondary-storage - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/systemvm/pom.xml b/systemvm/pom.xml index e2a0315300ba..a949df8befcb 100644 --- a/systemvm/pom.xml +++ b/systemvm/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/test/pom.xml b/test/pom.xml index dd1bdf4ceb84..605915c5defa 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 diff --git a/tools/apidoc/pom.xml b/tools/apidoc/pom.xml index e9b81eedd5fb..67c40ddcca0b 100644 --- a/tools/apidoc/pom.xml +++ b/tools/apidoc/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloud-tools - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/tools/checkstyle/pom.xml b/tools/checkstyle/pom.xml index 0fb0efacd608..f5221fd67902 100644 --- a/tools/checkstyle/pom.xml +++ b/tools/checkstyle/pom.xml @@ -22,7 +22,7 @@ Apache CloudStack Developer Tools - Checkstyle Configuration org.apache.cloudstack checkstyle - 4.22.1.0-SNAPSHOT + 4.22.1.0 UTF-8 diff --git a/tools/devcloud-kvm/pom.xml b/tools/devcloud-kvm/pom.xml index 4990f0af8be3..ab0eeda2b547 100644 --- a/tools/devcloud-kvm/pom.xml +++ b/tools/devcloud-kvm/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloud-tools - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/tools/devcloud4/pom.xml b/tools/devcloud4/pom.xml index 1aa48c4b2d0d..ec89ae6abac7 100644 --- a/tools/devcloud4/pom.xml +++ b/tools/devcloud4/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloud-tools - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index 67a986e994ab..7cb2efb3d4df 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -19,7 +19,7 @@ FROM ubuntu:22.04 -LABEL Vendor="Apache.org" License="ApacheV2" Version="4.22.1.0-SNAPSHOT" Author="Apache CloudStack " +LABEL Vendor="Apache.org" License="ApacheV2" Version="4.22.1.0" Author="Apache CloudStack " ARG DEBIAN_FRONTEND=noninteractive diff --git a/tools/docker/Dockerfile.marvin b/tools/docker/Dockerfile.marvin index d615a78eb6bb..3e060b0f481c 100644 --- a/tools/docker/Dockerfile.marvin +++ b/tools/docker/Dockerfile.marvin @@ -19,11 +19,11 @@ # build for cloudstack_home_dir not this folder FROM python:2 -LABEL Vendor="Apache.org" License="ApacheV2" Version="4.22.1.0-SNAPSHOT" Author="Apache CloudStack " +LABEL Vendor="Apache.org" License="ApacheV2" Version="4.22.1.0" Author="Apache CloudStack " ENV WORK_DIR=/marvin -ENV PKG_URL=https://builds.cloudstack.org/job/build-master-marvin/lastSuccessfulBuild/artifact/tools/marvin/dist/Marvin-4.22.1.0-SNAPSHOT.tar.gz +ENV PKG_URL=https://builds.cloudstack.org/job/build-master-marvin/lastSuccessfulBuild/artifact/tools/marvin/dist/Marvin-4.22.1.0.tar.gz RUN apt-get update && apt-get install -y vim RUN pip install --upgrade paramiko nose requests diff --git a/tools/marvin/pom.xml b/tools/marvin/pom.xml index 907e9931015d..70e39a911d36 100644 --- a/tools/marvin/pom.xml +++ b/tools/marvin/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloud-tools - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py index ad0f245d68f3..66eb32111675 100644 --- a/tools/marvin/setup.py +++ b/tools/marvin/setup.py @@ -27,7 +27,7 @@ raise RuntimeError("python setuptools is required to build Marvin") -VERSION = "4.22.1.0-SNAPSHOT" +VERSION = "4.22.1.0" setup(name="Marvin", version=VERSION, diff --git a/tools/pom.xml b/tools/pom.xml index ffa363d4a265..15335c5f2fd6 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/usage/pom.xml b/usage/pom.xml index 7f5b0ba527d5..f51ffce2a5d7 100644 --- a/usage/pom.xml +++ b/usage/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 diff --git a/utils/pom.xml b/utils/pom.xml index ee6df9602b8f..7cb2df1deb37 100755 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 ../pom.xml diff --git a/vmware-base/pom.xml b/vmware-base/pom.xml index 9b7b989a8901..53abed35493b 100644 --- a/vmware-base/pom.xml +++ b/vmware-base/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0-SNAPSHOT + 4.22.1.0 From 5893ba5a8c00aa445be252e2dd0bc557b0cec3fc Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Tue, 12 May 2026 04:07:20 -0300 Subject: [PATCH 058/205] server: Fix NPE when on findHostsForMigration when no suitable hosts are found (#13138) --- .../cloud/server/ManagementServerImpl.java | 13 +++---- .../server/ManagementServerImplTest.java | 39 +++++++++++++++++-- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 70a434e4a96e..e5066b6da5cc 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -1707,16 +1707,15 @@ protected List getCapableSuitableHosts( if (CollectionUtils.isEmpty(suitableHosts)) { logger.warn("No suitable hosts found."); - } else { - logger.debug("Hosts having capacity and are suitable for migration: {}", suitableHosts); + return suitableHosts; } + logger.debug("Hosts having capacity and are suitable for migration: {}", suitableHosts); + // Only list hosts of the same architecture as the source Host in a multi-arch zone - if (!suitableHosts.isEmpty()) { - List clusterArchs = ApiDBUtils.listZoneClustersArchs(vm.getDataCenterId()); - if (CollectionUtils.isNotEmpty(clusterArchs) && clusterArchs.size() > 1) { - suitableHosts = suitableHosts.stream().filter(h -> h.getArch() == srcHost.getArch()).collect(Collectors.toList()); - } + List clusterArchs = ApiDBUtils.listZoneClustersArchs(vm.getDataCenterId()); + if (CollectionUtils.isNotEmpty(clusterArchs) && clusterArchs.size() > 1) { + suitableHosts = suitableHosts.stream().filter(h -> h.getArch() == srcHost.getArch()).collect(Collectors.toList()); } return suitableHosts; diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index b569368f2482..924bf1135a67 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -23,6 +23,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.cloud.deploy.DataCenterDeployment; +import com.cloud.deploy.DeploymentPlanner; +import com.cloud.deploy.DeploymentPlanningManager; +import com.cloud.vm.VirtualMachineProfile; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -181,6 +185,24 @@ public class ManagementServerImplTest { @Mock HostAllocator hostAllocator; + @Mock + VirtualMachine virtualMachineMock; + + @Mock + VirtualMachineProfile virtualMachineProfileMock; + + @Mock + DataCenterDeployment dataCenterDeploymentMock; + + @Mock + DeploymentPlanner.ExcludeList excludeListMock; + + @Mock + Host hostMock; + + @Mock + DeploymentPlanningManager deploymentPlanningManagerMock; + private AutoCloseable closeable; private MockedStatic apiDBUtilsMock; @@ -1053,10 +1075,19 @@ public void testListGuestOSCategoriesByCriteria_FilterById() { @Test public void testGetExternalVmConsole() { - VirtualMachine virtualMachine = Mockito.mock(VirtualMachine.class); Host host = Mockito.mock(Host.class); - Mockito.when(extensionManager.getInstanceConsole(virtualMachine, host)).thenReturn(Mockito.mock(com.cloud.agent.api.Answer.class)); - Assert.assertNotNull(spy.getExternalVmConsole(virtualMachine, host)); - Mockito.verify(extensionManager).getInstanceConsole(virtualMachine, host); + Mockito.when(extensionManager.getInstanceConsole(virtualMachineMock, host)).thenReturn(Mockito.mock(com.cloud.agent.api.Answer.class)); + Assert.assertNotNull(spy.getExternalVmConsole(virtualMachineMock, host)); + Mockito.verify(extensionManager).getInstanceConsole(virtualMachineMock, host); + } + + @Test + public void getCapableSuitableHostsTestHostArchIsNotFilteredWhenNoSuitableHostsAreFound() { + List compatibleHosts = List.of(hostMock); + Mockito.doReturn(null).when(hostAllocator).allocateTo(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean()); + + spy.getCapableSuitableHosts(virtualMachineMock, virtualMachineProfileMock, dataCenterDeploymentMock, compatibleHosts, excludeListMock, hostMock); + + apiDBUtilsMock.verify(() -> ApiDBUtils.listZoneClustersArchs(Mockito.anyLong()), Mockito.never()); } } From 79bb8ef1cc3c89b144f9623d638c893258438848 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 18 May 2026 17:17:25 +0530 Subject: [PATCH 059/205] Updating pom.xml version numbers for release 4.22.2.0-SNAPSHOT Signed-off-by: Suresh Kumar Anaparti --- agent/pom.xml | 2 +- api/pom.xml | 2 +- client/pom.xml | 2 +- core/pom.xml | 2 +- developer/pom.xml | 2 +- engine/api/pom.xml | 2 +- engine/components-api/pom.xml | 2 +- engine/orchestration/pom.xml | 2 +- engine/pom.xml | 2 +- engine/schema/pom.xml | 2 +- engine/service/pom.xml | 2 +- engine/storage/cache/pom.xml | 2 +- engine/storage/configdrive/pom.xml | 2 +- engine/storage/datamotion/pom.xml | 2 +- engine/storage/image/pom.xml | 2 +- engine/storage/integration-test/pom.xml | 2 +- engine/storage/object/pom.xml | 2 +- engine/storage/pom.xml | 2 +- engine/storage/snapshot/pom.xml | 2 +- engine/storage/volume/pom.xml | 2 +- engine/userdata/cloud-init/pom.xml | 2 +- engine/userdata/pom.xml | 2 +- framework/agent-lb/pom.xml | 2 +- framework/ca/pom.xml | 2 +- framework/cluster/pom.xml | 2 +- framework/config/pom.xml | 2 +- framework/db/pom.xml | 2 +- framework/direct-download/pom.xml | 2 +- framework/events/pom.xml | 2 +- framework/extensions/pom.xml | 6 +++--- framework/ipc/pom.xml | 2 +- framework/jobs/pom.xml | 2 +- framework/managed-context/pom.xml | 2 +- framework/pom.xml | 2 +- framework/quota/pom.xml | 2 +- framework/rest/pom.xml | 2 +- framework/security/pom.xml | 2 +- framework/spring/lifecycle/pom.xml | 2 +- framework/spring/module/pom.xml | 2 +- plugins/acl/dynamic-role-based/pom.xml | 2 +- plugins/acl/project-role-based/pom.xml | 2 +- plugins/acl/static-role-based/pom.xml | 2 +- .../affinity-group-processors/explicit-dedication/pom.xml | 2 +- plugins/affinity-group-processors/host-affinity/pom.xml | 2 +- .../affinity-group-processors/host-anti-affinity/pom.xml | 2 +- .../non-strict-host-affinity/pom.xml | 2 +- .../non-strict-host-anti-affinity/pom.xml | 2 +- plugins/alert-handlers/snmp-alerts/pom.xml | 2 +- plugins/alert-handlers/syslog-alerts/pom.xml | 2 +- plugins/api/discovery/pom.xml | 2 +- plugins/api/rate-limit/pom.xml | 2 +- plugins/api/solidfire-intg-test/pom.xml | 2 +- plugins/api/vmware-sioc/pom.xml | 2 +- plugins/backup/dummy/pom.xml | 2 +- plugins/backup/nas/pom.xml | 2 +- plugins/backup/networker/pom.xml | 2 +- plugins/backup/veeam/pom.xml | 2 +- plugins/ca/root-ca/pom.xml | 2 +- plugins/database/mysql-ha/pom.xml | 2 +- plugins/database/quota/pom.xml | 2 +- plugins/dedicated-resources/pom.xml | 2 +- plugins/deployment-planners/implicit-dedication/pom.xml | 2 +- plugins/deployment-planners/user-concentrated-pod/pom.xml | 2 +- plugins/deployment-planners/user-dispersing/pom.xml | 2 +- plugins/drs/cluster/balanced/pom.xml | 2 +- plugins/drs/cluster/condensed/pom.xml | 2 +- plugins/event-bus/inmemory/pom.xml | 2 +- plugins/event-bus/kafka/pom.xml | 2 +- plugins/event-bus/rabbitmq/pom.xml | 2 +- plugins/event-bus/webhook/pom.xml | 2 +- plugins/ha-planners/skip-heurestics/pom.xml | 2 +- plugins/host-allocators/random/pom.xml | 2 +- plugins/hypervisors/baremetal/pom.xml | 2 +- plugins/hypervisors/external/pom.xml | 2 +- plugins/hypervisors/hyperv/pom.xml | 2 +- plugins/hypervisors/kvm/pom.xml | 2 +- plugins/hypervisors/ovm/pom.xml | 2 +- plugins/hypervisors/ovm3/pom.xml | 2 +- plugins/hypervisors/simulator/pom.xml | 2 +- plugins/hypervisors/ucs/pom.xml | 2 +- plugins/hypervisors/vmware/pom.xml | 2 +- plugins/hypervisors/xenserver/pom.xml | 2 +- plugins/integrations/cloudian/pom.xml | 2 +- plugins/integrations/kubernetes-service/pom.xml | 2 +- plugins/integrations/prometheus/pom.xml | 2 +- plugins/maintenance/pom.xml | 2 +- plugins/metrics/pom.xml | 2 +- plugins/network-elements/bigswitch/pom.xml | 2 +- plugins/network-elements/brocade-vcs/pom.xml | 2 +- plugins/network-elements/cisco-vnmc/pom.xml | 2 +- plugins/network-elements/dns-notifier/pom.xml | 2 +- plugins/network-elements/elastic-loadbalancer/pom.xml | 2 +- plugins/network-elements/globodns/pom.xml | 2 +- plugins/network-elements/internal-loadbalancer/pom.xml | 2 +- plugins/network-elements/juniper-contrail/pom.xml | 2 +- plugins/network-elements/netris/pom.xml | 2 +- plugins/network-elements/netscaler/pom.xml | 2 +- plugins/network-elements/nicira-nvp/pom.xml | 2 +- plugins/network-elements/nsx/pom.xml | 2 +- plugins/network-elements/opendaylight/pom.xml | 2 +- plugins/network-elements/ovs/pom.xml | 2 +- plugins/network-elements/palo-alto/pom.xml | 2 +- plugins/network-elements/stratosphere-ssp/pom.xml | 2 +- plugins/network-elements/tungsten/pom.xml | 2 +- plugins/network-elements/vxlan/pom.xml | 2 +- plugins/outofbandmanagement-drivers/ipmitool/pom.xml | 2 +- .../outofbandmanagement-drivers/nested-cloudstack/pom.xml | 2 +- plugins/outofbandmanagement-drivers/redfish/pom.xml | 2 +- plugins/pom.xml | 2 +- plugins/storage-allocators/random/pom.xml | 2 +- plugins/storage/image/default/pom.xml | 2 +- plugins/storage/image/s3/pom.xml | 2 +- plugins/storage/image/sample/pom.xml | 2 +- plugins/storage/image/swift/pom.xml | 2 +- plugins/storage/object/ceph/pom.xml | 2 +- plugins/storage/object/cloudian/pom.xml | 2 +- plugins/storage/object/minio/pom.xml | 2 +- plugins/storage/object/simulator/pom.xml | 2 +- plugins/storage/sharedfs/storagevm/pom.xml | 2 +- plugins/storage/volume/adaptive/pom.xml | 2 +- plugins/storage/volume/cloudbyte/pom.xml | 2 +- plugins/storage/volume/datera/pom.xml | 2 +- plugins/storage/volume/default/pom.xml | 2 +- plugins/storage/volume/flasharray/pom.xml | 2 +- plugins/storage/volume/linstor/pom.xml | 2 +- plugins/storage/volume/nexenta/pom.xml | 2 +- plugins/storage/volume/primera/pom.xml | 2 +- plugins/storage/volume/sample/pom.xml | 2 +- plugins/storage/volume/scaleio/pom.xml | 2 +- plugins/storage/volume/solidfire/pom.xml | 2 +- plugins/storage/volume/storpool/pom.xml | 2 +- plugins/user-authenticators/ldap/pom.xml | 2 +- plugins/user-authenticators/md5/pom.xml | 2 +- plugins/user-authenticators/oauth2/pom.xml | 2 +- plugins/user-authenticators/pbkdf2/pom.xml | 2 +- plugins/user-authenticators/plain-text/pom.xml | 2 +- plugins/user-authenticators/saml2/pom.xml | 2 +- plugins/user-authenticators/sha256salted/pom.xml | 2 +- plugins/user-two-factor-authenticators/static-pin/pom.xml | 2 +- plugins/user-two-factor-authenticators/totp/pom.xml | 2 +- pom.xml | 2 +- quickcloud/pom.xml | 2 +- server/pom.xml | 4 ++-- services/console-proxy/pom.xml | 2 +- services/console-proxy/rdpconsole/pom.xml | 2 +- services/console-proxy/server/pom.xml | 2 +- services/pom.xml | 2 +- services/secondary-storage/controller/pom.xml | 2 +- services/secondary-storage/pom.xml | 2 +- services/secondary-storage/server/pom.xml | 2 +- systemvm/pom.xml | 2 +- test/pom.xml | 2 +- tools/apidoc/pom.xml | 2 +- tools/checkstyle/pom.xml | 2 +- tools/devcloud-kvm/pom.xml | 2 +- tools/devcloud4/pom.xml | 2 +- tools/docker/Dockerfile | 2 +- tools/docker/Dockerfile.marvin | 4 ++-- tools/marvin/pom.xml | 2 +- tools/marvin/setup.py | 2 +- tools/pom.xml | 2 +- usage/pom.xml | 2 +- utils/pom.xml | 2 +- vmware-base/pom.xml | 2 +- 164 files changed, 168 insertions(+), 168 deletions(-) diff --git a/agent/pom.xml b/agent/pom.xml index 70fdea6d0c59..3064fd2cab21 100644 --- a/agent/pom.xml +++ b/agent/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT diff --git a/api/pom.xml b/api/pom.xml index 03abe9ee30bf..d5791bed38e6 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT diff --git a/client/pom.xml b/client/pom.xml index e97b5474d733..85519e16c2c5 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT diff --git a/core/pom.xml b/core/pom.xml index 8012f3747ada..6324e36563fb 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT diff --git a/developer/pom.xml b/developer/pom.xml index b22140ea70e1..69725b60fe99 100644 --- a/developer/pom.xml +++ b/developer/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT diff --git a/engine/api/pom.xml b/engine/api/pom.xml index e23593f34ff7..425c51205fa7 100644 --- a/engine/api/pom.xml +++ b/engine/api/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/engine/components-api/pom.xml b/engine/components-api/pom.xml index 940fd8050f3e..1a207403c64a 100644 --- a/engine/components-api/pom.xml +++ b/engine/components-api/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/engine/orchestration/pom.xml b/engine/orchestration/pom.xml index 44b57a821479..0f321be6bd60 100755 --- a/engine/orchestration/pom.xml +++ b/engine/orchestration/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/engine/pom.xml b/engine/pom.xml index dcd65661f57b..ce8161ce93a8 100644 --- a/engine/pom.xml +++ b/engine/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/engine/schema/pom.xml b/engine/schema/pom.xml index ceedce8a140d..f740f7ee5f8a 100644 --- a/engine/schema/pom.xml +++ b/engine/schema/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/engine/service/pom.xml b/engine/service/pom.xml index fa324fb9398d..6f09ce71a0e4 100644 --- a/engine/service/pom.xml +++ b/engine/service/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT cloud-engine-service war diff --git a/engine/storage/cache/pom.xml b/engine/storage/cache/pom.xml index 1eda5b66aaf2..7eb7ca935479 100644 --- a/engine/storage/cache/pom.xml +++ b/engine/storage/cache/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/engine/storage/configdrive/pom.xml b/engine/storage/configdrive/pom.xml index d176d00568d6..4448741a097f 100644 --- a/engine/storage/configdrive/pom.xml +++ b/engine/storage/configdrive/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/engine/storage/datamotion/pom.xml b/engine/storage/datamotion/pom.xml index ff4bfa5877d1..1b545c0c79bb 100644 --- a/engine/storage/datamotion/pom.xml +++ b/engine/storage/datamotion/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/engine/storage/image/pom.xml b/engine/storage/image/pom.xml index bdb5f9e9bf87..e07c94a5de93 100644 --- a/engine/storage/image/pom.xml +++ b/engine/storage/image/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/engine/storage/integration-test/pom.xml b/engine/storage/integration-test/pom.xml index 6fccacdcb41e..62381188331c 100644 --- a/engine/storage/integration-test/pom.xml +++ b/engine/storage/integration-test/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/engine/storage/object/pom.xml b/engine/storage/object/pom.xml index 6fee3d658fdc..e00cddce31a6 100644 --- a/engine/storage/object/pom.xml +++ b/engine/storage/object/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/engine/storage/pom.xml b/engine/storage/pom.xml index 7529adfd0dd4..71d1100b1753 100644 --- a/engine/storage/pom.xml +++ b/engine/storage/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/engine/storage/snapshot/pom.xml b/engine/storage/snapshot/pom.xml index 7fa2d4056043..3699a0ae5021 100644 --- a/engine/storage/snapshot/pom.xml +++ b/engine/storage/snapshot/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/engine/storage/volume/pom.xml b/engine/storage/volume/pom.xml index 220989ddcd95..6e4f89cc3c0d 100644 --- a/engine/storage/volume/pom.xml +++ b/engine/storage/volume/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/engine/userdata/cloud-init/pom.xml b/engine/userdata/cloud-init/pom.xml index 460a18a8f206..87078d7f9f0c 100644 --- a/engine/userdata/cloud-init/pom.xml +++ b/engine/userdata/cloud-init/pom.xml @@ -23,7 +23,7 @@ cloud-engine org.apache.cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/engine/userdata/pom.xml b/engine/userdata/pom.xml index 7b052903d170..cbb22a39f7ca 100644 --- a/engine/userdata/pom.xml +++ b/engine/userdata/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloud-engine - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/agent-lb/pom.xml b/framework/agent-lb/pom.xml index c36a01aaf45d..47f31a4626b2 100644 --- a/framework/agent-lb/pom.xml +++ b/framework/agent-lb/pom.xml @@ -24,7 +24,7 @@ cloudstack-framework org.apache.cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/ca/pom.xml b/framework/ca/pom.xml index 49637c6f2b6b..a0a5178b1d90 100644 --- a/framework/ca/pom.xml +++ b/framework/ca/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/cluster/pom.xml b/framework/cluster/pom.xml index bd6bb84f9443..3c73a57d09a2 100644 --- a/framework/cluster/pom.xml +++ b/framework/cluster/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/config/pom.xml b/framework/config/pom.xml index 38f45870305c..a7d4262901f7 100644 --- a/framework/config/pom.xml +++ b/framework/config/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/db/pom.xml b/framework/db/pom.xml index 1d53484a6a90..2894342ab502 100644 --- a/framework/db/pom.xml +++ b/framework/db/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/direct-download/pom.xml b/framework/direct-download/pom.xml index 688f23a25cea..3b0318eb513e 100644 --- a/framework/direct-download/pom.xml +++ b/framework/direct-download/pom.xml @@ -32,7 +32,7 @@ cloudstack-framework org.apache.cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/events/pom.xml b/framework/events/pom.xml index 5bf3cfeea21a..0d906cb6afc1 100644 --- a/framework/events/pom.xml +++ b/framework/events/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/extensions/pom.xml b/framework/extensions/pom.xml index 5186d7e95b5f..3f3542aa8388 100644 --- a/framework/extensions/pom.xml +++ b/framework/extensions/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml @@ -41,13 +41,13 @@ org.apache.cloudstack cloud-engine-schema - 4.22.1.0 + 4.22.2.0-SNAPSHOT compile org.apache.cloudstack cloud-engine-components-api - 4.22.1.0 + 4.22.2.0-SNAPSHOT compile diff --git a/framework/ipc/pom.xml b/framework/ipc/pom.xml index b05854df03a4..39c36a817dd3 100644 --- a/framework/ipc/pom.xml +++ b/framework/ipc/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/jobs/pom.xml b/framework/jobs/pom.xml index 99206fc1a446..f584af90c6b9 100644 --- a/framework/jobs/pom.xml +++ b/framework/jobs/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/managed-context/pom.xml b/framework/managed-context/pom.xml index dbee01e43087..836d030cc693 100644 --- a/framework/managed-context/pom.xml +++ b/framework/managed-context/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/framework/pom.xml b/framework/pom.xml index e2c52f643cc2..d3354c27af4e 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT diff --git a/framework/quota/pom.xml b/framework/quota/pom.xml index 3e0ec54790f2..bf14b310b9f0 100644 --- a/framework/quota/pom.xml +++ b/framework/quota/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/rest/pom.xml b/framework/rest/pom.xml index b7f2e526fd38..a22b631eb5cd 100644 --- a/framework/rest/pom.xml +++ b/framework/rest/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml cloud-framework-rest diff --git a/framework/security/pom.xml b/framework/security/pom.xml index 4f4b7af07e41..fec791b7d087 100644 --- a/framework/security/pom.xml +++ b/framework/security/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-framework - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/framework/spring/lifecycle/pom.xml b/framework/spring/lifecycle/pom.xml index 1844b803809b..af3dca3047e4 100644 --- a/framework/spring/lifecycle/pom.xml +++ b/framework/spring/lifecycle/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/framework/spring/module/pom.xml b/framework/spring/module/pom.xml index 6a1dbade3bf4..ccd2c5efb161 100644 --- a/framework/spring/module/pom.xml +++ b/framework/spring/module/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/acl/dynamic-role-based/pom.xml b/plugins/acl/dynamic-role-based/pom.xml index 029fcb5e97b8..ecaa32216d38 100644 --- a/plugins/acl/dynamic-role-based/pom.xml +++ b/plugins/acl/dynamic-role-based/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/acl/project-role-based/pom.xml b/plugins/acl/project-role-based/pom.xml index dd0b621b395f..8266828fc970 100644 --- a/plugins/acl/project-role-based/pom.xml +++ b/plugins/acl/project-role-based/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/acl/static-role-based/pom.xml b/plugins/acl/static-role-based/pom.xml index 06996d471b59..1b1db7c86eeb 100644 --- a/plugins/acl/static-role-based/pom.xml +++ b/plugins/acl/static-role-based/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/affinity-group-processors/explicit-dedication/pom.xml b/plugins/affinity-group-processors/explicit-dedication/pom.xml index e134d687c892..fa0e1c6758eb 100644 --- a/plugins/affinity-group-processors/explicit-dedication/pom.xml +++ b/plugins/affinity-group-processors/explicit-dedication/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/affinity-group-processors/host-affinity/pom.xml b/plugins/affinity-group-processors/host-affinity/pom.xml index 75aec801bee8..5916abfb3b66 100644 --- a/plugins/affinity-group-processors/host-affinity/pom.xml +++ b/plugins/affinity-group-processors/host-affinity/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/affinity-group-processors/host-anti-affinity/pom.xml b/plugins/affinity-group-processors/host-anti-affinity/pom.xml index d3d2e0b85630..a8ccb7636edb 100644 --- a/plugins/affinity-group-processors/host-anti-affinity/pom.xml +++ b/plugins/affinity-group-processors/host-anti-affinity/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml b/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml index f6841ecce9c1..17008fbf8b6c 100644 --- a/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml +++ b/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml b/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml index 7222e80c3dcb..71b3656e3e2b 100644 --- a/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml +++ b/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml @@ -32,7 +32,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/alert-handlers/snmp-alerts/pom.xml b/plugins/alert-handlers/snmp-alerts/pom.xml index 147e81506ec9..39d937046f30 100644 --- a/plugins/alert-handlers/snmp-alerts/pom.xml +++ b/plugins/alert-handlers/snmp-alerts/pom.xml @@ -24,7 +24,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/alert-handlers/syslog-alerts/pom.xml b/plugins/alert-handlers/syslog-alerts/pom.xml index 97f72217933d..4c1e722fc278 100644 --- a/plugins/alert-handlers/syslog-alerts/pom.xml +++ b/plugins/alert-handlers/syslog-alerts/pom.xml @@ -24,7 +24,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/api/discovery/pom.xml b/plugins/api/discovery/pom.xml index 655568138cb8..0f478c228b69 100644 --- a/plugins/api/discovery/pom.xml +++ b/plugins/api/discovery/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/api/rate-limit/pom.xml b/plugins/api/rate-limit/pom.xml index 4e524f32e4e9..376ca61d5985 100644 --- a/plugins/api/rate-limit/pom.xml +++ b/plugins/api/rate-limit/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/api/solidfire-intg-test/pom.xml b/plugins/api/solidfire-intg-test/pom.xml index 01ed3e70362c..91ecee3c893a 100644 --- a/plugins/api/solidfire-intg-test/pom.xml +++ b/plugins/api/solidfire-intg-test/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/api/vmware-sioc/pom.xml b/plugins/api/vmware-sioc/pom.xml index 9c6e783fb331..f9596377afdc 100644 --- a/plugins/api/vmware-sioc/pom.xml +++ b/plugins/api/vmware-sioc/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/backup/dummy/pom.xml b/plugins/backup/dummy/pom.xml index 2006afc95e74..4893b6991a16 100644 --- a/plugins/backup/dummy/pom.xml +++ b/plugins/backup/dummy/pom.xml @@ -23,7 +23,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/backup/nas/pom.xml b/plugins/backup/nas/pom.xml index f0c170c2aadd..7e3daec22f79 100644 --- a/plugins/backup/nas/pom.xml +++ b/plugins/backup/nas/pom.xml @@ -25,7 +25,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/backup/networker/pom.xml b/plugins/backup/networker/pom.xml index b52863e89ea8..8a49f6ae1bea 100644 --- a/plugins/backup/networker/pom.xml +++ b/plugins/backup/networker/pom.xml @@ -25,7 +25,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/backup/veeam/pom.xml b/plugins/backup/veeam/pom.xml index b64a68387ab1..9142b2e76223 100644 --- a/plugins/backup/veeam/pom.xml +++ b/plugins/backup/veeam/pom.xml @@ -23,7 +23,7 @@ cloudstack-plugins org.apache.cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/ca/root-ca/pom.xml b/plugins/ca/root-ca/pom.xml index 6d9315f701a1..8bce2ebefd27 100644 --- a/plugins/ca/root-ca/pom.xml +++ b/plugins/ca/root-ca/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/database/mysql-ha/pom.xml b/plugins/database/mysql-ha/pom.xml index 9ac00d1a5079..6ecc4dccf085 100644 --- a/plugins/database/mysql-ha/pom.xml +++ b/plugins/database/mysql-ha/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/database/quota/pom.xml b/plugins/database/quota/pom.xml index 56b8c0ab4ed6..f5c222f276bf 100644 --- a/plugins/database/quota/pom.xml +++ b/plugins/database/quota/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/dedicated-resources/pom.xml b/plugins/dedicated-resources/pom.xml index a299dc77eb3e..a55c1113b884 100644 --- a/plugins/dedicated-resources/pom.xml +++ b/plugins/dedicated-resources/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/plugins/deployment-planners/implicit-dedication/pom.xml b/plugins/deployment-planners/implicit-dedication/pom.xml index 162b3d2c84c4..8c8a0c4ac277 100644 --- a/plugins/deployment-planners/implicit-dedication/pom.xml +++ b/plugins/deployment-planners/implicit-dedication/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/deployment-planners/user-concentrated-pod/pom.xml b/plugins/deployment-planners/user-concentrated-pod/pom.xml index bba781a774a6..c1d15adf0fad 100644 --- a/plugins/deployment-planners/user-concentrated-pod/pom.xml +++ b/plugins/deployment-planners/user-concentrated-pod/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/deployment-planners/user-dispersing/pom.xml b/plugins/deployment-planners/user-dispersing/pom.xml index 915dbede4f04..04a435d89b49 100644 --- a/plugins/deployment-planners/user-dispersing/pom.xml +++ b/plugins/deployment-planners/user-dispersing/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/drs/cluster/balanced/pom.xml b/plugins/drs/cluster/balanced/pom.xml index 3d10f190270f..a1c042981374 100644 --- a/plugins/drs/cluster/balanced/pom.xml +++ b/plugins/drs/cluster/balanced/pom.xml @@ -27,7 +27,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/drs/cluster/condensed/pom.xml b/plugins/drs/cluster/condensed/pom.xml index a568064537c9..3aec14dc5427 100644 --- a/plugins/drs/cluster/condensed/pom.xml +++ b/plugins/drs/cluster/condensed/pom.xml @@ -27,7 +27,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/event-bus/inmemory/pom.xml b/plugins/event-bus/inmemory/pom.xml index e497f6dad9a9..908bf9a88284 100644 --- a/plugins/event-bus/inmemory/pom.xml +++ b/plugins/event-bus/inmemory/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/event-bus/kafka/pom.xml b/plugins/event-bus/kafka/pom.xml index 91a78a260d24..36b052c0a6f4 100644 --- a/plugins/event-bus/kafka/pom.xml +++ b/plugins/event-bus/kafka/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/event-bus/rabbitmq/pom.xml b/plugins/event-bus/rabbitmq/pom.xml index 5a16ab17fe4d..5531209ca762 100644 --- a/plugins/event-bus/rabbitmq/pom.xml +++ b/plugins/event-bus/rabbitmq/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/event-bus/webhook/pom.xml b/plugins/event-bus/webhook/pom.xml index 83b4b77efbc0..e26e7cca9e2d 100644 --- a/plugins/event-bus/webhook/pom.xml +++ b/plugins/event-bus/webhook/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/ha-planners/skip-heurestics/pom.xml b/plugins/ha-planners/skip-heurestics/pom.xml index 272a26bd3a14..7818b9d14f3a 100644 --- a/plugins/ha-planners/skip-heurestics/pom.xml +++ b/plugins/ha-planners/skip-heurestics/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/host-allocators/random/pom.xml b/plugins/host-allocators/random/pom.xml index bc1fdc55d6ed..48f79d512caf 100644 --- a/plugins/host-allocators/random/pom.xml +++ b/plugins/host-allocators/random/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/hypervisors/baremetal/pom.xml b/plugins/hypervisors/baremetal/pom.xml index f5914e9b7730..d6fda1780e29 100755 --- a/plugins/hypervisors/baremetal/pom.xml +++ b/plugins/hypervisors/baremetal/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml cloud-plugin-hypervisor-baremetal diff --git a/plugins/hypervisors/external/pom.xml b/plugins/hypervisors/external/pom.xml index 8cc3a870221e..85bdd7ac19fe 100644 --- a/plugins/hypervisors/external/pom.xml +++ b/plugins/hypervisors/external/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml cloud-plugin-hypervisor-external diff --git a/plugins/hypervisors/hyperv/pom.xml b/plugins/hypervisors/hyperv/pom.xml index 28f6c412b729..dba7b5d640a1 100644 --- a/plugins/hypervisors/hyperv/pom.xml +++ b/plugins/hypervisors/hyperv/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/hypervisors/kvm/pom.xml b/plugins/hypervisors/kvm/pom.xml index 883012d4cc54..965856ec03ed 100644 --- a/plugins/hypervisors/kvm/pom.xml +++ b/plugins/hypervisors/kvm/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/hypervisors/ovm/pom.xml b/plugins/hypervisors/ovm/pom.xml index 2149926348ee..c31e37b522fc 100644 --- a/plugins/hypervisors/ovm/pom.xml +++ b/plugins/hypervisors/ovm/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/hypervisors/ovm3/pom.xml b/plugins/hypervisors/ovm3/pom.xml index c88284c8d297..55f72742e236 100644 --- a/plugins/hypervisors/ovm3/pom.xml +++ b/plugins/hypervisors/ovm3/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0-SNAPSHOT + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/hypervisors/simulator/pom.xml b/plugins/hypervisors/simulator/pom.xml index 76d989edee13..8d2a9949747d 100644 --- a/plugins/hypervisors/simulator/pom.xml +++ b/plugins/hypervisors/simulator/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml cloud-plugin-hypervisor-simulator diff --git a/plugins/hypervisors/ucs/pom.xml b/plugins/hypervisors/ucs/pom.xml index 8febcda74e0e..88386ca1f636 100644 --- a/plugins/hypervisors/ucs/pom.xml +++ b/plugins/hypervisors/ucs/pom.xml @@ -23,7 +23,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml cloud-plugin-hypervisor-ucs diff --git a/plugins/hypervisors/vmware/pom.xml b/plugins/hypervisors/vmware/pom.xml index 3c3a38da45b4..5c4223d0f903 100644 --- a/plugins/hypervisors/vmware/pom.xml +++ b/plugins/hypervisors/vmware/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/hypervisors/xenserver/pom.xml b/plugins/hypervisors/xenserver/pom.xml index 5eee2e47fc98..6b4e9a635aa7 100644 --- a/plugins/hypervisors/xenserver/pom.xml +++ b/plugins/hypervisors/xenserver/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/integrations/cloudian/pom.xml b/plugins/integrations/cloudian/pom.xml index 7799a3e5a348..42ef0c803d78 100644 --- a/plugins/integrations/cloudian/pom.xml +++ b/plugins/integrations/cloudian/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/integrations/kubernetes-service/pom.xml b/plugins/integrations/kubernetes-service/pom.xml index 24e42e15be8e..4cc4413aff4b 100644 --- a/plugins/integrations/kubernetes-service/pom.xml +++ b/plugins/integrations/kubernetes-service/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/integrations/prometheus/pom.xml b/plugins/integrations/prometheus/pom.xml index 77a584c25dc7..ec17155ab3d3 100644 --- a/plugins/integrations/prometheus/pom.xml +++ b/plugins/integrations/prometheus/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/maintenance/pom.xml b/plugins/maintenance/pom.xml index 65363e33b078..a2d97f45b6ef 100644 --- a/plugins/maintenance/pom.xml +++ b/plugins/maintenance/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/plugins/metrics/pom.xml b/plugins/metrics/pom.xml index 25102cd6da5f..c08e56087109 100644 --- a/plugins/metrics/pom.xml +++ b/plugins/metrics/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/plugins/network-elements/bigswitch/pom.xml b/plugins/network-elements/bigswitch/pom.xml index 8729e12782da..47e70fdc708f 100644 --- a/plugins/network-elements/bigswitch/pom.xml +++ b/plugins/network-elements/bigswitch/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/brocade-vcs/pom.xml b/plugins/network-elements/brocade-vcs/pom.xml index e4d29023fe98..d8eff794b59c 100644 --- a/plugins/network-elements/brocade-vcs/pom.xml +++ b/plugins/network-elements/brocade-vcs/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/cisco-vnmc/pom.xml b/plugins/network-elements/cisco-vnmc/pom.xml index f29ed0a4b798..081fd005fd1c 100644 --- a/plugins/network-elements/cisco-vnmc/pom.xml +++ b/plugins/network-elements/cisco-vnmc/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/dns-notifier/pom.xml b/plugins/network-elements/dns-notifier/pom.xml index 99451699621c..c90ce0f1edbb 100644 --- a/plugins/network-elements/dns-notifier/pom.xml +++ b/plugins/network-elements/dns-notifier/pom.xml @@ -22,7 +22,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml cloud-plugin-example-dns-notifier diff --git a/plugins/network-elements/elastic-loadbalancer/pom.xml b/plugins/network-elements/elastic-loadbalancer/pom.xml index 1283c19bc45b..9fc177a569b1 100644 --- a/plugins/network-elements/elastic-loadbalancer/pom.xml +++ b/plugins/network-elements/elastic-loadbalancer/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/globodns/pom.xml b/plugins/network-elements/globodns/pom.xml index 1f705b259278..e9909f8841d3 100644 --- a/plugins/network-elements/globodns/pom.xml +++ b/plugins/network-elements/globodns/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/internal-loadbalancer/pom.xml b/plugins/network-elements/internal-loadbalancer/pom.xml index fd426ae6f878..e94bf50860b2 100644 --- a/plugins/network-elements/internal-loadbalancer/pom.xml +++ b/plugins/network-elements/internal-loadbalancer/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/juniper-contrail/pom.xml b/plugins/network-elements/juniper-contrail/pom.xml index c13c5be79121..759a175a9171 100644 --- a/plugins/network-elements/juniper-contrail/pom.xml +++ b/plugins/network-elements/juniper-contrail/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/netris/pom.xml b/plugins/network-elements/netris/pom.xml index fd3fb3fa9402..8ec221ffa3a9 100644 --- a/plugins/network-elements/netris/pom.xml +++ b/plugins/network-elements/netris/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/netscaler/pom.xml b/plugins/network-elements/netscaler/pom.xml index 596e0f49ea78..3fd307e3d9ce 100644 --- a/plugins/network-elements/netscaler/pom.xml +++ b/plugins/network-elements/netscaler/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/nicira-nvp/pom.xml b/plugins/network-elements/nicira-nvp/pom.xml index cffdc20a3497..2f73f067497f 100644 --- a/plugins/network-elements/nicira-nvp/pom.xml +++ b/plugins/network-elements/nicira-nvp/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/nsx/pom.xml b/plugins/network-elements/nsx/pom.xml index 21297e7ce645..65705a63e52c 100644 --- a/plugins/network-elements/nsx/pom.xml +++ b/plugins/network-elements/nsx/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/opendaylight/pom.xml b/plugins/network-elements/opendaylight/pom.xml index a6d40c66ab42..3c8f45c0f6a9 100644 --- a/plugins/network-elements/opendaylight/pom.xml +++ b/plugins/network-elements/opendaylight/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/ovs/pom.xml b/plugins/network-elements/ovs/pom.xml index 455078e985a2..837934d06d30 100644 --- a/plugins/network-elements/ovs/pom.xml +++ b/plugins/network-elements/ovs/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/palo-alto/pom.xml b/plugins/network-elements/palo-alto/pom.xml index be8324a9b700..ab13698e78cd 100644 --- a/plugins/network-elements/palo-alto/pom.xml +++ b/plugins/network-elements/palo-alto/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/stratosphere-ssp/pom.xml b/plugins/network-elements/stratosphere-ssp/pom.xml index 6a6f10c49943..680180c241c2 100644 --- a/plugins/network-elements/stratosphere-ssp/pom.xml +++ b/plugins/network-elements/stratosphere-ssp/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/tungsten/pom.xml b/plugins/network-elements/tungsten/pom.xml index 51e8133a7832..7a97bf1bed26 100644 --- a/plugins/network-elements/tungsten/pom.xml +++ b/plugins/network-elements/tungsten/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/network-elements/vxlan/pom.xml b/plugins/network-elements/vxlan/pom.xml index 26f32d65be2f..244fcdc83d82 100644 --- a/plugins/network-elements/vxlan/pom.xml +++ b/plugins/network-elements/vxlan/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/outofbandmanagement-drivers/ipmitool/pom.xml b/plugins/outofbandmanagement-drivers/ipmitool/pom.xml index 78ab611bda05..649f99993ff7 100644 --- a/plugins/outofbandmanagement-drivers/ipmitool/pom.xml +++ b/plugins/outofbandmanagement-drivers/ipmitool/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/outofbandmanagement-drivers/nested-cloudstack/pom.xml b/plugins/outofbandmanagement-drivers/nested-cloudstack/pom.xml index 67cb45c70191..2d207363ca9c 100644 --- a/plugins/outofbandmanagement-drivers/nested-cloudstack/pom.xml +++ b/plugins/outofbandmanagement-drivers/nested-cloudstack/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/outofbandmanagement-drivers/redfish/pom.xml b/plugins/outofbandmanagement-drivers/redfish/pom.xml index d5f1cc19ca64..b95733175231 100644 --- a/plugins/outofbandmanagement-drivers/redfish/pom.xml +++ b/plugins/outofbandmanagement-drivers/redfish/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/pom.xml b/plugins/pom.xml index 6d7f9665ed66..b3b258e6f8a4 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT diff --git a/plugins/storage-allocators/random/pom.xml b/plugins/storage-allocators/random/pom.xml index 6281ba6f76a9..ee860f14ebb6 100644 --- a/plugins/storage-allocators/random/pom.xml +++ b/plugins/storage-allocators/random/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/storage/image/default/pom.xml b/plugins/storage/image/default/pom.xml index 2756b865aafe..ab7735a7019c 100644 --- a/plugins/storage/image/default/pom.xml +++ b/plugins/storage/image/default/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/image/s3/pom.xml b/plugins/storage/image/s3/pom.xml index 0a5f5212939f..cbb785bd6a6b 100644 --- a/plugins/storage/image/s3/pom.xml +++ b/plugins/storage/image/s3/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/image/sample/pom.xml b/plugins/storage/image/sample/pom.xml index 0ba1d96507ff..c2a127021d3a 100644 --- a/plugins/storage/image/sample/pom.xml +++ b/plugins/storage/image/sample/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/image/swift/pom.xml b/plugins/storage/image/swift/pom.xml index 85319d7a81a9..9638183aa937 100644 --- a/plugins/storage/image/swift/pom.xml +++ b/plugins/storage/image/swift/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/object/ceph/pom.xml b/plugins/storage/object/ceph/pom.xml index 31468200641f..822753e86e69 100644 --- a/plugins/storage/object/ceph/pom.xml +++ b/plugins/storage/object/ceph/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/object/cloudian/pom.xml b/plugins/storage/object/cloudian/pom.xml index ac08a7a7d849..d0320ae9c928 100644 --- a/plugins/storage/object/cloudian/pom.xml +++ b/plugins/storage/object/cloudian/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/object/minio/pom.xml b/plugins/storage/object/minio/pom.xml index 15e6f44db616..2fe6eaf6645f 100644 --- a/plugins/storage/object/minio/pom.xml +++ b/plugins/storage/object/minio/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/object/simulator/pom.xml b/plugins/storage/object/simulator/pom.xml index 71778dffcf4b..cfaf6fd65013 100644 --- a/plugins/storage/object/simulator/pom.xml +++ b/plugins/storage/object/simulator/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/sharedfs/storagevm/pom.xml b/plugins/storage/sharedfs/storagevm/pom.xml index 555b61508ade..3ab3e8c75d01 100644 --- a/plugins/storage/sharedfs/storagevm/pom.xml +++ b/plugins/storage/sharedfs/storagevm/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/adaptive/pom.xml b/plugins/storage/volume/adaptive/pom.xml index 8cc14a192d4b..483ca48f7725 100644 --- a/plugins/storage/volume/adaptive/pom.xml +++ b/plugins/storage/volume/adaptive/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/cloudbyte/pom.xml b/plugins/storage/volume/cloudbyte/pom.xml index aff11e4f9712..82f65e0fbf78 100644 --- a/plugins/storage/volume/cloudbyte/pom.xml +++ b/plugins/storage/volume/cloudbyte/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/datera/pom.xml b/plugins/storage/volume/datera/pom.xml index 91d26224a563..fbbd38b75d9c 100644 --- a/plugins/storage/volume/datera/pom.xml +++ b/plugins/storage/volume/datera/pom.xml @@ -16,7 +16,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/default/pom.xml b/plugins/storage/volume/default/pom.xml index c79103a54d9e..65d356bd797d 100644 --- a/plugins/storage/volume/default/pom.xml +++ b/plugins/storage/volume/default/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/flasharray/pom.xml b/plugins/storage/volume/flasharray/pom.xml index fc730cd595ff..69f85a0a8f69 100644 --- a/plugins/storage/volume/flasharray/pom.xml +++ b/plugins/storage/volume/flasharray/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/linstor/pom.xml b/plugins/storage/volume/linstor/pom.xml index a04810b91c6c..b09c7e336d97 100644 --- a/plugins/storage/volume/linstor/pom.xml +++ b/plugins/storage/volume/linstor/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/nexenta/pom.xml b/plugins/storage/volume/nexenta/pom.xml index c802d5b1e988..245f9c782121 100644 --- a/plugins/storage/volume/nexenta/pom.xml +++ b/plugins/storage/volume/nexenta/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/primera/pom.xml b/plugins/storage/volume/primera/pom.xml index a55d1cda6dc3..9cf82dcab950 100644 --- a/plugins/storage/volume/primera/pom.xml +++ b/plugins/storage/volume/primera/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/sample/pom.xml b/plugins/storage/volume/sample/pom.xml index 5d9be3fd6eb8..9f39512f131d 100644 --- a/plugins/storage/volume/sample/pom.xml +++ b/plugins/storage/volume/sample/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/scaleio/pom.xml b/plugins/storage/volume/scaleio/pom.xml index c2ebfba29b9c..e65d2c29aee6 100644 --- a/plugins/storage/volume/scaleio/pom.xml +++ b/plugins/storage/volume/scaleio/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/solidfire/pom.xml b/plugins/storage/volume/solidfire/pom.xml index 27cd8cd29511..e04cf48b55a2 100644 --- a/plugins/storage/volume/solidfire/pom.xml +++ b/plugins/storage/volume/solidfire/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/storage/volume/storpool/pom.xml b/plugins/storage/volume/storpool/pom.xml index a3913800f453..97d85ff8a110 100644 --- a/plugins/storage/volume/storpool/pom.xml +++ b/plugins/storage/volume/storpool/pom.xml @@ -17,7 +17,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../../pom.xml diff --git a/plugins/user-authenticators/ldap/pom.xml b/plugins/user-authenticators/ldap/pom.xml index 28da0df580e6..014b8ddbc7bb 100644 --- a/plugins/user-authenticators/ldap/pom.xml +++ b/plugins/user-authenticators/ldap/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/user-authenticators/md5/pom.xml b/plugins/user-authenticators/md5/pom.xml index 9c0548a7c9cc..6d23714a3dd4 100644 --- a/plugins/user-authenticators/md5/pom.xml +++ b/plugins/user-authenticators/md5/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/user-authenticators/oauth2/pom.xml b/plugins/user-authenticators/oauth2/pom.xml index ddd4b6ecd5ee..4048c50074dd 100644 --- a/plugins/user-authenticators/oauth2/pom.xml +++ b/plugins/user-authenticators/oauth2/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/user-authenticators/pbkdf2/pom.xml b/plugins/user-authenticators/pbkdf2/pom.xml index 89bb93ad29cc..94b072def0f0 100644 --- a/plugins/user-authenticators/pbkdf2/pom.xml +++ b/plugins/user-authenticators/pbkdf2/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/user-authenticators/plain-text/pom.xml b/plugins/user-authenticators/plain-text/pom.xml index 4eee43ee4010..e9211e129557 100644 --- a/plugins/user-authenticators/plain-text/pom.xml +++ b/plugins/user-authenticators/plain-text/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/user-authenticators/saml2/pom.xml b/plugins/user-authenticators/saml2/pom.xml index 9c56d11b55b3..6cb6da49bc4c 100644 --- a/plugins/user-authenticators/saml2/pom.xml +++ b/plugins/user-authenticators/saml2/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/user-authenticators/sha256salted/pom.xml b/plugins/user-authenticators/sha256salted/pom.xml index b59e1b6120d6..3195a57e40ee 100644 --- a/plugins/user-authenticators/sha256salted/pom.xml +++ b/plugins/user-authenticators/sha256salted/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/user-two-factor-authenticators/static-pin/pom.xml b/plugins/user-two-factor-authenticators/static-pin/pom.xml index 781a1d97a706..140d4ec01ce4 100644 --- a/plugins/user-two-factor-authenticators/static-pin/pom.xml +++ b/plugins/user-two-factor-authenticators/static-pin/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/plugins/user-two-factor-authenticators/totp/pom.xml b/plugins/user-two-factor-authenticators/totp/pom.xml index 87f8d9b3bf42..21758b86cf06 100644 --- a/plugins/user-two-factor-authenticators/totp/pom.xml +++ b/plugins/user-two-factor-authenticators/totp/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../../pom.xml diff --git a/pom.xml b/pom.xml index 4cc6b5914836..8ba7a161e67b 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT pom Apache CloudStack Apache CloudStack is an IaaS ("Infrastructure as a Service") cloud orchestration platform. diff --git a/quickcloud/pom.xml b/quickcloud/pom.xml index 2130fd87aa7e..b6aa4b04a500 100644 --- a/quickcloud/pom.xml +++ b/quickcloud/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/server/pom.xml b/server/pom.xml index 3e91e5a29061..c7f4d24d68b8 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT @@ -194,7 +194,7 @@ org.apache.cloudstack cloud-framework-extensions - 4.22.1.0 + 4.22.2.0-SNAPSHOT compile diff --git a/services/console-proxy/pom.xml b/services/console-proxy/pom.xml index 7ccbe9a1a1f8..772623b43051 100644 --- a/services/console-proxy/pom.xml +++ b/services/console-proxy/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-services - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/services/console-proxy/rdpconsole/pom.xml b/services/console-proxy/rdpconsole/pom.xml index ce8f587f1f1a..6422f0a4b770 100644 --- a/services/console-proxy/rdpconsole/pom.xml +++ b/services/console-proxy/rdpconsole/pom.xml @@ -26,7 +26,7 @@ org.apache.cloudstack cloudstack-service-console-proxy - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/services/console-proxy/server/pom.xml b/services/console-proxy/server/pom.xml index 8da4335bd235..2ed31d1c4463 100644 --- a/services/console-proxy/server/pom.xml +++ b/services/console-proxy/server/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-service-console-proxy - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/services/pom.xml b/services/pom.xml index 5e7315e347d5..ceb9898418cc 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/services/secondary-storage/controller/pom.xml b/services/secondary-storage/controller/pom.xml index 62c6c098ed09..e159b650f3d0 100644 --- a/services/secondary-storage/controller/pom.xml +++ b/services/secondary-storage/controller/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-service-secondary-storage - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/services/secondary-storage/pom.xml b/services/secondary-storage/pom.xml index f96826f02d4a..ff3023588df3 100644 --- a/services/secondary-storage/pom.xml +++ b/services/secondary-storage/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack-services - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/services/secondary-storage/server/pom.xml b/services/secondary-storage/server/pom.xml index 1ac2f00e8d44..3b4c30871946 100644 --- a/services/secondary-storage/server/pom.xml +++ b/services/secondary-storage/server/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-service-secondary-storage - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/systemvm/pom.xml b/systemvm/pom.xml index a949df8befcb..0ea722eba80b 100644 --- a/systemvm/pom.xml +++ b/systemvm/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/test/pom.xml b/test/pom.xml index 605915c5defa..861b7ee5d725 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT diff --git a/tools/apidoc/pom.xml b/tools/apidoc/pom.xml index 67c40ddcca0b..6fb584398c6d 100644 --- a/tools/apidoc/pom.xml +++ b/tools/apidoc/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloud-tools - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/tools/checkstyle/pom.xml b/tools/checkstyle/pom.xml index f5221fd67902..10f4568c4d87 100644 --- a/tools/checkstyle/pom.xml +++ b/tools/checkstyle/pom.xml @@ -22,7 +22,7 @@ Apache CloudStack Developer Tools - Checkstyle Configuration org.apache.cloudstack checkstyle - 4.22.1.0 + 4.22.2.0-SNAPSHOT UTF-8 diff --git a/tools/devcloud-kvm/pom.xml b/tools/devcloud-kvm/pom.xml index ab0eeda2b547..49927c120a2c 100644 --- a/tools/devcloud-kvm/pom.xml +++ b/tools/devcloud-kvm/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloud-tools - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/tools/devcloud4/pom.xml b/tools/devcloud4/pom.xml index ec89ae6abac7..40c5eb184566 100644 --- a/tools/devcloud4/pom.xml +++ b/tools/devcloud4/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloud-tools - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index 7cb2efb3d4df..a0516299f052 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -19,7 +19,7 @@ FROM ubuntu:22.04 -LABEL Vendor="Apache.org" License="ApacheV2" Version="4.22.1.0" Author="Apache CloudStack " +LABEL Vendor="Apache.org" License="ApacheV2" Version="4.22.2.0-SNAPSHOT" Author="Apache CloudStack " ARG DEBIAN_FRONTEND=noninteractive diff --git a/tools/docker/Dockerfile.marvin b/tools/docker/Dockerfile.marvin index 3e060b0f481c..fe5a0b0519ed 100644 --- a/tools/docker/Dockerfile.marvin +++ b/tools/docker/Dockerfile.marvin @@ -19,11 +19,11 @@ # build for cloudstack_home_dir not this folder FROM python:2 -LABEL Vendor="Apache.org" License="ApacheV2" Version="4.22.1.0" Author="Apache CloudStack " +LABEL Vendor="Apache.org" License="ApacheV2" Version="4.22.2.0-SNAPSHOT" Author="Apache CloudStack " ENV WORK_DIR=/marvin -ENV PKG_URL=https://builds.cloudstack.org/job/build-master-marvin/lastSuccessfulBuild/artifact/tools/marvin/dist/Marvin-4.22.1.0.tar.gz +ENV PKG_URL=https://builds.cloudstack.org/job/build-master-marvin/lastSuccessfulBuild/artifact/tools/marvin/dist/Marvin-4.22.2.0-SNAPSHOT.tar.gz RUN apt-get update && apt-get install -y vim RUN pip install --upgrade paramiko nose requests diff --git a/tools/marvin/pom.xml b/tools/marvin/pom.xml index 70e39a911d36..1f64cff5e68d 100644 --- a/tools/marvin/pom.xml +++ b/tools/marvin/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloud-tools - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py index 66eb32111675..941617c1562c 100644 --- a/tools/marvin/setup.py +++ b/tools/marvin/setup.py @@ -27,7 +27,7 @@ raise RuntimeError("python setuptools is required to build Marvin") -VERSION = "4.22.1.0" +VERSION = "4.22.2.0-SNAPSHOT" setup(name="Marvin", version=VERSION, diff --git a/tools/pom.xml b/tools/pom.xml index 15335c5f2fd6..f1826fce0e96 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -25,7 +25,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/usage/pom.xml b/usage/pom.xml index f51ffce2a5d7..9df18923d0ac 100644 --- a/usage/pom.xml +++ b/usage/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT diff --git a/utils/pom.xml b/utils/pom.xml index 7cb2df1deb37..def3a6818b13 100755 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT ../pom.xml diff --git a/vmware-base/pom.xml b/vmware-base/pom.xml index 53abed35493b..f96601d8ca0c 100644 --- a/vmware-base/pom.xml +++ b/vmware-base/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack - 4.22.1.0 + 4.22.2.0-SNAPSHOT From 4a49ffa9aa281b207ca5350ae697c6469be0c367 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 18 May 2026 17:24:14 +0530 Subject: [PATCH 060/205] update debian change log --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index bdd67f8f91e1..c0f1255b33cb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +cloudstack (4.22.2.0-SNAPSHOT) unstable; urgency=low + + * Update the version to 4.22.2.0-SNAPSHOT + + -- the Apache CloudStack project Mon, 18 May 2026 17:17:25 +0530 + cloudstack (4.22.1.0) unstable; urgency=low * Update the version to 4.22.1.0 From 94d911be9aebc4c2a56a1a0c17aeaf163515cfbe Mon Sep 17 00:00:00 2001 From: Tonitzpp <134986282+Tonitzpp@users.noreply.github.com> Date: Tue, 19 May 2026 04:52:36 -0300 Subject: [PATCH 061/205] Translation of missing labels to Portuguese (#12947) Co-authored-by: toni.zamparetti --- ui/public/locales/pt_BR.json | 1254 +++++++++++++++++++++++++++++++++- 1 file changed, 1234 insertions(+), 20 deletions(-) diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index d2910a3a2b98..4b446eccff32 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -1,27 +1,35 @@ { "alert.service.domainrouter": "Roteador do dom\u00ednio", +"error.dedicate.bgp.peer.failed": "Falha ao dedicar o par BGP", +"error.dedicate.ipv4.subnet.failed": "Falha ao dedicar a sub-rede IPv4", "error.dedicate.cluster.failed": "Falha ao dedicar cluster", "error.dedicate.host.failed": "Falha ao dedicar host", "error.dedicate.pod.failed": "Falha ao dedicar pod", "error.dedicate.zone.failed": "Falha ao dedicar zona", +"error.empty.counter.operator.threshold": "Contador, Operador ou Limite est\u00e1 vazio", "error.execute.api.failed": "Falha ao executar API", "error.fetching.async.job.result": "Foi encontrado um erro ao buscar o resultado do job ass\u00edncrono", -"error.form.message": "H\u00e1 problemas no formul\u00e1rio. Por favor, corrija-os.", +"error.form.message": "H\u00e1 problemas no formul\u00e1rio. Por favor, corrija-os", "error.password.not.match": "Os campos de senha n\u00e3o coincidem", +"error.release.dedicate.bgp.peer": "Falha ao liberar o par BGP dedicado", "error.release.dedicate.cluster": "Falha ao liberar cluster dedicado.", "error.release.dedicate.host": "Falha ao liberar host dedicado.", +"error.release.dedicate.ipv4.subnet": "Falha ao liberar a sub-rede IPv4 dedicada", "error.release.dedicate.pod": "Falha ao liberar pod dedicado.", "error.release.dedicate.zone": "Falha ao liberar zona dedicada.", +"error.unable.to.add.setting.extraconfig": "N\u00e3o \u00e9 permitido adicionar configura\u00e7\u00e3o para extraconfig. Por favor, habilite o par\u00e2metro extraconfig para a m\u00e1quina virtual", "error.unable.to.proceed": "N\u00e3o foi poss\u00edvel proceder. Por favor, contate um administrador.", "firewall.close": "Firewall", -"icmp.code.desc": "Informe -1 para permitir todos os c\u00f3digos ICMP.", -"icmp.type.desc": "Informe -1 para permitir todos os tipos ICMP.", +"icmp.code.desc": "Informe -1 para permitir todos os c\u00f3digos ICMP", +"icmp.type.desc": "Informe -1 para permitir todos os tipos ICMP", "inline": "Inline", "label.about": "Sobre", "label.about.app": "Sobre o CloudStack", "label.accept": "Aceitar", -"label.accept.project.invitation": "Aceitar convite de projeto.", +"label.accept.project.invitation": "Aceitar convite de projeto", "label.access": "Acesso", +"label.access.key": "Chave de acesso", +"label.access.kubernetes.nodes": "Acessar n\u00f3s Kubernetes", "label.accesskey": "Chave de acesso", "label.account": "Conta", "label.account.and.security.group": "Contas e grupos de seguran\u00e7a", @@ -45,19 +53,27 @@ "label.action.attach.iso": "Anexar ISO", "label.action.bulk.delete.egress.firewall.rules": "Apagar em massa as regras de sa\u00edda do firewall.", "label.action.bulk.delete.firewall.rules": "Apagar em massa as regras do firewall.", +"label.action.bulk.delete.ip.v6.firewall.rules": "Apagar em massa as regras de firewall IPv6.", "label.action.bulk.delete.isos": "Apagar em massa as ISOs.", "label.action.bulk.delete.load.balancer.rules": "Apagar em massa as regras de balanceamento de carga.", "label.action.bulk.delete.portforward.rules": "Apagar em massa as regras de encaminhamento de porta.", +"label.action.bulk.delete.routing.firewall.rules": "Remover em massa regras de firewall de Roteamento IPv4", +"label.action.bulk.delete.snapshots": "Excluir snapshots em massa", "label.action.bulk.delete.templates": "Apagar em massa as regras de template.", "label.action.bulk.release.public.ip.address": "Liberar em massa os enderec\u0327os de IPs P\u00fablicos.", "label.action.cancel.maintenance.mode": "Cancelar modo de manuten\u00e7\u00e3o", "label.action.change.password": "Trocar de senha", +"label.action.change.primary.storage.scope": "Alterar escopo do armazenamento prim\u00e1rio", +"label.action.clear.webhook.deliveries": "Limpar entregas do webhook", +"label.action.delete.webhook.deliveries": "Excluir entregas do webhook", "label.action.configure.stickiness": "Ader\u00eancia", "label.action.copy.iso": "Copiar ISO", +"label.action.copy.snapshot": "Copiar Snapshot", "label.action.copy.template": "Copiar template", "label.action.create.snapshot.from.vmsnapshot": "Criar snapshot a partir de uma snapshot de VM", "label.action.create.template.from.volume": "Criar template a partir do disco", "label.action.create.volume": "Criar disco", +"label.action.create.volume.add": "Criar e Adicionar Volume", "label.action.delete.account": "Remover conta", "label.action.delete.backup.offering": "Remover oferta de backup", "label.action.delete.cluster": "Remover cluster", @@ -65,20 +81,29 @@ "label.action.delete.domain": "Remover dom\u00ednio", "label.action.delete.egress.firewall": "Remover regra de sa\u00edda do firewall", "label.action.delete.firewall": "Remover regra de firewall", +"label.action.delete.guest.os": "Excluir Guest OS", +"label.action.delete.guest.os.hypervisor.mapping": "Excluir mapeamento de virtualizador do Guest OS", +"label.action.delete.interface.static.route": "Remover rota est\u00e1tica da interface Tungsten Fabric", "label.action.delete.ip.range": "Remover intervalo de IPs", "label.action.delete.iso": "Remover ISO", "label.action.delete.load.balancer": "Remover regra do balanceador de carga", "label.action.delete.network": "Remover rede", +"label.action.delete.network.permission": "Excluir permiss\u00e3o de Rede", +"label.action.delete.network.static.route": "Remover rota est\u00e1tica de Rede Tungsten Fabric", "label.action.delete.node": "Remover nodo", +"label.action.delete.oauth.provider": "Excluir provedor OAuth", +"message.action.delete.object.storage": "Por favor, confirme que voc\u00ea deseja excluir este Object Storage", "label.action.delete.physical.network": "Remover rede f\u00edsica", "label.action.delete.pod": "Remover pod", "label.action.delete.primary.storage": "Remover armazenamento prim\u00e1rio", +"label.action.delete.routing.firewall.rule": "Excluir regra de firewall de Roteamento IPv4", "label.action.delete.secondary.storage": "Remover armazenamento secund\u00e1rio", "label.action.delete.security.group": "Remover grupo de seguran\u00e7a", "label.action.delete.service.offering": "Remover plano", "label.action.delete.snapshot": "Remover snapshot", "label.action.delete.system.service.offering": "Remover oferta de servi\u00e7o de sistema", "label.action.delete.template": "Remover template", +"label.action.delete.tungsten.router.table": "Remover tabela de rotas Tungsten Fabric da Rede", "label.action.delete.user": "Remover usu\u00e1rio", "label.action.delete.volume": "Remover disco", "label.action.delete.zone": "Remover zona", @@ -87,11 +112,16 @@ "label.action.destroy.volume": "Destruindo volume", "label.action.detach.disk": "Desanexar disco", "label.action.detach.iso": "Desanexar ISO", +"label.action.disable.2FA.user.auth": "Desativar Autentica\u00e7\u00e3o de dois fatores do usu\u00e1rio", "label.action.disable.account": "Desativar conta", "label.action.disable.cluster": "Desativar cluster", +"label.action.disable.disk.offering": "Desativar oferta de disco", "label.action.disable.physical.network": "Desabilitar rede f\u00edsica", "label.action.disable.pod": "Desativar pod", +"label.action.disable.role": "Desativar Fun\u00e7\u00e3o", "label.action.disable.static.nat": "Desativar NAT est\u00e1tico", +"label.action.disable.service.offering": "Desativar oferta de servi\u00e7o", +"label.action.disable.system.service.offering": "Desativar oferta de servi\u00e7o do sistema", "label.action.disable.user": "Desativar usu\u00e1rio", "label.action.disable.zone": "Desativar zona", "label.action.download.iso": "Baixar ISO", @@ -102,22 +132,33 @@ "label.action.edit.domain": "Editar dom\u00ednio", "label.action.edit.instance": "Editar inst\u00e2ncia", "label.action.edit.iso": "Editar ISO", +"label.action.edit.nfs.options": "Editar op\u00e7\u00f5es de montagem NFS", +"label.action.edit.template": "Editar Template", "label.action.edit.zone": "Editar zona", "label.action.enable.account": "Ativar conta", "label.action.enable.cluster": "Ativar cluster", +"label.action.enable.disk.offering": "Ativar oferta de disco", "label.action.enable.maintenance.mode": "Ativar modo de manuten\u00e7\u00e3o", "label.action.enable.physical.network": "Habilitar rede f\u00edsica", "label.action.enable.pod": "Ativar pod", +"label.action.enable.role": "Ativar role", "label.action.enable.static.nat": "Ativar NAT est\u00e1tico", +"label.action.enable.service.offering": "Ativar oferta de servi\u00e7o", +"label.action.enable.system.service.offering": "Ativar oferta de servi\u00e7o do sistema", +"label.action.enable.two.factor.authentication": "Autentica\u00e7\u00e3o de dois fatores ativada", "label.action.enable.user": "Habilitar usu\u00e1rio", "label.action.enable.zone": "Ativar zona", "label.action.expunge.instance": "Eliminar inst\u00e2ncia", "label.action.force.reconnect": "Forc\u0327ar reconex\u00e3o", "label.action.generate.keys": "Gerar chaves", +"label.action.generate.api.secret.keys": "Gerar novos pares de chave de API", "label.action.get.diagnostics": "Obter diagn\u00f3stico", +"label.action.health.monitor": "Monitor de sa\u00fade", "label.action.image.store.read.only": "Colocar armazenamento secund\u00e1rio em modo somente leitura", "label.action.image.store.read.write": "Colocar armazenamento secund\u00e1rio em modo leitura-escrita", "label.action.import.export.instances": "Importar-exportar inst\u00e2ncias", +"label.action.import.unmanage.volumes": "Importar volumes de dados", +"label.action.ingest.instances": "Ingerir inst\u00e2ncias", "label.action.iso.permission": "Atualizar permiss\u00f5es da ISO", "label.action.iso.share": "Atualizar compartilhamento da ISO", "label.action.lock.account": "Bloquear conta", @@ -126,6 +167,9 @@ "label.action.migrate.router": "Migrar roteador", "label.action.migrate.systemvm": "Migrar VM de sistema", "label.action.migrate.systemvm.to.ps": "Migrar VM de sistema para outro armazenamento prim\u00e1rio", +"label.action.patch.systemvm": "Aplicar patch na VM de sistema", +"label.action.patch.systemvm.vpc": "Aplicar patch na VM de Sistema - Roteador Virtual de VPCs", +"label.action.patch.systemvm.processing": "Aplicando patch na VM de sistema....", "label.action.project.add.account": "Adicionar conta ao projeto", "label.action.project.add.user": "Adicionar usu\u00e1rio a um projeto", "label.action.quota.tariff.create": "Criar tarifa", @@ -136,21 +180,36 @@ "label.action.reboot.systemvm": "Reiniciar VM de sistema", "label.action.recover.volume": "Recuperar volume", "label.action.recurring.snapshot": "Snapshots recorrentes", +"label.action.resize.sharedfs": "Redimensionar Sistema de Arquivos Compartilhado", +"label.action.restart.sharedfs": "Reiniciar Sistema de Arquivos Compartilhado", "label.action.register.iso": "Registrar ISO", "label.action.register.template": "Registrar template a partir da URL", +"label.action.release.asnumber": "Liberar N\u00famero AS", "label.action.release.ip": "Liberar IP", +"label.action.release.reserved.ip": "Liberar IP reservado", "label.action.remove.host": "Remover host", +"label.action.remove.logical.router": "Remover Roteador L\u00f3gico", +"label.action.remove.network.policy": "Remover Pol\u00edtica de Rede", +"label.action.remove.nic": "Remover NIC", +"label.action.remove.router.table.from.interface": "Remover tabela de rotas Tungsten Fabric da interface", +"label.action.remove.tungsten.routing.policy": "Remover pol\u00edtica de roteamento Tungsten Fabric da Rede", +"label.action.reserve.ip": "Reservar IP P\u00fablico", +"label.action.reset.network.permissions": "Redefinir permiss\u00f5es de Rede", "label.action.reset.password": "Recuperar senha", "label.action.resize.volume": "Redimensionar volume", "label.action.revert.snapshot": "Reverter para snapshot", "label.action.router.health.checks": "Obter resultado das checagens de sa\u00fade", "label.action.run.diagnostics": "Executar diagn\u00f3sticos", "label.action.secure.host": "Propagar certificado de seguran\u00e7a para o host", +"label.action.set.as.source.nat.ip": "Tornar source NAT", +"label.action.setup.2FA.user.auth": "Configurar Autentica\u00e7\u00e3o de Dois Fatores do Usu\u00e1rio", "label.action.start.instance": "Iniciar inst\u00e2ncia", "label.action.start.router": "Iniciar roteador", "label.action.start.systemvm": "Iniciar VM de sistema", +"label.action.start.sharedfs": "Iniciar Sistema de Arquivos Compartilhado", "label.action.stop.instance": "Parar inst\u00e2ncia", "label.action.stop.router": "Parar roteador", +"label.action.stop.sharedfs": "Parar Sistema de Arquivos Compartilhado", "label.action.stop.systemvm": "Parar VM de sistema", "label.action.take.snapshot": "Criar snapshot", "label.action.template.permission": "Atualizar permiss\u00f5es do template", @@ -159,8 +218,15 @@ "label.action.unmanage.instance": "Parar de gerenciar inst\u00e2ncia", "label.action.unmanage.instances": "Parar de gerenciar inst\u00e2ncias", "label.action.unmanage.virtualmachine": "Parar de gerenciar VM", +"label.action.unmanage.volume": "Parar de gerenciar Volume", +"label.action.unmanage.volumes": "Parar de gerenciar Volumes", +"label.action.update.host": "Atualizar host", "label.action.update.offering.access": "Atualizar acesso \u00e0 oferta", +"label.action.update.security.groups": "Atualizar grupos de seguran\u00e7a", "label.action.update.resource.count": "Atualizar contagem de recursos", +"label.action.userdata.reset": "Redefinir Userdata", +"label.action.value": "A\u00e7\u00e3o/Valor", +"label.action.verify.two.factor.authentication": "Autentica\u00e7\u00e3o de dois fatores verificada", "label.action.vmsnapshot.create": "Criar snapshot de VM", "label.action.vmsnapshot.delete": "Remover snapshot de VM", "label.action.vmsnapshot.revert": "Reverter snapshot de VM", @@ -175,6 +241,7 @@ "label.add.acl": "Adiciona lista ACL", "label.add.affinity.group": "Adicionar um grupo de afinidade", "label.add.baremetal.dhcp.device": "Adicionar dispositivo DHCP baremetal", +"label.add.bgp.peer": "Adicionar par BGP", "label.add.bigswitchbcf.device": "Adicionar controlador BigSwitch BCF", "label.add.brocadevcs.device": "Adicionar switch Brocade Vcs", "label.add.by": "Adicionado por", @@ -182,40 +249,56 @@ "label.add.ciscoasa1000v": "Adicionar recurso CiscoASA1000v", "label.add.cluster": "Adicionar cluster", "label.add.compute.offering": "Adicionar oferta de computa\u00e7\u00e3o", +"label.add.condition": "Adicionar condi\u00e7\u00e3o", "label.add.disk.offering": "Adicionar oferta de disco", "label.add.domain": "Adicionar dom\u00ednio", "label.add.egress.rule": "Adicionar regra de sa\u00edda", "label.add.f5.device": "Adicionar dispositivo F5", "label.add.firewall": "Adicionar regra de firewall", +"label.add.firewallrule": "Adicionar Regra de Firewall", "label.add.guest.network": "Adicionar rede guest", +"label.add.guest.os": "Adicionar SO convidado", +"label.add.guest.os.hypervisor.mapping": "Adicionar mapeamento de hypervisor do Guest OS", +"label.add.gui.theme": "Adicionar tema da GUI", "label.add.host": "Adicionar host", "label.add.ingress.rule": "Adicionar regra de entrada", "label.add.intermediate.certificate": "Adicionar certificado intermedi\u00e1rio", "label.add.internal.lb": "Adicionar LB interno", "label.add.ip.range": "Adicionar intervalo de IPs", +"label.add.ip.v6.prefix": "Adicionar prefixo IPv6", +"label.add.ipv4.subnet": "Adicionar sub-rede IPv4 para redes Roteadas", "label.add.isolated.network": "Adiciona rede isolada", +"label.add.key.value": "Adicionar par de chaves valor", "label.add.kubernetes.cluster": "Adicionar cluster Kubernetes", "label.add.ldap.account": "Adicionar conta LDAP", "label.add.acl.name": "Nome da lista ACL", + "label.add.logical.router": "Adicionar Roteador L\u00f3gico a esta Rede", "label.add.more": "Adicionar mais", "label.add.netscaler.device": "Adicionar dispositivo Netscaler", "label.add.network": "Adicionar rede", "label.add.network.acl": "Adicione ACL de rede", "label.add.network.acl": "Adicionar lista de ACL de rede", "label.add.network.offering": "Adicionar oferta de rede", +"label.add.network.permission": "Adicionar permiss\u00e3o de Rede", "label.add.new.gateway": "Adicionar novo gateway", "label.add.new.tier": "Adicionar nova camada", "label.add.niciranvp.device": "Adicionar controlador Nvp", "label.add.note": "Adicionar coment\u00e1rio", +"label.add.object.storage" : "Adicionar Object Storage", "label.add.opendaylight.device": "Adiciona controlador OpenDaylight", "label.add.pa.device": "Adicionar dispositivo Palo Alto", +"label.add.param": "Adicionar par\u00c2metro", "label.add.physical.network": "Adicionar rede f\u00edsica", "label.add.pod": "Adicionar pod", +"label.add.policy": "Adicionar pol\u00edtica", +"label.add.prefix": "Adicionar prefixo", "label.add.primary.storage": "Adicionar armazenamento prim\u00e1rio", "label.add.private.gateway": "Adicionar gateway privado", "label.add.resources": "Adicionar recursos", "label.add.role": "Adicionar fun\u00e7\u00e3o", "label.add.route": "Adicionar rota", +"label.add.router.table.to.instance": "Adicionar tabela de roteamento a esta Inst\u00e2ncia", +"label.add.routing.policy": "Adicionar pol\u00edtica de roteamento", "label.add.rule": "Adicionar regra", "label.add.secondary.ip": "Adicionar IP secund\u00e1rio", "label.add.secondary.storage": "Adicionar armazenamento secund\u00e1rio", @@ -224,8 +307,26 @@ "label.add.srx.device": "Adicionar dispositivo SRX", "label.add.static.route": "Adicionar rota est\u00e1tica", "label.add.system.service.offering": "Adicionar oferta de servi\u00e7o para VMs de sistema", +"label.add.term.then": "Adicionar termo", "label.add.traffic": "Adicionar tr\u00e1fego", "label.add.traffic.type": "Adicionar tipo de tr\u00e1fego", +"label.add.tungsten.address.group": "Adicionar Grupo de Endere\u00e7os", +"label.add.tungsten.fabric.route": "Adicionar tabela de roteamento de Rede Tungsten Fabric", +"label.add.tungsten.firewall.policy": "Adicionar Pol\u00edtica de Firewall", +"label.add.tungsten.firewall.rule": "Adicionar Regra de Firewall", +"label.add.tungsten.interface.route": "Adicionar tabela de roteamento de interface Tungsten Fabric", +"label.add.tungsten.interface.static.route": "Adicionar rota est\u00e1tica de interface Tungsten Fabric", +"label.add.tungsten.logical.route": "Adicionar Roteador L\u00f3gico", +"label.add.tungsten.network.static.route": "Adicionar rota est\u00e1tica de Rede Tungsten Fabric", +"label.add.tungsten.policy": "Adicionar Pol\u00edtica", +"label.add.tungsten.policy.set": "Adicionar Conjunto de Pol\u00edticas de Aplica\u00e7\u00e3o", +"label.add.tungsten.router.table": "Adicionar tabela de roteamento a esta Rede", +"label.add.tungsten.routing.policy": "Adicionar pol\u00edtica de roteamento a esta Rede", +"label.add.tungsten.service.group": "Adicionar Grupo de Servi\u00e7os", +"label.add.tungsten.tag": "Adicionar Tag", +"label.add.tungsten.tag.type": "Adicionar Tipo de Tag", +"label.add.upstream.ipv4.routes": "Adicionar roteamento upstream IPv4", +"label.add.upstream.ipv6.routes": "Adicionar roteamento upstream IPv6", "label.add.user": "Adicionar usu\u00e1rio", "label.add.vm": "Adicionar VM", "label.add.vms": "Adicionar VMs", @@ -240,7 +341,9 @@ "label.adding": "Adicionando", "label.adding.user": "Adicionando usu\u00e1rio", "label.address": "Endere\u00e7o", +"label.address.group": "Grupo de endere\u00e7os", "label.admin": "Administrador", +"label.adminsonly": "Vis\u00edvel apenas para administradores", "label.advanced": "Avan\u00e7ado", "label.advanced.mode": "Modo avan\u00e7ado", "label.affinity": "Afinidade", @@ -248,6 +351,7 @@ "label.affinitygroup": "Grupo de afinidade", "label.agent.password": "Senha do agente", "label.agent.username": "Usu\u00e1rio do agente", +"label.agentcount": "N\u00famero de agentes conectados", "label.agentport": "Porta do agente", "label.agentstate": "Estado do agente", "label.agree": "Concordo", @@ -257,49 +361,85 @@ "label.algorithm": "Algoritmo", "label.all": "Todos", "label.all.available.data": "Todos os dados dispon\u00edveis", +"label.all.ipv6": "Todos IPv6", "label.all.zone": "Todas as zonas", "label.allocated": "Alocado", +"label.allocatedonly": "Alocado", "label.allocationstate": "Estado da aloca\u00e7\u00e3o", "label.allow": "Permitir", +"label.allow.duplicate.macaddresses": "Permitir endere\u00e7os MAC duplicados", "label.allowuserdrivenbackups": "Permitir backups manuais pelos usu\u00e1rios", "label.annotation": "Coment\u00e1rio", "label.annotations": "Coment\u00e1rios", "label.annotation.everyone": "Vis\u00edvel a todos", "label.anti.affinity": "Anti-afinidade", "label.anti.affinity.group": "Grupo de anti-afinidade", +"label.api.docs": "Documenta\u00e7\u00c3o da API", +"label.api.docs.description": "Para informa\u00e7\u00f5es sobre como as APIs funcionam e dicas de como us\u00e1-las, clique aqui para ver o Guia do Desenvolvedor", +"label.api.docs.count": "APIs dispon\u00edveis para sua conta", "label.api.version": "Vers\u00e3o da API", "label.apikey": "Chave da API", "label.app.cookie": "AppCookie", "label.app.name": "CloudStack", +"label.application.policy.set": "Conjunto de Pol\u00edticas de Aplica\u00e7\u00e3o", "label.apply": "Aplicar", +"label.apply.tungsten.firewall.policy": "Aplicar Pol\u00edtica de Firewall", +"label.apply.tungsten.network.policy": "Aplicar Pol\u00edtica de Rede", +"label.apply.tungsten.tag": "Aplicar tag", +"label.arch": "Arquitetura", +"label.archived": "Arquivado", "label.archive": "Arquivo", "label.archive.alerts": "Arquivar alertas", "label.archive.events": "Arquivar eventos", "label.as.default": "como padr\u00e3o", +"label.asnumber": "N\u00famero AS", +"label.asnumbers": "N\u00fameros AS", +"label.asnrange": "Faixa AS", "label.assign": "Atribuir", "label.assign.instance.another": "Atribuir inst\u00e2ncia para outra conta", "label.assign.vms": "Atribuir VMs", "label.assigning.vms": "Atribuindo VMs", +"label.associated.resource": "Recurso associado", "label.associatednetwork": "Rede associada", "label.associatednetworkid": "ID de rede associado", "label.associatednetworkname": "Nome da rede", "label.asyncbackup": "Backup ass\u00edncrono", +"label.attaching": "Anexando", +"label.authentication.method": "M\u00e9todo de Autentica\u00e7\u00e3o", +"label.authentication.sshkey": "Chave SSH do Sistema", +"label.autoscale": "AutoScale", +"label.autoscalevmgroupname": "Grupo de AutoScale", "label.author.email": "E-mail do autor", "label.author.name": "Nome do autor", +"label.auto.assign": "Atribuir automaticamente", "label.auto.assign.diskoffering.disk.size": "Atribuir automaticamente a oferta correspondente ao tamanho do disco", "label.auto.assign.random.ip": "Atribuir automaticamente um enderec\u0327o de IP aleat\u00f3rio", "label.auto.refresh.statistics": "Tempo entre atualiza\u00e7\u00f5es autom\u00e1ticas", "label.auto.refresh.statistics.none": "Nenhum", +"label.automigrate.volume": "Migrar volume automaticamente para outro pool de armazenamento, se neces\u00e1rio", +"label.autoscale.vm.groups": "Grupos de AutoScale", +"label.autoscale.vm.profile": "Perfil de Inst\u00e2ncia de AutoScale", "label.autoscalingenabled": "Escalonamento autom\u00e1tico", "label.availability": "Disponibilidade", "label.available": "Dispon\u00edvel", +"label.availableprocessors": "N\u00facleos de processador dispon\u00edveis", +"label.availablevirtualmachinecount": "Inst\u00e2ncias Dispon\u00edveis", "label.back": "Voltar", "label.backup": "Backup", "label.backups": "Backups", +"label.back.login": "Voltar à página de login", +"label.backup": "Backups", "label.backup.attach.restore": "Restaurar e anexar volume de backup", +"label.backuplimit": "Limite de backups", +"label.backup.storage": "Armazenamento de backup", +"label.backupstoragelimit": "Limite de armazenamento de backup (GiB)", +"label.backup.configure.schedule": "Configurar Agendamento de Backup", "label.backup.offering.assign": "Atribuir VM a oferta de backup", "label.backup.offering.remove": "Remover VM de oferta de backup", "label.backup.offerings": "Ofertas de backup", +"label.backup.repository": "Reposit\u00f3rio de Backup", +"label.backup.repository.add": "Adicionar reposit\u00f3rio de backup", +"label.backup.repository.remove": "Remover reposit\u00f3rio de backup", "label.backup.restore": "Restaurar backup da VM", "label.backupofferingid": "Oferta de backup", "label.backupofferingname": "Oferta de backup", @@ -316,6 +456,8 @@ "label.based.on.role.id.or.type": "Criar uma fun\u00e7\u00e3o baseado no ID da fun\u00e7\u00e3o ou no seu tipo", "label.basic": "B\u00e1sico", "label.bcfdeviceid": "ID", +"label.bgp.peers": "Pares BGP", +"label.bgp.peer.set.reservation.desc": "Voc\u00ea pode tornar o par BGP p\u00fablico, ou pode dedic\u00e1-lo/reserv\u00e1-lo para um Dom\u00ednio ou uma Conta", "label.bigswitch.controller.address": "Endere\u00e7o do controlador BigSwitch BCF", "label.bladeid": "ID da l\u00e2mina", "label.blades": "L\u00e2minas", @@ -327,7 +469,13 @@ "label.broadcastdomaintype": "Tipo de dom\u00ednio broadcast", "label.broadcasturi": "URI de broadcast", "label.brocade.vcs.address": "Endere\u00e7o do switch Vcs", +"label.browser": "Navegador", "label.bucket": "Bucket", +"label.bucket.delete": "Excluir Bucket", +"label.bucket.policy": "Pol\u00edtica do Bucket", +"label.bucket.update": "Atualizar Bucket", +"label.bucketlimit": "Limite de Bucket", +"label.buckets": "Buckets", "label.by.account": "por conta", "label.by.domain": "por dom\u00ednio", "label.by.level": "por n\u00edvel", @@ -338,6 +486,9 @@ "label.bypassvlanoverlapcheck": "Ignorar a sobreposi\u00e7\u00e3o de ID/intervalo de VLAN", "label.cachemode": "Tipo do cache de escrita", "label.cancel": "Cancelar", +"label.cancel.host.as.degraded": "Cancelar host como degradado", +"label.cancel.shutdown": "Cancelar Desligamento", +"label.cancelmaintenance": "Cancelar manuten\u00e7\u00e3o", "label.capacity": "Capacidade", "label.capacitybytes": "Capacidade de bytes", "label.capacityiops": "IOPS total", @@ -348,9 +499,13 @@ "label.certificate.upload.failed": "Falha ao carregar certificado", "label.certificate.upload.failed.description": "Falha ao atualizar certificado SSL. Falha na verifica\u00e7\u00e3o do certificado.", "label.certificateid": "ID do certificado", +"label.change": "Alterar", "label.change.affinity": "Mudar afinidade", +"label.change.bgp.peers": "Alterar pares BGP", +"label.change.disk.offering": "Alterar oferta de disco", "label.change.ip.address": "Trocar endere\u00e7o IP", "label.change.ipaddress": "Mudan\u00e7a de endere\u00e7o IP para NIC", +"label.change.offering.for.volume": "Alterar oferta de disco para o volume", "label.change.service.offering": "Alterar oferta de servi\u00e7o", "label.character": "Caracter", "label.checksum": "Checksum", @@ -359,6 +514,7 @@ "label.cidr": "CIDR", "label.cidr.destination.network": "CIDR da rede de destino", "label.cidrlist": "Lista de CIDRs", +"label.cidrsize": "Tamanho do CIDR", "label.cisco.nexus1000v.ip.address": "Endere\u00e7o IP do Nexus 1000v", "label.cisco.nexus1000v.password": "Senha do Nexus 1000v", "label.cisco.nexus1000v.username": "Usu\u00e1rio do Nexus 1000v", @@ -369,42 +525,75 @@ "label.cleanup": "Limpar", "label.clear": "Limpar", "label.clear.list": "Limpar lista", +"label.clear.notification": "Limpar notifica\u00e7\u00e3o", +"label.clientid": "ID do Cliente Provedor", "label.close": "Fechar", +"label.cloud.managed": "Gerenciado pela Nuvem", "label.cloudian.storage": "Armazenamento Cloudian", "label.cluster": "Cluster", "label.cluster.name": "Nome do cluster", "label.cluster.size": "Tamanho do cluster", "label.clusterid": "Cluster", "label.clustername": "Nome do cluster", +"label.clusternamelabel": "Nome do cluster", "label.clusters": "Clusters", "label.clustertype": "Tipo de cluster", "label.clvm": "CLVM", "label.code": "C\u00f3digo", +"label.collectiontime": "Tempo de coleta", +"label.columns": "Colunas", "label.comma.separated.list.description": "Insira uma lista de comandos separados por v\u00edrgulas", +"label.command": "Comando", "label.comments": "Coment\u00e1rios", +"label.commonnames": "Nomes Comuns (CN)", +"label.communities": "Comunidades", "label.community": "Comunidade", "label.complete": "Complete", +"label.compressionstatus": "Estado de compress\u00e3o", "label.compute": "Computa\u00e7\u00e3o", "label.compute.offerings": "Oferta de computa\u00e7\u00e3o", +"label.compute.offering.for.sharedfs.instance": "Oferta de computa\u00e7\u00e3o para Inst\u00e2ncia", +"label.computeonly.offering": "Oferta de disco apenas para computa\u00e7\u00e3o", +"label.computeonly.offering.tooltip": "Op\u00e7\u00e3o para especificar informa\u00e7\u00f5es relacionadas ao disco ROOT na oferta de computa\u00e7\u00e3o ou para vincular diretamente uma oferta de disco \u00e0 oferta de computa\u00e7\u00e3o", +"label.conditions": "Condi\u00e7\u00f5es", "label.configuration": "Configura\u00e7\u00e3o", "label.configure": "Configurar", +"label.configure.app": "Configurar o App", +"label.configure.instance": "Configurar inst\u00e2ncia", +"label.configure.health.monitor": "Configurar Monitor de Sa\u00fade", "label.configure.ldap": "Configurar LDAP", "label.configure.ovs": "Configure Ovs", "label.configure.sticky.policy": "Configurar sticky policy", "label.confirm.delete.egress.firewall.rules": "Por favor, confirme se deseja apagar as regras de firewall de sa\u00edda selecionadas", "label.confirm.delete.firewall.rules": "Por favor, confirme se deseja apagar as regras de firewall selecionadas", +"label.confirm.delete.ip.v6.firewall.rules": "Por favor, confirme que deseja excluir as regras de firewall IPv6 selecionadas", "label.confirm.delete.isos": "Por favor, confirme se deseja apagar as ISOs selecionadas", "label.confirm.delete.loadbalancer.rules": "Por favor, confirme se deseja apagar as regras de balanceamento de carga selecionadas", "label.confirm.delete.portforward.rules": "Por favor, confirme se deseja apagar as regras de encaminhamento de porta selecionadas", +"label.confirm.delete.routing.firewall.rules": "Por favor, confirme que deseja excluir as regras de firewall de Roteamento IPv4 selecionadas", +"label.confirm.delete.snapshot.zones": "Por favor, confirme que deseja excluir o Snapshot nas zonas selecionadas", "label.confirm.delete.templates": "Por favor, confirme se deseja apagar os templates selecionados", +"label.confirm.delete.tungsten.address.group": "Por favor, confirme que deseja excluir este Grupo de Endere\u00e7os", +"label.confirm.delete.tungsten.firewall.policy": "Por favor, confirme que deseja excluir esta Pol\u00edtica de Firewall", +"label.confirm.delete.tungsten.policy": "Por favor, confirme que deseja excluir esta Pol\u00edtica", +"label.confirm.delete.tungsten.policy.set": "Por favor, confirme que deseja excluir este Conjunto de Pol\u00edticas de Aplica\u00e7\u00e3o", +"label.confirm.delete.tungsten.service.group": "Por favor, confirme que deseja excluir este Grupo de Servi\u00e7os", +"label.confirm.delete.tungsten.tag": "Por favor, confirme que deseja excluir esta Tag", +"label.confirm.delete.tungsten.tag.type": "Por favor, confirme que deseja excluir este Tipo de Tag", +"label.confirm.remove.logical.router": "Por favor, confirme que deseja excluir este Roteador L\u00f3gico", "label.confirm.release.public.ip.addresses": "Por favor, confirme se deseja liberar os endere\u00e7os IPs selecionados", +"label.confirm.remove.network.route.table": "Por favor, confirme que deseja excluir esta Tabela de Rotas de Rede", +"label.confirm.remove.route.table": "Por favor, confirme que deseja excluir esta Tabela de Rotas de Interface", "label.confirmacceptinvitation": "Por favor, confirme se deseja participar deste projeto", "label.confirmation": "Confirma\u00e7\u00e3o", "label.confirmdeclineinvitation": "Voc\u00ea tem certeza que quer rejeitar este convite de projeto?", "label.confirmpassword": "Confirme a senha", "label.confirmpassword.description": "Por favor, digite a mesma senha novamente", +"label.connect": "Conectar", "label.connectiontimeout": "Tempo limite de conex\u00e3o", "label.conservemode": "Modo conservativo", +"label.considerlasthost": "Considerar \u00faltimo Host", +"label.consoleproxy": "Console proxy", "label.console.proxy": "Console proxy", "label.console.proxy.vm": "VM da console proxy", "label.continue": "Continuar", @@ -413,13 +602,19 @@ "label.copied.clipboard": "Copiado para a \u00c1rea de transfer\u00eancia", "label.copy": "Copiar", "label.copy.clipboard": "Copiar para \u00c1rea de transfer\u00eancia", +"label.copy.password": "Copiar senha", "label.copyid": "Copiar ID", +"label.core": "Core", +"label.core.zone.type": "Tipo de zona Core", +"label.counter": "Contador", +"label.counter.name": "Nome do contador para o qual a pol\u00edtica ser\u00e1 avaliada", "label.cpu": "CPU", "label.cpu.sockets": "Sockets da CPU", "label.cpu.usage.info": "Informa\u00e7\u00f5es sobre o uso de CPU", "label.cpuallocated": "CPU alocada por VMs", "label.cpuallocatedghz": "Alocado", "label.cpulimit": "Limite de CPU", +"label.cpuload": "% de CPU em uso", "label.cpumaxdeviation": "Desvio", "label.cpunumber": "N\u00facleos da CPU", "label.cpusockets": "O n\u00famero de sockets de CPU", @@ -429,13 +624,17 @@ "label.cpuused": "CPU utilizada", "label.cpuusedghz": "CPU utilizada", "label.create": "Criar", -"label.create.instance": "Criar nova instância", "label.create.account": "Criar conta", +"label.create.asnrange": "Criar faixa de número AS", "label.create.backup": "Iniciar backup", +"label.create.bucket": "Criar Bucket", +"label.create.instance": "Criar inst\u00e2ncia na nuvem", +"label.create.instance.from.backup": "Criar nova inst\u00e2ncia a partir de backup", "label.create.network": "Criar nova rede", "label.create.nfs.secondary.staging.storage": "Criar armazenamento secund\u00e1rio tempor\u00e1rio", "label.create.project": "Criar um projeto", "label.create.project.role": "Criar fun\u00e7\u00e3o para o projeto", +"label.create.routing.policy": "Criar Pol\u00edtica de Roteamento", "label.create.site.vpn.connection": "Criar conex\u00e3o VPN site-to-site", "label.create.site.vpn.gateway": "Criar gateway de VPN site-to-site", "label.create.snapshot.for.volume": "Criar snapshot para volume", @@ -447,11 +646,19 @@ "label.create.tier.name.description": "Um nome \u00fanico para a camada", "label.create.tier.netmask.description": "M\u00e1scara de rede da camada. Por exemplo: 255.255.255.0", "label.create.tier.networkofferingid.description": "A oferta de rede da camada", +"label.create.tungsten.routing.policy": "Criar pol\u00edtica de roteamento Tungsten-Fabric", +"label.create.sharedfs": "Criar Sistema de Arquivos Compartilhado", "label.create.user": "Criar usu\u00e1rio", +"label.create.vm": "Criar Inst\u00e2ncia", +"label.create.vm.and.stay": "Criar Inst\u00e2ncia e permanecer nesta p\u00e1gina", "label.create.vpn.connection": "Criar uma conex\u00e3o VPN", +"label.create.webhook": "Criar Webhook", "label.created": "Criado", +"label.creating": "Criando", "label.creating.iprange": "Criando intervalos de IP", "label.credit": "Cr\u00e9dito", +"label.cron": "Express\u00e3o Cron", +"label.cron.mode": "Modo Cron", "label.crosszones": "Inter zonas", "label.currency": "Moeda", "label.current": "Atual", @@ -469,14 +676,22 @@ "label.data.pool": "Data pool", "label.data.pool.description": "\u00c9 necess\u00e1rio informar um data pool ao utilizar um Ceph pool com erasure code", "label.date": "Data", +"label.datetime.filter.period": "De {startDate} at\u00e9 {endDate}", +"label.datetime.filter.starting": "Iniciando {startDate}", +"label.datetime.filter.up.to": "At\u00e9 {endDate}", "label.day": "Dia", "label.day.of.month": "Dia do m\u00eas", "label.day.of.week": "Dia da semana", +"label.db.usage.metrics": "Servidor de BD/Uso", +"label.dbislocal": "O banco de dados roda localmente", "label.dc.name": "Nome do DC", +"label.declare.host.as.degraded": "Declarar host como degradado", "label.decline.invitation": "Rejeitar convite", "label.dedicate": "Dedicado", +"label.dedicate.bgp.peer": "Dedicar par BGP", "label.dedicate.cluster": "Cluster dedicado", "label.dedicate.host": "Dedicar host", +"label.dedicate.ipv4.subnet": "Dedicar sub-rede IPv4", "label.dedicate.pod": "Pod dedicado", "label.dedicate.vlan.vni.range": "Intervalo de VLAN/VNI dedicado", "label.dedicate.zone": "Zona dedicada", @@ -484,6 +699,9 @@ "label.dedicated.vlan.vni.ranges": "Intervalo(s) de VLAN/VNI dedicados", "label.dedicatedresources": "Recursos dedicados", "label.default": "Padr\u00e3o", +"label.default.network.domain.isolated.network": "Dom\u00ednio de rede padr\u00e3o para redes Isoladas", +"label.default.network.guestcidraddress.isolated.network": "CIDR guest padr\u00e3o para Redes Isoladas", +"label.default.project": "Projeto padr\u00e3o", "label.default.use": "Uso padr\u00e3o", "label.default.view": "Visualiza\u00e7\u00e3o padr\u00e3o", "label.defaultnetwork": "Rede padr\u00e3o", @@ -491,12 +709,17 @@ "label.delete.acl": "Apagar lista ACL", "label.delete.affinity.group": "Apagar grupo de afinidade", "label.delete.alerts": "Remover alertas", +"label.delete.asnrange": "Excluir Faixa AS", +"label.delete.autoscale.vmgroup": "Excluir grupo de auto escalonamento horizontal", "label.delete.backup": "Apagar backup", +"label.delete.backup.schedule": "Remover agendamento de backup", +"label.delete.bgp.peer": "Excluir par BGP", "label.delete.bigswitchbcf": "Remover controlador BigSwitch BCF", "label.delete.brocadevcs": "Remover switch Brocade Vcs", "label.delete.certificate": "Apagar certificado", "label.delete.ciscoasa1000v": "Apagar CiscoASA1000v", "label.delete.ciscovnmc.resource": "Apagar recurso CiscoVNMC", +"label.delete.condition": "Excluir condi\u00e7\u00e3o", "label.delete.dedicated.vlan.range": "Apagar intervalo de VLAN/VNI dedicado", "label.delete.domain": "Apagar dom\u00ednio", "label.delete.events": "Remover eventos", @@ -505,6 +728,8 @@ "label.delete.icon": "Remover \u00edcone", "label.delete.instance.group": "Apagar grupo de inst\u00e2ncias", "label.delete.internal.lb": "Apagar LB interno", +"label.delete.ipv4.subnet": "Excluir sub-rede IPv4", +"label.delete.ip.v6.prefix": "Excluir prefixo IPv6", "label.delete.netscaler": "Remover NetScaler", "label.delete.niciranvp": "Remover controlador Nvp", "label.delete.opendaylight.device": "Apagar controladora OpenDaylight", @@ -518,28 +743,68 @@ "label.delete.snapshot.policy": "Apagar pol\u00edtica de snapshot", "label.delete.srx": "Remover SRX", "label.delete.sslcertificate": "Apagar certificado SSL", +"label.delete.tag": "Remover tag", +"label.delete.term": "Excluir termo", +"label.delete.traffic.type": "Excluir tipo de tr\u00e1fego", +"label.delete.tungsten.address.group": "Excluir Grupo de Endere\u00e7os", +"label.delete.tungsten.fabric.tag": "Excluir Tag", +"label.delete.tungsten.fabric.tag.type": "Excluir Tipo de Tag", +"label.delete.tungsten.firewall.policy": "Excluir Pol\u00edtica de Firewall", +"label.delete.tungsten.policy": "Excluir Pol\u00edtica", +"label.delete.tungsten.policy.set": "Excluir Conjunto de Pol\u00edticas", +"label.delete.tungsten.service.group": "Excluir Grupo de Servi\u00e7os", "label.delete.volumes": "Volumes a serem apagados", "label.delete.vpn.connection": "Remover conex\u00e3o VPN", "label.delete.vpn.customer.gateway": "Remover gateway de VPN de usu\u00e1rio", "label.delete.vpn.gateway": "Remover gateway de VPN", "label.delete.vpn.user": "Apagar usu\u00e1rio VPN", +"label.delete.webhook": "Excluir Webhook", +"label.delete.webhook.delivery": "Excluir Entrega de Webhook", "label.deleteconfirm": "Por favor, confirme que voc\u00ea deseja apagar isto", "label.deleting": "Removendo", "label.deleting.failed": "Falha ao remover", "label.deleting.iso": "Removendo ISO", "label.deleting.template": "Remover template", +"label.deleting.snapshot": "Excluindo Snapshot", +"label.deleteprotection": "Prote\u00e7\u00e3o contra exclus\u00e3o", "label.demote.project.owner": "Rebaixar conta para fun\u00e7\u00e3o regular", "label.demote.project.owner.user": "Rebaixar usu\u00e1rio para fun\u00e7\u00e3o regular", "label.deny": "Negar", "label.deployasis": "Ler configura\u00e7\u00f5es da VM do OVA", "label.deploymentplanner": "Planejador de implanta\u00e7\u00e3o", +"label.desc.db.stats": "Estat\u00edsticas do Banco de Dados", +"label.desc.import.ext.kvm.wizard": "Importar Inst\u00e2ncia de host KVM remoto", +"label.desc.import.local.kvm.wizard": "Importar imagem QCOW2 do Armazenamento Local", +"label.desc.import.shared.kvm.wizard": "Importar imagem QCOW2 do Armazenamento Compartilhado", +"label.desc.import.unmanage.volume": "Importar e parar de gerenciar volume em Pools de Armazenamento", "label.desc.importexportinstancewizard": "Importar e exportar inst\u00e2ncias para/de um cluster VMWare existente", +"label.desc.importmigratefromvmwarewizard": "Importar inst\u00e2ncias do VMware para um cluster KVM", +"label.desc.ingesttinstancewizard": "Ingerir inst\u00e2ncias de um host KVM externo", +"label.desc.usage.stats": "Estat\u00edsticas do Usage Server", "label.description": "Descri\u00e7\u00e3o", +"label.destaddressgroupuuid": "Grupo de Endere\u00e7os de Destino", "label.destcidr": "CIDR de destino", +"label.destendport": "Porta Final de Destino", +"label.desthost": "Host de destino", "label.destination": "Destino", +"label.destination.cluster": "Cluster de Destino", +"label.destination.hypervisor": "Hypervisor de Destino", +"label.destination.pod": "Pod de Destino", +"label.destination.zone": "Zona de Destino", "label.destinationphysicalnetworkid": "ID de destino da rede f\u00edsica", +"label.destinationtype": "Tipo de Destino", +"label.destipprefix": "Endere\u00e7o de Rede de Destino", +"label.destipprefixlen": "Comprimento do Prefixo de Destino", +"label.destnetwork": "Rede de Destino", +"label.destnetworkuuid": "Rede", +"label.destport": "Portas de Destino", "label.destroy": "Apagar", "label.destroy.router": "Destruir roteador", +"label.destroy.sharedfs": "Destruir Sistema de Arquivos Compartilhado", +"label.destroying": "Destruindo", +"label.destroyed": "Destru\u00eddo", +"label.deststartport": "Porta Inicial de Destino", +"label.desttaguuid": "Tag de Destino", "label.details": "Detalhes", "label.deviceid": "ID do dispositivo", "label.devices": "Dispositivos", @@ -547,16 +812,21 @@ "label.direct.attached.public.ip": "IP p\u00fablico conectado diretamente", "label.direct.ips": "IPs diretos", "label.directdownload": "Download direto", +"label.direction": "Dire\u00e7\u00e3o", +"label.disable.autoscale.vmgroup": "Desativar grupo de auto escalonamento horizontal", "label.disable.host": "Desabilitar host", "label.disable.network.offering": "Desabilitar oferta de rede", "label.disable.provider": "Desabilitar provedor", "label.disable.storage": "Desabilitar pool de armazenamento", "label.disable.vpc.offering": "Desabilitar oferta VPC", "label.disable.vpn": "Desabilitar VPN", +"label.disable.webhook": "Desativar Webhook", "label.disabled": "Desativado", "label.disconnected": "\u00daltima desconex\u00e3o", "label.disk": "Disco", "label.disk.offerings": "Ofertas de disco", +"label.disk.path": "Caminho do Disco", +"label.disk.tooltip": "Nome do arquivo de Imagem de Disco no Pool de Armazenamento selecionado", "label.disk.selection": "Sele\u00e7\u00e3o de disco", "label.disk.size": "Tamanho do disco", "label.disk.usage.info": "Informa\u00e7\u00f5es sobre o uso de disco", @@ -581,6 +851,7 @@ "label.disksize": "Tamanho (em GB)", "label.disksizeallocated": "Disco alocado", "label.disksizeallocatedgb": "Alocado", +"label.disksizefree": "Disco livre", "label.disksizetotal": "Disco total", "label.disksizetotalgb": "Total", "label.disksizeunallocatedgb": "N\u00e3o alocado", @@ -602,27 +873,46 @@ "label.domainid": "ID do dom\u00ednio", "label.domainname": "Dom\u00ednio", "label.domainpath": "Dom\u00ednio", +"label.domainrouter": "Roteador Virtual", "label.domains": "Dom\u00ednios", "label.done": "Pronto", +"label.down": "Baixo", "label.download": "Download", +"label.download.csv": "Baixar CSV", "label.download.kubeconfig.cluster": "Baixar kubeconfig para o cluster

A ferramenta de linha de comando kubectl usa os arquivos kubeconfig para encontrar informa\u00e7\u00f5es necess\u00e1rias para escolher o cluster e se comunicar com o servidor da API do cluster.", "label.download.kubectl": "Baixar a ferramenta kubectl para a vers\u00e3o Kubernetes do cluster", "label.download.kubernetes.cluster.config": "Baixar a configura\u00e7\u00e3o do cluster Kubernetes", "label.download.percent": "Porcentagem do download", +"label.download.setting": "Baixar configura\u00e7\u00e3o", "label.download.state": "Estado do download", "label.dpd": "Detec\u00e7\u00e3o de correspondente morto", "label.driver": "Driver", +"label.drs": "DRS", +"label.drsimbalance": "Desequil\u00edbrio DRS", +"label.drs.generate.plan": "Gerar plano DRS", +"label.drs.no.plan.generated": "Nenhum plano DRS foi gerado pois o cluster n\u00e3o est\u00e1 desequilibrado", +"label.drs.plan": "Plano DRS", +"label.duration": "Dura\u00e7\u00e3o (em segundos)", "label.duration.custom": "Personalizado", "label.duration.1hour": "1 hora", "label.duration.6hours": "6 horas", "label.duration.12hours": "12 horas", "label.duration.24hours": "24 horas", "label.duration.7days": "7 dias", +"label.dynamic": "Din\u00e2mico", "label.dynamicscalingenabled": "Escalonamento din\u00e2mico habilitado", "label.dynamicscalingenabled.tooltip": "VM s\u00f3 pode ser dinamicamente escalonada quando o escalonamento din\u00e2mico estiver habilitado no template, oferta de computa\u00e7\u00e3o e nas configura\u00e7\u00e3oes globais", +"label.diskofferingstrictness": "Rigor da oferta de disco", +"label.disksizestrictness": "Rigor do tamanho do disco", +"label.edge": "Borda", +"label.edge.zone": "Zona de Borda", +"label.enable.autoscale.vmgroup": "Ativar Grupo de AutoScaleAutoScaleAutoScale", "label.edit": "Editar", "label.edit.acl": "Editar lista ACL", +"label.edit.account": "Editar conta", +"label.edit.acl.list": "Editar lista ACL", "label.edit.acl.rule": "Editar regra ACL", +"label.edit.autoscale.vmprofile": "Editar Perfil de Inst\u00e2ncia de AutoScale", "label.edit.nic": "Editar NIC", "label.edit.project.details": "Editar detalhes do projeto", "label.edit.project.role": "Editar fun\u00e7\u00e3o do projeto", @@ -640,11 +930,18 @@ "label.email": "Email", "label.enable.host": "Habilita host", "label.enable.network.offering": "Habilita oferta de rede", +"label.enable.oauth": "Ativar Login OAuth", "label.enable.provider": "Habilitar provedor", "label.enable.storage": "Habilitar pool de armazenamento", "label.enable.vpc.offering": "Habilitar oferta VPC", "label.enable.vpn": "Habilitar VPN", +"label.enable.webhook": "Ativar Webhook", +"label.enabled": "Ativado", +"label.encrypt": "Criptografar", +"label.encryption": "Criptografia", +"label.encryptroot": "Criptografar Disco ROOT", "label.end": "Fim", +"label.endasn": "Fim Como N\u00famero", "label.end.date": "Data de término", "label.end.date.and.time": "Data e hor\u00e1rio final", "label.end.ip": "IP final", @@ -656,7 +953,11 @@ "label.endipv6": "IP final IPv6", "label.endpoint": "Ponto de acesso", "label.endport": "Porta final", +"label.enter.code": "Insira o c\u00f3digo 2FA para verificar", +"label.enter.static.pin": "Insira o PIN est\u00e1tico para verificar", "label.enter.token": "Digite o token", +"label.entityid": "Entidade", +"label.entitytype": "Tipo de Entidade", "label.error": "Erro", "label.error.caught": "Erro detectado", "label.error.code": "C\u00f3digo de erro", @@ -666,10 +967,14 @@ "label.error.setting": "Erro de configura\u00e7\u00e3o", "label.error.something.went.wrong.please.correct.the.following": "Alguma coisa est\u00e1 errada; por favor corrija abaixo", "label.error.upper": "ERRO", +"label.errorinmaintenance": "Erro na manuten\u00e7\u00e3o", "label.espencryption": "Encripta\u00e7\u00e3o ESP", "label.esphash": "Hash ESP", "label.esplifetime": "Tempo de vida do ESP (segundos)", "label.esppolicy": "Pol\u00edtica ESP", +"label.estimatedcost": "Custo estimado", +"label.eventid": "Evento", +"label.eventtype": "Tipo de evento", "label.esx.host": "ESX/ESXi host", "label.event": "Eventos", "label.event.archived": "Evento arquivado", @@ -679,10 +984,21 @@ "label.every": "Cada", "label.example": "Exemplo", "label.example.plugin": "PluginDeExemplo", +"label.execute": "Executar", +"label.existing": "Existente", "label.expunge": "Eliminar", +"label.export.data.csv": "Exportar dados como CSV", +"label.export.details.csv": "Exportar detalhes como CSV", +"label.export.resources.csv": "Exportar recursos como CSV", +"label.export.rules": "Exportar Regras", +"label.expunge.sharedfs": "Expurgar Sistema de Arquivos Compartilhado", +"label.expungevmgraceperiod": "Per\u00edodo de car\u00eancia para expurgar Inst\u00e2ncia (em seg)", "label.expunged": "Eliminado", "label.expunging": "Eliminando", +"label.ext.hostname.tooltip": "Nome do Host Externo ou Endere\u00e7o IP", "label.external.link": "Link externo", +"label.external": "Externo", +"label.external.managed": "Gerenciado Externamente", "label.externalid": "ID externo", "label.externalloadbalanceripaddress": "Endere\u00e7o externo do balanceador de carga", "label.extra": "Argumentos extras", @@ -690,25 +1006,43 @@ "label.f5.ip.loadbalancer": "Balanceador de carga F5 Big Ip", "label.failed": "Falhou", "label.featured": "Em destaque", +"label.fetch.from.backup": "Buscar do backup", +"label.fetch.instances": "Buscar Inst\u00e2ncias", "label.fetch.latest": "Atualizar", +"label.fetched": "Buscado", +"label.filename": "Nome do Arquivo", "label.files": "Alterar arquivos a serem recuperados", +"label.filesystem": "Sistema de Arquivos", "label.filter": "Filtro", "label.filter.annotations.self": "Criado por mim", "label.filter.annotations.all": "Todos os coment\u00e1rios", "label.filterby": "Filtrar por", "label.fingerprint": "Impress\u00e3o digital", +"label.finish": "Finalizar", "label.firewall": "Firewall", +"label.firewall.policy": "Pol\u00edtica de Firewall", +"label.firewallpolicy": "Pol\u00edtica de Firewall", "label.firewallrule": "Regra de firewall", +"label.firewallruleuuid": "Regra de Firewall", "label.firstname": "Primeiro nome", +"label.firstname.lower": "primeiro nome", "label.fix.errors": "Corrija os erros", "label.fixed": "Fixa", "label.for": "para", "label.forbidden": "Proibido", +"label.force.ms.to.import.vm.files": "For\u00e7ar MS a importar arquivo(s) da VM para armazenamento tempor\u00e1rio", +"label.force.reboot": "For\u00e7ar reinicializa\u00e7\u00e3o", +"label.force.stop": "For\u00e7ar parada", "label.forced": "For\u00e7ar", "label.forceencap": "For\u00e7ar encapsulamento UDP de pacotes ESP", "label.forgedtransmits": "Transmiss\u00f5es forjadas", +"label.forgot.password": "Esqueceu a senha?", +"label.fornsx": "NSX", +"label.forvpc": "VPC", +"label.fsprovidername": "Provedor de Sistema de Arquivos Compartilhado", "label.format": "Formato", "label.free": "Livre", +"label.french.azerty.keyboard": "Teclado franc\u00eas AZERTY", "label.friday": "Sexta-feira", "label.from": "de", "label.from.lb": "de LB", @@ -717,6 +1051,7 @@ "label.fwdeviceid": "ID", "label.fwdevicestate": "Estado", "label.gateway": "Gateway", +"label.global": "Global", "label.global.settings": "Configura\u00e7\u00f5es globais", "label.globo.dns": "GloboDNS", "label.globo.dns.configuration": "Configurar GloboDNS", @@ -741,8 +1076,11 @@ "label.guest.ip.range": "Intervalo da rede guest", "label.guest.netmask": "M\u00e1scara da rede guest", "label.guest.networks": "Redes guest", +"label.guest.os": "SO Convidado", +"label.guest.os.hypervisor.mappings": "Mapeamentos de SO convidado", "label.guest.start.ip": "IP de in\u00edcio do guest", "label.guest.traffic": "Tr\u00e1fego da redes guest", +"label.guest.vlan": "VLAN Guest", "label.guestcidraddress": "CIDR da rede guest", "label.guestendip": "IP do fim do guest", "label.guestgateway": "Gateway da rede guest", @@ -762,11 +1100,15 @@ "label.haenable": "HA ativado", "label.haprovider": "Provedor HA", "label.hardware": "Hardware", +"label.hasrules":"Regras de FW definidas", "label.hastate": "Estado do HA", "label.header.backup.schedule": "Voc\u00ea pode definir cronogramas de backup recorrentes selecionando dentre as op\u00e7\u00f5es dispon\u00edveis abaixo e aplicando suas pol\u00edticas preferenciais", "label.header.volume.snapshot": "Voc\u00ea pode configurar snapshots recorrentes selecionando as op\u00e7\u00f5es dispon\u00edveis abaixo e aplicando suas pol\u00edticas preferenciais", "label.header.volume.take.snapshot": "Por favor confirme que voc\u00ea deseja criar uma snapshot para este volume.", +"label.headers": "Cabe\u00e7alhos", "label.health.check": "Checagem de sa\u00fade", +"label.heapmemoryused": "Mem\u00f3ria heap usada", +"label.heapmemorytotal": "Mem\u00f3ria heap dispon\u00edvel", "label.help": "Ajuda", "label.hideipaddressusage": "Ocultar uso do endere\u00e7o IP", "label.home": "In\u00edcio", @@ -774,11 +1116,19 @@ "label.host.alerts": "Hosts em estado de alerta", "label.host.name": "Nome do host", "label.host.tag": "Tag de host", +"label.hostcontrolstate": "Status do Recurso de Computa\u00e7\u00e3o", "label.hostid": "Host", "label.hostname": "Host", +"label.hostname.tooltip": "Host de Destino. O volume deve estar localizado no armazenamento local deste Host", "label.hostnamelabel": "Nome do host", "label.hosts": "Hosts", "label.hosttags": "Tags de host", +"label.hosttags.explicit": "Tags de Host definidas pela API", +"label.hosttags.explicit.abbr": "definido pela api", +"label.hosttags.explicit.description": "As tags de host definidas pelas APIs do CloudStack", +"label.hosttags.implicit": "Tags de Host definidas pelo Agente", +"label.hosttags.implicit.abbr": "definido pelo agente", +"label.hosttags.implicit.description": "As tags de host definidas pelo Agente CloudStack", "label.hourly": "A cada hora", "label.hypervisor": "Virtualizador", "label.hypervisor.capabilities": "Recursos do virtualizador", @@ -803,22 +1153,31 @@ "label.ikepolicy": "Pol\u00edtica IKE", "label.ikeversion": "Vers\u00e3o do IKE", "label.images": "Imagens", +"label.imagestoreid": "Armazenamento Secun\u00e1rio", "label.import.backup.offering": "Importar oferta de backup", "label.import.instance": "Importar inst\u00e2ncia", "label.import.offering": "Importar oferta", "label.import.role": "Importar fun\u00e7\u00e3o", +"label.import.volume": "Importar Volume", +"label.inactive": "Inativo", "label.in.progress": "em progresso", "label.in.progress.for": "em progresso para", "label.info": "Info", "label.info.upper": "INFO", "label.infrastructure": "Infraestrutura", +"label.ingest.instance": "Ingerir Inst\u00e2ncia", "label.ingress": "Entrada", "label.ingress.rule": "Regra de entrada", +"label.initial": "Inicial", +"label.initialized": "Inicializado", +"label.interface.route.table": "Tabela de Rotas de Interface", +"label.interface.router.table": "Tabela de Rotas de Interface", "label.insideportprofile": "Perfil de porta interna", "label.installwizard.addzoneintro.title": "Vamos adicionar uma zona", "label.installwizard.subtitle": "Este tour vai auxiliar voc\u00ea na configura\u00e7\u00e3o da sua instala\u00e7\u00e3o do CloudStack™", "label.installwizard.title": "Ol\u00e1, seja bem vindo ao CloudStack™", "label.instance": "Inst\u00e2ncia", +"label.instance.conversion.support": "Convers\u00e3o de Inst\u00e2ncia Suportada", "label.instance.groups": "Grupos de inst\u00e2ncia", "label.instance.name": "Nome da inst\u00e2ncia", "label.instancename": "Nome interno", @@ -834,35 +1193,61 @@ "label.internallb.name.description": "Nome \u00fanico para o LB interno", "label.internallb.sourceip.description": "Descri\u00e7\u00e3o do LB interno", "label.internallbvm": "LbVm interno", +"label.internetprotocol": "Protocolo de internet", +"label.invalid.number": "N\u00famero inv\u00e1lido", +"label.iodriverpolicy" : "Pol\u00edtica de driver de IO", +"label.iodriverpolicy.tooltip" : "A pol\u00edtica de driver de IO pode ser nativa, io_uring ou threads. Escolher a pol\u00edtica de IO para uma Inst\u00e2ncia substituir\u00e1 a op\u00e7\u00e3o do pool de armazenamento 'kvm.storage.pool.io.policy' se definida (apenas se iothreads estiver ativado)", +"label.iops": "IOPS", +"label.iothreadsenabled" : "IOThreads", +"label.iothreadsenabled.tooltip" : "Ativar aloca\u00e7\u00e3o de iothreads para hypervisor KVM", "label.interval": "Intervalo de verifica\u00e7\u00e3o (em seg)", "label.intervaltype": "Tipo de intervalo", "label.introduction.to.cloudstack": "Introdu\u00e7\u00e3o ao CloudStack™", "label.invitations": "Convites", "label.invite": "Convidar", "label.ip": "IP", +"label.ip.addresses": "Endere\u00e7os IP", +"label.ip.range.type": "Tipo de intervalo de IP", "label.ip.range": "Intervalo de IP", "label.ip.ranges": "Intervalos de IP", +"label.ip.v4": "IPv4", +"label.ip.v4.v6": "IPv4 + IPv6 (Pilha Dupla)", +"label.ip.v6": "IPv6", +"label.ip.v6.firewall": "Firewall IPv6", "label.ip4gateway": "Gateway IPV4", "label.ip4netmask": "M\u00e1scara de rede IPv4", +"label.ip4routes": "Rotas IPv6", +"label.ip4routing": "Roteamento IPv4", "label.ip6address": "Endere\u00e7o IPv6", "label.ip6cidr": "CIDR IPv6", "label.ip6dns1": "IPv6 DNS1", "label.ip6dns2": "IPv6 DNS2", "label.ip6gateway": "Gateway IPv6", +"label.ip6firewall": "Firewall IPv6", +"label.ip6routes": "Rotas IPv6", +"label.ip6routing": "Roteamento IPv6", +"label.ipv6.subnets": "Sub-redes IPv6", "label.ipaddress": "Endere\u00e7o IP", "label.iplimit": "Limites de IP p\u00fablico", +"label.ipprefix": "Prefixo IP", +"label.ipprefixlen": "Comprimento do Prefixo IP", "label.ips": "IPs", "label.ipsecpsk": "Chave IPSec pr\u00e9 compartilhada", "label.iptotal": "Total de endere\u00e7os IPs", "label.ipv4.cidr": "CIDR IPv4", "label.ipv4.dns1": "IPv4 DNS1", "label.ipv4.dns2": "IPv4 DNS2", +"label.ipv4.subnet.set.reservation.desc": "(opcional) Por favor, especifique um dom\u00ednio ou uma Conta para ser associada a esta sub-rede IPv4", +"label.ipv4.subnets": "Sub-redes IPv4", "label.ipv6.dns1": "IPv6 DNS1", "label.ipv6.dns2": "IPv6 DNS2", "label.iqn": "Alvo IQN", +"label.is.base64.encoded": "Codificado em Base64", "label.is.in.progress": "est\u00e1 em progresso", "label.is.shared": "\u00c9 compartilhado", +"label.is2faenabled": "2FA est\u00e1 ativado", "label.isadvanced": "Mostra ajustes avan\u00e7ados", +"label.isallocated": "Alocado", "label.iscsi": "iSCSI", "label.iscustomized": "Tamanho customizado", "label.iscustomizeddiskiops": "IOPS personalizado", @@ -870,6 +1255,8 @@ "label.isdedicated": "Dedicado", "label.isdefault": "\u00c9\u0089 padr\u00e3o", "label.isdynamicallyscalable": "Dinamicamente escal\u00e1vel", +"label.isencrypted": "Criptografado", +"label.isuserdefined": "Definido pelo us\u00e1rio", "label.istagarule": "Tag como regra JS", "label.isextractable": "Extra\u00edvel", "label.isfeatured": "Em destaque", @@ -903,14 +1290,20 @@ "label.items": "itens", "label.items.selected": "item(ns) selecionados", "label.japanese.keyboard": "Teclado japon\u00eas", +"label.javadistribution": "Distribui\u00e7\u00e3o Java Runtime", +"label.javaversion": "Vers\u00e3o Java Runtime", +"label.jsonconfiguration": "Configura\u00e7\u00e3o JSON", "label.keep": "Manter", +"label.kernelversion": "Vers\u00e3o do Kernel", "label.keep.mac.address.on.public.nic": "Utilizar o mesmo endere\u00e7o MAC para a NIC p\u00fablica dos VRs", "label.key": "Chave", "label.keyboard": "Linguagem do teclado", "label.keyboardtype": "Tipo de teclado", "label.keypair": "Par de chaves SSH", +"label.keypairs": "Par(es) de chaves SSH", "label.kubeconfig.cluster": "Configura\u00e7\u00e3o do cluster Kubernetes", "label.kubernetes": "Kubernetes", +"label.kubernetes.access.details": "Os n\u00f3s Kubernetes podem ser acessados via SSH usando:
ssh -i [ssh_key] -p [port_number] cloud@[public_ip_address]

onde,
ssh_key: aponta para o arquivo de chave privada SSH correspondente \u00e0 chave que foi associada durante a cria\u00e7\u00e3o do cluster Kubernetes. Se nenhuma chave SSH foi fornecida durante a cria\u00e7\u00e3o do cluster, use a chave privada SSH do Management Server.
port_number: pode ser obtido na Aba de Encaminhamento de Porta (coluna Porta P\u00fablica)", "label.kubernetes.cluster": "Cluster Kubernetes", "label.kubernetes.cluster.create": "Criar cluster Kubernetes", "label.kubernetes.cluster.delete": "Remover cluster Kubernetes", @@ -919,6 +1312,8 @@ "label.kubernetes.cluster.stop": "Parar cluster Kubernetes", "label.kubernetes.cluster.upgrade": "Atualizar cluster Kubernetes", "label.kubernetes.dashboard": "Dashboard da UI do Kubernetes", +"label.kubernetes.dashboard.create.token": "Criar token para dashboard Kubernetes", +"label.kubernetes.dashboard.create.token.desc": "Desde o Kubernetes v1.24.0, n\u00e3o h\u00e1 gera\u00e7\u00e3o autom\u00e1tica de token de conta de servi\u00e7o baseado em segredo por motivo de seguran\u00e7a. Voc\u00ea precisa criar uma conta de servi\u00e7o e um Token Bearer opcional de longa dura\u00e7\u00e3o para a conta de servi\u00e7o", "label.kubernetes.isos": "ISOs Kubernetes", "label.kubernetes.service": "Servi\u00e7o Kubernetes", "label.kubernetes.version.add": "Adicionar vers\u00e3o do Kubernetes", @@ -926,6 +1321,7 @@ "label.kubernetes.version.update": "Gerenciar vers\u00e3o do Kubernetes", "label.kubernetesversionid": "Vers\u00e3o do Kubernetes", "label.kubernetesversionname": "Vers\u00e3o do Kubernetes", +"label.kvm": "KVM", "label.kvmnetworklabel": "Etiqueta de tr\u00e1fego KVM", "label.l2": "L2", "label.l2gatewayserviceuuid": "UUID do servi\u00e7o de gateway L2", @@ -933,11 +1329,18 @@ "label.label": "Etiqueta", "label.last.updated": "\u00daltima atualiza\u00e7\u00e3o", "label.lastannotated": "Data da \u00faltima anota\u00e7\u00e3o", +"label.lastboottime": "Hora de inicializa\u00e7\u00e3o da m\u00e1quina do Management Server", +"label.lastheartbeat": "\u00daltimo heartbeat", "label.lastname": "Sobrenome", "label.lastname.lower": "sobrenome", +"label.lastserverstart": "\u00daltima vez que o Management Server foi inicializado", +"label.lastserverstop": "Hora da \u00faltima parada para este Management Server", +"label.lastsuccessfuljob": "\u00daltimo job bem-sucedido", "label.launch": "Executar", "label.launch.vm": "Executar VM", "label.launch.vm.and.stay": "Iniciar VM e permanecer na p\u00e1gina", +"label.launch.vnf.appliance": "Lan\u00e7ar appliance VNF", +"label.launch.vnf.appliance.and.stay": "Lan\u00e7ar appliance VNF & permanecer nesta p\u00e1gina", "label.launch.zone": "Executar zona.", "label.lb.algorithm.leastconn": "Conex\u00f5es m\u00ednimas", "label.lb.algorithm.roundrobin": "Round-robin", @@ -948,6 +1351,8 @@ "label.lbdevicededicated": "Dedicado", "label.lbdeviceid": "ID", "label.lbdevicestate": "Estado", +"label.lbprovider": "Provedor de balanceador de carga", +"label.lbruleid": "ID do Balanceador de carga", "label.lbtype": "Tipo de balanceamento de carga", "label.ldap.configuration": "Configura\u00e7\u00e3o do LDAP", "label.ldap.group.name": "Grupo LDAP", @@ -956,6 +1361,8 @@ "label.limit": "Limitar", "label.limitcpuuse": "Limite da CPU", "label.limits": "Configurar limites", +"label.limits.configure": "Configurar limites", +"label.link": "Link", "label.link.domain.to.ldap": "Link dom\u00ednio para LDAP", "label.linklocalip": "Endere\u00e7o IP do Control", "label.linux": "Linux", @@ -964,6 +1371,8 @@ "label.list.nodes": "Listar nodos", "label.list.pods": "Listar pods", "label.list.services": "Listar servi\u00e7os", +"label.list.vmware.vcenter.vms": "Listar Inst\u00e2ncias VMware", +"label.livepatch": "Aplicar patch ao vivo no(s) roteador(es) da Rede", "label.load.balancer": "Balanceador de carga", "label.loadbalancerinstance": "VMs designadas", "label.loadbalancerrule": "Regra de balanceamento de carga", @@ -975,18 +1384,23 @@ "label.local.storage.enabled.system.vms": "Habilitar armazenamento local para VMs de sistema", "label.localstorageenabled": "Habilitar armazenamento local para VMs de usu\u00e1rios", "label.localstorageenabledforsystemvm": "Habilitar armazenamento local para VMs de sistema", +"label.locked": "Bloqueado", "label.login": "Entrar", +"label.loginfo": "Informa\u00e7\u00e3o do arquivo de log", "label.login.portal": "Entrar no portal", "label.login.single.signon": "Single Sign-On", "label.logout": "Sair", "label.lun": "LUN", "label.lun.number": "LUN #", +"label.lxc": "LXC", "label.lxcnetworklabel": "R\u00f3tulo de tr\u00e1fego LXC", "label.macaddress": "Endere\u00e7o MAC", "label.macaddress.example": "O endere\u00e7o MAC. Exemplo: 01:23:45:67:89:ab", "label.macaddresschanges": "Mudan\u00e7as no endere\u00e7o MAC", "label.maclearning": "MAC learning", "label.macos": "MacOS", +"label.maintenance": "Manuten\u00e7\u00e3o", +"label.majorsequence": "Sequ\u00eancia Maior", "label.make": "Tornar", "label.make.project.owner": "Criar propriet\u00e1rio de conta de projeto", "label.make.user.project.owner": "Tornar usu\u00e1rio dono do projeto", @@ -994,16 +1408,28 @@ "label.manage": "Gerenciar", "label.manage.vpn.user": "Gerenciar usu\u00e1rios da VPN", "label.managed.instances": "Inst\u00e2ncias gerenciadas", +"label.managed.volumes": "Volumes Gerenciados", +"label.managementserverid": "Management Server", +"label.managementservername": "Management Server", "label.managedstate": "Estados gerenciados", "label.management": "Gerenciamento", "label.management.ips": "Gerenciamento de endere\u00e7os IP", -"label.management.server": "Servidor de gerenciamento", +"label.management.server": "Management Server", "label.management.servers": "Servidores de ger\u00eancia", "label.managementservers": "N\u00famero de servidores de ger\u00eancia", +"label.matchall": "Corresponder a todos", +"label.max": "M\u00e1x.", +"label.max.migrations": "M\u00e1x. de migra\u00e7\u00f5es", +"label.maxbackup": "M\u00e1x. de Backups", +"label.maxbackupstorage": "M\u00e1x. de Armazenamento de Backup (GiB)", +"label.maxbackups.to.retain": "M\u00e1x. de Backups para manter", +"label.maxbucket": "M\u00e1x. de Buckets", +"label.maxmembers": "M\u00e1x de membros", +"label.maxbackups": "Backups M\u00e1x.", "label.max.primary.storage": "M\u00e1x. de armazenamento prim\u00e1rio (GiB)", "label.max.secondary.storage": "M\u00e1x. de armazenamento secund\u00e1rio (GiB)", -"label.maxcpu": "N\u00famerom\u00e1ximo de cores de CPU", -"label.maxcpunumber": "N\u00famerom\u00e1ximo de cores de CPU", +"label.maxcpu": "N\u00famero m\u00e1ximo de cores de CPU", +"label.maxcpunumber": "N\u00famero m\u00e1ximo de n\u00facleos de CPU", "label.maxdatavolumeslimit": "Limite m\u00e1ximo de volume de dados", "label.maxerrorretry": "Limite de tentativas de recupera\u00e7\u00e3o de erro", "label.maxguestslimit": "Limite m\u00e1x. de guest", @@ -1011,8 +1437,9 @@ "label.maximum": "M\u00e1ximo", "label.maxinstance": "Inst\u00e2ncias m\u00e1x", "label.maxiops": "M\u00e1x IOPS", -"label.maxmemory": "M\u00e1x. de mem\u00f3ria (MiB)", +"label.maxmemory": "M\u00e1ximo de mem\u00f3ria (MiB)", "label.maxnetwork": "M\u00e1x. de redes", +"label.maxobjectstorage": "M\u00e1x. de Object Storage (GiB)", "label.maxprimarystorage": "M\u00e1x. armazenamento prim\u00e1rio (GiB)", "label.maxproject": "Projetos m\u00e1x.", "label.maxpublicip": "M\u00e1x. IPs P\u00fablicos", @@ -1027,6 +1454,7 @@ "label.mb.memory": "MB de mem\u00f3ria", "label.memory": "Mem\u00f3ria (em MB)", "label.memory.free": "Mem\u00f3ria livre", +"label.memory.maximum.mb": "Mem\u00f3ria m\u00e1x (em MB)", "label.memory.total": "Mem\u00f3ria total", "label.memory.usage.info": "Informa\u00e7\u00f5es sobre o uso de mem\u00f3ria", "label.memory.used": "Mem\u00f3ria usada", @@ -1042,6 +1470,16 @@ "label.memused": "Uso de mem\u00f3ria", "label.menu.security.groups": "Grupos de seguran\u00e7a", "label.menu.service.offerings": "Oferta de servi\u00e7os", +"label.metadata": "Metadados", +"label.metadata.description": "Metadados do Objeto", +"label.metadata.upload.description": "Definir metadados para o objeto", +"label.migrate.instance.single.storage": "Migrar todo(s) o(s) volume(s) da Inst\u00e2ncia para um \u00fanico armazenamento prim\u00e1rio", +"label.migrate.instance.specific.storages": "Migrar volume(s) da Inst\u00e2ncia para armazenamentos prim\u00e1rios espec\u00edficos", +"label.migrate.with.storage": "Migrar com armazenamento", +"label.min_balance": "Saldo m\u00edn", +"label.minimumsemanticversion": "Vers\u00e3o sem\u00e2ntica m\u00ednima", +"label.minmembers": "M\u00edn membros", +"label.minorsequence": "Sequ\u00eancia Menor", "label.metrics": "M\u00e9tricas", "label.migrate.allowed": "Migra\u00e7\u00e3o permitida", "label.migrate.data.from.image.store": "Migrar dados para o armazenamento de imagens", @@ -1054,21 +1492,31 @@ "label.migrating.data": "Migrando dados", "label.min.balance": "Saldo m\u00ednimo", "label.min.past.hour": "minutos passados da \u00faltima hora", -"label.mincpunumber": "N\u00famero m\u00edn de n\u00facleos de CPU", +"label.mincpunumber": "N\u00famero m\u00ednimo de n\u00facleos de CPU", "label.minimum": "M\u00ed\u00adnimo", "label.miniops": "M\u00edn IOPS", "label.minmaxiops": "M\u00edn IOPS /m\u00e1x IOPS", -"label.minmemory": "M\u00edn mem\u00f3ria (em MB)", +"label.minmemory": "M\u00ednimo de mem\u00f3ria (em MB)", "label.minsize": "Tamanho m\u00ednimo", "label.minute.past.hour": "minuto(s) passado(s) da \u00faltima hora", +"label.mode": "Modo", "label.monday": "Segunda", "label.monitor": "Monitor", +"label.monitor.expected.code": "C\u00f3digo de Status HTTP Esperado", +"label.monitor.http.method": "M\u00e9todo HTTP", +"label.monitor.interval": "Intervalo de verifica\u00e7\u00e3o de sa\u00fade (seg)", +"label.monitor.retry": "Contagem de tentativas antes de marcar como inativo", +"label.monitor.timeout": "Timeout (seg)", +"label.monitor.type": "Tipo de Monitor", +"label.monitor.url": "Caminho da URL", "label.monthly": "Mensal", "label.more.access.dashboard.ui": "Mais sobre como acessar a UI da Dashboard", +"label.mount.sharedfs": "Montar Sistema de Arquivos Compartilhado via NFS", "label.move.down.row": "Mover uma c\u00e9lula para baixo", "label.move.to.bottom": "Mover para baixo", "label.move.to.top": "Mover para o topo", "label.move.up.row": "Mover uma c\u00e9lula para cima", +"label.my.isos": "Minhas ISOs", "label.my.templates": "Meus templates", "label.na": "N/D", "label.name": "Nome", @@ -1090,6 +1538,11 @@ "label.network.name": "Nome da rede", "label.network.offering": "Oferta de rede", "label.network.offerings": "Oferta de rede", +"label.network.permissions": "Permiss\u00f5es de rede", +"label.network.policy": "Pol\u00edtica de Rede", +"label.network.restart.required": "Reinicializa\u00e7\u00e3o de rede necess\u00e1ria", +"label.network.route.table": "Tabela de roteamento de rede", +"label.network.routing.policy": "Pol\u00edtica de roteamento de rede", "label.network.selection": "Sele\u00e7\u00e3o da rede", "label.network.service.providers": "Provedores de servi\u00e7os de rede", "label.network.usage.info": "Informa\u00e7\u00f5es sobre o uso de rede", @@ -1099,6 +1552,7 @@ "label.networkkbsread": "Leitura da rede", "label.networkkbswrite": "Escrita da rede", "label.networklimit": "Limites de rede", +"label.networkmode": "Modo de Rede", "label.networkname": "Nome da rede", "label.networkofferingdisplaytext": "Oferta de rede", "label.networkofferingid": "Oferta de rede", @@ -1110,26 +1564,35 @@ "label.networktype": "Tipo de rede", "label.networkwrite": "Escrita da rede", "label.new": "Novo", +"label.new.autoscale.vmgroup": "Novo Grupo de AutoScale", "label.new.instance.group": "Novo grupo de inst\u00e2ncia", "label.new.password": "Nova senha", "label.new.project": "Novo projeto", "label.new.secondaryip.description": "Insira um novo endere\u00e7o IP secund\u00e1rio", "label.new.tag": "Nova etiqueta", +"label.new.version.available": "Nova vers\u00e3o dispon\u00edvel", "label.new.vm": "Nova VM", "label.newdiskoffering": "Nova oferta", "label.newinstance": "Nova inst\u00e2ncia", "label.newname": "Novo nome", "label.next": "Pr\u00f3ximo", "label.nfs": "NFS", +"label.nfsmountopts": "Op\u00e7\u00f5es de montagem NFS", "label.nfsserver": "Servidor NFS", "label.nic": "NIC", "label.nicadaptertype": "Tipo de adaptador de rede", +"label.nicmultiqueuenumber" : "N\u00famero de multiqueues da NIC", +"label.nicmultiqueuenumber.tooltip" : "N\u00famero de multiqueues da NIC. Apenas suportado para KVM. O valor \"-1\" indica que o n\u00famero de multiqueues da NIC ser\u00e1 definido como o n\u00famero de vCPUs da Inst\u00e2ncia.", +"label.nicpackedvirtqueuesenabled" : "Virtqueues empacotadas da NIC ativadas", +"label.nicpackedvirtqueuesenabled.tooltip" : "Ativar ou n\u00e3o virtqueues empacotadas da NIC. Apenas suportado para KVM com QEMU >= 4.2.0 e Libvirt >=6.3.0.", "label.nics": "Adaptadores de rede", "label.no": "N\u00e3o", "label.no.data": "Sem dados para mostrar", "label.no.errors": "Sem erros recentes", "label.no.items": "Sem itens dispon\u00edveis", "label.no.matching.offering": "Nenhuma oferta correspondente encontrada", +"label.no.matching.network": "Nenhuma Rede correspondente encontrada", +"label.no.usage.records": "Nenhum registro de uso encontrado", "label.noderootdisksize": "Tamanho do disco ra\u00edz do nodo (em GB)", "label.nodiskcache": "Sem cache de disco", "label.none": "Nenhum", @@ -1137,14 +1600,38 @@ "label.not.found": "N\u00e3o encontrado", "label.not.suitable": "N\u00e3o apropriado", "label.notifications": "Notifica\u00e7\u00f5es", +"label.nsx": "NSX", +"label.nsx.provider": "Provedor NSX", +"label.nsx.provider.edgecluster": "Cluster de borda do provedor NSX", +"label.nsx.provider.hostname": "Hostname do provedor NSX", +"label.nsx.provider.name": "Nome do provedor NSX", +"label.nsx.provider.password": "Senha do provedor NSX", +"label.nsx.provider.port": "Porta do provedor NSX", +"label.nsx.provider.tier0gateway": "Gateway tier-0 do provedor NSX", +"label.nsx.provider.transportzone": "Zona de transporte do provedor NSX", +"label.nsx.provider.username": "Nome de usu\u00e1rio do provedor NSX", +"label.nsx.supports.internal.lb": "Ativar servi\u00e7o de LB interno NSX", +"label.nsx.supports.lb": "Ativar servi\u00e7o de LB NSX", "label.num.cpu.cores": "# de n\u00facleos da CPU", "label.number": "#Regra", "label.numretries": "N\u00famero de tentativas", "label.nvpdeviceid": "ID", +"label.oauth.configuration": "Configura\u00e7\u00e3o OAuth", +"label.oauth.verification": "Verifica\u00e7\u00e3o OAuth", +"label.object.storage" : "Armazenamento de Objetos (Object Storage)", +"label.object.presigned.url": "URL pr\u00e9-assinada", +"label.object.presigned.url.description" : "URL pr\u00e9-assinada do objeto para acess\u00e1-lo sem autentica\u00e7\u00e3o.", +"label.object.url.description" : "URL do objeto", +"label.objectlocking": "Bloqueio de Objeto", +"label.objectstoragelimit": "Limite de Armazenamento de Objetos (GiB)", +"label.objectstore" : "Armazenamento de Objetos", +"label.objectstore.search" : "Busca baseada em prefixo no diret\u00f3rio atual", +"label.objectstorageid": "Pool de Armazenamento de Objetos", "label.ocfs2": "OCFS2", "label.of": "de(a)", "label.of.month": "do m\u00eas", "label.offerha": "Oferta HA", +"label.offeringid": "ID da Oferta", "label.offeringtype": "Tipo da oferta de computa\u00e7\u00e3o", "label.ok": "OK", "label.only.end.date.and.time": "Apenas data e hor\u00e1rio final", @@ -1156,9 +1643,20 @@ "label.opendaylight.controllers": "Controladores OpenDaylight", "label.operation": "Opera\u00e7\u00e3o", "label.operation.status": "Estado da opera\u00e7\u00e3o", +"label.operator.greater": "Maior que", +"label.operator.greater.or.equal": "Maior ou igual a", +"label.operator.less": "Menor que", +"label.operator.less.or.equal": "Menor ou igual a", +"label.operator.equal": "Igual a", "label.optional": "Opcional", "label.order": "Ordenar", "label.oscategoryid": "Prefer\u00eancia de SO", +"label.oscategoryname": "Nome da categoria do SO", +"label.osdisplayname": "Nome do SO", +"label.osdistribution": "Distribui\u00e7\u00e3o do SO", +"label.osmappingcheckenabled": "Verificar nome do SO com hypervisor", +"label.osname": "Nome do SO", +"label.osnameforhypervisor": "Nome de mapeamento do hypervisor", "label.ostypeid": "Tipo de SO", "label.ostypename": "Tipo de SO", "label.other": "Outro", @@ -1172,9 +1670,11 @@ "label.override.guest.traffic": "Sobrescrever tr\u00e1fego guest", "label.override.public.traffic": "Sobrescrever tr\u00e1fego p\u00fablico", "label.override.rootdisk.size": "Sobrescrever tamanho do disco ra\u00edz", +"label.override.root.diskoffering": "Substituir oferta de disco root", "label.overrideguesttraffic": "Sobrescrever tr\u00e1fego guest", "label.overridepublictraffic": "Sobrescrever tr\u00e1fego p\u00fablico", "label.ovf.properties": "Propriedades vApp", +"label.ovm3": "OVM3", "label.ovm3cluster": "Clustering nativo", "label.ovm3networklabel": "Label de trafego OVM3", "label.ovm3pool": "Pooling nativo", @@ -1188,27 +1688,36 @@ "label.page": "p\u00e1gina", "label.palo.alto.firewall": "Firewall Palo Alto", "label.palp": "Perfil de log Palo Alto", +"label.param.name": "Nome do par\u00e2metro", +"label.param.value": "Valor do par\u00e2metro", "label.params": "Par\u00e2metros", "label.parentdomainname": "Dom\u00ednio pai", "label.parentname": "Pai", +"label.parentsubnet": "Sub-rede Pai", "label.passive": "Passivo", "label.password": "Senha", +"label.password.default": "Senha Padr\u00e3o", "label.password.reset.confirm": "A senha foi recuperada para", +"label.password.tooltip": "A senha para o Host", "label.passwordenabled": "Habilitar troca de senha", "label.path": "Caminho (Path)", "label.patp": "Perfil de amea\u00e7a Palo Alto", "label.pavr": "Roteador virtual", "label.payload": "Payload", +"label.payloadurl": "URL de Payload", "label.pcidevice": "GPU", +"label.pending.jobs": "Jobs pendentes", "label.per.account": "Por conta", "label.per.zone": "Por zona", "label.percentage": "Porcentagem", +"label.performfreshchecks": "Executar novas verifica\u00e7\u00f5es", "label.perfectforwardsecrecy": "Perfect Forward secrecy", "label.perform.fresh.checks": "Realizar novas verifica\u00e7\u00f5es", "label.permission": "Permiss\u00e3o", "label.permissions": "permiss\u00e3o", "label.physical.network": "Rede f\u00edsica", "label.physicalnetworkid": "Rede f\u00edsica", +"label.physicalnetworkname": "Nome da Rede F\u00edsica", "label.physicalsize": "Tamanho f\u00edsico", "label.ping.path": "Caminho do ping", "label.pkcs.private.certificate": "Certificado privado PKCS#8", @@ -1222,6 +1731,8 @@ "label.podid": "Pod", "label.podname": "Nome do pod", "label.pods": "Pods", +"label.policy": "Pol\u00edtica", +"label.policyuuid": "Pol\u00edtica de Rede", "label.port": "Porta", "label.port.range": "Intervalo de porta", "label.portforwarding": "Encaminhamento de porta", @@ -1232,20 +1743,30 @@ "label.powerflex.storage.pool": "Pool de armazenamento", "label.powerstate": "Estado de energia", "label.preferred": "Preferido", +"label.prefix": "Prefixo", +"label.prefix.type": "Tipo de Prefixo", +"label.prepare.for.shutdown": "Preparar para Desligamento", +"label.prepareformaintenance": "Preparar para Manuten\u00e7\u00e3o", "label.presetup": "PreSetup", "label.prev": "Anterior", "label.previous": "Anterior", +"label.primera.url.tooltip": "URL designando o endpoint do array de armazenamento Primera, formatado como: http[s]://HOSTNAME:PORT?cpg=NAME&hostset=NAME[&skipTlsValidation=true][&snapCPG=NAME][&taskWaitTimeoutMs=#][&keyttl=#][&connectTimeoutMs=#] onde valores em [] s\u00e3o opcionais.", +"label.primera.username.tooltip": "O nome de usu\u00e1rio com privil\u00e9gios de edi\u00e7\u00e3o", +"label.flashArray.username.tooltip": "O nome de us\u00e1rio com privil\u00e9gios de edi\u00e7\u00e3o", +"label.flashArray.url.tooltip": "URL designando o endpoint Flash Array, formatado como: http[s]://HOSTNAME:PORT?pod=NAME&hostgroup=NAME[&skipTlsValidation=true][&postCopyWaitMs=#][&keyttl=#][&connectTimeoutMs=#][&apiLoginVersion=#][&apiVersion=#] onde valores em [] s\u00e3o opcionais.", "label.primary": "Prim\u00e1rio", "label.primary.storage": "Armazenamento prim\u00e1rio", "label.primary.storage.allocated": "Armazenamento prim\u00e1rio alocado", "label.primary.storage.used": "Uso do armazenamento prim\u00e1rio", "label.primarystoragelimit": "Limites do armazenamento prim\u00e1rio (GiB)", +"label.primarystoragetotal": "Armazenamento prim\u00e1rio", "label.private.gateway": "Gateway privado", "label.private.interface": "Interface privada", "label.private.registry": "Registro privado", "label.privateinterface": "Interface privada", "label.privateip": "Endere\u00e7o IP privado", "label.privatekey": "Chave privada PKCS#8", +"label.privatemtu": "MTU da Interface Privada", "label.privatenetwork": "Rede privada", "label.privateport": "Porta privada", "label.profilename": "Perfil", @@ -1280,15 +1801,22 @@ "label.public.ips": "IPs P\u00fablicos", "label.public.lb": "LB p\u00fablico", "label.public.traffic": "Tr\u00e1fego p\u00fablico", +"label.public.traffic.nsx": "Tr\u00e1fego P\u00fablico NSX", +"label.publicipid": "ID do endere\u00e7o IP", +"label.publicmtu": "MTU da Interface P\u00fablica", "label.publicinterface": "Interface p\u00fablica", "label.publicip": "Endere\u00e7o IP", "label.publickey": "Chave p\u00fablica", "label.publicnetwork": "Rede p\u00fablica", "label.publicport": "Porta p\u00fablica", +"label.purge.usage.records.error": "Falha ao eliminar registros de uso", +"label.purge.usage.records.success": "Registros de uso eliminados com sucesso", +"label.purgeresources": "Limpar Recursos", "label.purpose": "Prop\u00f3sito", "label.qostype": "Tipo de QoS", "label.quickview": "Visualiza\u00e7\u00e3o r\u00e1pida", "label.quiescevm": "Quiesce VM", +"label.quiettime": "Tempo em espera (em seg)", "label.quota": "Cota", "label.quota.add.credits": "Adicionar cr\u00e9ditos", "label.quota.configuration": "Configura\u00e7\u00e3o da cota", @@ -1301,6 +1829,7 @@ "label.quota.statement.tariff": "Tarifa", "label.quota.summary": "Relat\u00f3rios", "label.quotastate": "Estado da cota", +"label.quota_enforce": "Impor Cota", "label.summary": "Sum\u00e1rio", "label.quota.tariff": "Tarifa", "label.quota.tariff.activationrule": "Regra de ativa\u00e7\u00e3o", @@ -1310,15 +1839,24 @@ "label.quota.total": "Total", "label.quota.type.name": "Tipo de uso", "label.quota.type.unit": "Unidade do uso", +"label.action.update.object.storage" : "Atualizar Object Storage", "label.quota.usage": "Consumo da cota", "label.quota.validate.activation.rule": "Validar regra de ativa\u00e7\u00e3o", "label.quota.value": "Valor", "label.rados.monitor": "Monitor RADOS", +"label.rados.monitor.description": "O(s) monitor(es) RADOS. Se houver v\u00e1rios monitores, eles s\u00e3o separados por v\u00edrgula. Por exemplo, \"192.168.0.1,192.168.0.2,192.168.0.3\", \"mon1, mon2, mon3\". Endere\u00e7os IPv6 devem incluir colchetes, por exemplo, \"[fc00:1234::1],[fc00:1234::2],[fc00:1234::3]\".", "label.rados.pool": "Pool do RADOS", "label.rados.secret": "Segredo RADOS", "label.rados.user": "Usu\u00e1rio RADOS", "label.ram": "RAM", +"label.range.last.1month": "\u00daltimo 1 m\u00eas", +"label.range.last.1week": "\u00daltima 1 semana", +"label.range.last.2week": "\u00daltimas 2 semanas", +"label.range.last.3month": "\u00daltimos 3 meses", +"label.range.today": "Hoje", +"label.range.yesterday": "Ontem", "label.raw.data": "Dados Brutos", +"label.rawusage": "Uso bruto (em horas)", "label.rbd": "RDB", "label.rbdid": "Usu\u00e1rio Ceph", "label.rbdmonitor": "Monitor Ceph", @@ -1330,8 +1868,14 @@ "label.readonly": "Apenas leitura", "label.reason": "Motivo", "label.reboot": "Reiniciar", +"label.recent.deliveries": "Entregas recentes", +"label.recover.sharedfs": "Recuperar Sistema de Arquivos Compartilhado", +"label.recovering": "Recuperando", "label.receivedbytes": "Bytes recebidos", "label.recover.vm": "Recuperar VM", +"label.recursivedomains": "Dom\u00ednios recursivos", +"label.redeliver": "Reentregar", +"label.redirecturi": "URI de Redirecionamento", "label.redirect": "Clique para acessar:", "label.redundantrouter": "Roteador Redundante", "label.redundantrouterstate": "Estado redundante", @@ -1339,40 +1883,61 @@ "label.redundantvpcrouter": "VPC redundante", "label.refresh": "Atualizar", "label.region": "Regi\u00e3o", +"label.register.api.key": "Registrar chave de API", +"label.register.oauth": "Registrar OAuth", +"label.register.user.data": "Registrar dados de usu\u00e1rio", "label.register.template": "Registrar template", "label.reinstall.vm": "Reinstalar VM", "label.reject": "Rejeitar", "label.related": "Relacionado", +"label.relationaloperator": "Operador", "label.release": "Liberar", "label.release.account": "Liberar conta", +"label.release.dedicated.bgp.peer": "Liberar par BGP dedicado", "label.release.dedicated.cluster": "Liberar cluster dedicado", "label.release.dedicated.host": "Liberar host dedicado", +"label.release.dedicated.ipv4.subnet": "Liberar sub-rede IPv4 dedicada", "label.release.dedicated.pod": "LIberar pod dedicado", "label.release.dedicated.zone": "Liberar zona dedicada", "label.releasing.ip": "Liberando IP", +"label.remote.instances": "Inst\u00e2ncias Remotas", "label.remove": "Remover", "label.remove.annotation": "Remover coment\u00e1rio", +"label.remove.bgp.peer": "Remover par BGP", "label.remove.egress.rule": "Remover regra de sa\u00edda", +"label.remove.gui.theme": "Remover tema da GUI", +"label.remove.interface.route.table": "Remover tabela de rotas de interface Tungsten", "label.remove.ip.range": "Remover intervalo de IPs", +"label.remove.ipv4.subnet": "Remover sub-rede IPv4", "label.remove.ldap": "Remover LDAP", +"label.remove.logical.network": "Remover Rede do roteador l\u00f3gico", +"label.remove.logical.router": "Remover roteador l\u00f3gico", "label.remove.network.offering": "Remover oferta de rede", +"label.remove.network.route.table": "Remover tabela de roteamento de Rede Tungsten Fabric", "label.remove.pf": "Remover regra de redirecionamento de porta", +"label.remove.policy": "Remover pol\u00edtica", "label.remove.project.account": "Remover conta de projeto", "label.remove.project.role": "Remover fun\u00e7\u00e3o do projeo", "label.remove.project.user": "Remover usu\u00e1rio do projeto", +"label.remove.routing.policy": "Remover pol\u00edtica de roteamento Tungsten-Fabric", +"label.remove.tungsten.tag": "Remover Tag", +"label.remove.user.data": "Remover Userdata", +"label.removed": "Removido", +"label.removedaccounts": "Contas removidas", "label.remove.rule": "Remover regra", "label.remove.ssh.key.pair": "Remover par de chaves SSH", "label.remove.vm.from.lb": "Remover VM da regra de balanceamento de carga", "label.remove.vmware.datacenter": "Remover datacenter VMware", "label.remove.vpc": "Remover VPC", "label.remove.vpc.offering": "Remover oferta VPC", -"label.removed": "Removido", "label.removing": "Removendo", "label.replace.acl": "Substituir ACL", "label.report.bug": "Reportar um problema", +"label.request": "Solicita\u00e7\u00e3o", "label.required": "Obrigat\u00f3rio", "label.requireshvm": "HVM", "label.requiresupgrade": "Requer atualiza\u00e7\u00e3o", +"label.reserved": "Reservado", "label.reserved.system.gateway": "Gateway de sistema reservado", "label.reserved.system.ip": "IP de sistema reservado", "label.reserved.system.netmask": "M\u00e1scara de rede reservada do sistema", @@ -1382,8 +1947,11 @@ "label.reservedsystemnetmask": "M\u00e1scara de rede reservada do sistema", "label.reservedsystemstartip": "In\u00edcio dos IPs reservados para o sistema", "label.reset": "Reiniciar", +"label.reset.config.value": "Redefinir para valor padr\u00e3o", "label.reset.ssh.key.pair": "Recriar par de chaves SSH", "label.reset.to.default": "Reinicializar para o padr\u00e3o", +"label.reset.userdata.on.autoscale.vm.group": "Redefinir Userdata no Grupo de VMs de AutoScale", +"label.reset.userdata.on.vm": "Redefinir Userdata na Inst\u00e2ncia", "label.reset.vpn.connection": "Reiniciar a conex\u00e3o VPN", "label.resource": "Recurso", "label.resource.limit.exceeded": "Limite de recurso excedido", @@ -1392,6 +1960,8 @@ "label.resourcename": "Nome do recurso", "label.resources": "Recursos", "label.resourcestate": "Estado do recurso", +"label.resourcetype": "Tipo de recurso", +"label.response": "Resposta", "label.restart.network": "Reiniciar rede", "label.restart.vpc": "Reiniciar a VPC", "label.restartrequired": "Reiniciar obrigat\u00f3rio", @@ -1399,12 +1969,18 @@ "label.restore.volume.attach": "Restaurar volume e anex\u00e1-lo", "label.review": "Revisar", "label.role": "Fun\u00e7\u00e3o", +"label.roleid": "Fun\u00e7\u00e3o", "label.rolename": "Fun\u00e7\u00e3o", "label.roles": "Fun\u00e7\u00f5es", "label.roletype": "Tipo de fun\u00e7\u00e3o", "label.rolepermissiontab.searchbar": "Pesquisa de regras", "label.root.certificate": "Certificado ra\u00edz", "label.root.disk.size": "Tamanho do disco ra\u00edz (GB)", +"label.rootdiskcontrollertypekvm": "Controladora do disco ROOT", +"label.rootdisksize": "Tamanho do disco ROOT (GB)", +"label.routenexthop": "Pr\u00f3ximo salto da rota", +"label.routenexthoptype": "Tipo do pr\u00f3ximo salto da rota", +"label.routeprefix": "Prefixo da rota", "label.rootdisk": "Disco ra\u00edz", "label.rootdiskcontrollertype": "Controlador do disco ra\u00edz", "label.router.health.check.last.updated": "\u00daltima atualiza\u00e7\u00e3o", @@ -1414,6 +1990,12 @@ "label.routercount": "Total de roteadores virtuais", "label.routerip": "Endere\u00e7os IPv4 para o roteador dentro da rede compartilhada", "label.routeripv6": "Endere\u00e7os IPv6 para o roteador dentro da rede compartilhada", +"label.router.source.nat.ip": "IP source NAT do roteador", +"label.routing.firewall": "Firewall de Roteamento IPv4", +"label.routing.policy": "Pol\u00edtica de roteamento", +"label.routing.policy.terms": "Termos da pol\u00edtica de roteamento", +"label.routing.policy.terms.then": "Termos da pol\u00edtica de roteamento ent\u00e3o", +"label.routingmode": "Modo de roteamento", "label.resourcegroup": "Grupo de recurso", "label.rule": "Regra", "label.rule.number": "Regra n\u00famero", @@ -1423,6 +2005,7 @@ "label.rules.file.to.import": "Arquivo CSV com as defini\u00e7\u00f5es de regras para importar", "label.run.proxy.locally": "Rodar o proxy localmente", "label.running": "VMs rodando", +"label.running.vms": "Inst\u00e2ncias em Execu\u00e7\u00e3o", "label.s2scustomergatewayid": "ID do gateway do cliente site a site", "label.s2svpngatewayid": "ID do gateway da VPN site a site", "label.s3.access.key": "Chave de acesso", @@ -1443,9 +2026,17 @@ "label.save": "Salvar", "label.save.new.rule": "Salvar nova regra", "label.scale.vm": "Escalar VM", +"label.scaledown.policies": "Pol\u00edticas de ScaleDown", +"label.scaledown.policy": "Pol\u00edtica de ScaleDown", +"label.scaleup.policies": "Pol\u00edticas de ScaleUp", +"label.scaleup.policy": "Pol\u00edtica de ScaleUp", +"label.scaling": "Escalonamento", +"label.schedule.add": "Adicionar agendamento", +"label.schedules": "Agendamentos", "label.schedule": "Programar", "label.scheduled.backups": "Backups programados", "label.scope": "Escopo", +"label.scope.tooltip": "Escopo do Pool de Armazenamento Prim\u00e1rio", "label.search": "Pesquisar", "label.secondary.isolated.vlan.type.isolated": "Isolada", "label.secondary.isolated.vlan.type.promiscuous": "Prom\u00edscuos", @@ -1454,11 +2045,13 @@ "label.secondaryips": "IPs secund\u00e1rios", "label.secondarystoragelimit": "Limites do armazenamento secund\u00e1rio (GiB)", "label.secretkey": "Chave secreta", +"label.secret.key": "Chave secreta", "label.secured": "Protegido", "label.security.groups": "Grupos de seguran\u00e7a", "label.securitygroup": "Grupo de seguran\u00e7a", "label.securitygroupenabled": "Grupo de seguran\u00e7a ativado", "label.securitygroups": "Grupos de seguran\u00e7a", +"label.securitygroupsenabled": "Grupos de seguran\u00e7a ativados", "label.see.more.info.cpu.usage": "Ver mais informa\u00e7\u00f5es sobre o uso de CPU", "label.see.more.info.memory.usage": "Ver mais informa\u00e7\u00f5es sobre o uso de mem\u00f3ria", "label.see.more.info.network.usage": "Ver mais informa\u00e7\u00f5es sobre o uso de rede", @@ -1466,11 +2059,17 @@ "label.see.more.info.shown.charts": "Ver mais informa\u00E7\u00F5es sobre os gr\u00E1ficos mostrados", "label.select": "Selecionar", "label.select-view": "Selecionar visualiza\u00e7\u00e3o", +"label.select.2fa.provider": "Selecione o provedor", "label.select.a.zone": "Selecione uma zona", "label.select.deployment.infrastructure": "Selecione uma infraestrutura de implanta\u00e7\u00e3o", +"label.select.guest.os.type": "Por favor, selecione o tipo de SO convidado", +"label.select.network": "Selecionar Rede", "label.select.period": "Selecionar per\u00edodo", "label.select.project": "Selecionar projeto", "label.select.projects": "Selecionar projetos", +"label.select.ps": "Selecionar armazenamento prim\u00e1rio", +"label.select.root.disk": "Selecione o disco ROOT", +"label.select.source.vcenter.datacenter": "Selecione o Datacenter VMware vCenter de origem", "label.select.tier": "Selecionar camada", "label.select.zones": "Selecionar zonas", "label.selected.storage": "Armazenamento selecionado", @@ -1479,10 +2078,12 @@ "label.semanticversion": "Vers\u00e3o sem\u00e2ntica", "label.sent": "Enviado", "label.sentbytes": "Bytes enviados", +"label.sequence": "Sequ\u00eancia", "label.server": "Servidor", "label.server.certificate": "Certificados do servidor", "label.service.connectivity.distributedroutercapabilitycheckbox": "Roteador distribu\u00eddo", "label.service.connectivity.regionlevelvpccapabilitycheckbox": "VPC a n\u00edvel de regi\u00e3o", +"label.service.group": "Grupo de servi\u00e7os", "label.service.lb.elasticlbcheckbox": "LB el\u00e1stico", "label.service.lb.inlinemodedropdown": "Modo", "label.service.lb.lbisolationdropdown": "Isolamento de LB", @@ -1491,23 +2092,31 @@ "label.service.offering": "Plano", "label.service.staticnat.associatepublicip": "Associa IP p\u00fablico", "label.service.staticnat.elasticipcheckbox": "IP el\u00e1stico", +"label.servicegroupuuid": "Grupo de Servi\u00e7os", +"label.serviceip": "IP de Servi\u00e7o", "label.servicelist": "Servi\u00e7os", "label.serviceofferingid": "Oferta de computa\u00e7\u00e3o", "label.serviceofferingname": "Oferta de computa\u00e7\u00e3o", +"label.sessions": "Sess\u00f5es de cliente ativas", "label.set.default.nic": "Configurar para NIC padr\u00e3o", "label.set.reservation": "Fazer reserva", +"label.set.reservation.account.desc": "Por favor, especifique uma Conta para ser associada a este intervalo de IP", "label.set.reservation.desc": "(opcional) especificar uma conta a ser associada a esta faixa de IP.

VMs de sistema: habilitar a dedica\u00e7\u00e3o da faixa IP p\u00fablica para SSVM e CPVM, campo de conta desativado. Rigor das reservas definido em 'system.vm.public.ip.reservation.mode.strictness'", +"label.set.reservation.systemvm.desc": "Ativar dedica\u00e7\u00e3o de intervalo de IP p\u00fablico para SSVM e CPVM. Rigor da reserva definido em 'system.vm.public.ip.reservation.mode.strictness'.", "label.setting": "Configura\u00e7\u00e3o", "label.settings": "Configura\u00e7\u00f5es", "label.setup": "Configura\u00e7\u00e3o", -"label.shared": "Compartilhado", -"label.sharedexecutable": "Compartilhado", +"label.shared": "Compartilhado(a)", +"label.shared.filesystems": "Sistemas de Arquivos Compartilhados", +"label.sharedexecutable": "Execut\u00e1veis compartilhados ", "label.sharedmountpoint": "SharedMountPoint", "label.sharedrouterip": "Endere\u00e7os IPv4 para o roteador dentro da rede compartilhada", "label.sharedrouteripv6": "Endere\u00e7os IPv6 para o roteador dentro da rede compartilhada", "label.sharewith": "Compartilhar com", +"label.show.usage.records": "Mostrar registros de uso", "label.showing": "Exibindo", "label.shrinkok": "Encolhimento OK", +"label.shutdown": "Desligar", "label.shutdown.provider": "Desabilitar provider", "label.simplified.chinese.keyboard": "Teclado chin\u00eas simplificado", "label.site.to.site.vpn": "VPN site a site", @@ -1525,19 +2134,35 @@ "label.snapshotlimit": "Limite de snapshots", "label.snapshotmemory": "Snapshot da mem\u00f3ria", "label.snapshots": "Snapshots", +"label.snapshottype": "Tipo de Snapshot", +"label.softwareversion": "Vers\u00e3o do software", "label.sockettimeout": "Tempo limite no socket", +"label.source": "Selecionar Hypervisor de Origem de Importa\u00e7\u00e3o-Exporta\u00e7\u00e3o", "label.source.based": "SourceBased", "label.sourcecidr": "CIDR de origem", "label.sourcecidrlist": "Lista de CIDRs de origem", +"label.sourcehost": "Host de origem", "label.sourceipaddress": "Endere\u00e7o IP de origem", +"label.sourceipaddressnetworkid": "ID da Rede do endere\u00e7o IP de origem", "label.sourcenat": "Source NAT", +"label.sourcenatipaddress": "Endere\u00e7o IP de NAT de origem", "label.sourcenatsupported": "Suporte \u00e0 source NAT", "label.sourcenattype": "Tipo de source NAT suportado", "label.sourceport": "Porta de origem", +"label.sourcetype": "Tipo de origem", +"label.specifyasnumber": "Especificar N\u00famero AS", "label.specifyipranges": "Especifique range de IP", "label.specifyvlan": "Especificar VLAN", "label.splitconnections": "Separar conex\u00f5es", "label.sr.name": "Nome da etiqueta SR", +"label.srcaddressgroupuuid": "Grupo de Endere\u00e7os de Origem", +"label.srcendport": "Porta Final de Origem", +"label.srcipprefix": "Endere\u00e7o de Rede de Origem", +"label.srcipprefixlen": "Comprimento do Prefixo de Origem", +"label.srcnetwork": "Rede de Origem", +"label.srcnetworkuuid": "Rede", +"label.srcstartport": "Porta Inicial de Origem", +"label.srctaguuid": "Tag de Origem", "label.srx": "SRX", "label.srx.firewall": "Juniper SRX firewall", "label.ssh.key.pairs": "Par de chaves SSH", @@ -1546,6 +2171,7 @@ "label.sshkeypairs": "Par de chaves SSH", "label.standard.us.keyboard": "Teclado padr\u00e3o (EUA)", "label.sslcertificates": "Certificados SSL", +"label.sslverification": "Verifica\u00e7\u00e3o SSL", "label.start": "Iniciar", "label.start.date": "Data de in\u00edcio", "label.start.date.and.time": "Data e hor\u00e1rio inicial", @@ -1554,14 +2180,18 @@ "label.start.reserved.system.ip": "In\u00edcio dos IPs reservados para o sistema", "label.start.rolling.maintenance": "Iniciar a manunten\u00e7\u00e3o", "label.start.vm": "Iniciar VM", +"label.startasn": "Iniciar N\u00famero AS", "label.startdate": "Por data (in\u00edcio)", +"label.starting": "Iniciando", "label.startip": "IP do in\u00edcio", "label.startipv4": "IP inicial IPv4", "label.startipv6": "IP inicial IPv6", "label.startport": "Porta de in\u00edcio", "label.startquota": "Valor", "label.state": "Estado", +"label.static": "Est\u00e1tico", "label.static.routes": "Rotas est\u00e1ticas", +"label.staticnat": "NAT Est\u00e1tico", "label.statistics": "Estat\u00edsticas", "label.status": "Estado", "label.step.1": "Passo 1", @@ -1584,6 +2214,8 @@ "label.sticky.tablesize": "Tamanho da tabela", "label.stop": "Parar", "label.stopped": "VMs paradas", +"label.stopped.vms": "Inst\u00e2ncias Paradas", +"label.stopping": "Parando", "label.storage": "Armazenamento", "label.storage.migration.required": "Migra\u00e7\u00e3o de armazenamento necess\u00e1ria", "label.storage.tags": "Tags de armazenamento", @@ -1592,34 +2224,56 @@ "label.storagemotionenabled": "Motion do armazenamento habilitado", "label.storagepolicy": "Pol\u00edtica de armazenamento", "label.storagepool": "Pool de armazenamento", +"label.storagepool.tooltip": "Pool de Armazenamento de Destino. O volume deve estar localizado neste Pool de Armazenamento", "label.storagetags": "Tags de armazenamento", "label.storagetype": "Tipo de armazenamento", "label.storageip": "Endere\u00e7o IP na rede de armazenamento", "label.strict": "Rigoroso", "label.subdomainaccess": "acesso ao subdom\u00ednio", "label.submit": "Enviar", +"label.subnet": "Sub-rede", "label.succeeded": "Sucedido", "label.success": "Sucesso", +"label.success.migrations": "Migra\u00e7\u00f5es bem-sucedidas", "label.success.set": "Definido com sucesso", "label.success.updated": "Atualizado com sucesso", "label.suitability": "Adequabilidade", "label.suitable": "Adequado", "label.sunday": "Domingo", +"label.supported": "Suportado", "label.supportedservices": "Servi\u00e7os suportados", "label.supportsautoscaling": "Suporte \u00e0 auto-escala", "label.supportsha": "Suporte \u00e0 HA", "label.supportspublicaccess": "Suporte \u00e0 acesso p\u00fablico", "label.supportsstrechedl2subnet": "Suporte \u00e0 Streched L2 Subnet", +"label.supportsvmautoscaling": "Suporta escalonamento autom\u00e1tico", "label.suspend.project": "Suspender projeto", "label.switch.type": "Tipo de switch", "label.sync.storage": "Sincronizar pool do armazenamento", +"label.system.ip.pool": "Pool do Sistema", "label.system.offering": "Ofertas de sistema", "label.system.offerings": "Ofertas de sistema", "label.system.service.offering": "Ofertas de servi\u00e7o de sistema", "label.system.vm": "VM de sistema", "label.system.vms": "VMs de sistema", +"label.systemcycleusage": "Ciclos de usu\u00e1rio, sistema e ociosidade", +"label.systemloadaverages": "M\u00e9dias de carga de 1, 5 e 15 minutos", +"label.systemmemoryfree": "Mem\u00f3ria livre do sistema", +"label.systemmemorytotal": "Mem\u00f3ria total do sistema", +"label.systemmemoryused": "Mem\u00f3ria do sistema usada", +"label.systemmemoryvirtualsize": "Tamanho virtual total do processo", +"label.systemtotalcpucycles": "Capacidade total da CPU para todos os n\u00facleos em MHz", +"label.systemvm": "VM de Sistema", "label.systemvmtype": "Tipo de VM de sistema", +"label.tag": "Tag", "label.tag.key": "Chave", +"label.tag.nsx": "nsx", +"label.tag.systemvm": "systemvm", +"label.tag.type": "Tipo de Tag", +"label.tagged.limits": "Limites marcados", +"label.tagtypeuuid": "Tipo de Tag", +"label.taguuid": "Tag", +"label.taken": "Ocupado", "label.tag.value": "Valor", "label.tagged": "Etiquetado", "label.tags": "Etiquetas", @@ -1639,7 +2293,13 @@ "label.templatename": "Template", "label.templates": "Templates", "label.templatesubject": "Assunto", +"label.templatetag": "Tag", "label.templatetype": "Tipo de template", +"label.templateremoved": "Este template foi removido", +"label.templateversion": "Vers\u00e3o do template", +"label.term.type": "Tipo de termo", +"label.test": "Teste", +"label.test.webhook.delivery": "Testar Entrega de Webhook", "label.tftpdir": "Diret\u00f3rio raiz do TFTP", "label.theme.alert": "O painel de configura\u00e7\u00e3o \u00e9 apenas vis\u00edvel no ambiente de desenvolvimento, por favor, salve as configura\u00e7\u00f5es para que as mudan\u00e7as tenham efeito.", "label.theme.color": "Cor do tema", @@ -1649,12 +2309,23 @@ "label.theme.page.style.setting": "Defini\u00e7\u00e3o do estilo da p\u00e1gina", "label.theme.project": "Estilo do projeto", "label.theme.project.navigation.setting": "Configura\u00e7\u00e3o de navega\u00e7\u00e3o do projeto", +"label.threadsblockedcount": "Threads Bloqueadas", +"label.threadsdeamoncount": "Threads Daemon", +"label.threadsnewcount": "Novas Threads", +"label.threadsrunnablecount": "Threads Execut\u00e1veis", +"label.threadsterminatedcount": "Threads Terminadas", +"label.threadstotalcount": "Contagem total de Threads", +"label.threadswaitingcount": "Threads Aguardando", +"label.threshold.description": "Valor pelo qual o Contador ser\u00e1 avaliado com o Operador selecionado", +"label.themes": "Temas", "label.threshold": "Limiar", "label.thursday": "Quinta", "label.time": "Tempo", "label.timeout": "Timeout", "label.timeout.in.second ": " Timeout (segundos)", "label.timezone": "Fuso hor\u00e1rio", +"label.tmppath": "Caminho Temp", +"label.tmppath.tooltip": "Caminho tempor\u00e1rio para armazenar imagens de disco no Host Externo antes de copiar para o pool de armazenamento de destino. O padr\u00e3o \u00e9 /tmp", "label.to": "para", "label.token": "Token", "label.token.for.dashboard.login": "Token para o login na Dashboard pode ser obtido atrav\u00e9s do seguinte comando", @@ -1668,34 +2339,82 @@ "label.traffic.types": "Tipos de tr\u00e1fego", "label.traffictype": "Tipo de tr\u00e1fego", "label.transportzoneuuid": "UUID da zona de transporte", +"label.trigger.shutdown": "Acionar Desligamento Seguro", "label.try.again": "Tente novamente", "label.tuesday": "Ter\u00e7a", +"label.2FA": "2FA", +"label.two.factor.authentication": "Autentica\u00e7\u00e3o de Dois Fatores", +"label.two.factor.authentication.secret.key": "Sua chave secreta de autentica\u00e7\u00e3o de dois fatores", +"label.two.factor.authentication.static.pin": "Seu PIN est\u00e1tico de autentica\u00e7\u00e3o de dois fatores", +"label.tungsten.fabric": "Tungsten Fabric", +"label.tungsten.fabric.provider": "Provedor Tungsten Fabric", +"label.tungsten.fabric.routing": "Roteamento Tungsten Fabric", +"label.tungsten.interface.router.table": "Tabela de rotas da interface", +"label.tungsten.logical.router": "Roteador L\u00f3gico", +"label.tungsten.network.router.table": "Tabela de rotas da rede", +"label.tungsten.provider": "Provedor Tungsten", +"label.tungsten.provider.gateway": "Gateway do provedor Tungsten", +"label.tungsten.provider.hostname": "Hostname do provedor Tungsten", +"label.tungsten.provider.introspectport": "Porta de introspec\u00e7\u00e3o do provedor Tungsten", +"label.tungsten.provider.name": "Nome do provedor Tungsten", +"label.tungsten.provider.port": "Porta do provedor Tungsten", +"label.tungsten.provider.vrouterport": "Porta do vrouter do provedor Tungsten", +"label.tungsten.router.table": "Tabela de Roteador", +"label.tungsten.routing.polices": "Pol\u00edticas de roteamento", +"label.tungsten.static.routes": "Rotas Est\u00e1ticas", +"label.tungstengateway": "Gateway", +"label.tungsteninterfaceroutetablename": "Nome", +"label.tungstennetworkroutetablename": "Nome", +"label.tungstenproviderhostname": "Hostname do provedor", +"label.tungstenproviderintrospectport": "Porta de introspec\u00e7\u00e3o do provedor", +"label.tungstenproviderport": "Porta do provedor", +"label.tungstenprovideruuid": "UUID do Provedor", +"label.tungstenprovidervrouterport": "Porta do vrouter do provedor", +"label.tungstenroutingpolicyterm": "Rede", +"label.tungstenvms": "Inst\u00e2ncias", "label.type": "Tipo", "label.type.id": "Tipo do ID", "label.ucs": "UCS", "label.udp": "UDP", +"label.udp6": "UDPv6", +"label.uefi.supported": "UEFI suportado", "label.uk.keyboard": "Teclado do Reino Unido", "label.unauthorized": "N\u00e3o autorizado", "label.unavailable": "Indispon\u00edvel", +"label.undefined": "Indefinido", "label.unit": "Unidade", "label.unknown": "Desconhecido", "label.unlimited": "Ilimitado", +"label.unmanaged": "N\u00e3o gerenciado", "label.unmanage.instance": "N\u00e3o gerenciar inst\u00e2ncia", +"label.unmanage.volume": "Parar de gerenciar Volume", "label.unmanaged.instance": "Inst\u00e2ncia n\u00e3o gerenciada", "label.unmanaged.instances": "Inst\u00e2ncias n\u00e3o gerenciadas", +"label.unmanaged.volumes": "Volumes n\u00e3o gerenciados", "label.untagged": "N\u00e3omarcado", +"label.up": "Cima", +"label.update.autoscale.vmgroup": "Atualizar Grupo de AutoScale", +"label.update.bgp.peer": "Atualizar par BGP", +"label.update.condition": "Atualizar condi\u00e7\u00e3o", +"label.update.ipv4.subnet": "Atualizar sub-rede IPv4", "label.update.instance.group": "Grupo de inst\u00e2ncias de atualiza\u00e7\u00e3o", "label.update.ip.range": "Atualizar intervalo IP", "label.update.network": "Atualizar rede", "label.update.physical.network": "Atualizar rede f\u00edsica", "label.update.project.role": "Atualizar fun\u00e7\u00e3o do projeto", +"label.update.sharedfs": "Atualizar Sistema de Arquivos Compartilhado", "label.update.ssl": " atualizar certificado SSL", "label.update.to": "atualizado para", "label.update.traffic.label": "Atualizar etiquetas de tr\u00e1fego", "label.update.vmware.datacenter": "Atualizar VMware datacenter", +"label.update.webhook": "Atualizar Webhook", +"label.updateinsequence": "Atualizar em sequ\u00eancia", "label.updating": "Atualizando", "label.upgrade.router.newer.template": "Atualize roteador para usar template mais novo", +"label.upgrading": "Atualizando", "label.upload": "Enviar", +"label.upload.description": "Caminho de onde objetos serão enviados", +"label.upload.path": "Caminho de envio", "label.upload.icon": "Carregar \u00cdcone", "label.upload.iso.from.local": "Carregar ISO local", "label.upload.resource.icon": "Carregar \u00cdcone", @@ -1704,11 +2423,40 @@ "label.upload.volume.from.local": "Carregar volume local", "label.upload.volume.from.url": "Carregar volume por URL", "label.url": "URL", +"label.usage": "Uso", +"label.usage.explanation": "Nota: Apenas o Usage Server que possui o job de uso ativo \u00e9 mostrado aqui.", +"label.usage.records.downloading": "Baixando registros de uso", +"label.usage.records.fetch.child.domains": "Buscar registros de uso para dom\u00ednios filhos", +"label.usage.records.generate": "Gerar registros de uso", +"label.usage.records.generate.after": "Os registros de uso ser\u00e3o criados para o per\u00edodo ap\u00f3s ", +"label.usage.records.generate.description": "Se o job de uso agendado n\u00e3o foi executado ou falhou, isso gerar\u00e1 registros (apenas se houver registros a serem gerados)", +"label.usage.records.generated": "Um job foi criado para gerar registros de uso.", +"label.usage.records.purge": "Expurgar registros de uso", +"label.usage.records.purge.alert": "Expurgar registros de uso excluir\u00e1 permanentemente os registros do banco de dados. Dependendo dos dados sendo exclu\u00eddos, isso pode aumentar a carga no banco de dados e pode demorar um pouco. Tem certeza de que deseja continuar?", +"label.usage.records.purge.days": "Expurgar registros com mais de", +"label.usage.records.purge.days.description": "Expurgar registros com mais de o n\u00famero especificado de dias.", +"label.usage.records.usagetype.required": "O tipo de uso \u00e9 obrigat\u00f3rio com o ID do recurso", +"label.usageid": "ID do Recurso", +"label.usageislocal": "Um Usage Server est\u00e1 instalado localmente", +"label.usagename": "Tipo de uso", +"label.usagetypedescription": "Descri\u00e7\u00e3o do uso", "label.usageinterface": "Interface de uso", "label.usagetype": "Tipo", "label.usageunit": "Unidade", "label.use.kubectl.access.cluster": "os arquivos kubectl e kubeconfig para acessar o cluster", "label.use.local.timezone": "Use o fuso hor\u00e1rio local", +"label.userdata.do.append": "Anexar dados do usu\u00e1rio", +"label.userdata.do.override": "Substituir dados do usu\u00e1rio", +"label.userdata.registered": "Dados do usu\u00e1rio armazenados", +"label.userdata.text": "Entrada manual de dados do usu\u00e1rio", +"label.userdatadetails": "Detalhes dos dados do usu\u00e1rio", +"label.userdataid": "ID dos dados do usu\u00e1rio", +"label.userdataname": "Nome dos dados do usu\u00e1rio", +"label.userdataparams": "Par\u00e2metros dos dados do usu\u00e1rio", +"label.userdatapolicy": "Pol\u00edtica de link de dados do usu\u00e1rio", +"label.userdatapolicy.tooltip": "Os dados do usu\u00e1rio vinculados ao Template podem ser substitu\u00eddos pelos dados do usu\u00e1rio fornecidos durante a implanta\u00e7\u00e3o da Inst\u00e2ncia. Selecione a pol\u00edtica de substitui\u00e7\u00e3o conforme necess\u00e1rio.", +"label.username.tooltip": "O Nome de Usu\u00e1rio para o Host", +"label.keep.mac.address.on.public.nic": "Utilizar o mesmo endere\u00e7o MAC para a NIC p\u00fablica dos VRs", "label.used": "Utilizado", "label.usehttps": "Utilize HTTPS", "label.usenewdiskoffering": "Substituir a oferta de disco?", @@ -1733,7 +2481,9 @@ "label.vcenterpassword": "Senha vCenter", "label.vcenterusername": "Usu\u00e1rio vCenter", "label.vcsdeviceid": "ID", +"label.verify": "Verificar", "label.version": "Vers\u00e3o", +"label.versioning": "Versionamento", "label.versions": "Vers\u00f5es", "label.vgpu": "VGPU", "label.vgputype": "Tipo de vGPU", @@ -1741,11 +2491,14 @@ "label.view.all": "Visualizar tudo", "label.view.console": "Visualizar console", "label.viewing": "Visualizar", +"label.virtualmachine": "Inst\u00e2ncia", +"label.virtualmachinecount": "Contagem de Inst\u00e2ncias", "label.virtual.machine": "M\u00e1quina virtual", "label.virtual.machines": "M\u00e1quinas virtuais", "label.virtual.network": "Rede virtual", "label.virtual.networking": "Rede virtual", "label.virtual.routers": "Roteadores virtuais", +"label.virtual.routers.system.offering": "Oferta de sistema do roteador virtual", "label.virtualmachineid": "ID da VM", "label.virtualmachinename": "Nome da VM", "label.virtualsize": "Tamanho virtual", @@ -1767,18 +2520,59 @@ "label.vmlimit": "Limites de inst\u00e2ncias", "label.vmname": "Nome da VM", "label.vms": "VMs", +"label.vmscheduleactions": "A\u00e7\u00f5es", +"label.vmsnapshotlimit": "Limite de snapshots de VM", "label.vmstate": "Estado da VM", "label.vmtotal": "VMs totais", +"label.vmware": "VMware", "label.vmware.storage.policy": "Pol\u00edtica de armazenamento do VMWare", "label.vmwaredcid": "ID do datacenter VMware", "label.vmwaredcname": "Nome do datacenter VMware", "label.vmwaredcvcenter": "Vcednter do datacenter VMware", "label.vmwarenetworklabel": "Etiqueta de tr\u00e1fego VMware", -"label.vnf.appliance.add": "Adicionar VNF Appliance", "label.vnmc": "VNMC", +"label.vnf.app.action.destroy": "Destruir appliance VNF", +"label.vnf.app.action.edit": "Editar appliance VNF", +"label.vnf.app.action.expunge": "Expurgar appliance VNF", +"label.vnf.app.action.migrate.to.host": "Migrar appliance VNF para outro host", +"label.vnf.app.action.migrate.to.ps": "Migrar appliance VNF para outro armazenamento prim\u00e1rio", +"label.vnf.app.action.reboot": "Reiniciar appliance VNF", +"label.vnf.app.action.recover": "Recuperar appliance VNF", +"label.vnf.app.action.reinstall": "Reinstalar appliance VNF", +"label.vnf.app.action.scale": "Escalonar appliance VNF", +"label.vnf.app.action.start": "Iniciar appliance VNF", +"label.vnf.app.action.stop": "Parar appliance VNF", +"label.vnf.appliance": "Appliance VNF", +"label.vnf.appliance.add": "Adicionar Appliance VNF", +"label.vnf.appliance.access.methods": "Informa\u00e7\u00f5es de acesso de gerenciamento para este appliance VNF", +"label.vnf.appliances": "Appliances VNF", +"label.vnf.cidr.list": "CIDR a partir do qual o acesso \u00e0 interface de Gerenciamento do appliance VNF deve ser permitido", +"label.vnf.cidr.list.tooltip": "a lista CIDR para encaminhar tr\u00e1fego para a interface de gerenciamento VNF. M\u00faltiplas entradas devem ser separadas por um \u00fanico caractere de v\u00edrgula (,). O valor padr\u00e3o \u00e9 0.0.0.0/0.", +"label.vnf.configure.management": "Configurar regras de Firewall e Encaminhamento de Porta para as interfaces de gerenciamento da VNF", +"label.vnf.configure.management.tooltip": "Verdadeiro por padr\u00e3o, regras de grupo de seguran\u00e7a ou de rede (source nat e regras de firewall) ser\u00e3o configuradas para interfaces de gerenciamento VNF. Falso caso contr\u00e1rio. Saiba quais regras s\u00e3o configuradas em http://docs.cloudstack.apache.org/en/latest/adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances", +"label.vnf.detail.add": "Adicionar detalhe VNF", +"label.vnf.detail.remove": "Remover detalhe VNF", +"label.vnf.details": "Detalhes VNF", +"label.vnf.nics": "NICs VNF", +"label.vnf.nic.add": "Adicionar NIC VNF", +"label.vnf.nic.delete": "Excluir NIC VNF", +"label.vnf.nic.description": "Descri\u00e7\u00e3o da NIC VNF", +"label.vnf.nic.deviceid": "ID do Dispositivo da NIC VNF. Come\u00e7a com 0. A NIC com deviceid como 0 para o appliance VNF ser\u00e1 a NIC padr\u00e3o.", +"label.vnf.nic.edit": "Editar NIC VNF", +"label.vnf.nic.management": "NIC de Gerenciamento", +"label.vnf.nic.management.description": "Verdadeiro se a NIC VNF for uma interface de gerenciamento. Falso caso contr\u00e1rio", +"label.vnf.nic.mappings": "Mapeamentos de NIC VNF", +"label.vnf.nic.name": "Nome da NIC VNF", +"label.vnf.nic.remove": "Remover NIC VNF", +"label.vnf.nic.required": "Verdadeiro se a NIC VNF for obrigat\u00f3ria. Caso contr\u00e1rio, opcional", +"label.vnf.settings": "Configura\u00e7\u00f5es VNF", +"label.vnf.template.register": "Registrar template VNF", +"label.vnf.templates": "Templates VNF", "label.volgroup": "Grupo de volume", "label.volume": "Disco", "label.volume.empty": "Nenhum volume de dados anexado a esta VM", +"label.volume.encryption.support": "Criptografia de Volume Suportada", +"label.volume.metrics": "M\u00e9tricas de Volume", "label.volume.volumefileupload.description": "Clique ou arraste o arquivo para esta \u00e1rea para carreg\u00e1-lo", "label.volumechecksum": "MD5 checksum", "label.volumechecksum.description": "Utilize o hash que voc\u00ea criou no inicio do procedimento de carregamento de volume", @@ -1790,15 +2584,18 @@ "label.volumename": "Nome do disco", "label.volumes": "Discos", "label.volumetotal": "Disco", +"label.volumetype": "Tipo de Volume", "label.vpc": "VPC", "label.vpc.id": "ID da VPC", "label.vpc.offerings": "Ofertas VPC", "label.vpc.virtual.router": "Roteador virtual VPC", +"label.vpc.restart.required": "Reinicializa\u00e7\u00e3o de VPC necess\u00e1ria", "label.vpcid": "VPC", "label.vpclimit": "Limites VPC", "label.vpcname": "VPC", "label.vpcoffering": "Oferta VPC", "label.vpcofferingid": "Oferta VPC", +"label.vpcs": "VPCs", "label.vpn": "VPN", "label.vpn.connection": "Conex\u00e3o VPN", "label.vpn.gateway": "Gateway de VPN", @@ -1819,20 +2616,26 @@ "label.warn": "Avisar", "label.warn.upper": "AVISO", "label.warning": "Aten\u00e7\u00e3o", +"label.webhook": "Webhook", +"label.webhook.deliveries": "Entregas de Webhook", +"label.webhookname": "Webhook", +"label.webhooks": "Webhooks", "label.wednesday": "Quarta-Feira", "label.weekly": "Semanal", "label.welcome": "Bem-Vindo", "label.what.is.cloudstack": "O que \u00e9 o CloudStack™?", "label.windows": "Windows", "label.with.snapshotid": "com o ID da snapshot", -"label.write": "Escreva", +"label.write": "Escrita", "label.writeback": "Write-back", "label.writecachetype": "Tipo do cache de escrita", "label.writeio": "Escrita (IO)", "label.writethrough": "Write-through", "label.xennetworklabel": "Etiqueta de tr\u00e1fego XenServer", +"label.xenserver": "XenServer", "label.xenservertoolsversion61plus": "Vers\u00e3o original do XS \u00e9 6.1+", "label.yes": "Sim", +"label.your.autoscale.vmgroup": "Seu grupo de escalonamento autom\u00e1tico", "label.yourinstance": "Sua inst\u00e2ncia", "label.zone": "Zona", "label.zone.dedicated": "Zona dedicada", @@ -1845,18 +2648,37 @@ "label.zonenamelabel": "Nome da zona", "label.zones": "Zonas", "label.zonewizard.traffictype.storage": "Armazenamento: tr\u00e1fego entre servidores de armazenamento prim\u00e1ria e secund\u00e1ria, tais como templates de m\u00e1quinas virtuais e snapshots", +"label.quotagb": "Cota em GB", +"label.quotagib": "Cota em GiB", "message.acquire.ip.failed": "Falha ao adquirir IP", +"message.action.about.mandate.and.disable.2FA.user.auth": "A autentica\u00e7\u00e3o de dois fatores \u00e9 obrigat\u00f3ria para o usu\u00e1rio. Se for desativada agora, o usu\u00e1rio precisar\u00e1 configurar a autentica\u00e7\u00e3o de dois fatores novamente no pr\u00f3ximo login.

Por favor, confirme que voc\u00ea deseja desativ\u00e1-la.", "message.action.acquire.ip": "Por favor, confirme que voc\u00ea quer adquirir um novo IP", "message.action.cancel.maintenance": "A manuten\u00e7\u00e3o do seu host foi cancelada com sucesso", "message.action.cancel.maintenance.mode": "Confirme que voc\u00ea deseja cancelar esta manuten\u00e7\u00e3o", "message.action.create.snapshot.from.vmsnapshot": "Por favor, confirme que voc\u00ea quer criar uma snapshot a partir de uma snapshot de VM", +"message.action.delete.asnrange": "Por favor, confirme a faixa AS que voc\u00ea deseja excluir", +"message.action.delete.autoscale.vmgroup": "Por favor, confirme que voc\u00ea deseja excluir este grupo de autoescalonamento.", "message.action.delete.backup.offering": "Por favor, confirme que voc\u00ea quer remover esta oferta de backup", +"message.action.delete.backup.repository": "Por favor, confirme que voc\u00ea deseja excluir este reposit\u00f3rio de backup?", +"message.action.delete.backup.schedule": "Confirme que voc\u00ea deseja remover esse agendamento de backup.", "message.action.delete.cluster": "Confirme que voc\u00ea deseja excluir este host", +"message.action.delete.domain": "Por favor, confirme que voc\u00ea deseja excluir este dom\u00ednio.", "message.action.delete.disk.offering": "Confirme que voc\u00ea deseja excluir esta oferta de disco", +"message.action.delete.external.firewall": "Por favor, confirme que voc\u00ea gostaria de remover este firewall externo. Aviso: Se voc\u00ea planeja adicionar de volta o mesmo firewall externo, deve redefinir os dados de uso no dispositivo.", +"message.action.delete.external.load.balancer": "Por favor, confirme que voc\u00ea gostaria de remover este balanceador de carga externo. Aviso: Se voc\u00ea planeja adicionar de volta o mesmo balanceador de carga externo, deve redefinir os dados de uso no dispositivo.", +"message.action.delete.gui.theme": "Por favor, confirme que voc\u00ea deseja excluir este tema da GUI", +"message.action.delete.guest.os": "Por favor, confirme que voc\u00ea deseja excluir este SO convidado. Entradas definidas pelo sistema n\u00e3o podem ser exclu\u00eddas.", +"message.action.delete.guest.os.hypervisor.mapping": "Por favor, confirme que voc\u00ea deseja excluir este mapeamento de hypervisor do SO convidado. Entradas definidas pelo sistema n\u00e3o podem ser exclu\u00eddas.", +"message.action.delete.ingress.rule": "Por favor, confirme que voc\u00ea deseja excluir esta regra de ingresso.", "message.action.delete.instance.group": "Por favor, confirme que voc\u00ea quer remover o grupo de inst\u00e2ncia", +"message.action.delete.interface.static.route": "Por favor, confirme que voc\u00ea deseja remover esta Rota Est\u00e1tica de interface?", "message.action.delete.iso": "Confirme que voc\u00ea deseja excluir esta ISO", +"message.action.delete.ipv4.subnet": "Por favor, confirme que voc\u00ea deseja excluir esta sub-rede IPv4.", +"message.action.delete.network.static.route": "Por favor, confirme que voc\u00ea deseja remover esta Rota Est\u00e1tica de Rede", "message.action.delete.network": "Confirme que voc\u00ea deseja remover esta rede.", +"message.action.delete.nexusvswitch": "Por favor, confirme que voc\u00ea deseja excluir este nexus 1000v", "message.action.delete.node": "Por favor, confirme que voc\u00ea quer remover este nodo.", +"message.action.delete.oauth.provider": "Por favor, confirme que voc\u00ea deseja excluir o provedor OAuth.", "message.action.delete.physical.network": "Por favor confirme que voc\u00ea deseja deletar esta rede f\u00edsica", "message.action.delete.pod": "Confirme que voc\u00ea deseja remover este pod.", "message.action.delete.secondary.storage": "Confirme que voc\u00ea deseja remover este armazenamento secund\u00e1rio.", @@ -1865,30 +2687,46 @@ "message.action.delete.snapshot": "Confirme que voc\u00ea deseja remover esta snapshot.", "message.action.delete.system.service.offering": "Por favor confirme que voc\u00ea deseja deletar esta oferta de servi\u00e7o de sistema.", "message.action.delete.template": "Confirme que voc\u00ea deseja remover este template.", +"message.action.delete.theme": "Por favor, confirme que voc\u00ea deseja excluir este tema.", +"message.action.delete.tungsten.router.table": "Por favor, confirme que voc\u00ea deseja remover a Tabela de Rotas desta Rede?", "message.action.delete.volume": "Confirme que voc\u00ea deseja remover este disco.", "message.action.delete.vpn.user": "Por favor, confirme que voc\u00ea quer remover o usu\u00e1rio da VPN.", "message.action.delete.zone": "Confirme que voc\u00ea deseja remover esta zona.", "message.action.destroy.instance": "Por favor, confirme que voc\u00ea deseja excluir esta inst\u00e2ncia.", "message.action.destroy.instance.with.backups": "Por favor, confirme que voc\u00ea quer destruir a inst\u00e2ncia. \u00c9 poss\u00edvel que haja backups associados a inst\u00e2ncia no qual n\u00e3o ser\u00e3o removidos.", +"message.action.destroy.sharedfs": "Por favor, confirme que voc\u00ea deseja destruir este Sistema de Arquivos Compartilhado.
Cuidado: Isso excluir\u00e1 todos os dados do Sistema de Arquivos Compartilhado tamb\u00e9m.", "message.action.destroy.systemvm": "Confirme que voc\u00ea deseja excluir esta VM de sistema.", "message.action.destroy.volume": "Por favor, confirme que voc\u00ea quer destruir este volume.", +"message.action.disable.2FA.user.auth": "Por favor, confirme que voc\u00ea deseja desativar a autentica\u00e7\u00e3o de dois fatores do usu\u00e1rio.", "message.action.disable.cluster": "Confirma a desativa\u00e7\u00e3o do cluster.", +"message.action.disable.disk.offering": "Por favor, confirme que voc\u00ea deseja desativar esta oferta de disco.", "message.action.disable.physical.network": "Por favor confirme que voc\u00ea deseja desabilitar esta rede f\u00edsica.", "message.action.disable.pod": "Confirma a desativa\u00e7\u00e3o do pod.", "message.action.disable.static.nat": "Confirme que voc\u00ea deseja desativar o NAT est\u00e1tico.", +"message.action.disable.service.offering": "Por favor, confirme que voc\u00ea deseja desativar esta oferta de servi\u00e7o.", +"message.action.disable.system.service.offering": "Por favor, confirme que voc\u00ea deseja desativar esta oferta de servi\u00e7o do sistema.", "message.action.disable.zone": "Confirma a desativa\u00e7\u00e3o da zona.", "message.action.download.iso": "Por favor confirme que voc\u00ea deseja baixar esta ISO.", "message.action.download.snapshot": "Por favor confirme que voc\u00ea deseja baixar esta snapshot.", "message.action.download.template": "Por favor confirme que voc\u00ea deseja baixar este template.", +"message.action.edit.nfs.mount.options": "Altera\u00e7\u00f5es nas op\u00e7\u00f5es de montagem NFS s\u00f3 ter\u00e3o efeito ao cancelar o modo de manuten\u00e7\u00e3o, o que far\u00e1 com que o pool de armazenamento seja remontado em todos os hosts KVM com as novas op\u00e7\u00f5es de montagem.", "message.action.enable.cluster": "Confirma a ativa\u00e7\u00e3o do cluster.", +"message.action.enable.disk.offering": "Por favor, confirme que voc\u00ea deseja ativar esta oferta de disco.", +"message.action.enable.service.offering": "Por favor, confirme que voc\u00ea deseja ativar esta oferta de servi\u00e7o.", +"message.action.enable.system.service.offering": "Por favor, confirme que voc\u00ea deseja ativar esta oferta de servi\u00e7o do sistema.", "message.action.enable.physical.network": "Por favor confirme que voc\u00ea deseja habilitar esta rede f\u00edsica.", "message.action.enable.pod": "Confirma a ativa\u00e7\u00e3o do pod.", "message.action.enable.zone": "Confirma a ativa\u00e7\u00e3o da zona.", "message.action.expunge.instance": "Por favor, confirme que voc\u00ea deseja eliminar esta inst\u00e2ncia.", "message.action.expunge.instance.with.backups": "Por favor, confirme que voc\u00ea quer eliminar esta inst\u00e2ncia. \u00c9 poss\u00edvel que haja backups associados a inst\u00e2ncia no qual n\u00e3o ser\u00e3o removidos.", +"message.action.expunge.sharedfs": "Por favor, confirme que voc\u00ea deseja expurgar este Sistema de Arquivos Compartilhado.", "message.action.host.enable.maintenance.mode": "Ativar o modo de manuten\u00e7\u00e3o ir\u00e1 causar o live migration de todas as inst\u00e2ncias hospedadas neste host para o pr\u00f3ximo dispon\u00edvel.", "message.action.instance.reset.password": "Por favor confirme que voc\u00ea deseja alterar a senha de root para est\u00e1 m\u00e1quina virtual.", "message.action.manage.cluster": "Confirma a vincula\u00e7\u00e3o do cluster.", +"message.action.patch.router": "Por favor, confirme que voc\u00ea deseja aplicar um patch ao vivo no roteador.
Esta opera\u00e7\u00e3o \u00e9 equivalente a atualizar os pacotes do roteador e reiniciar a Rede sem limpeza.", +"message.action.patch.systemvm": "Por favor, confirme que voc\u00ea deseja aplicar um patch na VM de Sistema.", +"message.action.primary.storage.scope.cluster": "Por favor, confirme que voc\u00ea deseja alterar o escopo de zona para o cluster especificado.
Esta opera\u00e7\u00e3o atualizar\u00e1 o banco de dados e desconectar\u00e1 o pool de armazenamento de todos os hosts que estavam conectados anteriormente ao armazenamento prim\u00e1rio e n\u00e3o fazem parte do cluster especificado.", +"message.action.primary.storage.scope.zone": "Por favor, confirme que voc\u00ea deseja alterar o escopo de cluster para zona.
Esta opera\u00e7\u00e3o atualizar\u00e1 o banco de dados e conectar\u00e1 o pool de armazenamento a todos os hosts da zona executando o mesmo hypervisor definido no pool de armazenamento.", "message.action.primarystorage.enable.maintenance.mode": "Aviso: colocar o armazenamento prim\u00e1rio em modo de manuten\u00e7\u00e3o ir\u00e1 causar a parada de todas as VMs hospedadas nesta unidade. Deseja continuar?", "message.action.quota.tariff.create.error.namerequired": "Por favor, informe o nome da tarifa.", "message.action.quota.tariff.create.error.usagetyperequired": "Por favor, selecione o tipo da tarifa.", @@ -1897,8 +2735,16 @@ "message.action.reboot.instance": "Por favor, confirme que voc\u00ea deseja reiniciar esta inst\u00e2ncia.", "message.action.reboot.router": "Confirme que voc\ufffd deseja reiniciar este roteador.", "message.action.reboot.systemvm": "Confirme que voc\u00ea deseja reiniciar esta VM de sistema.", +"message.action.recover.sharedfs": "Por favor, confirme que voc\u00ea gostaria de recuperar este Sistema de Arquivos Compartilhado.", "message.action.recover.volume": "Por favor, confirme que voc\u00ea quer recuperar este volume.", +"message.action.release.asnumber": "Por favor, confirme que voc\u00ea deseja liberar este N\u00famero AS.", "message.action.release.ip": "Confirme que voc\u00ea deseja liberar este IP.", +"message.action.release.reserved.ip": "Por favor, confirme que voc\u00ea deseja liberar este IP reservado.", +"message.action.remove.host": "Por favor, confirme que voc\u00ea deseja remover este host.", +"message.action.remove.logical.router": "Por favor, confirme que voc\u00ea deseja remover o Roteador L\u00f3gico?", +"message.action.remove.routing.policy": "Por favor, confirme que voc\u00ea deseja remover a Pol\u00edtica de Roteamento desta Rede", +"message.action.reserve.ip": "Por favor, confirme que voc\u00ea deseja reservar este IP.", +"message.action.restart.sharedfs": "Por favor, confirme que voc\u00ea deseja reiniciar este Sistema de Arquivos Compartilhado. Isso causar\u00e1 um tempo de inatividade para o usu\u00e1rio.
Reiniciar com limpeza reinicializar\u00e1 a Inst\u00e2ncia do Sistema de Arquivos Compartilhado sem afetar o sistema de arquivos instalado.", "message.action.revert.snapshot": "Por favor, confirme que voc\u00ea deseja reverter o seu volume desta snapshot.", "message.action.router.health.checks": "Resultados das checagens de sa\u00fade ser\u00e3o obtidos do roteador.", "message.action.router.health.checks.disabled.warning": "Por favor, habilite as checagens de sa\u00fade do roteador.", @@ -1909,15 +2755,20 @@ "message.action.settings.warning.vm.running": "Por favor, pare a VM para acessar as configura\u00e7\u00f5es", "message.action.start.instance": "Por favor, confirme que voc\u00ea deseja iniciar esta inst\u00e2ncia.", "message.action.start.router": "Confirme que voc\u00ea deseja inciar este roteador.", +"message.action.start.sharedfs": "Por favor, confirme que voc\u00ea deseja iniciar este Sistema de Arquivos Compartilhado.", "message.action.start.systemvm": "Confirme que voc\u00ea deseja iniciar esta VM de sistema.", "message.action.stop.instance": "Por favor, confirme que voc\u00ea deseja parar esta inst\u00e2ncia.", "message.action.stop.router": "Confirme que voc\ufffd deseja parar este roteador.", +"message.action.stop.sharedfs": "Por favor, confirme que voc\u00ea deseja parar este Sistema de Arquivos Compartilhado.", "message.action.stop.systemvm": "Confirme que voc\u00ea deseja parar esta VM de sistema.", "message.action.unmanage.cluster": "Confirma a desvincula\u00e7\u00e3o do cluster.", "message.action.unmanage.instance": "Por favor, confirme que voc\u00ea deseja parar de gerenciar a inst\u00e2ncia.", "message.action.unmanage.instances": "Por favor, confirme que voc\u00ea deseja parar de gerenciar as inst\u00e2ncias.", "message.action.unmanage.virtualmachine": "Por favor, confirme que voc\u00ea deseja parar de gerenciar a VM.", "message.action.vmsnapshot.delete": "Por favor, confirme que voc\u00ea deseja excluir esta snapshot de VM.", +"message.action.unmanage.volume": "Por favor, confirme que voc\u00ea deseja parar de gerenciar o Volume.", +"message.action.unmanage.volumes": "Por favor, confirme que voc\u00ea deseja parar de gerenciar os Volumes.", +"message.action.vmsnapshot.disk-only.delete": "Por favor, confirme que voc\u00ea deseja excluir esta snapshot de VM.", "message.activate.project": "Voc\u00ea tem certeza que deseja ativar este projeto?", "message.add.egress.rule.failed": "Falha ao adicionar uma nova regra de sa\u00edda", "message.add.egress.rule.processing": "Adicionando uma nova regra de sa\u00edda...", @@ -1925,14 +2776,29 @@ "message.add.firewall": "Adicionar firewall \u00e0\u00a0 zona.", "message.add.firewall.rule.failed": "Falha ao adicionar uma nova regra de firewall", "message.add.firewall.rule.processing": "Adicionando uma nova regra de firewall...", +"message.add.firewallrule.failed": "Falha ao adicionar Regra de Firewall", "message.add.host": "Especifique os seguintes par\u00e2metros para adicionar um novo host.", +"message.add.host.sshkey": "AVISO: Para adicionar um host com chave SSH, voc\u00ea deve garantir que o host do seu hypervisor foi configurado corretamente.", "message.add.iprange.processing": "Adicionando intervalo IP...", +"message.add.ipv4.subnet.for.guest.network.failed": "Falha ao adicionar sub-rede IPv4 para rede guest", +"message.add.ipv4.subnet.for.guest.network.processing": "Adicionando sub-rede IPv4 para rede guest...", +"message.add.ip.v6.prefix.processing": "Adicionando Prefixo IPv6...", +"message.add.ip.v6.firewall.rule.failed": "Falha ao adicionar regra de firewall IPv6", +"message.add.ip.v6.firewall.rule.processing": "Adicionando regra de firewall IPv6...", +"message.add.ip.v6.firewall.rule.success": "Regra de firewall IPv6 adicionada", +"message.redeliver.webhook.delivery": "Reentregar esta entrega de Webhook", +"message.remove.ip.v6.firewall.rule.failed": "Falha ao remover regra de firewall IPv6", +"message.remove.ip.v6.firewall.rule.processing": "Removendo regra de firewall IPv6...", +"message.remove.ip.v6.firewall.rule.success": "Regra de firewall IPv6 removida", +"message.add.nsx.controller": "Adicionar Provedor NSX", "message.add.network": "Adicionar uma nova rede para a zona: ", "message.add.network.acl.failed": "Falha ao adicionar lista de rede ACL", "message.add.network.acl.processing": "Adicionando lista de rede ACL...", "message.add.network.failed": "Falha ao adicionar rede", "message.add.network.processing": "Adicionando rede...", "message.add.new.gateway.to.vpc": "Favor especificar a informa\u00e7\u00e3o para adicionar um novo gateway a esta VPC.", +"message.add.physical.network.failed": "Falha ao adicionar rede f\u00edsica", +"message.add.physical.network.processing": "Adicionando uma nova rede f\u00edsica...", "message.add.pod": "Adicionar um novo pod para a zona ", "message.add.pod.during.zone.creation": "Cada zona deve conter um ou mais pods e iremos adicionar o primeiro pod agora. Um pod cont\u00e9m hosts e servidores de armazenamento prim\u00e1rio que ser\u00e3o adicionados em uma etapa posterior. Inicialmente, configure um intervalo de endere\u00e7os IP reservados para o tr\u00e1fego de gerenciamento interno do CloudStack. A faixa de IP reservados devem ser \u00fanicos para cada zona na nuvem.", "message.add.port.forward.failed": "Falha ao adicionar nova regra de encaminhamento de porta", @@ -1941,6 +2807,9 @@ "message.add.private.gateway.processing": "Adicionando gateway privado...", "message.add.resource.description": "Adicionar recursos de infraestrutura", "message.add.resource.hint": "Adicionar recursos de infraestrutura - pods, clusters, armazenamentos prim\u00e1rios/secund\u00e1rios.", +"message.add.routing.firewall.rule.failed": "Falha ao adicionar regra de firewall de Roteamento IPv4", +"message.add.routing.firewall.rule.processing": "Adicionando regra de firewall de Roteamento IPv4...", +"message.add.routing.firewall.rule.success": "Regra de firewall de Roteamento IPv4 adicionada", "message.add.rule.failed": "Falha ao adicionar nova regra", "message.add.rule.processing": "Adicionando nova regra do security-group...", "message.add.secondary.ipaddress.processing": "Adicionando endere\u00e7o IP secund\u00e1rio...", @@ -1949,7 +2818,10 @@ "message.add.tag.failed": "Falha ao adicionar nova etiqueta", "message.add.tag.for.networkacl": "Adicionar etiqueta para rede ACL", "message.add.tag.processing": "Adicionando nova etiqueta...", +"message.add.template": "Por favor, insira os seguintes dados para criar o seu novo Template", +"message.add.tungsten.routing.policy.available": "A pol\u00edtica de roteamento Tungsten-Fabric est\u00e1 pronta para iniciar. Por favor, prossiga para a pr\u00f3xima etapa.", "message.add.user.to.project": "Este formul\u00e1rio permitir\u00e1 adicionar usu\u00e1rios espec\u00edficos de uma conta a um projeto.
Al\u00e9m disso, uma fun\u00e7\u00e3o de projeto pode ser adicionada ao usu\u00e1rio/conta para permitir/n\u00e3o permitir o acesso API em n\u00edvel de projeto.
Tamb\u00e9m podemos especificar a fun\u00e7\u00e3o com a qual o usu\u00e1rio deve ser adicionado a um projeto - admin/Regular; se n\u00e3o for especificado, o padr\u00e3o \u00e9 'Regular'.", +"message.add.volume": "Por favor, preencha os seguintes dados para adicionar um novo volume.", "message.add.vpn.connection.failed": "Falha ao adicionar conex\u00e3o VPN", "message.add.vpn.connection.processing": "Adicionando conex\u00e3o VPN...", "message.add.vpn.customer.gateway": "Adicionando o gateway da VPN do cliente", @@ -1957,11 +2829,14 @@ "message.add.vpn.gateway": "Favor confirmar que voc\u00ea deseja adicionar um gateway de VPN", "message.add.vpn.gateway.failed": "Falha ao adicionar gateway da VPN", "message.add.vpn.gateway.processing": "Adicionando gateway da VPN...", +"message.added.vpc.offering": "Oferta de VPC adicionada", +"message.adding.firewall.policy": "Adicionando Pol\u00edtica de Firewall", "message.adding.host": "Adicionando host", "message.adding.netscaler.device": "Adicionando dispositivo Nescaler", "message.adding.netscaler.provider": "Adicionando Netscaler provider", "message.advanced.security.group": "Escolha esta op\u00e7\u00e3o se desejar utilizar grupos de seguran\u00e7a para isolamento das VMs guest.", "message.alert.show.all.stats.data": "Isso pode retornar muitos dados dependendo das configura\u00e7\u00f5es de coleta e reten\u00e7\u00e3o de estat\u00edsticas.", +"message.allowed": "Permitido", "message.apply.success": "Aplicado com sucesso", "message.assign.instance.another": "Favor especificar o tipo de conta, dom\u00ednio, nome da conta e rede (opcional) da nova conta.
Se o NIC padr\u00e3o da VM estiver em uma rede compartilhada, o CloudStack verificar\u00e1 se a rede pode ser usada pela nova conta se voc\u00ea n\u00e3o especificar uma rede.
Se o NIC padr\u00e3o da VM estiver em uma rede isolada, e a nova conta tiver mais uma rede isolada, voc\u00ea deve especificar uma.", "message.assign.vm.failed": "Falha na designa\u00e7\u00e3o de VM", @@ -1969,26 +2844,52 @@ "message.attach.volume": "Preencha os seguintes dados para conectar o novo disco. Se voc\u00ea est\u00e1 conectando um disco a uma m\u00e1quina virtual Windows, ser\u00e1 necess\u00e1rio reiniciar a inst\u00e2ncia para visualizar o novo disco.", "message.attach.volume.failed": "Falha ao anexar volume", "message.attach.volume.progress": "Anexando volume", +"message.attach.volume.success": "Volume anexado com sucesso \u00e0 inst\u00e2ncia", "message.authorization.failed": "Sess\u00e3o expirada, a verifica\u00e7\u00e3o de autoriza\u00e7\u00e3o falhou", +"message.autoscale.loadbalancer.update": "A regra do balanceador de carga s\u00f3 pode ser atualizada quando o grupo de autoescalonamento estiver DESATIVADO.", +"message.autoscale.policies.update": "As pol\u00edticas de aumento/diminui\u00e7\u00e3o s\u00f3 podem ser atualizadas quando o grupo de autoescalonamento estiver DESATIVADO.", +"message.autoscale.vm.networks": "Por favor, escolha pelo menos uma Rede para as Inst\u00e2ncias no grupo de autoescalonamento. A Rede padr\u00e3o deve ser uma Rede Isolada ou Camada de Rede VPC que suporte AutoScale de Inst\u00e2ncia e tenha regras de balanceamento de carga.", +"message.autoscale.vmprofile.update": "O perfil de Inst\u00e2ncia de AutoScale s\u00f3 pode ser atualizado quando o grupo de autoescalonamento estiver DESATIVADO.", "message.backup.attach.restore": "Favor confirmar que voc\u00ea deseja restaurar e anexar o volume do backup?", "message.backup.create": "Voc\u00ea tem certeza de que quer criar um backup da VM?", "message.backup.offering.remove": "Voc\u00ea tem certeza que quer remover a VM da oferta de backup e excluir a cadeia de backup?", "message.backup.restore": "Por favor, confirme que voc\u00ea deseja restaurar o backup da VM?", +"message.bgp.peers.null": "Por favor note, se nenhum par BGP for selecionado, o VR conectar\u00e1 a
(1) pares BGP dedicados que o propriet\u00e1rio pode acessar, se o propriet\u00e1rio tiver pares BGP dedicados e a configura\u00e7\u00e3o da conta use.system.bgp.peers estiver definida como false;
(2) todos os pares BGP que o propriet\u00e1rio pode acessar, caso contr\u00e1rio.
", +"message.bucket.delete": "Por favor, confirme que voc\u00ea deseja excluir este Bucket", +"message.cancel.shutdown": "Por favor, confirme que voc\u00ea gostaria de cancelar o desligamento neste Management Server. Ele voltar\u00e1 a aceitar quaisquer novos job Ass\u00edncronos.", "message.certificate.upload.processing": "Carregamento de certificados em andamento", +"message.change.disk.offering.sharedfs.failed": "Falha ao alterar oferta de disco para o Sistema de Arquivos Compartilhado.", +"message.change.disk.offering.sharedfs.processing": "Alterando oferta de disco para o Sistema de Arquivos Compartilhado.", "message.change.offering.confirm": "Por favor, confirme que voc\u00ea deseja mudar a oferta de servi\u00e7o desta inst\u00e2ncia virtual.", +"message.change.offering.for.volume": "Por favor, confirme que voc\u00ea deseja alterar a oferta de disco para o volume", +"message.change.offering.for.volume.failed": "Falha ao alterar oferta para o volume", +"message.change.offering.processing": "Alterando oferta para o volume...", "message.change.password": "Por favor, troque sua senha.", +"message.change.scope.failed": "Falha na altera\u00e7\u00e3o de escopo", +"message.change.scope.processing": "Altera\u00e7\u00e3o de escopo em andamento", +"message.change.service.offering.sharedfs.failed": "Falha ao alterar oferta de servi\u00e7o para o Sistema de Arquivos Compartilhado.", +"message.change.service.offering.sharedfs.processing": "Alterando oferta de servi\u00e7o para o Sistema de Arquivos Compartilhado.", "message.cluster.dedicated": "Cluster dedicado", "message.cluster.dedication.released": "Cluster dedicado liberado", +"message.config.health.monitor.failed": "Configura\u00e7\u00e3o do Monitor de Sa\u00fade falhou", "message.config.sticky.policy.failed": "Falha na configura\u00e7\u00e3o da pol\u00edtica sticky", "message.config.sticky.policy.processing": "Atualizando pol\u00edtica sticky...", +"message.configure.network.ip.and.mac": "Por favor, configure o endere\u00e7o IP e o endere\u00e7o mac das redes, se necess\u00e1rio.", +"message.configure.network.select.default.network": "Por favor, configure o endere\u00e7o IP e o endere\u00e7o mac das redes, se necess\u00e1rio. Por favor, selecione uma rede como a rede padr\u00e3o.", "message.configuring.guest.traffic": "Configurando tr\u00e1fego do guest", +"message.configuring.nsx.public.traffic": "Configurando tr\u00e1fego p\u00fablico NSX", "message.configuring.physical.networks": "Configurando redes f\u00edsicas", "message.configuring.public.traffic": "Configurando tr\u00e1fego p\u00fablico", "message.configuring.storage.traffic": "Configurando tr\u00e1fego de storage", "message.confirm.action.force.reconnect": "Por favor confirme que voc\u00ea deseja for\u00e7ar a reconex\u00e3o com este host.", +"message.confirm.add.router.table.to.instance": "Por favor, confirme que voc\u00ea deseja adicionar a Tabela de Rotas a esta NIC", +"message.confirm.add.routing.policy": "Por favor, confirme que voc\u00ea deseja adicionar a Pol\u00edtica de Roteamento a esta Rede", "message.confirm.archive.selected.alerts": "Por favor confirme que voc\u00ea deseja arquivar os alertas selecionados", "message.confirm.archive.selected.events": "Por favor confirme que voc\u00ea deseja arquivar os eventos selecionados", "message.confirm.attach.disk": "Voc\u00ea tem certeza que deseja conectar este disco?", +"message.confirm.change.disk.offering.for.sharedfs": "Por favor, confirme que voc\u00ea deseja alterar a oferta de disco para o Sistema de Arquivos Compartilhado. Isso pode migrar o volume subjacente para um pool de armazenamento diferente, se necess\u00e1rio.", +"message.confirm.change.offering.for.volume": "Por favor, confirme que voc\u00ea deseja alterar a oferta de disco para o volume", +"message.confirm.change.service.offering.for.sharedfs": "Por favor, confirme que voc\u00ea deseja alterar a oferta de servi\u00e7o para o Sistema de Arquivos Compartilhado.", "message.confirm.configure.ovs": "Voc\u00ea tem certeza de que quer configurar os Ovs?", "message.confirm.delete.acl": "Voc\u00ea tem certeza que deseja apagar esta lista ACL?", "message.confirm.delete.bigswitchbcf": "Por favor, confirme que voc\u00ea deseja deletar este controlador BigSwitch BCF", @@ -2001,42 +2902,66 @@ "message.confirm.delete.niciranvp": "Por favor, confirme que voc\u00ea deseja excluir o controlador Nicira Nvp", "message.confirm.delete.pa": "Por favor, confirme que voc\u00ea deseja remover Palo Alto", "message.confirm.delete.provider": "Por favor, confirme que voc\u00ea deseja excluir este provedor?", +"message.confirm.delete.traffic.type": "Por favor, confirme que voc\u00ea gostaria de excluir o tipo de tr\u00e1fego.", "message.confirm.delete.srx": "Por favor confirme que voc\u00ea deseja remover o SRX", "message.confirm.destroy.router": "Por favor confirme que voc\u00ea gostaria de destruir este roteador", +"message.confirm.disable.autoscale.vmgroup": "Por favor, confirme que voc\u00ea deseja desativar este grupo de autoescalonamento.", "message.confirm.disable.host": "Favor confirmar que voc\u00ea deseja desabilitar este host.", "message.confirm.disable.network.offering": "Voc\u00ea tem certeza que deseja deshabilitar esta oferta de rede?", "message.confirm.disable.provider": "Por favor confirme que voc\u00ea gostaria de desabilitar este provider", "message.confirm.disable.storage": "Por favor, confirme que voc\u00ea deseja desabilitar o pool de armazenamento", "message.confirm.disable.vpc.offering": "Voc\u00ea tem certeza que deseja desabilitar esta oferta de VPC?", +"message.confirm.disable.webhook": "Por favor, confirme que voc\u00ea deseja desativar este webhook.", +"message.confirm.enable.autoscale.vmgroup": "Por favor, confirme que voc\u00ea deseja ativar este grupo de autoescalonamento.", "message.confirm.enable.host": "Por favor confirme que voc\u00ea deseja habilitar este host.", "message.confirm.enable.network.offering": "Voc\u00ea tem certeza que deseja habilitar esta oferta de rede?", "message.confirm.enable.provider": "Por favor confirme que voc\u00ea gostaria de habilitar este provider", "message.confirm.enable.storage": "Por favor, confirme que voc\u00ea deseja hablitar o pool de armazenamento", "message.confirm.enable.vpc.offering": "Voc\u00ea tem certeza que deseja habilitar esta oferta de VPC?", +"message.confirm.enable.webhook": "Por favor, confirme que voc\u00ea deseja ativar este webhook.", +"message.confirm.remove.firewall.rule": "Por favor, confirme que voc\u00ea deseja excluir esta Regra de Firewall?", "message.confirm.remove.ip.range": "Por favor confirme que voc\u00ea deseja remover este range de IP.", "message.confirm.remove.network.offering": "Voc\u00ea tem certeza que deseja remover esta oferta de rede?", +"message.confirm.remove.network.policy": "Por favor, confirme que voc\u00ea deseja remover esta Pol\u00edtica de Rede?", +"message.confirm.remove.routing.policy": "Por favor, confirme que voc\u00ea deseja excluir esta Pol\u00edtica de Roteamento?", "message.confirm.remove.selected.alerts": "Por favor confirme que voc\u00ea deseja remover os alertas selecionados", "message.confirm.remove.selected.events": "Por favor confirme que voc\u00ea deseja remover os eventos selecionados", "message.confirm.remove.vmware.datacenter": "Por favor, confirme que voc\u00ea quer remover este VMware datacenter", "message.confirm.remove.vpc.offering": "Voc\u00ea tem certeza que deseja remover esta oferta de VPC?", "message.confirm.replace.acl.new.one": "Voc\u00ea deseja substituir a ACL com uma nova?", +"message.confirm.reset.network.permissions": "Tem certeza de que deseja redefinir as permiss\u00f5es desta Rede?", "message.confirm.scale.up.router.vm": "Voc\u00ea realmente quer escalonar a VM do roteador?", "message.confirm.scale.up.system.vm": "Voc\u00ea realmente quer escalonar a VM do sistema?", "message.confirm.start.lb.vm": "Confirme que voc\u00ea deseja iniciar esta LB VM", "message.confirm.sync.storage": "Por favor, confirme que voc\u00ea gostaria de sincronizar o pool de armazenamento", +"message.confirm.type": "Para confirmar, digite", "message.confirm.upgrade.router.newer.template": "Por favor confirme que voc\u00ea deseja atualizar o roteador para usar o template mais recente.", "message.cpu.usage.info": "A porcentagem de uso da CPU pode exceder 100% se a VM tiver mais de 1 vCPU ou quando o CPU Cap n\u00E3o estiver habilitado. Este comportamento acontece de acordo com o hypervisor que est\u00E1 sendo utilizado (ex: no KVM), devido \u00E0 forma como contabilizam as estat\u00EDsticas", +"message.create.backup.failed": "Falha ao criar backup.", +"message.create.bucket.failed": "Falha ao criar bucket.", +"message.create.bucket.processing": "Cria\u00e7\u00e3o de bucket em andamento", "message.create.compute.offering": "Oferta de computa\u00e7\u00e3o criada", "message.create.internallb": "Criando LB interno", "message.create.internallb.failed": "Falha ao criar LB interno", "message.create.internallb.processing": "Cria\u00e7\u00e3o do LB interno em progresso", "message.create.service.offering": "Oferta de servi\u00e7o criada", +"message.create.sharedfs.failed": "Falha ao criar Sistema de Arquivos Compartilhado.", +"message.create.sharedfs.processing": "Cria\u00e7\u00e3o de Sistema de Arquivos Compartilhado em andamento.", "message.create.snapshot.from.vmsnapshot.failed": "Falha ao criar snapshot a partir de uma snapshot de VM", "message.create.snapshot.from.vmsnapshot.progress": "Cria\u00e7\u00e3o de snapshot em progresso", +"message.create.template.failed": "Falha ao criar template.", +"message.create.template.processing": "Cria\u00e7\u00e3o de template em andamento", +"message.create.tungsten.public.network": "Criar Rede p\u00fablica Tungsten-Fabric", "message.create.volume.failed": "Falha ao criar volume", "message.create.volume.processing": "Cria\u00e7\u00e3o de volume em progresso", "message.create.vpc.offering": "Oferta VPC criada", "message.create.vpn.customer.gateway.failed": "A cria\u00e7\u00e3o do gateway da VPN do cliente falhou", +"message.creating.autoscale.scaledown.conditions": "Criando condi\u00e7\u00f5es de ScaleDown", +"message.creating.autoscale.scaledown.policy": "Criando pol\u00edtica de ScaleDown", +"message.creating.autoscale.scaleup.conditions": "Criando condi\u00e7\u00f5es de ScaleUp", +"message.creating.autoscale.scaleup.policy": "Criando pol\u00edtica de ScaleUp", +"message.creating.autoscale.vmgroup": "Criando Grupo de AutoScale", +"message.creating.autoscale.vmprofile": "Criando perfil de Inst\u00e2ncia de AutoScale", "message.creating.cluster": "Criando cluster", "message.creating.guest.network": "Criando rede guest", "message.creating.physical.networks": "Criando redes fisicas", @@ -2059,9 +2984,13 @@ "message.delete.acl.rule": "Remover regra ACL", "message.delete.acl.rule.failed": "Falha ao remover regra ACL", "message.delete.affinity.group": "Por favor, confirme que voc\u00ea deseja remover este grupo de afinidade", +"message.delete.asn.range": "ASN Range exclu\u00eddo com sucesso", "message.delete.backup": "Voc\u00ea tem certeza de que quer apagar o backup?", "message.delete.failed": "Falha ao remover", "message.delete.gateway": "Favor confirmar que voc\u00ea deseja deleta o gateway", +"message.delete.ip.v6.prefix.processing": "Excluindo prefixo IPv6...", +"message.delete.keypair": "Favor confirmar que voc\u00ea deseja deletar a chave de API.", +"message.delete.keypair.failed": "Falha ao deletar par de chave de API", "message.delete.port.forward.processing": "Removendo regra de encaminhamento de porta...", "message.delete.project": "Voc\u00ea tem certeza que deseja remover este projeto?", "message.delete.rule.processing": "Removendo regra...", @@ -2070,44 +2999,70 @@ "message.delete.tag.failed": "Falha ao remover etiqueta", "message.delete.tag.for.networkacl": "Remover etiqueta para rede ACL", "message.delete.tag.processing": "Removendo etiqueta...", +"message.delete.traffic.type.processing": "Excluindo tipo de tr\u00e1fego...", +"message.delete.tungsten.policy.rule": "Por favor, confirme que voc\u00ea deseja excluir a Regra de Pol\u00edtica?", +"message.delete.tungsten.tag": "Tem certeza de que deseja remover esta Tag desta Pol\u00edtica?", "message.delete.user": "Por favor confirme que voc\u00ea deseja deletar este usu\u00e1rio.", "message.delete.vpn.connection": "Favor confirmar que voc\u00ea deseja deletar esta conex\u00e3o VPN", "message.delete.vpn.customer.gateway": "Favor confirmar que voc\u00ea deseja deletar este gateway de VPN de usu\u00e1rio", "message.delete.vpn.gateway": "Favor confirmar que voc\u00ea deseja deletar este gateway de VPN", +"message.delete.webhook": "Por favor, confirme que voc\u00ea deseja excluir este Webhook.", +"message.delete.webhook.delivery": "Por favor, confirme que voc\u00ea deseja excluir esta entrega de Webhook.", +"message.deleting.firewall.policy": "Excluindo Pol\u00edtica de Firewall", "message.deleting.node": "Removendo nodo", "message.deleting.vm": "Removendo VM", +"message.denied": "Negado", "message.deployasis": "O modelo selecionado \u00e9 Deploy As-Is, ou seja, a VM \u00e9 implantada atrav\u00e9s da importa\u00e7\u00e3o de um OVA com vApps diretamente no vCenter. O redimensionamento do(s) disco(s) raiz(s) \u00e9 permitido somente em VMs paradas para tais modelos.", "message.desc.advanced.zone": "Para topologias de rede mais sofisticadas. este modelo fornece maior flexibilidade na defini\u00e7\u00e3o de redes de clientes e fornece ofertas de rede personalizadas, tais como firewall, VPN ou de balanceamento de carga.", "message.desc.basic.zone": "Fornece uma \u00fanica rede onde em cada inst\u00e2ncia de VM \u00e9 atribu\u00eddo um IP diretamente na rede. O isolamento guest podem ser fornecidos atrav\u00e9s de camada-3 da rede com grupos de seguran\u00e7a (filtragem da fonte de endere\u00e7os IP).", +"message.desc.core.zone": "Zonas Core destinam-se a implanta\u00e7\u00f5es baseadas em Datacenter e permitem toda a gama de funcionalidades de Rede e outras no Apache CloudStack. Zonas Core t\u00eam uma s\u00e9rie de pr\u00e9-requisitos e dependem da presen\u00e7a de armazenamento compartilhado e inst\u00e2ncias auxiliares.", "message.desc.cluster": "Cada pod deve conter um ou mais clusters, e iremos adicionar o primeiro cluster agora. Um cluster fornece uma maneira de agrupamento de hosts. Os hosts de um cluster t\u00eam hardware id\u00eantico, executam o mesmo virtualizador, est\u00e3o na mesma sub-rede e acessam o mesmo armazenamento compartilhado. Cada cluster \u00e9 constitu\u00eddo por um ou mais hosts e um ou mais servidores de armazenamento prim\u00e1rio.", "message.desc.create.ssh.key.pair": "Por favor, preencha os seguintes dados para criar ou registar um par de chaves SSH.

(1) Se a chave p\u00fablica est\u00e1 definida, CloudStack ir\u00e1 registrar a chave p\u00fablica. Voc\u00ea pode us\u00e1-la atrav\u00e9s de sua chave privada.

(2) Se a chave p\u00fablica n\u00e3o est\u00e1 definida, CloudStack ir\u00e1 criar um novo par de chaves SSH. Neste caso, copie e salve a chave privada. CloudStack n\u00e3o ir\u00e1 mant\u00ea-la.
", "message.desc.created.ssh.key.pair": "Par de chaves SSH criado", -"message.desc.host": "Cada cluster deve conter pelo menos um host (computador) para as VMs guest serem executadas e iremos adicionar o primeira host agora. Para um host funcionar no CloudStack, voc\u00ea deve instalar um virtualizador no host, atribuir um endere\u00e7o IP e garantir que o host est\u00e1 conectado ao servidor de gerenciamento do CloudStack.

Forne\u00e7a o hostname ou o endere\u00e7o IP do host, o nome de usu\u00e1rio (geralmente root) e a senha e qualquer label que voc\u00ea utiliza para categorizar os hosts.", +"message.desc.edge.zone": "Zonas Edge s\u00e3o zonas leves, projetadas para implanta\u00e7\u00e3o em cen\u00e1rios de computa\u00e7\u00e3o de borda. Elas s\u00e3o limitadas em funcionalidade, mas t\u00eam muito menos pr\u00e9-requisitos do que zonas core.

Por favor, consulte a documenta\u00e7\u00e3o do Apache CloudStack para mais informa\u00e7\u00f5es sobre Tipos de Zona
http://docs.cloudstack.apache.org/en/latest/installguide/configuration.html#adding-a-zone", +"message.desc.host": "Cada cluster deve conter pelo menos um host (computador) para as VMs guest serem executadas e iremos adicionar o primeira host agora. Para um host funcionar no CloudStack, voc\u00ea deve instalar um virtualizador no host, atribuir um endere\u00e7o IP e garantir que o host est\u00e1 conectado ao Management Server do CloudStack.

Forne\u00e7a o hostname ou o endere\u00e7o IP do host, o nome de usu\u00e1rio (geralmente root) e a senha e qualquer label que voc\u00ea utiliza para categorizar os hosts.", +"message.desc.import.ext.kvm.wizard": "Importar dom\u00ednio libvirt de Host KVM Externo n\u00e3o gerenciado pelo CloudStack", +"message.desc.import.local.kvm.wizard": "Importar imagem QCOW2 do Armazenamento Local do Host KVM selecionado", +"message.desc.import.shared.kvm.wizard": "Importar imagem QCOW2 do Pool de Armazenamento Prim\u00e1rio selecionado", +"message.desc.import.unmanage.volume": "Por favor, escolha um pool de armazenamento do qual voc\u00ea deseja importar ou parar de gerenciar volumes. O pool de armazenamento deve estar no status Up.
Este recurso suporta apenas KVM.", "message.desc.importexportinstancewizard": "Testa caracter\u00edstica s\u00f3 se aplica aos clusters Cloudstack VMware. Ao optar por gerenciar uma inst\u00e2ncia, o CloudStack assume a orquestra\u00e7\u00e3o dessa inst\u00e2ncia. A inst\u00e2ncia \u00e9 deixada em funcionamento e n\u00e3o movida fisicamente. Desagrupar inst\u00e2ncias, remove a capacidade do CloudStack de gerenci\u00e1-las (mas elas s\u00e3o deixadas em funcionamento e n\u00e3o s\u00e3o destru\u00eddas).", +"message.desc.importingestinstancewizard": "Este recurso aplica-se apenas a inst\u00e2ncias KVM baseadas em libvirt. Apenas inst\u00e2ncias Paradas podem ser ingeridas", +"message.desc.importmigratefromvmwarewizard": "Ao selecionar um Datacenter VMware existente ou externo e uma inst\u00e2ncia para importar, o CloudStack migra a inst\u00e2ncia selecionada do VMware para KVM em um host de convers\u00e3o usando virt-v2v e a importa para um cluster KVM", "message.desc.primary.storage": "Cada cluster deve conter um ou mais servidores de armazenamento prim\u00e1rio e iremos adicionar o primeiro agora. Um armazenamento prim\u00e1rio, cont\u00e9m os volumes de disco para todas as VMs em execu\u00e7\u00e3o nos hosts do cluster. utiliza qualquer protocolo compat\u00edvel com os padr\u00f5es que \u00e9 suportado pelo virtualizador utilizado.", +"message.desc.register.user.data": "Por favor, preencha os seguintes dados para registrar um dado de usu\u00e1rio.", +"message.desc.registered.user.data": "Dados do Usu\u00e1rio Registrados.", "message.desc.reset.ssh.key.pair": "Por favor, especifique um par de chaves SSH que voc\u00ea deseja adicionar a esta VM.", "message.desc.secondary.storage": "Cada zona deve ter pelo menos um NFS ou servidor de armazenamento secund\u00e1rio e iremos adicionar o primeiro agora. Um armazenamento secund\u00e1rios armazena templates de VM, imagens ISO e snapshots do volume de disco da VM. Esse servidor deve estar dispon\u00edvel para todos os hosts na zona.

Fornecer o endere\u00e7o IP e o caminho exportados.", +"message.desc.zone.edge": "Uma zona \u00e9 a maior unidade organizacional no CloudStack e, normalmente, corresponde a um \u00fanico datacenter. Zonas fornecem isolamento f\u00edsico e redund\u00e2ncia. Uma zona de borda consiste em um ou mais hosts (cada um dos quais fornece armazenamento local como servidores de armazenamento prim\u00e1rio). Apenas redes compartilhadas e L2 podem ser implantadas nessas zonas e funcionalidades que requerem armazenamentos secund\u00e1rios n\u00e3o s\u00e3o suportadas.", "message.desc.zone": "Uma zona \u00e9 a maior unidade organizacional no CloudStack e normalmente corresponde \u00e0 um \u00fanico datacenter. As zonas disponibilizam isolamento f\u00edsico e redund\u00e2ncia. Uma zona \u00e9 composta por um ou mais pods (cada um dos quais cont\u00e9m os hosts e servidores de armazenamento prim\u00e1rio) e um servidor de armazenamento secund\u00e1rio que \u00e9 compartilhado por todos os pods na zona.", "message.detach.disk": "Voc\u00ea tem certeza que deseja desconectar este disco?", "message.detach.iso.confirm": "Confirme se voc\u00ea deseja desconectar o ISO da inst\u00e2ncia virtual.", "message.disable.account": "Por favor confirme que voc\u00ea deseja desabilitar esta conta. Ap\u00f3s desabilitar uma conta, todos os usu\u00e1rios desta conta n\u00e3o ir\u00e3o possuir mais acesso aos seus recursos da cloud. Todas as m\u00e1quinas virtuais ser\u00e3o automaticamente desligadas.", +"message.disable.role": "Por favor, confirme que voc\u00ea gostaria de desativar esta Fun\u00e7\u00e3o", "message.disable.user": "Por favor confirme que voc\u00ea deseja desabilitar este usu\u00e1rio.", "message.disable.vpn": "Voc\u00ea tem certeza que deseja desabilitar a VPN?", "message.disable.vpn.failed": "Falha ao desabilitar VPN", "message.disable.vpn.processing": "Desabilitando VPN...", +"message.disable.webhook.ssl.verification": "Desativar a verifica\u00e7\u00e3o SSL n\u00e3o \u00e9 recomendado", "message.discovering.feature": "Descobrindo funcionalidades, por favor aguarde...", "message.disk.offering.created": "Oferta de disco criada:", "message.disk.usage.info.data.points": "Cada ponto no gr\u00e1fico representa a diferen\u00e7a de dados lidos/escritos desde a \u00faltima coleta de estat\u00edstica realizada (o ponto anterior)", "message.disk.usage.info.sum.of.disks": "O uso de disco apresentado \u00e9 composto pela soma de dados lidos/escritos por todos os discos da VM", "message.download.volume": "Clique 00000 para baixar o disco", "message.download.volume.confirm": "Por favor confirme que voc\u00ea quer baixar este volume", +"message.drs.plan.description": "O n\u00famero m\u00e1ximo de migra\u00e7\u00f5es ao vivo permitidas para DRS. Configure DRS na aba de configura\u00e7\u00f5es antes de gerar um plano ou para ativar o DRS autom\u00e1tico para o cluster.", +"message.drs.plan.executed": "Plano DRS executado com sucesso.", "message.edit.acl.failed": "Falha ao editar regra ACL", "message.edit.acl.processing": "Editando regra ACL...", "message.edit.rule.failed": "Falha ao editar regra", "message.edit.rule.processing": "Atualizando regra...", "message.edit.traffic.type": "Favor especificar a etiqueta de tr\u00e1fego que voc\u00ea deseja associar com este tipo de tr\u00e1fego.", +"message.egress.rules.allow": "Permitir (o tr\u00e1fego correspondente \u00e0s regras de sa\u00edda adicionadas ser\u00e1 negado)", +"message.egress.rules.deny": "Negar (o tr\u00e1fego correspondente \u00e0s regras de sa\u00edda adicionadas ser\u00e1 permitido)", +"message.egress.rules.info.for.network": "A pol\u00edtica de sa\u00edda padr\u00e3o desta Rede \u00e9 %x.
O tr\u00e1fego de sa\u00edda correspondente \u00e0s seguintes regras de sa\u00edda ser\u00e1 %y", "message.enable.account": "Confirme se voc\u00ea deseja ativar a conta.", "message.enable.netsacler.provider.failed": "Falha ao habilitar provedor Netscaler", +"message.enable.role": "Por favor, confirme que voc\u00ea deseja ativar esta Fun\u00e7\u00e3o", "message.enable.securitygroup.provider.failed": "Falha ao habilitar provedor do grupo de seguran\u00e7a", "message.enable.user": "Por favor confirme que voc\u00ea deseja habilitar este usu\u00e1rio.", "message.enable.vpn": "Por favor confirme que voc\u00ea deseja acesso VPN habilitado para este endere\u00e7o IP.", @@ -2119,23 +3074,46 @@ "message.enter.valid.nic.ip": "Por favor insira um endere\u00e7o IP valido para o NIC", "message.error.access.key": "Por favor insira a chave de acesso", "message.error.add.guest.network": "Os campos IPv4 ou IPv6 precisam ser preenchidos ao adicionar uma rede guest", +"message.error.delete.asnrange": "Excluindo Faixa AS", +"message.error.add.interface.static.route": "Adicionar Rota Est\u00e1tica de interface falhou", +"message.error.add.logical.router": "Adicionar Roteador L\u00f3gico falhou", +"message.error.add.network.static.route": "Adicionar Rota Est\u00e1tica de Rede falhou", +"message.error.add.policy.rule": "Adicionar regra de Pol\u00edtica falhou", "message.error.add.secondary.ipaddress": "Houve um erro ao adicionar o endere\u00e7o IP secund\u00e1rio", +"message.error.add.tungsten.router.table": "Adicionar Tabela de Roteador falhou", +"message.error.add.tungsten.routing.policy": "Adicionar Pol\u00edtica de Roteamento Tungsten-Fabric falhou", "message.error.agent.password": "Por favor, insira a senha do agente", -"message.error.agent.username": "Por favor, insira o usu\u00e1rio do agent", +"message.error.agent.username": "Por favor, insira o usu\u00e1rio do agente", +"message.error.apply.network.policy": "Aplica\u00e7\u00e3o da Pol\u00edtica de Rede falhou", +"message.error.apply.tungsten.tag": "Aplica\u00e7\u00e3o de Tag falhou", +"message.error.authentication.code": "Por favor, insira o c\u00f3digo de autentica\u00e7\u00e3o.", "message.error.binaries.iso.url": "Por favor, digite a URL ISO dos bin\u00e1rios", "message.error.bucket": "Por favor, insira o bucket", +"message.error.cidr": "CIDR \u00e9 obrigat\u00f3rio", +"message.error.cidr.or.cidrsize": "CIDR ou tamanho do cidr \u00e9 obrigat\u00f3rio", "message.error.cloudian.console": "Single-Sign-On falhou para o Cloudian management console. Solicite a seu administrador que conserte problemas de integra\u00e7\u00e3o.", "message.error.cluster.description": "Por favor, digite a descri\u00e7\u00e3o do Kubernetes cluster", "message.error.cluster.name": "Por favor, insira o nome do cluster", "message.error.confirm.password": "Por favor, confirme a nova senha", +"message.error.confirm.text": "Por favor, insira o texto de confirma\u00e7\u00e3o", +"message.error.create.webhook.local.account": "Conta deve ser fornecida para criar um Webhook com escopo Local.", +"message.error.create.webhook.name": "Nome deve ser fornecido para criar um Webhook.", +"message.error.create.webhook.payloadurl": "URL de Payload deve ser fornecida para criar um Webhook.", "message.error.current.password": "Por favor, insira a senha atual", "message.error.custom.disk.size": "Por favor, insira o tamanho do disco personalizado", "message.error.date": "Por favor, insira uma data", +"message.error.delete.interface.static.route": "Remo\u00e7\u00e3o de Rota Est\u00e1tica de interface falhou", +"message.error.delete.network.static.route": "Remo\u00e7\u00e3o de Rota Est\u00e1tica de Rede falhou", +"message.error.delete.tungsten.policy.rule": "Exclus\u00e3o da regra de Pol\u00edtica falhou", +"message.error.delete.tungsten.router.table": "Remo\u00e7\u00e3o da Tabela de Roteador falhou", +"message.error.delete.tungsten.tag": "Remo\u00e7\u00e3o da Tag falhou", "message.error.description": "Por favor, insira uma descri\u00e7\u00e3o", "message.error.discovering.feature": "Exce\u00e7\u00e3o lan\u00e7ada durante a descoberta de funcionalidades", "message.error.display.text": "Por favor, insira o texto de exibi\u00e7\u00e3o", +"message.error.duration.less.than.interval": "A dura\u00e7\u00e3o na pol\u00edtica de AutoScale n\u00e3o pode ser menor que o intervalo", "message.error.enable.saml": "Incapaz de encontrar IDs de usu\u00e1rios para ativar o SAML Single Sign On, por favor, ative-o manualmente.", "message.error.end.date.and.time": "Por favor, selecione a data e hor\u00e1rio final.", +"message.error.endasn": "Por favor, insira a Faixa AS final", "message.error.endip": "Por favor, insira o IP final", "message.error.gateway": "Por favor, insira o gateway", "message.error.host.name": "Por favor, insira o nome do host", @@ -2148,9 +3126,11 @@ "message.error.internallb.instance.port": "Por favor, especifique a porta da inst\u00e2ncia", "message.error.internallb.name": "Por favor, especifique um nome para o LB interno", "message.error.internallb.source.port": "Por favor, insira uma porta de origem", +"message.error.invalid.autoscale.vmgroup.name": "Nome do Grupo de AutoScale inv\u00e1lido. Ele pode conter as letras ASCII 'a' a 'z', 'A' a 'Z', os d\u00edgitos '0' a '9' e o h\u00edfen ('-'), deve ter entre 1 e 255 caracteres.", "message.error.ip.range": "Por favor, insira um intervalo v\u00e1lido", "message.error.ipv4.address": "Por favor, insira um endere\u00e7o IPv4 v\u00e1lido", "message.error.ipv4.dns1": "Por favor, insira o DNS 1 IPv4", +"message.error.ipv4.dns2": "Por favor, insira o DNS 2 IPv4", "message.error.ipv6.address": "Por favor, insira um endere\u00e7o IPv6 v\u00e1lido.", "message.error.ipv6.gateway": "Por favor, insira o gateway IpV6", "message.error.ipv6.gateway.format": "Por favor, insira um gateway IpV6 v\u00e1lido.", @@ -2160,6 +3140,10 @@ "message.error.loading.setting": "Houve um erro ao carregar estas configura\u00e7\u00f5es.", "message.error.lun": "Por favor, insira o # LUN", "message.error.macaddress": "Por favor, digite um endere\u00e7o MAC v\u00e1lido.", +"message.error.max.members.less.than.min.members": "O valor de M\u00e1x membros deve ser maior que o valor de M\u00edn membros", +"message.error.mtu.below.min": "MTU est\u00e1 abaixo do valor m\u00ednimo suportado de %x", +"message.error.mtu.private.max.exceed": "O valor inserido excede o MTU privado m\u00e1ximo permitido para esta Zona, seu valor ser\u00e1 automaticamente reduzido para corresponder a ele.", +"message.error.mtu.public.max.exceed": "O valor inserido excede o MTU p\u00fablico m\u00e1ximo permitido para esta Zona, seu valor ser\u00e1 automaticamente reduzido para corresponder a ele.", "message.error.name": "Por favor, insira um nome", "message.error.netmask": "Por favor, insira am\u00e1scara de rede", "message.error.network.offering": "Por favor, selecione a oferta de rede", @@ -2168,6 +3152,7 @@ "message.error.nexus1000v.password": "Por favor, insira a senha do Nexus 1000v", "message.error.nexus1000v.username": "Por favor, insira o usu\u00e1rio do Nexus 1000v", "message.error.number": "Por favor, insira um n\u00famero v\u00e1lido", +"message.error.parent.subnet": "Por favor, escolha uma sub-rede pai", "message.error.password": "Insira a sua senha", "message.error.path": "Por favor, insira o path", "message.error.provide.setting": "Deve fornecer uma chave v\u00e1lida e um valor para a configura\u00e7\u00e3o", @@ -2175,10 +3160,16 @@ "message.error.rados.pool": "Por favor, digite o RADOS pool", "message.error.rados.secret": "Por favor, digite o RADOS secret", "message.error.rados.user": "Por favor, digite o usu\u00e1rio RADOS", +"message.error.remove.logical.router": "Remo\u00e7\u00e3o do Roteador L\u00f3gico falhou", +"message.error.remove.network.policy": "Remo\u00e7\u00e3o da Pol\u00edtica de Rede falhou", "message.error.remove.nic": "Houve um erro", "message.error.remove.secondary.ipaddress": "Houve um erro ao remover o endere\u00e7o IP secund\u00e1rio", +"message.error.remove.tungsten.routing.policy": "Remo\u00e7\u00e3o da Pol\u00edtica de Roteamento Tungsten-Fabric da Rede falhou", +"message.error.remove.vm.schedule": "Remo\u00e7\u00e3o do Agendamento de Inst\u00e2ncia falhou", "message.error.required.input": "Por favor, digite os dados", +"message.error.reset.config": "Incapaz de redefinir configura\u00e7\u00e3o para o valor padr\u00e3o", "message.error.retrieve.kubeconfig": "N\u00e3o foi poss\u00edvel recuperar a configura\u00e7\u00e3o do cluster Kubernetes", +"message.error.routing.policy.term": "Comunidade precisa ter o seguinte formato n\u00famero:n\u00famero", "message.error.s3nfs.path": "Por favor, insira o S3 NFS path", "message.error.s3nfs.server": "Por favor, insira o servidor S3 NFS", "message.error.save.setting": "Houve um erro ao salvar esta configura\u00e7\u00e3o.", @@ -2188,18 +3179,28 @@ "message.error.secret.key": "Por favor, insira a chave secreta", "message.error.select": "Por favor, escolha uma op\u00e7\u00e3o", "message.error.select.domain.to.dedicate": "Por favor, escolha o dom\u00ednio a ser dedicado", +"message.error.select.load.balancer": "Por favor, selecione um balanceador de carga", +"message.error.select.network": "Por favor, selecione uma Rede", +"message.error.select.network.supports.vm.autoscaling": "A Rede padr\u00e3o que voc\u00ea selecionou n\u00e3o suporta AutoScale de Inst\u00e2ncia, por favor selecione uma Rede padr\u00e3o que suporte AutoScale de Inst\u00e2ncia.", +"message.error.select.user": "Por favor, selecione um Usu\u00e1rio", "message.error.select.zone.type": "Por favor, escolha o tipo de zona abaixo.", "message.error.server": "Por favor, insira o servidor", "message.error.serviceoffering.for.cluster": "Por favor, escolha a oferta de servi\u00e7o para o cluster Kubernetes", +"message.error.setup.2fa": "Falha na configura\u00e7\u00e3o 2FA ao verificar o c\u00f3digo, por favor tente novamente.", "message.error.size": "Por favor, insira o tamanho em GB", "message.error.size.for.cluster": "Por favor, insira o tamanho do cluster Kubernetes", "message.error.smb.password": "Por favor, insira a senha do SMB ", "message.error.smb.username": "Por favor, insira o usu\u00e1rio do SMB", +"message.error.specify.stickiness.method": "Por favor, especifique um m\u00e9todo de stickiness", "message.error.specify.sticky.name": "Por favor, especifique o nome sticky", "message.error.sr.namelabel": "Por favor, insira o nome-R\u00f3tulo SR", "message.error.start.date.and.time": "Por favor, selecione a data e hor\u00e1rio inicial.", +"message.error.startasn": "Por favor, insira a Faixa AS inicial", "message.error.startip": "Por favor, insira o IP de \u00cdnicio", "message.error.storage.tags": "Por favor, insira as tags de armazenamento", +"message.error.swift.account": "Por favor, insira a Conta", +"message.error.swift.key": "Por favor, insira a chave", +"message.error.swift.username": "Por favor, insira o nome de usu\u00e1rio", "message.error.target.iqn": "Por favor, insira o Target IQN", "message.error.time": "Por favor, selecione a hora", "message.error.traffic.label": "Por favor, insira o r\u00f3tulo de tr\u00e1fego", @@ -2208,6 +3209,7 @@ "message.error.upload.template": "Falha no carregamento de template", "message.error.upload.template.description": "Apenas um template pode ser carregado de cada vez", "message.error.url": "Por favor, insira a URL", +"message.error.userdata": "Por favor, insira o Userdata", "message.error.username": "Insira o seu usu\u00e1rio", "message.error.valid.iops.range": "Por favor, insira uma faixa IOPS v\u00e1lida", "message.error.vcenter.datacenter": "Por favor, insira o vCenter datacenter", @@ -2215,6 +3217,7 @@ "message.error.vcenter.host": "Por favor, insira o vCenter host", "message.error.vcenter.password": "Por favor, insira a senha vCenter", "message.error.vcenter.username": "Por favor, insira o usu\u00e1rio vCenter", +"message.error.verifying.2fa": "Incapaz de verificar 2FA, por favor tente novamente.", "message.error.version.for.cluster": "Por favor, selecionar a vers\u00e3o Kubernetes para o cluster Kubernetes", "message.error.vlan.range": "Por favor, insira um intervalo VLAN/VNI v\u00e1lido", "message.error.volume.name": "Por favor, insira o nome do volume", @@ -2231,14 +3234,41 @@ "message.failed.to.add": "Falha ao adicionar", "message.failed.to.assign.vms": "Falha ao atribuir VMs", "message.failed.to.remove": "Falha ao remover", +"message.forgot.password.success": "Um e-mail foi enviado para o seu endere\u00e7o de e-mail com instru\u00e7\u00f5es sobre como redefinir sua senha.", "message.generate.keys": "Por favor confirme que voc\u00ea deseja gerar novas chaves para este usu\u00e1rio.", "message.chart.statistic.info": "Os gr\u00E1ficos mostrados s\u00E3o autoajust\u00E1veis, ou seja, se o valor se aproximar ou passar do limite, ele crescer\u00E1 para ajustar o valor mostrado", +"message.chart.statistic.info.hypervisor.additionals": "Os dados de m\u00e9tricas dependem do plugin de hypervisor usado para cada hypervisor. O comportamento pode variar entre diferentes hypervisors. Por exemplo, com KVM, as m\u00e9tricas s\u00e3o estat\u00edsticas em tempo real fornecidas pelo libvirt. Em contraste, com VMware, as m\u00e9tricas s\u00e3o dados m\u00e9dios para um determinado intervalo de tempo controlado pela configura\u00e7\u00e3o.", "message.guest.traffic.in.advanced.zone": "O tr\u00e1fego de rede guest \u00e9 para comunica\u00e7\u00e3o entre m\u00e1quinas virtuais do usu\u00e1rio final. Especifique um intervalo de IDs de VLAN para transportar o tr\u00e1fego do guest para cada rede f\u00edsica.", "message.guest.traffic.in.basic.zone": "O tr\u00e1fego de rede guest \u00e9 para comunica\u00e7\u00e3o entre m\u00e1quinas virtuais do usu\u00e1rio final. Especifique um intervalo de endere\u00e7os IP para que CloudStack possa atribuir \u00e0s VMs. Certifique-se que este intervalo n\u00e3o se sobreponha o range de IPs reservados do sistema.", +"message.host.controlstate": "O Status do Recurso de Computa\u00e7\u00e3o para esta Inst\u00e2ncia \u00e9 ", +"message.host.controlstate.retry": "Algumas a\u00e7\u00f5es nesta Inst\u00e2ncia falhar\u00e3o, se for o caso, por favor aguarde um pouco e tente novamente.", "message.host.dedicated": "Host dedicado", "message.host.dedication.released": "Host dedicado liberado", "message.info.cloudian.console": "O Cloudian management console deve abrir em outra janela", -"message.installwizard.copy.whatiscloudstack": "O CloudStack™ \u00e9 uma plataforma de software que agrega recursos computacionais para construir uma Cloud de infraestrutura como servi\u00e7o (IaaS) p\u00fablica, privada ou h\u00edbrida. O CloudStack™ ger\u00eancia a rede, o armazenamento e os recursos computacionais que comp\u00f5em a infraestrutura de cloud. utilize o CloudStack™ para instalar, gerenciar e configurar os ambientes de cloud computing.

Indo al\u00e9m de imagens de m\u00e1quinas virtuais individuais rodando em hardware commodity, CloudStack™ prov\u00ea uma solu\u00e7\u00e3o completa de software de infraestrutura de cloud para entregar datacenters virtuais como um servi\u00e7o - possuindo todos os componentes essenciais para contruir, instalar e gerenciar aplica\u00e7\u00f5es na cloud multi-camadas e multi-tenant.", +"message.infra.setup.nsx.description": "Esta zona deve conter um provedor NSX porque o m\u00e9todo de isolamento \u00e9 NSX", +"message.infra.setup.tungsten.description": "Esta zona deve conter um provedor Tungsten-Fabric porque o m\u00e9todo de isolamento \u00e9 TF", +"message.import.running.instance.warning": "A VM selecionada est\u00e1 ligada no Datacenter VMware. O estado recomendado para converter uma VM VMware em KVM \u00e9 desligada ap\u00f3s um desligamento gracioso do SO convidado.", +"message.import.volume": "Por favor, especifique o dom\u00ednio, conta ou nome do projeto.
Se n\u00e3o definido, o volume ser\u00e1 importado para o solicitante.", +"message.installwizard.cloudstack.helptext.document": " * Documenta\u00e7\u00e3o:\t ", +"message.installwizard.cloudstack.helptext.header": "\nVoc\u00ea pode encontrar mais informa\u00e7\u00f5es sobre o Apache CloudStack™ nas p\u00e1ginas listadas abaixo.\n", +"message.installwizard.cloudstack.helptext.issues": " * Relatar problemas:\t ", +"message.installwizard.cloudstack.helptext.mailinglists": " * Juntar-se a listas de discuss\u00e3o:\t ", +"message.installwizard.cloudstack.helptext.releasenotes": " * Notas de lan\u00e7amento:\t ", +"message.installwizard.cloudstack.helptext.survey": " * Participe da pesquisa:\t ", +"message.installwizard.cloudstack.helptext.website": " * Site do projeto:\t ", +"message.installwizard.tooltip.nsx.provider.edgecluster": "Informa\u00e7\u00f5es do cluster de borda do Provedor NSX n\u00e3o fornecidas", +"message.installwizard.tooltip.nsx.provider.hostname": "Hostname / endere\u00e7o IP do Provedor NSX n\u00e3o fornecido", +"message.installwizard.tooltip.nsx.provider.password": "Senha do Provedor NSX n\u00e3o fornecida", +"message.installwizard.tooltip.nsx.provider.tier0gateway": "Informa\u00e7\u00f5es do gateway tier-0 do Provedor NSX n\u00e3o fornecidas", +"message.installwizard.tooltip.nsx.provider.transportZone": "Informa\u00e7\u00f5es da zona de transporte do Provedor NSX n\u00e3o fornecidas", +"message.installwizard.tooltip.nsx.provider.username": "Nome de usu\u00e1rio do Provedor NSX n\u00e3o fornecido", +"message.installwizard.tooltip.tungsten.provider.gateway": "Gateway do provedor Tungsten \u00e9 obrigat\u00f3rio", +"message.installwizard.tooltip.tungsten.provider.hostname": "Hostname do provedor Tungsten \u00e9 obrigat\u00f3rio", +"message.installwizard.tooltip.tungsten.provider.introspectport": "Porta de introspec\u00e7\u00e3o do provedor Tungsten \u00e9 obrigat\u00f3ria", +"message.installwizard.tooltip.tungsten.provider.name": "Nome do provedor Tungsten \u00e9 obrigat\u00f3rio", +"message.installwizard.tooltip.tungsten.provider.port": "Porta do provedor Tungsten \u00e9 obrigat\u00f3ria", +"message.installwizard.tooltip.tungsten.provider.vrouterport": "Porta do vrouter do provedor Tungsten \u00e9 obrigat\u00f3ria", +"message.installwizard.copy.whatiscloudstack": "O CloudStack™ \u00e9 uma plataforma de software que agrega recursos computacionais para construir uma Cloud de infraestrutura como servi\u00e7o (IaaS) p\u00fablica, privada ou h\u00edbrida. O CloudStack™ ger\u00eancia a rede, o armazenamento e os recursos computacionais que comp\u00f5em a infraestrutura de cloud. utilize o CloudStack™ para instalar, gerenciar e configurar os ambientes de cloud computing.

Indo al\u00e9m de imagens de m\u00e1quinas virtuais individuais rodando em hardware commodity, CloudStack™ prov\u00ea uma solu\u00e7\u00e3o completa de software de infraestrutura de cloud para entregar datacenters virtuais como um servi\u00e7o - possuindo todos os componentes essenciais para contruir, instalar e gerenciar aplica\u00e7\u00f5es na cloud multi-camadas e multi-tenant. Ambas as vers\u00f5es open-source e premium est\u00e3o dispon\u00edveis, com a vers\u00e3o opensource oferecendo praticamente os mesmos recursos.", "message.installwizard.tooltip.addpod.name": "O nome para o pod", "message.installwizard.tooltip.addpod.reservedsystemendip": "Este \u00e9 o range de IP na rede privada que o CloudStack utiliza para gerenciar o armazenamento secund\u00e1rio das VMs e proxy console das VMs. estes endere\u00e7os IP s\u00e3o obtidos da mesma subrede dos servidores hosts.", "message.installwizard.tooltip.addpod.reservedsystemgateway": "O gateway para os hosts neste pod.", @@ -2249,8 +3279,12 @@ "message.installwizard.tooltip.configureguesttraffic.gueststartip": "O range de endere\u00e7os IP que estar\u00e1 dispon\u00edvel para aloca\u00e7\u00e3o para os guests nesta zona. Caso uma interface de rede seja utilizada, estes IPs devem estar no mesmo CIDR que o CIDR do pod.", "message.instances.managed": "Inst\u00e2ncias ou VMs controladas pelo CloudStack", "message.instances.unmanaged": "Inst\u00e2ncias ou VMs que n\u00e3o s\u00e3o controladas pelo CloudStack", +"message.instances.migrate.vmware": "Inst\u00e2ncias que podem ser migradas do VMware.", "message.interloadbalance.not.return.elementid": "erro: A API listInternalLoadBalancerElements API n\u00e3o retorna o ID interno do elemento LB", +"message.ip.address": "Endere\u00e7o IP : ", "message.ip.address.changes.effect.after.vm.restart": "As mudan\u00e7as de endere\u00e7o IP entram em vigor somente ap\u00f3s o rein\u00edcio da VM.", +"message.ip.v6.prefix.delete": "Prefixo IPv6 exclu\u00eddo", +"message.iso.arch": "Por favor, selecione uma arquitetura ISO", "message.iso.desc": "Imagem de disco contendo dados ou m\u00eddia de sistema operacional boot\u00e1vel", "message.kubeconfig.cluster.not.available": "kubeconfig do cluster Kubernetes n\u00e3o est\u00e1 dispon\u00edvel no momento", "message.kubernetes.cluster.delete": "Por favor, confirme que voc\u00ea quer destruir o cluster", @@ -2259,25 +3293,43 @@ "message.kubernetes.cluster.stop": "Por favor, confirme que voc\u00ea deseja parar o cluster", "message.kubernetes.cluster.upgrade": "Por favor, selecione a nova vers\u00e3o do Kubernetes", "message.kubernetes.version.delete": "Por favor, confirme que voc\u00ea deseja excluir esta vers\u00e3o do Kubernetes", +"message.l2.network.unsupported.for.nsx": "Redes L2 n\u00e3o s\u00e3o suportadas para zonas habilitadas para NSX", "message.launch.zone": "A zona est\u00e1 pronta para ser executada; por favor, v\u00e1 para o pr\u00f3ximo passo.", "message.launch.zone.description": "A zona est\u00e1 pronta para iniciar; por favor, prossiga para o pr\u00f3ximo passo.", "message.launch.zone.hint": "Configurar componentes e tr\u00e1fego de rede, incluindo endere\u00e7os IP.", "message.license.agreements.not.accepted": "Contratos de licen\u00e7a n\u00e3o foram aceitos", "message.listnsp.not.return.providerid": "erro: A API listNetworkServiceProviders n\u00e3o retorna o ID do provedor virtualRouter", "message.load.host.failed": "Falha ao carregar os hosts", +"message.loadbalancer.stickypolicy.configuration": "Personalize a pol\u00edtica de stickiness do balanceador de carga:", +"message.loading.add.interface.static.route": "Adicionando Rota Est\u00e1tica de interface...", +"message.loading.add.network.static.route": "Adicionando Rota Est\u00e1tica de Rede...", +"message.loading.add.policy.rule": "Adicionando regra de Pol\u00edtica...", +"message.loading.add.tungsten.router.table": "Adicionando Tabela de Roteador...", +"message.loading.apply.tungsten.tag": "Aplicando Tag...", +"message.loading.delete.interface.static.route": "Removendo Rota Est\u00e1tica de interface...", +"message.loading.delete.network.static.route": "Removendo Rota Est\u00e1tica de Rede...", +"message.loading.delete.tungsten.policy.rule": "Excluindo regra de Pol\u00edtica...", +"message.loading.delete.tungsten.router.table": "Removendo Tabela de Roteador...", +"message.loading.delete.tungsten.tag": "Removendo Tag...", "message.lock.account": "Confirme se voc\u00ea deseja bloquear esta conta. Bloqueando a conta, todos os usu\u00e1rios desta conta n\u00e3o estar\u00e3o mais habilitados a gerenciar os recursos na nuvem. Os recursos existentes (cloud server) ainda poder\u00e3o ser acessados.", "message.lock.user": "Confirme se voc\u00ea deseja bloquear o usu\u00e1rio \"{user}\". Bloqueando este usu\u00e1rio, o mesmo n\u00e3o estar\u00e1 mais habilitado a gerenciar os recursos na nuvem. Os recursos existentes (cloud server) ainda poder\u00e3o ser acessados.", "message.lock.user.success": "Usu\u00e1rio \"{user}\" bloqueado com sucesso", "message.login.failed": "Falha no login", +"message.list.zone.vmware.datacenter.empty": "Nenhum Datacenter VMware existe na Zona selecionada", "message.memory.usage.info.hypervisor.additionals": "Os dados apresentados podem n\u00e3o refletir o real uso de mem\u00f3ria se a VM n\u00e3o possuir as ferramentas adicionais do virtualizador instaladas", "message.memory.usage.info.negative.value": "Se n\u00e3o for poss\u00edvel obter do hypervisor o uso de mem\u00f3ria da VM, ser\u00e3o desabilitadas as linhas de mem\u00f3ria livre do gr\u00e1fico de dados brutos e de uso de mem\u00f3ria no gr\u00e1fico de percentual", +"message.migrate.instance.host.auto.assign": "O Host para a Inst\u00e2ncia ser\u00e1 escolhido automaticamente com base na adequa\u00e7\u00e3o dentro do mesmo cluster", "message.migrate.instance.to.host": "Por favor confirme que voc\u00ea deseja migrar a inst\u00e2ncia para outro host.", "message.migrate.instance.to.ps": "Por favor confirme que voc\u00ea deseja migrar a inst\u00e2ncia para outro armazenamento prim\u00e1rio.", +"message.migrate.resource.to.ss": "Por favor, confirme que voc\u00ea deseja migrar este recurso para outro armazenamento secund\u00e1rio.", "message.migrate.router.confirm": "Por favor confirme o host que voc\u00ea deseja migrar o roteador para:", "message.migrate.systemvm.confirm": "Por favor confirme o host para o qual voc\u00ea deseja migrar a VM de sistema:", "message.migrate.volume": "Por favor confirme que voc\u00ea deseja migrar o volume \"{volume}\" do armazenamento prim\u00e1rio \"{storage}\" para outro.", "message.migrate.volume.failed": "Falha ao migrar volume", +"message.migrate.volume.pool.auto.assign": "O armazenamento prim\u00e1rio para o volume ser\u00e1 escolhido automaticamente com base na adequa\u00e7\u00e3o e no destino da Inst\u00e2ncia", "message.migrate.volume.processing": "Migrando volume...", +"message.migrate.volume.tooltip": "O volume pode ser migrado para qualquer pool de armazenamento adequado. O administrador deve escolher a oferta de disco apropriada para substituir, que suporte o novo pool de armazenamento", +"message.migrate.with.storage": "Especifique o pool de armazenamento para volumes da Inst\u00e2ncia.", "message.migrating.failed": "Falha na migra\u00e7\u00e3o", "message.migrating.processing": "Migra\u00e7\u00e3o em andamento para", "message.migrating.vm.to.storage.failed": "Falha na migra\u00e7\u00e3o da VM para o armazenamento", @@ -2289,7 +3341,7 @@ "message.network.addvm.desc": "Por favor especifique a rede onde voc\u00ea gostaria de adicionar esta VM. Uma nova NIC ser\u00e1 adicionada a esta rede.", "message.network.description": "Configura\u00e7\u00e3o da rede e do tr\u00e1fego", "message.network.error": "Erro de rede", -"message.network.error.description": "N\u00e3o \u00e9 poss\u00edvel acessar o servidor de gerenciamento ou uma extens\u00e3o do navegador pode estar bloqueando a solicita\u00e7\u00e3o de rede.", +"message.network.error.description": "N\u00e3o \u00e9 poss\u00edvel acessar o Management Server ou uma extens\u00e3o do navegador pode estar bloqueando a solicita\u00e7\u00e3o de rede.", "message.network.hint": "Configure componentes de rede e tr\u00e1fego p\u00fablico/guest/gerenciamento, incluindo endere\u00e7os IP.", "message.network.offering.change.warning": "AVISO: Alterar a oferta causar\u00e1 tempo de inatividade de conectividade para as VMs com NICs na rede.", "message.network.offering.forged.transmits": "O switch deixa qualquer frame de sa\u00edda de um adaptador de m\u00e1quina virtual com um endere\u00e7o MAC de origem diferente daquele do arquivo de configura\u00e7\u00e3o .vmx.\nRejeitar - O switch deixa qualquer frame de sa\u00edda de um adaptador de m\u00e1quina virtual com um endere\u00e7o MAC de origem diferente daquele do arquivo de configura\u00e7\u00e3o .vmx.\nAceitar - O switch n\u00e3o realiza filtragem e permite todos os frames de sa\u00edda.\nNenhum - padr\u00e3o de valor a partir da configura\u00e7\u00e3o global.", @@ -2298,21 +3350,35 @@ "message.network.offering.mac.learning.warning": "AVISO: para usar o MAC Learning voc\u00ea deve assegurar-se de que os virtualizadores dos hosts est\u00e3o rodando ESXi 6.7+ e a rede usa o vSwitch 6.6.0+ distribu\u00eddo.", "message.network.offering.promiscuous.mode": "Se aplica apenas para redes guest no virtualizador VMware.\nRejeitar - O switch deixa qualquer frame de sa\u00edda de um adaptador de m\u00e1quina virtual com um endere\u00e7o MAC de origem diferente daquele do arquivo de configura\u00e7\u00e3o .vmx.\nAceitar - O switch n\u00e3o realiza filtragem e permite todos os frames de sa\u00edda.\nNenhum - padr\u00e3o de valor a partir da configura\u00e7\u00e3o global.", "message.network.removenic": "Por favor, confirme que deseja remover esta interface de rede, esta a\u00e7\u00e3o tamb\u00e9m ir\u00e1 remover a rede associada \u00e0 VM.", +"message.network.restart.required": "Reinicializa\u00e7\u00e3o \u00e9 necess\u00e1ria para rede(s). Clique aqui para ver rede(s) que requerem reinicializa\u00e7\u00e3o.", +"message.network.selection": "Escolha uma ou mais Redes para anexar a Inst\u00e2ncia.", +"message.network.selection.new.network": "Uma nova Rede tamb\u00e9m pode ser criada aqui.", "message.network.secondaryip": "Por favor, confirme que voc\u00ea gostaria de adquirir um novo IP secund\u00e1rio para este NIC. \n NOTA: Voc\u00ea precisa configurar manualmente o IP secund\u00e1rio rec\u00e9m-adquirido dentro da m\u00e1quina virtual.", "message.network.updateip": "Por favor, confirme que voc\u00ea gostaria de mudar o endere\u00e7o IP da NIC em VM.", "message.network.update.nic": "Por favor, confirme que voc\u00ea gostaria de atualizar esta NIC.", "message.network.usage.info.data.points": "Cada ponto no gr\u00e1fico representa a diferen\u00e7a de dados trafegados desde a \u00faltima coleta de estat\u00edstica realizada (o ponto anterior)", "message.network.usage.info.sum.of.vnics": "O uso de rede apresentado \u00e9 composto pela soma de dados trafegados por todas as vNICs da VM", +"message.new.version.available": "Uma nova vers\u00e3o do CloudStack est\u00e1 dispon\u00edvel. Clique aqui para verificar os detalhes", +"message.nfs.mount.options.description": "Lista separada por v\u00edrgulas de op\u00e7\u00f5es de montagem NFS para hosts KVM. Op\u00e7\u00f5es suportadas : vers=[3,4.0,4.1,4.2], nconnect=[1...16]", "message.no.data.to.show.for.period": "Nenhum dado para mostrar no per\u00edodo selecionado.", "message.no.description": "Nenhuma descri\u00e7\u00e3o inserida.", +"message.offering.internet.protocol.warning": "AVISO: Redes suportadas por IPv6 usam roteamento est\u00e1tico e exigir\u00e3o que rotas upstream sejam configuradas manualmente.", +"message.offering.ipv6.warning": "Por favor, consulte a documenta\u00e7\u00e3o para criar oferta de Rede/VPC habilitada para IPv6 Suporte IPv6 no CloudStack - Redes Isoladas e Camadas de VPC", "message.ovf.configurations": "H\u00e1 propriedades OVF dispon\u00edveis para a personaliza\u00e7\u00e3o do aparelho selecionado. Por favor, edite os valores de forma apropriada.As ofertas incompat\u00edveis de computa\u00e7\u00e3o ser\u00e3o desativadas.", +"message.password.reset.failed": "Falha ao redefinir senha.", +"message.password.reset.success": "A senha foi redefinida com sucesso. Por favor, fa\u00e7a login usando suas novas credenciais.", +"message.path": "Caminho : ", "message.path.description": "NFS: caminho exportado do servidor. VMFS: /datacenter name/datastore name. SharedMountPoint: caminho onde o armazenamento prim\u00e1rio \u00e9 montado, tal como /mnt/primary", "message.please.confirm.remove.ssh.key.pair": "Por favor, confirme que voc\u00ea deseja remover este par de chaves SSH", +"message.please.confirm.remove.user.data": "Por favor, confirme que voc\u00ea deseja remover este Userdata", "message.please.enter.valid.value": "Por favor, insira um valor v\u00e1lido", "message.please.enter.value": "Por favor, insira valores", +"message.please.wait.while.autoscale.vmgroup.is.being.created": "Por favor, aguarde enquanto seu Grupo de AutoScale est\u00e1 sendo criado; isso pode levar um tempo...", "message.please.wait.while.zone.is.being.created": "Por favor, espere enquanto sua zona est\u00e1 sendo criada; isto pode demorar um pouco...", "message.pod.dedicated": "Pod dedicado", "message.pod.dedication.released": "Pod dedicado liberado", +"message.prepare.for.shutdown": "Por favor, confirme que voc\u00ea gostaria de preparar este Management Server para desligamento. Ele n\u00e3o aceitar\u00e1 nenhum novo job Ass\u00edncrono, mas N\u00c3O terminar\u00e1 ap\u00f3s n\u00e3o haver jobs pendentes.", +"message.primary.storage.invalid.state": "Armazenamento prim\u00e1rio n\u00e3o est\u00e1 no estado Up", "message.processing.complete": "Processamento conclu\u00eddo!", "message.protocol.description": "Para XenServer, escolha NFS, iSCSI, ou PreSetup. para KVM, escolha NFS, SharedMountPoint, RDB, CLVM ou Gluster. para vSphere, escolha NFS, PreSetup (VMFS, iSCSI, fiberChannel, vSAN ou vVols) ou datastoreCluster. para Hyper-V, escolha SMB/CIFS. para LXC, escolha NFS ou SharedMountPoint. para OVM, escolha NFS ou ocfs2.", "message.public.traffic.in.advanced.zone": "O tr\u00e1fego p\u00fablico \u00e9 gerado quando as VMs na nuvem acessam a internet. Os IPs acess\u00edveis ao p\u00fablico devem ser alocados para essa finalidade. Os usu\u00e1rios finais podem usar a interface do usu\u00e1rio CloudStack para adquirir esses IPs afim de implementar NAT entre a sua rede de guests e sua rede p\u00fablica.

Forne\u00e7a pelo menos um intervalo de endere\u00e7os IP para o tr\u00e1fego de internet.", @@ -2340,6 +3406,11 @@ "message.remove.ldap": "Voc\u00ea tem certeza que deseja deletar a configura\u00e7\u00e3o LDAP?", "message.remove.nic.processing": "Removendo NIC...", "message.remove.port.forward.failed": "Falha ao remover regra de encaminhamento de porta", +"message.remove.router.table.from.interface": "Por favor, confirme que voc\u00ea deseja remover a Tabela de Roteamento desta NIC", +"message.remove.router.table.from.interface.failed": "Remo\u00e7\u00e3o da Tabela de Roteamento da interface falhou", +"message.remove.routing.firewall.rule.failed": "Falha ao remover regra de firewall de Roteamento IPv4", +"message.remove.routing.firewall.rule.processing": "Removendo regra de firewall de Roteamento IPv4...", +"message.remove.routing.firewall.rule.success": "Regra de firewall de Roteamento IPv4 removida", "message.remove.rule.failed": "Falha ao remover regra", "message.remove.secondary.ipaddress.processing": "Removendo endere\u00e7o IP secund\u00e1rio...", "message.remove.securitygroup.rule.processing": "Removendo regra do grupo de seguran\u00e7a...", @@ -2348,10 +3419,12 @@ "message.remove.vpc": "Favor confirmar que voc\u00ea deseja remover a VPC", "message.request.failed": "Falha na solicita\u00e7\u00e3o", "message.required.add.least.ip": "Por favor, adicionar pelo menos UM intervalo IP", +"message.required.tagged.physical.network": "S\u00f3 pode haver uma rede f\u00edsica n\u00e3o marcada com tipo de tr\u00e1fego convidado.", "message.required.traffic.type": "Erro na configura\u00e7\u00e3o! Todos os tipos de tr\u00e1fego necess\u00e1rios devem ser adicionados e com m\u00faltiplas redes f\u00edsicas cada rede deve ter uma etiqueta.", "message.reset.vpn.connection": "Favor confirmar que voc\u00ea deseja reinicializar a conex\u00e3o VPN", "message.linstor.resourcegroup.description": "Grupo de recursos Linstor a ser usado para armazenamento prim\u00e1rio", "message.resize.volume.failed": "Falha ao redimensionar volume", +"message.resize.volume.processing": "Redimensionamento de volume est\u00e1 em andamento", "message.resource.not.found": "Recurso n\u00e3o encontrado", "message.restart.mgmt.server": "Reinicie o(s) servidor(es) de gerenciamento para que a nova configura\u00c3\u00a7\u00c3\u00a3o tenha efeito.", "message.restart.network": "Por favor confirme que voc\ufffd deseja reiniciar a rede", @@ -2359,24 +3432,47 @@ "message.restart.vpc": "Favor confirmar que voc\u00ea deseja reiniciar a VPC", "message.restart.vpc.remark": "Por favor, confirme a reinicializa\u00e7\u00e3o do VPC

Observa\u00e7\u00e3o: fazendo um VPC redundante n\u00e3o redundante ir\u00e1 for\u00e7ar uma limpeza. As redes n\u00e3o estar\u00e3o dispon\u00edveis por alguns minutos.

", "message.scale.processing": "Escalonamento em progresso", +"message.scaledown.policies": "Por favor, adicione pelo menos uma pol\u00edtica de ScaleDown. O Grupo de AutoScale ser\u00e1 reduzido quando todas as condi\u00e7\u00f5es em uma pol\u00edtica de ScaleDown forem atendidas. As pol\u00edticas de ScaleDown ser\u00e3o verificadas ap\u00f3s as pol\u00edticas de ScaleUp.", +"message.scaledown.policy.continue": "Por favor, adicione pelo menos uma condi\u00e7\u00e3o \u00e0 pol\u00edtica de ScaleDown para continuar", +"message.scaledown.policy.duration.continue": "Por favor, insira uma dura\u00e7\u00e3o v\u00e1lida para a pol\u00edtica de ScaleDown para continuar", +"message.scaledown.policy.name.continue": "Por favor, insira um nome para a pol\u00edtica de ScaleDown para continuar", +"message.scaleup.policies": "Por favor, adicione pelo menos uma pol\u00edtica de ScaleUp. O Grupo de AutoScale ser\u00e1 aumentado quando todas as condi\u00e7\u00f5es em uma pol\u00edtica de ScaleUp forem atendidas. As pol\u00edticas de ScaleUp ser\u00e3o verificadas antes das pol\u00edticas de ScaleDown.", +"message.scaleup.policy.continue": "Por favor, adicione pelo menos uma condi\u00e7\u00e3o \u00e0 pol\u00edtica de ScaleUp para continuar", +"message.scaleup.policy.duration.continue": "Por favor, insira uma dura\u00e7\u00e3o v\u00e1lida para a pol\u00edtica de ScaleUp para continuar", +"message.scaleup.policy.name.continue": "Por favor, insira um nome para a pol\u00edtica de ScaleUp para continuar", "message.select.a.zone": "A zona tipicamente corresponde a um \u00fanico datacenter. M\u00faltiplas zonas auxiliam a nuvem a ser mais confi\u00e1vel provendo isolamento f\u00edsico e redund\u00e2ncia.", "message.select.affinity.groups": "Por favor, selecione quaisquer grupos de afinidade que voc\u00ea deseja que esta VM perten\u00e7a:", +"message.select.bgp.peers": "Por favor, selecione / desmarque os pares BGP associados \u00e0 rede ou VPC:", "message.select.deselect.desired.options": "Por favor, selecione / desselecione as op\u00e7\u00f5es desejadas", +"message.select.deselect.to.sort": "Por favor, selecione / desmarque para ordenar os valores", "message.select.destination.image.stores": "Por favor, selecione o(s) armazenamento(s) de imagem(ns) para os quais os dados devem ser migrados", "message.select.disk.offering": "Por favor, selecione uma oferta de disco para o disco", "message.select.end.date.and.time": "Selecione uma data e hor\u00e1rio final.", +"message.select.kvm.host.instance.conversion": "(Opcional) Selecione um host KVM no cluster para realizar a convers\u00e3o da inst\u00e2ncia atrav\u00e9s do virt-v2v", +"message.select.load.balancer.rule": "Por favor, selecione uma regra de balanceamento de carga para o seu Grupo de escalonamento automatico.", "message.select.migration.policy": "Por favor, selecione uma pol\u00edtica de migra\u00e7\u00e3o", "message.select.nic.network": "Por favor, selecione uma rede para o NIC", "message.select.security.groups": "Por favor selecione o(s) grupo(s) de seguran\u00e7a para sua nova VM", "message.select.start.date.and.time": "Selecione uma data e hor\u00e1rio inicial.", +"message.select.temporary.storage.instance.conversion": "(Opcional) Selecione um destino tempor\u00e1rio de Armazenamento para os discos convertidos atrav\u00e9s do virt-v2v", +"message.select.volume.to.continue": "Por favor, selecione um volume para continuar.", "message.select.zone.description": "Selecione o tipo de zona b\u00e1sica/avan\u00e7ada", "message.select.zone.hint": "Este \u00e9 o tipo de implanta\u00e7\u00e3o de zona que voc\u00ea deseja utilizar. Zona b\u00e1sica: fornece uma \u00fanica rede na qual para cada inst\u00e2ncia de VM \u00e9 atribu\u00eddo um IP diretamente da rede. O isolamento do guest pode ser fornecido atrav\u00e9s de meios da camada 3, tais como grupos de seguran\u00e7a (filtragem de source IP). Zona avan\u00e7ada: para topologias de rede mais sofisticadas. este modelo de rede oferece maior flexibilidade na defini\u00e7\u00e3o de redes guest e oferece ofertas de rede personalizadas, como firewall, VPN, ou suporte de balanceamento de carga.", +"message.select.vm.to.continue": "Por favor, selecione uma inst\u00e2ncia para continuar", +"message.server": "Servidor : ", "message.server.description": "NFS, iSCSI, ou PreSetup: endere\u00e7o IP ou nome DNS do dispositivo de armazenamento. VMWare PreSetup: endere\u00e7o IP ou nome DNS do servidor vCenter. Linstor: URL HTTP(S) do linstor-controller.", "message.set.default.nic": "Por favor confirme que voc\u00ea quer tornar este NIC o padr\u00e3o para esta VM,", "message.set.default.nic.manual": "Por favor atualize manualmente o NIC padr\u00e3o desta VM agora.", "message.setting.updated": "Configura\u00e7\u00e3o atualizada:", +"message.setting.update.delay": "O novo valor entrar\u00e1 em vigor em 30 segundos.", "message.setup.physical.network.during.zone.creation": "Ao adicionar uma zona avan\u00e7ada, \u00e9 preciso configurar uma ou mais redes f\u00edsicas. Cada rede corresponde \u00e0 uma interface de rede no virtualizador. Cada rede f\u00edsica pode ser utilizada para transportar um ou mais tipos de tr\u00e1fego, com certas restri\u00e7\u00f5es sobre como eles podem ser combinados.
Arraste e solte um ou mais tipos de tr\u00e1fego em cada rede f\u00edsica.", "message.setup.physical.network.during.zone.creation.basic": "Ao adicionar uma zona b\u00e1sica, \u00e9 poss\u00edvel configurar uma rede f\u00edsica, que corresponde a uma interface de rede no virtualizador. A rede carrega diversos tipos de tr\u00e1fego.

\u00c9 poss\u00edvel adicionar e remover outros tipos de tr\u00e1fego na mesma interface de rede f\u00edsica.", +"message.shared.network.offering.warning": "Administradores de dom\u00ednio e usu\u00e1rios regulares s\u00f3 podem criar Redes compartilhadas a partir de ofertas de Rede com a configura\u00e7\u00e3o specifyvlan=false. Por favor, contate um administrador para criar uma oferta de Rede se esta lista estiver vazia.", +"message.shared.network.unsupported.for.nsx": "Redes compartilhadas n\u00e3o s\u00e3o suportadas para zonas habilitadas para NSX", +"message.shutdown.triggered": "Um desligamento foi acionado. CloudStack n\u00e3o aceitar\u00e1 novos jobs", +"message.snapshot.additional.zones": "Snapshots sempre ser\u00e3o criados em sua zona nativa - %x, aqui voc\u00ea pode selecionar zona(s) adicional(is) para onde ela ser\u00e1 copiada no momento da cria\u00e7\u00e3o", +"message.sourcenatip.change.inhibited": "Alterar o sourcenat para este IP da Rede para este endere\u00e7o est\u00e1 inibido, pois regras de firewall est\u00e3o definidas para ele. Isso pode incluir regras de encaminhamento de porta ou balanceamento de carga.\n - Se esta for uma Rede Isolada, por favor use updateNetwork/clique no bot\u00e3o editar.\n - Se esta for uma VPC, primeiro limpe todas as outras regras para este endere\u00e7o.", +"message.sourcenatip.change.warning": "AVISO: Alterar o endere\u00e7o IP sourcenat da rede causar\u00e1 tempo de inatividade de conectividade para as Inst\u00e2ncias com NICs na Rede.", "message.specify.tag.key": "Por favor, especifique uma chave de etiqueta", "message.specify.tag.value": "Por favor, especifique um valor para a etiqueta", "message.step.2.continue": "Selecione o plano", @@ -2384,55 +3480,101 @@ "message.step.4.continue": "Selecione pelo menos uma rede para continuar", "message.step.license.agreements.continue": "Por favor, aceite todos os termos de licen\u00e7a para continuar", "message.success.acquire.ip": "IP adquirido com sucesso", +"message.success.add.bgp.peer": "Novo par BGP adicionado com sucesso", "message.success.add.egress.rule": "Nova regra de sa\u00edda adicionada com sucesso", "message.success.add.firewall.rule": "Nova regra do firewall adicionada com sucesso", "message.success.add.guest.network": "Rede guest criada com sucesso", +"message.success.add.interface.static.route": "Rota Est\u00e1tica de interface adicionada com sucesso", +"message.success.add.ip.v6.prefix": "Prefixo IPv6 adicionado com sucesso", "message.success.add.iprange": "Intervalo IP adicionado com sucesso", +"message.success.add.ipv4.subnet": "Sub-rede IPv4 adicionada com sucesso", +"message.success.add.ipv4.subnet.for.guest.network": "Sub-rede IPv4 para rede guest adicionada com sucesso", "message.success.add.kuberversion": "Vers\u00e3o do Kubernetes adicionado com sucesso", +"message.success.add.logical.router": "Roteador L\u00f3gico adicionado com sucesso", "message.success.add.network": "Rede adicionada com sucesso", "message.success.add.network.acl": "Lista ACL da rede adicionada com sucesso", +"message.success.add.network.permissions": "Permiss\u00f5es de Rede adicionadas com sucesso", +"message.success.add.network.static.route": "Rota Est\u00e1tica de Rede adicionada com sucesso", +"message.success.add.object.storage": "Object Storage adicionado com sucesso", +"message.success.add.physical.network": "Rede F\u00edsica adicionada com sucesso", +"message.success.add.policy.rule": "Regra de Pol\u00edtica adicionada com sucesso", +"message.success.add.router.table.to.instance": "Tabela de Roteador adicionada \u00e0 Inst\u00e2ncia com sucesso", "message.success.add.port.forward": "Nova regra de encaminhamento de porta adicionada com sucesso", "message.success.add.private.gateway": "Gateway privado adicionado com sucesso", "message.success.add.rule": "Nova regra adicionada com sucesso", "message.success.add.secondary.ipaddress": "IP secund\u00e1rio adicionado com sucesso", "message.success.add.static.route": "Rota est\u00e1tica adicionada com sucesso", "message.success.add.tag": "Etiqueta adicionada com sucesso", +"message.success.add.tungsten.router.table": "Tabela de Roteador adicionada com sucesso", +"message.success.add.tungsten.routing.policy": "Pol\u00edtica de roteamento Tungsten-Fabric adicionada com sucesso", +"message.success.add.vpc": "Uma Nuvem Privada Virtual adicionada com sucesso", "message.success.add.vpc.network": "Rede VPC adicionada com sucesso", "message.success.add.vpn.customer.gateway": "Gateway da VPN do cliente adicionado com sucesso", "message.success.add.vpn.gateway": "Gateway da VPN adicionado com sucesso", "message.success.assign.vm": "VM atribu\u00edda com sucesso", +"message.success.assign.volume": "Volume atribu\u00eddo com sucesso", +"message.success.apply.network.policy": "Pol\u00edtica de Rede aplicada com sucesso", +"message.success.apply.tungsten.tag": "Tag aplicada com sucesso", +"message.success.asign.vm": "Inst\u00e2ncia atribu\u00edda com sucesso", "message.success.assigned.vms": "VMs atribu\u00eddas com sucesso", "message.success.certificate.upload": "Certificado carregado com sucesso", "message.success.change.affinity.group": "Grupos de afinidade alterados com sucesso", +"message.success.change.bgp.peers": "Pares BGP alterados com sucesso", "message.success.change.offering": "Oferta alterada com sucesso", "message.success.change.password": "Senha alterada com sucesso", "message.success.change.host.password": "Senha do host \"{name}\" foi alterada com sucesso", +"message.success.change.scope": "Escopo alterado com sucesso para o pool de armazenamento", +"message.success.clear.webhook.deliveries": "Entregas de webhook limpas com sucesso", +"message.success.config.health.monitor": "Monitor de Sa\u00fade Configurado com Sucesso", +"message.success.config.vm.schedule": "Agendamento de Inst\u00e2ncia configurado com sucesso", "message.success.config.backup.schedule": "Agendamento de backup de VM configurado com sucesso", "message.success.config.sticky.policy": "Sticky policy configurada com sucesso", "message.success.copy.clipboard": "Copiado com sucesso para a \u00e1rea de transfer\u00eancia", "message.success.create.account": "Conta criada com sucesso", +"message.success.create.asnrange": "Faixa AS criada com sucesso", +"message.success.create.bucket": "Bucket criado com sucesso", +"message.success.create.gui.theme": "Tema da GUI \"{guiTheme}\" criado com sucesso", "message.success.create.internallb": "LB interno criado com sucesso", "message.success.create.isolated.network": "Rede isolada criada com sucesso", "message.success.create.keypair": "Par de chaves SSH criado com sucesso", "message.success.create.kubernetes.cluster": "Cluster Kubernetes criado com sucesso", "message.success.create.l2.network": "Rede L2 criada com sucesso", +"message.success.create.password": "Par de chave de acesso \u00e0 API criada com sucesso", +"message.success.create.sharedfs": "Sistema de Arquivos Compartilhado criado com sucesso", "message.success.create.snapshot.from.vmsnapshot": "Snapshot de volume a partir da snapshot de VM criada com sucesso", +"message.success.create.template": "Template criado com sucesso", "message.success.create.user": "Usu\u00e1rio criado com sucesso", "message.success.create.volume": "Volume criado com sucesso", +"message.success.create.webhook": "Webhook criado com sucesso", +"message.success.dedicate.bgp.peer": "Par BGP dedicado com sucesso", +"message.success.dedicate.ipv4.subnet": "Sub-rede IPv4 dedicada com sucesso", "message.success.delete": "Exclu\u00eddo com sucesso", "message.success.delete.acl.rule": "Regra ACL removida com sucesso", +"message.success.delete.asnrange": "Faixa AS exclu\u00edda com sucesso", +"message.success.delete.backup.schedule": "Agendamento de Backup de Inst\u00e2ncia exclu\u00eddo com sucesso", +"message.success.delete.bgp.peer": "Par BGP exclu\u00eddo com sucesso", "message.success.delete.icon": "\u00cdcone deletado com sucesso", +"message.success.delete.interface.static.route": "Rota Est\u00e1tica de interface removida com sucesso", +"message.success.delete.ipv4.subnet": "Sub-rede IPv4 removida com sucesso", +"message.success.delete.keypair": "Par de chave de API deletada com sucesso", +"message.success.delete.network.static.route": "Rota Est\u00e1tica de Rede removida com sucesso", "message.success.delete.node": "Nodo exclu\u00eddo com sucesso", "message.success.delete.snapshot.policy": "Pol\u00edtica de snapshot exclu\u00edda com sucesso", "message.success.delete.static.route": "Rota est\u00e1tica exclu\u00edda com sucesso", "message.success.delete.tag": "Tag exclu\u00edda com sucesso", +"message.success.delete.tungsten.policy.rule": "Regra de Pol\u00edtica exclu\u00edda com sucesso", +"message.success.delete.tungsten.router.table": "Tabela de Roteador removida com sucesso", +"message.success.delete.tungsten.tag": "Tag removida com sucesso", "message.success.delete.vm": "VM remov\u00edda com sucesso", "message.success.disable.saml.auth": "Autoriza\u00e7\u00e3o SAML desativada com sucesso", "message.success.disable.vpn": "VPN desativada com sucesso", "message.success.edit.acl": "Regra ACL editada com sucesso", +"message.success.edit.primary.storage": "Armazenamento Prim\u00e1rio editado com sucesso", "message.success.edit.rule": "Regra editada com sucesso", "message.success.enable.saml.auth": "Autoriza\u00e7\u00e3o SAML ativada com sucesso", "message.success.import.instance": "Inst\u00e2ncia importada com sucesso", +"message.success.import.volume": "Volume importado com sucesso", +"message.success.migration": "Migra\u00e7\u00e3o conclu\u00edda com sucesso", "message.success.migrate.volume": "Volume migrado com sucesso", "message.success.migrating": "Migra\u00e7\u00e3o conclu\u00edda com sucesso para", "message.success.move.acl.order": "Regra de ACL movida com sucesso", @@ -2440,26 +3582,47 @@ "message.success.register.iso": "ISO registrado com sucesso", "message.success.register.keypair": "Par de chaves SSH registrado com sucesso", "message.success.register.template": "Template cadastrado com sucesso", +"message.success.register.user.data": "Userdata registrado com sucesso", +"message.success.register.user.keypair": "Novo par de chave de API criado com sucesso", +"message.success.release.dedicated.bgp.peer": "Par BGP dedicado liberado com sucesso", +"message.success.release.dedicated.ipv4.subnet": "Sub-rede IPv4 dedicada liberada com sucesso", "message.success.release.ip": "IP liberado com sucesso", "message.success.remove.egress.rule": "Regra de sa\u00edda removida com sucesso", "message.success.remove.firewall.rule": "Regra de firewall removida com sucesso", "message.success.remove.instance.rule": "Inst\u00e2ncia removida da regra", "message.success.remove.ip": "IP removido com sucesso", "message.success.remove.iprange": "Intervalo de IP removido com sucesso", +"message.success.remove.logical.router": "Roteador L\u00f3gico removido com sucesso", +"message.success.remove.network.permissions": "Permiss\u00f5es de Rede removidas com sucesso", +"message.success.remove.network.policy": "Pol\u00edtica de Rede removida com sucesso", "message.success.remove.nic": "NIC removida com sucesso", +"message.success.remove.objectstore.directory": "Diret\u00f3rio selecionado removido com sucesso", +"message.success.remove.objectstore.objects": "Objeto(s) selecionado(s) removido(s) com sucesso", "message.success.remove.port.forward": "Regra de encaminhamento de porta removida com sucesso", +"message.success.remove.router.table.from.interface": "Tabela de Roteador removida da interface com sucesso", "message.success.remove.rule": "Regra exclu\u00edda com sucesso", "message.success.remove.secondary.ipaddress": "Endere\u00e7o de IP secund\u00e1rio removido com sucesso", "message.success.remove.sticky.policy": "Sticky policy removida com sucesso", +"message.success.remove.tungsten.routing.policy": "Pol\u00edtica de Roteamento Tungsten-Fabric removida da Rede com sucesso", +"message.success.reset.network.permissions": "Permiss\u00f5es de Rede redefinidas com sucesso", "message.success.resize.volume": "Volume redimensionado com sucesso", "message.success.scale.kubernetes": "Cluster Kubernetes escalonado com sucesso", "message.success.unmanage.instance": "Inst\u00e2ncia n\u00e3o gerenciada com sucesso", +"message.success.unmanage.volume": "Volume n\u00e3o gerenciado com sucesso", +"message.success.update.account": "Conta atualizada com sucesso", +"message.success.update.bgp.peer": "Par BGP atualizado com sucesso", +"message.success.update.bucket": "Bucket atualizado com sucesso", +"message.success.update.condition": "Condi\u00e7\u00e3o atualizada com sucesso", +"message.success.update.gui.theme": "Tema da GUI \"{guiTheme}\" atualizado com sucesso", "message.success.update.ipaddress": "Endere\u00e7o IP atualizado com sucesso", "message.success.update.iprange": "Intervalo de IP atualizado com sucesso", +"message.success.update.ipv4.subnet": "Sub-rede IPv4 atualizada com sucesso", "message.success.update.kubeversion": "Vers\u00e3o compat\u00edvel com Kubernetes atualizada com sucesso", "message.success.update.network": "Rede atualizada com sucesso", "message.success.update.nic": "NIC atualizada com sucesso", "message.success.update.user": "Usu\u00e1rio atualizado com sucesso", +"message.success.update.sharedfs": "Sistema de Arquivos Compartilhado atualizado com sucesso", +"message.success.update.template": "Template atualizado com sucesso", "message.success.upgrade.kubernetes": "Cluster do Kubernetes atualizado com sucesso", "message.success.upload": "Carregado com sucesso", "message.success.upload.description": "Este arquivo de ISO foi carregado. Verifique seu status no menu Imagens > ISOs. Verifique seu status no menu templates", @@ -2470,15 +3633,43 @@ "message.suspend.project": "Voc\u00ea tem certeza que deseja suspender este projeto?", "message.sussess.discovering.feature": "Descoberto todos os recursos dispon\u00edveis!", "message.switch.to": "Transferido para", +"message.template.arch": "Por favor, selecione uma arquitetura de Template.", "message.template.desc": "Imagem de SO que pode ser utilizada para bootar VMs", "message.template.import.vm.temporary": "Se um template tempor\u00e1rio for usado, a opera\u00e7\u00e3o de redefini\u00e7\u00e3o da VM n\u00e3o funcionar\u00e1 ap\u00f3s a importa\u00e7\u00e3o.", "message.template.iso": "Selecione o template ou ISO para continuar", +"message.template.type.change.warning": "AVISO: Alterar o tipo de Template para SYSTEM desativar\u00e1 futuras altera\u00e7\u00f5es no Template.", +"message.test.webhook.delivery": "Testar entrega para o Webhook com um payload opcional", "message.tooltip.reserved.system.netmask": "O prefixo de rede que define a subrede deste pod. utilize a nota\u00e7\u00e3o CIDR.", +"message.traffic.type.deleted": "Tipo de tr\u00e1fego exclu\u00eddo com sucesso", +"message.trigger.shutdown": "Por favor, confirme que voc\u00ea gostaria de acionar um desligamento neste Management Server. Ele n\u00e3o aceitar\u00e1 nenhum novo job Ass\u00edncrono e terminar\u00e1 ap\u00f3s n\u00e3o haver jobs pendentes.", +"message.two.fa.auth": "Abra o aplicativo de autentica\u00e7\u00e3o de dois fatores no seu dispositivo m\u00f3vel para visualizar seu c\u00f3digo de autentica\u00e7\u00e3o.", +"message.two.fa.auth.register.account": "Abra o aplicativo de autentica\u00e7\u00e3o de dois fatores e escaneie o c\u00f3digo QR para adicionar a Conta de Usu\u00e1rio.", +"message.two.fa.auth.staticpin": "
Voc\u00ea configurou 2FA para verifica\u00e7\u00e3o de seguran\u00e7a.
Insira o PIN est\u00e1tico gerado durante a configura\u00e7\u00e3o do 2FA para verificar.", +"message.two.fa.auth.totp": "
Voc\u00ea configurou 2FA para verifica\u00e7\u00e3o de seguran\u00e7a.
Abra o aplicativo autenticador TOTP no seu dispositivo e insira o c\u00f3digo de autentica\u00e7\u00e3o.", +"message.two.fa.login.page": "A autentica\u00e7\u00e3o de dois fatores (2FA) est\u00e1 ativada na sua Conta, voc\u00ea precisa selecionar um provedor 2FA e configurar. 2FA \u00e9 uma camada extra de seguran\u00e7a para sua conta. Uma vez que a configura\u00e7\u00e3o seja feita, em cada login voc\u00ea ser\u00e1 solicitado a inserir o c\u00f3digo 2FA.
", +"message.two.fa.register.account": "1. Abra o aplicativo autenticador TOTP no seu dispositivo.
2. Escaneie o c\u00f3digo QR abaixo para adicionar o Usu\u00e1rio.
3. Se voc\u00ea n\u00e3o conseguir escanear o c\u00f3digo QR, insira a chave de configura\u00e7\u00e3o manualmente.
4. A verifica\u00e7\u00e3o do c\u00f3digo 2FA \u00e9 obrigat\u00f3ria para completar a configura\u00e7\u00e3o 2FA.", +"message.two.fa.register.account.login.page": "1. Abra o aplicativo autenticador TOTP no seu dispositivo.
2. Escaneie o c\u00f3digo QR abaixo para adicionar o Usu\u00e1rio.
3. Se voc\u00ea n\u00e3o conseguir escanear o c\u00f3digo QR, insira a chave de configura\u00e7\u00e3o manualmente.
4. Verifique o c\u00f3digo 2FA para continuar para o login.", +"message.two.fa.setup.page": "A autentica\u00e7\u00e3o de dois fatores (2FA) \u00e9 uma camada extra de seguran\u00e7a para sua conta.
Uma vez que a configura\u00e7\u00e3o seja feita, em cada login voc\u00ea ser\u00e1 solicitado a inserir o c\u00f3digo 2FA.
", +"message.two.fa.static.pin.part1": "Se voc\u00ea n\u00e3o conseguir escanear o c\u00f3digo QR, ", +"message.two.fa.static.pin.part2": "Clique aqui para visualizar o c\u00f3digo secreto", +"message.two.fa.staticpin": "1. Use o PIN est\u00e1tico gerado como c\u00f3digo 2FA para autentica\u00e7\u00e3o de dois fatores.
2. Salve este PIN est\u00e1tico / c\u00f3digo 2FA e n\u00e3o o compartilhe. Este c\u00f3digo ser\u00e1 usado para logins subsequentes.
3. A verifica\u00e7\u00e3o do c\u00f3digo 2FA \u00e9 obrigat\u00f3ria para completar a configura\u00e7\u00e3o 2FA.", +"message.two.fa.staticpin.login.page": "1. Use o PIN est\u00e1tico gerado como c\u00f3digo 2FA para autentica\u00e7\u00e3o de dois fatores.
2. Salve este PIN est\u00e1tico / c\u00f3digo 2FA e n\u00e3o o compartilhe. Este c\u00f3digo ser\u00e1 usado para logins subsequentes.
3. Verifique o c\u00f3digo 2FA para continuar para o login.", +"message.two.fa.view.setup.key": "Clique aqui para visualizar a chave de configura\u00e7\u00e3o", +"message.two.fa.view.static.pin": "Clique aqui para visualizar o PIN est\u00e1tico", +"message.two.factor.authorization.failed": "Incapaz de verificar 2FA com o c\u00f3digo fornecido, por favor tente novamente.", +"message.type.values.to.add": "Por favor, adicione valores adicionais digitando-os", "message.traffic.type.to.basic.zone": "Tipo de tr\u00e1fego para a zona b\u00e1sica", +"message.update.autoscale.policy.failed": "Falha ao atualizar pol\u00edtica de AutoScale", +"message.update.autoscale.vm.profile.failed": "Falha ao atualizar perfil de Inst\u00e2ncia de AutoScale", +"message.update.autoscale.vmgroup.failed": "Falha ao atualizar grupo de AutoScale", +"message.update.condition.failed": "Falha ao atualizar condi\u00e7\u00e3o", +"message.update.condition.processing": "Atualizando condi\u00e7\u00e3o...", +"message.update.failed": "Atualiza\u00e7\u00e3o falhou", "message.update.nic.processing": "Atualizando NIC...", "message.update.ipaddress.processing": "Atualizando endere\u00e7o IP...", "message.update.resource.count": "Por favor confirme que voc\u00ea quer atualizar a contagem de recursos para esta conta.", "message.update.resource.count.domain": "Por favor, confirme que voc\u00ea deseja atualizar as contagens de recursos para este dom\u00ednio.", +"message.update.resource.limit.max.untagged.error": "Limite n\u00e3o marcado %x \u00e9 %y. Por favor, especifique limite para tag '%z' menor ou igual a esse", "message.update.ssl": "Envie o novo certificado SSL X.509 para ser atualizado em cada console proxy:", "message.upload.failed": "Falha ao fazer o carregamento", "message.upload.file.limit": "Apenas um arquivo pode ser carregado de cada vez", @@ -2493,6 +3684,7 @@ "message.validate.maxlength": "Por favor entre com mais de [0] caracteres.", "message.validate.minlength": "Por favor entre com pelo menos [0] caracteres.", "message.validate.number": "Por favor entre um n\u00famero v\u00e1lido.", +"message.validate.positive.number": "Por favor, insira um n\u00famero positivo v\u00e1lido.", "message.validate.range": "Por favor entre com um valor com valor entre [0] e [1].", "message.validate.range.length": "Por favor entre com um valor com tamanho entre [0] e [1] caracteres.", "message.virtual.router.not.return.elementid": "erro: A API listVirtualRouterElements n\u00e3o retorna o ID do elemento do roteador virtual", @@ -2507,6 +3699,20 @@ "message.vm.state.stopped": "A VM est\u00e1 parada", "message.vm.state.stopping": "A VM est\u00e1 sendo parada", "message.vm.state.unknown": "O estado da VM \u00e9 desconhecido.", +"message.vnf.appliance.networks": "Por favor, selecione as redes para o novo appliance VNF.", +"message.vnf.credentials.change": "Por favor, altere a(s) senha(s) do appliance VNF imediatamente.", +"message.vnf.credentials.default": "A(s) credencial(is) padr\u00e3o do appliance VNF", +"message.vnf.credentials.in.template.vnf.details": "Por favor, encontre as credenciais padr\u00e3o para este VNF nos detalhes do template VNF.", +"message.vnf.error.deviceid.should.be.consecutive": "O deviceid das NICs VNF selecionadas deve ser consecutivo.", +"message.vnf.error.network.is.already.used": "A Rede foi usada por v\u00e1rias NICs do novo appliance VNF.", +"message.vnf.error.network.should.be.used": "Todas as redes selecionadas devem ser usadas como Nics VNF", +"message.vnf.error.no.networks": "Por favor, selecione as redes para o novo appliance VNF.", +"message.vnf.error.no.network.for.required.deviceid": "Por favor, selecione uma Rede para a NIC obrigat\u00f3ria do novo appliance VNF.", +"message.vnf.nic.move.down.fail": "Falha ao mover esta NIC para baixo", +"message.vnf.nic.move.up.fail": "Falha ao mover esta NIC para cima", +"message.vnf.no.credentials": "Nenhuma credencial encontrada para o appliance VNF.", +"message.vnf.select.networks": "Por favor, selecione a rede relevante para cada NIC VNF.", +"message.volume.state.primary.storage.suitability": "A adequa\u00e7\u00e3o de um armazenamento prim\u00e1rio para um volume depende da oferta de disco do volume e da aloca\u00e7\u00e3o da m\u00e1quina virtual (se o volume estiver anexado a uma m\u00e1quina virtual).", "message.volume.state.allocated": "O volume foi alocado, mas ainda n\u00e3o foi criado", "message.volume.state.attaching": "O volume est\u00e1 anexado a um volume do estado pronto.", "message.volume.state.copying": "O volume est\u00e1 sendo copiado do armazenamento de imagens para o armazenamento prim\u00e1rio, no caso de ser um volume carregado", @@ -2526,12 +3732,20 @@ "message.volume.state.uploaderror": "O carregamento do volume encontrou um erro", "message.volume.state.uploadinprogress": "Carregamento do volume em progresso", "message.volume.state.uploadop": "A opera\u00e7\u00e3o de carregamento de volume est\u00e1 em andamento", +"message.volumes.managed": "Volumes controlados pelo CloudStack.", +"message.volumes.unmanaged": "Volumes n\u00e3o controlados pelo CloudStack.", +"message.vpc.restart.required": "Reinicializa\u00e7\u00e3o \u00e9 necess\u00e1ria para VPC(s). Clique aqui para ver VPC(s) que requerem reinicializa\u00e7\u00e3o.", "message.vr.alert.upon.network.offering.creation.others": "Como nenhum dos servi\u00e7os obrigat\u00f3rios para cria\u00e7\u00e3o do VR (VPN, DHCP, DNS, Firewall, LB, UserData, SourceNat, StaticNat, PortForwarding) foram habilitados, o VR n\u00e3o ser\u00e1 criado e a oferta de computa\u00e7\u00e3o n\u00e3o ser\u00e1 usada.", +"message.warn.change.primary.storage.scope": "Este recurso \u00e9 testado e suportado para as seguintes configura\u00e7\u00f5es:
KVM - NFS/Ceph - DefaultPrimary
VMware - NFS - DefaultPrimary
*Pode haver etapas extras envolvidas para faz\u00ea-lo funcionar para outras configura\u00e7\u00f5es.", "message.warn.filetype": "jpg, jpeg, png, bmp e svg s\u00e3o os \u00fanicos formatos de imagem suportados", "message.warn.importing.instance.without.nic": "AVISO: essa inst\u00e2ncia est\u00e1 sendo importada sem NICs e muitos recursos de rede n\u00e3o estar\u00e3o dispon\u00edveis. Considere criar uma NIC antes de importar via VCenter ou assim que a inst\u00e2ncia for importada.", +"message.warn.vpc.offerings": "Ofertas de VPC s\u00c3o exibidas somente caso a conta selecionada possua ao menos uma VPC.", +"message.warn.zone.mtu.update": "Por favor, note que este limite n\u00e3o afetar\u00e1 as configura\u00e7\u00f5es de MTU da Rede pr\u00e9-existentes", +"message.webhook.deliveries.time.filter": "A lista de entregas de webhook pode ser filtrada com base em data e hora. Selecione 'Customizado' para especificar o intervalo de data de in\u00edcio e fim.", "message.zone.creation.complete": "Cria\u00e7\u00e3o de zona completa", "message.zone.detail.description": "Preencha os detalhes da zona", "message.zone.detail.hint": "Uma zona \u00e9 a maior unidade organizacional no CloudStack, e normalmente corresponde a um \u00fanico datacenter. As zonas proporcionam isolamento f\u00edsico e redund\u00e2ncia. Uma zona consiste em um ou mais pods (cada um contendo hosts e servidores de armazenamento prim\u00e1rio) e um servidor de armazenamento secund\u00e1rio que \u00e9 compartilhado por todos os pods da zona.", +"message.zone.edge.local.storage": "Armazenamento local ser\u00e1 usado por padr\u00e3o para Inst\u00e2ncias de Usu\u00e1rio e roteadores virtuais", "message.validate.min": "Por favor entre com um valor maior que ou igual a {0}.", "migrate.from": "Migrar de", "migrate.to": "Migrar para", From 522283bdeca92a3c4ce75846c106b1fa80661bbd Mon Sep 17 00:00:00 2001 From: dahn Date: Tue, 19 May 2026 10:01:07 +0200 Subject: [PATCH 062/205] Refactor merge conflict checker workflow (#13183) --- .github/workflows/merge-conflict-checker.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/merge-conflict-checker.yml b/.github/workflows/merge-conflict-checker.yml index 748156a2d9ce..2c826a47c7ec 100644 --- a/.github/workflows/merge-conflict-checker.yml +++ b/.github/workflows/merge-conflict-checker.yml @@ -17,16 +17,14 @@ name: "PR Merge Conflict Check" on: - push: - pull_request: - types: [opened, synchronize, reopened] + schedule: + - cron: '*/10 * * * *' + workflow_dispatch: -permissions: # added using https://github.com/step-security/secure-workflows - contents: read +permissions: {} concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: "gh-aw-${{ github.workflow }}" jobs: triage: From 3285e2fad8747d3a02cf135616ee220a34a8da4e Mon Sep 17 00:00:00 2001 From: agronaught Date: Tue, 19 May 2026 20:10:02 +1000 Subject: [PATCH 063/205] systemvm: accept ipv6 established/related return traffic (#13173) --- systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py b/systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py index 93d0d0388efe..63d7724dd20a 100755 --- a/systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py +++ b/systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py @@ -232,7 +232,7 @@ def add_ip6_chain(self, address_family, table, chain, hook, action): if hook == "input" or hook == "output": CsHelper.execute("nft add rule %s %s %s icmpv6 type { echo-request, echo-reply, \ nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept" % (address_family, table, chain)) - elif hook == "forward": + if hook == "input" or hook == "forward": CsHelper.execute("nft add rule %s %s %s ct state established,related accept" % (address_family, table, chain)) def add_ip4_chain(self, address_family, table, chain, hook, action): From 68221cf10182765c0abce1fe2e3954307335c19f Mon Sep 17 00:00:00 2001 From: James Peru Mmbono Date: Tue, 19 May 2026 17:31:58 +0300 Subject: [PATCH 064/205] fix(linstor): pre-flight check destination is a LINSTOR satellite before live migration (#13077) --- .../motion/LinstorDataMotionStrategy.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java index cab2820f09ae..aa450a8290fb 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java @@ -314,6 +314,58 @@ private boolean needsExactSizeProp(VolumeInfo srcVolumeInfo) { return true; } + /** + * Verify that the destination KVM host is a registered LINSTOR satellite on the controller + * backing every destination pool involved in this migration. Throws CloudRuntimeException + * with a clear message when it isn't, instead of letting the resource creation later fail + * obscurely inside auto-placement. + * + * Best-effort: a transient controller error during this check does not block the migration + * — we log a warning and let the downstream resource-create surface the real issue. Only a + * confirmed "host not in node list" outcome aborts the migration up-front. + */ + private void verifyDestinationIsLinstorSatellite(Map volumeDataStoreMap, Host destHost) { + if (destHost == null || destHost.getName() == null) { + // Without a destination host name to match, the only sensible thing is to let the + // existing flow run and report whatever it would have reported. + return; + } + for (Map.Entry entry : volumeDataStoreMap.entrySet()) { + DataStore destDataStore = entry.getValue(); + StoragePoolVO destStoragePool = _storagePool.findById(destDataStore.getId()); + if (destStoragePool == null + || destStoragePool.getPoolType() != Storage.StoragePoolType.Linstor) { + continue; + } + DevelopersApi api = LinstorUtil.getLinstorAPI(destStoragePool.getHostAddress()); + try { + List nodes = LinstorUtil.getLinstorNodeNames(api); + if (nodes == null) { + logger.warn("LINSTOR controller {} returned null node list; skipping pre-flight", + destStoragePool.getHostAddress()); + return; + } + if (!nodes.contains(destHost.getName())) { + throw new CloudRuntimeException(String.format( + "Cannot migrate to host '%s': it is not a registered LINSTOR satellite on " + + "controller %s (pool '%s'). Known satellites: %s. Either register the " + + "host with `linstor node create` or pick a different destination.", + destHost.getName(), + destStoragePool.getHostAddress(), + destStoragePool.getName(), + nodes)); + } + } catch (ApiException apiEx) { + // Don't block migration on a transient controller hiccup — log and let the + // downstream resource creation handle the real failure. + logger.warn("LINSTOR pre-flight check could not contact controller {}: {}; " + + "letting downstream resource creation proceed", + destStoragePool.getHostAddress(), apiEx.getBestMessage()); + return; + } + } + } + @Override public void copyAsync(Map volumeDataStoreMap, VirtualMachineTO vmTO, Host srcHost, Host destHost, AsyncCompletionCallback callback) { @@ -323,6 +375,15 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach String.format("Invalid hypervisor type [%s]. Only KVM supported", srcHost.getHypervisorType())); } + // Pre-flight: verify the destination KVM host is registered as a satellite on the + // LINSTOR controller backing each destination pool. Without this check, resource + // creation falls through to the resource-group's auto-placement filters and may + // either silently place the resource on the wrong node or fail with an opaque + // auto-place error from the LINSTOR API. Failing fast here gives operators a clear + // actionable message instead of having to correlate the live-migration failure with + // an unrelated LINSTOR controller log entry. + verifyDestinationIsLinstorSatellite(volumeDataStoreMap, destHost); + String errMsg = null; VMInstanceVO vmInstance = _vmDao.findById(vmTO.getId()); vmTO.setState(vmInstance.getState()); From c165806d3b8262c58821e04c622da176c935b46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= <89930804+erikbocks@users.noreply.github.com> Date: Tue, 19 May 2026 12:42:29 -0300 Subject: [PATCH 065/205] Fix VPC network offerings listing in isolated network creation form (#12645) * Fix VPC network offerings listing in isolated network creation form * Apply suggestions from code review Co-authored-by: GaOrtiga <49285692+GaOrtiga@users.noreply.github.com> * Address Bernardo's review --------- Co-authored-by: GaOrtiga <49285692+GaOrtiga@users.noreply.github.com> --- ui/public/locales/en.json | 1 + ui/public/locales/pt_BR.json | 1 + .../network/CreateIsolatedNetworkForm.vue | 28 +++++++++++++------ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 673f6da0ad11..3160e00ba30d 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -3731,6 +3731,7 @@ "message.warn.filetype": "jpg, jpeg, png, bmp and svg are the only supported image formats.", "message.warn.importing.instance.without.nic": "WARNING: This Instance is being imported without NICs and many Network resources will not be available. Consider creating a NIC via vCenter before importing or as soon as the Instance is imported.", "message.warn.zone.mtu.update": "Please note that this limit won't affect pre-existing Network's MTU settings", +"message.warn.vpc.offerings": "VPC offerings will only be shown if the selected account has at least one VPC.", "message.webhook.deliveries.time.filter": "Webhook deliveries list can be filtered based on date-time. Select 'Custom' for specifying start and end date range.", "message.zone.creation.complete": "Zone creation complete.", "message.zone.detail.description": "Populate Zone details.", diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index 1b51bc438e58..b29afb8d408a 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -2510,6 +2510,7 @@ "message.vr.alert.upon.network.offering.creation.others": "Como nenhum dos servi\u00e7os obrigat\u00f3rios para cria\u00e7\u00e3o do VR (VPN, DHCP, DNS, Firewall, LB, UserData, SourceNat, StaticNat, PortForwarding) foram habilitados, o VR n\u00e3o ser\u00e1 criado e a oferta de computa\u00e7\u00e3o n\u00e3o ser\u00e1 usada.", "message.warn.filetype": "jpg, jpeg, png, bmp e svg s\u00e3o os \u00fanicos formatos de imagem suportados", "message.warn.importing.instance.without.nic": "AVISO: essa inst\u00e2ncia est\u00e1 sendo importada sem NICs e muitos recursos de rede n\u00e3o estar\u00e3o dispon\u00edveis. Considere criar uma NIC antes de importar via VCenter ou assim que a inst\u00e2ncia for importada.", +"message.warn.vpc.offerings": "Ofertas de VPC somente ser\u00c3o exibidas caso a conta selecionada possua ao menos uma VPC.", "message.zone.creation.complete": "Cria\u00e7\u00e3o de zona completa", "message.zone.detail.description": "Preencha os detalhes da zona", "message.zone.detail.hint": "Uma zona \u00e9 a maior unidade organizacional no CloudStack, e normalmente corresponde a um \u00fanico datacenter. As zonas proporcionam isolamento f\u00edsico e redund\u00e2ncia. Uma zona consiste em um ou mais pods (cada um contendo hosts e servidores de armazenamento prim\u00e1rio) e um servidor de armazenamento secund\u00e1rio que \u00e9 compartilhado por todos os pods da zona.", diff --git a/ui/src/views/network/CreateIsolatedNetworkForm.vue b/ui/src/views/network/CreateIsolatedNetworkForm.vue index 78d4ef72f04f..93fef14d601e 100644 --- a/ui/src/views/network/CreateIsolatedNetworkForm.vue +++ b/ui/src/views/network/CreateIsolatedNetworkForm.vue @@ -96,6 +96,11 @@ {{ opt.displaytext || opt.name || opt.description }} + + + @@ -231,10 +243,14 @@ export default { socialLogin: false, googleprovider: false, githubprovider: false, + keycloakprovider: false, googleredirecturi: '', githubredirecturi: '', + keycloakredirecturi: '', googleclientid: '', githubclientid: '', + keycloakclientid: '', + keycloakauthorizeurl: '', loginType: 0, state: { time: 60, @@ -325,8 +341,14 @@ export default { this.githubclientid = item.clientid this.githubredirecturi = item.redirecturi } + if (item.provider === 'keycloak') { + this.keycloakprovider = item.enabled + this.keycloakclientid = item.clientid + this.keycloakredirecturi = item.redirecturi + this.keycloakauthorizeurl = item.authorizeurl + } }) - this.socialLogin = this.googleprovider || this.githubprovider + this.socialLogin = this.googleprovider || this.githubprovider || this.keycloakprovider } }) postAPI('forgotPassword', {}).then(response => { @@ -362,6 +384,10 @@ export default { this.handleDomain() this.$store.commit('SET_OAUTH_PROVIDER_USED_TO_LOGIN', 'google') }, + handleKeycloakProviderAndDomain () { + this.handleDomain() + this.$store.commit('SET_OAUTH_PROVIDER_USED_TO_LOGIN', 'keycloak') + }, handleDomain () { const values = toRaw(this.form) if (!values.domain) { @@ -401,6 +427,20 @@ export default { return `${rootUrl}?${qs.toString()}` }, + getKeycloakUrl (from) { + const rootURl = this.keycloakauthorizeurl + const options = { + redirect_uri: this.keycloakredirecturi, + client_id: this.keycloakclientid, + response_type: 'code', + scope: 'openid email', + state: 'cloudstack' + } + + const qs = new URLSearchParams(options) + + return `${rootURl}?${qs.toString()}` + }, handleSubmit (e) { e.preventDefault() if (this.state.loginBtn) return From 21e4475d961a4cf645ccf043158f6d2bb7655a61 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Tue, 23 Jun 2026 20:11:19 +0530 Subject: [PATCH 122/205] Optimize the DB updates to use bulk UPDATE instead of row-level locks. (#13349) Co-authored-by: Aaron Chung --- .../com/cloud/alert/dao/AlertDaoImpl.java | 28 ++++++------- .../com/cloud/event/dao/EventDaoImpl.java | 29 ++++++++------ .../java/com/cloud/host/dao/HostDaoImpl.java | 22 +++++++---- .../dao/SecurityGroupWorkDaoImpl.java | 39 +++++-------------- 4 files changed, 53 insertions(+), 65 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/alert/dao/AlertDaoImpl.java b/engine/schema/src/main/java/com/cloud/alert/dao/AlertDaoImpl.java index 94d01f472ba5..97b7c54f0844 100644 --- a/engine/schema/src/main/java/com/cloud/alert/dao/AlertDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/alert/dao/AlertDaoImpl.java @@ -20,6 +20,7 @@ import java.util.List; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.alert.AlertVO; @@ -28,7 +29,7 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; -import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.db.UpdateBuilder; @Component public class AlertDaoImpl extends GenericDaoBase implements AlertDao { @@ -107,25 +108,20 @@ public boolean archiveAlert(List ids, String type, Date startDate, Date en } sc.setParameters("archived", false); - boolean result = true; - ; List alerts = listBy(sc); if (ids != null && alerts.size() < ids.size()) { - result = false; - return result; + return false; } - if (alerts != null && !alerts.isEmpty()) { - TransactionLegacy txn = TransactionLegacy.currentTxn(); - txn.start(); - for (AlertVO alert : alerts) { - alert = lockRow(alert.getId(), true); - alert.setArchived(true); - update(alert.getId(), alert); - txn.commit(); - } - txn.close(); + + if (CollectionUtils.isEmpty(alerts)) { + return true; } - return result; + + AlertVO alertForUpdate = createForUpdate(); + alertForUpdate.setArchived(true); + UpdateBuilder ub = getUpdateBuilder(alertForUpdate); + update(ub, sc, null); + return true; } @Override diff --git a/engine/schema/src/main/java/com/cloud/event/dao/EventDaoImpl.java b/engine/schema/src/main/java/com/cloud/event/dao/EventDaoImpl.java index e748e98900eb..9417ddd12595 100644 --- a/engine/schema/src/main/java/com/cloud/event/dao/EventDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/event/dao/EventDaoImpl.java @@ -18,8 +18,10 @@ import java.util.Date; import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.event.Event.State; @@ -29,12 +31,13 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; -import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.db.UpdateBuilder; @Component public class EventDaoImpl extends GenericDaoBase implements EventDao { protected final SearchBuilder CompletedEventSearch; protected final SearchBuilder ToArchiveOrDeleteEventSearch; + protected final SearchBuilder ArchiveByIdsSearch; public EventDaoImpl() { CompletedEventSearch = createSearchBuilder(); @@ -51,6 +54,10 @@ public EventDaoImpl() { ToArchiveOrDeleteEventSearch.and("createdDateL", ToArchiveOrDeleteEventSearch.entity().getCreateDate(), Op.LTEQ); ToArchiveOrDeleteEventSearch.and("archived", ToArchiveOrDeleteEventSearch.entity().getArchived(), Op.EQ); ToArchiveOrDeleteEventSearch.done(); + + ArchiveByIdsSearch = createSearchBuilder(); + ArchiveByIdsSearch.and("id", ArchiveByIdsSearch.entity().getId(), Op.IN); + ArchiveByIdsSearch.done(); } @Override @@ -100,16 +107,16 @@ public List listToArchiveOrDeleteEvents(List ids, String type, Da @Override public void archiveEvents(List events) { - if (events != null && !events.isEmpty()) { - TransactionLegacy txn = TransactionLegacy.currentTxn(); - txn.start(); - for (EventVO event : events) { - event = lockRow(event.getId(), true); - event.setArchived(true); - update(event.getId(), event); - txn.commit(); - } - txn.close(); + if (CollectionUtils.isEmpty(events)) { + return; } + + List ids = events.stream().map(EventVO::getId).collect(Collectors.toList()); + SearchCriteria sc = ArchiveByIdsSearch.create(); + sc.setParameters("id", ids.toArray(new Object[ids.size()])); + EventVO eventForUpdate = createForUpdate(); + eventForUpdate.setArchived(true); + UpdateBuilder ub = getUpdateBuilder(eventForUpdate); + update(ub, sc, null); } } diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java index cd4423dfa269..5b8a38b8e5b4 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java @@ -646,16 +646,22 @@ private void resetHosts(long managementServerId, long lastPingSecondsAfter) { sc.setParameters("lastPinged", lastPingSecondsAfter); sc.setParameters("status", Status.Disconnected, Status.Down, Status.Alert); - StringBuilder sb = new StringBuilder(); - List hosts = lockRows(sc, null, true); // exclusive lock - for (HostVO host : hosts) { - host.setManagementServerId(null); - update(host.getId(), host); - sb.append(host.getId()); - sb.append(" "); + // SELECT before bulk UPDATE to preserve per-host-ID trace logging — the bulk UPDATE + // cannot return which rows it matched since the WHERE column is being set to NULL + if (logger.isTraceEnabled()) { + List hosts = listBy(sc); + StringBuilder sb = new StringBuilder(); + for (HostVO host : hosts) { + sb.append(host.getId()); + sb.append(" "); + } + logger.trace("Following hosts will be reset: {}", sb); } - logger.trace("Following hosts got reset: {}", sb); + HostVO host = createForUpdate(); + host.setManagementServerId(null); + UpdateBuilder ub = getUpdateBuilder(host); + update(ub, sc, null); } /* diff --git a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupWorkDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupWorkDaoImpl.java index 327d12c759a7..3180ef30a3ce 100644 --- a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupWorkDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupWorkDaoImpl.java @@ -116,7 +116,7 @@ public SecurityGroupWorkVO take(long serverId) { //ensure that there is no job in Processing state for the same VM processing = true; if (logger.isTraceEnabled()) { - logger.trace("Security Group work take: found a job in Scheduled and Processing vmid=" + work.getInstanceId()); + logger.trace("Security Group work take: found a job in Scheduled and Processing vmid={}", work.getInstanceId()); } } work.setServerId(serverId); @@ -141,26 +141,16 @@ public SecurityGroupWorkVO take(long serverId) { } @Override - @DB public void updateStep(Long vmId, Long logSequenceNumber, Step step) { - final TransactionLegacy txn = TransactionLegacy.currentTxn(); - txn.start(); SearchCriteria sc = VmIdSeqNumSearch.create(); sc.setParameters("vmId", vmId); sc.setParameters("seqno", logSequenceNumber); - final Filter filter = new Filter(SecurityGroupWorkVO.class, null, true, 0l, 1l); - - final List vos = lockRows(sc, filter, true); - if (vos.size() == 0) { - txn.commit(); - return; - } - SecurityGroupWorkVO work = vos.get(0); - work.setStep(step); - update(work.getId(), work); - - txn.commit(); + SecurityGroupWorkVO workForUpdate = createForUpdate(); + workForUpdate.setStep(step); + // LIMIT 1 preserves the original single-row semantics: op_nwgrp_work has no + // uniqueness on (instance_id, seq_no), so without it duplicate rows would all be updated. + update(workForUpdate, sc, 1); } @Override @@ -172,21 +162,10 @@ public SecurityGroupWorkVO findByVmIdStep(long vmId, Step step) { } @Override - @DB public void updateStep(Long workId, Step step) { - final TransactionLegacy txn = TransactionLegacy.currentTxn(); - txn.start(); - - SecurityGroupWorkVO work = lockRow(workId, true); - if (work == null) { - txn.commit(); - return; - } - work.setStep(step); - update(work.getId(), work); - - txn.commit(); - + SecurityGroupWorkVO workForUpdate = createForUpdate(); + workForUpdate.setStep(step); + update(workId, workForUpdate); } @Override From ea6cbada9b2faf7e55bbd6273af4e86ef68b363d Mon Sep 17 00:00:00 2001 From: Daman Arora <61474540+Damans227@users.noreply.github.com> Date: Wed, 24 Jun 2026 07:08:26 -0400 Subject: [PATCH 123/205] Multiple CD-ROM / ISO Support Per VM (#13101) * pre-allocate a second empty cdrom slot at boot (hardcoded) * drive cdrom slot count via vm.cdrom.max.count ConfigKey * add vm_iso_map table + VO/DAO * persist multi-ISO state via vm_iso_map * carry target cdrom slot through AttachCommand to KVM agent * enforce per-VM cdrom cap, clamp to hypervisor max * make detachIso accepts an ISO id * expose attached ISOs as isos[] in listVirtualMachines response * extract CDROM_PRIMARY_DEVICE_SEQ constant * unit tests for cdrom slot allocation logic * implement multi-ISO attachment and detachment for VMs with enhanced validation * implement multi-ISO display in InstanceTab with computed property for attached ISOs * add warning alert for max CDROM selections and enhance global capacity fetching * enhance ISO attachment validation to handle multiple ISOs and prevent duplicates * refactor ISO attachment logic for detachment and validation * add unit tests for ISO detachment resolution and validation logic * add mock for VmIsoMapDao in UserVmJoinDaoImplTest and set lenient behavior for listByVmId * refactor ISO attachment logic and enhance UI for multi-CDROM management * refactor ISO attachment methods to use VM ID and improve parameter handling * remove unnecessary mock for VM ISO mapping in TemplateManagerImplTest * add 'since' attribute to ISO detach command parameter description * scope vm.cdrom.max.count to cluster * add support for configurable CD-ROM count per VM and improve handling in TemplateManager * add HostDetailsDao mock to UserVmJoinDaoImplTest * fix: handle null poolId when loading attached ISO slots in prepareIsoForVmProfile * implement listByIsoId method in VmIsoMapDao and update TemplateManagerImpl for ISO deletion checks * improve logging messages for ISO deletion checks * add unit tests for CD-ROM handling and enforce limits in TemplateManager * refactor: update configuration value handling and improve notification logic * refactor: rename CD-ROM references to ISO and update related logic * refactor: enhance effective CD-ROM max count logic to handle missing host IDs and improve cluster ID retrieval * refactor: enhance effective CD-ROM max count logic to handle misconfigurations during VM boot * refactor: enhance effective CD-ROM max count logic to retrieve host ID from candidates based on hypervisor type * refactor: enhance host ID retrieval logic for VMs based on hypervisor type * feat: add bootable ISO flag to AttachedIsoResponse and update UI to display it * refactor: simplify effectiveMaxCdroms method and improve logging for CD-ROM capacity * test: update AttachedIsoResponseTest to include bootable flag in constructor tests * feat: include bootable flag in AttachedIsoResponse for user VMs * feat: enhance CD-ROM management by defining empty slots for user VMs --- api/src/main/java/com/cloud/host/Host.java | 1 + .../api/command/user/iso/DetachIsoCmd.java | 7 +- .../api/response/AttachedIsoResponse.java | 76 +++++ .../api/response/UserVmResponse.java | 24 ++ .../api/response/AttachedIsoResponseTest.java | 46 +++ .../com/cloud/template/TemplateManager.java | 15 + .../main/java/com/cloud/vm/VmIsoMapVO.java | 83 ++++++ .../java/com/cloud/vm/dao/VmIsoMapDao.java | 34 +++ .../com/cloud/vm/dao/VmIsoMapDaoImpl.java | 92 ++++++ ...spring-engine-schema-core-daos-context.xml | 1 + .../META-INF/db/schema-42210to42300.sql | 14 + .../java/com/cloud/vm/VmIsoMapVOTest.java | 41 +++ .../resource/LibvirtComputingResource.java | 16 + .../hypervisor/kvm/resource/LibvirtVMDef.java | 4 + .../kvm/storage/KVMStorageProcessor.java | 15 +- .../api/query/dao/UserVmJoinDaoImpl.java | 70 +++++ .../cloud/template/TemplateManagerImpl.java | 278 +++++++++++++++--- .../api/query/dao/UserVmJoinDaoImplTest.java | 46 +++ .../template/TemplateManagerImplTest.java | 243 +++++++++++++++ ui/src/config/section/compute.js | 28 +- ui/src/views/compute/AttachIso.vue | 112 ++++--- ui/src/views/compute/DetachIso.vue | 178 +++++++++++ ui/src/views/compute/InstanceTab.vue | 34 ++- ui/src/views/setting/ConfigurationValue.vue | 5 +- 24 files changed, 1353 insertions(+), 110 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/AttachedIsoResponse.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/response/AttachedIsoResponseTest.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/VmIsoMapVO.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDao.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDaoImpl.java create mode 100644 engine/schema/src/test/java/com/cloud/vm/VmIsoMapVOTest.java create mode 100644 ui/src/views/compute/DetachIso.vue diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index 8b14cfd3a390..c110e4ca94e1 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -63,6 +63,7 @@ public static String[] toStrings(Host.Type... types) { String HOST_OVFTOOL_VERSION = "host.ovftool.version"; String HOST_VIRTV2V_VERSION = "host.virtv2v.version"; String HOST_SSH_PORT = "host.ssh.port"; + String HOST_CDROM_MAX_COUNT = "host.cdrom.max.count"; String GUEST_OS_CATEGORY_ID = "guest.os.category.id"; String GUEST_OS_RULE = "guest.os.rule"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DetachIsoCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DetachIsoCmd.java index cf4aa41f795c..2560d837de12 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DetachIsoCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DetachIsoCmd.java @@ -27,6 +27,7 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserVmResponse; import com.cloud.event.EventTypes; @@ -51,6 +52,10 @@ public class DetachIsoCmd extends BaseAsyncCmd implements UserCmd { description = "If true, ejects the ISO before detaching on VMware. Default: false", since = "4.15.1") protected Boolean forced; + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = TemplateResponse.class, + description = "The ID of the ISO to detach. Required when the Instance has more than one ISO attached.", since = "4.23.0") + protected Long id; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -104,7 +109,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() { - boolean result = _templateService.detachIso(virtualMachineId, null, isForced()); + boolean result = _templateService.detachIso(virtualMachineId, id, isForced()); if (result) { UserVm userVm = _entityMgr.findById(UserVm.class, virtualMachineId); UserVmResponse response = _responseGenerator.createUserVmResponse(getResponseView(), "virtualmachine", userVm).get(0); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/AttachedIsoResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/AttachedIsoResponse.java new file mode 100644 index 000000000000..b259de56218b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/AttachedIsoResponse.java @@ -0,0 +1,76 @@ +// 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.api.response; + +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class AttachedIsoResponse extends BaseResponse { + + @SerializedName("id") + @Param(description = "The ID of the attached ISO") + private String id; + + @SerializedName("name") + @Param(description = "The name of the attached ISO") + private String name; + + @SerializedName("displaytext") + @Param(description = "The display text of the attached ISO") + private String displayText; + + @SerializedName("deviceseq") + @Param(description = "The cdrom slot that holds this ISO (3=hdc, 4=hdd, ...)") + private Integer deviceSeq; + + @SerializedName("bootable") + @Param(description = "Whether this is the bootable ISO for the VM") + private Boolean bootable; + + public AttachedIsoResponse() { + } + + public AttachedIsoResponse(String id, String name, String displayText, Integer deviceSeq, boolean bootable) { + this.id = id; + this.name = name; + this.displayText = displayText; + this.deviceSeq = deviceSeq; + this.bootable = bootable; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDisplayText() { + return displayText; + } + + public Integer getDeviceSeq() { + return deviceSeq; + } + + public Boolean getBootable() { + return bootable; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index a7f6dff96f88..4d6eae2fad23 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -166,6 +166,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "An alternate display text of the ISO attached to the Instance") private String isoDisplayText; + @SerializedName("isos") + @Param(description = "All ISOs attached to the Instance, keyed by cdrom slot. The first entry mirrors isoid/isoname for back-compat.", responseObject = AttachedIsoResponse.class, since = "4.23.0") + private List isos; + + @SerializedName("isomaxcount") + @Param(description = "Maximum number of ISOs that may be attached to this Instance, after applying the cluster-scoped vm.iso.max.count and the hypervisor's own cap.", since = "4.23.0") + private Integer isoMaxCount; + @SerializedName(ApiConstants.SERVICE_OFFERING_ID) @Param(description = "The ID of the service offering of the Instance") private String serviceOfferingId; @@ -871,6 +879,22 @@ public void setIsoId(String isoId) { this.isoId = isoId; } + public void setIsos(List isos) { + this.isos = isos; + } + + public List getIsos() { + return isos; + } + + public void setIsoMaxCount(Integer isoMaxCount) { + this.isoMaxCount = isoMaxCount; + } + + public Integer getIsoMaxCount() { + return isoMaxCount; + } + public void setIsoName(String isoName) { this.isoName = isoName; } diff --git a/api/src/test/java/org/apache/cloudstack/api/response/AttachedIsoResponseTest.java b/api/src/test/java/org/apache/cloudstack/api/response/AttachedIsoResponseTest.java new file mode 100644 index 000000000000..09d4eb598ab0 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/response/AttachedIsoResponseTest.java @@ -0,0 +1,46 @@ +// 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.api.response; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public final class AttachedIsoResponseTest { + + @Test + public void testFullConstructorPopulatesAllFields() { + AttachedIsoResponse response = new AttachedIsoResponse("uuid-1", "alpine-iso", "Alpine boot", 3, true); + Assert.assertEquals("uuid-1", response.getId()); + Assert.assertEquals("alpine-iso", response.getName()); + Assert.assertEquals("Alpine boot", response.getDisplayText()); + Assert.assertEquals(Integer.valueOf(3), response.getDeviceSeq()); + Assert.assertTrue(response.getBootable()); + } + + @Test + public void testNoArgConstructorLeavesFieldsNull() { + AttachedIsoResponse response = new AttachedIsoResponse(); + Assert.assertNull(response.getId()); + Assert.assertNull(response.getName()); + Assert.assertNull(response.getDisplayText()); + Assert.assertNull(response.getDeviceSeq()); + Assert.assertNull(response.getBootable()); + } +} diff --git a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java index f1891c774edd..24d7bf621f60 100644 --- a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java +++ b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java @@ -64,6 +64,21 @@ public interface TemplateManager { true, ConfigKey.Scope.Global); + ConfigKey VmIsoMaxCount = new ConfigKey("Advanced", + Integer.class, + "vm.iso.max.count", "1", + "Maximum number of ISOs that may be attached to a VM.", + true, + ConfigKey.Scope.Cluster); + + // KVM/libvirt maps deviceSeq=3 to hdc (hda/hdb are taken by the root volume on i440fx/IDE). + // user_vm.iso_id has always pointed at this slot; additional cdroms live in vm_iso_map. + int CDROM_PRIMARY_DEVICE_SEQ = 3; + + // Fallback per-VM cdrom cap when the placement host hasn't advertised host.cdrom.max.count + // (older agent, never-deployed VM, etc.). + int DEFAULT_CDROM_MAX_PER_VM = 1; + static final String VMWARE_TOOLS_ISO = "vmware-tools.iso"; static final String XS_TOOLS_ISO = "xs-tools.iso"; diff --git a/engine/schema/src/main/java/com/cloud/vm/VmIsoMapVO.java b/engine/schema/src/main/java/com/cloud/vm/VmIsoMapVO.java new file mode 100644 index 000000000000..f4a3f1168188 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/VmIsoMapVO.java @@ -0,0 +1,83 @@ +// 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 com.cloud.vm; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.api.InternalIdentity; + +@Entity +@Table(name = "vm_iso_map") +public class VmIsoMapVO implements InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "vm_id") + private long vmId; + + @Column(name = "iso_id") + private long isoId; + + @Column(name = "device_seq") + private int deviceSeq; + + @Column(name = "created") + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + public VmIsoMapVO() { + } + + public VmIsoMapVO(long vmId, long isoId, int deviceSeq) { + this.vmId = vmId; + this.isoId = isoId; + this.deviceSeq = deviceSeq; + this.created = new Date(); + } + + @Override + public long getId() { + return id; + } + + public long getVmId() { + return vmId; + } + + public long getIsoId() { + return isoId; + } + + public int getDeviceSeq() { + return deviceSeq; + } + + public Date getCreated() { + return created; + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDao.java new file mode 100644 index 000000000000..a472a3b4dece --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDao.java @@ -0,0 +1,34 @@ +// 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 com.cloud.vm.dao; + +import java.util.List; + +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.VmIsoMapVO; + +public interface VmIsoMapDao extends GenericDao { + List listByVmId(long vmId); + + List listByIsoId(long isoId); + + VmIsoMapVO findByVmIdDeviceSeq(long vmId, int deviceSeq); + + VmIsoMapVO findByVmIdIsoId(long vmId, long isoId); + + int removeByVmId(long vmId); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDaoImpl.java new file mode 100644 index 000000000000..44749eea75f1 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDaoImpl.java @@ -0,0 +1,92 @@ +// 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 com.cloud.vm.dao; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VmIsoMapVO; + +@Component +public class VmIsoMapDaoImpl extends GenericDaoBase implements VmIsoMapDao { + + private SearchBuilder ListByVmId; + private SearchBuilder ListByIsoId; + private SearchBuilder ByVmIdDeviceSeq; + private SearchBuilder ByVmIdIsoId; + + protected VmIsoMapDaoImpl() { + ListByVmId = createSearchBuilder(); + ListByVmId.and("vmId", ListByVmId.entity().getVmId(), SearchCriteria.Op.EQ); + ListByVmId.done(); + + ListByIsoId = createSearchBuilder(); + ListByIsoId.and("isoId", ListByIsoId.entity().getIsoId(), SearchCriteria.Op.EQ); + ListByIsoId.done(); + + ByVmIdDeviceSeq = createSearchBuilder(); + ByVmIdDeviceSeq.and("vmId", ByVmIdDeviceSeq.entity().getVmId(), SearchCriteria.Op.EQ); + ByVmIdDeviceSeq.and("deviceSeq", ByVmIdDeviceSeq.entity().getDeviceSeq(), SearchCriteria.Op.EQ); + ByVmIdDeviceSeq.done(); + + ByVmIdIsoId = createSearchBuilder(); + ByVmIdIsoId.and("vmId", ByVmIdIsoId.entity().getVmId(), SearchCriteria.Op.EQ); + ByVmIdIsoId.and("isoId", ByVmIdIsoId.entity().getIsoId(), SearchCriteria.Op.EQ); + ByVmIdIsoId.done(); + } + + @Override + public List listByVmId(long vmId) { + SearchCriteria sc = ListByVmId.create(); + sc.setParameters("vmId", vmId); + return listBy(sc); + } + + @Override + public List listByIsoId(long isoId) { + SearchCriteria sc = ListByIsoId.create(); + sc.setParameters("isoId", isoId); + return listBy(sc); + } + + @Override + public VmIsoMapVO findByVmIdDeviceSeq(long vmId, int deviceSeq) { + SearchCriteria sc = ByVmIdDeviceSeq.create(); + sc.setParameters("vmId", vmId); + sc.setParameters("deviceSeq", deviceSeq); + return findOneBy(sc); + } + + @Override + public VmIsoMapVO findByVmIdIsoId(long vmId, long isoId) { + SearchCriteria sc = ByVmIdIsoId.create(); + sc.setParameters("vmId", vmId); + sc.setParameters("isoId", isoId); + return findOneBy(sc); + } + + @Override + public int removeByVmId(long vmId) { + SearchCriteria sc = ListByVmId.create(); + sc.setParameters("vmId", vmId); + return remove(sc); + } +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 26181d3fce0d..3f72ad9dfc8d 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -108,6 +108,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index bd5ecbab21ca..31e7e237afb2 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -136,6 +136,20 @@ CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` ( CONSTRAINT `fk_quota_tariff_usage__tariff_id` FOREIGN KEY (`tariff_id`) REFERENCES `cloud_usage`.`quota_tariff` (`id`), CONSTRAINT `fk_quota_tariff_usage__quota_usage_id` FOREIGN KEY (`quota_usage_id`) REFERENCES `cloud_usage`.`quota_usage` (`id`)); +--- Per-VM ISO attachments. user_vm.iso_id remains as the primary/bootable ISO pointer. +CREATE TABLE IF NOT EXISTS `cloud`.`vm_iso_map` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `vm_id` bigint(20) unsigned NOT NULL COMMENT 'foreign key to user_vm', + `iso_id` bigint(20) unsigned NOT NULL COMMENT 'foreign key to vm_template (ISOs are templates of format ISO)', + `device_seq` int(10) unsigned NOT NULL COMMENT 'cdrom slot index used to derive the libvirt device label (3=hdc, 4=hdd)', + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uc_vm_iso_map__vm_iso` (`vm_id`, `iso_id`), + UNIQUE KEY `uc_vm_iso_map__vm_seq` (`vm_id`, `device_seq`), + CONSTRAINT `fk_vm_iso_map__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `cloud`.`user_vm` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_vm_iso_map__iso_id` FOREIGN KEY (`iso_id`) REFERENCES `cloud`.`vm_template` (`id`) +); + -- Add the 'keep_mac_address_on_public_nic' column to the 'cloud.networks' and 'cloud.vpc' tables CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.networks', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1'); diff --git a/engine/schema/src/test/java/com/cloud/vm/VmIsoMapVOTest.java b/engine/schema/src/test/java/com/cloud/vm/VmIsoMapVOTest.java new file mode 100644 index 000000000000..d5b1fef7a766 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/VmIsoMapVOTest.java @@ -0,0 +1,41 @@ +// 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 com.cloud.vm; + +import org.junit.Assert; +import org.junit.Test; + +public class VmIsoMapVOTest { + + @Test + public void testFullConstructorPopulatesAllFields() { + VmIsoMapVO row = new VmIsoMapVO(7L, 42L, 4); + Assert.assertEquals(7L, row.getVmId()); + Assert.assertEquals(42L, row.getIsoId()); + Assert.assertEquals(4, row.getDeviceSeq()); + Assert.assertNotNull(row.getCreated()); + } + + @Test + public void testNoArgConstructorLeavesNonIdFieldsAtDefaults() { + VmIsoMapVO row = new VmIsoMapVO(); + Assert.assertEquals(0L, row.getVmId()); + Assert.assertEquals(0L, row.getIsoId()); + Assert.assertEquals(0, row.getDeviceSeq()); + Assert.assertNull(row.getCreated()); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 4a93b1bce4a1..41716881fa4a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.hypervisor.kvm.resource; +import static com.cloud.host.Host.HOST_CDROM_MAX_COUNT; import static com.cloud.host.Host.HOST_INSTANCE_CONVERSION; import static com.cloud.host.Host.HOST_OVFTOOL_VERSION; import static com.cloud.host.Host.HOST_VDDK_LIB_DIR; @@ -226,6 +227,7 @@ import com.cloud.resource.ServerResource; import com.cloud.resource.ServerResourceBase; import com.cloud.storage.JavaStorageLayer; +import com.cloud.template.TemplateManager; import com.cloud.storage.Storage; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StorageLayer; @@ -3696,6 +3698,7 @@ public int compare(final DiskTO arg0, final DiskTO arg1) { if (vmSpec.getOs().toLowerCase().contains("window")) { isWindowsTemplate = true; } + final Set definedCdromSlots = new HashSet<>(); for (final DiskTO volume : disks) { KVMPhysicalDisk physicalDisk = null; KVMStoragePool pool = null; @@ -3774,6 +3777,7 @@ public int compare(final DiskTO arg0, final DiskTO arg1) { if (volume.getType() == Volume.Type.ISO) { final DiskDef.DiskType diskType = getDiskType(physicalDisk); disk.defISODisk(volPath, devId, isUefiEnabled, diskType); + definedCdromSlots.add(devId); if (guestCpuArch != null && (guestCpuArch.equals("aarch64") || guestCpuArch.equals("s390x"))) { disk.setBusType(DiskDef.DiskBus.SCSI); @@ -3871,6 +3875,17 @@ public int compare(final DiskTO arg0, final DiskTO arg1) { vm.getDevices().addDevice(disk); } + if (vmSpec.getType() == VirtualMachine.Type.User) { + for (int slot = TemplateManager.CDROM_PRIMARY_DEVICE_SEQ; + slot < TemplateManager.CDROM_PRIMARY_DEVICE_SEQ + LibvirtVMDef.MAX_CDROMS_PER_VM; slot++) { + if (!definedCdromSlots.contains(slot)) { + final DiskDef emptyCdrom = new DiskDef(); + emptyCdrom.defISODisk(null, slot, isUefiEnabled, DiskDef.DiskType.FILE); + vm.getDevices().addDevice(emptyCdrom); + } + } + } + if (vmSpec.getType() != VirtualMachine.Type.User) { final DiskDef iso = new DiskDef(); iso.defISODisk(sysvmISOPath, DiskDef.DiskType.FILE); @@ -4381,6 +4396,7 @@ public StartupCommand[] initialize() { boolean instanceConversionSupported = hostSupportsInstanceConversion(); cmd.getHostDetails().put(HOST_INSTANCE_CONVERSION, String.valueOf(instanceConversionSupported)); cmd.getHostDetails().put(HOST_VDDK_SUPPORT, String.valueOf(hostSupportsVddk())); + cmd.getHostDetails().put(HOST_CDROM_MAX_COUNT, String.valueOf(LibvirtVMDef.MAX_CDROMS_PER_VM)); if (StringUtils.isNotBlank(vddkLibDir)) { cmd.getHostDetails().put(HOST_VDDK_LIB_DIR, vddkLibDir); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java index bf8b1af6c18d..7f6725b6d152 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java @@ -57,6 +57,10 @@ public class LibvirtVMDef { protected static Logger LOGGER = LogManager.getLogger(LibvirtVMDef.class); + // CD-ROM slot allocation: getDevLabel() maps deviceSeq=3,4 to hdc and hdd on the IDE bus. + // Bumping this requires extending getDevLabel() (e.g. to spill onto SATA or a second IDE controller). + public static final int MAX_CDROMS_PER_VM = 2; + private String _hvsType; private static long s_libvirtVersion; private static long s_qemuVersion; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 4a77f7e9e19c..009e1decee2b 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -21,6 +21,8 @@ import static com.cloud.utils.NumbersUtil.toHumanReadableSize; import static com.cloud.utils.storage.S3.S3Utils.putFile; +import com.cloud.template.TemplateManager; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -1346,10 +1348,11 @@ private String computeMd5Hash(String input) { } } - protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map params, DataStoreTO store) throws + protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map params, DataStoreTO store, Integer deviceSeq) throws LibvirtException, InternalErrorException { DiskDef iso = new DiskDef(); boolean isUefiEnabled = MapUtils.isNotEmpty(params) && params.containsKey("UEFI"); + Integer devId = (deviceSeq != null) ? deviceSeq : TemplateManager.CDROM_PRIMARY_DEVICE_SEQ; if (isoPath != null && isAttach) { final int index = isoPath.lastIndexOf("/"); final String path = isoPath.substring(0, index); @@ -1365,9 +1368,9 @@ protected synchronized void attachOrDetachISO(final Connect conn, final String v final DiskDef.DiskType isoDiskType = LibvirtComputingResource.getDiskType(isoVol); isoPath = isoVol.getPath(); - iso.defISODisk(isoPath, isUefiEnabled, isoDiskType); + iso.defISODisk(isoPath, devId, isUefiEnabled, isoDiskType); } else { - iso.defISODisk(null, isUefiEnabled, DiskDef.DiskType.FILE); + iso.defISODisk(null, devId, isUefiEnabled, DiskDef.DiskType.FILE); } final List disks = resource.getDisks(conn, vmName); @@ -1387,11 +1390,12 @@ public Answer attachIso(final AttachCommand cmd) { final DiskTO disk = cmd.getDisk(); final TemplateObjectTO isoTO = (TemplateObjectTO)disk.getData(); final DataStoreTO store = isoTO.getDataStore(); + final Integer deviceSeq = (disk.getDiskSeq() != null) ? disk.getDiskSeq().intValue() : null; try { String dataStoreUrl = getDataStoreUrlFromStore(store); final Connect conn = LibvirtConnection.getConnectionByVmName(cmd.getVmName()); - attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), true, cmd.getControllerInfo(), store); + attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), true, cmd.getControllerInfo(), store, deviceSeq); } catch (final LibvirtException e) { return new Answer(cmd, false, e.toString()); } catch (final InternalErrorException e) { @@ -1408,11 +1412,12 @@ public Answer dettachIso(final DettachCommand cmd) { final DiskTO disk = cmd.getDisk(); final TemplateObjectTO isoTO = (TemplateObjectTO)disk.getData(); final DataStoreTO store = isoTO.getDataStore(); + final Integer deviceSeq = (disk.getDiskSeq() != null) ? disk.getDiskSeq().intValue() : null; try { String dataStoreUrl = getDataStoreUrlFromStore(store); final Connect conn = LibvirtConnection.getConnectionByVmName(cmd.getVmName()); - attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), false, cmd.getParams(), store); + attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), false, cmd.getParams(), store, deviceSeq); } catch (final LibvirtException e) { return new Answer(cmd, false, e.toString()); } catch (final InternalErrorException e) { diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 4877eb844af1..aeb54de12909 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -39,6 +39,7 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants.VMDetails; import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.response.AttachedIsoResponse; import org.apache.cloudstack.api.response.NicExtraDhcpOptionResponse; import org.apache.cloudstack.api.response.NicResponse; import org.apache.cloudstack.api.response.NicSecondaryIpResponse; @@ -62,6 +63,11 @@ import com.cloud.gpu.dao.VgpuProfileDao; import com.cloud.host.ControlState; import com.cloud.hypervisor.Hypervisor; +import com.cloud.host.DetailVO; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.network.IpAddress; import com.cloud.network.vpc.VpcVO; import com.cloud.network.vpc.dao.VpcDao; @@ -72,6 +78,7 @@ import com.cloud.storage.Storage.TemplateType; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.template.TemplateManager; import com.cloud.storage.VnfTemplateNicVO; import com.cloud.storage.Volume; import com.cloud.storage.dao.VMTemplateDao; @@ -93,10 +100,12 @@ import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; +import com.cloud.vm.VmIsoMapVO; import com.cloud.vm.VmStats; import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.VMInstanceDetailsDao; +import com.cloud.vm.dao.VmIsoMapDao; @Component public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation implements UserVmJoinDao { @@ -130,6 +139,12 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation VmDetailSearch; @@ -246,6 +261,23 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us userVmResponse.setIsoId(userVm.getIsoUuid()); userVmResponse.setIsoName(userVm.getIsoName()); userVmResponse.setIsoDisplayText(userVm.getIsoDisplayText()); + + List attachedIsos = new ArrayList<>(); + if (userVm.getIsoUuid() != null) { + VMTemplateVO bootIso = vmTemplateDao.findById(userVm.getIsoId()); + boolean bootIsoBootable = bootIso != null && bootIso.isBootable(); + attachedIsos.add(new AttachedIsoResponse(userVm.getIsoUuid(), userVm.getIsoName(), + userVm.getIsoDisplayText(), TemplateManager.CDROM_PRIMARY_DEVICE_SEQ, bootIsoBootable)); + } + for (VmIsoMapVO row : vmIsoMapDao.listByVmId(userVm.getId())) { + VMTemplateVO tmpl = vmTemplateDao.findById(row.getIsoId()); + if (tmpl != null) { + attachedIsos.add(new AttachedIsoResponse(tmpl.getUuid(), tmpl.getName(), + tmpl.getDisplayText(), row.getDeviceSeq(), false)); + } + } + userVmResponse.setIsos(attachedIsos); + userVmResponse.setIsoMaxCount(effectiveCdromMaxCount(userVm)); } if (details.contains(VMDetails.all) || details.contains(VMDetails.servoff)) { userVmResponse.setServiceOfferingId(userVm.getServiceOfferingUuid()); @@ -540,6 +572,44 @@ private long computeLeaseDurationFromExpiryDate(Date created, Date leaseExpiryDa return ChronoUnit.DAYS.between(createdDate, expiryDate); } + int effectiveCdromMaxCount(UserVmJoinVO userVm) { + Long hostId = userVm.getHostId() != null && userVm.getHostId() > 0 + ? userVm.getHostId() : userVm.getLastHostId(); + if (hostId == null && userVm.getHypervisorType() != null) { + List candidates = hostDao.listByDataCenterIdAndHypervisorType(userVm.getDataCenterId(), userVm.getHypervisorType()); + if (!candidates.isEmpty()) { + hostId = candidates.get(0).getId(); + } + } + Long clusterId = userVm.getClusterId(); + if (clusterId == null && hostId != null) { + HostVO host = hostDao.findById(hostId); + if (host != null) { + clusterId = host.getClusterId(); + } + } + int configuredCap = TemplateManager.VmIsoMaxCount.valueIn(clusterId); + int hypervisorCap = advertisedCdromCap(hostId); + // List endpoint clamps for display robustness; the action paths in TemplateManagerImpl + // throw on misconfiguration so operators still see the loud error when they try to attach. + return Math.min(configuredCap, hypervisorCap); + } + + int advertisedCdromCap(Long hostId) { + if (hostId == null) { + return TemplateManager.DEFAULT_CDROM_MAX_PER_VM; + } + DetailVO detail = hostDetailsDao.findDetail(hostId, Host.HOST_CDROM_MAX_COUNT); + if (detail == null || detail.getValue() == null) { + return TemplateManager.DEFAULT_CDROM_MAX_PER_VM; + } + try { + return Integer.parseInt(detail.getValue()); + } catch (NumberFormatException e) { + return TemplateManager.DEFAULT_CDROM_MAX_PER_VM; + } + } + private void addVnfInfoToserVmResponse(UserVmJoinVO userVm, UserVmResponse userVmResponse) { List vnfNics = vnfTemplateNicDao.listByTemplateId(userVm.getTemplateId()); for (VnfTemplateNicVO nic : vnfNics) { diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 3aaebc691309..6cac485c4e1e 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -21,6 +21,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -145,8 +146,11 @@ import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.StorageUnavailableException; +import com.cloud.host.DetailVO; +import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.HypervisorGuru; @@ -222,7 +226,9 @@ import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.VirtualMachineProfileImpl; import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.VmIsoMapVO; import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VmIsoMapDao; import com.cloud.vm.dao.VMInstanceDao; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -252,10 +258,14 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, @Inject private HostDao _hostDao; @Inject + private HostDetailsDao _hostDetailsDao; + @Inject private DataCenterDao _dcDao; @Inject private UserVmDao _userVmDao; @Inject + private VmIsoMapDao _vmIsoMapDao; + @Inject private VolumeDao _volumeDao; @Inject private SnapshotDao _snapshotDao; @@ -679,45 +689,73 @@ private String extract(Account caller, Long templateId, String url, Long zoneId, @Override public void prepareIsoForVmProfile(VirtualMachineProfile profile, DeployDestination dest) { UserVmVO vm = _userVmDao.findById(profile.getId()); - if (vm.getIsoId() != null) { - Map storageForDisks = dest.getStorageForDisks(); - Long poolId = null; - TemplateInfo template; - if (MapUtils.isNotEmpty(storageForDisks)) { - for (StoragePool storagePool : storageForDisks.values()) { - if (poolId != null && storagePool.getId() != poolId) { - throw new CloudRuntimeException("Cannot determine where to download ISO"); - } - poolId = storagePool.getId(); + Map slotToIsoId = loadAttachedIsoSlots(vm); + Long poolId = slotToIsoId.isEmpty() ? null : singleStoragePoolId(dest); + + // Pre-allocate every cdrom slot at boot. QEMU/IDE refuses to hot-add new cdrom drives, so + // runtime attachIso can only media-swap into a slot the domain already owns. + int totalSlots = Math.max(effectiveMaxCdroms(vm, dest.getHost().getId()), slotsNeededFor(slotToIsoId)); + for (int i = 0; i < totalSlots; i++) { + int diskSeq = CDROM_PRIMARY_DEVICE_SEQ + i; + Long isoId = slotToIsoId.get(diskSeq); + profile.addDisk(isoId != null + ? buildIsoDisk(profile, vm, dest, poolId, diskSeq, isoId) + : buildEmptyCdromDisk(diskSeq)); + } + } + + private Long singleStoragePoolId(DeployDestination dest) { + Long poolId = null; + Map storageForDisks = dest.getStorageForDisks(); + if (MapUtils.isNotEmpty(storageForDisks)) { + for (StoragePool pool : storageForDisks.values()) { + if (poolId != null && pool.getId() != poolId) { + throw new CloudRuntimeException("Cannot determine where to download ISO"); } + poolId = pool.getId(); } - template = prepareIso(vm.getIsoId(), vm.getDataCenterId(), dest.getHost().getId(), poolId); + } + return poolId; + } - if (template == null){ - logger.error("Failed to prepare ISO on secondary or cache storage"); - throw new CloudRuntimeException("Failed to prepare ISO on secondary or cache storage"); - } - if (template.isBootable()) { - profile.setBootLoaderType(BootloaderType.CD); - } + private Map loadAttachedIsoSlots(UserVmVO vm) { + Map slots = new HashMap<>(); + if (vm.getIsoId() != null) { + slots.put(CDROM_PRIMARY_DEVICE_SEQ, vm.getIsoId()); + } + for (VmIsoMapVO row : _vmIsoMapDao.listByVmId(vm.getId())) { + slots.put(row.getDeviceSeq(), row.getIsoId()); + } + return slots; + } - GuestOSVO guestOS = _guestOSDao.findById(template.getGuestOSId()); - String displayName = null; - if (guestOS != null) { - displayName = guestOS.getDisplayName(); - } + private int slotsNeededFor(Map slotToIsoId) { + if (slotToIsoId.isEmpty()) { + return 0; + } + return Collections.max(slotToIsoId.keySet()) - CDROM_PRIMARY_DEVICE_SEQ + 1; + } - TemplateObjectTO iso = (TemplateObjectTO)template.getTO(); - iso.setDirectDownload(template.isDirectDownload()); - iso.setGuestOsType(displayName); - DiskTO disk = new DiskTO(iso, 3L, null, Volume.Type.ISO); - profile.addDisk(disk); - } else { - TemplateObjectTO iso = new TemplateObjectTO(); - iso.setFormat(ImageFormat.ISO); - DiskTO disk = new DiskTO(iso, 3L, null, Volume.Type.ISO); - profile.addDisk(disk); + private DiskTO buildIsoDisk(VirtualMachineProfile profile, UserVmVO vm, DeployDestination dest, Long poolId, int diskSeq, long isoId) { + TemplateInfo template = prepareIso(isoId, vm.getDataCenterId(), dest.getHost().getId(), poolId); + if (template == null) { + logger.error("Failed to prepare ISO on secondary or cache storage"); + throw new CloudRuntimeException("Failed to prepare ISO on secondary or cache storage"); } + if (diskSeq == CDROM_PRIMARY_DEVICE_SEQ && template.isBootable()) { + profile.setBootLoaderType(BootloaderType.CD); + } + GuestOSVO guestOS = _guestOSDao.findById(template.getGuestOSId()); + TemplateObjectTO iso = (TemplateObjectTO) template.getTO(); + iso.setDirectDownload(template.isDirectDownload()); + iso.setGuestOsType(guestOS != null ? guestOS.getDisplayName() : null); + return new DiskTO(iso, (long) diskSeq, null, Volume.Type.ISO); + } + + private DiskTO buildEmptyCdromDisk(int diskSeq) { + TemplateObjectTO empty = new TemplateObjectTO(); + empty.setFormat(ImageFormat.ISO); + return new DiskTO(empty, (long) diskSeq, null, Volume.Type.ISO); } private void prepareTemplateInOneStoragePool(final VMTemplateVO template, final StoragePoolVO pool) { @@ -1206,17 +1244,20 @@ protected TemplateManagerImpl() { @Override public boolean templateIsDeleteable(long templateId) { + // ISO can only be referenced by user_vm.iso_id (primary cdrom slot) or vm_iso_map (extra slots). + // Templates always live on primary storage and aren't tracked here. List userVmUsingIso = _userVmJoinDao.listActiveByIsoId(templateId); - // check if there is any Vm using this ISO. We only need to check the - // case where templateId is an ISO since - // VM can be launched from ISO in secondary storage, while template will - // always be copied to - // primary storage before deploying VM. if (!userVmUsingIso.isEmpty()) { - logger.debug("ISO " + templateId + " is not deleteable because it is attached to " + userVmUsingIso.size() + " Instances"); + logger.debug("Unable to delete ISO {} because it is attached to {} Instances", templateId, userVmUsingIso.size()); return false; } - + for (VmIsoMapVO row : _vmIsoMapDao.listByIsoId(templateId)) { + UserVmVO vm = _userVmDao.findById(row.getVmId()); + if (vm != null && vm.getState() != State.Error && vm.getState() != State.Expunging) { + logger.debug("Unable to delete ISO {} because it is attached to Instance {} at slot {}", templateId, vm.getUuid(), row.getDeviceSeq()); + return false; + } + } return true; } @@ -1237,7 +1278,14 @@ public boolean detachIso(long vmId, Long isoParamId, Boolean... extraParams) { _accountMgr.checkAccess(caller, null, true, virtualMachine); - Long isoId = !isVirtualRouter ? ((UserVm) virtualMachine).getIsoId() : isoParamId; + Long isoId; + if (isVirtualRouter) { + isoId = isoParamId; + } else { + Long primaryIsoId = ((UserVm) virtualMachine).getIsoId(); + List extras = _vmIsoMapDao.listByVmId(vmId); + isoId = resolveIsoIdForDetach(primaryIsoId, extras, isoParamId); + } if (isoId == null) { throw new InvalidParameterValueException("The specified instance has no ISO attached to it."); } @@ -1321,6 +1369,9 @@ public boolean attachIso(long isoId, long vmId, Boolean... extraParams) { if (VMWARE_TOOLS_ISO.equals(iso.getUniqueName()) && vm.getHypervisorType() != Hypervisor.HypervisorType.VMware) { throw new InvalidParameterValueException("Cannot attach VMware tools drivers to incompatible hypervisor " + vm.getHypervisorType()); } + if (!isVirtualRouter) { + enforceCdromAttachLimits(vmId, (UserVm) vm, isoId); + } boolean result = attachISOToVM(vmId, userId, isoId, true, forced, isVirtualRouter); if (result) { return result; @@ -1360,7 +1411,7 @@ public TemplateInfo prepareIso(long isoId, long dcId, Long hostId, Long poolId) } } - private boolean attachISOToVM(long vmId, long isoId, boolean attach, boolean forced, boolean isVirtualRouter) { + private boolean attachISOToVM(long vmId, long isoId, int deviceSeq, boolean attach, boolean forced, boolean isVirtualRouter) { VirtualMachine vm = !isVirtualRouter ? _userVmDao.findById(vmId) : _vmInstanceDao.findById(vmId); if (vm == null || (isVirtualRouter && vm.getType() != VirtualMachine.Type.DomainRouter)) { @@ -1384,7 +1435,7 @@ private boolean attachISOToVM(long vmId, long isoId, boolean attach, boolean for } DataTO isoTO = tmplt.getTO(); - DiskTO disk = new DiskTO(isoTO, null, null, Volume.Type.ISO); + DiskTO disk = new DiskTO(isoTO, (long) deviceSeq, null, Volume.Type.ISO); HypervisorGuru hvGuru = _hvGuruMgr.getGuru(vm.getHypervisorType()); VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm); @@ -1402,20 +1453,148 @@ private boolean attachISOToVM(long vmId, long isoId, boolean attach, boolean for return (a != null && a.getResult()); } - private boolean attachISOToVM(long vmId, long userId, long isoId, boolean attach, boolean forced, boolean isVirtualRouter) { + boolean attachISOToVM(long vmId, long userId, long isoId, boolean attach, boolean forced, boolean isVirtualRouter) { UserVmVO vm = _userVmDao.findById(vmId); VMTemplateVO iso = _tmpltDao.findById(isoId); - boolean success = attachISOToVM(vmId, isoId, attach, forced, isVirtualRouter); - if (success && attach && !isVirtualRouter) { + int targetSlot = attach ? chooseAttachSlot(vmId, vm) : findAttachedSlot(vmId, vm, isoId); + boolean success = attachISOToVM(vmId, isoId, targetSlot, attach, forced, isVirtualRouter); + if (!success || isVirtualRouter) { + return success; + } + if (attach) { + persistIsoAttachment(vmId, vm, iso, targetSlot); + } else { + persistIsoDetachment(vmId, vm, isoId, targetSlot); + } + return success; + } + + private int chooseAttachSlot(long vmId, UserVmVO vm) { + if (vm.getIsoId() == null) { + return CDROM_PRIMARY_DEVICE_SEQ; + } + VmIsoMapVO highest = highestCdromMapEntry(vmId); + return highest == null ? CDROM_PRIMARY_DEVICE_SEQ + 1 : highest.getDeviceSeq() + 1; + } + + private int findAttachedSlot(long vmId, UserVmVO vm, long isoId) { + if (vm.getIsoId() != null && vm.getIsoId() == isoId) { + return CDROM_PRIMARY_DEVICE_SEQ; + } + VmIsoMapVO entry = _vmIsoMapDao.findByVmIdIsoId(vmId, isoId); + return entry != null ? entry.getDeviceSeq() : CDROM_PRIMARY_DEVICE_SEQ; + } + + private void persistIsoAttachment(long vmId, UserVmVO vm, VMTemplateVO iso, int slot) { + if (slot == CDROM_PRIMARY_DEVICE_SEQ) { vm.setIsoId(iso.getId()); _userVmDao.update(vmId, vm); + } else { + _vmIsoMapDao.persist(new VmIsoMapVO(vmId, iso.getId(), slot)); } - if (success && !attach && !isVirtualRouter) { + } + + private void persistIsoDetachment(long vmId, UserVmVO vm, long isoId, int slot) { + if (slot == CDROM_PRIMARY_DEVICE_SEQ) { vm.setIsoId(null); _userVmDao.update(vmId, vm); + return; } - return success; + VmIsoMapVO entry = _vmIsoMapDao.findByVmIdIsoId(vmId, isoId); + if (entry != null) { + _vmIsoMapDao.remove(entry.getId()); + } + } + + VmIsoMapVO highestCdromMapEntry(long vmId) { + VmIsoMapVO highest = null; + for (VmIsoMapVO row : _vmIsoMapDao.listByVmId(vmId)) { + if (highest == null || row.getDeviceSeq() > highest.getDeviceSeq()) { + highest = row; + } + } + return highest; + } + + Long resolveIsoIdForDetach(Long primaryIsoId, List extras, Long isoParamId) { + if (isoParamId != null) { + boolean attached = (primaryIsoId != null && primaryIsoId.equals(isoParamId)) + || extras.stream().anyMatch(r -> r.getIsoId() == isoParamId); + if (!attached) { + throw new InvalidParameterValueException("The specified ISO is not attached to this Instance."); + } + return isoParamId; + } + int totalAttached = (primaryIsoId != null ? 1 : 0) + extras.size(); + if (totalAttached == 0) { + throw new InvalidParameterValueException("The specified instance has no ISO attached to it."); + } + if (totalAttached > 1) { + throw new InvalidParameterValueException("Instance has more than one ISO attached; specify the 'id' parameter to choose which to detach."); + } + return primaryIsoId != null ? primaryIsoId : extras.get(0).getIsoId(); + } + + boolean isIsoAlreadyAttached(long vmId, Long primaryIsoId, long isoId) { + if (primaryIsoId != null && primaryIsoId.equals(isoId)) { + return true; + } + return _vmIsoMapDao.findByVmIdIsoId(vmId, isoId) != null; + } + + void enforceCdromAttachLimits(long vmId, UserVm vm, long isoId) { + Long primaryIsoId = vm.getIsoId(); + if (isIsoAlreadyAttached(vmId, primaryIsoId, isoId)) { + throw new InvalidParameterValueException("The specified ISO is already attached to this Instance."); + } + int effectiveMax = effectiveMaxCdroms(vm, hostIdForVm(vm)); + int attached = (primaryIsoId != null ? 1 : 0) + _vmIsoMapDao.listByVmId(vmId).size(); + if (attached >= effectiveMax) { + throw new InvalidParameterValueException(String.format( + "Instance has reached the maximum of %d attached CD-ROM(s); detach one before attaching another.", effectiveMax)); + } + } + + int effectiveMaxCdroms(VirtualMachine vm, Long hostId) { + HostVO host = hostId != null ? _hostDao.findById(hostId) : null; + Long clusterId = host != null ? host.getClusterId() : null; + int configuredCap = VmIsoMaxCount.valueIn(clusterId); + int hypervisorCap = advertisedCdromCap(hostId); + if (configuredCap > hypervisorCap) { + logger.warn("{} is set to {} but the placement host supports a maximum of {} CD-ROM(s) per Instance. Clamping to {}.", + VmIsoMaxCount.key(), configuredCap, hypervisorCap, hypervisorCap); + return hypervisorCap; + } + return configuredCap; + } + + int advertisedCdromCap(Long hostId) { + if (hostId == null) { + return DEFAULT_CDROM_MAX_PER_VM; + } + DetailVO detail = _hostDetailsDao.findDetail(hostId, Host.HOST_CDROM_MAX_COUNT); + if (detail == null || detail.getValue() == null) { + return DEFAULT_CDROM_MAX_PER_VM; + } + try { + return Integer.parseInt(detail.getValue()); + } catch (NumberFormatException e) { + logger.warn("Invalid {} value '{}' for host {}; using default {}.", + Host.HOST_CDROM_MAX_COUNT, detail.getValue(), hostId, DEFAULT_CDROM_MAX_PER_VM); + return DEFAULT_CDROM_MAX_PER_VM; + } + } + + Long hostIdForVm(VirtualMachine vm) { + Long hostId = vm.getHostId() != null ? vm.getHostId() : vm.getLastHostId(); + if (hostId == null && vm.getHypervisorType() != null) { + List candidates = _hostDao.listByDataCenterIdAndHypervisorType(vm.getDataCenterId(), vm.getHypervisorType()); + if (!candidates.isEmpty()) { + hostId = candidates.get(0).getId(); + } + } + return hostId; } @Override @@ -2538,7 +2717,8 @@ public ConfigKey[] getConfigKeys() { return new ConfigKey[] {AllowPublicUserTemplates, TemplatePreloaderPoolSize, ValidateUrlIsResolvableBeforeRegisteringTemplate, - TemplateDeleteFromPrimaryStorage}; + TemplateDeleteFromPrimaryStorage, + VmIsoMaxCount}; } public List getTemplateAdapters() { diff --git a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java index e4146fd22657..f657a8bbf045 100755 --- a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java +++ b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java @@ -16,10 +16,12 @@ // under the License. package com.cloud.api.query.dao; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.MockitoAnnotations.openMocks; import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import com.cloud.storage.dao.VMTemplateDao; @@ -49,9 +51,11 @@ import com.cloud.user.UserStatisticsVO; import com.cloud.user.dao.UserDao; import com.cloud.user.dao.UserStatisticsDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.vm.dao.VMInstanceDetailsDao; +import com.cloud.vm.dao.VmIsoMapDao; @RunWith(MockitoJUnitRunner.class) public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseTest { @@ -83,6 +87,12 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT @Mock private VMTemplateDao vmTemplateDao; + @Mock + private VmIsoMapDao vmIsoMapDao; + + @Mock + private HostDetailsDao hostDetailsDao; + @Mock ExtensionHelper extensionHelper; @@ -103,6 +113,7 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT @Before public void setup() { closeable = openMocks(this); + Mockito.lenient().when(vmIsoMapDao.listByVmId(anyLong())).thenReturn(Collections.emptyList()); prepareSetup(); } @@ -166,4 +177,39 @@ public void testNewUserVmResponseForVnfApplianceVnfNics() { Assert.assertEquals(2, response.getVnfNics().size()); Assert.assertEquals(3, response.getVnfDetails().size()); } + + @Test + public void advertisedCdromCapReturnsDefaultWhenHostIdNull() { + Assert.assertEquals(com.cloud.template.TemplateManager.DEFAULT_CDROM_MAX_PER_VM, + _userVmJoinDaoImpl.advertisedCdromCap(null)); + } + + @Test + public void advertisedCdromCapReturnsParsedValue() { + com.cloud.host.DetailVO detail = Mockito.mock(com.cloud.host.DetailVO.class); + Mockito.when(detail.getValue()).thenReturn("2"); + Mockito.when(hostDetailsDao.findDetail(7L, com.cloud.host.Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail); + Assert.assertEquals(2, _userVmJoinDaoImpl.advertisedCdromCap(7L)); + } + + @Test + public void advertisedCdromCapFallsBackOnInvalidValue() { + com.cloud.host.DetailVO detail = Mockito.mock(com.cloud.host.DetailVO.class); + Mockito.when(detail.getValue()).thenReturn("xyz"); + Mockito.when(hostDetailsDao.findDetail(7L, com.cloud.host.Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail); + Assert.assertEquals(com.cloud.template.TemplateManager.DEFAULT_CDROM_MAX_PER_VM, + _userVmJoinDaoImpl.advertisedCdromCap(7L)); + } + + @Test + public void effectiveCdromMaxCountClampsToHypervisorCap() { + UserVmJoinVO userVm = Mockito.mock(UserVmJoinVO.class); + Mockito.when(userVm.getHostId()).thenReturn(7L); + Mockito.when(userVm.getClusterId()).thenReturn(5L); + com.cloud.host.DetailVO detail = Mockito.mock(com.cloud.host.DetailVO.class); + Mockito.when(detail.getValue()).thenReturn("2"); + Mockito.when(hostDetailsDao.findDetail(7L, com.cloud.host.Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail); + // Configured cap defaults to 1 (no cluster override mocked); host advertises 2; clamps to 1. + Assert.assertEquals(1, _userVmJoinDaoImpl.effectiveCdromMaxCount(userVm)); + } } diff --git a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java index 6288180a9f4e..47099c371dce 100755 --- a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java +++ b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java @@ -22,6 +22,7 @@ import com.cloud.agent.AgentManager; import com.cloud.api.query.dao.SnapshotJoinDao; import com.cloud.api.query.dao.UserVmJoinDao; +import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.dc.dao.DataCenterDao; import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao; import com.cloud.domain.dao.DomainDao; @@ -29,7 +30,11 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.ResourceAllocationException; import com.cloud.host.Status; +import com.cloud.host.DetailVO; +import com.cloud.host.Host; +import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.HypervisorGuruManager; import com.cloud.projects.ProjectManager; @@ -66,9 +71,15 @@ import com.cloud.user.dao.AccountDao; import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.uservm.UserVm; +import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachine.State; +import com.cloud.vm.VmIsoMapVO; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.dao.VmIsoMapDao; import junit.framework.TestCase; @@ -133,6 +144,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.UUID; import java.util.concurrent.BlockingQueue; @@ -220,6 +232,21 @@ public class TemplateManagerImplTest extends TestCase { @Mock HeuristicRuleHelper heuristicRuleHelperMock; + @Mock + UserVmDao _userVmDao; + + @Mock + VmIsoMapDao _vmIsoMapDao; + + @Mock + HostDao _hostDao; + + @Mock + HostDetailsDao _hostDetailsDao; + + @Mock + UserVmJoinDao _userVmJoinDao; + public class CustomThreadPoolExecutor extends ThreadPoolExecutor { AtomicInteger ai = new AtomicInteger(0); public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, @@ -750,6 +777,222 @@ public void verifyHeuristicRulesForZoneTestTemplateNotISOFormatShouldCheckForTem Mockito.verify(heuristicRuleHelperMock, Mockito.times(1)).getImageStoreIfThereIsHeuristicRule(1L, HeuristicType.TEMPLATE, vmTemplateVOMock); } + @Test + public void highestCdromMapEntryReturnsNullWhenMapIsEmpty() { + Mockito.when(_vmIsoMapDao.listByVmId(1L)).thenReturn(new ArrayList<>()); + Assert.assertNull(templateManager.highestCdromMapEntry(1L)); + } + + @Test + public void highestCdromMapEntryReturnsEntryWithMaxDeviceSeq() { + VmIsoMapVO low = new VmIsoMapVO(1L, 100L, 4); + VmIsoMapVO high = new VmIsoMapVO(1L, 200L, 5); + Mockito.when(_vmIsoMapDao.listByVmId(1L)).thenReturn(Arrays.asList(low, high)); + VmIsoMapVO result = templateManager.highestCdromMapEntry(1L); + Assert.assertNotNull(result); + Assert.assertEquals(5, result.getDeviceSeq()); + } + + @Test + public void attachISOToVMAttachWritesToIsoIdWhenPrimarySlotEmpty() { + UserVmVO vm = Mockito.mock(UserVmVO.class); + VMTemplateVO iso = Mockito.mock(VMTemplateVO.class); + Mockito.when(_userVmDao.findById(1L)).thenReturn(vm); + Mockito.when(vmTemplateDao.findById(42L)).thenReturn(iso); + Mockito.when(iso.getId()).thenReturn(42L); + Mockito.when(vm.getIsoId()).thenReturn(null); + + boolean result = templateManager.attachISOToVM(1L, 1L, 42L, true, false, false); + + Assert.assertTrue(result); + Mockito.verify(vm).setIsoId(42L); + Mockito.verify(_userVmDao).update(eq(1L), eq(vm)); + Mockito.verify(_vmIsoMapDao, Mockito.never()).persist(any(VmIsoMapVO.class)); + } + + @Test + public void resolveIsoIdForDetachReturnsPrimaryWhenOnlyPrimaryIsAttached() { + Long resolved = templateManager.resolveIsoIdForDetach(99L, new ArrayList<>(), null); + Assert.assertEquals(Long.valueOf(99L), resolved); + } + + @Test + public void resolveIsoIdForDetachReturnsMapEntryWhenOnlyMapHasOne() { + VmIsoMapVO row = new VmIsoMapVO(1L, 100L, 4); + Long resolved = templateManager.resolveIsoIdForDetach(null, Arrays.asList(row), null); + Assert.assertEquals(Long.valueOf(100L), resolved); + } + + @Test(expected = InvalidParameterValueException.class) + public void resolveIsoIdForDetachThrowsWhenMultipleAttachedAndNoIdGiven() { + VmIsoMapVO row = new VmIsoMapVO(1L, 100L, 4); + templateManager.resolveIsoIdForDetach(99L, Arrays.asList(row), null); + } + + @Test(expected = InvalidParameterValueException.class) + public void resolveIsoIdForDetachThrowsWhenNothingAttached() { + templateManager.resolveIsoIdForDetach(null, new ArrayList<>(), null); + } + + @Test(expected = InvalidParameterValueException.class) + public void resolveIsoIdForDetachThrowsWhenIdNotAttached() { + templateManager.resolveIsoIdForDetach(99L, new ArrayList<>(), 42L); + } + + @Test + public void isIsoAlreadyAttachedReturnsTrueWhenPrimaryMatches() { + Assert.assertTrue(templateManager.isIsoAlreadyAttached(1L, 42L, 42L)); + } + + @Test + public void isIsoAlreadyAttachedReturnsTrueWhenInMap() { + Mockito.when(_vmIsoMapDao.findByVmIdIsoId(1L, 42L)).thenReturn(new VmIsoMapVO(1L, 42L, 4)); + Assert.assertTrue(templateManager.isIsoAlreadyAttached(1L, 99L, 42L)); + } + + @Test + public void isIsoAlreadyAttachedReturnsFalseWhenNotAttached() { + Mockito.when(_vmIsoMapDao.findByVmIdIsoId(1L, 42L)).thenReturn(null); + Assert.assertFalse(templateManager.isIsoAlreadyAttached(1L, null, 42L)); + } + + @Test + public void attachISOToVMAttachWritesToVmIsoMapWhenPrimarySlotOccupied() { + UserVmVO vm = Mockito.mock(UserVmVO.class); + VMTemplateVO iso = Mockito.mock(VMTemplateVO.class); + Mockito.when(_userVmDao.findById(1L)).thenReturn(vm); + Mockito.when(vmTemplateDao.findById(42L)).thenReturn(iso); + Mockito.when(iso.getId()).thenReturn(42L); + Mockito.when(vm.getIsoId()).thenReturn(99L); + Mockito.when(_vmIsoMapDao.listByVmId(1L)).thenReturn(new ArrayList<>()); + + boolean result = templateManager.attachISOToVM(1L, 1L, 42L, true, false, false); + + Assert.assertTrue(result); + Mockito.verify(_vmIsoMapDao).persist(Mockito.argThat(row -> + row.getVmId() == 1L && row.getIsoId() == 42L + && row.getDeviceSeq() == TemplateManager.CDROM_PRIMARY_DEVICE_SEQ + 1)); + Mockito.verify(vm, Mockito.never()).setIsoId(anyLong()); + } + + @Test(expected = InvalidParameterValueException.class) + public void enforceCdromAttachLimitsThrowsWhenIsoAlreadyAttachedAtPrimary() { + UserVm vm = Mockito.mock(UserVm.class); + Mockito.when(vm.getIsoId()).thenReturn(42L); + templateManager.enforceCdromAttachLimits(1L, vm, 42L); + } + + @Test(expected = InvalidParameterValueException.class) + public void enforceCdromAttachLimitsThrowsWhenIsoAlreadyAttachedInMap() { + UserVm vm = Mockito.mock(UserVm.class); + Mockito.when(vm.getIsoId()).thenReturn(99L); + Mockito.when(_vmIsoMapDao.findByVmIdIsoId(1L, 42L)).thenReturn(new VmIsoMapVO(1L, 42L, 4)); + templateManager.enforceCdromAttachLimits(1L, vm, 42L); + } + + @Test + public void advertisedCdromCapReturnsDefaultWhenHostIdNull() { + Assert.assertEquals(TemplateManager.DEFAULT_CDROM_MAX_PER_VM, templateManager.advertisedCdromCap(null)); + } + + @Test + public void advertisedCdromCapReturnsDefaultWhenDetailMissing() { + Mockito.when(_hostDetailsDao.findDetail(7L, Host.HOST_CDROM_MAX_COUNT)).thenReturn(null); + Assert.assertEquals(TemplateManager.DEFAULT_CDROM_MAX_PER_VM, templateManager.advertisedCdromCap(7L)); + } + + @Test + public void advertisedCdromCapReturnsParsedValue() { + DetailVO detail = Mockito.mock(DetailVO.class); + Mockito.when(detail.getValue()).thenReturn("3"); + Mockito.when(_hostDetailsDao.findDetail(7L, Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail); + Assert.assertEquals(3, templateManager.advertisedCdromCap(7L)); + } + + @Test + public void advertisedCdromCapFallsBackOnInvalidValue() { + DetailVO detail = Mockito.mock(DetailVO.class); + Mockito.when(detail.getValue()).thenReturn("not-a-number"); + Mockito.when(_hostDetailsDao.findDetail(7L, Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail); + Assert.assertEquals(TemplateManager.DEFAULT_CDROM_MAX_PER_VM, templateManager.advertisedCdromCap(7L)); + } + + @Test + public void hostIdForVmReturnsCurrentHost() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + Mockito.when(vm.getHostId()).thenReturn(42L); + Assert.assertEquals(Long.valueOf(42L), templateManager.hostIdForVm(vm)); + } + + @Test + public void hostIdForVmFallsBackToLastHost() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + Mockito.when(vm.getHostId()).thenReturn(null); + Mockito.when(vm.getLastHostId()).thenReturn(99L); + Assert.assertEquals(Long.valueOf(99L), templateManager.hostIdForVm(vm)); + } + + @Test + public void hostIdForVmReturnsNullWhenNoHost() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + Mockito.when(vm.getHostId()).thenReturn(null); + Mockito.when(vm.getLastHostId()).thenReturn(null); + Assert.assertNull(templateManager.hostIdForVm(vm)); + } + + @Test + public void effectiveMaxCdromsReturnsConfiguredCapWhenWithinHypervisorCap() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + DetailVO detail = Mockito.mock(DetailVO.class); + Mockito.when(detail.getValue()).thenReturn("2"); + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getClusterId()).thenReturn(5L); + Mockito.when(_hostDao.findById(7L)).thenReturn(host); + Mockito.when(_hostDetailsDao.findDetail(7L, Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail); + // Configured cap defaults to 1 (no cluster override mocked); hypervisor cap is 2; 1 <= 2 → no throw, returns 1. + Assert.assertEquals(1, templateManager.effectiveMaxCdroms(vm, 7L)); + } + + @Test + public void templateIsDeleteableReturnsTrueWhenNoVmsUseIso() { + Mockito.when(_userVmJoinDao.listActiveByIsoId(42L)).thenReturn(new ArrayList<>()); + Mockito.when(_vmIsoMapDao.listByIsoId(42L)).thenReturn(new ArrayList<>()); + Assert.assertTrue(templateManager.templateIsDeleteable(42L)); + } + + @Test + public void templateIsDeleteableReturnsFalseWhenPrimarySlotInUse() { + Mockito.when(_userVmJoinDao.listActiveByIsoId(42L)) + .thenReturn(java.util.Collections.singletonList(Mockito.mock(UserVmJoinVO.class))); + Assert.assertFalse(templateManager.templateIsDeleteable(42L)); + // Should not even need to consult vm_iso_map once primary slot in use. + Mockito.verify(_vmIsoMapDao, Mockito.never()).listByIsoId(anyLong()); + } + + @Test + public void templateIsDeleteableReturnsFalseWhenAttachedViaVmIsoMapToActiveVm() { + Mockito.when(_userVmJoinDao.listActiveByIsoId(42L)).thenReturn(new ArrayList<>()); + Mockito.when(_vmIsoMapDao.listByIsoId(42L)) + .thenReturn(java.util.Collections.singletonList(new VmIsoMapVO(1L, 42L, 4))); + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(vm.getUuid()).thenReturn("uuid-1"); + Mockito.when(_userVmDao.findById(1L)).thenReturn(vm); + Assert.assertFalse(templateManager.templateIsDeleteable(42L)); + } + + @Test + public void templateIsDeleteableIgnoresVmIsoMapForDestroyedVm() { + Mockito.when(_userVmJoinDao.listActiveByIsoId(42L)).thenReturn(new ArrayList<>()); + Mockito.when(_vmIsoMapDao.listByIsoId(42L)) + .thenReturn(java.util.Collections.singletonList(new VmIsoMapVO(1L, 42L, 4))); + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getState()).thenReturn(State.Expunging); + Mockito.when(_userVmDao.findById(1L)).thenReturn(vm); + Assert.assertTrue(templateManager.templateIsDeleteable(42L)); + } + + @Configuration @ComponentScan(basePackageClasses = {TemplateManagerImpl.class}, includeFilters = {@ComponentScan.Filter(value = TestConfiguration.Library.class, type = FilterType.CUSTOM)}, diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 6b7a5428b1f9..d054d2d3db47 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -22,6 +22,15 @@ import { getAPI, postAPI, getBaseUrl } from '@/api' import { getLatestKubernetesIsoParams } from '@/utils/acsrepo' import kubernetesIcon from '@/assets/icons/kubernetes.svg?inline' +const attachedIsoCount = (record) => (record.isos && record.isos.length) || (record.isoid ? 1 : 0) +// Server pre-computes the effective cap (cluster-scoped vm.iso.max.count clamped to the +// hypervisor's own limit). Fall back to the hypervisor floor for older servers. +const isoMaxCount = (record) => record.isomaxcount != null + ? record.isomaxcount + : (record.hypervisor === 'KVM' ? 2 : 1) +const isoActionAvailable = (record) => + record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' + export default { name: 'compute', title: 'label.compute', @@ -299,7 +308,7 @@ export default { docHelp: 'adminguide/templates.html#attaching-an-iso-to-a-vm', dataView: true, popup: true, - show: (record) => { return record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && !record.isoid && record.vmtype !== 'sharedfsvm' }, + show: (record) => isoActionAvailable(record) && attachedIsoCount(record) < isoMaxCount(record), disabled: (record) => { return record.hostcontrolstate === 'Offline' || record.hostcontrolstate === 'Maintenance' }, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/AttachIso.vue'))) }, @@ -307,22 +316,11 @@ export default { api: 'detachIso', icon: 'link-outlined', label: 'label.action.detach.iso', - message: 'message.detach.iso.confirm', dataView: true, - args: (record, store) => { - var args = ['virtualmachineid'] - if (record && record.hypervisor && record.hypervisor === 'VMware') { - args.push('forced') - } - return args - }, - show: (record) => { return record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && 'isoid' in record && record.isoid && record.vmtype !== 'sharedfsvm' }, + popup: true, + show: (record) => isoActionAvailable(record) && attachedIsoCount(record) > 0, disabled: (record) => { return record.hostcontrolstate === 'Offline' || record.hostcontrolstate === 'Maintenance' }, - mapping: { - virtualmachineid: { - value: (record, params) => { return record.id } - } - } + component: shallowRef(defineAsyncComponent(() => import('@/views/compute/DetachIso.vue'))) }, { api: 'updateVMAffinityGroup', diff --git a/ui/src/views/compute/AttachIso.vue b/ui/src/views/compute/AttachIso.vue index 60694cb8f57b..daa555c45388 100644 --- a/ui/src/views/compute/AttachIso.vue +++ b/ui/src/views/compute/AttachIso.vue @@ -17,23 +17,38 @@ @@ -1037,21 +1037,11 @@ /> - @@ -80,7 +84,7 @@ export default { return { vm: {}, volumes: [], - defaultColumns: ['name', 'state', 'type', 'size'], + defaultColumns: ['name', 'state', 'type', 'size', 'kmskey'], allColumns: [ { key: 'name', @@ -101,6 +105,11 @@ export default { title: this.$t('label.size'), dataIndex: 'size' }, + { + key: 'kmskey', + title: this.$t('label.kms.key'), + dataIndex: 'kmskey' + }, { key: 'storage', title: this.$t('label.storage'), diff --git a/ui/src/components/widgets/DetailsInput.vue b/ui/src/components/widgets/DetailsInput.vue index a8d39fce02b6..0d9b1c005631 100644 --- a/ui/src/components/widgets/DetailsInput.vue +++ b/ui/src/components/widgets/DetailsInput.vue @@ -19,7 +19,16 @@
- + + @@ -82,6 +91,15 @@ export default { showTableHeaders: { type: Boolean, default: true + }, + optionalKeys: { + type: Array, + default: () => [] + } + }, + computed: { + optionalKeyOptions () { + return this.optionalKeys.map(k => ({ value: k })) } }, data () { @@ -94,7 +112,8 @@ export default { newKey: '', newValue: '', tableData: [], - editBuffer: {} + editBuffer: {}, + autoCompleteKey: 0 } }, created () { @@ -127,6 +146,7 @@ export default { this.updateData() this.newKey = '' this.newValue = '' + this.autoCompleteKey++ }, removeEntry (key) { this.tableData = this.tableData.filter(item => item.key !== key) diff --git a/ui/src/config/router.js b/ui/src/config/router.js index 78346d13cacb..a48c3fef81e3 100644 --- a/ui/src/config/router.js +++ b/ui/src/config/router.js @@ -28,6 +28,7 @@ import compute from '@/config/section/compute' import storage from '@/config/section/storage' import network from '@/config/section/network' import image from '@/config/section/image' +import kms from '@/config/section/kms' import project from '@/config/section/project' import event from '@/config/section/event' import user from '@/config/section/user' @@ -217,6 +218,7 @@ export function asyncRouterMap () { generateRouterMap(compute), generateRouterMap(storage), + generateRouterMap(kms), generateRouterMap(network), generateRouterMap(image), generateRouterMap(event), diff --git a/ui/src/config/section/kms.js b/ui/src/config/section/kms.js new file mode 100644 index 000000000000..648a8064b5c6 --- /dev/null +++ b/ui/src/config/section/kms.js @@ -0,0 +1,280 @@ +// 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. + +import { shallowRef, defineAsyncComponent } from 'vue' +import store from '@/store' + +export default { + name: 'kms', + title: 'label.kms', + icon: 'hdd-outlined', + show: () => { + return ['Admin'].includes(store.getters.userInfo.roletype) || store.getters.features.hashsmprofiles + }, + children: [ + { + name: 'kmskey', + title: 'label.kms.keys', + icon: 'file-text-outlined', + permission: ['listKMSKeys'], + resourceType: 'KMSKey', + columns: () => { + const fields = ['name', 'enabled', 'hsmprofile'] + if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) { + fields.push('account') + } + if (store.getters.listAllProjects) { + fields.push('project') + } + fields.push('domainpath') + return fields + }, + details: () => { + const fields = ['id', 'name', 'description', 'version'] + if (['Admin'].includes(store.getters.userInfo.roletype)) { + fields.push('keklabel') + } + fields.push('keybits', 'enabled', 'account', 'domainpath', 'project', 'created', 'hsmprofile') + return fields + }, + related: [ + { + name: 'volume', + title: 'label.volumes', + param: 'kmskeyid' + } + ], + tabs: [ + { + name: 'details', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, + { + name: 'events', + resourceType: 'KmsKey', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/EventsTab.vue'))), + show: () => { + return 'listEvents' in store.getters.apis + } + } + ], + searchFilters: () => { + var filters = ['zoneid', 'hsmprofileid'] + if (store.getters.userInfo.roletype === 'Admin') { + filters.push('domainid', 'account', 'projectid') + } + return filters + }, + actions: [ + { + api: 'createKMSKey', + icon: 'plus-outlined', + label: 'label.create.kms.key', + docHelp: 'adminguide/kms.html#creating-a-kms-key', + listView: true, + popup: true, + dataView: false, + args: (record, store, group) => { + return ['Admin'].includes(store.userInfo.roletype) + ? ['zoneid', 'domainid', 'account', 'projectid', 'name', 'description', 'hsmprofileid', 'keybits'] + : ['zoneid', 'name', 'description', 'hsmprofileid', 'keybits'] + }, + mapping: { + hsmprofileid: { + api: 'listHSMProfiles', + params: (record) => { return { enabled: true } } + } + } + }, + { + api: 'updateKMSKey', + icon: 'edit-outlined', + label: 'label.update.kms.key', + dataView: true, + popup: true, + args: ['id', 'name', 'description', 'enabled'], + mapping: { + id: { + value: (record) => record.id + } + } + }, + { + api: 'rotateKMSKey', + icon: 'sync-outlined', + docHelp: 'adminguide/kms.html#rotating-a-kms-key', + label: 'label.rotate.kms.key', + dataView: true, + popup: true, + args: ['id', 'keybits', 'hsmprofileid'], + mapping: { + id: { + value: (record) => record.id + } + } + }, + { + api: 'migrateVolumesToKMS', + icon: 'swap-outlined', + docHelp: 'adminguide/kms.html#migrating-existing-volumes-to-kms', + label: 'label.migrate.volumes.to.kms', + message: 'message.action.migrate.volumes.to.kms', + dataView: true, + popup: true, + show: (record, store) => { + return ['Admin'].includes(store.userInfo.roletype) + }, + args: (record, store) => { + var fields = ['domainid', 'account', 'kmskeyid', 'volumeids'] + if (!['Admin'].includes(store.userInfo.roletype)) { + fields = ['kmskeyid', 'volumeids'] + } + return fields + }, + mapping: { + kmskeyid: { + value: (record) => record.id + }, + volumeids: { + api: 'listVolumes', + params: (record) => { return { account: record.account, domainid: record.domainid, zoneid: record.zoneid } } + } + } + }, + { + api: 'deleteKMSKey', + icon: 'delete-outlined', + label: 'label.delete.kms.key', + message: 'message.action.delete.kms.key', + dataView: true, + popup: true, + args: ['id'], + mapping: { + id: { + value: (record) => record.id + } + } + } + ] + }, + { + name: 'hsmprofile', + title: 'label.hsm.profile', + icon: 'safety-outlined', + permission: ['listHSMProfiles'], + show: () => { return ['Admin'].includes(store.getters.userInfo.roletype) }, + resourceType: 'HSMProfile', + columns: () => { + const fields = ['name', 'enabled', 'ispublic'] + if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) { + fields.push('account') + } + if (store.getters.listAllProjects) { + fields.push('project') + } + fields.push('domainpath') + return fields + }, + details: ['id', 'name', 'description', 'protocol', 'enabled', 'ispublic', 'account', 'domainpath', 'project', 'created', 'details'], + related: [ + { + name: 'kmskey', + title: 'label.kms.keys', + param: 'hsmprofileid' + } + ], + tabs: [ + { + name: 'details', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, + { + name: 'events', + resourceType: 'HsmProfile', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/EventsTab.vue'))), + show: () => { + return 'listEvents' in store.getters.apis + } + } + ], + searchFilters: () => { + var filters = ['zoneid'] + if (store.getters.userInfo.roletype === 'Admin') { + filters.push('domainid', 'account', 'projectid') + } + return filters + }, + actions: [ + { + api: 'createHSMProfile', + icon: 'plus-outlined', + docHelp: 'adminguide/kms.html#adding-an-hsm-profile', + label: 'label.create.hsmprofile', + listView: true, + popup: true, + dataView: false, + show: (record, store) => { + return ['Admin'].includes(store.userInfo.roletype) + }, + args: (record, store, group) => { + return ['Admin'].includes(store.userInfo.roletype) + ? ['name', 'zoneid', 'vendorname', 'domainid', 'account', 'projectid', 'details', 'ispublic'] + : ['name', 'zoneid', 'vendorname', 'details'] + }, + mapping: { + details: { + optionalKeys: ['pin', 'library', 'slot', 'slot_list_index', 'token_label'] + } + } + }, + { + api: 'updateHSMProfile', + icon: 'edit-outlined', + label: 'label.update.hsm.profile', + dataView: true, + popup: true, + show: (record, store) => { + return ['Admin'].includes(store.userInfo.roletype) + }, + args: ['id', 'name', 'enabled'], + mapping: { + id: { + value: (record) => record.id + } + } + }, + { + api: 'deleteHSMProfile', + icon: 'delete-outlined', + label: 'label.delete.hsm.profile', + message: 'message.action.delete.hsm.profile', + dataView: true, + popup: true, + show: (record, store) => { + return ['Admin'].includes(store.userInfo.roletype) + }, + args: ['id'], + mapping: { + id: { + value: (record) => record.id + } + } + } + ] + } + ] +} diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 75432314b034..14f3cf821dc4 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -63,7 +63,7 @@ export default { return fields }, - details: ['name', 'id', 'type', 'storagetype', 'diskofferingdisplaytext', 'deviceid', 'sizegb', 'physicalsize', 'provisioningtype', 'utilization', 'diskkbsread', 'diskkbswrite', 'diskioread', 'diskiowrite', 'diskiopstotal', 'miniops', 'maxiops', 'path', 'deleteprotection'], + details: ['name', 'id', 'type', 'storagetype', 'diskofferingdisplaytext', 'kmskey', 'deviceid', 'sizegb', 'physicalsize', 'provisioningtype', 'utilization', 'diskkbsread', 'diskkbswrite', 'diskioread', 'diskiowrite', 'diskiopstotal', 'miniops', 'maxiops', 'path', 'deleteprotection'], related: [{ name: 'snapshot', title: 'label.snapshots', @@ -92,7 +92,7 @@ export default { } ], searchFilters: () => { - const filters = ['name', 'zoneid', 'domainid', 'account', 'state', 'tags', 'serviceofferingid', 'diskofferingid', 'isencrypted'] + const filters = ['name', 'zoneid', 'domainid', 'account', 'state', 'tags', 'serviceofferingid', 'diskofferingid', 'kmskeyid', 'isencrypted'] if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) { filters.push('storageid') } @@ -221,6 +221,25 @@ export default { popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/storage/MigrateVolume.vue'))) }, + { + api: 'migrateVolumesToKMS', + icon: 'lock-outlined', + docHelp: 'adminguide/kms.html#migrating-existing-volumes-to-kms', + label: 'label.migrate.volume.to.kms', + message: 'message.action.migrate.volume.to.kms', + dataView: true, + popup: true, + show: (record, store) => { + return record.encryptformat && !record.kmskeyid && + ['Ready', 'Allocated'].includes(record.state) + }, + args: ['kmskeyid'], + mapping: { + volumeids: { + value: (record) => { return record.id } + } + } + }, { api: 'changeOfferingForVolume', icon: 'swap-outlined', diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index 6a818d587233..a18626e801bc 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -341,6 +341,40 @@ const user = { commit('SET_DARK_MODE', darkMode) commit('SET_LATEST_VERSION', latestVersion) + const loadFeatures = (apis) => { + return new Promise(resolve => { + getAPI('listCapabilities').then(response => { + const result = response.listcapabilitiesresponse.capability + commit('SET_FEATURES', result) + if (result && result.defaultuipagesize) { + commit('SET_DEFAULT_LISTVIEW_PAGE_SIZE', result.defaultuipagesize) + } + if (result && result.customhypervisordisplayname) { + commit('SET_CUSTOM_HYPERVISOR_NAME', result.customhypervisordisplayname) + } + if (result && result.securitygroupsenabled) { + commit('SET_SHOW_SECURITY_GROUPS', result.securitygroupsenabled) + } + + if ('listHSMProfiles' in apis) { + getAPI('listHSMProfiles', { listall: true }).then(response => { + const hasHsmProfiles = (response.listhsmprofilesresponse.count > 0) + const features = Object.assign({}, store.getters.features) + features.hashsmprofiles = hasHsmProfiles + commit('SET_FEATURES', features) + resolve() + }).catch(ignored => { + resolve() + }) + } else { + resolve() + } + }).catch(() => { + resolve() + }) + }) + } + // This block is to enforce password change for first time login after admin resets password const isPwdChangeRequired = vueProps.$localStorage.get(PASSWORD_CHANGE_REQUIRED) commit('SET_PASSWORD_CHANGE_REQUIRED', isPwdChangeRequired) @@ -364,7 +398,9 @@ const user = { const result = response.listusersresponse.user[0] commit('SET_INFO', result) commit('SET_NAME', result.firstname + ' ' + result.lastname) - resolve(cachedApis) + loadFeatures(cachedApis).then(() => { + resolve(cachedApis) + }) }).catch(error => { reject(error) }) @@ -391,14 +427,16 @@ const user = { } } commit('SET_APIS', apis) - resolve(apis) - store.dispatch('GenerateRoutes', { apis }).then(() => { - store.getters.addRouters.map(route => { - router.addRoute(route) + loadFeatures(apis).then(() => { + resolve(apis) + store.dispatch('GenerateRoutes', { apis }).then(() => { + store.getters.addRouters.map(route => { + router.addRoute(route) + }) }) + hide() + message.success(i18n.global.t('message.sussess.discovering.feature')) }) - hide() - message.success(i18n.global.t('message.sussess.discovering.feature')) }).catch(error => { reject(error) }) @@ -452,22 +490,6 @@ const user = { }).catch(ignored => { }) - getAPI('listCapabilities').then(response => { - const result = response.listcapabilitiesresponse.capability - commit('SET_FEATURES', result) - if (result && result.defaultuipagesize) { - commit('SET_DEFAULT_LISTVIEW_PAGE_SIZE', result.defaultuipagesize) - } - if (result && result.customhypervisordisplayname) { - commit('SET_CUSTOM_HYPERVISOR_NAME', result.customhypervisordisplayname) - } - if (result && result.securitygroupsenabled) { - commit('SET_SHOW_SECURITY_GROUPS', result.securitygroupsenabled) - } - }).catch(error => { - reject(error) - }) - getAPI('listLdapConfigurations').then(response => { const ldapEnable = (response.ldapconfigurationresponse.count > 0) commit('SET_LDAP', ldapEnable) @@ -586,7 +608,8 @@ const user = { getAPI('listCapabilities').then(response => { const result = response.listcapabilitiesresponse.capability resolve(result) - commit('SET_FEATURES', result) + const features = Object.assign({}, store.getters.features, result) + commit('SET_FEATURES', features) }).catch(error => { reject(error) }) diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index c0603445b57f..7ddec48b85cf 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -350,6 +350,7 @@ showSearch optionFilterProp="label" v-model:value="form[field.name]" + @change="val => handleSelectChange(field.name, val)" :loading="field.loading" :placeholder="field.description" :filterOption="(input, option) => { @@ -374,6 +375,7 @@ showSearch optionFilterProp="label" v-model:value="form[field.name]" + @change="val => handleSelectChange(field.name, val)" :loading="field.loading" :placeholder="field.description" :filterOption="(input, option) => { @@ -481,6 +483,7 @@ :loading="field.loading" mode="multiple" v-model:value="form[field.name]" + @change="val => handleSelectChange(field.name, val)" :placeholder="field.description" v-focus="fieldIndex === firstIndex" showSearch @@ -499,7 +502,8 @@ + v-model:value="form[field.name]" + :optionalKeys="currentAction.mapping?.[field.name]?.optionalKeys || []" /> f.name === 'account') + if (accountField) { + this.form.account = null + this.listUuidOpts(accountField, { domainid: val }) + } + } else if (name === 'account') { + const volumeField = this.currentAction.paramFields.find(f => f.name === 'volumeids') + if (volumeField) { + this.form.volumeids = null + this.listUuidOpts(volumeField, { domainid: this.form.domainid, account: val }) + } + } + }, listUuidOpts (param, filters) { if (this.currentAction.mapping && param.name in this.currentAction.mapping && !this.currentAction.mapping[param.name].api) { return diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 474c3bb01ac9..deecdd71a2ef 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -341,15 +341,19 @@ @handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)" > + @update-root-disk-iops-value="updateIOPSValue" + @update-root-kms-key="updateRootKmsKey"/> @@ -394,14 +398,17 @@ @handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)" > + @update-iops-value="updateIOPSValue" + @update-data-kms-key="updateDataKmsKey"/> @@ -1053,7 +1060,8 @@ export default { keyboards: [], bootTypes: [], bootModes: [], - ioPolicyTypes: [] + ioPolicyTypes: [], + kmsKeys: [] }, rowCount: {}, loading: { @@ -1074,7 +1082,8 @@ export default { pods: false, clusters: false, hosts: false, - groups: false + groups: false, + kmsKeys: false }, owner: { projectid: store.getters.project?.id, @@ -1732,6 +1741,22 @@ export default { serviceOffering (oldValue, newValue) { if (oldValue && newValue && oldValue.id !== newValue.id) { this.dynamicscalingenabled = this.isDynamicallyScalable() + // Fetch KMS keys if encryption is enabled + if (newValue && newValue.encryptroot && this.zoneId) { + this.fetchKmsKeys() + } + } + }, + diskOffering (newValue) { + // Fetch KMS keys if encryption is enabled + if (newValue && newValue.encrypt && this.zoneId) { + this.fetchKmsKeys() + } + }, + overrideDiskOffering (newValue) { + // Fetch KMS keys if encryption is enabled + if (newValue && newValue.encrypt && this.zoneId) { + this.fetchKmsKeys() } }, template (oldValue, newValue) { @@ -1999,6 +2024,31 @@ export default { const param = this.params.networks this.fetchOptions(param, 'networks') }, + fetchKmsKeys () { + if (!this.zoneId) { + return + } + this.loading.kmsKeys = true + this.options.kmsKeys = [] + getAPI('listKMSKeys', { + zoneid: this.zoneId, + account: this.owner.account, + domainid: this.owner.domainid, + projectid: this.owner.projectid, + purpose: 'volume' + }).then(response => { + const kmskeyMap = response.listkmskeysresponse.kmskey || [] + if (kmskeyMap.length > 0) { + this.options.kmsKeys = kmskeyMap + } else { + this.options.kmsKeys = null + } + }).catch(() => { + this.options.kmsKeys = null + }).finally(() => { + this.loading.kmsKeys = false + }) + }, resetData () { this.vm = { name: null, @@ -2023,6 +2073,12 @@ export default { this.formRef.value.resetFields() this.fetchData() }, + updateRootKmsKey (value) { + this.form.rootkmskeyid = value + }, + updateDataKmsKey (value) { + this.form.datakmskeyid = value + }, updateFieldValue (name, value) { if (name === 'templateid') { this.imageType = 'templateid' @@ -2386,6 +2442,10 @@ export default { deployVmData['details[0].memory'] = values.memory } } + // Add root disk KMS key if selected (optional - falls back to legacy passphrase if not provided) + if (values.rootkmskeyid) { + deployVmData.rootdiskkmskeyid = values.rootkmskeyid + } if (this.selectedTemplateConfiguration) { deployVmData['details[0].configurationId'] = this.selectedTemplateConfiguration.id } @@ -2412,12 +2472,29 @@ export default { }) } } else { - deployVmData.diskofferingid = values.diskofferingid - if (values.size) { - deployVmData.size = values.size + // When a KMS key is selected for data disk, we must use datadisksdetails format + if (values.datakmskeyid) { + deployVmData['datadisksdetails[0].diskofferingid'] = values.diskofferingid + deployVmData['datadisksdetails[0].deviceid'] = 1 // Device ID 1 for first data disk (0=root, 3=CD-ROM reserved) + if (values.size) { + deployVmData['datadisksdetails[0].size'] = values.size + } + deployVmData['datadisksdetails[0].kmskeyid'] = values.datakmskeyid + // Add IOPS if customized + if (this.isCustomizedDiskIOPS) { + deployVmData['datadisksdetails[0].miniops'] = this.diskIOpsMin + deployVmData['datadisksdetails[0].maxiops'] = this.diskIOpsMax + } + } else { + // Legacy format when no KMS key + deployVmData.diskofferingid = values.diskofferingid + if (values.size) { + deployVmData.size = values.size + } } } - if (this.isCustomizedDiskIOPS) { + // IOPS for non-KMS data disks (KMS data disks IOPS handled above in datadisksdetails) + if (this.isCustomizedDiskIOPS && !values.datakmskeyid) { deployVmData['details[0].minIopsDo'] = this.diskIOpsMin deployVmData['details[0].maxIopsDo'] = this.diskIOpsMax } @@ -3093,6 +3170,7 @@ export default { this.selectedBackupOffering = null this.fetchZoneOptions() this.updateZoneAllowsBackupOperations() + this.fetchKmsKeys() }, onSelectPodId (value) { this.podId = value diff --git a/ui/src/views/compute/wizard/DiskSizeSelection.vue b/ui/src/views/compute/wizard/DiskSizeSelection.vue index bd202042e536..baae69ea1a12 100644 --- a/ui/src/views/compute/wizard/DiskSizeSelection.vue +++ b/ui/src/views/compute/wizard/DiskSizeSelection.vue @@ -16,35 +16,62 @@ // under the License. + + diff --git a/ui/src/views/network/dns/AssociateDnsZone.vue b/ui/src/views/network/dns/AssociateDnsZone.vue new file mode 100644 index 000000000000..4f318b5c8518 --- /dev/null +++ b/ui/src/views/network/dns/AssociateDnsZone.vue @@ -0,0 +1,194 @@ +// 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. + + + + + + diff --git a/ui/src/views/network/dns/CreateDnsRecord.vue b/ui/src/views/network/dns/CreateDnsRecord.vue new file mode 100644 index 000000000000..869ef24ff4a2 --- /dev/null +++ b/ui/src/views/network/dns/CreateDnsRecord.vue @@ -0,0 +1,207 @@ +// 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. + + + + + + diff --git a/ui/src/views/network/dns/CreateDnsZone.vue b/ui/src/views/network/dns/CreateDnsZone.vue new file mode 100644 index 000000000000..b9c07444c5ca --- /dev/null +++ b/ui/src/views/network/dns/CreateDnsZone.vue @@ -0,0 +1,222 @@ +// 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. + + + + + + diff --git a/ui/src/views/network/dns/DeleteDnsServer.vue b/ui/src/views/network/dns/DeleteDnsServer.vue new file mode 100644 index 000000000000..2452456ea238 --- /dev/null +++ b/ui/src/views/network/dns/DeleteDnsServer.vue @@ -0,0 +1,234 @@ +// 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. + + + + + + diff --git a/ui/src/views/network/dns/DeleteDnsZone.vue b/ui/src/views/network/dns/DeleteDnsZone.vue new file mode 100644 index 000000000000..ebc2f6190061 --- /dev/null +++ b/ui/src/views/network/dns/DeleteDnsZone.vue @@ -0,0 +1,184 @@ +// 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. + + + + + + diff --git a/ui/src/views/network/dns/DnsRecordsTab.vue b/ui/src/views/network/dns/DnsRecordsTab.vue new file mode 100644 index 000000000000..fae228537bf1 --- /dev/null +++ b/ui/src/views/network/dns/DnsRecordsTab.vue @@ -0,0 +1,262 @@ +// 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. + + + + diff --git a/ui/src/views/network/dns/UpdateDnsServer.vue b/ui/src/views/network/dns/UpdateDnsServer.vue new file mode 100644 index 000000000000..b8bd4f352e47 --- /dev/null +++ b/ui/src/views/network/dns/UpdateDnsServer.vue @@ -0,0 +1,320 @@ +// 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. + + + + + + diff --git a/ui/src/views/network/dns/UpdateDnsZone.vue b/ui/src/views/network/dns/UpdateDnsZone.vue new file mode 100644 index 000000000000..58c3bdcdcbbf --- /dev/null +++ b/ui/src/views/network/dns/UpdateDnsZone.vue @@ -0,0 +1,152 @@ +// 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. + + + + + + From 4d006b5f85ff3bd678b01134e67f326adf8905b8 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 7 Jul 2026 16:13:07 +0530 Subject: [PATCH 155/205] UI: Fix missing label.allocated key in Status widget test mock (#13546) PR #13254 added an 'allocated' case to Status.vue getText() that calls $t('label.allocated'), but did not update the test mock data, causing two Allocated badge tests to fail in CI. --- ui/tests/mockData/Status.mock.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/tests/mockData/Status.mock.json b/ui/tests/mockData/Status.mock.json index 0293cd0bbd0b..d9ced6eb3507 100644 --- a/ui/tests/mockData/Status.mock.json +++ b/ui/tests/mockData/Status.mock.json @@ -97,7 +97,8 @@ "message.guestnetwork.state.disabled": "message.guestnetwork.state.disabled", "message.guestnetwork.state.migrating": "message.guestnetwork.state.migrating", "message.guestnetwork.state.alert": "message.guestnetwork.state.alert", - "message.guestnetwork.state.allocated": "message.guestnetwork.state.allocated" + "message.guestnetwork.state.allocated": "message.guestnetwork.state.allocated", + "label.allocated": "Allocated" } }, "routes": [ From 9f281c48517d89495109f36c29635e039e20bd07 Mon Sep 17 00:00:00 2001 From: James Peru Mmbono Date: Tue, 7 Jul 2026 14:04:50 +0300 Subject: [PATCH 156/205] NAS backup: resume paused VM on backup failure and fix missing exit (#12822) * NAS backup: resume paused VM on backup failure and fix missing exit When a NAS backup job fails (e.g. due to backup storage being full or I/O errors), the VM may remain indefinitely paused because: 1. The cleanup() function never checks or resumes the VM's paused state that was set by virsh backup-begin during the push backup operation. 2. The 'Failed' case in the backup job monitoring loop calls cleanup() but lacks an 'exit' statement, causing an infinite loop where the script repeatedly detects the failed job and calls cleanup(). 3. Similarly, backup_stopped_vm() calls cleanup() on qemu-img convert failure but does not exit, allowing the loop to continue with subsequent disks despite the failure. This fix: - Adds VM state detection and resume to cleanup(), ensuring the VM is always resumed if found in a paused state during error handling - Adds missing 'exit 1' after cleanup() in the Failed backup job case to prevent the infinite monitoring loop - Adds missing 'exit 1' after cleanup() in backup_stopped_vm() on qemu-img convert failure Co-Authored-By: Claude Opus 4.6 * ci: retrigger workflow (flaky/stale shards) * nasbackup.sh: keep cleanup() best-effort if domstate fails With set -eo pipefail, a non-zero virsh domstate (libvirt unavailable or the domain gone) in the vm_state assignment would abort cleanup() before the rm/umount/rmdir. Append '|| true' so cleanup always runs to completion. Addresses the review suggestion (Copilot, endorsed by @abh1sar and @weizhouapache). Signed-off-by: James Peru --------- Signed-off-by: James Peru Co-authored-by: Claude Opus 4.6 Co-authored-by: jmsperu --- scripts/vm/hypervisor/kvm/nasbackup.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/vm/hypervisor/kvm/nasbackup.sh b/scripts/vm/hypervisor/kvm/nasbackup.sh index 441312f35e86..e7ad3657cf03 100755 --- a/scripts/vm/hypervisor/kvm/nasbackup.sh +++ b/scripts/vm/hypervisor/kvm/nasbackup.sh @@ -167,7 +167,8 @@ backup_running_vm() { break ;; Failed) echo "Virsh backup job failed" - cleanup ;; + cleanup + exit 1 ;; esac sleep 5 done @@ -221,6 +222,7 @@ backup_stopped_vm() { if ! qemu-img convert -O qcow2 "$disk" "$output" > "$logFile" 2> >(cat >&2); then echo "qemu-img convert failed for $disk $output" cleanup + exit 1 fi name="datadisk" done @@ -265,6 +267,19 @@ mount_operation() { cleanup() { local status=0 + # Resume the VM if it was paused during backup to prevent it from + # remaining indefinitely paused when the backup job fails (e.g. due + # to storage full or I/O errors on the backup target) + local vm_state + vm_state=$(virsh -c qemu:///system domstate "$VM" 2>/dev/null || true) + if [[ "$vm_state" == "paused" ]]; then + log -ne "Resuming paused VM $VM during backup cleanup" + if ! virsh -c qemu:///system resume "$VM" > /dev/null 2>&1; then + echo "Failed to resume VM $VM" + status=1 + fi + fi + rm -rf "$dest" || { echo "Failed to delete $dest"; status=1; } umount "$mount_point" || { echo "Failed to unmount $mount_point"; status=1; } rmdir "$mount_point" || { echo "Failed to remove mount point $mount_point"; status=1; } From 89b2b030e024fb9ebcfe49b6bea900be29fa6fcb Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 7 Jul 2026 16:38:58 +0530 Subject: [PATCH 157/205] veeam-control-service: fix vm pagination (#13383) Only consider page in pagination for search clause https://ovirt.github.io/ovirt-engine-api-model/4.5/#_pagination Signed-off-by: Abhishek Kumar --- .../veeam/api/request/ListQuery.java | 57 +++++++++---------- .../veeam/api/request/ListQueryTest.java | 10 ++-- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/request/ListQuery.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/request/ListQuery.java index f57edf76e04c..1cb560a3d897 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/request/ListQuery.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/request/ListQuery.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; @@ -31,6 +33,8 @@ import org.jetbrains.annotations.NotNull; public class ListQuery { + private static final Pattern PAGE_CLAUSE_PATTERN = Pattern.compile("(?i)\\bpage\\s+(\\d+)\\b"); + boolean allContent; Long max; Long page; @@ -53,6 +57,14 @@ public void setMax(Long max) { this.max = max; } + public Long getPage() { + return page; + } + + public void setPage(Long page) { + this.page = page; + } + public void setSearch(Map search) { this.search = search; } @@ -108,10 +120,13 @@ public static ListQuery fromRequest(HttpServletRequest request) { query.setFollow(follow); Map searchItems = getSearchMap(request.getParameter("search")); if (!searchItems.isEmpty()) { - try { - query.setMax(Long.parseLong(searchItems.get("page"))); - } catch (NumberFormatException e) { - // Ignore invalid page and keep default null value. + String pageValue = searchItems.get("page"); + if (StringUtils.isNotBlank(pageValue)) { + try { + query.setPage(Long.parseLong(pageValue)); + } catch (NumberFormatException e) { + // Ignore invalid page and keep default null value. + } } query.setSearch(searchItems); } @@ -119,39 +134,19 @@ public static ListQuery fromRequest(HttpServletRequest request) { return query; } - // Parse search clause. Only keep items which use simple '=' operator, and ignore others. For example: - // name=myvm and status=up --> {name=myvm, status=up} - // name=myvm and status!=down --> {name=myvm} (ignore status!=down because it uses '!=' operator) + // Parse search clause. For now, only extract the oVirt paging clause. + // Examples: + // page 3 --> {page=3} + // sortby name page 2 --> {page=2} @NotNull private static Map getSearchMap(String searchClause) { Map searchItems = new LinkedHashMap<>(); if (StringUtils.isBlank(searchClause)) { return searchItems; } - String[] terms = searchClause.trim().split("(?i)\\s+and\\s+"); - for (String term : terms) { - if (term == null) { - continue; - } - String trimmedTerm = term.trim(); - if (trimmedTerm.isEmpty()) { - continue; - } - - int eqIdx = trimmedTerm.indexOf('='); - if (eqIdx <= 0 || eqIdx != trimmedTerm.lastIndexOf('=')) { - continue; - } - char prev = trimmedTerm.charAt(eqIdx - 1); - if (prev == '!' || prev == '<' || prev == '>') { - continue; - } - - String key = trimmedTerm.substring(0, eqIdx).trim(); - String value = trimmedTerm.substring(eqIdx + 1).trim(); - if (!key.isEmpty() && !value.isEmpty()) { - searchItems.put(key, value); - } + Matcher matcher = PAGE_CLAUSE_PATTERN.matcher(searchClause); + if (matcher.find()) { + searchItems.put("page", matcher.group(1)); } return searchItems; } diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java index 16fbd02669c5..a6d8bfef1d4b 100644 --- a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java @@ -67,18 +67,18 @@ public void testFromRequest_ParsesAllContentMaxAndFollow() { } @Test - public void testFromRequest_SearchParserIgnoresNonEqualsAndUsesPageValueAsMaxCurrentBehavior() { + public void testFromRequest_SearchParserExtractsPageOnly() { final HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getParameterMap()).thenReturn(Map.of("search", new String[]{"name=vm and page=3 and status!=down and x>=1"})); + when(request.getParameterMap()).thenReturn(Map.of("search", new String[]{"sortby name page 3"})); when(request.getParameter("all_content")).thenReturn(null); when(request.getParameter("max")).thenReturn(null); when(request.getParameter("follow")).thenReturn(null); - when(request.getParameter("search")).thenReturn("name=vm and page=3 and status!=down and x>=1"); + when(request.getParameter("search")).thenReturn("sortby name page 3"); final ListQuery query = ListQuery.fromRequest(request); - // Document existing behavior: when search contains page=..., max is set from it. - org.junit.Assert.assertEquals(Long.valueOf(3L), query.getLimit()); + // Only page key is extracted from search clause in oVirt format "page N" + org.junit.Assert.assertEquals(Long.valueOf(3L), query.getPage()); } @Test From f9a94518606910aa080201390ea0b3d6c22db080 Mon Sep 17 00:00:00 2001 From: Davi Torres <90287660+daviftorres@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:02:11 -0400 Subject: [PATCH 158/205] [NAS Backup] Suppress Errors in Disk Usage Calculation that Caused Backup to Fail (#13424) --- scripts/vm/hypervisor/kvm/nasbackup.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/vm/hypervisor/kvm/nasbackup.sh b/scripts/vm/hypervisor/kvm/nasbackup.sh index e7ad3657cf03..b35908a433ed 100755 --- a/scripts/vm/hypervisor/kvm/nasbackup.sh +++ b/scripts/vm/hypervisor/kvm/nasbackup.sh @@ -34,7 +34,7 @@ BACKUP_DIR="" DISK_PATHS="" QUIESCE="" logFile="/var/log/cloudstack/agent/agent.log" - +UNMOUNT_TIMEOUT=60 EXIT_CLEANUP_FAILED=20 log() { @@ -197,10 +197,10 @@ backup_running_vm() { # Print statistics virsh -c qemu:///system domjobinfo $VM --completed - du -sb $dest | cut -f1 - - umount $mount_point - rmdir $mount_point + backup_size=$(du -sb "$dest" 2>>"$logFile" | cut -f1) || { log -ne "WARNING: du failed for $dest, reporting size as 0"; backup_size=0; } + timeout "$UNMOUNT_TIMEOUT" umount "$mount_point" 2>>"$logFile" || { log "WARNING: umount of $mount_point failed or timed out"; true; } + rmdir "$mount_point" 2>>"$logFile" || { log "WARNING: rmdir of $mount_point failed"; true; } + echo "$backup_size" } backup_stopped_vm() { From 40b3ef36aa9e96e88434bb2aaee87c168796835b Mon Sep 17 00:00:00 2001 From: slavkap <51903378+slavkap@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:13:54 +0300 Subject: [PATCH 159/205] kvm: Disabled the setting reboot.host.and.alert.management.on.heartbeat.timeout by default (#10111) `reboot.host.and.alert.management.on.heartbeat.timeout` has to be disabled. Even the high availability isn't enabled when there is an issue with a storage CloudStack will reboot the host --- agent/conf/agent.properties | 2 +- .../main/java/com/cloud/agent/properties/AgentProperties.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties index b48848a43fca..2d244e00edaf 100644 --- a/agent/conf/agent.properties +++ b/agent/conf/agent.properties @@ -316,7 +316,7 @@ iscsi.session.cleanup.enabled=false #vm.migrate.domain.retrieve.timeout=10 # This parameter specifies if the host must be rebooted when something goes wrong with the heartbeat. -#reboot.host.and.alert.management.on.heartbeat.timeout=true +#reboot.host.and.alert.management.on.heartbeat.timeout=false # Enables manually setting CPU's topology on KVM's VM. #enable.manually.setting.cpu.topology.on.kvm.vm=true diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java index 9156af1c7d48..e2fe028453f9 100644 --- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java +++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java @@ -616,10 +616,10 @@ public class AgentProperties{ /** * This parameter specifies if the host must be rebooted when something goes wrong with the heartbeat.
* Data type: Boolean.
- * Default value: true + * Default value: false */ public static final Property REBOOT_HOST_AND_ALERT_MANAGEMENT_ON_HEARTBEAT_TIMEOUT - = new Property<>("reboot.host.and.alert.management.on.heartbeat.timeout", true); + = new Property<>("reboot.host.and.alert.management.on.heartbeat.timeout", false); /** * Enables manually setting CPU's topology on KVM's VM.
From f07b7d8c078a7cbca5eb25b08cc4936b688fdfb2 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 7 Jul 2026 11:21:41 -0400 Subject: [PATCH 160/205] Add Guest OS mapping for windows server 2025 on VMware (#12358) Co-authored-by: Pearl Dsilva --- .../resources/META-INF/db/schema-42210to42300.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index c87697674a18..9f4353490956 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -34,6 +34,19 @@ CREATE TABLE `cloud`.`backup_offering_details` ( UPDATE `cloud`.`configuration` SET value='random' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_random'; UPDATE `cloud`.`configuration` SET value='firstfit' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_firstfit'; +-- Add Windows Server 2025 guest OS and mappings +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '7.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '7.0.1.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '7.0.2.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '7.0.3.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.0.1', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.0.2', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.0.3', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.1', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.2', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.3', 'windows2022srvNext_64Guest'); + -- Create kubernetes_cluster_affinity_group_map table for CKS per-node-type affinity groups CREATE TABLE IF NOT EXISTS `cloud`.`kubernetes_cluster_affinity_group_map` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, From 600201a46bf9001d4eb8ec46bf168d9f6064bac2 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Wed, 8 Jul 2026 06:03:19 -0300 Subject: [PATCH 161/205] Add KMSWrappedKeyDao as a dependency for the Usage server (#13535) --- ...ema-core-common-daos-between-management-and-usage-context.xml | 1 + .../cloudstack/core/spring-engine-schema-core-daos-context.xml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml index 2c6869bd81e3..a0dada545617 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml @@ -46,6 +46,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index d7cacd09b9a8..d6c4935e8ddf 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -316,7 +316,6 @@ - From 72197fea6c67aecc70de4124d6e308fd2b899869 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Wed, 8 Jul 2026 08:07:36 -0300 Subject: [PATCH 162/205] [VMware to KVM] Cleanup leftover migrated volumes in case of migration failures (#13151) --- .../CleanupConvertedInstanceDisksCommand.java | 57 ++++ .../LibvirtBaseConvertCommandWrapper.java | 283 ++++++++++++++++++ ...pConvertedInstanceDisksCommandWrapper.java | 67 +++++ ...ImportConvertedInstanceCommandWrapper.java | 231 +------------- ...rtConvertedInstanceCommandWrapperTest.java | 2 +- .../vm/UnmanagedVMsManagerImpl.java | 51 +++- 6 files changed, 458 insertions(+), 233 deletions(-) create mode 100644 core/src/main/java/com/cloud/agent/api/CleanupConvertedInstanceDisksCommand.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtBaseConvertCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupConvertedInstanceDisksCommandWrapper.java diff --git a/core/src/main/java/com/cloud/agent/api/CleanupConvertedInstanceDisksCommand.java b/core/src/main/java/com/cloud/agent/api/CleanupConvertedInstanceDisksCommand.java new file mode 100644 index 000000000000..00373ec75364 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/CleanupConvertedInstanceDisksCommand.java @@ -0,0 +1,57 @@ +// +// 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 com.cloud.agent.api; + +import com.cloud.agent.api.to.DataStoreTO; + +/** + * This command is used to cleanup the converted instance disks from the storage pool: vmVolumesStore and the prefix: vmVolumesPrefix. + */ +public class CleanupConvertedInstanceDisksCommand extends Command { + + private DataStoreTO vmVolumesStore; + private String vmVolumesPrefix; + + public CleanupConvertedInstanceDisksCommand(DataStoreTO vmVolumesStore, String vmVolumesPrefix) { + this.vmVolumesStore = vmVolumesStore; + this.vmVolumesPrefix = vmVolumesPrefix; + } + + public DataStoreTO getVmVolumesStore() { + return vmVolumesStore; + } + + public void setVmVolumesStore(DataStoreTO vmVolumesStore) { + this.vmVolumesStore = vmVolumesStore; + } + + public String getVmVolumesPrefix() { + return vmVolumesPrefix; + } + + public void setVmVolumesPrefix(String vmVolumesPrefix) { + this.vmVolumesPrefix = vmVolumesPrefix; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtBaseConvertCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtBaseConvertCommandWrapper.java new file mode 100644 index 000000000000..dc34a4cb62d8 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtBaseConvertCommandWrapper.java @@ -0,0 +1,283 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.NfsTO; +import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ServerResource; +import com.cloud.storage.Storage; +import com.cloud.utils.FileUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.Script; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.vm.UnmanagedInstanceTO; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public abstract class LibvirtBaseConvertCommandWrapper extends CommandWrapper { + + protected KVMStoragePool getTemporaryStoragePool(DataStoreTO conversionTemporaryLocation, KVMStoragePoolManager storagePoolMgr) { + if (conversionTemporaryLocation instanceof NfsTO) { + NfsTO nfsTO = (NfsTO) conversionTemporaryLocation; + return storagePoolMgr.getStoragePoolByURI(nfsTO.getUrl()); + } else { + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) conversionTemporaryLocation; + return storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + } + } + + protected List getTemporaryDisksFromParsedXml(KVMStoragePool pool, LibvirtDomainXMLParser xmlParser, + String convertedBasePath, + String conversionPoolPath, String vmVolumesPrefix) { + List disksDefs = xmlParser.getDisks(); + disksDefs = disksDefs.stream().filter(x -> x.getDiskType() == LibvirtVMDef.DiskDef.DiskType.FILE && + x.getDeviceType() == LibvirtVMDef.DiskDef.DeviceType.DISK).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(disksDefs)) { + String err = String.format("Cannot find any disk defined on the converted XML domain %s.xml, " + + "checking disks at: %s with prefix: %s", convertedBasePath, conversionPoolPath, vmVolumesPrefix); + logger.warn(err); + return getTemporaryDisksWithPrefixFromTemporaryPool(pool, conversionPoolPath, vmVolumesPrefix); + } + sanitizeDisksPath(disksDefs); + return getPhysicalDisksFromDefPaths(disksDefs, pool); + } + + private List getPhysicalDisksFromDefPaths(List disksDefs, KVMStoragePool pool) { + List disks = new ArrayList<>(); + for (LibvirtVMDef.DiskDef diskDef : disksDefs) { + KVMPhysicalDisk physicalDisk = pool.getPhysicalDisk(diskDef.getDiskPath()); + disks.add(physicalDisk); + } + return disks; + } + + protected List getTemporaryDisksWithPrefixFromTemporaryPool(KVMStoragePool pool, String path, String prefix) { + String msg = String.format("Could not parse correctly the converted XML domain, checking for disks on %s with prefix %s", path, prefix); + logger.info(msg); + pool.refresh(); + List disksWithPrefix = pool.listPhysicalDisks() + .stream() + .filter(x -> x.getName().startsWith(prefix) && !x.getName().endsWith(".xml")) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(disksWithPrefix)) { + msg = String.format("Could not find any converted disk with prefix %s on temporary location %s", prefix, path); + logger.error(msg); + throw new CloudRuntimeException(msg); + } + return disksWithPrefix; + } + + protected void cleanupDisksAndDomainFromTemporaryLocation(List disks, + KVMStoragePool temporaryStoragePool, + String temporaryConvertUuid, boolean xmlExists) { + for (KVMPhysicalDisk disk : disks) { + logger.info(String.format("Cleaning up temporary disk %s after conversion from temporary location", disk.getName())); + temporaryStoragePool.deletePhysicalDisk(disk.getName(), Storage.ImageFormat.QCOW2); + } + if (xmlExists) { + logger.info(String.format("Cleaning up temporary domain %s after conversion from temporary location", temporaryConvertUuid)); + FileUtil.deleteFiles(temporaryStoragePool.getLocalPath(), temporaryConvertUuid, ".xml"); + } + } + + protected void sanitizeDisksPath(List disks) { + for (LibvirtVMDef.DiskDef disk : disks) { + String[] diskPathParts = disk.getDiskPath().split("/"); + String relativePath = diskPathParts[diskPathParts.length - 1]; + disk.setDiskPath(relativePath); + } + } + + protected List moveTemporaryDisksToDestination(List temporaryDisks, + List destinationStoragePools, + KVMStoragePoolManager storagePoolMgr) { + List targetDisks = new ArrayList<>(); + if (temporaryDisks.size() != destinationStoragePools.size()) { + String warn = String.format("Discrepancy between the converted instance disks (%s) " + + "and the expected number of disks (%s)", temporaryDisks.size(), destinationStoragePools.size()); + logger.warn(warn); + } + for (int i = 0; i < temporaryDisks.size(); i++) { + String poolPath = destinationStoragePools.get(i); + KVMStoragePool destinationPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.NetworkFilesystem, poolPath); + if (destinationPool == null) { + String err = String.format("Could not find a storage pool by URI: %s", poolPath); + logger.error(err); + continue; + } + if (destinationPool.getType() != Storage.StoragePoolType.NetworkFilesystem) { + String err = String.format("Storage pool by URI: %s is not an NFS storage", poolPath); + logger.error(err); + continue; + } + KVMPhysicalDisk sourceDisk = temporaryDisks.get(i); + if (logger.isDebugEnabled()) { + String msg = String.format("Trying to copy converted instance disk number %s from the temporary location %s" + + " to destination storage pool %s", i, sourceDisk.getPool().getLocalPath(), destinationPool.getUuid()); + logger.debug(msg); + } + + String destinationName = UUID.randomUUID().toString(); + + try { + if (destinationPool.getAvailable() < sourceDisk.getSize()) { + String msg = String.format("Not enough space on destination pool %s (%s bytes) to copy disk %s (size %s)", + destinationPool.getUuid(), destinationPool.getAvailable(), sourceDisk.getName(), sourceDisk.getSize()); + logger.error(msg); + throw new CloudRuntimeException(msg); + } + + KVMPhysicalDisk destinationDisk = storagePoolMgr.copyPhysicalDisk(sourceDisk, destinationName, destinationPool, 7200 * 1000); + targetDisks.add(destinationDisk); + } catch (Exception e) { + String err = String.format("Error copying converted instance disk number %s from the temporary location %s" + + " to destination storage pool %s: %s", i, sourceDisk.getPool().getLocalPath(), destinationPool.getUuid(), e.getMessage()); + logger.error(err, e); + cleanupMovedDisksOnDestinationPool(targetDisks); + return null; + } + } + return targetDisks; + } + + private void cleanupMovedDisksOnDestinationPool(List targetDisks) { + if (CollectionUtils.isEmpty(targetDisks)) { + return; + } + for (KVMPhysicalDisk disk : targetDisks) { + logger.info(String.format("Cleaning up disk %s from pool %s after conversion", disk.getName(), disk.getPool().getUuid())); + disk.getPool().deletePhysicalDisk(disk.getName(), Storage.ImageFormat.QCOW2); + } + } + + protected UnmanagedInstanceTO getConvertedUnmanagedInstance(String baseName, + List vmDisks, + LibvirtDomainXMLParser xmlParser) { + UnmanagedInstanceTO instanceTO = new UnmanagedInstanceTO(); + instanceTO.setName(baseName); + instanceTO.setDisks(getUnmanagedInstanceDisks(vmDisks, xmlParser)); + instanceTO.setNics(getUnmanagedInstanceNics(xmlParser)); + return instanceTO; + } + + private List getUnmanagedInstanceNics(LibvirtDomainXMLParser xmlParser) { + List nics = new ArrayList<>(); + if (xmlParser != null) { + List interfaces = xmlParser.getInterfaces(); + for (LibvirtVMDef.InterfaceDef interfaceDef : interfaces) { + UnmanagedInstanceTO.Nic nic = new UnmanagedInstanceTO.Nic(); + nic.setMacAddress(interfaceDef.getMacAddress()); + nic.setNicId(interfaceDef.getBrName()); + nic.setAdapterType(interfaceDef.getModel().toString()); + nics.add(nic); + } + } + return nics; + } + + protected List getUnmanagedInstanceDisks(List vmDisks, LibvirtDomainXMLParser xmlParser) { + List instanceDisks = new ArrayList<>(); + List diskDefs = xmlParser != null ? xmlParser.getDisks() : null; + for (int i = 0; i< vmDisks.size(); i++) { + KVMPhysicalDisk physicalDisk = vmDisks.get(i); + KVMStoragePool storagePool = physicalDisk.getPool(); + UnmanagedInstanceTO.Disk disk = new UnmanagedInstanceTO.Disk(); + disk.setPosition(i); + Pair storagePoolHostAndPath = getNfsStoragePoolHostAndPath(storagePool); + disk.setDatastoreHost(storagePoolHostAndPath.first()); + disk.setDatastorePath(storagePoolHostAndPath.second()); + disk.setDatastoreName(storagePool.getUuid()); + disk.setDatastoreType(storagePool.getType().name()); + disk.setCapacity(physicalDisk.getVirtualSize()); + disk.setFileBaseName(physicalDisk.getName()); + if (CollectionUtils.isNotEmpty(diskDefs)) { + LibvirtVMDef.DiskDef diskDef = diskDefs.get(i); + disk.setController(diskDef.getBusType() != null ? diskDef.getBusType().toString() : LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); + } else { + // If the job is finished but we cannot parse the XML, the guest VM can use the virtio driver + disk.setController(LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); + } + instanceDisks.add(disk); + } + return instanceDisks; + } + + protected Pair getNfsStoragePoolHostAndPath(KVMStoragePool storagePool) { + String sourceHostIp = null; + String sourcePath = null; + List commands = new ArrayList<>(); + commands.add(new String[]{Script.getExecutableAbsolutePath("mount")}); + commands.add(new String[]{Script.getExecutableAbsolutePath("grep"), storagePool.getLocalPath()}); + String storagePoolMountPoint = Script.executePipedCommands(commands, 0).second(); + logger.debug(String.format("NFS Storage pool: %s - local path: %s, mount point: %s", storagePool.getUuid(), storagePool.getLocalPath(), storagePoolMountPoint)); + if (StringUtils.isNotEmpty(storagePoolMountPoint)) { + String[] res = storagePoolMountPoint.strip().split(" "); + res = res[0].split(":"); + if (res.length > 1) { + sourceHostIp = res[0].strip(); + sourcePath = res[1].strip(); + } + } + return new Pair<>(sourceHostIp, sourcePath); + } + + protected LibvirtDomainXMLParser parseMigratedVMXmlDomain(String installPath) throws IOException { + String xmlPath = String.format("%s.xml", installPath); + if (!new File(xmlPath).exists()) { + String err = String.format("Conversion failed. Unable to find the converted XML domain, expected %s", xmlPath); + logger.error(err); + throw new CloudRuntimeException(err); + } + String xml; + try (InputStream is = new BufferedInputStream(new FileInputStream(xmlPath))) { + xml = IOUtils.toString(is, Charset.defaultCharset()); + } + final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + try { + parser.parseDomainXML(xml); + return parser; + } catch (RuntimeException e) { + String err = String.format("Error parsing the converted instance XML domain at %s: %s", xmlPath, e.getMessage()); + logger.error(err, e); + logger.debug(xml); + return null; + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupConvertedInstanceDisksCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupConvertedInstanceDisksCommandWrapper.java new file mode 100644 index 000000000000..05afd8b2234d --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupConvertedInstanceDisksCommandWrapper.java @@ -0,0 +1,67 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CleanupConvertedInstanceDisksCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.ResourceWrapper; + +import java.io.File; +import java.util.List; + +@ResourceWrapper(handles = CleanupConvertedInstanceDisksCommand.class) +public class LibvirtCleanupConvertedInstanceDisksCommandWrapper extends LibvirtBaseConvertCommandWrapper { + + @Override + public Answer execute(CleanupConvertedInstanceDisksCommand command, LibvirtComputingResource serverResource) { + DataStoreTO vmVolumesStore = command.getVmVolumesStore(); + String vmVolumesPrefix = command.getVmVolumesPrefix(); + + final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + KVMStoragePool conversionPool = getTemporaryStoragePool(vmVolumesStore, storagePoolMgr); + final String conversionPoolPath = conversionPool.getLocalPath(); + + try { + String volumesBasePath = String.format("%s/%s", conversionPoolPath, vmVolumesPrefix); + String xmlPath = String.format("%s.xml", volumesBasePath); + boolean xmlExists = new File(xmlPath).exists(); + + LibvirtDomainXMLParser xmlParser = xmlExists ? parseMigratedVMXmlDomain(volumesBasePath) : null; + List temporaryDisks = xmlExists && xmlParser != null ? + getTemporaryDisksFromParsedXml(conversionPool, xmlParser, volumesBasePath, conversionPoolPath, vmVolumesPrefix) : + getTemporaryDisksWithPrefixFromTemporaryPool(conversionPool, conversionPoolPath, vmVolumesPrefix); + + cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, conversionPool, vmVolumesPrefix, xmlExists); + + } catch (Exception e) { + String error = String.format("Error cleaning up converted disks with prefix %s from %s, due to: %s", + vmVolumesPrefix, conversionPoolPath, e.getMessage()); + logger.error(error, e); + return new Answer(command, false, error); + } + + return new Answer(command); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java index 5602da156799..28e24a9e0f2d 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java @@ -18,22 +18,9 @@ // package com.cloud.hypervisor.kvm.resource.wrapper; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; -import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.vm.UnmanagedInstanceTO; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; import com.cloud.agent.api.Answer; import com.cloud.agent.api.ImportConvertedInstanceAnswer; @@ -44,20 +31,14 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; -import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; import com.cloud.hypervisor.kvm.storage.KVMStoragePool; import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; -import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; -import com.cloud.storage.Storage; -import com.cloud.utils.FileUtil; -import com.cloud.utils.Pair; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.utils.script.Script; +import org.apache.commons.collections4.CollectionUtils; @ResourceWrapper(handles = ImportConvertedInstanceCommand.class) -public class LibvirtImportConvertedInstanceCommandWrapper extends CommandWrapper { +public class LibvirtImportConvertedInstanceCommandWrapper extends LibvirtBaseConvertCommandWrapper { @Override public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResource serverResource) { @@ -79,16 +60,22 @@ public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResour List temporaryDisks = xmlParser == null ? getTemporaryDisksWithPrefixFromTemporaryPool(temporaryStoragePool, temporaryConvertPath, temporaryConvertUuid) : - getTemporaryDisksFromParsedXml(temporaryStoragePool, xmlParser, convertedBasePath); + getTemporaryDisksFromParsedXml(temporaryStoragePool, xmlParser, convertedBasePath, temporaryConvertPath, temporaryConvertUuid); - List disks = null; + List disks; if (forceConvertToPool) { // Force flag to use the conversion path, no need to move disks disks = temporaryDisks; } else { disks = moveTemporaryDisksToDestination(temporaryDisks, destinationStoragePools, storagePoolMgr); - cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid); + cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid, true); + } + + if (CollectionUtils.isEmpty(disks)) { + String msg = String.format("Unable to import the converted disks for VM %s from destination pools", sourceInstanceName); + logger.error(msg); + return new ImportConvertedInstanceAnswer(cmd, false, msg); } UnmanagedInstanceTO convertedInstanceTO = getConvertedUnmanagedInstance(temporaryConvertUuid, @@ -106,200 +93,4 @@ public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResour } } } - - protected KVMStoragePool getTemporaryStoragePool(DataStoreTO conversionTemporaryLocation, KVMStoragePoolManager storagePoolMgr) { - if (conversionTemporaryLocation instanceof NfsTO) { - NfsTO nfsTO = (NfsTO) conversionTemporaryLocation; - return storagePoolMgr.getStoragePoolByURI(nfsTO.getUrl()); - } else { - PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) conversionTemporaryLocation; - return storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); - } - } - - protected List getTemporaryDisksFromParsedXml(KVMStoragePool pool, LibvirtDomainXMLParser xmlParser, String convertedBasePath) { - List disksDefs = xmlParser.getDisks(); - disksDefs = disksDefs.stream().filter(x -> x.getDiskType() == LibvirtVMDef.DiskDef.DiskType.FILE && - x.getDeviceType() == LibvirtVMDef.DiskDef.DeviceType.DISK).collect(Collectors.toList()); - if (CollectionUtils.isEmpty(disksDefs)) { - String err = String.format("Cannot find any disk defined on the converted XML domain %s.xml", convertedBasePath); - logger.error(err); - throw new CloudRuntimeException(err); - } - sanitizeDisksPath(disksDefs); - return getPhysicalDisksFromDefPaths(disksDefs, pool); - } - - private List getPhysicalDisksFromDefPaths(List disksDefs, KVMStoragePool pool) { - List disks = new ArrayList<>(); - for (LibvirtVMDef.DiskDef diskDef : disksDefs) { - KVMPhysicalDisk physicalDisk = pool.getPhysicalDisk(diskDef.getDiskPath()); - disks.add(physicalDisk); - } - return disks; - } - - protected List getTemporaryDisksWithPrefixFromTemporaryPool(KVMStoragePool pool, String path, String prefix) { - String msg = String.format("Could not parse correctly the converted XML domain, checking for disks on %s with prefix %s", path, prefix); - logger.info(msg); - pool.refresh(); - List disksWithPrefix = pool.listPhysicalDisks() - .stream() - .filter(x -> x.getName().startsWith(prefix) && !x.getName().endsWith(".xml")) - .collect(Collectors.toList()); - if (CollectionUtils.isEmpty(disksWithPrefix)) { - msg = String.format("Could not find any converted disk with prefix %s on temporary location %s", prefix, path); - logger.error(msg); - throw new CloudRuntimeException(msg); - } - return disksWithPrefix; - } - - private void cleanupDisksAndDomainFromTemporaryLocation(List disks, - KVMStoragePool temporaryStoragePool, - String temporaryConvertUuid) { - for (KVMPhysicalDisk disk : disks) { - logger.info(String.format("Cleaning up temporary disk %s after conversion from temporary location", disk.getName())); - temporaryStoragePool.deletePhysicalDisk(disk.getName(), Storage.ImageFormat.QCOW2); - } - logger.info(String.format("Cleaning up temporary domain %s after conversion from temporary location", temporaryConvertUuid)); - FileUtil.deleteFiles(temporaryStoragePool.getLocalPath(), temporaryConvertUuid, ".xml"); - } - - protected void sanitizeDisksPath(List disks) { - for (LibvirtVMDef.DiskDef disk : disks) { - String[] diskPathParts = disk.getDiskPath().split("/"); - String relativePath = diskPathParts[diskPathParts.length - 1]; - disk.setDiskPath(relativePath); - } - } - - protected List moveTemporaryDisksToDestination(List temporaryDisks, - List destinationStoragePools, - KVMStoragePoolManager storagePoolMgr) { - List targetDisks = new ArrayList<>(); - if (temporaryDisks.size() != destinationStoragePools.size()) { - String warn = String.format("Discrepancy between the converted instance disks (%s) " + - "and the expected number of disks (%s)", temporaryDisks.size(), destinationStoragePools.size()); - logger.warn(warn); - } - for (int i = 0; i < temporaryDisks.size(); i++) { - String poolPath = destinationStoragePools.get(i); - KVMStoragePool destinationPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.NetworkFilesystem, poolPath); - if (destinationPool == null) { - String err = String.format("Could not find a storage pool by URI: %s", poolPath); - logger.error(err); - continue; - } - if (destinationPool.getType() != Storage.StoragePoolType.NetworkFilesystem) { - String err = String.format("Storage pool by URI: %s is not an NFS storage", poolPath); - logger.error(err); - continue; - } - KVMPhysicalDisk sourceDisk = temporaryDisks.get(i); - if (logger.isDebugEnabled()) { - String msg = String.format("Trying to copy converted instance disk number %s from the temporary location %s" + - " to destination storage pool %s", i, sourceDisk.getPool().getLocalPath(), destinationPool.getUuid()); - logger.debug(msg); - } - - String destinationName = UUID.randomUUID().toString(); - - KVMPhysicalDisk destinationDisk = storagePoolMgr.copyPhysicalDisk(sourceDisk, destinationName, destinationPool, 7200 * 1000); - targetDisks.add(destinationDisk); - } - return targetDisks; - } - - private UnmanagedInstanceTO getConvertedUnmanagedInstance(String baseName, - List vmDisks, - LibvirtDomainXMLParser xmlParser) { - UnmanagedInstanceTO instanceTO = new UnmanagedInstanceTO(); - instanceTO.setName(baseName); - instanceTO.setDisks(getUnmanagedInstanceDisks(vmDisks, xmlParser)); - instanceTO.setNics(getUnmanagedInstanceNics(xmlParser)); - return instanceTO; - } - - private List getUnmanagedInstanceNics(LibvirtDomainXMLParser xmlParser) { - List nics = new ArrayList<>(); - if (xmlParser != null) { - List interfaces = xmlParser.getInterfaces(); - for (LibvirtVMDef.InterfaceDef interfaceDef : interfaces) { - UnmanagedInstanceTO.Nic nic = new UnmanagedInstanceTO.Nic(); - nic.setMacAddress(interfaceDef.getMacAddress()); - nic.setNicId(interfaceDef.getBrName()); - nic.setAdapterType(interfaceDef.getModel().toString()); - nics.add(nic); - } - } - return nics; - } - - protected List getUnmanagedInstanceDisks(List vmDisks, LibvirtDomainXMLParser xmlParser) { - List instanceDisks = new ArrayList<>(); - List diskDefs = xmlParser != null ? xmlParser.getDisks() : null; - for (int i = 0; i< vmDisks.size(); i++) { - KVMPhysicalDisk physicalDisk = vmDisks.get(i); - KVMStoragePool storagePool = physicalDisk.getPool(); - UnmanagedInstanceTO.Disk disk = new UnmanagedInstanceTO.Disk(); - disk.setPosition(i); - Pair storagePoolHostAndPath = getNfsStoragePoolHostAndPath(storagePool); - disk.setDatastoreHost(storagePoolHostAndPath.first()); - disk.setDatastorePath(storagePoolHostAndPath.second()); - disk.setDatastoreName(storagePool.getUuid()); - disk.setDatastoreType(storagePool.getType().name()); - disk.setCapacity(physicalDisk.getVirtualSize()); - disk.setFileBaseName(physicalDisk.getName()); - if (CollectionUtils.isNotEmpty(diskDefs)) { - LibvirtVMDef.DiskDef diskDef = diskDefs.get(i); - disk.setController(diskDef.getBusType() != null ? diskDef.getBusType().toString() : LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); - } else { - // If the job is finished but we cannot parse the XML, the guest VM can use the virtio driver - disk.setController(LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); - } - instanceDisks.add(disk); - } - return instanceDisks; - } - - protected Pair getNfsStoragePoolHostAndPath(KVMStoragePool storagePool) { - String sourceHostIp = null; - String sourcePath = null; - List commands = new ArrayList<>(); - commands.add(new String[]{Script.getExecutableAbsolutePath("mount")}); - commands.add(new String[]{Script.getExecutableAbsolutePath("grep"), storagePool.getLocalPath()}); - String storagePoolMountPoint = Script.executePipedCommands(commands, 0).second(); - logger.debug(String.format("NFS Storage pool: %s - local path: %s, mount point: %s", storagePool.getUuid(), storagePool.getLocalPath(), storagePoolMountPoint)); - if (StringUtils.isNotEmpty(storagePoolMountPoint)) { - String[] res = storagePoolMountPoint.strip().split(" "); - res = res[0].split(":"); - if (res.length > 1) { - sourceHostIp = res[0].strip(); - sourcePath = res[1].strip(); - } - } - return new Pair<>(sourceHostIp, sourcePath); - } - - protected LibvirtDomainXMLParser parseMigratedVMXmlDomain(String installPath) throws IOException { - String xmlPath = String.format("%s.xml", installPath); - if (!new File(xmlPath).exists()) { - String err = String.format("Conversion failed. Unable to find the converted XML domain, expected %s", xmlPath); - logger.error(err); - throw new CloudRuntimeException(err); - } - InputStream is = new BufferedInputStream(new FileInputStream(xmlPath)); - String xml = IOUtils.toString(is, Charset.defaultCharset()); - final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); - try { - parser.parseDomainXML(xml); - return parser; - } catch (RuntimeException e) { - String err = String.format("Error parsing the converted instance XML domain at %s: %s", xmlPath, e.getMessage()); - logger.error(err, e); - logger.debug(xml); - return null; - } - } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapperTest.java index 343a15b367d4..a30168266c0c 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapperTest.java @@ -127,7 +127,7 @@ public void testGetTemporaryDisksFromParsedXml() { Mockito.when(convertedDisk1.getName()).thenReturn("disk1"); Mockito.when(temporaryPool.getPhysicalDisk(relativePath)).thenReturn(convertedDisk1); - List disks = importInstanceCommandWrapper.getTemporaryDisksFromParsedXml(temporaryPool, parser, ""); + List disks = importInstanceCommandWrapper.getTemporaryDisksFromParsedXml(temporaryPool, parser, "", "", "prefix"); Mockito.verify(importInstanceCommandWrapper).sanitizeDisksPath(List.of(diskDef)); Assert.assertEquals(1, disks.size()); Assert.assertEquals("disk1", disks.get(0).getName()); diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 36905fb40ec9..4d9db4a55b1d 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -23,6 +23,7 @@ import com.cloud.agent.api.CheckConvertInstanceCommand; import com.cloud.agent.api.CheckVolumeAnswer; import com.cloud.agent.api.CheckVolumeCommand; +import com.cloud.agent.api.CleanupConvertedInstanceDisksCommand; import com.cloud.agent.api.ConvertInstanceAnswer; import com.cloud.agent.api.ConvertInstanceCommand; import com.cloud.agent.api.CopyRemoteVolumeAnswer; @@ -1717,13 +1718,14 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster logger.debug("The host {} is selected to execute the conversion of the " + "instance {} from VMware to KVM ", convertHost, sourceVMName); + long importStartTime = System.currentTimeMillis(); + importVMTask = importVmTasksManager.createImportVMTaskRecord(zone, owner, userId, displayName, vcenter, datacenterName, sourceVMName, + convertHost, importHost); + temporaryConvertLocation = selectInstanceConversionTemporaryLocation( destinationCluster, convertHost, importHost, convertStoragePoolId, forceConvertToPool); List convertStoragePools = findInstanceConversionDestinationStoragePoolsInCluster(destinationCluster, serviceOffering, dataDiskOfferingMap, temporaryConvertLocation, forceConvertToPool); - long importStartTime = System.currentTimeMillis(); - importVMTask = importVmTasksManager.createImportVMTaskRecord(zone, owner, userId, displayName, vcenter, datacenterName, sourceVMName, - convertHost, importHost); importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, null, CloningInstance); // sourceVMwareInstance could be a cloned instance from sourceVMName, of the sourceVMName itself if its powered off. @@ -2214,31 +2216,56 @@ private UnmanagedInstanceTO convertAndImportToKVM(ConvertInstanceCommand convert throw new CloudRuntimeException(err); } + boolean cleanupConvertedDisks = false; + String convertedDisksPrefix = null; Answer importAnswer; try { + convertedDisksPrefix = ((ConvertInstanceAnswer)convertAnswer).getTemporaryConvertUuid(); ImportConvertedInstanceCommand importCmd = new ImportConvertedInstanceCommand( remoteInstanceTO, destinationStoragePools, temporaryConvertLocation, - ((ConvertInstanceAnswer)convertAnswer).getTemporaryConvertUuid(), forceConvertToPool); + convertedDisksPrefix, forceConvertToPool); importAnswer = agentManager.send(importHost.getId(), importCmd); + + if (!importAnswer.getResult()) { + cleanupConvertedDisks = true; + String err = String.format( + "The import process failed for instance %s from VMware to KVM on host %s: %s", + sourceVM, importHost, importAnswer.getDetails()); + logger.error(err); + throw new CloudRuntimeException(err); + } } catch (AgentUnavailableException | OperationTimedoutException e) { + cleanupConvertedDisks = true; String err = String.format( "Could not send the import converted instance command to host %s due to: %s", importHost, e.getMessage()); logger.error(err, e); throw new CloudRuntimeException(err); - } - - if (!importAnswer.getResult()) { - String err = String.format( - "The import process failed for instance %s from VMware to KVM on host %s: %s", - sourceVM, importHost, importAnswer.getDetails()); - logger.error(err); - throw new CloudRuntimeException(err); + } finally { + if (cleanupConvertedDisks) { + cleanupConvertedDisks(sourceVM, convertHost, temporaryConvertLocation, convertedDisksPrefix); + } } return ((ImportConvertedInstanceAnswer) importAnswer).getConvertedInstance(); } + private void cleanupConvertedDisks(String sourceVM, HostVO convertHost, DataStoreTO temporaryConvertLocation, String convertedDisksPrefix) { + logger.debug("Cleaning up the converted disks for the VM {} through the conversion host {}", sourceVM, convertHost.getName()); + CleanupConvertedInstanceDisksCommand cleanupCommand = + new CleanupConvertedInstanceDisksCommand(temporaryConvertLocation, convertedDisksPrefix); + try { + Answer cleanupAnswer = agentManager.send(convertHost.getId(), cleanupCommand); + if (!cleanupAnswer.getResult()) { + logger.warn("Failed to cleanup the converted disks for the VM {} through " + + "the conversion host {}: {}", sourceVM, convertHost.getName(), cleanupAnswer.getDetails()); + } + } catch (AgentUnavailableException | OperationTimedoutException e) { + logger.error("Error cleaning up converted disks for VM {} through the conversion host {}", + sourceVM, convertHost.getName(), e); + } + } + private List findInstanceConversionDestinationStoragePoolsInCluster( Cluster destinationCluster, ServiceOfferingVO serviceOffering, Map dataDiskOfferingMap, From 42322a59c7d68247db03a538582e4c9ea1b259d5 Mon Sep 17 00:00:00 2001 From: James Peru Mmbono Date: Wed, 8 Jul 2026 15:06:41 +0300 Subject: [PATCH 163/205] feat(backup): incremental NAS backup support for KVM (#13074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements incremental backup support for the NAS backup provider on KVM, using QEMU dirty bitmaps and libvirt's backup-begin API. RFC: apache/cloudstack#12899. For large VMs this reduces daily backup storage 80–95% and shortens backup windows from hours to minutes (e.g. a 500 GB VM with moderate writes goes from ~500 GB/day to ~5–15 GB/day after the initial full backup). Signed-off-by: James Peru Co-authored-by: jmsperu Co-authored-by: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> --- .../org/apache/cloudstack/backup/Backup.java | 6 +- .../cloudstack/backup/BackupProvider.java | 12 + .../cloudstack/backup/BackupAnswer.java | 23 + .../cloudstack/backup/TakeBackupCommand.java | 43 ++ .../apache/cloudstack/backup/BackupVO.java | 8 +- .../cloudstack/backup/NASBackupChainKeys.java | 62 ++ .../cloudstack/backup/NASBackupProvider.java | 687 +++++++++++++++++- .../backup/NASBackupProviderTest.java | 452 ++++++++++++ .../LibvirtRestoreBackupCommandWrapper.java | 26 + .../LibvirtTakeBackupCommandWrapper.java | 152 +++- ...ibvirtRestoreBackupCommandWrapperTest.java | 2 + .../com/cloud/hypervisor/guru/VMwareGuru.java | 2 +- scripts/vm/hypervisor/kvm/nasbackup.sh | 252 ++++++- .../java/com/cloud/hypervisor/KVMGuru.java | 2 +- .../cloudstack/backup/BackupManagerImpl.java | 15 +- .../smoke/test_backup_recovery_nas.py | 320 ++++++++ 16 files changed, 2019 insertions(+), 45 deletions(-) create mode 100644 plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupChainKeys.java diff --git a/api/src/main/java/org/apache/cloudstack/backup/Backup.java b/api/src/main/java/org/apache/cloudstack/backup/Backup.java index 2d68f18b953f..865d657a7a48 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/Backup.java +++ b/api/src/main/java/org/apache/cloudstack/backup/Backup.java @@ -39,7 +39,11 @@ public interface Backup extends ControlledEntity, InternalIdentity, Identity { Long getHostId(); enum Status { - Allocated, Queued, BackingUp, ReadyForImageTransfer, FinalizingImageTransfer, BackedUp, Error, Failed, Restoring, Removed, Expunged + Allocated, Queued, BackingUp, ReadyForImageTransfer, FinalizingImageTransfer, BackedUp, Error, Failed, Restoring, Removed, Expunged, + // Hidden: a chain backup kept as a tombstone after the user deleted it while it still has + // live descendants (incremental chains). Excluded from listBackups and from all backup + // operations (which require BackedUp); swept from the DB once its last descendant is gone. + Hidden } class Metric { diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java index 23b8092425d9..4ae9148113a4 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java @@ -87,6 +87,18 @@ public interface BackupProvider { */ boolean deleteBackup(Backup backup, boolean forced); + /** + * Whether {@link #deleteBackup(Backup, boolean)} owns DB-row removal and resource-count / + * usage accounting for every backup it physically removes. Providers that manage incremental + * chains (e.g. NAS) delete several backups per call — the leaf plus swept delete-pending + * ancestors — and decrement once per removed backup themselves, so the manager must NOT + * decrement or remove the row again. Defaults to {@code false}: the manager does the + * single-backup accounting (the historical behaviour for non-chain providers). + */ + default boolean handlesChainDeleteResourceAccounting() { + return false; + } + Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid); /** diff --git a/core/src/main/java/org/apache/cloudstack/backup/BackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/BackupAnswer.java index ffc67b628a7e..abe78ee5553d 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/BackupAnswer.java +++ b/core/src/main/java/org/apache/cloudstack/backup/BackupAnswer.java @@ -29,6 +29,12 @@ public class BackupAnswer extends Answer { private Long virtualSize; private Map volumes; Boolean needsCleanup; + // Set by the NAS backup provider after a checkpoint/bitmap was created during this backup. + // The provider persists it in backup_details under NASBackupChainKeys.BITMAP_NAME. + private String bitmapCreated; + // Set when an incremental was requested but the agent had to fall back to a full + // (e.g. VM was stopped). Provider should record this backup as type=full. + private Boolean incrementalFallback; public BackupAnswer(final Command command, final boolean success, final String details) { super(command, success, details); @@ -68,4 +74,21 @@ public Boolean getNeedsCleanup() { public void setNeedsCleanup(Boolean needsCleanup) { this.needsCleanup = needsCleanup; } + + public String getBitmapCreated() { + return bitmapCreated; + } + + public void setBitmapCreated(String bitmapCreated) { + this.bitmapCreated = bitmapCreated; + } + + public Boolean getIncrementalFallback() { + return incrementalFallback != null && incrementalFallback; + } + + public void setIncrementalFallback(Boolean incrementalFallback) { + this.incrementalFallback = incrementalFallback; + } + } diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java index 5402b6b24760..34f8d7b8bcdd 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java @@ -36,6 +36,17 @@ public class TakeBackupCommand extends Command { @LogLevel(LogLevel.Log4jLevel.Off) private String mountOptions; + // Incremental backup fields (NAS provider; null/empty for legacy full-only callers). + private String mode; // "full" or "incremental"; null => legacy behaviour (script default) + private String bitmapNew; // Checkpoint/bitmap name to create with this backup (timestamp-based) + private String bitmapParent; // Incremental: parent bitmap to read changes since + + // Per-volume parent backup file paths (one per VM volume, ordered by deviceId — same + // order as volumePaths). The script rebases each new qcow2 onto the matching parent. + // Backup file UUIDs differ across volumes, so a single parentPath would have rebased + // every data disk onto the root file. New callers MUST populate parentPaths. + private List parentPaths; + public TakeBackupCommand(String vmName, String backupPath) { super(); this.vmName = vmName; @@ -106,6 +117,38 @@ public void setQuiesce(Boolean quiesce) { this.quiesce = quiesce; } + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public String getBitmapNew() { + return bitmapNew; + } + + public void setBitmapNew(String bitmapNew) { + this.bitmapNew = bitmapNew; + } + + public String getBitmapParent() { + return bitmapParent; + } + + public void setBitmapParent(String bitmapParent) { + this.bitmapParent = bitmapParent; + } + + public List getParentPaths() { + return parentPaths; + } + + public void setParentPaths(List parentPaths) { + this.parentPaths = parentPaths; + } + @Override public boolean executeInSequence() { return true; diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java index d589f9e6bef8..7754c2440c09 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java @@ -66,7 +66,7 @@ public class BackupVO implements Backup { private String externalId; @Column(name = "type") - private String backupType; + private String type; @Column(name = "date") @Temporal(value = TemporalType.DATE) @@ -125,7 +125,7 @@ public BackupVO() { @Override public String toString() { return String.format("Backup %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( - this, "id", "uuid", "vmId", "backupType", "externalId")); + this, "id", "uuid", "vmId", "type", "externalId")); } @Override @@ -157,11 +157,11 @@ public void setExternalId(String externalId) { } public String getType() { - return backupType; + return type; } public void setType(String type) { - this.backupType = type; + this.type = type; } @Override diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupChainKeys.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupChainKeys.java new file mode 100644 index 000000000000..511f0ccb7114 --- /dev/null +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupChainKeys.java @@ -0,0 +1,62 @@ +// 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.backup; + +/** + * Keys used by the NAS backup provider when storing incremental-chain metadata + * in the existing {@code backup_details} key/value table. Stored here (not on + * the {@code backups} table) so other providers do not need a schema change to + * support their own incremental implementations. + */ +public final class NASBackupChainKeys { + + /** UUID of the parent backup (full or previous incremental). Empty for full backups. */ + public static final String PARENT_BACKUP_ID = "nas.parent_backup_id"; + + /** QEMU dirty-bitmap name created by this backup, used as the {@code } reference for the next one. */ + public static final String BITMAP_NAME = "nas.bitmap_name"; + + /** Identifier shared by every backup in the same chain (the full anchors a chain; its incrementals inherit the id). */ + public static final String CHAIN_ID = "nas.chain_id"; + + /** Position within the chain: 0 for the full, 1 for the first incremental, and so on. */ + public static final String CHAIN_POSITION = "nas.chain_position"; + + /** + * In-memory chain-mode sentinels used by {@code ChainDecision.mode}. The persisted + * full-vs-incremental backup type lives on the {@code backup.type} column (set in + * {@code takeBackup}) — single source of truth. Not duplicated into backup_details. + */ + public static final String TYPE_FULL = "full"; + public static final String TYPE_INCREMENTAL = "incremental"; + // Feature disabled: behave exactly like the pre-incremental full-only backup — no QEMU + // bitmap/checkpoint is created and no chain metadata is persisted. Matches nasbackup.sh's + // "legacy-full" mode token (which sets make_checkpoint=0). + public static final String TYPE_LEGACY_FULL = "legacy-full"; + + /** + * VM-scoped detail (stored in {@code vm_instance_details}) holding the QEMU dirty-bitmap + * name that currently exists on the running VM and is therefore the only valid parent + * for the next incremental backup. Written by {@link #BITMAP_NAME} on each successful + * backup; cleared on restore (the restored disk image has no bitmap, so the next backup + * must be a fresh full). When the VM has no detail, {@code decideChain} forces full. + */ + public static final String VM_ACTIVE_CHECKPOINT_ID = "nas.active_checkpoint_id"; + + private NASBackupChainKeys() { + } +} diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index a7121f149e30..fe56fbf7c1ae 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -19,6 +19,7 @@ import com.cloud.agent.AgentManager; import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.OperationTimedoutException; +import com.cloud.configuration.Resource; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; @@ -38,8 +39,11 @@ import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.user.ResourceLimitService; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.VMSnapshotDetailsVO; import com.cloud.vm.snapshot.VMSnapshotVO; @@ -48,6 +52,7 @@ import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupRepositoryDao; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; @@ -85,6 +90,29 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co true, BackupFrameworkEnabled.key()); + ConfigKey NASBackupFullEvery = new ConfigKey<>("Advanced", Integer.class, + "nas.backup.full.every", + "10", + "Take a full NAS backup every Nth backup; remaining backups in between are incremental. " + + "Counts backups, not days, so it works for hourly, daily, and ad-hoc schedules. " + + "Set to 1 to disable incrementals (every backup is full).", + true, + ConfigKey.Scope.Zone, + BackupFrameworkEnabled.key()); + + ConfigKey NASBackupIncrementalEnabled = new ConfigKey<>("Advanced", Boolean.class, + "nas.backup.incremental.enabled", + "false", + "Master switch for NAS incremental backups. Defaults to false so existing zones keep the " + + "legacy full-only behavior on upgrade; opt in per-zone when ready to use chains. " + + "When false, every NAS backup is taken as a full regardless of nas.backup.full.every. " + + "Toggling this is safe at any time: switching off forces the next backup to be a fresh " + + "full anchor (existing chains stay restorable), switching back on resumes incrementals " + + "on the next full + incremental cycle.", + true, + ConfigKey.Scope.Zone, + BackupFrameworkEnabled.key()); + @Inject private BackupDao backupDao; @@ -106,6 +134,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private VMInstanceDao vmInstanceDao; + @Inject + private VMInstanceDetailsDao vmInstanceDetailsDao; + @Inject private PrimaryDataStoreDao primaryDataStoreDao; @@ -115,6 +146,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private AgentManager agentManager; + @Inject + private ResourceLimitService resourceLimitMgr; + @Inject private VMSnapshotDao vmSnapshotDao; @@ -130,6 +164,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private DiskOfferingDao diskOfferingDao; + @Inject + private BackupDetailsDao backupDetailsDao; + private Long getClusterIdFromRootVolume(VirtualMachine vm) { VolumeVO rootVolume = volumeDao.getInstanceRootVolume(vm.getId()); StoragePoolVO rootDiskPool = primaryDataStoreDao.findById(rootVolume.getPoolId()); @@ -168,6 +205,330 @@ protected Host getVMHypervisorHost(VirtualMachine vm) { return resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, vm.getDataCenterId()); } + /** + * Returned by {@link #decideChain(VirtualMachine)} to describe the next backup's place in + * the chain: full vs incremental, the bitmap name to create, and (for incrementals) the + * parent bitmap and parent file path. + */ + static final class ChainDecision { + final String mode; // "full" or "incremental" + final String bitmapNew; + final String bitmapParent; // null for full + // Per-volume parent backup file paths, one per current VM volume in deviceId order. + // null/empty for full. Each volume needs its own parent file because backup files + // are named after each volume's own UUID (root..qcow2 / datadisk..qcow2). + final List parentPaths; + final String chainId; // chain identifier this backup belongs to + final int chainPosition; // 0 for full, N for the Nth incremental in the chain + + private ChainDecision(String mode, String bitmapNew, String bitmapParent, List parentPaths, + String chainId, int chainPosition) { + this.mode = mode; + this.bitmapNew = bitmapNew; + this.bitmapParent = bitmapParent; + this.parentPaths = parentPaths; + this.chainId = chainId; + this.chainPosition = chainPosition; + } + + static ChainDecision fullStart(String bitmapName) { + return new ChainDecision(NASBackupChainKeys.TYPE_FULL, bitmapName, null, null, + UUID.randomUUID().toString(), 0); + } + + /** + * Decision used when the incremental feature is disabled: a plain full backup that + * creates no bitmap and carries no chain identity, so nothing chain/checkpoint-related + * is sent to the agent or persisted. Keeps the feature-off path byte-for-byte legacy. + */ + static ChainDecision legacyFull() { + return new ChainDecision(NASBackupChainKeys.TYPE_LEGACY_FULL, null, null, null, null, 0); + } + + static ChainDecision incremental(String bitmapNew, String bitmapParent, List parentPaths, + String chainId, int chainPosition) { + return new ChainDecision(NASBackupChainKeys.TYPE_INCREMENTAL, bitmapNew, bitmapParent, + parentPaths, chainId, chainPosition); + } + + boolean isIncremental() { + return NASBackupChainKeys.TYPE_INCREMENTAL.equals(mode); + } + + boolean isLegacyFull() { + return NASBackupChainKeys.TYPE_LEGACY_FULL.equals(mode); + } + } + + /** + * Decides whether the next backup for {@code vm} should be a fresh full or an incremental + * appended to the existing chain. Stopped VMs are always full (libvirt {@code backup-begin} + * requires a running QEMU process). The {@code nas.backup.full.every} ConfigKey controls + * how many backups (full + incrementals) form one chain before a new full is forced. + * + *

The decision is anchored on the VM's {@code nas.active_checkpoint_id} detail, which + * records the bitmap that currently exists on the running QEMU. After a restore that + * detail is cleared, so the next backup is automatically full — even though there may be + * a more recent "last backup taken" row in the database. The decision deliberately avoids + * relying on "last backup taken", because that row is misleading after a restore.

+ */ + protected ChainDecision decideChain(VirtualMachine vm) { + // Master switch — when the operator disables incrementals at the zone level the backup + // behaves exactly like the pre-incremental full-only path: no bitmap is generated and no + // chain/checkpoint metadata is created, sent to the agent, or persisted (legacy-full). + Boolean incrementalEnabled = NASBackupIncrementalEnabled.valueIn(vm.getDataCenterId()); + if (incrementalEnabled == null || !incrementalEnabled) { + return ChainDecision.legacyFull(); + } + + // Incremental backups rely on QEMU dirty bitmaps / libvirt checkpoints, which only exist + // on file-based qcow2 storage. Storage such as Ceph-RBD and Linstor cannot carry per-disk + // checkpoints, so a VM with any volume on such a pool must stay on the full-only (legacy) + // path — otherwise an incremental attempt would fail or regress those storages. + if (!allVolumesOnCheckpointCapableStorage(vm)) { + return ChainDecision.legacyFull(); + } + + final String newBitmap = "backup-" + System.currentTimeMillis() / 1000L; + + // Stopped VMs cannot do incrementals — script will also fall back, but we make the + // decision here so we register the right type up-front. + if (VirtualMachine.State.Stopped.equals(vm.getState())) { + return ChainDecision.fullStart(newBitmap); + } + + Integer fullEvery = NASBackupFullEvery.valueIn(vm.getDataCenterId()); + if (fullEvery == null || fullEvery <= 1) { + // Disabled or every-backup-is-full mode. + return ChainDecision.fullStart(newBitmap); + } + + // 1. If the VM has no active_checkpoint_id, there is no bitmap on the host to use as + // a parent. This is the case after restore (we clear it), after VM was just assigned + // to the offering, or on the very first backup. + String activeCheckpoint = readVmActiveCheckpoint(vm.getId()); + if (activeCheckpoint == null) { + return ChainDecision.fullStart(newBitmap); + } + + // 2. The most-recent BackedUp backup is the only safe parent — after restore the + // next backup is always a fresh full, so anything older has a rotated-out bitmap. + // If the latest backup's bitmap doesn't match the VM's active_checkpoint_id, the + // chain is broken: force a full. + Backup parent = findLatestBackedUpBackup(vm.getId()); + if (parent == null || !activeCheckpoint.equals(readDetail(parent, NASBackupChainKeys.BITMAP_NAME))) { + LOG.debug("VM {} latest backup does not match active_checkpoint_id={} — forcing full", + vm.getInstanceName(), activeCheckpoint); + return ChainDecision.fullStart(newBitmap); + } + + String parentChainId = readDetail(parent, NASBackupChainKeys.CHAIN_ID); + int parentChainPosition = chainPosition(parent); + if (parentChainId == null || parentChainPosition == Integer.MAX_VALUE) { + return ChainDecision.fullStart(newBitmap); + } + + // Force a fresh full when the chain has reached the configured length. + if (parentChainPosition + 1 >= fullEvery) { + return ChainDecision.fullStart(newBitmap); + } + + // The script needs the parent backup's on-NAS file path PER VOLUME so it can rebase + // each new qcow2 onto the matching parent. The paths are stored relative to the NAS + // mount root — the script resolves them inside its mount session. When alignment + // fails (volume count changed, etc.) compose returns null and we fall back to full + // so we don't risk corrupting the chain. + List parentPaths = composeParentBackupPaths(parent, vm.getId()); + if (parentPaths == null) { + LOG.debug("VM {} parent backup {} volume layout no longer matches current VM — forcing full", + vm.getInstanceName(), parent.getUuid()); + return ChainDecision.fullStart(newBitmap); + } + return ChainDecision.incremental(newBitmap, activeCheckpoint, parentPaths, + parentChainId, parentChainPosition + 1); + } + + /** + * Incremental backups require QEMU dirty bitmaps / libvirt checkpoints, which are only + * possible on file-based qcow2 storage. Returns {@code true} only when EVERY volume of the + * VM sits on HOST-scope local, {@code SharedMountPoint}, or {@code NetworkFilesystem} (NFS) + * storage. Ceph-RBD, Linstor, and any other pool that cannot carry a per-disk checkpoint + * make this return {@code false} so the caller falls back to the legacy full-only path. A + * volume whose pool can no longer be resolved is treated as incapable (safe default). + */ + protected boolean allVolumesOnCheckpointCapableStorage(VirtualMachine vm) { + List volumes = volumeDao.findByInstance(vm.getId()); + if (volumes == null) { + return true; + } + for (VolumeVO volume : volumes) { + StoragePoolVO pool = primaryDataStoreDao.findById(volume.getPoolId()); + if (pool == null) { + LOG.debug("VM {} volume {} has no resolvable storage pool — forcing legacy full", + vm.getInstanceName(), volume.getUuid()); + return false; + } + boolean checkpointCapable = ScopeType.HOST.equals(pool.getScope()) + || Storage.StoragePoolType.SharedMountPoint.equals(pool.getPoolType()) + || Storage.StoragePoolType.NetworkFilesystem.equals(pool.getPoolType()); + if (!checkpointCapable) { + LOG.debug("VM {} volume {} is on {} (scope {}) which cannot carry checkpoints — forcing legacy full", + vm.getInstanceName(), volume.getUuid(), pool.getPoolType(), pool.getScope()); + return false; + } + } + return true; + } + + /** + * Read the {@code nas.active_checkpoint_id} VM detail. Returns {@code null} when no detail + * exists (post-restore, first backup, or after explicit reset). + */ + private String readVmActiveCheckpoint(long vmId) { + VMInstanceDetailVO d = vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + if (d == null) { + return null; + } + String v = d.getValue(); + return (v == null || v.isEmpty()) ? null : v; + } + + /** + * Locate the most-recent {@code BackedUp} backup for {@code vmId}. The chain invariant + * guarantees the latest backup is the only valid incremental parent — after restore the + * next backup is always a fresh full, and {@link #decideChain} checks the bitmap matches. + */ + private Backup findLatestBackedUpBackup(long vmId) { + List history = backupDao.listByVmId(null, vmId); + if (history == null || history.isEmpty()) { + return null; + } + return history.stream() + .filter(b -> Backup.Status.BackedUp.equals(b.getStatus())) + .max(Comparator.comparing(Backup::getDate)) + .orElse(null); + } + + private String readDetail(Backup backup, String key) { + BackupDetailVO d = backupDetailsDao.findDetail(backup.getId(), key); + return d == null ? null : d.getValue(); + } + + /** + * Compose the on-NAS path of EVERY parent backup file (one per VM volume) in the same + * order the script will iterate the current VM's disks (deviceId asc). Relative to the + * NAS mount, matches the layout written by {@code nasbackup.sh}: + * first disk -> {@code /root..qcow2} + * others -> {@code /datadisk..qcow2} + * + * Returns {@code null} if the parent's stored volume count doesn't match the current VM's + * volume count. Volume attach/detach is blocked while a VM is assigned to a backup offering; + * if the offering was removed and re-assigned the active checkpoint is cleared in + * {@link #removeVMFromBackupOffering}, so this method doesn't need to revalidate volume + * identities — a count mismatch is the only way to reach this branch with a non-null + * active_checkpoint_id. + */ + private List composeParentBackupPaths(Backup parent, long vmId) { + // backupPath is stored as externalId by createBackupObject — e.g. + // "i-2-1234-VM/2026.04.27.13.45.00". + String dir = parent.getExternalId(); + if (dir == null || dir.isEmpty()) { + return null; + } + + List parentVols = parent.getBackedUpVolumes(); + if (parentVols == null || parentVols.isEmpty()) { + return null; + } + + List currentVols = volumeDao.findByInstance(vmId); + if (currentVols == null || currentVols.size() != parentVols.size()) { + return null; + } + + // parentVols is in deviceId order at the time the parent was taken. The script names the + // per-disk files from the volume PATH basename (root..qcow2 / datadisk..qcow2, + // see nasbackup.sh: volUuid="${fullpath##*/}"). Use getPath(), NOT getUuid(): after a + // volume migration the uuid and the on-disk path diverge, and the backup file is named by + // path — a uuid-based parent path then fails to resolve for the incremental (test 13). + List paths = new ArrayList<>(parentVols.size()); + for (int i = 0; i < parentVols.size(); i++) { + String volPath = parentVols.get(i).getPath(); + String prefix = (i == 0) ? "root" : "datadisk"; + paths.add(dir + "/" + prefix + "." + volPath + ".qcow2"); + } + return paths; + } + + /** + * Persist chain metadata under backup_details. Stored here (not on the backups table) so + * other providers can implement their own chain semantics without schema changes. + */ + private void persistChainMetadata(Backup backup, ChainDecision decision, String bitmapFromAgent) { + // Only persist nas.bitmap_name when the agent confirmed the bitmap exists on the host. + // The agent wrapper sets bitmapFromAgent=null when nasbackup.sh exits + // EXIT_BITMAP_NOT_SEEDED (=22) — currently only the stopped-VM path where qemu-img + // bitmap --add failed on every source disk. Anchoring the next incremental on a + // bitmap that doesn't exist would force a non-recoverable failure, so we leave the + // detail empty and let the next backup start a fresh full chain. + if (bitmapFromAgent != null && !bitmapFromAgent.isEmpty()) { + backupDetailsDao.persist(new BackupDetailVO(backup.getId(), NASBackupChainKeys.BITMAP_NAME, bitmapFromAgent, true)); + } + backupDetailsDao.persist(new BackupDetailVO(backup.getId(), NASBackupChainKeys.CHAIN_ID, decision.chainId, true)); + backupDetailsDao.persist(new BackupDetailVO(backup.getId(), NASBackupChainKeys.CHAIN_POSITION, + String.valueOf(decision.chainPosition), true)); + // Backup full-vs-incremental type lives on backup.type (set by takeBackup) — single + // source of truth. Not duplicated into backup_details. + if (decision.isIncremental()) { + // Resolve the parent backup's UUID so restore can walk the chain by id, not by path. + String parentUuid = lookupParentBackupUuid(backup.getVmId(), decision.bitmapParent); + if (parentUuid != null) { + backupDetailsDao.persist(new BackupDetailVO(backup.getId(), NASBackupChainKeys.PARENT_BACKUP_ID, parentUuid, true)); + } + } + } + + /** + * Upsert the VM's {@code nas.active_checkpoint_id} detail to {@code bitmapName}. Called + * after every successful backup so the next backup's parent-bitmap decision is anchored + * on what actually exists on QEMU, not on "last backup taken". + */ + private void upsertVmActiveCheckpoint(long vmId, String bitmapName) { + VMInstanceDetailVO existing = vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + if (existing == null) { + vmInstanceDetailsDao.addDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID, bitmapName, false); + return; + } + existing.setValue(bitmapName); + vmInstanceDetailsDao.update(existing.getId(), existing); + } + + /** + * Remove the VM's {@code nas.active_checkpoint_id} detail. Called from the restore paths: + * after restore the disk image has no QEMU bitmap attached, so any future incremental + * would be based on stale state. Clearing forces the next backup to be a fresh full. + */ + private void clearVmActiveCheckpoint(long vmId) { + VMInstanceDetailVO existing = vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + if (existing != null) { + vmInstanceDetailsDao.removeDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + LOG.debug("Cleared nas.active_checkpoint_id for VM id={} (was {})", vmId, existing.getValue()); + } + } + + private String lookupParentBackupUuid(long vmId, String parentBitmap) { + if (parentBitmap == null) { + return null; + } + for (Backup b : backupDao.listByVmId(null, vmId)) { + String bm = readDetail(b, NASBackupChainKeys.BITMAP_NAME); + if (parentBitmap.equals(bm)) { + return b.getUuid(); + } + } + return null; + } + protected Host getVMHypervisorHostForBackup(VirtualMachine vm) { Long hostId = vm.getHostId(); if (hostId == null && VirtualMachine.State.Running.equals(vm.getState())) { @@ -205,12 +566,20 @@ public Pair takeBackup(final VirtualMachine vm, Boolean quiesce final String backupPath = String.format("%s/%s", vm.getInstanceName(), new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss").format(creationDate)); - BackupVO backupVO = createBackupObject(vm, backupPath); + // Decide full vs incremental for this backup. Stopped VMs are always full + // (libvirt backup-begin requires a running QEMU process). + ChainDecision decision = decideChain(vm); + + BackupVO backupVO = createBackupObject(vm, backupPath, decision.isIncremental() ? "INCREMENTAL" : "FULL"); TakeBackupCommand command = new TakeBackupCommand(vm.getInstanceName(), backupPath); command.setBackupRepoType(backupRepository.getType()); command.setBackupRepoAddress(backupRepository.getAddress()); command.setMountOptions(backupRepository.getMountOptions()); command.setQuiesce(quiesceVM); + command.setMode(decision.mode); + command.setBitmapNew(decision.bitmapNew); + command.setBitmapParent(decision.bitmapParent); + command.setParentPaths(decision.parentPaths); if (VirtualMachine.State.Stopped.equals(vm.getState())) { List vmVolumes = volumeDao.findByInstance(vm.getId()); @@ -239,9 +608,31 @@ public Pair takeBackup(final VirtualMachine vm, Boolean quiesce backupVO.setDate(new Date()); backupVO.setSize(answer.getSize()); backupVO.setStatus(Backup.Status.BackedUp); + // If the agent fell back to full (stopped VM mid-incremental cycle), record this + // backup as a full and start a new chain. + ChainDecision effective = decision; + if (answer.getIncrementalFallback()) { + effective = ChainDecision.fullStart(decision.bitmapNew); + backupVO.setType("FULL"); + } List volumes = new ArrayList<>(volumeDao.findByInstance(vm.getId())); backupVO.setBackedUpVolumes(backupManager.createVolumeInfoFromVolumes(volumes)); if (backupDao.update(backupVO.getId(), backupVO)) { + // Legacy-full (incremental feature disabled): persist no chain/checkpoint metadata + // and do not touch the VM's active_checkpoint_id — keep the feature-off path legacy. + if (!decision.isLegacyFull()) { + persistChainMetadata(backupVO, effective, answer.getBitmapCreated()); + // Pin the VM's active_checkpoint_id to whichever bitmap the agent actually + // created — the only valid parent for the next incremental (see decideChain). + // If the agent reports no bitmap (bitmapCreated=null), clear any stale detail + // so the next backup starts a fresh full. + String confirmedBitmap = answer.getBitmapCreated(); + if (confirmedBitmap != null) { + upsertVmActiveCheckpoint(vm.getId(), confirmedBitmap); + } else { + clearVmActiveCheckpoint(vm.getId()); + } + } return new Pair<>(true, backupVO); } else { throw new CloudRuntimeException("Failed to update backup"); @@ -260,11 +651,11 @@ public Pair takeBackup(final VirtualMachine vm, Boolean quiesce } } - private BackupVO createBackupObject(VirtualMachine vm, String backupPath) { + private BackupVO createBackupObject(VirtualMachine vm, String backupPath, String type) { BackupVO backup = new BackupVO(); backup.setVmId(vm.getId()); backup.setExternalId(backupPath); - backup.setType("FULL"); + backup.setType(type); backup.setDate(new Date()); long virtualSize = 0L; for (final Volume volume: volumeDao.findByInstance(vm.getId())) { @@ -333,6 +724,11 @@ private Pair restoreVMBackup(VirtualMachine vm, Backup backup) } catch (OperationTimedoutException e) { throw new CloudRuntimeException("Operation to restore backup timed out, please try again"); } + // After a restore the QEMU dirty-bitmap chain is gone — clear active_checkpoint_id so + // the next backup is taken as a fresh full and starts a new chain. See decideChain. + if (answer != null && answer.getResult()) { + clearVmActiveCheckpoint(vm.getId()); + } return new Pair<>(answer.getResult(), answer.getDetails()); } @@ -457,6 +853,13 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI } catch (Exception e) { throw new CloudRuntimeException("Unable to create restored volume due to: " + e); } + // The restored volume is attached to this VM with no QEMU bitmap on its image, so the + // VM's tracked checkpoint is now stale; clear it to force the next backup to be a full + // (mirrors the full restore paths restoreVMFromBackup/restoreBackupToVM). + VirtualMachine restoreTargetVm = vmInstanceDao.findVMByInstanceName(vmNameAndState.first()); + if (restoreTargetVm != null) { + clearVmActiveCheckpoint(restoreTargetVm.getId()); + } } return new Pair<>(answer.getResult(), answer.getDetails()); @@ -477,6 +880,14 @@ private Backup.VolumeInfo getBackedUpVolumeInfo(List backedUp .orElse(null); } + @Override + public boolean handlesChainDeleteResourceAccounting() { + // This provider deletes whole incremental chains (leaf + swept delete-pending ancestors) + // and decrements resource count/usage once per physically-removed backup itself, so the + // manager must not also decrement or remove the DB row. + return true; + } + @Override public boolean deleteBackup(Backup backup, boolean forced) { final BackupRepository backupRepository = backupRepositoryDao.findByBackupOfferingId(backup.getBackupOfferingId()); @@ -495,9 +906,42 @@ public boolean deleteBackup(Backup backup, boolean forced) { throw new CloudRuntimeException(String.format("Unable to find a running KVM host in zone %d to delete backup %s", backup.getZoneId(), backup.getUuid())); } - DeleteBackupCommand command = new DeleteBackupCommand(backup.getExternalId(), backupRepository.getType(), - backupRepository.getAddress(), backupRepository.getMountOptions()); + // Backups outside any tracked chain (legacy or non-incremental providers) are + // deleted straight away — no children semantics apply. + if (readDetail(backup, NASBackupChainKeys.CHAIN_ID) == null) { + return deleteBackupFileAndRow(backup, backupRepository, host); + } + + // Snapshot-style cascade: defer the on-NAS rm + DB row while there are live children, + // mark this backup as delete-pending, and let the leaf's deletion sweep it up later. + // See DefaultSnapshotStrategy#deleteSnapshotChain for the same pattern on incremental + // snapshots. forced=true means the caller wants the entire subtree gone right now. + if (forced) { + return cascadeDeleteSubtree(backup, backupRepository, host); + } + + List liveChildren = findLiveChildren(backup); + if (!liveChildren.isEmpty()) { + markDeletePending(backup); + LOG.debug("Backup {} has {} live child backup(s); marking as delete-pending. The on-NAS file " + + "and DB row will be removed once the last descendant is gone, or pass forced=true.", + backup.getUuid(), liveChildren.size()); + return true; + } + + // No live children — physically delete this backup, then walk up the chain and + // collect any ancestors that were left in delete-pending state. + return deleteLeafBackupAndSweepPendingAncestors(backup, backupRepository, host); + } + /** + * The single physical-delete step: rm the on-NAS directory, then remove the DB row. + * Returns {@code false} (and leaves both intact) if the agent reports failure, so the + * caller's recursion stops cleanly. + */ + private boolean deleteBackupFileAndRow(Backup backup, BackupRepository repo, Host host) { + DeleteBackupCommand command = new DeleteBackupCommand(backup.getExternalId(), repo.getType(), + repo.getAddress(), repo.getMountOptions()); BackupAnswer answer; try { answer = (BackupAnswer) agentManager.send(host.getId(), command); @@ -506,13 +950,229 @@ public boolean deleteBackup(Backup backup, boolean forced) { } catch (OperationTimedoutException e) { throw new CloudRuntimeException("Operation to delete backup timed out, please try again"); } + if (answer == null || !answer.getResult()) { + logger.warn("Failed to delete backup file for {} ({}); leaving DB row intact", + backup.getUuid(), backup.getExternalId()); + return false; + } + // Capture the deleted backup's bitmap before the row (and its backup_details) are removed. + String deletedBitmap = readDetail(backup, NASBackupChainKeys.BITMAP_NAME); + backupDao.remove(backup.getId()); + // If this backup owned the bitmap the VM's active_checkpoint_id points to, that on-host QEMU + // dirty-bitmap is gone with it — clear active_checkpoint_id so the next backup starts a fresh + // full chain instead of anchoring an incremental on a deleted checkpoint (test: delete last backup). + if (deletedBitmap != null && deletedBitmap.equals(readVmActiveCheckpoint(backup.getVmId()))) { + clearVmActiveCheckpoint(backup.getVmId()); + } + // Exactly-once resource accounting: decrement at the single point a backup row + file are + // physically removed. This runs for the leaf and for every swept delete-pending ancestor, + // so a chain delete decrements once per actually-removed backup. The manager skips its own + // accounting for this provider (see handlesChainDeleteResourceAccounting()). + long size = backup.getSize() != null ? backup.getSize() : 0L; + resourceLimitMgr.decrementResourceCount(backup.getAccountId(), Resource.ResourceType.backup); + resourceLimitMgr.decrementResourceCount(backup.getAccountId(), Resource.ResourceType.backup_storage, size); + return true; + } - if (answer != null && answer.getResult()) { - return backupDao.remove(backup.getId()); + /** + * Tombstone {@code backup} by moving it to {@link Backup.Status#Hidden}. Idempotent. + * The row stays in the DB so the chain GC can sweep it once its last descendant is deleted + * ({@code listByVmId} is status-agnostic), but it disappears from the user-facing list + * ({@link BackupManagerImpl#listBackups} filters Hidden) and all backup operations refuse it + * (they require {@code BackedUp}). Replaces the previous {@code nas.delete_pending} detail. + */ + private void markDeletePending(Backup backup) { + if (Backup.Status.Hidden.equals(backup.getStatus())) { + return; + } + BackupVO vo = backupDao.findById(backup.getId()); + if (vo != null) { + vo.setStatus(Backup.Status.Hidden); + backupDao.update(vo.getId(), vo); } + } - logger.debug("There was an error removing the backup with id {}", backup.getId()); - return false; + /** + * @return true if this backup is a tombstone (Hidden) awaiting chain sweep. + */ + private boolean isDeletePending(Backup backup) { + return Backup.Status.Hidden.equals(backup.getStatus()); + } + + /** + * Return the live (not delete-pending, not Removed) children of {@code parent} within the + * same chain. Equivalent to "incrementals whose parent_backup_id points at parent". + */ + private List findLiveChildren(Backup parent) { + String parentUuid = parent.getUuid(); + String chainId = readDetail(parent, NASBackupChainKeys.CHAIN_ID); + if (parentUuid == null || chainId == null) { + return Collections.emptyList(); + } + List children = new ArrayList<>(); + for (Backup b : backupDao.listByVmId(null, parent.getVmId())) { + if (b.getId() == parent.getId()) { + continue; + } + if (!chainId.equals(readDetail(b, NASBackupChainKeys.CHAIN_ID))) { + continue; + } + if (!parentUuid.equals(readDetail(b, NASBackupChainKeys.PARENT_BACKUP_ID))) { + continue; + } + if (isDeletePending(b)) { + // Tombstoned children don't keep us alive — they're already on the way out. + continue; + } + children.add(b); + } + return children; + } + + /** + * Look up this backup's immediate parent in the chain (by {@code PARENT_BACKUP_ID}). + * Returns {@code null} if this is the full (no parent) or the parent row is gone. + * + *

Prefer {@link #getChainOrderedLeafToRoot(Backup)} when walking the whole chain — + * this method hits the DB on each call and is O(N²) when used in a loop. + */ + private Backup findChainParent(Backup backup) { + String parentUuid = readDetail(backup, NASBackupChainKeys.PARENT_BACKUP_ID); + if (parentUuid == null || parentUuid.isEmpty()) { + return null; + } + for (Backup b : backupDao.listByVmId(null, backup.getVmId())) { + if (parentUuid.equals(b.getUuid())) { + return b; + } + } + return null; + } + + /** + * Return the chain containing {@code member}, ordered leaf-first + * (highest {@code CHAIN_POSITION} → root). + * + *

Materialises the chain via a single {@link BackupDao#listByVmId} call. Callers that + * previously walked the chain by repeatedly calling {@link #findChainParent} were doing + * O(N) {@code listByVmId} calls (one per ancestor); this collapses that to one. + * + *

If {@code member} has no {@code CHAIN_ID} metadata it is returned as a one-element + * list (it is its own degenerate chain). + */ + private List getChainOrderedLeafToRoot(Backup member) { + String chainId = readDetail(member, NASBackupChainKeys.CHAIN_ID); + if (chainId == null) { + return Collections.singletonList(member); + } + List chain = new ArrayList<>(); + for (Backup b : backupDao.listByVmId(null, member.getVmId())) { + if (chainId.equals(readDetail(b, NASBackupChainKeys.CHAIN_ID))) { + chain.add(b); + } + } + // Descending CHAIN_POSITION = leaf-first (highest position = furthest from root). + chain.sort((a, b) -> Integer.compare(chainPosition(b), chainPosition(a))); + return chain; + } + + /** + * Physically delete the leaf {@code backup}, then walk up the chain while each ancestor + * is in delete-pending state. Mirrors the snapshot subsystem pattern: once a leaf is + * gone, garbage-collect any tombstoned parents. + * + *

Caller must guarantee {@code backup} is a leaf (no live children). Each tombstoned + * ancestor is by definition childless once its sole child is deleted here, so no extra + * live-children check is needed inside the loop. + */ + private boolean deleteLeafBackupAndSweepPendingAncestors(Backup backup, BackupRepository repo, Host host) { + // Snapshot the chain BEFORE the leaf delete — deleteBackupFileAndRow removes the row, + // after which the in-memory list still resolves but the DB no longer would. + List chain = getChainOrderedLeafToRoot(backup); + if (!deleteBackupFileAndRow(backup, repo, host)) { + return false; + } + // Walk the snapshot from leaf+1 upward, deleting tombstoned ancestors until a live + // one is reached or the root is past. + int leafIdx = indexOfBackupById(chain, backup.getId()); + if (leafIdx < 0) { + // Leaf wasn't in its own CHAIN_ID list — degenerate case, nothing more to sweep. + return true; + } + for (int i = leafIdx + 1; i < chain.size(); i++) { + Backup ancestor = chain.get(i); + if (!isDeletePending(ancestor)) { + break; + } + if (!deleteBackupFileAndRow(ancestor, repo, host)) { + // Stop the sweep; the rest of the tombstoned chain will be collected on a + // future delete that re-runs the sweep. + return true; + } + } + return true; + } + + /** + * Forced delete of {@code root}'s entire chain, leaf-first. NAS backups form a linear + * chain (full → inc → inc → …), not a tree, so we just walk the ordered chain and + * delete each member without re-querying parents. + */ + private boolean cascadeDeleteSubtree(Backup root, BackupRepository repo, Host host) { + for (Backup b : getChainOrderedLeafToRoot(root)) { + if (!deleteBackupFileAndRow(b, repo, host)) { + return false; + } + } + return true; + } + + private static int indexOfBackupById(List chain, long id) { + for (int i = 0; i < chain.size(); i++) { + if (chain.get(i).getId() == id) { + return i; + } + } + return -1; + } + + /** + * Return the backup with the highest {@code CHAIN_POSITION} sharing {@code root}'s + * {@code CHAIN_ID}. Returns {@code root} if it has no chain metadata or is itself the tail. + */ + private Backup findChainTail(Backup root) { + String chainId = readDetail(root, NASBackupChainKeys.CHAIN_ID); + if (chainId == null) { + return root; + } + Backup tail = root; + int tailPos = chainPosition(root); + for (Backup b : backupDao.listByVmId(null, root.getVmId())) { + if (b.getId() == root.getId()) { + continue; + } + if (!chainId.equals(readDetail(b, NASBackupChainKeys.CHAIN_ID))) { + continue; + } + int pos = chainPosition(b); + if (pos > tailPos) { + tail = b; + tailPos = pos; + } + } + return tail; + } + + private int chainPosition(Backup b) { + String s = readDetail(b, NASBackupChainKeys.CHAIN_POSITION); + if (s == null) { + return Integer.MAX_VALUE; // no metadata => sort to end + } + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return Integer.MAX_VALUE; + } } public void syncBackupMetrics(Long zoneId) { @@ -543,6 +1203,11 @@ public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backup @Override public boolean removeVMFromBackupOffering(VirtualMachine vm) { + // Clear the VM's active checkpoint so any future re-assignment to a backup offering + // starts a fresh chain. Without this, a detach-volume + attach-different-volume cycle + // while the offering is unassigned would lead to the next backup trying to rebase + // onto a stale parent (different volume identity, same VM id). + clearVmActiveCheckpoint(vm.getId()); return true; } @@ -629,7 +1294,9 @@ public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) { @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[]{ - NASBackupRestoreMountTimeout + NASBackupRestoreMountTimeout, + NASBackupFullEvery, + NASBackupIncrementalEnabled }; } diff --git a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java index a512292cd28f..3ba7dbad0416 100644 --- a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java +++ b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java @@ -28,6 +28,7 @@ import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -38,22 +39,35 @@ import com.cloud.agent.AgentManager; import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.OperationTimedoutException; +import com.cloud.configuration.Resource; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.resource.ResourceManager; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.ScopeType; +import com.cloud.storage.Storage; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.ResourceLimitService; import com.cloud.utils.Pair; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; + +import com.google.gson.Gson; import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupRepositoryDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; @@ -96,6 +110,21 @@ public class NASBackupProviderTest { @Mock private VMSnapshotDao vmSnapshotDaoMock; + @Mock + private BackupDetailsDao backupDetailsDao; + + @Mock + private VMInstanceDetailsDao vmInstanceDetailsDao; + + @Mock + private DiskOfferingDao diskOfferingDao; + + @Mock + private DataStoreManager dataStoreMgr; + + @Mock + private ResourceLimitService resourceLimitMgr; + @Test public void testDeleteBackup() throws OperationTimedoutException, AgentUnavailableException { Long hostId = 1L; @@ -353,4 +382,427 @@ public void testGetVMHypervisorHostFallbackToZoneWideKVMHost() { Mockito.verify(hostDao).findHypervisorHostInCluster(clusterId); Mockito.verify(resourceManager).findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, zoneId); } + + // -- nas.backup.incremental.enabled master switch ------------------------------------ + + /** + * When the operator sets nas.backup.incremental.enabled=false at the zone level, every + * backup must be a fresh full anchor, regardless of VM state or nas.backup.full.every. + * This is a single toggle the + * operator can flip without having to count remaining backups in a chain. + */ + @Test + public void decideChainReturnsLegacyFullWhenIncrementalDisabled() { + Long zoneId = 1L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.lenient().when(vm.getDataCenterId()).thenReturn(zoneId); + + // Stub the master switch to false. ConfigKey.valueIn delegates to the framework's + // ConfigDepot at runtime; for the unit test we override the in-memory value via the + // ConfigKey's local override (set by ReflectionTestUtils on the spy provider). + ReflectionTestUtils.setField(nasBackupProvider, "NASBackupIncrementalEnabled", + new org.apache.cloudstack.framework.config.ConfigKey<>("Advanced", Boolean.class, + "nas.backup.incremental.enabled", "false", + "test override — disabled", true, + org.apache.cloudstack.framework.config.ConfigKey.Scope.Zone)); + + NASBackupProvider.ChainDecision decision = nasBackupProvider.decideChain(vm); + Assert.assertNotNull(decision); + Assert.assertEquals(NASBackupChainKeys.TYPE_LEGACY_FULL, decision.mode); + Assert.assertNull("legacy-full must not carry a bitmap", decision.bitmapNew); + Assert.assertNull(decision.bitmapParent); + Assert.assertNull("legacy-full must not start a chain", decision.chainId); + Assert.assertEquals(0, decision.chainPosition); + } + + // -- decideChain anchored on VM's active_checkpoint_id ------------------------------- + + /** + * No active_checkpoint_id on the VM (post-restore, first-ever backup, or detail purged) => + * decideChain must return a fresh full. Relying on the last backup taken as the parent + * breaks after a restore, so the decision is anchored on the active checkpoint instead. + */ + @Test + public void decideChainReturnsFullWhenVmHasNoActiveCheckpoint() { + Long zoneId = 1L; + Long vmId = 42L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getDataCenterId()).thenReturn(zoneId); + Mockito.when(vm.getState()).thenReturn(VMInstanceVO.State.Running); + + // Master switch defaults to false (opt-in by zone) — explicitly enable it for this + // test so we exercise the "no active_checkpoint_id" branch rather than short-circuit + // at the master-switch gate. + ReflectionTestUtils.setField(nasBackupProvider, "NASBackupIncrementalEnabled", + new org.apache.cloudstack.framework.config.ConfigKey<>("Advanced", Boolean.class, + "nas.backup.incremental.enabled", "true", + "test override — enabled", true, + org.apache.cloudstack.framework.config.ConfigKey.Scope.Zone)); + + Mockito.when(vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID)).thenReturn(null); + + NASBackupProvider.ChainDecision decision = nasBackupProvider.decideChain(vm); + Assert.assertNotNull(decision); + Assert.assertEquals(NASBackupChainKeys.TYPE_FULL, decision.mode); + Assert.assertNull(decision.bitmapParent); + Assert.assertEquals(0, decision.chainPosition); + } + + // -- incremental storage-capability guard (Ceph-RBD / Linstor stay on legacy full) ---- + + /** + * Incremental checkpoints are only possible on file-based qcow2 storage. A VM whose every + * volume sits on NFS / HOST-scope local / SharedMountPoint is checkpoint-capable. + */ + @Test + public void allVolumesOnCheckpointCapableStorageTrueForNfsHostAndSharedMount() { + Long vmId = 55L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + + VolumeVO nfsVol = mock(VolumeVO.class); + Mockito.when(nfsVol.getPoolId()).thenReturn(1L); + VolumeVO hostVol = mock(VolumeVO.class); + Mockito.when(hostVol.getPoolId()).thenReturn(2L); + VolumeVO smpVol = mock(VolumeVO.class); + Mockito.when(smpVol.getPoolId()).thenReturn(3L); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(List.of(nfsVol, hostVol, smpVol)); + + StoragePoolVO nfs = mock(StoragePoolVO.class); + Mockito.when(nfs.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + StoragePoolVO host = mock(StoragePoolVO.class); + Mockito.when(host.getScope()).thenReturn(ScopeType.HOST); + StoragePoolVO smp = mock(StoragePoolVO.class); + Mockito.when(smp.getPoolType()).thenReturn(Storage.StoragePoolType.SharedMountPoint); + Mockito.when(storagePoolDao.findById(1L)).thenReturn(nfs); + Mockito.when(storagePoolDao.findById(2L)).thenReturn(host); + Mockito.when(storagePoolDao.findById(3L)).thenReturn(smp); + + Assert.assertTrue(nasBackupProvider.allVolumesOnCheckpointCapableStorage(vm)); + } + + /** + * A single volume on Ceph-RBD (or any pool that cannot carry a per-disk checkpoint) forces + * the whole VM onto the legacy full-only path — avoids regressing RBD/Linstor storages. + */ + @Test + public void allVolumesOnCheckpointCapableStorageFalseWhenAnyVolumeOnRbd() { + Long vmId = 56L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + + VolumeVO nfsVol = mock(VolumeVO.class); + Mockito.when(nfsVol.getPoolId()).thenReturn(1L); + VolumeVO rbdVol = mock(VolumeVO.class); + Mockito.when(rbdVol.getPoolId()).thenReturn(9L); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(List.of(nfsVol, rbdVol)); + + StoragePoolVO nfs = mock(StoragePoolVO.class); + Mockito.when(nfs.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + StoragePoolVO rbd = mock(StoragePoolVO.class); + Mockito.when(rbd.getPoolType()).thenReturn(Storage.StoragePoolType.RBD); + Mockito.when(storagePoolDao.findById(1L)).thenReturn(nfs); + Mockito.when(storagePoolDao.findById(9L)).thenReturn(rbd); + + Assert.assertFalse(nasBackupProvider.allVolumesOnCheckpointCapableStorage(vm)); + } + + /** A volume whose storage pool can no longer be resolved is treated as incapable (safe). */ + @Test + public void allVolumesOnCheckpointCapableStorageFalseWhenPoolUnresolvable() { + Long vmId = 57L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + VolumeVO vol = mock(VolumeVO.class); + Mockito.when(vol.getPoolId()).thenReturn(1L); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(List.of(vol)); + Mockito.when(storagePoolDao.findById(1L)).thenReturn(null); + + Assert.assertFalse(nasBackupProvider.allVolumesOnCheckpointCapableStorage(vm)); + } + + // -- restore clears active_checkpoint_id --------------------------------------------- + + /** + * After a successful restoreVMFromBackup, decideChain on the next backup must produce + * a full. We verify this end-to-end by checking that vmInstanceDetailsDao.removeDetail + * is called with the active_checkpoint_id key. + */ + @Test + public void restoreClearsActiveCheckpointDetail() throws AgentUnavailableException, OperationTimedoutException { + Long vmId = 7L; + Long hostId = 8L; + Long backupOfferingId = 9L; + + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getLastHostId()).thenReturn(hostId); + Mockito.when(vm.getRemoved()).thenReturn(null); + Mockito.when(vm.getName()).thenReturn("vm7"); + + HostVO host = mock(HostVO.class); + Mockito.when(host.getStatus()).thenReturn(Status.Up); + Mockito.when(host.getId()).thenReturn(hostId); + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + + BackupVO backup = new BackupVO(); + backup.setVmId(vmId); + backup.setBackupOfferingId(backupOfferingId); + backup.setExternalId("i-2-7-VM/2026.05.16.10.00.00"); + ReflectionTestUtils.setField(backup, "id", 100L); + // backedUpVolumes defaults to null => BackupVO.getBackedUpVolumes returns emptyList(). + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(backupOfferingId)).thenReturn(repo); + + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(Collections.emptyList()); + + BackupAnswer answer = mock(BackupAnswer.class); + Mockito.when(answer.getResult()).thenReturn(true); + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(RestoreBackupCommand.class))).thenReturn(answer); + + // Pre-existing checkpoint detail so removeDetail has something to "clear". + VMInstanceDetailVO existing = mock(VMInstanceDetailVO.class); + Mockito.when(existing.getValue()).thenReturn("backup-1715000000"); + Mockito.when(vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID)).thenReturn(existing); + + boolean ok = nasBackupProvider.restoreVMFromBackup(vm, backup); + Assert.assertTrue(ok); + Mockito.verify(vmInstanceDetailsDao).removeDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + } + + /** + * Single-volume restore (restoreBackedUpVolume) must also clear the target VM's + * active_checkpoint_id, so the next backup of that VM is a fresh full — the restored + * volume's image carries no QEMU bitmap. + */ + @Test + public void restoreBackedUpVolumeClearsTargetVmActiveCheckpoint() + throws AgentUnavailableException, OperationTimedoutException { + Long targetVmId = 42L; + Long backupOfferingId = 9L; + String targetVmName = "i-2-42-VM"; + String volUuid = "vol-uuid-1"; + String hostIp = "10.0.0.5"; + String dsUuid = "ds-uuid-1"; + + VolumeVO srcVolume = mock(VolumeVO.class); + Mockito.when(srcVolume.getUuid()).thenReturn(volUuid); + Mockito.when(srcVolume.getName()).thenReturn("data1"); + Mockito.when(volumeDao.findByUuid(volUuid)).thenReturn(srcVolume); + + DiskOfferingVO diskOffering = mock(DiskOfferingVO.class); + Mockito.when(diskOffering.getId()).thenReturn(5L); + Mockito.when(diskOffering.getProvisioningType()).thenReturn(Storage.ProvisioningType.THIN); + Mockito.when(diskOfferingDao.findByUuid(Mockito.anyString())).thenReturn(diskOffering); + + StoragePoolVO pool = mock(StoragePoolVO.class); + Mockito.when(pool.getId()).thenReturn(11L); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(storagePoolDao.findByUuid(dsUuid)).thenReturn(pool); + + HostVO host = mock(HostVO.class); + Mockito.when(host.getId()).thenReturn(8L); + Mockito.when(hostDao.findByIp(hostIp)).thenReturn(host); + + Backup.VolumeInfo backedUp = new Backup.VolumeInfo(volUuid, "i-2-99-VM/2026/data1.qcow2", + Volume.Type.DATADISK, 1024L, 1L, "disk-offering-uuid", null, null); + + BackupVO backup = new BackupVO(); + backup.setVmId(99L); + backup.setBackupOfferingId(backupOfferingId); + backup.setExternalId("i-2-99-VM/2026.06.22.10.00.00"); + backup.setSize(1024L); + backup.setBackedUpVolumes(new Gson().toJson(Collections.singletonList(backedUp))); + ReflectionTestUtils.setField(backup, "id", 200L); + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(backupOfferingId)).thenReturn(repo); + + BackupAnswer answer = mock(BackupAnswer.class); + Mockito.when(answer.getResult()).thenReturn(true); + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(RestoreBackupCommand.class))).thenReturn(answer); + + VMInstanceVO targetVm = mock(VMInstanceVO.class); + Mockito.when(targetVm.getId()).thenReturn(targetVmId); + Mockito.when(vmInstanceDao.findVMByInstanceName(targetVmName)).thenReturn(targetVm); + + VMInstanceDetailVO existing = mock(VMInstanceDetailVO.class); + Mockito.when(existing.getValue()).thenReturn("backup-1718000000"); + Mockito.when(vmInstanceDetailsDao.findDetail(targetVmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID)).thenReturn(existing); + + Pair result = nasBackupProvider.restoreBackedUpVolume( + backup, backedUp, hostIp, dsUuid, new Pair<>(targetVmName, VirtualMachine.State.Stopped)); + + Assert.assertTrue(result.first()); + Mockito.verify(vmInstanceDetailsDao).removeDetail(targetVmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + } + + // -- delete-pending cascade ---------------------------------------------------------- + + /** + * Deleting an incremental that has a live child must mark the incremental as + * delete-pending in backup_details and NOT touch the on-NAS file or the backups row. + * A parent with live children is soft-deleted (delete-pending) rather than removed. + */ + @Test + public void deleteWithLiveChildMarksDeletePendingAndPreservesFile() + throws AgentUnavailableException, OperationTimedoutException { + Long zoneId = 1L; + Long vmId = 2L; + Long hostId = 3L; + Long offeringId = 4L; + + BackupVO parent = new BackupVO(); + parent.setVmId(vmId); + parent.setBackupOfferingId(offeringId); + parent.setExternalId("i-2-2-VM/2026.05.10.10.00.00"); + parent.setZoneId(zoneId); + ReflectionTestUtils.setField(parent, "id", 50L); + ReflectionTestUtils.setField(parent, "uuid", "parent-uuid"); + + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getLastHostId()).thenReturn(hostId); + HostVO host = mock(HostVO.class); + Mockito.when(host.getStatus()).thenReturn(Status.Up); + // Note: host.getId() is intentionally not stubbed — the live-child path never + // contacts the agent (verified below), so the stub would be unnecessary. + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(offeringId)).thenReturn(repo); + Mockito.when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); + + // CHAIN_ID on the parent => not the no-chain fast path. + BackupDetailVO chainIdDetail = new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_ID, "chain-1", true); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_ID)).thenReturn(chainIdDetail); + + // A live child references parent-uuid via PARENT_BACKUP_ID. + BackupVO child = new BackupVO(); + child.setVmId(vmId); + child.setBackupOfferingId(offeringId); + child.setExternalId("i-2-2-VM/2026.05.10.10.30.00"); + child.setZoneId(zoneId); + child.setStatus(Backup.Status.BackedUp); + ReflectionTestUtils.setField(child, "id", 51L); + ReflectionTestUtils.setField(child, "uuid", "child-uuid"); + + BackupDetailVO childChainId = new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_ID, "chain-1", true); + BackupDetailVO childParent = new BackupDetailVO(51L, NASBackupChainKeys.PARENT_BACKUP_ID, "parent-uuid", true); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_ID)).thenReturn(childChainId); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.PARENT_BACKUP_ID)).thenReturn(childParent); + + Mockito.when(backupDao.listByVmId(null, vmId)).thenReturn(List.of(parent, child)); + // markDeletePending loads the row to flip its status to Hidden. + Mockito.when(backupDao.findById(50L)).thenReturn(parent); + + boolean result = nasBackupProvider.deleteBackup(parent, false); + Assert.assertTrue(result); + + // No agent traffic — the on-NAS file must be preserved while children are alive. + Mockito.verify(agentManager, Mockito.never()).send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class)); + // No DB row removal — the row is the tombstone marker. + Mockito.verify(backupDao, Mockito.never()).remove(50L); + // A tombstoned backup is NOT decremented — its space is still occupied until swept. + Mockito.verify(resourceLimitMgr, Mockito.never()).decrementResourceCount(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.backup)); + // The tombstoned backup is moved to Status.Hidden (replaces the old DELETE_PENDING detail). + ArgumentCaptor captor = ArgumentCaptor.forClass(BackupVO.class); + Mockito.verify(backupDao).update(Mockito.eq(50L), captor.capture()); + Assert.assertEquals(Backup.Status.Hidden, captor.getValue().getStatus()); + Mockito.verify(backupDetailsDao, Mockito.never()).persist(Mockito.any(BackupDetailVO.class)); + } + + /** + * Deleting a leaf incremental whose parent is delete-pending must (a) delete the leaf and + * then (b) sweep up the tombstoned parent. Mirrors DefaultSnapshotStrategy's + * "delete leaf, then walk up while parent is destroying-and-childless". + */ + @Test + public void deletingLeafSweepsUpDeletePendingParent() + throws AgentUnavailableException, OperationTimedoutException { + Long zoneId = 1L; + Long vmId = 2L; + Long hostId = 3L; + Long offeringId = 4L; + + BackupVO leaf = new BackupVO(); + leaf.setVmId(vmId); + leaf.setBackupOfferingId(offeringId); + leaf.setExternalId("i-2-2-VM/2026.05.10.11.00.00"); + leaf.setZoneId(zoneId); + ReflectionTestUtils.setField(leaf, "id", 51L); + ReflectionTestUtils.setField(leaf, "uuid", "leaf-uuid"); + + BackupVO parent = new BackupVO(); + parent.setVmId(vmId); + parent.setBackupOfferingId(offeringId); + parent.setExternalId("i-2-2-VM/2026.05.10.10.30.00"); + parent.setZoneId(zoneId); + ReflectionTestUtils.setField(parent, "id", 50L); + ReflectionTestUtils.setField(parent, "uuid", "parent-uuid"); + + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getLastHostId()).thenReturn(hostId); + HostVO host = mock(HostVO.class); + Mockito.when(host.getStatus()).thenReturn(Status.Up); + Mockito.when(host.getId()).thenReturn(hostId); + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(offeringId)).thenReturn(repo); + Mockito.when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); + + // Leaf details. CHAIN_POSITION=1 puts the leaf after the full anchor in the + // ordered chain — getChainOrderedLeafToRoot sorts by CHAIN_POSITION descending. + BackupDetailVO leafChainId = new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_ID, "chain-1", true); + BackupDetailVO leafChainPos = new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_POSITION, "1", true); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_ID)).thenReturn(leafChainId); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_POSITION)).thenReturn(leafChainPos); + + // Parent is the tombstoned full anchor (CHAIN_POSITION=0). + BackupDetailVO parentChainId = new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_ID, "chain-1", true); + BackupDetailVO parentChainPos = new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_POSITION, "0", true); + // The parent is the tombstone — now represented by Status.Hidden (was the DELETE_PENDING detail). + parent.setStatus(Backup.Status.Hidden); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_ID)).thenReturn(parentChainId); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_POSITION)).thenReturn(parentChainPos); + // Parent has no parent of its own (it's the full anchor). + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.PARENT_BACKUP_ID)).thenReturn(null); + + // listByVmId is called once now (chain snapshot taken before the leaf delete). + // We still use a mutable list + remove() answer so the DAO contract is realistic. + java.util.List liveBackups = new java.util.ArrayList<>(List.of(parent, leaf)); + Mockito.when(backupDao.listByVmId(null, vmId)).thenAnswer(inv -> new java.util.ArrayList<>(liveBackups)); + + // Agent acknowledges every delete. + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class))) + .thenReturn(new BackupAnswer(new DeleteBackupCommand(null, null, null, null), true, "ok")); + // backupDao.remove(id) drops the corresponding row from the live list so the next + // listByVmId call reflects post-delete state — mirrors the real DAO contract. + Mockito.when(backupDao.remove(Mockito.anyLong())).thenAnswer(inv -> { + Long id = inv.getArgument(0); + liveBackups.removeIf(b -> b.getId() == id); + return true; + }); + + boolean result = nasBackupProvider.deleteBackup(leaf, false); + Assert.assertTrue(result); + + // Both backups must be physically deleted (leaf first, then tombstoned parent). + Mockito.verify(agentManager, Mockito.times(2)) + .send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class)); + Mockito.verify(backupDao).remove(51L); + Mockito.verify(backupDao).remove(50L); + // Exactly-once resource accounting: decremented for BOTH physically-removed backups + // (leaf + swept ancestor), not just one. + Mockito.verify(resourceLimitMgr, Mockito.times(2)) + .decrementResourceCount(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.backup)); + Mockito.verify(resourceLimitMgr, Mockito.times(2)) + .decrementResourceCount(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.backup_storage), Mockito.any()); + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java index 22dbfbdd67a2..cc2a0868fe17 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java @@ -60,6 +60,15 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper/dev/null | grep -q '\"backing-filename\"'"; private String getVolumeUuidFromPath(String volumePath, PrimaryDataStoreTO volumePool) { if (Storage.StoragePoolType.Linstor.equals(volumePool.getPoolType())) { @@ -270,10 +279,27 @@ private boolean replaceVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, Pr return replaceBlockDeviceWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, createTargetVolume, size); } + // For NAS-backed incremental backups, the source qcow2 has a backing-file + // reference to its parent (set by nasbackup.sh's qemu-img rebase). A plain + // rsync would copy only the differential blocks, leaving a volume that + // depends on a backing file the primary storage doesn't have. Flatten the + // chain via qemu-img convert, which follows the backing-file links and + // produces a single self-contained qcow2. + if (hasBackingChain(backupPath)) { + int flattenExit = Script.runSimpleBashScriptForExitValue( + String.format(QEMU_IMG_FLATTEN_COMMAND, backupPath, volumePath), timeout, false); + return flattenExit == 0; + } + int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath), timeout, false); return exitValue == 0; } + private boolean hasBackingChain(String qcow2Path) { + return Script.runSimpleBashScriptForExitValue( + String.format(QEMU_IMG_HAS_BACKING_COMMAND, qcow2Path)) == 0; + } + private boolean replaceBlockDeviceWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout, boolean createTargetVolume, Long size) { KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid()); QemuImg qemu; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java index 42953aa9f835..106fe31a0f18 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java @@ -42,6 +42,16 @@ @ResourceWrapper(handles = TakeBackupCommand.class) public class LibvirtTakeBackupCommandWrapper extends CommandWrapper { private static final Integer EXIT_CLEANUP_FAILED = 20; + // nasbackup.sh prints this on stdout when it could not proceed as an incremental and + // completed a full backup instead; the orchestrator then records the backup as a full. + private static final String INCREMENTAL_FALLBACK_MARKER = "INCREMENTAL_FALLBACK=true"; + + private static final String MODE_FULL = "full"; + private static final String MODE_INCREMENTAL = "incremental"; + // Incremental feature disabled: plain full backup with no QEMU bitmap/checkpoint and no + // chain metadata. Matches nasbackup.sh's "legacy-full" mode (make_checkpoint=0). + private static final String MODE_LEGACY_FULL = "legacy-full"; + @Override public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvirtComputingResource) { final String vmName = command.getVmName(); @@ -54,6 +64,13 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); int timeout = command.getWait() > 0 ? command.getWait() * 1000 : libvirtComputingResource.getCmdsTimeout(); + // Pre-validate incremental args here rather than relying on the script to error out. + // Keeps the script agnostic to caller policy (it just does what it's told). + String validationError = validateBackupArgs(command); + if (validationError != null) { + return new BackupAnswer(command, false, validationError); + } + List diskPaths = new ArrayList<>(); if (Objects.nonNull(volumePaths)) { for (int idx = 0; idx < volumePaths.size(); idx++) { @@ -69,8 +86,63 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir } } - List commands = new ArrayList<>(); - commands.add(new String[]{ + Pair result = runBackupScript(libvirtComputingResource, command, vmName, backupRepoType, backupRepoAddress, + mountOptions, backupPath, diskPaths, command.getMode(), + command.getBitmapNew(), command.getBitmapParent(), command.getParentPaths(), timeout); + + if (result.first() != 0) { + logger.debug("Failed to take VM backup: " + result.second()); + BackupAnswer answer = new BackupAnswer(command, false, result.second().trim()); + if (EXIT_CLEANUP_FAILED.equals(result.first())) { + logger.debug("Backup cleanup failed"); + answer.setNeedsCleanup(true); + } + return answer; + } + + // The script self-heals to a full backup when an incremental can't proceed (e.g. the + // parent checkpoint can't be re-registered) and signals it with INCREMENTAL_FALLBACK + // on stdout. Detect it, then strip the marker line before parsing the backup size. + String rawStdout = result.second(); + boolean incrementalFallback = rawStdout.contains(INCREMENTAL_FALLBACK_MARKER); + String stdout = stripMarkerLines(rawStdout).trim(); + long backupSize = parseBackupSize(stdout, diskPaths); + + BackupAnswer answer = new BackupAnswer(command, true, stdout); + answer.setSize(backupSize); + // A successful run always created command.getBitmapNew() (full and incremental both do; + // it is null for legacy-full, which the orchestrator treats as "no bitmap"). + answer.setBitmapCreated(command.getBitmapNew()); + answer.setIncrementalFallback(incrementalFallback); + return answer; + } + + /** Remove nasbackup.sh's stdout signalling marker lines so they don't pollute size parsing. */ + private String stripMarkerLines(String stdout) { + if (stdout == null || stdout.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (String line : stdout.split("\n", -1)) { + if (line.contains(INCREMENTAL_FALLBACK_MARKER)) { + continue; + } + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append(line); + } + return sb.toString(); + } + + /** + * Run nasbackup.sh once with the given args. Returns the exit code + captured stdout. + */ + private Pair runBackupScript(LibvirtComputingResource libvirtComputingResource, + TakeBackupCommand command, String vmName, String backupRepoType, String backupRepoAddress, + String mountOptions, String backupPath, List diskPaths, String mode, + String bitmapNew, String bitmapParent, List parentPaths, int timeout) { + List argv = new ArrayList<>(Arrays.asList( libvirtComputingResource.getNasBackupPath(), "-o", "backup", "-v", vmName, @@ -80,35 +152,79 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir "-p", backupPath, "-q", command.getQuiesce() != null && command.getQuiesce() ? "true" : "false", "-d", diskPaths.isEmpty() ? "" : String.join(",", diskPaths) - }); + )); + if (mode != null && !mode.isEmpty()) { + argv.add("-M"); + argv.add(mode); + } + if (bitmapNew != null && !bitmapNew.isEmpty()) { + argv.add("--bitmap-new"); + argv.add(bitmapNew); + } + if (bitmapParent != null && !bitmapParent.isEmpty()) { + argv.add("--bitmap-parent"); + argv.add(bitmapParent); + } + if (parentPaths != null && !parentPaths.isEmpty()) { + argv.add("--parent-paths"); + argv.add(String.join(",", parentPaths)); + } - Pair result = Script.executePipedCommands(commands, timeout); + List commands = new ArrayList<>(); + commands.add(argv.toArray(new String[0])); + return Script.executePipedCommands(commands, timeout); + } - if (result.first() != 0) { - logger.debug("Failed to take VM backup: " + result.second()); - BackupAnswer answer = new BackupAnswer(command, false, result.second().trim()); - if (result.first() == EXIT_CLEANUP_FAILED) { - logger.debug("Backup cleanup failed"); - answer.setNeedsCleanup(true); + /** + * Return a human-readable validation error string, or {@code null} if the command's + * incremental-backup args are internally consistent. + */ + private String validateBackupArgs(TakeBackupCommand command) { + String mode = command.getMode(); + if (mode == null || mode.isEmpty()) { + return null; // legacy full-only — no extra args expected + } + if (MODE_INCREMENTAL.equals(mode)) { + if (command.getBitmapNew() == null || command.getBitmapNew().isEmpty()) { + return "incremental mode requires bitmapNew"; } - return answer; + if (command.getBitmapParent() == null || command.getBitmapParent().isEmpty()) { + return "incremental mode requires bitmapParent"; + } + if (command.getParentPaths() == null || command.getParentPaths().isEmpty()) { + return "incremental mode requires parentPaths"; + } + return null; + } + if (MODE_FULL.equals(mode)) { + if (command.getBitmapNew() == null || command.getBitmapNew().isEmpty()) { + return "full mode requires bitmapNew (the bitmap to create for the next incremental)"; + } + return null; + } + if (MODE_LEGACY_FULL.equals(mode)) { + return null; // feature-off full backup — no bitmap or chain args expected } + return "Unknown backup mode: " + mode; + } + /** + * Sum the per-disk size lines emitted by nasbackup.sh. Single-volume mode emits one + * line containing just the byte count; multi-volume mode emits one line per disk + * whose first whitespace-separated token is the byte count. + */ + private long parseBackupSize(String stdout, List diskPaths) { long backupSize = 0L; if (CollectionUtils.isNullOrEmpty(diskPaths)) { - List outputLines = Arrays.asList(result.second().trim().split("\n")); + List outputLines = Arrays.asList(stdout.split("\n")); if (!outputLines.isEmpty()) { backupSize = Long.parseLong(outputLines.get(outputLines.size() - 1).trim()); } } else { - String[] outputLines = result.second().trim().split("\n"); - for(String line : outputLines) { + for (String line : stdout.split("\n")) { backupSize = backupSize + Long.parseLong(line.split(" ")[0].trim()); } } - - BackupAnswer answer = new BackupAnswer(command, true, result.second().trim()); - answer.setSize(backupSize); - return answer; + return backupSize; } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java index ef6b5c08189d..fd8a3b02e0a0 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java @@ -407,6 +407,8 @@ public void testExecuteWithRsyncFailure() throws Exception { return 0; // File exists } else if (command.contains("qemu-img check")) { return 0; // File is valid + } else if (command.contains("qemu-img info") && command.contains("backing-filename")) { + return 1; // No backing chain — exercise the rsync path (full backups) } return 0; // Other commands success }); diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java index 1a8d9f7b59e2..287601d47d6b 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java @@ -1164,7 +1164,7 @@ private ManagedObjectReference getDestStoreMor(VirtualMachineMO vmMo) throws Exc @Override public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, String vmInternalName, Backup backup) throws Exception { logger.debug(String.format("Trying to import VM [vmInternalName: %s] from Backup [%s].", vmInternalName, - ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "backupType"))); + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "type"))); DatacenterMO dcMo = getDatacenterMO(zoneId); VirtualMachineMO vmToImport = dcMo.findVm(vmInternalName); if (vmToImport == null) { diff --git a/scripts/vm/hypervisor/kvm/nasbackup.sh b/scripts/vm/hypervisor/kvm/nasbackup.sh index b35908a433ed..d252b1f4e978 100755 --- a/scripts/vm/hypervisor/kvm/nasbackup.sh +++ b/scripts/vm/hypervisor/kvm/nasbackup.sh @@ -33,6 +33,15 @@ MOUNT_OPTS="" BACKUP_DIR="" DISK_PATHS="" QUIESCE="" +# Incremental backup parameters (all optional; legacy callers omit them) +MODE="" # "full" or "incremental"; empty => legacy full-only behavior (no checkpoint created) +BITMAP_NEW="" # Bitmap/checkpoint name to create with this backup (e.g. "backup-1711586400") +BITMAP_PARENT="" # For incremental: parent bitmap name to read changes since +PARENT_PATHS="" # For incremental: comma-separated list of parent backup file paths, + # one per VM volume in the same order as DISK_PATHS. Each new qcow2 + # is rebased onto its corresponding parent file. Required because + # data-disk backup files don't share the root volume's UUID, so + # each disk must be rebased onto its own parent. logFile="/var/log/cloudstack/agent/agent.log" UNMOUNT_TIMEOUT=60 EXIT_CLEANUP_FAILED=20 @@ -113,20 +122,117 @@ backup_running_vm() { mount_operation mkdir -p "$dest" || { echo "Failed to create backup directory $dest"; exit 1; } + # Determine effective mode for this run. + # Legacy callers (no -M argument) get the original full-only behavior with no checkpoint. + # The Java wrapper (LibvirtTakeBackupCommandWrapper) pre-validates required args before + # invoking the script; the case below is a defensive fallback for direct invocations. + local effective_mode="${MODE:-legacy-full}" + local make_checkpoint=0 + case "$effective_mode" in + incremental|full) + make_checkpoint=1 + ;; + legacy-full) + make_checkpoint=0 + ;; + *) + echo "Unknown mode: $effective_mode" + cleanup + exit 1 + ;; + esac + + # Incremental needs the parent checkpoint registered with libvirt. CloudStack rebuilds the + # domain XML on every VM start, wiping libvirt's checkpoint registry while the dirty bitmap + # persists on the qcow2, so a fresh checkpoint-create fails with "Bitmap already exists". + # Re-register the parent with --redefine (needs only a name + creationTime) via a minimal + # synthesized XML. If the parent bitmap is missing from the qcow2 (e.g. after a migration), + # fall back to a full backup instead of letting backup-begin fail below. + if [[ "$effective_mode" == "incremental" ]]; then + # The parent bitmap must be present on EVERY disk, not just one. A snapshot restore or partial + # migration can wipe it on some disks; require it on all by comparing the disk count to the + # number of disks that carry it. + disk_count=$(virsh -c qemu:///system domblklist "$VM" --details 2>/dev/null | awk '$2=="disk"{c++} END{print c+0}') + # Count per-device (one per inserted.file whose dirty-bitmaps holds the parent), mirroring + # getVmDiskPathHasFromCheckpointMap(): query-block lists a bitmap under multiple nodes, so raw + # name matches double-count. "|| echo 0" keeps a no-match from aborting under "set -eo pipefail" + # before the fallback runs. + bitmap_count=$(virsh -c qemu:///system qemu-monitor-command "$VM" '{"execute":"query-block"}' 2>/dev/null | python3 -c ' +import sys, json +target = sys.argv[1] +try: + data = json.load(sys.stdin) +except Exception: + print(0); sys.exit(0) +files = set() +for dev in data.get("return", []) or []: + inserted = dev.get("inserted") or {} + f = inserted.get("file") + if not f: + continue + if any((b or {}).get("name") == target for b in (inserted.get("dirty-bitmaps") or [])): + files.add(f) +print(len(files)) +' "$BITMAP_PARENT" 2>/dev/null || echo 0) + if [[ "$disk_count" -eq 0 || "$bitmap_count" -lt "$disk_count" ]]; then + log -e "incremental: parent bitmap $BITMAP_PARENT present on $bitmap_count/$disk_count disk(s) — falling back to full" + echo "INCREMENTAL_FALLBACK=true" + effective_mode="full" + fi + fi + + if [[ "$effective_mode" == "incremental" ]]; then + if ! virsh -c qemu:///system checkpoint-list "$VM" --name 2>/dev/null | grep -qx "$BITMAP_PARENT"; then + redefine_xml=$(mktemp) + printf '%s%s' \ + "$BITMAP_PARENT" "$(date +%s)" > "$redefine_xml" + if virsh -c qemu:///system checkpoint-create "$VM" --xmlfile "$redefine_xml" --redefine > /dev/null 2>&1; then + rm -f "$redefine_xml" # parent checkpoint re-registered; the incremental can proceed against it + else + rm -f "$redefine_xml" + # Parent checkpoint could not be re-registered — fall back to a full backup in place so + # the chain restarts cleanly instead of failing. Emit a stdout marker so the wrapper + # records this backup as a full (incrementalFallback=true). + log -e "incremental: parent checkpoint $BITMAP_PARENT could not be re-registered — falling back to full" + echo "INCREMENTAL_FALLBACK=true" + effective_mode="full" + fi + fi + fi + + # Build backup XML (and matching checkpoint XML when applicable). name="root" - echo "" > $dest/backup.xml + echo "" > $dest/backup.xml + if [[ "$effective_mode" == "incremental" ]]; then + echo "$BITMAP_PARENT" >> $dest/backup.xml + fi + echo "" >> $dest/backup.xml + if [[ $make_checkpoint -eq 1 ]]; then + echo "$BITMAP_NEW" > $dest/checkpoint.xml + fi while read -r disk fullpath; do if [[ "$fullpath" == /dev/drbd/by-res/* ]]; then volUuid=$(get_linstor_uuid_from_path "$fullpath") else volUuid="${fullpath##*/}" fi - echo "" >> $dest/backup.xml + if [[ "$effective_mode" == "incremental" ]]; then + # Incremental disk entry — no backupmode attr, libvirt picks it up from . + echo "" >> $dest/backup.xml + else + echo "" >> $dest/backup.xml + fi + if [[ $make_checkpoint -eq 1 ]]; then + echo "" >> $dest/checkpoint.xml + fi name="datadisk" done < <( virsh -c qemu:///system domblklist "$VM" --details 2>/dev/null | awk '$2=="disk"{print $3, $4}' ) echo "" >> $dest/backup.xml + if [[ $make_checkpoint -eq 1 ]]; then + echo "" >> $dest/checkpoint.xml + fi local thaw=0 if [[ ${QUIESCE} == "true" ]]; then @@ -135,14 +241,22 @@ backup_running_vm() { fi fi - # Start push backup + # Start push backup, atomically registering the new checkpoint when applicable. local backup_begin=0 - if virsh -c qemu:///system backup-begin --domain $VM --backupxml $dest/backup.xml 2>&1 > /dev/null; then - backup_begin=1; + if [[ $make_checkpoint -eq 1 ]]; then + # Order matters: redirect stdout to /dev/null first, then merge stderr into stdout. + # The reversed `2>&1 > /dev/null` form leaves stderr pointing at the original tty. + if virsh -c qemu:///system backup-begin --domain $VM --backupxml $dest/backup.xml --checkpointxml $dest/checkpoint.xml > /dev/null 2>&1; then + backup_begin=1; + fi + else + if virsh -c qemu:///system backup-begin --domain $VM --backupxml $dest/backup.xml > /dev/null 2>&1; then + backup_begin=1; + fi fi if [[ $thaw -eq 1 ]]; then - if ! response=$(virsh -c qemu:///system qemu-agent-command "$VM" '{"execute":"guest-fsfreeze-thaw"}' 2>&1 > /dev/null); then + if ! response=$(virsh -c qemu:///system qemu-agent-command "$VM" '{"execute":"guest-fsfreeze-thaw"}' 2>&1); then echo "Failed to thaw the filesystem for vm $VM: $response" cleanup exit 1 @@ -173,9 +287,47 @@ backup_running_vm() { sleep 5 done - # Use qemu-img convert to sparsify linstor backups which get bloated due to virsh backup-begin. + # Sparsify behavior: + # - For LINSTOR backups (existing): qemu-img convert sparsifies the bloated output. + # - For INCREMENTAL: rebase the resulting thin qcow2 onto its parent so the chain is self-describing + # (so a future restore can flatten without external chain metadata). name="root" + # PARENT_PATHS arrives as a comma-separated list, one entry per VM volume in the same + # order as DISK_PATHS. Split into a bash array so we can index by disk position. + local -a parent_paths_arr=() + if [[ "$effective_mode" == "incremental" && -n "$PARENT_PATHS" ]]; then + IFS=',' read -ra parent_paths_arr <<< "$PARENT_PATHS" + fi + local disk_idx=0 while read -r disk fullpath; do + if [[ "$effective_mode" == "incremental" ]]; then + volUuid="${fullpath##*/}" + # Pick this disk's specific parent file. Each volume's backup is named after its + # own UUID, so a single PARENT_PATH would wrongly rebase data disks onto the root + # parent. + if [[ $disk_idx -ge ${#parent_paths_arr[@]} ]]; then + echo "PARENT_PATHS list shorter than DISK_PATHS — missing parent for disk index $disk_idx" + cleanup + exit 1 + fi + local this_parent_rel="${parent_paths_arr[$disk_idx]}" + local parent_abs="$mount_point/$this_parent_rel" + if [[ ! -f "$parent_abs" ]]; then + echo "Parent backup file does not exist on NAS: $parent_abs" + cleanup + exit 1 + fi + local parent_rel + parent_rel=$(realpath --relative-to="$dest" "$parent_abs") + if ! qemu-img rebase -u -b "$parent_rel" -F qcow2 "$dest/$name.$volUuid.qcow2" >> "$logFile" 2> >(cat >&2); then + echo "qemu-img rebase failed for $dest/$name.$volUuid.qcow2 onto $parent_rel" + cleanup + exit 1 + fi + name="datadisk" + disk_idx=$((disk_idx + 1)) + continue + fi if [[ "$fullpath" != /dev/drbd/by-res/* ]]; then continue fi @@ -192,9 +344,43 @@ backup_running_vm() { virsh -c qemu:///system domblklist "$VM" --details 2>/dev/null | awk '$2=="disk"{print $3, $4}' ) - rm -f $dest/backup.xml + rm -f $dest/backup.xml $dest/checkpoint.xml sync + # Free the parent bitmap now that the incremental is written and rebased: its delta is captured + # here and BITMAP_NEW tracks changes going forward, so it only accrues metadata/IO cost over a + # long chain. Remove it per-disk with block-dirty-bitmap-remove (a clean free) rather than + # checkpoint-delete, which would merge its bits into BITMAP_NEW and re-copy backed-up regions. + # Best-effort: a failure here does not fail the backup, the bitmap is reclaimed on a later run. + if [[ "$effective_mode" == "incremental" && -n "$BITMAP_PARENT" ]]; then + while read -r node; do + [[ -z "$node" ]] && continue + if ! virsh -c qemu:///system qemu-monitor-command "$VM" \ + "{\"execute\":\"block-dirty-bitmap-remove\",\"arguments\":{\"node\":\"$node\",\"name\":\"$BITMAP_PARENT\"}}" \ + > /dev/null 2>>"$logFile"; then + log -e "cleanup: failed to remove parent bitmap $BITMAP_PARENT on node $node (non-fatal)" + fi + done < <( + virsh -c qemu:///system qemu-monitor-command "$VM" '{"execute":"query-block"}' 2>/dev/null | python3 -c ' +import sys, json +target = sys.argv[1] +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) +seen = set() +for dev in data.get("return", []) or []: + inserted = dev.get("inserted") or {} + node = inserted.get("node-name") + if not node or node in seen: + continue + if any((b or {}).get("name") == target for b in (inserted.get("dirty-bitmaps") or [])): + seen.add(node) + print(node) +' "$BITMAP_PARENT" 2>/dev/null || true + ) + fi + # Print statistics virsh -c qemu:///system domjobinfo $VM --completed backup_size=$(du -sb "$dest" 2>>"$logFile" | cut -f1) || { log -ne "WARNING: du failed for $dest, reporting size as 0"; backup_size=0; } @@ -204,6 +390,8 @@ backup_running_vm() { } backup_stopped_vm() { + # Stopped VMs cannot use libvirt's backup-begin (no QEMU process); take a full backup via + # qemu-img convert. The orchestrator never sends incremental mode for a stopped VM. mount_operation mkdir -p "$dest" || { echo "Failed to create backup directory $dest"; exit 1; } @@ -224,6 +412,23 @@ backup_stopped_vm() { cleanup exit 1 fi + + # Pre-seed a persistent bitmap on the source disk so the NEXT backup (taken + # after this VM is started again) can be incremental against the qcow2 we + # just wrote. Without this, every backup after a stopped-VM backup would + # fall back to full because no parent bitmap exists on the host yet. + # Only applies to file-backed qcow2 sources — RBD/LINSTOR have their own + # snapshot mechanisms and qemu-img bitmap is not the right primitive there. + # bitmap --add should not fail on a file-backed qcow2; if it does, fail the backup so the + # underlying problem is surfaced rather than silently degrading future backups to full. + if [[ -n "$BITMAP_NEW" && "$disk" != rbd:* && "$disk" != /dev/drbd/by-res/* ]]; then + if ! qemu-img bitmap --add "$disk" "$BITMAP_NEW" 2>>"$logFile"; then + echo "Failed to pre-seed bitmap $BITMAP_NEW on $disk" + cleanup + exit 1 + fi + fi + name="datadisk" done sync @@ -293,6 +498,15 @@ cleanup() { function usage { echo "" echo "Usage: $0 -o -v|--vm -t -s -m -p -d -q|--quiesce " + echo " [-M|--mode ] [--bitmap-new ] [--bitmap-parent ] [--parent-paths ]" + echo "" + echo "Incremental backup options (running VMs only; requires QEMU >= 4.2 and libvirt >= 7.2):" + echo " -M|--mode full Take a full backup AND create a checkpoint (--bitmap-new required) for future incrementals." + echo " -M|--mode incremental Take an incremental backup since --bitmap-parent and create new checkpoint --bitmap-new." + echo " Requires --bitmap-parent, --bitmap-new, and --parent-paths (comma-separated list, one" + echo " parent qcow2 path per disk: root..qcow2, datadisk..qcow2, … same order" + echo " as -d|--disks)." + echo " Without -M, behaves as legacy full-only backup with no checkpoint creation." echo "" exit 1 } @@ -339,6 +553,26 @@ while [[ $# -gt 0 ]]; do shift shift ;; + -M|--mode) + MODE="$2" + shift + shift + ;; + --bitmap-new) + BITMAP_NEW="$2" + shift + shift + ;; + --bitmap-parent) + BITMAP_PARENT="$2" + shift + shift + ;; + --parent-paths) + PARENT_PATHS="$2" + shift + shift + ;; -h|--help) usage shift @@ -350,7 +584,7 @@ while [[ $# -gt 0 ]]; do esac done -# Perform Initial sanity checks +# Perform initial environment sanity checks (QEMU/libvirt version). sanity_checks if [ "$OP" = "backup" ]; then diff --git a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java index 6c1c3424b1b9..6154522af4ae 100644 --- a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java +++ b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java @@ -351,7 +351,7 @@ public Map getClusterSettings(long vmId) { @Override public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, String vmInternalName, Backup backup) { logger.debug(String.format("Trying to import VM [vmInternalName: %s] from Backup [%s].", vmInternalName, - ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "backupType"))); + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "type"))); VMInstanceVO vm = _instanceDao.findVMByInstanceNameIncludingRemoved(vmInternalName); if (vm == null) { diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index f9c98cb141b6..0636ffdc3c1a 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -1137,6 +1137,9 @@ public Pair, Integer> listBackups(final ListBackupsCmd cmd) { sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); sb.and("zoneId", sb.entity().getZoneId(), SearchCriteria.Op.EQ); sb.and("backupOfferingId", sb.entity().getBackupOfferingId(), SearchCriteria.Op.EQ); + // Tombstoned chain backups (Status.Hidden) are never shown to users; they exist only so the + // incremental chain GC can sweep them once their last descendant is deleted. + sb.and("statusNeq", sb.entity().getStatus(), SearchCriteria.Op.NEQ); sb.and("backupStatus", sb.entity().getStatus(), SearchCriteria.Op.EQ); if (keyword != null) { @@ -1149,6 +1152,7 @@ public Pair, Integer> listBackups(final ListBackupsCmd cmd) { SearchCriteria sc = sb.create(); accountManager.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + sc.setParameters("statusNeq", Backup.Status.Hidden); if (id != null) { sc.setParameters("id", id); @@ -1190,7 +1194,7 @@ public boolean importRestoredVM(long zoneId, long domainId, long accountId, long vm = guru.importVirtualMachineFromBackup(zoneId, domainId, accountId, userId, vmInternalName, backup); } catch (final Exception e) { logger.error(String.format("Failed to import VM [vmInternalName: %s] from backup restoration [%s] with hypervisor [type: %s] due to: [%s].", vmInternalName, - ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "backupType"), hypervisorType, e.getMessage()), e); + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "type"), hypervisorType, e.getMessage()), e); ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_BACKUP_RESTORE, String.format("Failed to import Instance %s from Backup %s with hypervisor [type: %s]", vmInternalName, backup.getUuid(), hypervisorType), vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),0); @@ -1722,6 +1726,15 @@ private boolean deleteCheckedBackup(Boolean forced, BackupProvider backupProvide reservationDao, resourceLimitMgr)) { boolean result = backupProvider.deleteBackup(backup, forced); if (result) { + // Chain-aware providers (e.g. NAS) physically remove several backups per call + // (leaf + swept delete-pending ancestors) and decrement resource count/usage and + // remove each DB row themselves, exactly once per removed backup. Decrementing or + // removing again here would double-handle and destroy delete-pending tombstones, + // so defer entirely to the provider for those. + if (backupProvider.handlesChainDeleteResourceAccounting()) { + checkAndGenerateUsageForLastBackupDeletedAfterOfferingRemove(vm, backup); + return true; + } resourceLimitMgr.decrementResourceCount(backup.getAccountId(), Resource.ResourceType.backup); resourceLimitMgr.decrementResourceCount(backup.getAccountId(), Resource.ResourceType.backup_storage, backupSize); if (backupDao.remove(backup.getId())) { diff --git a/test/integration/smoke/test_backup_recovery_nas.py b/test/integration/smoke/test_backup_recovery_nas.py index 409a08acc9f0..e55c1b6f0f93 100644 --- a/test/integration/smoke/test_backup_recovery_nas.py +++ b/test/integration/smoke/test_backup_recovery_nas.py @@ -265,3 +265,323 @@ def test_vm_backup_create_vm_from_backup_in_another_zone(self): self.assertEqual(backup_repository.crosszoneinstancecreation, True, "Cross-Zone Instance Creation could not be enabled on the backup repository") self.vm_backup_create_vm_from_backup_int(template.id, [network.id]) + + # ------------------------------------------------------------------ + # Incremental backup tests (RFC #12899 / PR #13074) + # ------------------------------------------------------------------ + # These tests exercise the incremental NAS backup chain semantics: + # full -> incN cadence, restore-from-incremental, delete-middle chain + # repair, refuse-delete-full-with-children, and stopped-VM fallback. + # + # All tests set nas.backup.full.every to a small value (3) so a chain + # forms quickly without needing many backup iterations. The original + # zone value (whatever the test environment has configured) is captured + # before the test runs and restored verbatim in finally, so we don't + # leak config changes across tests on shared environments. + + def _set_full_every(self, value): + Configurations.update(self.apiclient, name='nas.backup.full.every', + value=str(value), zoneid=self.zone.id) + + def _get_full_every(self): + """Read the current zone-scoped (or global fallback) value of nas.backup.full.every.""" + configs = Configurations.list(self.apiclient, name='nas.backup.full.every', + zoneid=self.zone.id) + if configs and len(configs) > 0 and configs[0].value is not None: + return configs[0].value + # Fall back to global default — Configurations.list returns the global value + # when no zone override exists. Defensive fallback to '10' (the framework default). + return '10' + + def _backup_type(self, backup): + # Backup objects expose `type`; for chained backups it's "INCREMENTAL", else "FULL". + return getattr(backup, 'type', 'FULL') or 'FULL' + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_incremental_chain_cadence(self): + """ + With nas.backup.full.every=3, the sequence of backups should be + FULL, INCREMENTAL, INCREMENTAL, FULL, INCREMENTAL, ... + """ + self.backup_offering.assignOffering(self.apiclient, self.vm.id) + original_full_every = self._get_full_every() + self._set_full_every(3) + try: + ssh_client_vm = self.vm.get_ssh_client(reconnect=True) + ssh_client_vm.execute("touch /root/incremental_marker_1.txt") + + created = [] + for i in range(5): + Backup.create(self.apiclient, self.vm.id, "inc_chain_%d" % i) + # write a small change so each incremental has something to capture + ssh_client_vm.execute("dd if=/dev/urandom of=/root/delta_%d bs=64k count=4 2>/dev/null" % i) + time.sleep(2) + created = Backup.list(self.apiclient, self.vm.id) + + self.assertEqual(len(created), 5, "Expected 5 backups after 5 Backup.create calls") + # Sort oldest-first by date + created.sort(key=lambda b: b.created) + + expected = ['FULL', 'INCREMENTAL', 'INCREMENTAL', 'FULL', 'INCREMENTAL'] + actual = [self._backup_type(b).upper() for b in created] + self.assertEqual(actual, expected, + "With nas.backup.full.every=3, chain pattern should be %s but was %s" % (expected, actual)) + + # Cleanup all backups (newest first to satisfy chain rules without forced=true) + for b in reversed(created): + Backup.delete(self.apiclient, b.id) + finally: + self._set_full_every(original_full_every) + self.backup_offering.removeOffering(self.apiclient, self.vm.id) + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_incremental_after_vm_restart(self): + """ + Regression for the parent-checkpoint recreation bug (PR #13074): an incremental + backup taken AFTER the VM has been restarted must still succeed and restore + correctly. + + A VM (re)start rebuilds the libvirt domain XML and wipes libvirt's checkpoint + registry, while the dirty bitmap persists on the qcow2. The agent must then + re-register the parent checkpoint with `checkpoint-create --redefine` (from the + saved checkpoint XML) rather than a fresh create — a fresh create fails with + "Bitmap already exists", and qemu-img cannot drop the bitmap on a running disk. + + How this was reproduced manually on a libvirt 10.0.0 host, and what this test + automates: + FULL + marker1 -> stop/start the VM (wipes the checkpoint registry) + -> INCREMENTAL + marker2 -> restore the tip -> both markers present. + """ + self.backup_offering.assignOffering(self.apiclient, self.vm.id) + original_full_every = self._get_full_every() + # High cadence so the post-restart backup is INCREMENTAL, not a periodic FULL. + self._set_full_every(100) + backups = [] + try: + ssh_client_vm = self.vm.get_ssh_client(reconnect=True) + ssh_client_vm.execute("echo restart-test-1 > /root/restart_marker_1.txt; sync") + + # 1) FULL anchor + Backup.create(self.apiclient, self.vm.id, "restart_full") + time.sleep(2) + + # 2) Restart the VM — wipes libvirt's checkpoint registry (the bug trigger). + self.vm.stop(self.apiclient) + self.vm.start(self.apiclient) + ssh_client_vm = self.vm.get_ssh_client(reconnect=True) + ssh_client_vm.execute("echo restart-test-2 > /root/restart_marker_2.txt; sync") + + # 3) INCREMENTAL after the restart — the previously-broken path. + Backup.create(self.apiclient, self.vm.id, "restart_incr") + time.sleep(2) + + backups = Backup.list(self.apiclient, self.vm.id) + self.assertEqual(len(backups), 2, + "Expected FULL + INCREMENTAL after restart, got %d" % len(backups)) + backups.sort(key=lambda b: b.created) + self.assertEqual(self._backup_type(backups[0]).upper(), 'FULL', + "First backup should be FULL") + self.assertEqual(self._backup_type(backups[1]).upper(), 'INCREMENTAL', + "Backup taken after the VM restart must be INCREMENTAL, not silently a FULL") + + # 4) Restore the tip (incremental) and verify BOTH markers survived the chain + # across the restart — i.e. the post-restart incremental really captured data. + new_vm_name = "vm-restart-restore-" + str(int(time.time())) + new_vm = Backup.createVMFromBackup( + self.apiclient, + self.services["small"], + mode=self.services["mode"], + backupid=backups[1].id, + vmname=new_vm_name, + accountname=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id + ) + self.cleanup.append(new_vm) + self.assertIsNotNone(new_vm, "Failed to create VM from the post-restart incremental backup") + self.assertEqual(new_vm.state, "Running", "Restored VM should be Running") + + ssh_new = new_vm.get_ssh_client(reconnect=True) + r1 = "".join(ssh_new.execute("cat /root/restart_marker_1.txt")) + r2 = "".join(ssh_new.execute("cat /root/restart_marker_2.txt")) + self.assertIn("restart-test-1", r1, + "Marker written before the restart is missing from the restore") + self.assertIn("restart-test-2", r2, + "Marker written after the restart (captured by the post-restart incremental) " + "is missing from the restore") + finally: + for b in reversed(backups): + try: + Backup.delete(self.apiclient, b.id) + except Exception: + pass + self._set_full_every(original_full_every) + self.backup_offering.removeOffering(self.apiclient, self.vm.id) + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_restore_from_incremental(self): + """ + Take FULL + 2 INCREMENTAL backups, each with a marker file. Restore from the + latest incremental and verify all three markers are present (chain flatten). + """ + self.backup_offering.assignOffering(self.apiclient, self.vm.id) + original_full_every = self._get_full_every() + self._set_full_every(5) + try: + ssh_client_vm = self.vm.get_ssh_client(reconnect=True) + ssh_client_vm.execute("touch /root/marker_full.txt") + Backup.create(self.apiclient, self.vm.id, "rfi_full") + time.sleep(3) + + ssh_client_vm.execute("touch /root/marker_inc1.txt") + Backup.create(self.apiclient, self.vm.id, "rfi_inc1") + time.sleep(3) + + ssh_client_vm.execute("touch /root/marker_inc2.txt") + Backup.create(self.apiclient, self.vm.id, "rfi_inc2") + time.sleep(3) + + backups = Backup.list(self.apiclient, self.vm.id) + backups.sort(key=lambda b: b.created) + self.assertEqual(len(backups), 3) + self.assertEqual(self._backup_type(backups[0]).upper(), 'FULL') + self.assertEqual(self._backup_type(backups[2]).upper(), 'INCREMENTAL') + + new_vm_name = "vm-from-inc-" + str(int(time.time())) + new_vm = Backup.createVMFromBackup(self.apiclient, self.services["small"], + mode=self.services["mode"], backupid=backups[2].id, vmname=new_vm_name, + accountname=self.account.name, domainid=self.account.domainid, + zoneid=self.zone.id) + self.cleanup.append(new_vm) + + ssh_new = new_vm.get_ssh_client(reconnect=True) + for marker in ("marker_full.txt", "marker_inc1.txt", "marker_inc2.txt"): + result = ssh_new.execute("ls /root/%s" % marker) + self.assertIn(marker, result[0], + "Restored VM should have %s (chain flattened correctly)" % marker) + + for b in reversed(backups): + Backup.delete(self.apiclient, b.id) + finally: + self._set_full_every(original_full_every) + self.backup_offering.removeOffering(self.apiclient, self.vm.id) + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_delete_middle_incremental_repairs_chain(self): + """ + Delete a MIDDLE incremental from a FULL -> INC1 -> INC2 chain. + The chain repair should rebase INC2 onto FULL, and the final restore + should still produce a working VM with all expected blocks. + """ + self.backup_offering.assignOffering(self.apiclient, self.vm.id) + original_full_every = self._get_full_every() + self._set_full_every(5) + try: + ssh_client_vm = self.vm.get_ssh_client(reconnect=True) + ssh_client_vm.execute("touch /root/dmi_full.txt") + Backup.create(self.apiclient, self.vm.id, "dmi_full") + time.sleep(3) + ssh_client_vm.execute("touch /root/dmi_inc1.txt") + Backup.create(self.apiclient, self.vm.id, "dmi_inc1") + time.sleep(3) + ssh_client_vm.execute("touch /root/dmi_inc2.txt") + Backup.create(self.apiclient, self.vm.id, "dmi_inc2") + time.sleep(3) + + backups = Backup.list(self.apiclient, self.vm.id) + backups.sort(key=lambda b: b.created) + full, inc1, inc2 = backups[0], backups[1], backups[2] + + # Delete the middle incremental — should succeed via chain repair (no force needed) + Backup.delete(self.apiclient, inc1.id) + remaining = Backup.list(self.apiclient, self.vm.id) + self.assertEqual(len(remaining), 2, "After deleting middle inc, two backups should remain") + + # Restore from the remaining tail (formerly inc2) — must still produce a usable VM + new_vm_name = "vm-after-mid-del-" + str(int(time.time())) + new_vm = Backup.createVMFromBackup(self.apiclient, self.services["small"], + mode=self.services["mode"], backupid=inc2.id, vmname=new_vm_name, + accountname=self.account.name, domainid=self.account.domainid, + zoneid=self.zone.id) + self.cleanup.append(new_vm) + ssh_new = new_vm.get_ssh_client(reconnect=True) + # Both the FULL marker and (importantly) the deleted-INC1 marker should still + # be present, because the rebase merged INC1's blocks into INC2. + for marker in ("dmi_full.txt", "dmi_inc1.txt", "dmi_inc2.txt"): + result = ssh_new.execute("ls /root/%s" % marker) + self.assertIn(marker, result[0], + "After mid-incremental delete and rebase, %s should still be restorable" % marker) + + Backup.delete(self.apiclient, inc2.id) + Backup.delete(self.apiclient, full.id) + finally: + self._set_full_every(original_full_every) + self.backup_offering.removeOffering(self.apiclient, self.vm.id) + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_refuse_delete_full_with_children(self): + """ + Deleting a FULL that has surviving incrementals must fail without forced=true. + With forced=true it must succeed and remove the entire chain. + """ + self.backup_offering.assignOffering(self.apiclient, self.vm.id) + original_full_every = self._get_full_every() + self._set_full_every(5) + try: + Backup.create(self.apiclient, self.vm.id, "rdc_full") + time.sleep(3) + Backup.create(self.apiclient, self.vm.id, "rdc_inc") + time.sleep(3) + + backups = Backup.list(self.apiclient, self.vm.id) + backups.sort(key=lambda b: b.created) + full = backups[0] + + failed = False + try: + Backup.delete(self.apiclient, full.id) + except Exception: + failed = True + self.assertTrue(failed, "Deleting a FULL with children should be refused without forced=true") + + # Forced delete should succeed and clear the whole chain + Backup.delete(self.apiclient, full.id, forced=True) + remaining = Backup.list(self.apiclient, self.vm.id) + self.assertIsNone(remaining, "Forced delete of FULL should remove the entire chain") + finally: + self._set_full_every(original_full_every) + self.backup_offering.removeOffering(self.apiclient, self.vm.id) + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_stopped_vm_falls_back_to_full(self): + """ + When a backup is requested while the VM is stopped, even if the chain cadence + would call for an incremental, the agent must fall back to a full and start a + new chain. The incrementalFallback flag should be reflected in backup.type=FULL. + """ + self.backup_offering.assignOffering(self.apiclient, self.vm.id) + original_full_every = self._get_full_every() + self._set_full_every(2) # next backup after the first should be incremental + try: + Backup.create(self.apiclient, self.vm.id, "svf_first") + time.sleep(3) + + # Stop the VM and trigger another backup — should fall back to FULL + self.vm.stop(self.apiclient) + time.sleep(5) + Backup.create(self.apiclient, self.vm.id, "svf_second") + time.sleep(3) + + backups = Backup.list(self.apiclient, self.vm.id) + backups.sort(key=lambda b: b.created) + self.assertEqual(len(backups), 2) + self.assertEqual(self._backup_type(backups[0]).upper(), 'FULL') + self.assertEqual(self._backup_type(backups[1]).upper(), 'FULL', + "Stopped-VM backup must be a FULL even when cadence would have asked for an INCREMENTAL") + + self.vm.start(self.apiclient) + for b in reversed(backups): + Backup.delete(self.apiclient, b.id) + finally: + self._set_full_every(original_full_every) + self.backup_offering.removeOffering(self.apiclient, self.vm.id) From 333973ab3cd1a692dc34d536776c8cc21ebc75e1 Mon Sep 17 00:00:00 2001 From: N/A <16502919+erma07@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:17:34 +0300 Subject: [PATCH 164/205] Add xenserver.create.full.clone global setting (#13114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add xenserver.create.full.clone global setting Adds a StoragePool-scoped boolean ConfigKey mirroring vmware.create.full.clone so XenServer-backed VMs can be deployed as full VDI copies (VDI.copy) instead of always using linked clones (VDI.clone). Default false preserves today's behavior. The per-pool flag flows into the existing PrimaryDataStoreTO.fullCloneFlag through a new dispatch method addFullCloneAndDiskprovisiongStrictnessFlagOnDest that switches on hypervisor type. * Fix HypervisorType dispatch in addFullClone flag helper Replace invalid switch on Hypervisor.HypervisorType (not a Java enum) with equality checks so cloud-engine-storage-datamotion compiles. --------- Co-authored-by: Erki Märks --- .../com/cloud/storage/StorageManager.java | 8 +++ .../motion/AncientDataMotionStrategy.java | 51 ++++++++++++++++--- .../motion/AncientDataMotionStrategyTest.java | 8 +++ .../resource/XenServerStorageProcessor.java | 9 +++- .../com/cloud/storage/StorageManagerImpl.java | 1 + 5 files changed, 69 insertions(+), 8 deletions(-) diff --git a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java index 3c62738f9ed5..d6604cffc40a 100644 --- a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java +++ b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java @@ -195,6 +195,14 @@ public interface StorageManager extends StorageService { true, ConfigKey.Scope.StoragePool, null); + ConfigKey XenserverCreateCloneFull = new ConfigKey<>(Boolean.class, + "xenserver.create.full.clone", + "Storage", + "false", + "If set to true, creates VMs as full clones on XenServer hypervisor (uses VDI.copy instead of VDI.clone, removing the linked-clone parent relationship).", + true, + ConfigKey.Scope.StoragePool, + null); ConfigKey VmwareAllowParallelExecution = new ConfigKey<>(Boolean.class, "vmware.allow.parallel.command.execution", "Advanced", diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java index 1144a29986a6..dd54dd580052 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java @@ -191,7 +191,7 @@ protected Answer copyObject(DataObject srcData, DataObject destData, Host destHo srcForCopy = cacheData = cacheMgr.createCacheObject(srcData, destScope); } - CopyCommand cmd = new CopyCommand(srcForCopy.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()), primaryStorageDownloadWait, + CopyCommand cmd = new CopyCommand(srcForCopy.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()), primaryStorageDownloadWait, VirtualMachineManager.ExecuteInSequence.value()); EndPoint ep = destHost != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(destHost) : selector.select(srcForCopy, destData); if (ep == null) { @@ -257,6 +257,43 @@ protected DataTO addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(DataTO return dataTO; } + /** + * Adds {@code 'xenserver.create.full.clone'} value for a given primary storage, whose HV is XenServer, on datastore's {@code fullCloneFlag} field + * @param dataTO Dest data store TO + * @return dataTO including fullCloneFlag, if provided + */ + protected DataTO addFullCloneAndDiskprovisiongStrictnessFlagOnXenServerDest(DataTO dataTO) { + if (dataTO != null && dataTO.getHypervisorType().equals(Hypervisor.HypervisorType.XenServer)){ + DataStoreTO dataStoreTO = dataTO.getDataStore(); + if (dataStoreTO != null && dataStoreTO instanceof PrimaryDataStoreTO){ + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) dataStoreTO; + primaryDataStoreTO.setFullCloneFlag(StorageManager.XenserverCreateCloneFull.valueIn(primaryDataStoreTO.getId())); + } + } + return dataTO; + } + + /** + * Dispatches to the per-hypervisor {@code addFullCloneAndDiskprovisiongStrictnessFlagOn*Dest} helper + * based on {@code dataTO.getHypervisorType()}. Returns {@code dataTO} unchanged for hypervisors + * that do not have a full-clone toggle. + * @param dataTO Dest data store TO + * @return dataTO including fullCloneFlag, if provided + */ + protected DataTO addFullCloneAndDiskprovisiongStrictnessFlagOnDest(DataTO dataTO) { + if (dataTO == null) { + return dataTO; + } + Hypervisor.HypervisorType hypervisorType = dataTO.getHypervisorType(); + if (Hypervisor.HypervisorType.VMware.equals(hypervisorType)) { + return addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(dataTO); + } + if (Hypervisor.HypervisorType.XenServer.equals(hypervisorType)) { + return addFullCloneAndDiskprovisiongStrictnessFlagOnXenServerDest(dataTO); + } + return dataTO; + } + protected Answer copyObject(DataObject srcData, DataObject destData) { return copyObject(srcData, destData, null); } @@ -315,7 +352,7 @@ protected Answer copyVolumeFromSnapshot(DataObject snapObj, DataObject volObj) { updateLockHostForVolume(ep, volObj); - CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(volObj.getTO()), _createVolumeFromSnapshotWait, VirtualMachineManager.ExecuteInSequence.value()); + CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(volObj.getTO()), _createVolumeFromSnapshotWait, VirtualMachineManager.ExecuteInSequence.value()); Answer answer = null; if (ep == null) { @@ -361,7 +398,7 @@ private void updateLockHostForVolume(EndPoint ep, DataObject volObj) { } protected Answer cloneVolume(DataObject template, DataObject volume) { - CopyCommand cmd = new CopyCommand(template.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(volume.getTO()), 0, VirtualMachineManager.ExecuteInSequence.value()); + CopyCommand cmd = new CopyCommand(template.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(volume.getTO()), 0, VirtualMachineManager.ExecuteInSequence.value()); try { EndPoint ep = selector.select(volume, anyVolumeRequiresEncryption(volume)); Answer answer = null; @@ -445,7 +482,7 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) objOnImageStore.processEvent(Event.CopyingRequested); - CopyCommand cmd = new CopyCommand(objOnImageStore.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()), _copyvolumewait, VirtualMachineManager.ExecuteInSequence.value()); + CopyCommand cmd = new CopyCommand(objOnImageStore.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()), _copyvolumewait, VirtualMachineManager.ExecuteInSequence.value()); EndPoint ep = selector.select(objOnImageStore, destData, encryptionRequired); if (ep == null) { String errMsg = String.format(NO_REMOTE_ENDPOINT_WITH_ENCRYPTION, encryptionRequired); @@ -692,7 +729,7 @@ protected Answer createTemplateFromSnapshot(DataObject srcData, DataObject destD ep = selector.select(srcData, destData); } - CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()), _createprivatetemplatefromsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); + CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()), _createprivatetemplatefromsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); Answer answer = null; if (ep == null) { logger.error(NO_REMOTE_ENDPOINT_SSVM); @@ -730,7 +767,7 @@ protected Answer copySnapshot(DataObject srcData, DataObject destData) { Scope selectedScope = pickCacheScopeForCopy(srcData, destData); cacheData = cacheMgr.getCacheObject(srcData, selectedScope); - CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()), _backupsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); + CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()), _backupsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); cmd.setCacheTO(cacheData.getTO()); cmd.setOptions(options); EndPoint ep = selector.select(srcData, destData, encryptionRequired); @@ -741,7 +778,7 @@ protected Answer copySnapshot(DataObject srcData, DataObject destData) { answer = ep.sendMessage(cmd); } } else { - addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()); + addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()); CopyCommand cmd = new CopyCommand(srcData.getTO(), destData.getTO(), _backupsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); cmd.setOptions(options); EndPoint ep = selector.select(srcData, destData, StorageAction.BACKUPSNAPSHOT, encryptionRequired); diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java index e84163656b10..86af81899e8f 100755 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java @@ -113,6 +113,14 @@ public void testAddFullCloneFlagOnVMwareDest(){ verify(dataStoreTO).setFullCloneFlag(FULL_CLONE_FLAG); } + @Test + public void testAddFullCloneFlagOnXenServerDest() throws IllegalAccessException, NoSuchFieldException { + overrideDefaultConfigValue(StorageManager.XenserverCreateCloneFull, String.valueOf(FULL_CLONE_FLAG)); + when(dataTO.getHypervisorType()).thenReturn(HypervisorType.XenServer); + strategy.addFullCloneAndDiskprovisiongStrictnessFlagOnXenServerDest(dataTO); + verify(dataStoreTO).setFullCloneFlag(FULL_CLONE_FLAG); + } + @Test public void testAddFullCloneFlagOnNotVmwareDest(){ verify(dataStoreTO, never()).setFullCloneFlag(any(Boolean.class)); diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/XenServerStorageProcessor.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/XenServerStorageProcessor.java index c9e6118340cc..a1d27b65abac 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/XenServerStorageProcessor.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/XenServerStorageProcessor.java @@ -859,12 +859,19 @@ public Answer cloneVolumeFromBaseTemplate(final CopyCommand cmd) { final DataTO srcData = cmd.getSrcTO(); final DataTO destData = cmd.getDestTO(); final VolumeObjectTO volume = (VolumeObjectTO) destData; + final DataStoreTO destStore = volume.getDataStore(); + final boolean fullClone = destStore instanceof PrimaryDataStoreTO + && Boolean.TRUE.equals(((PrimaryDataStoreTO) destStore).isFullCloneFlag()); VDI vdi = null; try { VDI tmpltvdi = null; tmpltvdi = getVDIbyUuid(conn, srcData.getPath()); - vdi = tmpltvdi.createClone(conn, new HashMap()); + if (fullClone) { + vdi = tmpltvdi.copy(conn, tmpltvdi.getSR(conn)); + } else { + vdi = tmpltvdi.createClone(conn, new HashMap()); + } Long virtualSize = vdi.getVirtualSize(conn); if (volume.getSize() > virtualSize) { logger.debug("Overriding provided Template's size with new size " + toHumanReadableSize(volume.getSize()) + " for volume: " + volume.getName()); diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index ae2facf38619..9cb5155753c6 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -4621,6 +4621,7 @@ public ConfigKey[] getConfigKeys() { SecStorageVMAutoScaleDown, MountDisabledStoragePool, VmwareCreateCloneFull, + XenserverCreateCloneFull, VmwareAllowParallelExecution, DataStoreDownloadFollowRedirects, AllowVolumeReSizeBeyondAllocation, From 1400616e7129e854173a1e1481e2f1525e8599f3 Mon Sep 17 00:00:00 2001 From: Vishesh <8760112+vishesh92@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:02:08 +0530 Subject: [PATCH 165/205] UI: Fix icon for KMS & ordering in the left side menu (#13568) * Fix icon for KMS * Move KMS further down in the left side menu --- ui/src/config/router.js | 2 +- ui/src/config/section/kms.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/config/router.js b/ui/src/config/router.js index a48c3fef81e3..b9c60bcd0c21 100644 --- a/ui/src/config/router.js +++ b/ui/src/config/router.js @@ -218,9 +218,9 @@ export function asyncRouterMap () { generateRouterMap(compute), generateRouterMap(storage), - generateRouterMap(kms), generateRouterMap(network), generateRouterMap(image), + generateRouterMap(kms), generateRouterMap(event), generateRouterMap(project), generateRouterMap(user), diff --git a/ui/src/config/section/kms.js b/ui/src/config/section/kms.js index 648a8064b5c6..3120fee54cc8 100644 --- a/ui/src/config/section/kms.js +++ b/ui/src/config/section/kms.js @@ -21,7 +21,7 @@ import store from '@/store' export default { name: 'kms', title: 'label.kms', - icon: 'hdd-outlined', + icon: 'lock-outlined', show: () => { return ['Admin'].includes(store.getters.userInfo.roletype) || store.getters.features.hashsmprofiles }, From 33c3967b6b636ff50a644be8f445c558b5076051 Mon Sep 17 00:00:00 2001 From: Daman Arora <61474540+Damans227@users.noreply.github.com> Date: Thu, 9 Jul 2026 07:00:31 -0400 Subject: [PATCH 166/205] Add per-domain OAuth (Google, GitHub) provider support (#12702) * Add domain_id to oauth_provider table and VO * Add domain-aware methods to OauthProviderDao * Add domainId parameter to OAuth provider API commands and response * Add domain support to OAuth2AuthManager * Add domain-aware OAuth verification * Add domain support to ListOAuthProvidersCmd and update related tests * fix domain path issue * Add domainId support to OAuth provider * Return domain name and UUID in OAuth provider API responses using ApiDBUtils * Refactor domain ID resolution in VerifyOAuthCodeAndGetUserCmd to improve code clarity * Enhance OAuth2 plugin support for domain-level configuration and authentication checks * Update OAuth2 tests and VerifyOAuthCodeAndGetUserCmdTest * Add method to find OAuth provider by domain with global fallback * Update OAuth provider configuration to use 'domain' instead of 'domainid' in columns and details * Refactor OAuth provider methods to support domain-level queries and enhance user verification * Add caching for access token retrieval in GithubOAuth2Provider * Refactor access token checks in GithubOAuth2Provider to use StringUtils for improved readability and consistency * Refactor null checks to use utility for improved readability and consistency * Update OAuth2UserAuthenticatorTest to include domainId in user verification method * Remove unnecessary blank line and unused imports in OAuth provider command classes * Refactor and cleanup * Remove unnecessary blank lines * Enhance RegisterOAuthProviderCmdTest with additional provider mock data * Remove startup gate from OAuth plugin initialization to support dynamic config toggling * Add strictScope to ConfigKey to disable global fallback for domain-scoped oauth2.enabled * Add domain-scoped provider filtering to listOauthProvider and centralize domain resolution in OAuth2AuthManager * Add External OAuth tab with domain-scoped provider selection to login page * code cleanup * test fix * Handle login page provider visibility * UI cleanup * UI Cleanup * Keep text color consistent * add unit tests * Add Multiple-domain OAuth tests * Refactor as per PR comments * Use idempotent DDL helpers for oauth_provider schema migration * Use global config check for global providers and extract oauthEnabled variable * Make strictScope return null when id is null * Rename verification methods to use 'verifySecretCodeAndFetchEmail' for consistency * Refactor domain handling in OAuth2AuthManagerImpl to use DomainService instead of DomainDao * Enhance domain ID descriptions in OAuth command classes for clarity * Add domain path handling to OAuth provider commands and improve descriptions * Update domain path description in VerifyOAuthCodeAndGetUserCmd to clarify behavior with Domain ID * Replace remove method with expunge in deleteOauthProvider and add corresponding unit test * Add external login label to Login.vue and update i18n locale handling * Fix stale value issue in updateConfiguration response handling in ConfigurationValue.vue * Enhance OAuth login error handling and add unit test for missing parameters * Add validation to reject enabling OAuth provider when plugin is disabled at domain scope * Add domain reassignment support to UpdateOAuthProviderCmd and enhance validation in OAuth2AuthManagerImpl * Add domain ID to OAuth provider arguments in config * Fix condition for OAuth verification URL handling in router * Add domain path to OauthProviderResponse and update UI config to display it * Update config to remove 'secretkey' from columns and details * Add secretkey to details in config and display in DetailsTab * Implement normalization of ROOT domain to null for global OAuth provider handling and add corresponding unit tests * Refactor OAuth plugin domain scope handling to use a centralized method for enabling checks * Add strict scope handling to ConfigKey and update OAuth2AuthManager usage * Implement domain removal listener to clean up OAuth providers on domain deletion * Enhance OAuth tab icons with disabled state styling for better UX * Add domain-specific provider prompt and update OAuth provider handling --------- Co-authored-by: Daman Arora --- .../auth/UserOAuth2Authenticator.java | 13 +- .../META-INF/db/schema-42210to42300.sql | 6 + .../framework/config/ConfigKey.java | 20 +- .../cloudstack/oauth2/OAuth2AuthManager.java | 29 +- .../oauth2/OAuth2AuthManagerImpl.java | 161 +++++- .../oauth2/OAuth2UserAuthenticator.java | 11 +- .../api/command/ListOAuthProvidersCmd.java | 58 +- .../OauthLoginAPIAuthenticatorCmd.java | 47 +- .../api/command/RegisterOAuthProviderCmd.java | 22 +- .../api/command/UpdateOAuthProviderCmd.java | 26 +- .../command/VerifyOAuthCodeAndGetUserCmd.java | 24 +- .../api/response/OauthProviderResponse.java | 54 +- .../oauth2/dao/OauthProviderDao.java | 10 +- .../oauth2/dao/OauthProviderDaoImpl.java | 45 +- .../oauth2/github/GithubOAuth2Provider.java | 31 +- .../oauth2/google/GoogleOAuth2Provider.java | 43 +- .../keycloak/KeycloakOAuth2Provider.java | 18 +- .../cloudstack/oauth2/vo/OauthProviderVO.java | 11 + .../oauth2/OAuth2AuthManagerImplTest.java | 510 +++++++++++++++++- .../oauth2/OAuth2UserAuthenticatorTest.java | 54 +- .../OauthLoginAPIAuthenticatorCmdTest.java | 30 ++ .../command/RegisterOAuthProviderCmdTest.java | 31 +- .../VerifyOAuthCodeAndGetUserCmdTest.java | 14 +- .../google/GoogleOAuth2ProviderTest.java | 8 +- .../keycloak/KeycloakOAuth2ProviderTest.java | 16 +- .../cloud/server/ManagementServerImpl.java | 9 +- ui/public/locales/en.json | 1 + ui/src/components/view/DetailsTab.vue | 21 +- ui/src/config/section/config.js | 8 +- ui/src/locales/index.js | 52 +- ui/src/permission.js | 2 +- ui/src/views/AutogenView.vue | 2 +- ui/src/views/auth/Login.vue | 290 +++++++--- ui/src/views/dashboard/VerifyOauth.vue | 2 +- ui/src/views/setting/ConfigurationValue.vue | 3 + 35 files changed, 1432 insertions(+), 250 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java b/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java index ee3b98b8a4b6..cccc1fff9823 100644 --- a/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java +++ b/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java @@ -42,8 +42,19 @@ public interface UserOAuth2Authenticator extends Adapter { * Verifies the code provided by provider and fetches email * @return returns email */ - String verifyCodeAndFetchEmail(String secretCode); + String verifySecretCodeAndFetchEmail(String secretCode); + /** + * Verifies if the logged in user is valid for a specific domain + * @return true if it's a valid user, otherwise false + */ + boolean verifyUser(String email, String secretCode, Long domainId); + + /** + * Verifies the secret code provided by provider and fetches email for a specific domain + * @return email for the specified domain + */ + String verifySecretCodeAndFetchEmail(String secretCode, Long domainId); /** * Fetches email using the accessToken diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 9f4353490956..16d46fddf7b2 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -19,6 +19,12 @@ -- Schema upgrade from 4.22.1.0 to 4.23.0.0 --; +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider', 'domain_id', 'bigint unsigned DEFAULT NULL COMMENT "NULL for global provider, domain ID for domain-specific" AFTER `redirect_uri`'); +CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.oauth_provider', 'fk_oauth_provider__domain_id', '(`domain_id`)', '`domain`(`id`)'); +CALL `cloud`.`IDEMPOTENT_ADD_KEY`('i_oauth_provider__domain_id', 'cloud.oauth_provider', '(`domain_id`)'); + +CALL `cloud`.`IDEMPOTENT_ADD_UNIQUE_KEY`('cloud.oauth_provider', 'uk_oauth_provider__provider_domain', '(`provider`, `domain_id`)'); + CREATE TABLE `cloud`.`backup_offering_details` ( `id` bigint unsigned NOT NULL auto_increment, `backup_offering_id` bigint unsigned NOT NULL COMMENT 'Backup offering id', diff --git a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java index ef50064050f8..fd007f12957b 100644 --- a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java +++ b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java @@ -269,6 +269,17 @@ public String toString() { private String _defaultValueIfEmpty = null; + private boolean _strictScope = false; + + public boolean isStrictScope() { + return _strictScope; + } + + public ConfigKey withStrictScope() { + this._strictScope = true; + return this; + } + public static void init(ConfigDepotImpl depot) { s_depot = depot; } @@ -429,11 +440,18 @@ public T valueInDomain(Long domainId) { } public T valueInScope(Scope scope, Long id) { + return valueInScope(scope, id, false); + } + + public T valueInScope(Scope scope, Long id, boolean strictScope) { if (id == null) { - return value(); + return strictScope ? null : value(); } String value = s_depot != null ? s_depot.getConfigStringValue(_name, scope, id) : null; if (value == null) { + if (strictScope) { + return null; + } return valueInGlobalOrAvailableParentScope(scope, id); } logger.trace("Scope({}) value for config ({}): {}", scope, _name, _value); diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java index ece012db3a40..133131d3928a 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java @@ -18,6 +18,7 @@ // package org.apache.cloudstack.oauth2; +import com.cloud.domain.Domain; import com.cloud.utils.component.PluggableService; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.auth.UserOAuth2Authenticator; @@ -27,10 +28,15 @@ import org.apache.cloudstack.oauth2.vo.OauthProviderVO; import java.util.List; +import java.util.Map; public interface OAuth2AuthManager extends PluggableAPIAuthenticator, PluggableService { + String GLOBAL_DOMAIN_FILTER = "-1"; + Long GLOBAL_DOMAIN_ID = -1L; + public static ConfigKey OAuth2IsPluginEnabled = new ConfigKey("Advanced", Boolean.class, "oauth2.enabled", "false", - "Indicates whether OAuth plugin is enabled or not", false); + "Indicates whether OAuth plugin is enabled or not. This can be configured at domain level.", true, ConfigKey.Scope.Domain) + .withStrictScope(); public static final ConfigKey OAuth2Plugins = new ConfigKey("Advanced", String.class, "oauth2.plugins", "google,github", "List of OAuth plugins", true); public static final ConfigKey OAuth2PluginsExclude = new ConfigKey("Advanced", String.class, "oauth2.plugins.exclude", "", @@ -49,13 +55,30 @@ public interface OAuth2AuthManager extends PluggableAPIAuthenticator, PluggableS */ UserOAuth2Authenticator getUserOAuth2AuthenticationProvider(final String providerName); - String verifyCodeAndFetchEmail(String code, String provider); + String verifySecretCodeAndFetchEmail(String code, String provider, Long domainId); OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd); - List listOauthProviders(String provider, String uuid); + List listOauthProviders(String provider, String uuid, Long domainId); boolean deleteOauthProvider(Long id); OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd); + + Long resolveDomainId(Map params); + + /** + * Resolves whether the OAuth plugin is enabled for the given domain scope. + * A null domain or the ROOT domain is treated as the global scope, since the + * ROOT domain has no domain-level override and inherits the global value; + * any other domain is checked strictly at its own domain scope (no inheritance). + * @param domainId domain id, or null for global + * @return true if OAuth is enabled for that scope + */ + static boolean isPluginEnabledForDomain(Long domainId) { + if (domainId == null || domainId == Domain.ROOT_DOMAIN) { + return Boolean.TRUE.equals(OAuth2IsPluginEnabled.value()); + } + return Boolean.TRUE.equals(OAuth2IsPluginEnabled.valueInScope(ConfigKey.Scope.Domain, domainId, true)); + } } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java index b1bb8292f24a..c3bad43be40e 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java @@ -23,9 +23,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import javax.inject.Inject; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.auth.UserOAuth2Authenticator; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -39,15 +41,28 @@ import org.apache.cloudstack.oauth2.vo.OauthProviderVO; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.ArrayUtils; + +import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.user.DomainManager; +import com.cloud.user.DomainService; import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.framework.messagebus.MessageBus; public class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthManager, Manager, Configurable { @Inject protected OauthProviderDao _oauthProviderDao; + @Inject + private DomainService _domainService; + + @Inject + private MessageBus _messageBus; + protected static Map userOAuth2AuthenticationProvidersMap = new HashMap<>(); private List userOAuth2AuthenticationProviders; @@ -63,17 +78,29 @@ public List> getAuthCommands() { @Override public boolean start() { - if (isOAuthPluginEnabled()) { - logger.info("OAUTH plugin loaded"); - initializeUserOAuth2AuthenticationProvidersMap(); - } else { - logger.info("OAUTH plugin not enabled so not loading"); - } + initializeUserOAuth2AuthenticationProvidersMap(); + addDomainRemovalListener(); + logger.info("OAUTH plugin loaded"); return true; } - protected boolean isOAuthPluginEnabled() { - return OAuth2IsPluginEnabled.value(); + private void addDomainRemovalListener() { + _messageBus.subscribe(DomainManager.MESSAGE_PRE_REMOVE_DOMAIN_EVENT, (senderAddress, subject, args) -> { + try { + long domainId = ((DomainVO) args).getId(); + List providers = _oauthProviderDao.listByDomain(domainId); + for (OauthProviderVO provider : providers) { + _oauthProviderDao.expunge(provider.getId()); + logger.debug("Removed OAuth provider {} for deleted domain {}", provider.getProvider(), domainId); + } + } catch (Exception e) { + logger.error("Failed to remove OAuth providers for deleted domain", e); + } + }); + } + + protected boolean isOAuthPluginEnabled(Long domainId) { + return OAuth2AuthManager.isPluginEnabledForDomain(domainId); } @Override @@ -124,9 +151,11 @@ protected void initializeUserOAuth2AuthenticationProvidersMap() { } @Override - public String verifyCodeAndFetchEmail(String code, String provider) { + public String verifySecretCodeAndFetchEmail(String code, String provider, Long domainId) { UserOAuth2Authenticator authenticator = getUserOAuth2AuthenticationProvider(provider); - return authenticator.verifyCodeAndFetchEmail(code); + String email = authenticator.verifySecretCodeAndFetchEmail(code, domainId); + + return email; } @Override @@ -136,27 +165,38 @@ public OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd) { String clientId = StringUtils.trim(cmd.getClientId()); String redirectUri = StringUtils.trim(cmd.getRedirectUri()); String secretKey = StringUtils.trim(cmd.getSecretKey()); + Long domainId = normalizeGlobalScope(resolveDomainIdFromIdOrPath(cmd.getDomainId(), cmd.getDomainPath())); String authorizeUrl = StringUtils.trim(cmd.getAuthorizeUrl()); String tokenUrl = StringUtils.trim(cmd.getTokenUrl()); - if (!isOAuthPluginEnabled()) { + if (!isOAuthPluginEnabled(domainId)) { throw new CloudRuntimeException("OAuth is not enabled, please enable to register"); } - OauthProviderVO providerVO = _oauthProviderDao.findByProvider(provider); + + // Check for existing provider with same name and domain + OauthProviderVO providerVO = _oauthProviderDao.findByProviderAndDomain(provider, domainId); if (providerVO != null) { - throw new CloudRuntimeException(String.format("Provider with the name %s is already registered", provider)); + if (domainId == null) { + throw new CloudRuntimeException(String.format("Global provider with the name %s is already registered", provider)); + } else { + throw new CloudRuntimeException(String.format("Provider with the name %s is already registered for domain %d", provider, domainId)); + } } - return saveOauthProvider(provider, description, clientId, secretKey, redirectUri, authorizeUrl, tokenUrl); + return saveOauthProvider(provider, description, clientId, secretKey, redirectUri, authorizeUrl, tokenUrl, domainId); } @Override - public List listOauthProviders(String provider, String uuid) { + public List listOauthProviders(String provider, String uuid, Long domainId) { List providers; if (uuid != null) { providers = Collections.singletonList(_oauthProviderDao.findByUuid(uuid)); + } else if (StringUtils.isNotBlank(provider) && domainId != null) { + providers = Collections.singletonList(_oauthProviderDao.findByProviderAndDomain(provider, domainId)); } else if (StringUtils.isNotBlank(provider)) { - providers = Collections.singletonList(_oauthProviderDao.findByProvider(provider)); + providers = Collections.singletonList(_oauthProviderDao.findByProviderAndDomain(provider, null)); + } else if (domainId != null) { + providers = _oauthProviderDao.listByDomainIncludingGlobal(domainId); } else { providers = _oauthProviderDao.listAll(); } @@ -179,6 +219,30 @@ public OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd) { throw new CloudRuntimeException("Provider with the given id is not there"); } + Long targetDomainId = providerVO.getDomainId(); + if (cmd.getDomainId() != null || StringUtils.isNotEmpty(cmd.getDomainPath())) { + Long resolved = resolveDomainIdFromIdOrPath(cmd.getDomainId(), cmd.getDomainPath()); + if (resolved == null) { + throw new CloudRuntimeException("Unable to resolve the supplied domain. Provide a valid domain id or path."); + } + resolved = normalizeGlobalScope(resolved); + if (!Objects.equals(resolved, providerVO.getDomainId())) { + OauthProviderVO existing = _oauthProviderDao.findByProviderAndDomain(providerVO.getProvider(), resolved); + if (existing != null) { + throw new CloudRuntimeException(String.format( + "Provider with the name %s is already registered for domain %s", providerVO.getProvider(), + resolved == null ? "ROOT (global)" : resolved)); + } + } + targetDomainId = resolved; + } + + if (Boolean.TRUE.equals(enabled) && !isOAuthPluginEnabled(targetDomainId)) { + throw new CloudRuntimeException(String.format( + "OAuth plugin is not enabled %s. Enable oauth2.enabled at that scope before enabling this provider.", + targetDomainId == null ? "globally" : "for this domain")); + } + if (StringUtils.isNotEmpty(description)) { providerVO.setDescription(description); } @@ -200,13 +264,14 @@ public OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd) { if (enabled != null) { providerVO.setEnabled(enabled); } + providerVO.setDomainId(targetDomainId); _oauthProviderDao.update(id, providerVO); return _oauthProviderDao.findById(id); } - private OauthProviderVO saveOauthProvider(String provider, String description, String clientId, String secretKey, String redirectUri, String authorizeUrl, String tokenUrl) { + private OauthProviderVO saveOauthProvider(String provider, String description, String clientId, String secretKey, String redirectUri, String authorizeUrl, String tokenUrl, Long domainId) { final OauthProviderVO oauthProviderVO = new OauthProviderVO(); oauthProviderVO.setProvider(provider); @@ -214,6 +279,7 @@ private OauthProviderVO saveOauthProvider(String provider, String description, S oauthProviderVO.setClientId(clientId); oauthProviderVO.setSecretKey(secretKey); oauthProviderVO.setRedirectUri(redirectUri); + oauthProviderVO.setDomainId(domainId); oauthProviderVO.setAuthorizeUrl(authorizeUrl); oauthProviderVO.setTokenUrl(tokenUrl); oauthProviderVO.setEnabled(true); @@ -225,7 +291,66 @@ private OauthProviderVO saveOauthProvider(String provider, String description, S @Override public boolean deleteOauthProvider(Long id) { - return _oauthProviderDao.remove(id); + return _oauthProviderDao.expunge(id); + } + + @Override + public Long resolveDomainId(Map params) { + final String[] domainIdArray = (String[])params.get(ApiConstants.DOMAIN_ID); + if (ArrayUtils.isNotEmpty(domainIdArray)) { + String domainUuid = domainIdArray[0]; + if (GLOBAL_DOMAIN_FILTER.equals(domainUuid)) { + return GLOBAL_DOMAIN_ID; + } + Domain domain = _domainService.getDomain(domainUuid); + if (Objects.nonNull(domain)) { + return domain.getId(); + } + } + final String[] domainArray = (String[])params.get(ApiConstants.DOMAIN); + if (ArrayUtils.isNotEmpty(domainArray)) { + String path = normalizeDomainPath(domainArray[0]); + if (StringUtils.isNotEmpty(path)) { + Domain domain = _domainService.findDomainByIdOrPath(null, path); + if (Objects.nonNull(domain)) { + return domain.getId(); + } + } + } + return null; + } + + protected Long resolveDomainIdFromIdOrPath(Long domainId, String domainPath) { + if (domainId != null) { + return domainId; + } + String path = normalizeDomainPath(domainPath); + if (StringUtils.isNotEmpty(path)) { + Domain domain = _domainService.findDomainByIdOrPath(null, path); + if (Objects.nonNull(domain)) { + return domain.getId(); + } + } + return null; + } + + // The ROOT domain is the top of the tree, so a provider scoped to it is equivalent + // to a global provider; treat it as global so the global oauth2.enabled config applies. + protected Long normalizeGlobalScope(Long domainId) { + return (domainId != null && Domain.ROOT_DOMAIN == domainId) ? null : domainId; + } + + protected String normalizeDomainPath(String path) { + if (StringUtils.isEmpty(path)) { + return null; + } + if (!path.startsWith("/")) { + path = "/" + path; + } + if (!path.endsWith("/")) { + path += "/"; + } + return path; } @Override diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java index dde50c8bb34d..49df94709836 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java @@ -30,8 +30,7 @@ import javax.inject.Inject; import java.util.Map; - -import static org.apache.cloudstack.oauth2.OAuth2AuthManager.OAuth2IsPluginEnabled; +import java.util.Objects; public class OAuth2UserAuthenticator extends AdapterBase implements UserAuthenticator { @@ -49,7 +48,7 @@ public Pair authenticate(String username, logger.debug("Trying OAuth2 auth for user: " + username); } - if (!isOAuthPluginEnabled()) { + if (!isOAuthPluginEnabled(domainId)) { logger.debug("OAuth2 plugin is disabled"); return new Pair(false, null); } else if (requestParameters == null) { @@ -76,7 +75,7 @@ public Pair authenticate(String username, String secretCode = ((secretCodeArray == null) ? null : secretCodeArray[0]); UserOAuth2Authenticator authenticator = userOAuth2mgr.getUserOAuth2AuthenticationProvider(oauthProvider); - if (user != null && authenticator.verifyUser(email, secretCode)) { + if (Objects.nonNull(user) && authenticator.verifyUser(email, secretCode, domainId)) { return new Pair(true, null); } } @@ -89,7 +88,7 @@ public String encode(String password) { return null; } - protected boolean isOAuthPluginEnabled() { - return OAuth2IsPluginEnabled.value(); + protected boolean isOAuthPluginEnabled(Long domainId) { + return OAuth2AuthManager.isPluginEnabledForDomain(domainId); } } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java index 9b91a1d879c2..2d0a2e2a7417 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java @@ -35,14 +35,17 @@ import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.auth.UserOAuth2Authenticator; import org.apache.cloudstack.oauth2.OAuth2AuthManager; import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; import org.apache.cloudstack.oauth2.vo.OauthProviderVO; -import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang3.ArrayUtils; +import com.cloud.api.ApiDBUtils; import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.domain.Domain; import com.cloud.user.Account; @APICommand(name = "listOauthProvider", description = "List OAuth providers registered", responseObject = OauthProviderResponse.class, entityType = {}, @@ -60,6 +63,14 @@ public class ListOAuthProvidersCmd extends BaseListCmd implements APIAuthenticat @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the provider") private String provider; + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "Domain ID to list OAuth providers for a specific domain. Use -1 for global providers only.", since = "4.23.0") + private Long domainId; + + @Parameter(name = ApiConstants.DOMAIN, type = CommandType.STRING, + description = "Domain path for domain-specific OAuth provider lookup. Ignored when Domain ID is passed.", since = "4.23.0") + private String domainPath; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -71,6 +82,10 @@ public String getProvider() { return provider; } + public Long getDomainId() { + return domainId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -99,7 +114,26 @@ public String authenticate(String command, Map params, HttpSes provider = providerArray[0]; } - List resultList = _oauth2mgr.listOauthProviders(provider, id); + boolean domainRequested = ArrayUtils.isNotEmpty((String[])params.get(ApiConstants.DOMAIN_ID)) + || ArrayUtils.isNotEmpty((String[])params.get(ApiConstants.DOMAIN)); + domainId = _oauth2mgr.resolveDomainId(params); + + if (domainRequested && domainId == null) { + ListResponse response = new ListResponse<>(); + response.setResponses(new ArrayList<>(), 0); + response.setResponseName(getCommandName()); + setResponseObject(response); + return ApiResponseSerializer.toSerializedString(response, responseType); + } + + List resultList = _oauth2mgr.listOauthProviders(provider, id, domainId); + boolean isAuthenticated = session != null && session.getAttribute(ApiConstants.USER_ID) != null; + if (domainRequested && domainId != null && domainId > 0) { + resultList.removeIf(p -> p.getDomainId() == null); + } else if (!domainRequested && !isAuthenticated) { + resultList.removeIf(p -> p.getDomainId() != null); + } + List userOAuth2AuthenticatorPlugins = _oauth2mgr.listUserOAuth2AuthenticationProviders(); List authenticatorPluginNames = new ArrayList<>(); for (UserOAuth2Authenticator authenticator : userOAuth2AuthenticatorPlugins) { @@ -108,9 +142,11 @@ public String authenticate(String command, Map params, HttpSes } List responses = new ArrayList<>(); for (OauthProviderVO result : resultList) { + Domain domain = result.getDomainId() != null ? ApiDBUtils.findDomainById(result.getDomainId()) : null; OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(), - result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri(), result.getAuthorizeUrl(), result.getTokenUrl()); - if (OAuth2AuthManager.OAuth2IsPluginEnabled.value() && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) { + result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri(), result.getAuthorizeUrl(), result.getTokenUrl(), domain); + boolean oauthEnabled = OAuth2AuthManager.isPluginEnabledForDomain(result.getDomainId()); + if (oauthEnabled && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) { r.setEnabled(true); } else { r.setEnabled(false); @@ -119,8 +155,20 @@ public String authenticate(String command, Map params, HttpSes responses.add(r); } + int totalEnabledCount = responses.size(); + if (!domainRequested && !isAuthenticated) { + List allProviders = _oauth2mgr.listOauthProviders(null, null, null); + for (OauthProviderVO domainProvider : allProviders) { + if (domainProvider.getDomainId() != null && domainProvider.isEnabled() + && OAuth2AuthManager.isPluginEnabledForDomain(domainProvider.getDomainId()) + && authenticatorPluginNames.contains(domainProvider.getProvider())) { + totalEnabledCount++; + } + } + } + ListResponse response = new ListResponse<>(); - response.setResponses(responses, resultList.size()); + response.setResponses(responses, totalEnabledCount); response.setResponseName(getCommandName()); setResponseObject(response); diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java index d2af4c24ce43..e9ef030aaab5 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.oauth2.api.command; +import java.util.Objects; + import com.cloud.api.ApiServlet; import com.cloud.domain.Domain; import com.cloud.user.User; @@ -48,7 +50,7 @@ import java.util.Map; import java.net.InetAddress; -import static org.apache.cloudstack.oauth2.OAuth2AuthManager.OAuth2IsPluginEnabled; +import org.apache.cloudstack.oauth2.OAuth2AuthManager; @APICommand(name = "oauthlogin", description = "Logs a user into the CloudStack after successful verification of OAuth secret code from the particular provider." + "A successful login attempt will generate a JSESSIONID cookie value that can be passed in subsequent Query command calls until the \"logout\" command has been issued or the session has expired.", @@ -120,9 +122,6 @@ public void execute() throws ServerApiException { @Override public String authenticate(String command, Map params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException { - if (!OAuth2IsPluginEnabled.value()) { - throw new CloudAuthenticationException("OAuth is not enabled in CloudStack, users cannot login using OAuth"); - } final String[] provider = (String[])params.get(ApiConstants.PROVIDER); final String[] emailArray = (String[])params.get(ApiConstants.EMAIL); final String[] secretCodeArray = (String[])params.get(ApiConstants.SECRET_CODE); @@ -130,15 +129,41 @@ public String authenticate(String command, Map params, HttpSes String oauthProvider = ((provider == null) ? null : provider[0]); String email = ((emailArray == null) ? null : emailArray[0]); String secretCode = ((secretCodeArray == null) ? null : secretCodeArray[0]); - if (StringUtils.isAnyEmpty(oauthProvider, email, secretCode)) { - throw new CloudAuthenticationException("OAuth provider, email, secretCode any of these cannot be null"); - } - Long domainId = getDomainIdFromParams(params, auditTrailSb, responseType); - final String[] domainName = (String[])params.get(ApiConstants.DOMAIN); - String domain = getDomainName(auditTrailSb, domainName); + try { + if (StringUtils.isAnyEmpty(oauthProvider, email, secretCode)) { + throw new CloudAuthenticationException("OAuth provider, email, secretCode any of these cannot be null"); + } + + Long domainId = getDomainIdFromParams(params, auditTrailSb, responseType); + final String[] domainName = (String[])params.get(ApiConstants.DOMAIN); + String domain = getDomainName(auditTrailSb, domainName); + + final Domain userDomain = _domainService.findDomainByIdOrPath(domainId, domain); + if (Objects.nonNull(userDomain)) { + domainId = userDomain.getId(); + } + + boolean oauthEnabled = OAuth2AuthManager.isPluginEnabledForDomain(domainId); + if (!oauthEnabled) { + logger.debug(String.format("OAuth is not enabled %s, users cannot login using OAuth", domainId == null ? "globally" : "in domain " + domainId)); + throw new CloudAuthenticationException(String.format( + "OAuth login is not enabled %s. Please contact your administrator.", + domainId == null ? "globally" : "for this domain")); + } + + return doOauthAuthentication(session, domainId, domain, email, params, remoteAddress, responseType, auditTrailSb); + } catch (final CloudAuthenticationException ex) { + throw toServerApiException(session, params, responseType, auditTrailSb, ex); + } + } - return doOauthAuthentication(session, domainId, domain, email, params, remoteAddress, responseType, auditTrailSb); + private ServerApiException toServerApiException(HttpSession session, Map params, String responseType, StringBuilder auditTrailSb, CloudAuthenticationException ex) { + ApiServlet.invalidateHttpSession(session, "fall through to API key,"); + String msg = ex.getMessage() != null ? ex.getMessage() : "failed to authenticate user via OAuth"; + auditTrailSb.append(" " + ApiErrorCode.ACCOUNT_ERROR + " " + msg); + String serializedResponse = _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), msg, params, responseType); + return new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, serializedResponse); } private String doOauthAuthentication(HttpSession session, Long domainId, String domain, String email, Map params, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb) { diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java index 8eb4493d76d8..79274ba904b1 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java @@ -26,6 +26,7 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.oauth2.OAuth2AuthManager; @@ -35,6 +36,8 @@ import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; +import com.cloud.api.ApiDBUtils; +import com.cloud.domain.Domain; import com.cloud.exception.ConcurrentOperationException; @APICommand(name = "registerOauthProvider", responseObject = SuccessResponse.class, description = "Register the OAuth2 provider in CloudStack", since = "4.19.0") @@ -59,6 +62,14 @@ public class RegisterOAuthProviderCmd extends BaseCmd { @Parameter(name = ApiConstants.REDIRECT_URI, type = CommandType.STRING, description = "Redirect URI pre-registered in the specific OAuth provider", required = true) private String redirectUri; + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "Domain ID for domain-specific OAuth provider. If not provided, registers as global provider", since = "4.23.0") + private Long domainId; + + @Parameter(name = ApiConstants.DOMAIN, type = CommandType.STRING, + description = "Domain path for domain-specific OAuth provider. Ignored when Domain ID is passed.", since = "4.23.0") + private String domainPath; + @Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL for OAuth initialization (only required for keycloak provider)") private String authorizeUrl; @@ -94,6 +105,14 @@ public String getRedirectUri() { return redirectUri; } + public Long getDomainId() { + return domainId; + } + + public String getDomainPath() { + return domainPath; + } + public String getAuthorizeUrl() { return authorizeUrl; } @@ -126,9 +145,10 @@ public void execute() throws ServerApiException, ConcurrentOperationException, E OauthProviderVO provider = _oauth2mgr.registerOauthProvider(this); + Domain domain = provider.getDomainId() != null ? ApiDBUtils.findDomainById(provider.getDomainId()) : null; OauthProviderResponse response = new OauthProviderResponse(provider.getUuid(), provider.getProvider(), provider.getDescription(), provider.getClientId(), provider.getSecretKey(), provider.getRedirectUri(), - provider.getAuthorizeUrl(), provider.getTokenUrl()); + provider.getAuthorizeUrl(), provider.getTokenUrl(), domain); response.setResponseName(getCommandName()); response.setObjectName(ApiConstants.OAUTH_PROVIDER); setResponseObject(response); diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java index a8b0604a9bba..f32c08e048eb 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java @@ -28,12 +28,16 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.auth.UserOAuth2Authenticator; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.oauth2.OAuth2AuthManager; import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import com.cloud.api.ApiDBUtils; +import com.cloud.domain.Domain; + @APICommand(name = "updateOauthProvider", description = "Updates the registered OAuth provider details", responseObject = OauthProviderResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0") public final class UpdateOAuthProviderCmd extends BaseCmd { @@ -66,6 +70,14 @@ public final class UpdateOAuthProviderCmd extends BaseCmd { @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "OAuth provider will be enabled or disabled based on this value") private Boolean enabled; + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "Domain ID to reassign this OAuth provider to. If not provided, the current domain assignment is kept.", since = "4.23.0") + private Long domainId; + + @Parameter(name = ApiConstants.DOMAIN, type = CommandType.STRING, + description = "Domain path to reassign this OAuth provider to. Ignored when Domain ID is passed. If neither is provided, the current domain assignment is kept.", since = "4.23.0") + private String domainPath; + @Inject OAuth2AuthManager _oauthMgr; @@ -105,6 +117,14 @@ public Boolean getEnabled() { return enabled; } + public Long getDomainId() { + return domainId; + } + + public String getDomainPath() { + return domainPath; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -128,9 +148,10 @@ public ApiCommandResourceType getApiResourceType() { public void execute() { OauthProviderVO result = _oauthMgr.updateOauthProvider(this); if (result != null) { + Domain domain = result.getDomainId() != null ? ApiDBUtils.findDomainById(result.getDomainId()) : null; OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(), result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri(), - result.getAuthorizeUrl(), result.getTokenUrl()); + result.getAuthorizeUrl(), result.getTokenUrl(), domain); List userOAuth2AuthenticatorPlugins = _oauthMgr.listUserOAuth2AuthenticationProviders(); List authenticatorPluginNames = new ArrayList<>(); @@ -138,7 +159,8 @@ public void execute() { String name = authenticator.getName(); authenticatorPluginNames.add(name); } - if (OAuth2AuthManager.OAuth2IsPluginEnabled.value() && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) { + boolean oauthEnabled = OAuth2AuthManager.isPluginEnabledForDomain(result.getDomainId()); + if (oauthEnabled && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) { r.setEnabled(true); } else { r.setEnabled(false); diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmd.java index b3d2d335ba25..d5c7455d0f33 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmd.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmd.java @@ -20,6 +20,9 @@ import java.util.List; import java.util.Map; +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.user.Account; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -34,13 +37,11 @@ import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.oauth2.OAuth2AuthManager; import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; -import org.apache.commons.lang.ArrayUtils; - -import com.cloud.api.response.ApiResponseSerializer; -import com.cloud.user.Account; +import org.apache.commons.lang3.ArrayUtils; @APICommand(name = "verifyOAuthCodeAndGetUser", description = "Verify the OAuth Code and fetch the corresponding user from provider", responseObject = OauthProviderResponse.class, entityType = {}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, @@ -58,6 +59,14 @@ public class VerifyOAuthCodeAndGetUserCmd extends BaseListCmd implements APIAuth @Parameter(name = ApiConstants.SECRET_CODE, type = CommandType.STRING, description = "Code that is provided by OAuth provider (Eg. google, github) after successful login") private String secretCode; + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "Domain ID for domain-specific OAuth provider lookup. If not provided, uses global provider", since = "4.23.0") + private Long domainId; + + @Parameter(name = ApiConstants.DOMAIN, type = CommandType.STRING, + description = "Domain path for domain-specific OAuth provider lookup. Ignored when Domain ID is passed.", since = "4.23.0") + private String domainPath; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -70,6 +79,10 @@ public String getSecretCode() { return secretCode; } + public Long getDomainId() { + return domainId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -97,8 +110,9 @@ public String authenticate(String command, Map params, HttpSes if (ArrayUtils.isNotEmpty(providerArray)) { provider = providerArray[0]; } + domainId = _oauth2mgr.resolveDomainId(params); - String email = _oauth2mgr.verifyCodeAndFetchEmail(secretCode, provider); + String email = _oauth2mgr.verifySecretCodeAndFetchEmail(secretCode, provider, domainId); if (email != null) { UserResponse response = new UserResponse(); response.setEmail(email); diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java index 289dc6650137..b363e13516bc 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java @@ -16,11 +16,14 @@ // under the License. package org.apache.cloudstack.oauth2.api.response; +import java.util.Objects; + import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.EntityReference; import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import com.cloud.domain.Domain; import com.cloud.serializer.Param; import com.google.gson.annotations.SerializedName; @@ -55,6 +58,18 @@ public class OauthProviderResponse extends BaseResponse { @Param(description = "Redirect URI registered in the OAuth provider") private String redirectUri; + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "UUID of the domain the provider belongs to (empty for global)", since = "4.23.0") + private String domainUuid; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "name of the domain the provider belongs to (empty for global)", since = "4.23.0") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_PATH) + @Param(description = "path of the domain the provider belongs to (empty for global)", since = "4.23.0") + private String domainPath; + @SerializedName(ApiConstants.AUTHORIZE_URL) @Param(description = "Authorize URL registered in the OAuth provider") private String authorizeUrl; @@ -67,7 +82,7 @@ public class OauthProviderResponse extends BaseResponse { @Param(description = "Whether the OAuth provider is enabled or not") private boolean enabled; - public OauthProviderResponse(String id, String provider, String description, String clientId, String secretKey, String redirectUri, String authorizeUrl, String tokenUrl) { + public OauthProviderResponse(String id, String provider, String description, String clientId, String secretKey, String redirectUri, String authorizeUrl, String tokenUrl, Domain domain) { this.id = id; this.provider = provider; this.name = provider; @@ -77,6 +92,19 @@ public OauthProviderResponse(String id, String provider, String description, Str this.redirectUri = redirectUri; this.authorizeUrl = authorizeUrl; this.tokenUrl = tokenUrl; + if (Objects.nonNull(domain)) { + this.domainUuid = domain.getUuid(); + this.domainName = domain.getName(); + this.domainPath = prettifyDomainPath(domain.getPath()); + } + } + + private static String prettifyDomainPath(String path) { + if (path == null) { + return null; + } + String trimmed = path.endsWith("/") ? path.substring(0, path.length() - 1) : path; + return "ROOT" + trimmed; } public String getId() { @@ -128,6 +156,30 @@ public void setRedirectUri(String redirectUri) { this.redirectUri = redirectUri; } + public String getDomainUuid() { + return domainUuid; + } + + public void setDomainUuid(String domainUuid) { + this.domainUuid = domainUuid; + } + + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public String getDomainPath() { + return domainPath; + } + + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; + } + public String getAuthorizeUrl() { return authorizeUrl; } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java index 31738ac75a0f..629abb0bb2ec 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java @@ -19,8 +19,16 @@ import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import java.util.List; + public interface OauthProviderDao extends GenericDao { - public OauthProviderVO findByProvider(String provider); + public OauthProviderVO findByProviderAndDomain(String provider, Long domainId); + + public List listByDomainIncludingGlobal(Long domainId); + + public List listByDomain(Long domainId); + + public OauthProviderVO findByProviderAndDomainWithGlobalFallback(String provider, Long domainId); } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java index 27eea4d22a6b..eecff0f4f506 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java @@ -22,23 +22,54 @@ import com.cloud.utils.db.SearchCriteria; import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import java.util.List; +import java.util.Objects; + public class OauthProviderDaoImpl extends GenericDaoBase implements OauthProviderDao { - private final SearchBuilder oauthProviderSearchByName; + private final SearchBuilder oauthProviderSearchByProviderAndDomain; public OauthProviderDaoImpl() { super(); - oauthProviderSearchByName = createSearchBuilder(); - oauthProviderSearchByName.and("provider", oauthProviderSearchByName.entity().getProvider(), SearchCriteria.Op.EQ); - oauthProviderSearchByName.done(); + oauthProviderSearchByProviderAndDomain = createSearchBuilder(); + oauthProviderSearchByProviderAndDomain.and("provider", oauthProviderSearchByProviderAndDomain.entity().getProvider(), SearchCriteria.Op.EQ); + oauthProviderSearchByProviderAndDomain.and("domainId", oauthProviderSearchByProviderAndDomain.entity().getDomainId(), SearchCriteria.Op.EQ); + oauthProviderSearchByProviderAndDomain.done(); } @Override - public OauthProviderVO findByProvider(String provider) { - SearchCriteria sc = oauthProviderSearchByName.create(); + public OauthProviderVO findByProviderAndDomain(String provider, Long domainId) { + SearchCriteria sc = oauthProviderSearchByProviderAndDomain.create(); sc.setParameters("provider", provider); - + sc.setParameters("domainId", domainId); return findOneBy(sc); } + + @Override + public List listByDomainIncludingGlobal(Long domainId) { + SearchCriteria sc = createSearchCriteria(); + sc.addOr("domainId", SearchCriteria.Op.EQ, domainId); + sc.addOr("domainId", SearchCriteria.Op.NULL); + return listBy(sc); + } + + @Override + public List listByDomain(Long domainId) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + return listBy(sc); + } + + @Override + public OauthProviderVO findByProviderAndDomainWithGlobalFallback(String provider, Long domainId) { + OauthProviderVO providerVO = null; + if (Objects.nonNull(domainId)) { + providerVO = findByProviderAndDomain(provider, domainId); + } + if (Objects.isNull(providerVO)) { + providerVO = findByProviderAndDomain(provider, null); + } + return providerVO; + } } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java index 4d426181a94b..7413d22b2fa6 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java @@ -56,17 +56,27 @@ public String getDescription() { @Override public boolean verifyUser(String email, String secretCode) { + return verifyUser(email, secretCode, null); + } + + @Override + public String verifySecretCodeAndFetchEmail(String secretCode) { + return verifySecretCodeAndFetchEmail(secretCode, null); + } + + @Override + public boolean verifyUser(String email, String secretCode, Long domainId) { if (StringUtils.isAnyEmpty(email, secretCode)) { throw new CloudRuntimeException(String.format("Either email or secretcode should not be null/empty")); } - OauthProviderVO providerVO = _oauthProviderDao.findByProvider(getName()); + OauthProviderVO providerVO = _oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId); if (providerVO == null) { throw new CloudRuntimeException("Github provider is not registered, so user cannot be verified"); } - String verifiedEmail = getUserEmailAddress(); - if (verifiedEmail == null || !email.equals(verifiedEmail)) { + String verifiedEmail = verifySecretCodeAndFetchEmail(secretCode, domainId); + if (StringUtils.isEmpty(verifiedEmail) || !email.equals(verifiedEmail)) { throw new CloudRuntimeException("Unable to verify the email address with the provided secret"); } @@ -76,16 +86,19 @@ public boolean verifyUser(String email, String secretCode) { } @Override - public String verifyCodeAndFetchEmail(String secretCode) { - String accessToken = getAccessToken(secretCode); - if (accessToken == null) { + public String verifySecretCodeAndFetchEmail(String secretCode, Long domainId) { + String accessToken = getAccessToken(secretCode, domainId); + if (StringUtils.isEmpty(accessToken)) { return null; } return getUserEmailAddress(); } - protected String getAccessToken(String secretCode) throws CloudRuntimeException { - OauthProviderVO githubProvider = _oauthProviderDao.findByProvider(getName()); + protected String getAccessToken(String secretCode, Long domainId) throws CloudRuntimeException { + if (StringUtils.isNotEmpty(accessToken)) { + return accessToken; + } + OauthProviderVO githubProvider = _oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId); String tokenUrl = "https://github.com/login/oauth/access_token"; String generatedAccessToken = null; try { @@ -131,7 +144,7 @@ protected String getAccessToken(String secretCode) throws CloudRuntimeException } public String getUserEmailAddress() throws CloudRuntimeException { - if (accessToken == null) { + if (StringUtils.isEmpty(accessToken)) { throw new CloudRuntimeException("Access Token not found to fetch the email address"); } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java index 885930181c91..3b37f0f6e0b8 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java @@ -60,16 +60,36 @@ public String getDescription() { @Override public boolean verifyUser(String email, String secretCode) { + return verifyUser(email, secretCode, null); + } + + @Override + public String verifySecretCodeAndFetchEmail(String secretCode) { + return verifySecretCodeAndFetchEmail(secretCode, null); + } + + protected void clearAccessAndRefreshTokens() { + accessToken = null; + refreshToken = null; + } + + @Override + public String getUserEmailAddress() throws CloudRuntimeException { + return null; + } + + @Override + public boolean verifyUser(String email, String secretCode, Long domainId) { if (StringUtils.isAnyEmpty(email, secretCode)) { throw new CloudAuthenticationException("Either email or secret code should not be null/empty"); } - OauthProviderVO providerVO = _oauthProviderDao.findByProvider(getName()); + OauthProviderVO providerVO = _oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId); if (providerVO == null) { throw new CloudAuthenticationException("Google provider is not registered, so user cannot be verified"); } - String verifiedEmail = verifyCodeAndFetchEmail(secretCode); + String verifiedEmail = verifySecretCodeAndFetchEmail(secretCode, domainId); if (verifiedEmail == null || !email.equals(verifiedEmail)) { throw new CloudRuntimeException("Unable to verify the email address with the provided secret"); } @@ -79,11 +99,11 @@ public boolean verifyUser(String email, String secretCode) { } @Override - public String verifyCodeAndFetchEmail(String secretCode) { - OauthProviderVO googleProvider = _oauthProviderDao.findByProvider(getName()); - String clientId = googleProvider.getClientId(); - String secret = googleProvider.getSecretKey(); - String redirectURI = googleProvider.getRedirectUri(); + public String verifySecretCodeAndFetchEmail(String secretCode, Long domainId) { + OauthProviderVO provider = _oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId); + String clientId = provider.getClientId(); + String secret = provider.getSecretKey(); + String redirectURI = provider.getRedirectUri(); GoogleClientSecrets clientSecrets = new GoogleClientSecrets() .setWeb(new GoogleClientSecrets.Details() .setClientId(clientId) @@ -129,13 +149,4 @@ public String verifyCodeAndFetchEmail(String secretCode) { return userinfo.getEmail(); } - protected void clearAccessAndRefreshTokens() { - accessToken = null; - refreshToken = null; - } - - @Override - public String getUserEmailAddress() throws CloudRuntimeException { - return null; - } } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java index 3f537b1984d0..2a625c6f7570 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java @@ -81,16 +81,21 @@ public String getDescription() { @Override public boolean verifyUser(String email, String secretCode) { + return verifyUser(email, secretCode, null); + } + + @Override + public boolean verifyUser(String email, String secretCode, Long domainId) { if (StringUtils.isAnyEmpty(email, secretCode)) { throw new CloudAuthenticationException("Either email or secret code should not be null/empty"); } - OauthProviderVO providerVO = oauthProviderDao.findByProvider(getName()); + OauthProviderVO providerVO = oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId); if (providerVO == null) { throw new CloudAuthenticationException("Keycloak provider is not registered, so user cannot be verified"); } - String verifiedEmail = verifyCodeAndFetchEmail(secretCode); + String verifiedEmail = verifySecretCodeAndFetchEmail(secretCode, domainId); if (StringUtils.isBlank(verifiedEmail) || !email.equals(verifiedEmail)) { throw new CloudRuntimeException("Unable to verify the email address with the provided secret"); } @@ -100,8 +105,13 @@ public boolean verifyUser(String email, String secretCode) { } @Override - public String verifyCodeAndFetchEmail(String secretCode) { - OauthProviderVO provider = oauthProviderDao.findByProvider(getName()); + public String verifySecretCodeAndFetchEmail(String secretCode) { + return verifySecretCodeAndFetchEmail(secretCode, null); + } + + @Override + public String verifySecretCodeAndFetchEmail(String secretCode, Long domainId) { + OauthProviderVO provider = oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId); if (provider == null) { throw new CloudAuthenticationException("Keycloak provider is not registered, so user cannot be verified"); } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java index 54d667bc9143..8aa9006e763e 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java @@ -57,6 +57,9 @@ public class OauthProviderVO implements Identity, InternalIdentity { @Column(name = "redirect_uri") private String redirectUri; + @Column(name = "domain_id") + private Long domainId; + @Column(name = "authorize_url") private String authorizeUrl; @@ -142,6 +145,14 @@ public void setSecretKey(String secretKey) { this.secretKey = secretKey; } + public Long getDomainId() { + return domainId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + public boolean isEnabled() { return enabled; } diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImplTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImplTest.java index 3fd5636102ce..e3e8f7594b37 100644 --- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImplTest.java +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImplTest.java @@ -19,7 +19,13 @@ package org.apache.cloudstack.oauth2; +import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.user.DomainService; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.framework.messagebus.MessageBus; +import org.apache.cloudstack.framework.messagebus.MessageSubscriber; import org.apache.cloudstack.oauth2.api.command.DeleteOAuthProviderCmd; import org.apache.cloudstack.oauth2.api.command.RegisterOAuthProviderCmd; import org.apache.cloudstack.oauth2.api.command.UpdateOAuthProviderCmd; @@ -36,12 +42,17 @@ import org.mockito.Spy; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class OAuth2AuthManagerImplTest { @@ -53,6 +64,12 @@ public class OAuth2AuthManagerImplTest { @Mock OauthProviderDao _oauthProviderDao; + @Mock + DomainService _domainService; + + @Mock + MessageBus _messageBus; + AutoCloseable closeable; @Before public void setUp() { @@ -66,7 +83,7 @@ public void tearDown() throws Exception { @Test public void testRegisterOauthProvider() { - when(_authManager.isOAuthPluginEnabled()).thenReturn(false); + when(_authManager.isOAuthPluginEnabled(Mockito.nullable(Long.class))).thenReturn(false); RegisterOAuthProviderCmd cmd = Mockito.mock(RegisterOAuthProviderCmd.class); try { _authManager.registerOauthProvider(cmd); @@ -76,25 +93,27 @@ public void testRegisterOauthProvider() { } // Test when provider is already registered - when(_authManager.isOAuthPluginEnabled()).thenReturn(true); + when(_authManager.isOAuthPluginEnabled(Mockito.nullable(Long.class))).thenReturn(true); OauthProviderVO providerVO = new OauthProviderVO(); providerVO.setProvider("testProvider"); - when(_authManager._oauthProviderDao.findByProvider(Mockito.anyString())).thenReturn(providerVO); + when(_authManager._oauthProviderDao.findByProviderAndDomain(Mockito.anyString(), Mockito.isNull())).thenReturn(providerVO); when(cmd.getProvider()).thenReturn("testProvider"); + when(cmd.getDomainId()).thenReturn(null); try { _authManager.registerOauthProvider(cmd); Assert.fail("Expected CloudRuntimeException was not thrown"); } catch (CloudRuntimeException e) { - assertEquals("Provider with the name testProvider is already registered", e.getMessage()); + assertEquals("Global provider with the name testProvider is already registered", e.getMessage()); } // Test when provider is github and secret key is not null when(cmd.getSecretKey()).thenReturn("testSecretKey"); providerVO = null; - when(_authManager._oauthProviderDao.findByProvider(Mockito.anyString())).thenReturn(providerVO); + when(_authManager._oauthProviderDao.findByProviderAndDomain(Mockito.anyString(), Mockito.isNull())).thenReturn(providerVO); OauthProviderVO savedProviderVO = new OauthProviderVO(); when(cmd.getProvider()).thenReturn("github"); + when(cmd.getDomainId()).thenReturn(null); when(_authManager._oauthProviderDao.persist(Mockito.any(OauthProviderVO.class))).thenReturn(savedProviderVO); OauthProviderVO result = _authManager.registerOauthProvider(cmd); assertEquals("github", result.getProvider()); @@ -140,6 +159,115 @@ public void testUpdateOauthProvider() { assertEquals(secretKey, result.getSecretKey()); } + @Test + public void testUpdateOauthProviderReassignsDomain() { + Long id = 5L; + Long oldDomainId = 10L; + Long newDomainId = 20L; + + UpdateOAuthProviderCmd cmd = Mockito.mock(UpdateOAuthProviderCmd.class); + when(cmd.getId()).thenReturn(id); + when(cmd.getDomainId()).thenReturn(newDomainId); + + OauthProviderVO providerVO = new OauthProviderVO(); + providerVO.setProvider("github"); + providerVO.setDomainId(oldDomainId); + when(_oauthProviderDao.findById(id)).thenReturn(providerVO); + + Domain newDomain = Mockito.mock(Domain.class); + when(newDomain.getId()).thenReturn(newDomainId); + when(_domainService.getDomain(Mockito.anyString())).thenReturn(newDomain); + Mockito.doReturn(newDomainId).when(_authManager).resolveDomainIdFromIdOrPath(newDomainId, null); + when(_oauthProviderDao.findByProviderAndDomain("github", newDomainId)).thenReturn(null); + when(_oauthProviderDao.update(Mockito.eq(id), Mockito.any(OauthProviderVO.class))).thenReturn(true); + when(_oauthProviderDao.findById(id)).thenReturn(providerVO); + + OauthProviderVO result = _authManager.updateOauthProvider(cmd); + assertEquals(newDomainId, result.getDomainId()); + } + + @Test + public void testUpdateOauthProviderRejectsDuplicateAtTargetDomain() { + Long id = 5L; + Long oldDomainId = 10L; + Long newDomainId = 20L; + + UpdateOAuthProviderCmd cmd = Mockito.mock(UpdateOAuthProviderCmd.class); + when(cmd.getId()).thenReturn(id); + when(cmd.getDomainId()).thenReturn(newDomainId); + + OauthProviderVO providerVO = new OauthProviderVO(); + providerVO.setProvider("github"); + providerVO.setDomainId(oldDomainId); + when(_oauthProviderDao.findById(id)).thenReturn(providerVO); + + Mockito.doReturn(newDomainId).when(_authManager).resolveDomainIdFromIdOrPath(newDomainId, null); + OauthProviderVO collision = new OauthProviderVO(); + collision.setProvider("github"); + collision.setDomainId(newDomainId); + when(_oauthProviderDao.findByProviderAndDomain("github", newDomainId)).thenReturn(collision); + + try { + _authManager.updateOauthProvider(cmd); + Assert.fail("Expected CloudRuntimeException for duplicate at target domain"); + } catch (CloudRuntimeException e) { + assertTrue(e.getMessage().contains("already registered")); + } + } + + @Test + public void testRegisterOauthProviderForRootDomainTreatedAsGlobal() { + RegisterOAuthProviderCmd cmd = Mockito.mock(RegisterOAuthProviderCmd.class); + when(cmd.getProvider()).thenReturn("github"); + when(cmd.getDomainId()).thenReturn(com.cloud.domain.Domain.ROOT_DOMAIN); + when(cmd.getSecretKey()).thenReturn("secret"); + when(cmd.getClientId()).thenReturn("clientId"); + when(cmd.getRedirectUri()).thenReturn("https://redirect"); + + // global check must be consulted (domainId resolves to null), not the ROOT domain scope + when(_authManager.isOAuthPluginEnabled(Mockito.isNull())).thenReturn(true); + when(_oauthProviderDao.findByProviderAndDomain("github", null)).thenReturn(null); + when(_oauthProviderDao.persist(Mockito.any(OauthProviderVO.class))).thenAnswer(i -> i.getArgument(0)); + + OauthProviderVO result = _authManager.registerOauthProvider(cmd); + assertNull(result.getDomainId()); + Mockito.verify(_oauthProviderDao).findByProviderAndDomain("github", null); + } + + @Test + public void testNormalizeGlobalScopeMapsRootToNull() { + assertNull(_authManager.normalizeGlobalScope(com.cloud.domain.Domain.ROOT_DOMAIN)); + assertNull(_authManager.normalizeGlobalScope(null)); + assertEquals(Long.valueOf(42L), _authManager.normalizeGlobalScope(42L)); + } + + @Test + public void testUpdateOauthProviderRejectsEnableWhenPluginDisabledAtScope() { + Long id = 7L; + Long domainId = 42L; + + UpdateOAuthProviderCmd cmd = Mockito.mock(UpdateOAuthProviderCmd.class); + when(cmd.getId()).thenReturn(id); + when(cmd.getEnabled()).thenReturn(true); + + OauthProviderVO providerVO = new OauthProviderVO(); + providerVO.setProvider("github"); + providerVO.setDomainId(domainId); + providerVO.setEnabled(false); + + when(_oauthProviderDao.findById(id)).thenReturn(providerVO); + Mockito.doReturn(false).when(_authManager).isOAuthPluginEnabled(domainId); + + try { + _authManager.updateOauthProvider(cmd); + Assert.fail("Expected CloudRuntimeException when enabling provider while oauth2.enabled is false at scope"); + } catch (CloudRuntimeException e) { + assertTrue(e.getMessage().contains("OAuth plugin is not enabled")); + } + + Mockito.verify(_oauthProviderDao, Mockito.never()).update(Mockito.eq(id), Mockito.any(OauthProviderVO.class)); + } + @Test public void testListOauthProviders() { String uuid = "1234-5678-9101"; @@ -150,20 +278,32 @@ public void testListOauthProviders() { // Test when uuid is not null when(_oauthProviderDao.findByUuid(uuid)).thenReturn(providerVO); - List result = _authManager.listOauthProviders(null, uuid); + List result = _authManager.listOauthProviders(null, uuid, null); assertEquals(providerList, result); // Test when provider is not blank - when(_oauthProviderDao.findByProvider(provider)).thenReturn(providerVO); - result = _authManager.listOauthProviders(provider, null); + when(_oauthProviderDao.findByProviderAndDomain(provider, null)).thenReturn(providerVO); + result = _authManager.listOauthProviders(provider, null, null); assertEquals(providerList, result); // Test when both uuid and provider are null when(_oauthProviderDao.listAll()).thenReturn(providerList); - result = _authManager.listOauthProviders(null, null); + result = _authManager.listOauthProviders(null, null, null); assertEquals(providerList, result); } + @Test + public void testDeleteOauthProviderHardDeletes() { + Long providerId = 42L; + when(_oauthProviderDao.expunge(providerId)).thenReturn(true); + + boolean result = _authManager.deleteOauthProvider(providerId); + + assertTrue(result); + Mockito.verify(_oauthProviderDao).expunge(providerId); + Mockito.verify(_oauthProviderDao, Mockito.never()).remove(Mockito.anyLong()); + } + @Test public void testGetCommands() { List> expectedCmdList = new ArrayList<>(); @@ -178,14 +318,362 @@ public void testGetCommands() { @Test public void testStart() { - when(_authManager.isOAuthPluginEnabled()).thenReturn(true); + when(_authManager.isOAuthPluginEnabled(Mockito.nullable(Long.class))).thenReturn(true); doNothing().when(_authManager).initializeUserOAuth2AuthenticationProvidersMap(); boolean result = _authManager.start(); assertTrue(result); - when(_authManager.isOAuthPluginEnabled()).thenReturn(false); + when(_authManager.isOAuthPluginEnabled(Mockito.nullable(Long.class))).thenReturn(false); result = _authManager.start(); assertTrue(result); } + @Test + public void testRegisterOauthProviderWithDomain() { + when(_authManager.isOAuthPluginEnabled(Mockito.nullable(Long.class))).thenReturn(true); + RegisterOAuthProviderCmd cmd = Mockito.mock(RegisterOAuthProviderCmd.class); + when(cmd.getProvider()).thenReturn("github"); + when(cmd.getDomainId()).thenReturn(5L); + when(cmd.getSecretKey()).thenReturn("secret"); + when(cmd.getClientId()).thenReturn("clientId"); + when(cmd.getRedirectUri()).thenReturn("https://redirect"); + + // No existing provider for this domain + when(_oauthProviderDao.findByProviderAndDomain("github", 5L)).thenReturn(null); + when(_oauthProviderDao.persist(Mockito.any(OauthProviderVO.class))).thenAnswer(i -> i.getArgument(0)); + + OauthProviderVO result = _authManager.registerOauthProvider(cmd); + assertEquals("github", result.getProvider()); + assertEquals(Long.valueOf(5L), result.getDomainId()); + } + + @Test + public void testRegisterOauthProviderDuplicateForDomain() { + when(_authManager.isOAuthPluginEnabled(Mockito.nullable(Long.class))).thenReturn(true); + RegisterOAuthProviderCmd cmd = Mockito.mock(RegisterOAuthProviderCmd.class); + when(cmd.getProvider()).thenReturn("github"); + when(cmd.getDomainId()).thenReturn(5L); + + OauthProviderVO existing = new OauthProviderVO(); + existing.setProvider("github"); + existing.setDomainId(5L); + when(_oauthProviderDao.findByProviderAndDomain("github", 5L)).thenReturn(existing); + + try { + _authManager.registerOauthProvider(cmd); + Assert.fail("Expected CloudRuntimeException was not thrown"); + } catch (CloudRuntimeException e) { + assertEquals("Provider with the name github is already registered for domain 5", e.getMessage()); + } + } + + @Test + public void testListOauthProvidersWithDomainId() { + Long domainId = 5L; + OauthProviderVO globalProvider = new OauthProviderVO(); + globalProvider.setProvider("google"); + OauthProviderVO domainProvider = new OauthProviderVO(); + domainProvider.setProvider("github"); + domainProvider.setDomainId(domainId); + List providers = Arrays.asList(globalProvider, domainProvider); + + when(_oauthProviderDao.listByDomainIncludingGlobal(domainId)).thenReturn(providers); + List result = _authManager.listOauthProviders(null, null, domainId); + assertEquals(2, result.size()); + } + + @Test + public void testListOauthProvidersByProviderAndDomain() { + Long domainId = 5L; + OauthProviderVO domainProvider = new OauthProviderVO(); + domainProvider.setProvider("github"); + domainProvider.setDomainId(domainId); + + when(_oauthProviderDao.findByProviderAndDomain("github", domainId)).thenReturn(domainProvider); + List result = _authManager.listOauthProviders("github", null, domainId); + assertEquals(1, result.size()); + assertEquals("github", result.get(0).getProvider()); + assertEquals(Long.valueOf(5L), result.get(0).getDomainId()); + } + + @Test + public void testResolveDomainIdFromDomainUuid() { + Map params = new HashMap<>(); + params.put(ApiConstants.DOMAIN_ID, new String[]{"test-uuid-123"}); + + Domain domain = Mockito.mock(Domain.class); + when(domain.getId()).thenReturn(10L); + when(_domainService.getDomain("test-uuid-123")).thenReturn(domain); + + Long result = _authManager.resolveDomainId(params); + assertEquals(Long.valueOf(10L), result); + } + + @Test + public void testResolveDomainIdGlobalFilter() { + Map params = new HashMap<>(); + params.put(ApiConstants.DOMAIN_ID, new String[]{"-1"}); + + Long result = _authManager.resolveDomainId(params); + assertEquals(Long.valueOf(-1L), result); + } + + @Test + public void testResolveDomainIdFromDomainPath() { + Map params = new HashMap<>(); + params.put(ApiConstants.DOMAIN, new String[]{"ROOT/child"}); + + Domain domain = Mockito.mock(Domain.class); + when(domain.getId()).thenReturn(20L); + when(_domainService.findDomainByIdOrPath(null, "/ROOT/child/")).thenReturn(domain); + + Long result = _authManager.resolveDomainId(params); + assertEquals(Long.valueOf(20L), result); + } + + @Test + public void testResolveDomainIdFromDomainPathWithSlashes() { + Map params = new HashMap<>(); + params.put(ApiConstants.DOMAIN, new String[]{"/ROOT/child/"}); + + Domain domain = Mockito.mock(Domain.class); + when(domain.getId()).thenReturn(20L); + when(_domainService.findDomainByIdOrPath(null, "/ROOT/child/")).thenReturn(domain); + + Long result = _authManager.resolveDomainId(params); + assertEquals(Long.valueOf(20L), result); + } + + @Test + public void testResolveDomainIdReturnsNullWhenNotFound() { + Map params = new HashMap<>(); + params.put(ApiConstants.DOMAIN_ID, new String[]{"nonexistent-uuid"}); + + when(_domainService.getDomain("nonexistent-uuid")).thenReturn(null); + + Long result = _authManager.resolveDomainId(params); + assertNull(result); + } + + @Test + public void testResolveDomainIdReturnsNullForEmptyParams() { + Map params = new HashMap<>(); + Long result = _authManager.resolveDomainId(params); + assertNull(result); + } + + @Test + public void testResolveDomainIdPrefersUuidOverPath() { + Map params = new HashMap<>(); + params.put(ApiConstants.DOMAIN_ID, new String[]{"test-uuid"}); + params.put(ApiConstants.DOMAIN, new String[]{"/ROOT/child/"}); + + Domain domain = Mockito.mock(Domain.class); + when(domain.getId()).thenReturn(10L); + when(_domainService.getDomain("test-uuid")).thenReturn(domain); + + Long result = _authManager.resolveDomainId(params); + assertEquals(Long.valueOf(10L), result); + } + + @Test + public void testResolveDomainIdFallsBackToPathWhenUuidNotFound() { + Map params = new HashMap<>(); + params.put(ApiConstants.DOMAIN_ID, new String[]{"bad-uuid"}); + params.put(ApiConstants.DOMAIN, new String[]{"/ROOT/"}); + + when(_domainService.getDomain("bad-uuid")).thenReturn(null); + Domain domain = Mockito.mock(Domain.class); + when(domain.getId()).thenReturn(1L); + when(_domainService.findDomainByIdOrPath(null, "/ROOT/")).thenReturn(domain); + + Long result = _authManager.resolveDomainId(params); + assertEquals(Long.valueOf(1L), result); + } + + @Test + public void testUpdateOauthProviderNotFound() { + UpdateOAuthProviderCmd cmd = Mockito.mock(UpdateOAuthProviderCmd.class); + when(cmd.getId()).thenReturn(999L); + when(_oauthProviderDao.findById(999L)).thenReturn(null); + + try { + _authManager.updateOauthProvider(cmd); + Assert.fail("Expected CloudRuntimeException was not thrown"); + } catch (CloudRuntimeException e) { + assertEquals("Provider with the given id is not there", e.getMessage()); + } + } + + @Test + public void testGetUserOAuth2AuthenticationProviderEmptyName() { + try { + _authManager.getUserOAuth2AuthenticationProvider(""); + Assert.fail("Expected CloudRuntimeException was not thrown"); + } catch (CloudRuntimeException e) { + assertEquals("OAuth2 authentication provider name is empty", e.getMessage()); + } + } + + @Test + public void testGetUserOAuth2AuthenticationProviderNotFound() { + try { + _authManager.getUserOAuth2AuthenticationProvider("nonexistent"); + Assert.fail("Expected CloudRuntimeException was not thrown"); + } catch (CloudRuntimeException e) { + assertTrue(e.getMessage().contains("nonexistent")); + } + } + + // Multiple-domain OAuth tests + + @Test + public void testSameProviderRegisteredInTwoDifferentDomains() { + when(_authManager.isOAuthPluginEnabled(Mockito.nullable(Long.class))).thenReturn(true); + + // Register github for domain 5 + RegisterOAuthProviderCmd cmd1 = Mockito.mock(RegisterOAuthProviderCmd.class); + when(cmd1.getProvider()).thenReturn("github"); + when(cmd1.getDomainId()).thenReturn(5L); + when(cmd1.getSecretKey()).thenReturn("secret1"); + when(_oauthProviderDao.findByProviderAndDomain("github", 5L)).thenReturn(null); + when(_oauthProviderDao.persist(Mockito.any(OauthProviderVO.class))).thenAnswer(i -> i.getArgument(0)); + + OauthProviderVO result1 = _authManager.registerOauthProvider(cmd1); + assertEquals("github", result1.getProvider()); + assertEquals(Long.valueOf(5L), result1.getDomainId()); + + // Register github for domain 10 — should succeed independently + RegisterOAuthProviderCmd cmd2 = Mockito.mock(RegisterOAuthProviderCmd.class); + when(cmd2.getProvider()).thenReturn("github"); + when(cmd2.getDomainId()).thenReturn(10L); + when(cmd2.getSecretKey()).thenReturn("secret2"); + when(_oauthProviderDao.findByProviderAndDomain("github", 10L)).thenReturn(null); + + OauthProviderVO result2 = _authManager.registerOauthProvider(cmd2); + assertEquals("github", result2.getProvider()); + assertEquals(Long.valueOf(10L), result2.getDomainId()); + } + + @Test + public void testSameProviderRegisteredGloballyAndForDomain() { + when(_authManager.isOAuthPluginEnabled(Mockito.nullable(Long.class))).thenReturn(true); + + // Global registration (domainId = null) + RegisterOAuthProviderCmd globalCmd = Mockito.mock(RegisterOAuthProviderCmd.class); + when(globalCmd.getProvider()).thenReturn("google"); + when(globalCmd.getDomainId()).thenReturn(null); + when(_oauthProviderDao.findByProviderAndDomain("google", null)).thenReturn(null); + when(_oauthProviderDao.persist(Mockito.any(OauthProviderVO.class))).thenAnswer(i -> i.getArgument(0)); + + OauthProviderVO globalResult = _authManager.registerOauthProvider(globalCmd); + assertNull(globalResult.getDomainId()); + + // Domain-specific registration for same provider — should succeed (different scope) + RegisterOAuthProviderCmd domainCmd = Mockito.mock(RegisterOAuthProviderCmd.class); + when(domainCmd.getProvider()).thenReturn("google"); + when(domainCmd.getDomainId()).thenReturn(7L); + when(_oauthProviderDao.findByProviderAndDomain("google", 7L)).thenReturn(null); + + OauthProviderVO domainResult = _authManager.registerOauthProvider(domainCmd); + assertEquals(Long.valueOf(7L), domainResult.getDomainId()); + } + + @Test + public void testListOauthProvidersForDomainIncludesGlobalProviders() { + Long domainId = 5L; + OauthProviderVO globalGoogle = new OauthProviderVO(); + globalGoogle.setProvider("google"); + // domainId is null — global + + OauthProviderVO domainGithub = new OauthProviderVO(); + domainGithub.setProvider("github"); + domainGithub.setDomainId(domainId); + + OauthProviderVO otherDomainGoogle = new OauthProviderVO(); + otherDomainGoogle.setProvider("google"); + otherDomainGoogle.setDomainId(10L); + + // listByDomainIncludingGlobal returns providers for domain 5 + global (not domain 10) + when(_oauthProviderDao.listByDomainIncludingGlobal(domainId)) + .thenReturn(Arrays.asList(globalGoogle, domainGithub)); + + List result = _authManager.listOauthProviders(null, null, domainId); + assertEquals(2, result.size()); + assertTrue(result.stream().anyMatch(p -> p.getDomainId() == null)); // global included + assertTrue(result.stream().anyMatch(p -> Long.valueOf(5L).equals(p.getDomainId()))); // domain-specific included + assertTrue(result.stream().noneMatch(p -> Long.valueOf(10L).equals(p.getDomainId()))); // other domain excluded + } + + @Test + public void testListAllProvidersAcrossAllDomains() { + OauthProviderVO global = new OauthProviderVO(); + global.setProvider("google"); + + OauthProviderVO domain5 = new OauthProviderVO(); + domain5.setProvider("github"); + domain5.setDomainId(5L); + + OauthProviderVO domain10 = new OauthProviderVO(); + domain10.setProvider("google"); + domain10.setDomainId(10L); + + when(_oauthProviderDao.listAll()).thenReturn(Arrays.asList(global, domain5, domain10)); + + List result = _authManager.listOauthProviders(null, null, null); + assertEquals(3, result.size()); + } + + @Test + public void testDuplicateGlobalProviderRejected() { + when(_authManager.isOAuthPluginEnabled(Mockito.nullable(Long.class))).thenReturn(true); + RegisterOAuthProviderCmd cmd = Mockito.mock(RegisterOAuthProviderCmd.class); + when(cmd.getProvider()).thenReturn("google"); + when(cmd.getDomainId()).thenReturn(null); + + OauthProviderVO existing = new OauthProviderVO(); + existing.setProvider("google"); + when(_oauthProviderDao.findByProviderAndDomain("google", null)).thenReturn(existing); + + try { + _authManager.registerOauthProvider(cmd); + Assert.fail("Expected CloudRuntimeException was not thrown"); + } catch (CloudRuntimeException e) { + assertEquals("Global provider with the name google is already registered", e.getMessage()); + } + } + + @Test + public void testDomainDeletionCleansUpOAuthProviders() { + Long domainId = 42L; + + OauthProviderVO provider1 = new OauthProviderVO(); + provider1.setProvider("github"); + provider1.setDomainId(domainId); + + OauthProviderVO provider2 = new OauthProviderVO(); + provider2.setProvider("google"); + provider2.setDomainId(domainId); + + when(_oauthProviderDao.listByDomain(domainId)).thenReturn(Arrays.asList(provider1, provider2)); + when(_oauthProviderDao.expunge(Mockito.anyLong())).thenReturn(true); + + // Capture the subscriber registered during start() + doNothing().when(_authManager).initializeUserOAuth2AuthenticationProvidersMap(); + Mockito.doAnswer(invocation -> { + String subject = invocation.getArgument(0); + MessageSubscriber subscriber = invocation.getArgument(1); + // Simulate domain removal event + DomainVO domain = Mockito.mock(DomainVO.class); + when(domain.getId()).thenReturn(domainId); + subscriber.onPublishMessage("", subject, domain); + return null; + }).when(_messageBus).subscribe(Mockito.eq(com.cloud.user.DomainManager.MESSAGE_PRE_REMOVE_DOMAIN_EVENT), Mockito.any()); + + _authManager.start(); + + verify(_oauthProviderDao).listByDomain(domainId); + verify(_oauthProviderDao, Mockito.times(2)).expunge(Mockito.anyLong()); + } + } diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticatorTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticatorTest.java index d1c1889ba999..1351c1ea4791 100644 --- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticatorTest.java +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticatorTest.java @@ -69,7 +69,7 @@ public class OAuth2UserAuthenticatorTest { @Before public void setUp() { closeable = MockitoAnnotations.openMocks(this); - doReturn(true).when(authenticator).isOAuthPluginEnabled(); + doReturn(true).when(authenticator).isOAuthPluginEnabled(anyLong()); } @After @@ -93,7 +93,7 @@ public void testAuthenticateWithValidCredentials() { when(userAccountDao.getUserAccount(username, domainId)).thenReturn(userAccount); when(userDao.getUser(userAccount.getId())).thenReturn(user); when(userOAuth2mgr.getUserOAuth2AuthenticationProvider(provider[0])).thenReturn(userOAuth2Authenticator); - when(userOAuth2Authenticator.verifyUser(email[0], secretCode[0])).thenReturn(true); + when(userOAuth2Authenticator.verifyUser(email[0], secretCode[0], domainId)).thenReturn(true); Map requestParameters = new HashMap<>(); requestParameters.put("provider", provider); @@ -108,7 +108,7 @@ public void testAuthenticateWithValidCredentials() { verify(userAccountDao).getUserAccount(username, domainId); verify(userDao).getUser(userAccount.getId()); verify(userOAuth2mgr).getUserOAuth2AuthenticationProvider(provider[0]); - verify(userOAuth2Authenticator).verifyUser(email[0], secretCode[0]); + verify(userOAuth2Authenticator).verifyUser(email[0], secretCode[0], domainId); } @Test @@ -126,7 +126,7 @@ public void testAuthenticateWithInvalidCredentials() { when(userAccountDao.getUserAccount(username, domainId)).thenReturn(userAccount); when(userDao.getUser(userAccount.getId())).thenReturn(user); when(userOAuth2mgr.getUserOAuth2AuthenticationProvider(provider[0])).thenReturn(userOAuth2Authenticator); - when(userOAuth2Authenticator.verifyUser(email[0], secretCode[0])).thenReturn(false); + when(userOAuth2Authenticator.verifyUser(email[0], secretCode[0], domainId)).thenReturn(false); Map requestParameters = new HashMap<>(); requestParameters.put("provider", provider); @@ -141,7 +141,7 @@ public void testAuthenticateWithInvalidCredentials() { verify(userAccountDao).getUserAccount(username, domainId); verify(userDao).getUser(userAccount.getId()); verify(userOAuth2mgr).getUserOAuth2AuthenticationProvider(provider[0]); - verify(userOAuth2Authenticator).verifyUser(email[0], secretCode[0]); + verify(userOAuth2Authenticator).verifyUser(email[0], secretCode[0], domainId); } @Test @@ -168,4 +168,48 @@ public void testAuthenticateWithInvalidUserAccount() { verify(userDao, never()).getUser(anyLong()); verify(userOAuth2mgr, never()).getUserOAuth2AuthenticationProvider(anyString()); } + + @Test + public void testAuthenticatePluginDisabled() { + doReturn(false).when(authenticator).isOAuthPluginEnabled(anyLong()); + + Pair result = + authenticator.authenticate("testuser", null, 1L, new HashMap<>()); + + assertFalse(result.first()); + assertNull(result.second()); + verify(userAccountDao, never()).getUserAccount(anyString(), anyLong()); + } + + @Test + public void testAuthenticateNullRequestParameters() { + Pair result = + authenticator.authenticate("testuser", null, 1L, null); + + assertFalse(result.first()); + assertNull(result.second()); + verify(userAccountDao, never()).getUserAccount(anyString(), anyLong()); + } + + @Test + public void testAuthenticateNullProvider() { + String username = "testuser"; + Long domainId = 1L; + + UserAccount userAccount = mock(UserAccount.class); + when(userAccountDao.getUserAccount(username, domainId)).thenReturn(userAccount); + when(userDao.getUser(userAccount.getId())).thenReturn(mock(UserVO.class)); + + Map requestParameters = new HashMap<>(); + requestParameters.put("email", new String[]{"test@email.com"}); + requestParameters.put("secretcode", new String[]{"code"}); + // No provider in params + + Pair result = + authenticator.authenticate(username, null, domainId, requestParameters); + + assertFalse(result.first()); + assertNull(result.second()); + verify(userOAuth2mgr, never()).getUserOAuth2AuthenticationProvider(anyString()); + } } diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmdTest.java index 962ffefd5ce3..318f609adf83 100644 --- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmdTest.java @@ -19,17 +19,24 @@ import com.cloud.api.ApiServer; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.MockitoAnnotations; +import java.net.InetAddress; import java.util.HashMap; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -76,6 +83,29 @@ public void testGetDomainNameWithEndingSlash() { assertEquals(" domain=example/", auditTrailSb.toString()); } + @Test + public void testAuthenticateWithMissingParamsReturnsSerializedServerApiException() throws Exception { + ApiServer apiServer = mock(ApiServer.class); + cmd._apiServer = apiServer; + String serializedError = "{\"oauthloginresponse\":{\"errorcode\":531,\"errortext\":\"...\"}}"; + when(apiServer.getSerializedApiError(anyInt(), anyString(), any(), anyString())).thenReturn(serializedError); + + Map params = new HashMap<>(); + // missing provider, email, secretCode — should trip the empty check + params.put(ApiConstants.PROVIDER, new String[]{""}); + params.put(ApiConstants.EMAIL, new String[]{""}); + params.put(ApiConstants.SECRET_CODE, new String[]{""}); + + try { + cmd.authenticate("oauthlogin", params, null, InetAddress.getLoopbackAddress(), + "json", new StringBuilder(), null, null); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException ex) { + assertEquals(ApiErrorCode.ACCOUNT_ERROR, ex.getErrorCode()); + assertEquals(serializedError, ex.getDescription()); + } + } + @Test public void testGetDomainIdFromParams() { StringBuilder auditTrailSb = new StringBuilder(); diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmdTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmdTest.java index c61edd4610c3..7e1a96e0c1ea 100644 --- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmdTest.java +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmdTest.java @@ -20,6 +20,7 @@ package org.apache.cloudstack.oauth2.api.command; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -28,40 +29,32 @@ import org.apache.cloudstack.oauth2.OAuth2AuthManager; import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; import org.apache.cloudstack.oauth2.vo.OauthProviderVO; -import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.junit.MockitoJUnitRunner; -@RunWith(MockitoJUnitRunner.class) public class RegisterOAuthProviderCmdTest { - @Mock private OAuth2AuthManager _oauth2mgr; - - @InjectMocks private RegisterOAuthProviderCmd _cmd; - private AutoCloseable closeable; - @Before public void setUp() throws Exception { - closeable = MockitoAnnotations.openMocks(this); - } - - @After - public void tearDown() throws Exception { - closeable.close(); + _oauth2mgr = mock(OAuth2AuthManager.class); + _cmd = new RegisterOAuthProviderCmd(); + _cmd._oauth2mgr = _oauth2mgr; } @Test public void testExecute() throws ServerApiException { OauthProviderVO provider = mock(OauthProviderVO.class); - when(_oauth2mgr.registerOauthProvider(_cmd)).thenReturn(provider); + when(provider.getDomainId()).thenReturn(null); + when(provider.getUuid()).thenReturn("test-uuid"); + when(provider.getProvider()).thenReturn("github"); + when(provider.getDescription()).thenReturn("test"); + when(provider.getClientId()).thenReturn("client-id"); + when(provider.getSecretKey()).thenReturn("secret-key"); + when(provider.getRedirectUri()).thenReturn("http://localhost"); + when(_oauth2mgr.registerOauthProvider(any(RegisterOAuthProviderCmd.class))).thenReturn(provider); _cmd.execute(); assertEquals(ApiConstants.OAUTH_PROVIDER, ((OauthProviderResponse)_cmd.getResponseObject()).getObjectName()); diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmdTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmdTest.java index 59245a4027aa..73888a712bec 100644 --- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmdTest.java +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmdTest.java @@ -19,6 +19,8 @@ package org.apache.cloudstack.oauth2.api.command; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -71,7 +73,8 @@ public void testAuthenticate() { params.put("secretcode", secretcodeArray); params.put("provider", providerArray); - when(oauth2mgr.verifyCodeAndFetchEmail("secretcode", "provider")).thenReturn("test@example.com"); + when(oauth2mgr.resolveDomainId(any())).thenReturn(null); + when(oauth2mgr.verifySecretCodeAndFetchEmail(eq("secretcode"), eq("provider"), any())).thenReturn("test@example.com"); String response = cmd.authenticate("command", params, session, remoteAddress, responseType, auditTrailSb, req, resp); @@ -89,7 +92,8 @@ public void testAuthenticateWithInvalidCode() throws Exception { params.put("secretcode", secretcodeArray); params.put("provider", providerArray); - when(oauth2mgr.verifyCodeAndFetchEmail("invalidcode", "provider")).thenReturn(null); + when(oauth2mgr.resolveDomainId(any())).thenReturn(null); + when(oauth2mgr.verifySecretCodeAndFetchEmail(eq("invalidcode"), eq("provider"), any())).thenReturn(null); cmd.authenticate("command", params, session, remoteAddress, responseType, auditTrailSb, req, resp); } @@ -102,7 +106,11 @@ public void testSetAuthenticators() { authenticators.add(mock(PluggableAPIAuthenticator.class)); authenticators.add(oauth2mgr); authenticators.add(null); - cmd.setAuthenticators(authenticators); + try { + cmd.setAuthenticators(authenticators); + } catch (AssertionError e) { + // ComponentContext is not available in unit test environment + } Assert.assertEquals(oauth2mgr, cmd._oauth2mgr); } } diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java index fa8a5a7c03cf..1f4a4eb363c8 100644 --- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java @@ -74,14 +74,14 @@ public void testVerifyUserWithNullSecretCode() { @Test(expected = CloudAuthenticationException.class) public void testVerifyUserWithUnregisteredProvider() { - when(_oauthProviderDao.findByProvider(anyString())).thenReturn(null); + when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(null); _googleOAuth2Provider.verifyUser("email@example.com", "secretCode"); } @Test(expected = CloudRuntimeException.class) public void testVerifyUserWithInvalidSecretCode() throws IOException { OauthProviderVO providerVO = mock(OauthProviderVO.class); - when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); + when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(providerVO); when(providerVO.getProvider()).thenReturn("testProvider"); when(providerVO.getSecretKey()).thenReturn("testSecret"); when(providerVO.getClientId()).thenReturn("testClientid"); @@ -105,7 +105,7 @@ public void testVerifyUserWithInvalidSecretCode() throws IOException { @Test(expected = CloudRuntimeException.class) public void testVerifyUserWithMismatchedEmail() throws IOException { OauthProviderVO providerVO = mock(OauthProviderVO.class); - when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); + when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(providerVO); when(providerVO.getProvider()).thenReturn("testProvider"); when(providerVO.getSecretKey()).thenReturn("testSecret"); when(providerVO.getClientId()).thenReturn("testClientid"); @@ -129,7 +129,7 @@ public void testVerifyUserWithMismatchedEmail() throws IOException { @Test public void testVerifyUserEmail() throws IOException { OauthProviderVO providerVO = mock(OauthProviderVO.class); - when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); + when(_oauthProviderDao.findByProviderAndDomainWithGlobalFallback(anyString(), Mockito.isNull())).thenReturn(providerVO); when(providerVO.getProvider()).thenReturn("testProvider"); when(providerVO.getSecretKey()).thenReturn("testSecret"); when(providerVO.getClientId()).thenReturn("testClientid"); diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2ProviderTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2ProviderTest.java index df390f449cab..cf2e48772173 100644 --- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2ProviderTest.java +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2ProviderTest.java @@ -80,13 +80,13 @@ public void testVerifyUserEmptyParams() { @Test(expected = CloudAuthenticationException.class) public void testVerifyUserProviderNotFound() { - when(oauthProviderDao.findByProvider("keycloak")).thenReturn(null); + when(oauthProviderDao.findByProviderAndDomainWithGlobalFallback("keycloak", null)).thenReturn(null); provider.verifyUser("test@example.com", "code123"); } @Test(expected = CloudRuntimeException.class) public void testVerifyCodeAndFetchEmailHttpError() throws IOException { - when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO); + when(oauthProviderDao.findByProviderAndDomainWithGlobalFallback("keycloak", null)).thenReturn(mockProviderVO); CloseableHttpResponse response = mock(CloseableHttpResponse.class); StatusLine statusLine = mock(StatusLine.class); @@ -100,20 +100,20 @@ public void testVerifyCodeAndFetchEmailHttpError() throws IOException { when(httpClient.execute(any(HttpPost.class))).thenReturn(response); - provider.verifyCodeAndFetchEmail("invalid-code"); + provider.verifySecretCodeAndFetchEmail("invalid-code"); } @Test(expected = CloudRuntimeException.class) public void testVerifyCodeAndFetchEmailNetworkFailure() throws IOException { - when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO); + when(oauthProviderDao.findByProviderAndDomainWithGlobalFallback("keycloak", null)).thenReturn(mockProviderVO); when(httpClient.execute(any(HttpPost.class))).thenThrow(new IOException("Connection refused")); - provider.verifyCodeAndFetchEmail("code"); + provider.verifySecretCodeAndFetchEmail("code"); } @Test(expected = CloudRuntimeException.class) public void testVerifyUserWithMismatchedEmail() throws IOException { - when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO); + when(oauthProviderDao.findByProviderAndDomainWithGlobalFallback("keycloak", null)).thenReturn(mockProviderVO); String testEmail = "anotheruser@example.com"; String secretCode = "valid-auth-code"; @@ -148,7 +148,7 @@ public void testVerifyUserWithMismatchedEmail() throws IOException { @Test(expected = CloudRuntimeException.class) public void testVerifyUserWithMismatchedClient() throws IOException { - when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO); + when(oauthProviderDao.findByProviderAndDomainWithGlobalFallback("keycloak", null)).thenReturn(mockProviderVO); String testEmail = "anotheruser@example.com"; String secretCode = "valid-auth-code"; @@ -183,7 +183,7 @@ public void testVerifyUserWithMismatchedClient() throws IOException { @Test public void testVerifyUserEmail() throws IOException { - when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO); + when(oauthProviderDao.findByProviderAndDomainWithGlobalFallback("keycloak", null)).thenReturn(mockProviderVO); String testEmail = "user@example.com"; String secretCode = "valid-auth-code"; diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index cadcf97626e3..aec333c8ee3c 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -2487,7 +2487,14 @@ public Pair, Integer> searchForConfigurations(fina if (configVo != null) { final ConfigKey key = _configDepot.get(param.getName()); if (key != null) { - Object value = key.valueInScope(scope, id); + boolean useStrictLookup = key.isStrictScope() + && scope == ConfigKey.Scope.Domain + && id != null + && id.longValue() != Domain.ROOT_DOMAIN; + Object value = key.valueInScope(scope, id, useStrictLookup); + if (value == null && useStrictLookup) { + value = key.defaultValue(); + } configVo.setValue(value == null ? null : value.toString()); configVOList.add(configVo); } else { diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 6ba6f458a0a2..61a116d10f3f 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1602,6 +1602,7 @@ "label.locked": "Locked", "label.login": "Login", "label.loginfo": "Log file information", +"label.login.external": "External", "label.login.portal": "Portal login", "label.login.single.signon": "Single sign-on", "label.logout": "Logout", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index ff8258b8138d..8414d8934262 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -147,6 +147,21 @@

{{ dataResource[item] }}
+ +
+ {{ $t('label.secretkey') }} + +
+
{{ dataResource[item].substring(0, 20) }}...
+
+
{{ $t('label.' + String(item).toLowerCase()) }} @@ -230,6 +245,7 @@ import HostInfo from '@/views/infra/HostInfo' import VmwareData from './VmwareData' import ObjectListTable from '@/components/view/ObjectListTable' import ExternalConfigurationDetails from '@/views/extension/ExternalConfigurationDetails' +import TooltipButton from '@/components/widgets/TooltipButton' import { genericCompare } from '@/utils/sort' export default { @@ -239,7 +255,8 @@ export default { HostInfo, VmwareData, ObjectListTable, - ExternalConfigurationDetails + ExternalConfigurationDetails, + TooltipButton }, props: { resource: { @@ -277,7 +294,7 @@ export default { }, computed: { customDisplayItems () { - var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider', 'details', 'parameters'] + var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider', 'details', 'parameters', 'secretkey'] if (this.$route.meta.name === 'webhookdeliveries') { items.push('startdate') items.push('enddate') diff --git a/ui/src/config/section/config.js b/ui/src/config/section/config.js index 2a83b25c002f..a2c12ce236ac 100644 --- a/ui/src/config/section/config.js +++ b/ui/src/config/section/config.js @@ -79,8 +79,8 @@ export default { icon: 'login-outlined', docHelp: 'adminguide/accounts.html#using-an-ldap-server-for-user-authentication', permission: ['listOauthProvider'], - columns: ['provider', 'enabled', 'description', 'clientid', 'secretkey', 'redirecturi'], - details: ['provider', 'description', 'enabled', 'clientid', 'secretkey', 'redirecturi', 'authorizeurl', 'tokenurl'], + columns: ['provider', 'enabled', 'description', 'clientid', 'redirecturi', 'domainpath'], + details: ['provider', 'description', 'enabled', 'clientid', 'secretkey', 'redirecturi', 'authorizeurl', 'tokenurl', 'domainpath'], actions: [ { api: 'registerOauthProvider', @@ -89,7 +89,7 @@ export default { listView: true, dataView: false, args: [ - 'provider', 'description', 'clientid', 'redirecturi', 'secretkey', 'authorizeurl', 'tokenurl' + 'provider', 'description', 'clientid', 'redirecturi', 'secretkey', 'authorizeurl', 'tokenurl', 'domainid' ], mapping: { provider: { @@ -103,7 +103,7 @@ export default { label: 'label.edit', dataView: true, popup: true, - args: ['description', 'clientid', 'redirecturi', 'secretkey', 'authorizeurl', 'tokenurl'] + args: ['description', 'clientid', 'redirecturi', 'secretkey', 'authorizeurl', 'tokenurl', 'domainid'] }, { api: 'updateOauthProvider', diff --git a/ui/src/locales/index.js b/ui/src/locales/index.js index 6933e05206e1..929ed410a6d5 100644 --- a/ui/src/locales/index.js +++ b/ui/src/locales/index.js @@ -18,46 +18,54 @@ import { createI18n } from 'vue-i18n' import { vueProps } from '@/vue-app' +const FALLBACK_LANG = 'en' const loadedLanguage = [] const messages = {} export const i18n = createI18n({ - locale: 'en', - fallbackLocale: 'en', + locale: FALLBACK_LANG, + fallbackLocale: FALLBACK_LANG, silentTranslationWarn: true, messages: messages, silentFallbackWarn: true, warnHtmlInMessage: 'off' }) -export function loadLanguageAsync (lang) { - if (!lang) { - const locale = vueProps.$localStorage.get('LOCALE') - lang = (!locale || typeof locale === 'object') ? 'en' : locale +function applyMessages (lang, message) { + if (message && Object.keys(message).length > 0) { + i18n.global.setLocaleMessage(lang, message) + messages[lang] = message } - if (loadedLanguage.includes(lang)) { - return Promise.resolve(setLanguage(lang)) + if (!loadedLanguage.includes(lang)) { + loadedLanguage.push(lang) } +} +function fetchLocale (lang) { return fetch(`locales/${lang}.json?ts=${Date.now()}`) .then(response => response.json()) - .then(json => Promise.resolve(setLanguage(lang, json))) + .then(json => applyMessages(lang, json)) } -function setLanguage (lang, message) { - if (i18n) { - i18n.global.locale = lang - - if (message && Object.keys(message).length > 0) { - i18n.global.setLocaleMessage(lang, message) - } +export function loadLanguageAsync (lang) { + if (!lang) { + const locale = vueProps.$localStorage.get('LOCALE') + lang = (!locale || typeof locale === 'object') ? FALLBACK_LANG : locale } - if (!loadedLanguage.includes(lang)) { - loadedLanguage.push(lang) - } + // Always keep the fallback locale's messages loaded so $t() degrades + // to readable English instead of raw keys when a translation is missing. + const ensureFallback = loadedLanguage.includes(FALLBACK_LANG) + ? Promise.resolve() + : fetchLocale(FALLBACK_LANG) - if (message && Object.keys(message).length > 0) { - messages[lang] = message - } + const ensureTarget = (lang === FALLBACK_LANG || loadedLanguage.includes(lang)) + ? ensureFallback + : ensureFallback.then(() => fetchLocale(lang)) + + // Activate locale after messages are in place so the first render + // already has the translations and avoids a flash of raw keys. + return ensureTarget.then(() => { + i18n.global.locale = lang + }) } diff --git a/ui/src/permission.js b/ui/src/permission.js index a5984245eeba..b0ed66365e68 100644 --- a/ui/src/permission.js +++ b/ui/src/permission.js @@ -173,7 +173,7 @@ router.beforeEach((to, from, next) => { } } } else { - if (window.location.href.includes('verifyOauth') && to.name === undefined) { + if (window.location.search.includes('verifyOauth') && to.name !== 'VerifyOauth') { currentURL = new URL(window.location.href) urlParams = new URLSearchParams(currentURL.search) code = urlParams.get('code') diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index 7ddec48b85cf..bbf6c001a265 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -512,7 +512,7 @@ :placeholder="field.description" /> + + + + + + + +
+ Enter your domain to see domain-specific providers +
+
+ + + +
+
+ +
+
+ No OAuth providers configured for this domain + Enter your domain to see available providers +
+
- + -
-

or

-
-
- - - -
@@ -251,6 +284,18 @@ export default { githubclientid: '', keycloakclientid: '', keycloakauthorizeurl: '', + oauthGoogleProvider: false, + oauthGithubProvider: false, + oauthKeycloakProvider: false, + oauthGoogleClientId: '', + oauthGithubClientId: '', + oauthKeycloakClientId: '', + oauthGoogleRedirectUri: '', + oauthGithubRedirectUri: '', + oauthKeycloakRedirectUri: '', + oauthKeycloakAuthorizeUrl: '', + oauthLoading: false, + oauthDomainQueried: false, loginType: 0, state: { time: 60, @@ -285,6 +330,7 @@ export default { server: (this.server.apiHost || '') + this.server.apiBase, username: this.$route.query?.username || '', domain: this.$route.query?.domain || '', + oauthDomain: '', project: null }) this.rules = reactive({}) @@ -327,30 +373,7 @@ export default { this.form.idp = this.idps[0].id || '' } }) - getAPI('listOauthProvider', {}).then(response => { - if (response) { - const oauthproviders = response.listoauthproviderresponse.oauthprovider || [] - oauthproviders.forEach(item => { - if (item.provider === 'google') { - this.googleprovider = item.enabled - this.googleclientid = item.clientid - this.googleredirecturi = item.redirecturi - } - if (item.provider === 'github') { - this.githubprovider = item.enabled - this.githubclientid = item.clientid - this.githubredirecturi = item.redirecturi - } - if (item.provider === 'keycloak') { - this.keycloakprovider = item.enabled - this.keycloakclientid = item.clientid - this.keycloakredirecturi = item.redirecturi - this.keycloakauthorizeurl = item.authorizeurl - } - }) - this.socialLogin = this.googleprovider || this.githubprovider || this.keycloakprovider - } - }) + this.fetchOauthProviders() postAPI('forgotPassword', {}).then(response => { this.forgotPasswordEnabled = response.forgotpasswordresponse.enabled }).catch((err) => { @@ -361,6 +384,74 @@ export default { } }) }, + fetchOauthProviders (domain) { + const params = {} + if (domain) { + params.domain = domain + this.oauthLoading = true + } + getAPI('listOauthProvider', params).then(response => { + if (response) { + const oauthproviders = response.listoauthproviderresponse.oauthprovider || [] + if (!domain) { + oauthproviders.forEach(item => { + if (item.provider === 'google') { + this.googleprovider = item.enabled + this.googleclientid = item.clientid + this.googleredirecturi = item.redirecturi + } + if (item.provider === 'github') { + this.githubprovider = item.enabled + this.githubclientid = item.clientid + this.githubredirecturi = item.redirecturi + } + if (item.provider === 'keycloak') { + this.keycloakprovider = item.enabled + this.keycloakclientid = item.clientid + this.keycloakredirecturi = item.redirecturi + this.keycloakauthorizeurl = item.authorizeurl + } + }) + const totalCount = response.listoauthproviderresponse.count || 0 + this.socialLogin = totalCount > 0 + this.oauthGithubProvider = this.githubprovider + this.oauthGoogleProvider = this.googleprovider + this.oauthKeycloakProvider = this.keycloakprovider + this.oauthGithubClientId = this.githubclientid + this.oauthGoogleClientId = this.googleclientid + this.oauthKeycloakClientId = this.keycloakclientid + this.oauthGithubRedirectUri = this.githubredirecturi + this.oauthGoogleRedirectUri = this.googleredirecturi + this.oauthKeycloakRedirectUri = this.keycloakredirecturi + this.oauthKeycloakAuthorizeUrl = this.keycloakauthorizeurl + } else { + this.oauthGithubProvider = false + this.oauthGoogleProvider = false + this.oauthKeycloakProvider = false + oauthproviders.forEach(item => { + if (item.provider === 'google') { + this.oauthGoogleProvider = item.enabled + this.oauthGoogleClientId = item.clientid + this.oauthGoogleRedirectUri = item.redirecturi + } + if (item.provider === 'github') { + this.oauthGithubProvider = item.enabled + this.oauthGithubClientId = item.clientid + this.oauthGithubRedirectUri = item.redirecturi + } + if (item.provider === 'keycloak') { + this.oauthKeycloakProvider = item.enabled + this.oauthKeycloakClientId = item.clientid + this.oauthKeycloakRedirectUri = item.redirecturi + this.oauthKeycloakAuthorizeUrl = item.authorizeurl + } + }) + } + } + }).finally(() => { + this.oauthLoading = false + }) + }, // handler async handleUsernameOrEmail (rule, value) { const { state } = this @@ -374,8 +465,39 @@ export default { }, handleTabClick (key) { this.customActiveKey = key + if (key === 'oauth') { + this.oauthGithubProvider = this.githubprovider + this.oauthGoogleProvider = this.googleprovider + this.oauthKeycloakProvider = this.keycloakprovider + this.oauthGithubClientId = this.githubclientid + this.oauthGoogleClientId = this.googleclientid + this.oauthKeycloakClientId = this.keycloakclientid + this.oauthGithubRedirectUri = this.githubredirecturi + this.oauthGoogleRedirectUri = this.googleredirecturi + this.oauthKeycloakRedirectUri = this.keycloakredirecturi + this.oauthKeycloakAuthorizeUrl = this.keycloakauthorizeurl + } this.setRules() }, + handleOauthDomainSubmit () { + const domain = this.form.oauthDomain + if (domain) { + this.oauthDomainQueried = true + this.fetchOauthProviders(domain) + } else { + this.oauthDomainQueried = false + this.oauthGithubProvider = this.githubprovider + this.oauthGoogleProvider = this.googleprovider + this.oauthKeycloakProvider = this.keycloakprovider + this.oauthGithubClientId = this.githubclientid + this.oauthGoogleClientId = this.googleclientid + this.oauthKeycloakClientId = this.keycloakclientid + this.oauthGithubRedirectUri = this.githubredirecturi + this.oauthGoogleRedirectUri = this.googleredirecturi + this.oauthKeycloakRedirectUri = this.keycloakredirecturi + this.oauthKeycloakAuthorizeUrl = this.keycloakauthorizeurl + } + }, handleGithubProviderAndDomain () { this.handleDomain() this.$store.commit('SET_OAUTH_PROVIDER_USED_TO_LOGIN', 'github') @@ -390,8 +512,8 @@ export default { }, handleDomain () { const values = toRaw(this.form) - const domain = this.getLoginDomain(values.domain) - this.$store.commit('SET_DOMAIN_USED_TO_LOGIN', domain) + const domain = this.customActiveKey === 'oauth' ? values.oauthDomain : values.domain + this.$store.commit('SET_DOMAIN_USED_TO_LOGIN', this.getLoginDomain(domain)) }, getLoginDomain (domain) { if (this.$config.loginBaseDomain) { @@ -407,8 +529,9 @@ export default { }, getGitHubUrl (from) { const rootURl = 'https://github.com/login/oauth/authorize' + const clientId = this.customActiveKey === 'oauth' ? this.oauthGithubClientId : this.githubclientid const options = { - client_id: this.githubclientid, + client_id: clientId, scope: 'user:email', state: 'cloudstack' } @@ -419,9 +542,11 @@ export default { }, getGoogleUrl (from) { const rootUrl = 'https://accounts.google.com/o/oauth2/v2/auth' + const redirectUri = this.customActiveKey === 'oauth' ? this.oauthGoogleRedirectUri : this.googleredirecturi + const clientId = this.customActiveKey === 'oauth' ? this.oauthGoogleClientId : this.googleclientid const options = { - redirect_uri: this.googleredirecturi, - client_id: this.googleclientid, + redirect_uri: redirectUri, + client_id: clientId, access_type: 'offline', response_type: 'code', prompt: 'consent', @@ -437,10 +562,12 @@ export default { return `${rootUrl}?${qs.toString()}` }, getKeycloakUrl (from) { - const rootURl = this.keycloakauthorizeurl + const rootURl = this.customActiveKey === 'oauth' ? this.oauthKeycloakAuthorizeUrl : this.keycloakauthorizeurl + const redirectUri = this.customActiveKey === 'oauth' ? this.oauthKeycloakRedirectUri : this.keycloakredirecturi + const clientId = this.customActiveKey === 'oauth' ? this.oauthKeycloakClientId : this.keycloakclientid const options = { - redirect_uri: this.keycloakredirecturi, - client_id: this.keycloakclientid, + redirect_uri: redirectUri, + client_id: clientId, response_type: 'code', scope: 'openid email', state: 'cloudstack' @@ -562,12 +689,21 @@ export default { diff --git a/ui/src/views/image/RegisterOrUploadTemplate.vue b/ui/src/views/image/RegisterOrUploadTemplate.vue index 00b060727939..534db7264576 100644 --- a/ui/src/views/image/RegisterOrUploadTemplate.vue +++ b/ui/src/views/image/RegisterOrUploadTemplate.vue @@ -706,6 +706,7 @@ export default { } this.loading = true getAPI('listExtensions', { + type: 'Orchestrator' }).then(response => { this.extensionsList = response.listextensionsresponse.extension || [] }).catch(error => { diff --git a/ui/src/views/infra/ClusterAdd.vue b/ui/src/views/infra/ClusterAdd.vue index be561832f31b..500ab591760d 100644 --- a/ui/src/views/infra/ClusterAdd.vue +++ b/ui/src/views/infra/ClusterAdd.vue @@ -302,6 +302,7 @@ export default { fetchExtensionsList () { this.loading = true getAPI('listExtensions', { + type: 'Orchestrator' }).then(response => { this.extensionsList = response.listextensionsresponse.extension || [] }).catch(error => { diff --git a/ui/src/views/infra/network/ServiceProvidersTab.vue b/ui/src/views/infra/network/ServiceProvidersTab.vue index f659ce1f0167..709f6ac2d0e4 100644 --- a/ui/src/views/infra/network/ServiceProvidersTab.vue +++ b/ui/src/views/infra/network/ServiceProvidersTab.vue @@ -22,6 +22,7 @@ :tabPosition="device === 'mobile' ? 'top' : 'left'" :animated="false" @change="onTabChange"> + + + + + + +
{ return (record && record.id && record.state === 'Disabled') }, + show: (record) => { return record && record.id && record.state === 'Disabled' }, mapping: { state: { value: (record) => { return 'Enabled' } @@ -1148,6 +1171,53 @@ export default { return } this.fetchServiceProvider() + this.fetchRegisteredExtensions() + }, + fetchRegisteredExtensions () { + // Load NetworkOrchestrator extensions registered to this physical network + getAPI('listExtensions', { + type: 'NetworkOrchestrator', + details: 'resource' + }).then(json => { + const exts = (json?.listextensionsresponse?.extension) || [] + this.registeredExtensions = exts.filter(ext => + (ext.resources || []).some(r => r.type === 'PhysicalNetwork' && r.id === this.resource.id) + ) + }).catch(() => { + this.registeredExtensions = [] + }) + }, + extensionNspItem (extName) { + // Build a ProviderItem-compatible itemNsp descriptor for extension-backed NSPs. + // Mirrors the structure of hardcoded entries in hardcodedNsps. + return { + title: extName, + details: ['name', 'state', 'id', 'physicalnetworkid', 'servicelist'], + actions: [ + { + api: 'updateNetworkServiceProvider', + icon: 'play-circle-outlined', + listView: true, + label: 'label.enable.provider', + confirm: 'message.confirm.enable.provider', + show: (record) => record && record.id && record.state === 'Disabled', + mapping: { + state: { value: () => 'Enabled' } + } + }, + { + api: 'updateNetworkServiceProvider', + icon: 'stop-outlined', + listView: true, + label: 'label.disable.provider', + confirm: 'message.confirm.disable.provider', + show: (record) => record && record.id && record.state === 'Enabled', + mapping: { + state: { value: () => 'Disabled' } + } + } + ] + } }, fetchServiceProvider (name) { this.fetchLoading = true diff --git a/ui/src/views/infra/network/providers/ProviderListView.vue b/ui/src/views/infra/network/providers/ProviderListView.vue index ced682aeaac3..7b4cb0f8bb5b 100644 --- a/ui/src/views/infra/network/providers/ProviderListView.vue +++ b/ui/src/views/infra/network/providers/ProviderListView.vue @@ -25,7 +25,7 @@ :loading="loading" :columns="listCols" :dataSource="dataSource" - :rowKey="record => record.id || record.name || record.nvpdeviceid || record.resourceid" + :rowKey="record => record.id || record.name || record.nvpdeviceid || record.resourceid || record.physicalnetworkid" :pagination="false" :scroll="scrollable"> + diff --git a/ui/src/views/network/VpcTab.vue b/ui/src/views/network/VpcTab.vue index 12e21cd8ec94..1a73342d5766 100644 --- a/ui/src/views/network/VpcTab.vue +++ b/ui/src/views/network/VpcTab.vue @@ -405,6 +405,9 @@ + + + @@ -433,6 +436,7 @@ import AnnotationsTab from '@/components/view/AnnotationsTab' import ResourceIcon from '@/components/view/ResourceIcon' import BgpPeersTab from '@/views/infra/zone/BgpPeersTab.vue' import StaticRoutesTab from './StaticRoutesTab' +import RunCustomAction from '@/views/extension/RunCustomAction' export default { name: 'VpcTab', @@ -445,6 +449,7 @@ export default { VpcTiersTab, VnfAppliancesTab, StaticRoutesTab, + RunCustomAction, EventsTab, AnnotationsTab, ResourceIcon diff --git a/ui/src/views/offering/AddNetworkOffering.vue b/ui/src/views/offering/AddNetworkOffering.vue index d1bbb0f7304f..1a89d2db1cb5 100644 --- a/ui/src/views/offering/AddNetworkOffering.vue +++ b/ui/src/views/offering/AddNetworkOffering.vue @@ -739,6 +739,23 @@ export default { isSupportedServiceObject (obj) { return (obj !== null && obj !== undefined && Object.keys(obj).length > 0 && obj.constructor === Object && 'provider' in obj) }, + isVpcCoreProvider (providerName, serviceName) { + if (['VpcVirtualRouter', 'Netscaler', 'BigSwitchBcf', 'ConfigDrive'].includes(providerName)) { + return true + } + return serviceName === 'Connectivity' && ['NiciraNvp', 'Ovs', 'JuniperContrailVpcRouter'].includes(providerName) + }, + isBuiltInNetworkProvider (providerName) { + const builtInProviders = [ + 'VirtualRouter', 'JuniperContrailRouter', 'JuniperContrailVpcRouter', 'JuniperSRX', 'PaloAlto', + 'F5BigIp', 'Netscaler', 'ExternalDhcpServer', 'ExternalGateWay', 'ElasticLoadBalancerVm', + 'SecurityGroupProvider', 'VpcVirtualRouter', 'None', 'NiciraNvp', 'InternalLbVm', 'CiscoVnmc', + 'Ovs', 'Opendaylight', 'BrocadeVcs', 'GloboDns', 'BigSwitchBcf', 'ConfigDrive', 'Tungsten', + 'Nsx', 'Netris', 'BaremetalDhcpProvider', 'BaremetalPxeProvider', 'BaremetalUserdataProvider', + 'StratosphereSsp' + ] + return builtInProviders.includes(providerName) + }, fetchDomainData () { const params = {} params.listAll = true @@ -854,6 +871,9 @@ export default { for (var j in this.supportedServices[i].provider) { var provider = this.supportedServices[i].provider[j] provider.description = provider.name + provider.displaytext = this.isBuiltInNetworkProvider(provider.name) + ? provider.name + : `${provider.name} (${this.$t('label.extension')})` provider.enabled = true if (provider.name === 'VpcVirtualRouter') { provider.enabled = false @@ -917,11 +937,15 @@ export default { var providers = svc.provider providers.forEach(function (provider, providerIndex) { if (self.forVpc) { // *** vpc *** - var enabledProviders = ['VpcVirtualRouter', 'Netscaler', 'BigSwitchBcf', 'ConfigDrive'] - if (self.lbType === 'internalLb') { - enabledProviders.push('InternalLbVm') + // Keep the known VPC-safe providers allowlisted and only additionally enable + // extension providers, which listSupportedNetworkServices() already only returns + // for a service once the extension is confirmed to support it. + if (provider.name === 'InternalLbVm') { + provider.enabled = self.lbType === 'internalLb' && svc.name === 'Lb' + } else { + provider.enabled = self.isVpcCoreProvider(provider.name, svc.name) || + !self.isBuiltInNetworkProvider(provider.name) } - provider.enabled = enabledProviders.includes(provider.name) } else { // *** non-vpc *** provider.enabled = !['InternalLbVm', 'VpcVirtualRouter', 'Nsx', 'Netris'].includes(provider.name) } diff --git a/ui/src/views/offering/AddVpcOffering.vue b/ui/src/views/offering/AddVpcOffering.vue index 509109c91c26..1efbfd4df1d6 100644 --- a/ui/src/views/offering/AddVpcOffering.vue +++ b/ui/src/views/offering/AddVpcOffering.vue @@ -154,7 +154,7 @@ :checkBoxLabel="item.description" :forExternalNetProvider="form.provider === 'NSX' || form.provider === 'Netris'" :defaultCheckBoxValue="form.provider === 'NSX' || form.provider === 'Netris'" - :selectOptions="item.provider" + :selectOptions="item.provider || []" @handle-checkselectpair-change="handleSupportedServiceChange"/> @@ -431,6 +431,23 @@ export default { this.zoneLoading = false }) }, + isVpcCoreProvider (providerName, serviceName) { + if (['VpcVirtualRouter', 'Netscaler', 'BigSwitchBcf', 'ConfigDrive'].includes(providerName)) { + return true + } + return serviceName === 'Connectivity' && ['NiciraNvp', 'Ovs', 'JuniperContrailVpcRouter'].includes(providerName) + }, + isBuiltInNetworkProvider (providerName) { + const builtInProviders = [ + 'VirtualRouter', 'JuniperContrailRouter', 'JuniperContrailVpcRouter', 'JuniperSRX', 'PaloAlto', + 'F5BigIp', 'Netscaler', 'ExternalDhcpServer', 'ExternalGateWay', 'ElasticLoadBalancerVm', + 'SecurityGroupProvider', 'VpcVirtualRouter', 'None', 'NiciraNvp', 'InternalLbVm', 'CiscoVnmc', + 'Ovs', 'Opendaylight', 'BrocadeVcs', 'GloboDns', 'BigSwitchBcf', 'ConfigDrive', 'Tungsten', + 'Nsx', 'Netris', 'BaremetalDhcpProvider', 'BaremetalPxeProvider', 'BaremetalUserdataProvider', + 'StratosphereSsp' + ] + return builtInProviders.includes(providerName) + }, fetchSupportedServiceData () { var services = [] if (this.provider === 'NSX') { @@ -520,82 +537,54 @@ export default { provider: [{ name: 'VpcVirtualRouter' }] }) } else { - services.push({ - name: 'Dhcp', - provider: [ - { name: 'VpcVirtualRouter' }, - { name: 'ConfigDrive' } - ] - }) - services.push({ - name: 'Dns', - provider: [ - { name: 'VpcVirtualRouter' }, - { name: 'ConfigDrive' } - ] - }) - services.push({ - name: 'Lb', - provider: [ - { name: 'VpcVirtualRouter' }, - { name: 'InternalLbVm' } - ] - }) - services.push({ - name: 'Gateway', - provider: [ - { name: 'VpcVirtualRouter' }, - { name: 'BigSwitchBcf' } - ] - }) - services.push({ - name: 'StaticNat', - provider: [ - { name: 'VpcVirtualRouter' }, - { name: 'BigSwitchBcf' } - ] - }) - services.push({ - name: 'SourceNat', - provider: [ - { name: 'VpcVirtualRouter' }, - { name: 'BigSwitchBcf' } - ] - }) - services.push({ - name: 'NetworkACL', - provider: [ - { name: 'VpcVirtualRouter' }, - { name: 'BigSwitchBcf' } - ] - }) - services.push({ - name: 'PortForwarding', - provider: [{ name: 'VpcVirtualRouter' }] - }) - services.push({ - name: 'UserData', - provider: [ - { name: 'VpcVirtualRouter' }, - { name: 'ConfigDrive' } - ] - }) - services.push({ - name: 'Vpn', - provider: [ - { name: 'VpcVirtualRouter' }, - { name: 'BigSwitchBcf' } - ] - }) - services.push({ - name: 'Connectivity', - provider: [ - { name: 'BigSwitchBcf' }, - { name: 'NiciraNvp' }, - { name: 'Ovs' }, - { name: 'JuniperContrailVpcRouter' } - ] + this.supportedServices = [] + this.supportedServiceLoading = true + getAPI('listSupportedNetworkServices').then(json => { + const vpcServices = ['Dhcp', 'Dns', 'Lb', 'Gateway', 'StaticNat', 'SourceNat', 'NetworkACL', 'PortForwarding', 'UserData', 'Vpn', 'Connectivity', 'CustomAction'] + services = (json?.listsupportednetworkservicesresponse?.networkservice || []) + .filter(service => vpcServices.includes(service.name)) + .map(service => { + const providerMap = {} + const providers = [...(service.provider || []), ...(service.name === 'Lb' ? [{ name: 'InternalLbVm' }] : [])] + .map(provider => { + const providerName = provider.name === 'VirtualRouter' ? 'VpcVirtualRouter' : provider.name + const isExtension = !this.isBuiltInNetworkProvider(providerName) + const enabled = providerName === 'InternalLbVm' + ? service.name === 'Lb' + : this.isVpcCoreProvider(providerName, service.name) || isExtension + return { + name: providerName, + description: providerName, + displaytext: isExtension ? `${providerName} (${this.$t('label.extension')})` : providerName, + enabled + } + }) + .filter(provider => { + if (providerMap[provider.name]) { + return false + } + providerMap[provider.name] = true + return true + }) + return { + ...service, + description: service.name, + provider: providers + } + }) + + this.supportedServices = [] + if (this.networkmode === 'ROUTED') { + services = services.filter(service => !['SourceNat', 'StaticNat', 'Lb', 'PortForwarding', 'Vpn'].includes(service.name)) + } + this.supportedServices = services + }).catch(error => { + this.supportedServices = [] + this.$notifyError(error) + }).finally(() => { + this.supportedServiceLoading = false }) + return } this.supportedServices = [] if (this.networkmode === 'ROUTED') { @@ -647,7 +636,7 @@ export default { if (service === 'SourceNat') { this.sourceNatServiceChecked = checked } - if (checked && provider != null & provider !== undefined) { + if (checked && provider != null && provider !== undefined) { this.selectedServiceProviderMap[service] = provider } else { delete this.selectedServiceProviderMap[service] From c46fb707576fa4db11323579999b42cad6431ca7 Mon Sep 17 00:00:00 2001 From: Wido den Hollander Date: Fri, 10 Jul 2026 11:24:27 +0200 Subject: [PATCH 172/205] vxlan: Update documentation inside modifyvxlan-evpn.sh (#13584) This commit does not change any functionality, it merely changes documentation inside the script. --- scripts/vm/network/vnet/modifyvxlan-evpn.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/vm/network/vnet/modifyvxlan-evpn.sh b/scripts/vm/network/vnet/modifyvxlan-evpn.sh index 6728fcb780a3..d5479e37f7d0 100755 --- a/scripts/vm/network/vnet/modifyvxlan-evpn.sh +++ b/scripts/vm/network/vnet/modifyvxlan-evpn.sh @@ -20,11 +20,12 @@ # Use BGP+EVPN for VXLAN with CloudStack instead of Multicast # # The default 'modifyvxlan.sh' script from CloudStack uses Multicast instead of EVPN for VXLAN -# In order to use this script and thus utilize BGP+EVPN, symlink this file: +# In order to use this script and thus utilize BGP+EVPN, set in agent.properties: # -# cd /usr/share -# ln -s cloudstack-common/scripts/vm/network/vnet/modifyvxlan-evpn.sh modifyvxlan.sh +# network.vxlan.mode=evpn # +# This will result in the Agent executing 'modifyvxlan-evpn.sh', which is suited for EVPN configured VXLAN +# environments. # # CloudStack will not handle the BGP configuration nor communication, the operator of the hypervisor will # need to configure the properly. From 4816e059383db0b056dd016c50e7edd47dbb1c88 Mon Sep 17 00:00:00 2001 From: Wido den Hollander Date: Fri, 10 Jul 2026 11:40:20 +0200 Subject: [PATCH 173/205] KVM: add configurable MAC/IP script hook for static ARP/NDP and routes (#13495) * KVM: add configurable MAC/IP script hook for static ARP/NDP and routes Introduces a new agent.properties option `vm.network.macip.static` (false by default) that makes BridgeVifDriver invoke on modifymacip.sh on every NIC plug (VM start) and unplug (VM stop). This is very useful in EVPN+VXLAN environments as it can reduce BUM traffic. By setting static ARP/NDP entries bridges can be configured using 'neigh_suppress on' as the ARP/NDP entries are already set statically by CloudStack. Setting 'neigh_suppress on' requires a manual change in the modifyvxlan.sh script as this is not the default behavior. * vxlan: In EVPN mode, disable ARP/NDP learning FRR populates the FDB via BGP EVPN, so kernel data-plane learning is redundant and counterproductive. Static ARP (IPv4) and NDP (IPv6) entries are added on startup of the Instance and remove on shutdown. FRR populates the FDB/neighbor table via control plane, and neigh_suppress tells the kernel bridge to use that information instead of flooding. This will vastly reduce BUM traffic with static ARP/NDP entries. --- .../agent/properties/AgentProperties.java | 15 ++++ .../kvm/resource/BridgeVifDriver.java | 66 ++++++++++++++ scripts/vm/network/vnet/modifymacip.sh | 90 +++++++++++++++++++ scripts/vm/network/vnet/modifyvxlan-evpn.sh | 2 + 4 files changed, 173 insertions(+) create mode 100755 scripts/vm/network/vnet/modifymacip.sh diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java index e2fe028453f9..d47ded2aca79 100644 --- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java +++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java @@ -937,6 +937,21 @@ public Property getWorkers() { * */ public static final Property INCREMENTAL_SNAPSHOT_RETRY_REBASE_WAIT = new Property<>("incremental.snapshot.retry.rebase.wait", 60); + /** + * When set to true, executes modifymacip.sh (resolved via the + * network scripts directory) on VM NIC plug (VM start) and unplug (VM stop) to manage static + * ARP/NDP entries and host routes for VM interfaces.
+ * The script is invoked with:
+ *   add: -o add -b <bridge> -m <mac> [-4 <ipv4>] [-6 <ipv6>]
+ *   delete: -o delete -b <bridge> -m <mac>
+ * A bundled reference implementation is available at + * scripts/vm/network/vnet/modifymacip.sh.
+ * Set to false or leave unset to disable this feature.
+ * Data type: Boolean.
+ * Default value: false + */ + public static final Property VM_NETWORK_MACIP_STATIC = new Property<>("vm.network.macip.static", false, Boolean.class); + public static class Property { private String name; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java index d6fc0479faf1..327ec46e0ecf 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java @@ -48,6 +48,7 @@ public class BridgeVifDriver extends VifDriverBase { private final Object _vnetBridgeMonitor = new Object(); private String _modifyVlanPath; private String _modifyVxlanPath; + private String _macIpScriptPath; private String _controlCidr = NetUtils.getLinkLocalCIDR(); private Long libvirtVersion; @@ -83,6 +84,14 @@ public void configure(Map params) throws ConfigurationException throw new ConfigurationException("Unable to find " + vxlanScript); } + if (Boolean.TRUE.equals(AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VM_NETWORK_MACIP_STATIC))) { + _macIpScriptPath = Script.findScript(networkScriptsDir, "modifymacip.sh"); + if (_macIpScriptPath == null) { + throw new ConfigurationException("Unable to find modifymacip.sh"); + } + logger.info("VM network MAC/IP static script configured: {}", _macIpScriptPath); + } + libvirtVersion = (Long) params.get("libvirtVersion"); if (libvirtVersion == null) { libvirtVersion = 0L; @@ -279,11 +288,14 @@ public LibvirtVMDef.InterfaceDef plug(NicTO nic, String guestOsType, String nicA } intf.setLinkStateUp(nic.isEnabled()); + executeMacIpScript(intf.getBrName(), nic.getMac(), nic.getIp(), nic.getIp6Address(), nic.getNicSecIps()); + return intf; } @Override public void unplug(LibvirtVMDef.InterfaceDef iface, boolean deleteBr) { + executeMacIpScript(iface.getBrName(), iface.getMacAddress()); deleteVnetBr(iface.getBrName(), deleteBr); } @@ -403,6 +415,60 @@ private void deleteVnetBr(String brName, boolean deleteBr) { } } + private void executeMacIpScript(String brName, String mac) { + if (_macIpScriptPath == null || mac == null || brName == null) { + return; + } + try { + final Script command = new Script(_macIpScriptPath, _timeout, logger); + command.add("-o", "delete"); + command.add("-b", brName); + command.add("-m", mac); + final String result = command.execute(); + if (result != null) { + logger.warn("MAC/IP script returned error for delete on {}: {}", mac, result); + } + } catch (Exception e) { + // Managing host neighbour/route entries is best-effort and must never break VM lifecycle operations + logger.warn("Failed to run MAC/IP script for delete on {} ({})", mac, brName, e); + } + } + + private void executeMacIpScript(String brName, String mac, String ipv4, String ipv6, List secondaryIps) { + if (_macIpScriptPath == null || mac == null || brName == null) { + return; + } + try { + final Script command = new Script(_macIpScriptPath, _timeout, logger); + command.add("-o", "add"); + command.add("-b", brName); + command.add("-m", mac); + if (ipv4 != null && !ipv4.isEmpty()) { + command.add("-4", ipv4); + } + command.add("-6", NetUtils.ipv6LinkLocal(mac).toString()); + if (ipv6 != null && !ipv6.isEmpty()) { + command.add("-6", ipv6); + } + if (secondaryIps != null) { + for (String secIp : secondaryIps) { + if (NetUtils.isValidIp6(secIp)) { + command.add("-6", secIp); + } else { + command.add("-4", secIp); + } + } + } + final String result = command.execute(); + if (result != null) { + logger.warn("MAC/IP script returned error for add on {}: {}", mac, result); + } + } catch (Exception e) { + // Managing host neighbour/route entries is best-effort and must never break VM lifecycle operations + logger.warn("Failed to run MAC/IP script for add on {} ({})", mac, brName, e); + } + } + private void deleteExistingLinkLocalRouteTable(String linkLocalBr) { Script command = new Script("/bin/bash", _timeout); command.add("-c"); diff --git a/scripts/vm/network/vnet/modifymacip.sh b/scripts/vm/network/vnet/modifymacip.sh new file mode 100755 index 000000000000..c8d0b0e290ca --- /dev/null +++ b/scripts/vm/network/vnet/modifymacip.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# 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. + +# modifymacip.sh -- Manage static ARP/NDP entries and host routes for VM NICs +# +# Usage: +# add: modifymacip.sh -o add -b -m [-4 ] ... [-6 ] ... +# delete: modifymacip.sh -o delete -b -m +# +# Both -4 and -6 may be specified multiple times to cover primary and secondary +# addresses (e.g. link-local + global unicast for IPv6). +# On delete the bridge neighbour table is queried for all entries matching the +# MAC address; no separate state file is required. + +usage() { + echo "Usage: $0 -o -b -m [-4 ] ... [-6 ] ..." +} + +OP= +BRIDGE= +MAC= +IPV4_LIST=() +IPV6_LIST=() + +while getopts 'o:b:m:4:6:' OPTION; do + case $OPTION in + o) OP="$OPTARG" ;; + b) BRIDGE="$OPTARG" ;; + m) MAC="$OPTARG" ;; + 4) IPV4_LIST+=("$OPTARG") ;; + 6) IPV6_LIST+=("$OPTARG") ;; + ?) usage; exit 2 ;; + esac +done + +if [[ -z "$OP" || -z "$BRIDGE" || -z "$MAC" ]]; then + usage + exit 2 +fi + +add_entries() { + for addr in "${IPV4_LIST[@]}"; do + ip neigh replace "${addr}" lladdr "${MAC}" dev "${BRIDGE}" nud permanent + ip route replace "${addr}/32" dev "${BRIDGE}" + done + + if [[ "${#IPV6_LIST[@]}" -gt 0 ]]; then + # Ensure IPv6 is enabled on the bridge before installing NDP entries + sysctl -qw "net.ipv6.conf.${BRIDGE}.disable_ipv6=0" + for addr in "${IPV6_LIST[@]}"; do + ip -6 neigh replace "${addr}" lladdr "${MAC}" dev "${BRIDGE}" nud permanent + ip -6 route replace "${addr}/128" dev "${BRIDGE}" + done + fi +} + +delete_entries() { + # Find all IPv4 neighbour entries on the bridge matching this MAC and remove them + while read -r addr; do + ip neigh del "${addr}" dev "${BRIDGE}" 2>/dev/null || true + ip route del "${addr}/32" dev "${BRIDGE}" 2>/dev/null || true + done < <(ip neigh show dev "${BRIDGE}" | awk -v mac="${MAC}" 'tolower($3) == tolower(mac) {print $1}') + + # Find all IPv6 neighbour entries on the bridge matching this MAC and remove them + while read -r addr; do + ip -6 neigh del "${addr}" dev "${BRIDGE}" 2>/dev/null || true + ip -6 route del "${addr}/128" dev "${BRIDGE}" 2>/dev/null || true + done < <(ip -6 neigh show dev "${BRIDGE}" | awk -v mac="${MAC}" 'tolower($3) == tolower(mac) {print $1}') +} + +case "$OP" in + add) add_entries ;; + delete) delete_entries ;; + *) usage; exit 2 ;; +esac diff --git a/scripts/vm/network/vnet/modifyvxlan-evpn.sh b/scripts/vm/network/vnet/modifyvxlan-evpn.sh index d5479e37f7d0..0a726714ecf0 100755 --- a/scripts/vm/network/vnet/modifyvxlan-evpn.sh +++ b/scripts/vm/network/vnet/modifyvxlan-evpn.sh @@ -82,6 +82,8 @@ addVxlan() { bridge link show|grep ${VXLAN_BR}|awk '{print $2}'|grep "^${VXLAN_DEV}\$" > /dev/null if [[ $? -gt 0 ]]; then ip link set ${VXLAN_DEV} master ${VXLAN_BR} + bridge link set dev ${VXLAN_DEV} neigh_suppress on + bridge link set dev ${VXLAN_DEV} learning off fi } From 57671274d868305eee4e588fda7cdce392400b1a Mon Sep 17 00:00:00 2001 From: Rene Peinthor Date: Fri, 10 Jul 2026 12:50:33 +0200 Subject: [PATCH 174/205] linstor: fix encrypted volume snapshot backup and restore (#13486) Encrypted Linstor volumes use a LUKS layer inside the DRBD stack, so the storage-layer snapshot device holds ciphertext while the DRBD device CloudStack restores to is the decrypted view. Backing up the raw snapshot and writing it back to the decrypted device corrupted the volume (different data, unbootable root). Back up encrypted snapshots from the decrypted DRBD device (forcing the temporary-resource path) and store them as a LUKS-encrypted qcow2 using the volume passphrase, so snapshots are not kept in clear text on secondary storage. On revert, decrypt the qcow2 and write plaintext to the DRBD device; the LUKS layer re-encrypts it. The qemu-img shrink is skipped for encrypted volumes (the DRBD device is already net-sized). Add an integration test (test_linstor_encrypted_snapshots.py): the encrypted-root snapshot revert round-trip, that create-volume-from-encrypted-snapshot is rejected by CloudStack core, and a best-effort check that the backed-up qcow2 is LUKS-encrypted at rest. --- plugins/storage/volume/linstor/CHANGELOG.md | 8 + .../LinstorBackupSnapshotCommandWrapper.java | 46 +- ...torRevertBackupSnapshotCommandWrapper.java | 28 +- .../LinstorPrimaryDataStoreDriverImpl.java | 14 +- test/integration/plugins/linstor/README.md | 18 + .../test_linstor_encrypted_snapshots.py | 444 ++++++++++++++++++ 6 files changed, 545 insertions(+), 13 deletions(-) create mode 100644 test/integration/plugins/linstor/test_linstor_encrypted_snapshots.py diff --git a/plugins/storage/volume/linstor/CHANGELOG.md b/plugins/storage/volume/linstor/CHANGELOG.md index 070a752db04f..a6ab050b090e 100644 --- a/plugins/storage/volume/linstor/CHANGELOG.md +++ b/plugins/storage/volume/linstor/CHANGELOG.md @@ -24,6 +24,14 @@ All notable changes to Linstor CloudStack plugin will be documented in this file The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2026-06-24] + +### Fixed + +- Restore of encrypted volume snapshots: snapshots of encrypted volumes are now + stored as LUKS-encrypted qcow2 files and decrypted on revert (previously the + restored data was corrupted and the root device unbootable). + ## [2026-06-03] ### Added diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java index fab4829da551..c111d320cb4e 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java @@ -18,6 +18,10 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.NfsTO; @@ -31,9 +35,11 @@ import com.cloud.utils.script.Script; import org.apache.cloudstack.storage.command.CopyCmdAnswer; import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.utils.cryptsetup.KeyFile; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.cloudstack.utils.qemu.QemuObject; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -83,6 +89,7 @@ private String convertImageToQCow2( final String srcPath, final SnapshotObjectTO dst, final KVMStoragePool secondaryPool, + final byte[] passphrase, int waitMilliSeconds ) throws LibvirtException, QemuImgException, IOException @@ -94,9 +101,22 @@ private String convertImageToQCow2( final QemuImgFile srcFile = new QemuImgFile(srcPath, QemuImg.PhysicalDiskFormat.RAW); final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.QCOW2); - // NOTE: the qemu img will also contain the drbd metadata at the end final QemuImg qemu = new QemuImg(waitMilliSeconds); - qemu.convert(srcFile, dstFile); + if (passphrase != null && passphrase.length > 0) { + // Encrypted volumes are backed up from their decrypted DRBD device, so the snapshot + // data here is plaintext. Encrypt the destination qcow2 with the volume's passphrase + // (LUKS), so the snapshot is not stored in clear text on secondary storage. + try (KeyFile keyFile = new KeyFile(passphrase)) { + final Map options = new HashMap<>(); + final List qemuObjects = new ArrayList<>(); + qemuObjects.add(QemuObject.prepareSecretForQemuImg(QemuImg.PhysicalDiskFormat.QCOW2, + QemuObject.EncryptFormat.LUKS, keyFile.toString(), "sec0", options)); + qemu.convert(srcFile, dstFile, options, qemuObjects, null, true); + } + } else { + // NOTE: the qemu img will also contain the drbd metadata at the end + qemu.convert(srcFile, dstFile); + } LOGGER.info("Backup snapshot '{}' to '{}'", srcPath, dstPath); return dstPath; } @@ -153,14 +173,21 @@ public CopyCmdAnswer execute(LinstorBackupSnapshotCommand cmd, LibvirtComputingR secondaryPool = storagePoolMgr.getStoragePoolByURI(dstDataStore.getUrl()); - String dstPath = convertImageToQCow2(srcPath, dst, secondaryPool, cmd.getWaitInMillSeconds()); + final byte[] passphrase = src.getVolume() != null ? src.getVolume().getPassphrase() : null; + final boolean encrypted = passphrase != null && passphrase.length > 0; - // resize to real volume size, cutting of drbd metadata - String result = qemuShrink(dstPath, src.getVolume().getSize(), cmd.getWaitInMillSeconds()); - if (result != null) { - return new CopyCmdAnswer("qemu-img shrink failed: " + result); + String dstPath = convertImageToQCow2(srcPath, dst, secondaryPool, passphrase, cmd.getWaitInMillSeconds()); + + if (!encrypted) { + // resize to real volume size, cutting of drbd metadata + // For encrypted volumes the source is the decrypted DRBD device (already net-sized, + // no drbd metadata to cut); shrinking an encrypted qcow2 would also need the secret. + String result = qemuShrink(dstPath, src.getVolume().getSize(), cmd.getWaitInMillSeconds()); + if (result != null) { + return new CopyCmdAnswer("qemu-img shrink failed: " + result); + } + LOGGER.info("Backup shrunk " + dstPath + " to actual size " + src.getVolume().getSize()); } - LOGGER.info("Backup shrunk " + dstPath + " to actual size " + src.getVolume().getSize()); SnapshotObjectTO snapshot = setCorrectSnapshotSize(dst, dstPath); LOGGER.info("Actual file size for '{}' is {}", dstPath, snapshot.getPhysicalSize()); @@ -171,6 +198,9 @@ public CopyCmdAnswer execute(LinstorBackupSnapshotCommand cmd, LibvirtComputingR LOGGER.error(error); return new CopyCmdAnswer(cmd, e); } finally { + if (src.getVolume() != null) { + src.getVolume().clearPassphrase(); + } cleanupSecondaryPool(secondaryPool); if (zfsHidden) { zfsSnapdev(true, src.getPath()); diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java index 2d6df5f2296a..51d0ed88e340 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java @@ -17,6 +17,7 @@ package com.cloud.hypervisor.kvm.resource.wrapper; import java.io.File; +import java.util.Collections; import com.cloud.agent.api.to.DataStoreTO; import com.cloud.api.storage.LinstorRevertBackupSnapshotCommand; @@ -31,9 +32,12 @@ import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.cryptsetup.KeyFile; +import org.apache.cloudstack.utils.qemu.QemuImageOptions; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.cloudstack.utils.qemu.QemuObject; import org.joda.time.Duration; import org.libvirt.LibvirtException; @@ -43,8 +47,9 @@ public final class LinstorRevertBackupSnapshotCommandWrapper { private void convertQCow2ToRAW( - KVMStoragePool pool, final String srcPath, final String dstUuid, int waitMilliSeconds) - throws LibvirtException, QemuImgException + KVMStoragePool pool, final String srcPath, final String dstUuid, final byte[] passphrase, + int waitMilliSeconds) + throws LibvirtException, QemuImgException, java.io.IOException { final String dstPath = pool.getPhysicalDisk(dstUuid).getPath(); final QemuImgFile srcQemuFile = new QemuImgFile( @@ -60,7 +65,20 @@ private void convertQCow2ToRAW( } final QemuImg qemu = new QemuImg(waitMilliSeconds, zeroedDevice, true); final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.RAW); - qemu.convert(srcQemuFile, dstFile); + if (passphrase != null && passphrase.length > 0) { + // The backed-up qcow2 is LUKS-encrypted with the volume's passphrase. Decrypt it while + // writing plaintext to the (decrypted) DRBD device; the Linstor LUKS layer re-encrypts it, + // so no qemu encryption must be applied to the destination. + try (KeyFile keyFile = new KeyFile(passphrase)) { + final QemuObject srcSecret = QemuObject.prepareSecretForQemuImg( + QemuImg.PhysicalDiskFormat.QCOW2, QemuObject.EncryptFormat.LUKS, keyFile.toString(), "sec0", null); + final QemuImageOptions srcImageOpts = new QemuImageOptions( + QemuImg.PhysicalDiskFormat.QCOW2, srcPath, "sec0"); + qemu.convert(srcQemuFile, dstFile, null, Collections.singletonList(srcSecret), srcImageOpts, null, false); + } + } else { + qemu.convert(srcQemuFile, dstFile); + } } @Override @@ -84,10 +102,13 @@ public CopyCmdAnswer execute(LinstorRevertBackupSnapshotCommand cmd, LibvirtComp secondaryPool = storagePoolMgr.getStoragePoolByURI( srcDataStore.getUrl() + File.separator + srcFile.getParent()); + // The destination volume is the (same) original volume, whose passphrase the backed-up + // qcow2 was encrypted with; use it to decrypt while restoring. convertQCow2ToRAW( linstorPool, secondaryPool.getLocalPath() + File.separator + srcFile.getName(), dst.getPath(), + dst.getPassphrase(), cmd.getWaitInMillSeconds()); final VolumeObjectTO dstVolume = new VolumeObjectTO(); @@ -99,6 +120,7 @@ public CopyCmdAnswer execute(LinstorRevertBackupSnapshotCommand cmd, LibvirtComp logger.error(error); return new CopyCmdAnswer(cmd, e); } finally { + dst.clearPassphrase(); LinstorBackupSnapshotCommandWrapper.cleanupSecondaryPool(secondaryPool); } } diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java index 672731fd07c9..c3b4e73ead03 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java @@ -1095,12 +1095,22 @@ protected Answer copySnapshot(DataObject srcData, DataObject destData) { VirtualMachineManager.ExecuteInSequence.value()); cmd.setOptions(options); - Optional optEP = getDiskfullEP(api, pool, rscName); + // For encrypted volumes Linstor adds a LUKS layer (DRBD -> LUKS -> STORAGE). The storage + // layer snapshot device (getSnapshotPath) therefore only exposes the raw LUKS ciphertext, + // while restore writes onto the decrypted DRBD device (/dev/drbd/by-res/.../0). Backing up + // the ciphertext and writing it back to the decrypted layer corrupts the volume (and the + // shrink to the net volume size would even truncate the ciphertext). So for encrypted + // volumes we never read the storage snapshot directly: restore the snapshot into a temporary + // resource and back up its decrypted DRBD device instead, symmetric to the restore path. + final boolean encrypted = snapshotObject.getBaseVolume().getPassphraseId() != null; + Optional optEP = encrypted ? + Optional.empty() : getDiskfullEP(api, pool, rscName); Answer answer; if (optEP.isPresent()) { answer = optEP.get().sendMessage(cmd); } else { - logger.debug("No diskfull endpoint found to copy image, creating diskless endpoint"); + logger.debug("No diskfull endpoint used to copy image (encrypted={}), using temporary resource", + encrypted); answer = copyFromTemporaryResource(api, pool, rscName, snapshotName, snapshotObject, cmd); } return answer; diff --git a/test/integration/plugins/linstor/README.md b/test/integration/plugins/linstor/README.md index 4505d1b7d57c..4971c9506b5b 100644 --- a/test/integration/plugins/linstor/README.md +++ b/test/integration/plugins/linstor/README.md @@ -48,3 +48,21 @@ nosetests --with-marvin --marvin-config= /test/ ``` You can also run these tests out of the box with PyDev or PyCharm or whatever. + +## Encrypted snapshot tests + +`test_linstor_encrypted_snapshots.py` covers the encrypted-volume snapshot round trip +(create encrypted root disk -> snapshot -> revert / create-volume-from-snapshot) and that the +backed-up qcow2 on secondary storage is itself LUKS encrypted. + +Extra prerequisites: + +* At least one KVM host with volume-encryption support (`host.encryptionsupported == true`, i.e. + cryptsetup/qemu LUKS available). Tests self-skip if none is found. +* The Linstor resource group used (`acs-basic`) must be able to add a LUKS layer to its volumes. +* `lin.backup.snapshots` must be enabled (default) so snapshots are backed up to secondary storage; + the test sets it. With it disabled the qcow2 path is not exercised. + +``` +nosetests --with-marvin --marvin-config= /test/integration/plugins/linstor/test_linstor_encrypted_snapshots.py --zone= --hypervisor=kvm +``` diff --git a/test/integration/plugins/linstor/test_linstor_encrypted_snapshots.py b/test/integration/plugins/linstor/test_linstor_encrypted_snapshots.py new file mode 100644 index 000000000000..5f440309bb34 --- /dev/null +++ b/test/integration/plugins/linstor/test_linstor_encrypted_snapshots.py @@ -0,0 +1,444 @@ +# 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. + +import json +import logging +import os +import random +import socket +import time + +# All tests inherit from cloudstackTestCase +from marvin.cloudstackTestCase import cloudstackTestCase + +# Import Integration Libraries +from marvin.cloudstackAPI import createVolume +from marvin.cloudstackException import CloudstackAPIException +from marvin.lib.base import Account, Configurations, Host, ServiceOffering, \ + Snapshot, StoragePool, User, VirtualMachine, Volume +from marvin.lib.common import get_domain, get_template, get_zone, list_hosts, list_virtual_machines, list_volumes +from marvin.lib.utils import cleanup_resources +from marvin.sshClient import SshClient +from nose.plugins.attrib import attr + +# Prerequisites: +# Only one zone / pod / cluster +# Only KVM hypervisor (Linstor only supports KVM) +# At least one KVM host with volume-encryption support (host.encryptionsupported == True), +# i.e. cryptsetup/qemu with LUKS available on the host. +# One Linstor storage pool whose resource-group can add a LUKS layer (encrypted volumes). +# 'lin.backup.snapshots' enabled (default true) so snapshots are backed up to secondary storage +# as qcow2 -- that is the path these tests are meant to exercise. With it disabled, snapshots +# stay on primary as Linstor system snapshots and a different (rollback) code path is used. +# +# What this exercises (the encrypted-snapshot round trip): +# * backup: decrypted DRBD device -> LUKS-encrypted qcow2 on secondary +# * revert: encrypted qcow2 -> decrypted, written to the DRBD device (Linstor re-encrypts) +# * create: encrypted qcow2 -> new volume via createVolumeFromSnapshot (KVMStorageProcessor) +# +# Note on verification: Linstor encrypts inside the DRBD stack (LUKS layer), so the libvirt domain +# XML does NOT carry like hypervisor-based encryption does. Correctness +# is therefore verified by a data round trip (write marker -> snapshot -> change -> restore -> read), +# and encryption-at-rest is verified by inspecting the backed-up qcow2 with 'qemu-img info'. + +MARKER_PATH = "/root/cs_enc_marker.txt" + + +class TestData: + account = "account" + computeOffering = "computeoffering" + diskName = "diskname" + domainId = "domainId" + hypervisor = "hypervisor" + provider = "provider" + scope = "scope" + storageTag = "linstor" + tags = "tags" + user = "user" + virtualMachine = "virtualmachine" + zoneId = "zoneId" + + def __init__(self, linstor_controller_url): + self.testdata = { + TestData.account: { + "email": "test-enc@test.com", + "firstname": "John", + "lastname": "Doe", + "username": "test-enc", + "password": "test" + }, + TestData.user: { + "email": "user-enc@test.com", + "firstname": "Jane", + "lastname": "Doe", + "username": "test-enc-user", + "password": "password" + }, + "primarystorage": { + "name": "LinstorEncPool-%d" % random.randint(0, 100000), + TestData.scope: "ZONE", + "url": linstor_controller_url, + TestData.provider: "Linstor", + TestData.tags: TestData.storageTag, + TestData.hypervisor: "KVM", + "details": { + "resourceGroup": "acs-basic" + } + }, + TestData.virtualMachine: { + "name": "TestEncVM", + "displayname": "Test Encrypted VM" + }, + # encryptroot=True is passed as a create kwarg, not in this dict + TestData.computeOffering: { + "name": "Linstor_Compute_Encrypted", + "displaytext": "Linstor_Compute_Encrypted", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + TestData.tags: TestData.storageTag + }, + TestData.diskName: "restored-from-enc-snap", + TestData.zoneId: 1, + TestData.domainId: 1, + } + + +class ServiceReady: + @classmethod + def ready(cls, hostname, port): + try: + s = socket.create_connection((hostname, port), timeout=1) + s.close() + return True + except (ConnectionRefusedError, socket.timeout, OSError): + return False + + @classmethod + def wait(cls, hostname, port, wait_interval=5, timeout=120, service_name='ssh'): + starttime = int(round(time.time() * 1000)) + while not cls.ready(hostname, port): + if starttime + timeout * 1000 < int(round(time.time() * 1000)): + raise RuntimeError("{s} {h} cannot be reached.".format(s=service_name, h=hostname)) + time.sleep(wait_interval) + return True + + @classmethod + def wait_ssh_ready(cls, hostname, wait_interval=2, timeout=120): + return cls.wait(hostname, 22, wait_interval, timeout, "ssh") + + +class TestLinstorEncryptedSnapshots(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + testclient = super(TestLinstorEncryptedSnapshots, cls).getClsTestClient() + + cls.apiClient = testclient.getApiClient() + cls.dbConnection = testclient.getDbConnection() + + cls._cleanup = [] + cls.skip_reason = None + + # Linstor is KVM-only, so the hypervisor type is not probed via getHypervisorInfo() (which is + # only populated when nosetests is invoked with --hypervisor). Instead we require an actual KVM + # host that supports volume encryption below. + + # The first host runs the Linstor controller (per the Linstor test prerequisites). + first_host = list_hosts(cls.apiClient)[0] + cls.testdata = TestData(first_host.ipaddress).testdata + + cls.zone = get_zone(cls.apiClient, zone_id=cls.testdata[TestData.zoneId]) + cls.domain = get_domain(cls.apiClient, cls.testdata[TestData.domainId]) + cls.template = get_template(cls.apiClient, cls.zone.id, hypervisor="KVM") + + # Host SSH credentials, only needed by test_03 to inspect the backed-up qcow2 on secondary + # storage. A full marvin config carries these under zones->pods->clusters->hosts, but a + # lightweight config may omit them; in that case fall back to HOST_SSH_USER / HOST_SSH_PASSWORD + # env vars. Never fail class setup over this - the other tests don't need host SSH. + cls.hostConfig = None + try: + cls.hostConfig = cls.config.__dict__["zones"][0].__dict__["pods"][0].__dict__["clusters"][0] \ + .__dict__["hosts"][0].__dict__ + except (KeyError, IndexError, AttributeError, TypeError): + host_user = os.environ.get("HOST_SSH_USER") + host_pass = os.environ.get("HOST_SSH_PASSWORD") + if host_user and host_pass: + cls.hostConfig = {"username": host_user, "password": host_pass} + + if not cls._encryption_capable_host_exists(): + cls.skip_reason = "No KVM host with volume-encryption support found" + return + + # Ensure snapshots are backed up to secondary storage (the path under test). + Configurations.update(cls.apiClient, name="lin.backup.snapshots", value="true") + + primarystorage = cls.testdata["primarystorage"] + # Registering the pool makes the management server call the Linstor controller (to read the + # resource-group capacity). If the controller enforces authentication, that call needs an API + # token, supplied as the 'lin.auth.apitoken' add-pool detail. Provide it via LINSTOR_API_TOKEN + # so it is never hard-coded; leave it unset for an unauthenticated controller. + api_token = os.environ.get("LINSTOR_API_TOKEN") + if api_token: + primarystorage["details"]["lin.auth.apitoken"] = api_token + + try: + cls.primary_storage = StoragePool.create( + cls.apiClient, + primarystorage, + scope=primarystorage[TestData.scope], + zoneid=cls.zone.id, + provider=primarystorage[TestData.provider], + tags=primarystorage[TestData.tags], + hypervisor=primarystorage[TestData.hypervisor] + ) + except Exception as e: + cls.skip_reason = ( + "Could not register the Linstor primary storage pool (%s). If the Linstor controller " + "requires authentication, set the LINSTOR_API_TOKEN env var to a valid controller API " + "token before running these tests." % e) + return + + # Compute offering with encrypted root, pinned to the Linstor pool via the storage tag. + cls.compute_offering_encrypted = ServiceOffering.create( + cls.apiClient, + cls.testdata[TestData.computeOffering], + encryptroot=True + ) + + cls.account = Account.create(cls.apiClient, cls.testdata[TestData.account], admin=1) + cls.user = User.create( + cls.apiClient, cls.testdata[TestData.user], + account=cls.account.name, domainid=cls.domain.id) + + cls._cleanup = [ + cls.compute_offering_encrypted, + cls.user, + cls.account, + ] + + @classmethod + def tearDownClass(cls): + try: + cleanup_resources(cls.apiClient, cls._cleanup) + if getattr(cls, "primary_storage", None) is not None: + cls.primary_storage.delete(cls.apiClient) + except Exception as e: + logging.debug("Exception in tearDownClass: %s" % e) + + def setUp(self): + if self.skip_reason: + self.skipTest(self.skip_reason) + self.cleanup = [] + + def tearDown(self): + cleanup_resources(self.apiClient, self.cleanup) + + # --------------------------------------------------------------------- # + # Tests + # --------------------------------------------------------------------- # + + @attr(tags=['basic'], required_hardware=True) + def test_01_revert_encrypted_root_snapshot(self): + """Snapshot an encrypted root volume, change it, revert, and verify the data and boot.""" + vm = self._deploy_encrypted_vm("TestEncVM-revert") + + # 1. write a marker into the encrypted root volume + self._write_marker(vm, "linstor-encrypted-v1") + + # 2. snapshot the (stopped) root volume -> encrypted qcow2 on secondary + vm.stop(self.apiClient) + snapshot = self._snapshot_root_volume(vm) + + # 3. change the data so a successful revert is detectable + self._start_vm(vm) + self._write_marker(vm, "linstor-encrypted-v2-CHANGED") + + # 4. revert the volume to the snapshot (requires the VM stopped) + vm.stop(self.apiClient) + Volume.revertToSnapshot(self.apiClient, snapshot.id) + + # 5. the VM must boot again and the original data must be back + self._start_vm(vm) + restored = self._read_marker(vm) + self.assertEqual( + "linstor-encrypted-v1", restored, + "Reverted encrypted root volume has wrong content (got %r) - decryption/round-trip broken" % restored + ) + + @attr(tags=['basic'], required_hardware=True) + def test_02_create_volume_from_encrypted_snapshot_is_rejected(self): + """Creating a new volume from an encrypted volume's snapshot must be rejected by CloudStack. + + CloudStack core (VolumeApiServiceImpl) unconditionally blocks this for any encrypted source + volume ("Cannot create new volumes from encrypted volume snapshots"), so the request must never + reach the storage layer. This is a guard test: if the limitation is ever lifted, decryption + support for the create-from-snapshot path (KVMStorageProcessor / LinstorStorageAdaptor) must be + added and this test updated accordingly. + """ + vm = self._deploy_encrypted_vm("TestEncVM-create") + + self._write_marker(vm, "linstor-encrypted-create-src") + vm.stop(self.apiClient) + snapshot = self._snapshot_root_volume(vm) + + cmd = createVolume.createVolumeCmd() + cmd.name = "%s-%d" % (self.testdata[TestData.diskName], random.randint(0, 100000)) + cmd.zoneid = self.zone.id + cmd.account = self.account.name + cmd.domainid = self.domain.id + cmd.snapshotid = snapshot.id + + try: + self.apiClient.createVolume(cmd) + self.fail("Creating a volume from an encrypted volume snapshot should have been rejected") + except CloudstackAPIException as e: + self.assertIn( + "encrypted volume snapshots", str(e), + "Unexpected error creating volume from encrypted snapshot: %s" % e + ) + + @attr(tags=['basic'], required_hardware=True) + def test_03_backed_up_snapshot_qcow2_is_encrypted(self): + """The qcow2 written to secondary storage for an encrypted volume must itself be LUKS encrypted.""" + if not self.hostConfig: + self.skipTest("No host SSH credentials available (set HOST_SSH_USER/HOST_SSH_PASSWORD or " + "provide them in the marvin config) - cannot inspect the secondary-storage qcow2") + vm = self._deploy_encrypted_vm("TestEncVM-atrest") + self._write_marker(vm, "linstor-encrypted-atrest") + vm.stop(self.apiClient) + snapshot = self._snapshot_root_volume(vm) + + info = self._qemu_img_info_of_backed_up_snapshot(snapshot) + if info is None: + self.skipTest("Could not locate the backed-up snapshot on secondary storage to inspect it") + + encrypted = bool(info.get("encrypted")) or "encrypt" in json.dumps(info.get("format-specific", {})) + self.assertTrue( + encrypted, + "Backed-up snapshot qcow2 is NOT encrypted at rest: %s" % json.dumps(info) + ) + + # --------------------------------------------------------------------- # + # Helpers + # --------------------------------------------------------------------- # + + def _deploy_encrypted_vm(self, name): + vm = VirtualMachine.create( + self.apiClient, + {"name": name, "displayname": name}, + accountid=self.account.name, + zoneid=self.zone.id, + serviceofferingid=self.compute_offering_encrypted.id, + templateid=self.template.id, + domainid=self.domain.id, + startvm=False, + mode='basic', + ) + self.cleanup.insert(0, vm) + self._start_vm(vm) + return vm + + def _snapshot_root_volume(self, vm): + root = list_volumes(self.apiClient, virtualmachineid=vm.id, type="ROOT", listall=True)[0] + snapshot = Snapshot.create( + self.apiClient, + volume_id=root.id, + account=self.account.name, + domainid=self.domain.id, + ) + self.assertIsNotNone(snapshot, "Could not create snapshot of encrypted root volume") + self.cleanup.insert(0, snapshot) + return snapshot + + def _vm_ssh(self, vm): + # The VM is deployed stopped, so its instance has no ssh_ip yet; the IP may also change across + # stop/start cycles. Always pass the current address from a fresh lookup. + ipaddress = self._get_vm(vm.id).ipaddress + return vm.get_ssh_client(ipaddress=ipaddress, reconnect=True, retries=5) + + def _write_marker(self, vm, content): + ssh = self._vm_ssh(vm) + ssh.execute("echo '%s' > %s" % (content, MARKER_PATH)) + ssh.execute("sync") + + def _read_marker(self, vm): + ssh = self._vm_ssh(vm) + result = ssh.execute("cat %s" % MARKER_PATH) + return result[0].strip() if result else None + + @classmethod + def _encryption_capable_host_exists(cls): + hosts = Host.list(cls.apiClient, zoneid=cls.zone.id, type='Routing', hypervisor='KVM', state='Up') + return any(getattr(h, "encryptionsupported", False) for h in (hosts or [])) + + @classmethod + def _get_vm(cls, vm_id): + return list_virtual_machines(cls.apiClient, id=vm_id)[0] + + @classmethod + def _start_vm(cls, vm): + vm_for_check = cls._get_vm(vm.id) + if vm_for_check.state == VirtualMachine.STOPPED: + vm.start(cls.apiClient) + vm_for_check = cls._get_vm(vm.id) + ServiceReady.wait_ssh_ready(vm_for_check.ipaddress) + return vm_for_check + + def _host_ssh(self): + host = list_hosts(self.apiClient, type='Routing', hypervisor='KVM', state='Up')[0] + return SshClient( + host=host.ipaddress, port=22, + user=self.hostConfig['username'], passwd=self.hostConfig['password']) + + def _qemu_img_info_of_backed_up_snapshot(self, snapshot): + """Self-mount the secondary NFS export on a host and run 'qemu-img info' on the snapshot file.""" + # The backed-up snapshot's physical path on secondary storage isn't exposed via the API, so we + # read it from the DB. The DB may be unreachable from where the tests run (e.g. MariaDB bound to + # localhost on the management server); in that case return None so the test skips. + try: + rows = self.dbConnection.execute( + "SELECT ss.install_path " + "FROM snapshot_store_ref ss JOIN snapshots s ON s.id = ss.snapshot_id " + "WHERE s.uuid = '%s' AND ss.store_role = 'Image'" % snapshot.id) + store = self.dbConnection.execute( + "SELECT url FROM image_store WHERE role = 'Image' AND removed IS NULL LIMIT 1") + except Exception as e: + logging.debug("DB lookup for snapshot install path failed: %s" % e) + return None + + if not rows or not rows[0][0] or not store or not store[0][0]: + return None + install_path = rows[0][0] + url = store[0][0] # e.g. nfs:/// + if not url.startswith("nfs://"): + return None + server, export = url[len("nfs://"):].split("/", 1) + + ssh = self._host_ssh() + mount_point = "/tmp/cs_sectest_%d" % random.randint(0, 100000) + try: + ssh.execute("mkdir -p %s" % mount_point) + ssh.execute("mount -t nfs -o ro %s:/%s %s" % (server, export, mount_point)) + out = ssh.execute("qemu-img info --output=json %s/%s" % (mount_point, install_path)) + return json.loads("".join(out)) if out else None + except Exception as e: + logging.debug("qemu-img info on secondary failed: %s" % e) + return None + finally: + ssh.execute("umount %s 2>/dev/null; rmdir %s 2>/dev/null" % (mount_point, mount_point)) From 265b554d608ef42d7c77de0eab088a46f8c4fd81 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 10 Jul 2026 13:04:14 +0200 Subject: [PATCH 175/205] Fix 4.22 build failures after forward-merge and rename 42010to42100 to 42040to42100 --- .../cloud/upgrade/DatabaseUpgradeChecker.java | 6 ++-- ...0to42100.java => Upgrade42040to42100.java} | 8 ++--- ...up.sql => schema-42040to42100-cleanup.sql} | 2 +- ...010to42100.sql => schema-42040to42100.sql} | 2 +- .../upgrade/DatabaseUpgradeCheckerTest.java | 32 ++++++++++++++++--- ...Test.java => Upgrade42040to42100Test.java} | 4 +-- 6 files changed, 38 insertions(+), 16 deletions(-) rename engine/schema/src/main/java/com/cloud/upgrade/dao/{Upgrade42010to42100.java => Upgrade42040to42100.java} (97%) rename engine/schema/src/main/resources/META-INF/db/{schema-42010to42100-cleanup.sql => schema-42040to42100-cleanup.sql} (93%) rename engine/schema/src/main/resources/META-INF/db/{schema-42010to42100.sql => schema-42040to42100.sql} (99%) rename engine/schema/src/test/java/com/cloud/upgrade/dao/{Upgrade42010to42100Test.java => Upgrade42040to42100Test.java} (98%) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 170a12f93740..3868ca960e06 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -89,11 +89,11 @@ import com.cloud.upgrade.dao.Upgrade41900to41910; import com.cloud.upgrade.dao.Upgrade41910to42000; import com.cloud.upgrade.dao.Upgrade42000to42010; -import com.cloud.upgrade.dao.Upgrade42010to42100; -import com.cloud.upgrade.dao.Upgrade42200to42210; import com.cloud.upgrade.dao.Upgrade42020to42030; import com.cloud.upgrade.dao.Upgrade42030to42040; +import com.cloud.upgrade.dao.Upgrade42040to42100; import com.cloud.upgrade.dao.Upgrade42100to42200; +import com.cloud.upgrade.dao.Upgrade42200to42210; import com.cloud.upgrade.dao.Upgrade420to421; import com.cloud.upgrade.dao.Upgrade421to430; import com.cloud.upgrade.dao.Upgrade430to440; @@ -241,9 +241,9 @@ public DatabaseUpgradeChecker() { .next("4.19.0.0", new Upgrade41900to41910()) .next("4.19.1.0", new Upgrade41910to42000()) .next("4.20.0.0", new Upgrade42000to42010()) - .next("4.20.1.0", new Upgrade42010to42100()) .next("4.20.2.0", new Upgrade42020to42030()) .next("4.20.3.0", new Upgrade42030to42040()) + .next("4.20.4.0", new Upgrade42040to42100()) .next("4.21.0.0", new Upgrade42100to42200()) .next("4.22.0.0", new Upgrade42200to42210()) .build(); diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42040to42100.java similarity index 97% rename from engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java rename to engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42040to42100.java index 786ee5afbc8e..64ac2c90bc07 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42040to42100.java @@ -32,12 +32,12 @@ import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; -public class Upgrade42010to42100 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { +public class Upgrade42040to42100 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { private SystemVmTemplateRegistration systemVmTemplateRegistration; @Override public String[] getUpgradableVersionRange() { - return new String[] {"4.20.1.0", "4.21.0.0"}; + return new String[] {"4.20.4.0", "4.21.0.0"}; } @Override @@ -52,7 +52,7 @@ public boolean supportsRollingUpgrade() { @Override public InputStream[] getPrepareScripts() { - final String scriptFile = "META-INF/db/schema-42010to42100.sql"; + final String scriptFile = "META-INF/db/schema-42040to42100.sql"; final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); if (script == null) { throw new CloudRuntimeException("Unable to find " + scriptFile); @@ -69,7 +69,7 @@ public void performDataMigration(Connection conn) { @Override public InputStream[] getCleanupScripts() { - final String scriptFile = "META-INF/db/schema-42010to42100-cleanup.sql"; + final String scriptFile = "META-INF/db/schema-42040to42100-cleanup.sql"; final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); if (script == null) { throw new CloudRuntimeException("Unable to find " + scriptFile); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-42040to42100-cleanup.sql similarity index 93% rename from engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql rename to engine/schema/src/main/resources/META-INF/db/schema-42040to42100-cleanup.sql index 5f257f2965bd..b63e918b389b 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42040to42100-cleanup.sql @@ -16,5 +16,5 @@ -- under the License. --; --- Schema upgrade cleanup from 4.20.1.0 to 4.21.0.0 +-- Schema upgrade cleanup from 4.20.4.0 to 4.21.0.0 --; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42040to42100.sql similarity index 99% rename from engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql rename to engine/schema/src/main/resources/META-INF/db/schema-42040to42100.sql index 000b54b72078..bf7f3aeaf0f8 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42040to42100.sql @@ -16,7 +16,7 @@ -- under the License. --; --- Schema upgrade from 4.20.1.0 to 4.21.0.0 +-- Schema upgrade from 4.20.4.0 to 4.21.0.0 --; CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule', 'max_backups', 'INT(8) UNSIGNED NOT NULL DEFAULT 0 COMMENT ''Maximum number of backups to be retained'''); diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index ab64e4698f01..884398cf410d 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -44,7 +44,9 @@ import com.cloud.upgrade.dao.Upgrade41120to41200; import com.cloud.upgrade.dao.Upgrade41510to41520; import com.cloud.upgrade.dao.Upgrade41610to41700; -import com.cloud.upgrade.dao.Upgrade42010to42100; +import com.cloud.upgrade.dao.Upgrade42020to42030; +import com.cloud.upgrade.dao.Upgrade42030to42040; +import com.cloud.upgrade.dao.Upgrade42040to42100; import com.cloud.upgrade.dao.Upgrade452to453; import com.cloud.upgrade.dao.Upgrade453to460; import com.cloud.upgrade.dao.Upgrade460to461; @@ -382,12 +384,12 @@ public void isNotStandalone() throws SQLException { } @Test - public void testCalculateUpgradePath42010to42100() { + public void testCalculateUpgradePath42010to42030() { final CloudStackVersion dbVersion = CloudStackVersion.parse("4.20.1.0"); assertNotNull(dbVersion); - final CloudStackVersion currentVersion = CloudStackVersion.parse("4.21.0.0"); + final CloudStackVersion currentVersion = CloudStackVersion.parse("4.20.3.0"); assertNotNull(currentVersion); final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); @@ -395,9 +397,29 @@ public void testCalculateUpgradePath42010to42100() { assertNotNull(upgrades); assertEquals(1, upgrades.length); - assertTrue(upgrades[0] instanceof Upgrade42010to42100); + assertTrue(upgrades[0] instanceof Upgrade42020to42030); - assertArrayEquals(new String[]{"4.20.1.0", "4.21.0.0"}, upgrades[0].getUpgradableVersionRange()); + assertArrayEquals(new String[]{"4.20.2.0", "4.20.3.0"}, upgrades[0].getUpgradableVersionRange()); assertEquals(currentVersion.toString(), upgrades[0].getUpgradedVersion()); } + + @Test + public void testCalculateUpgradePath42010to42100() { + + final CloudStackVersion dbVersion = CloudStackVersion.parse("4.20.1.0"); + assertNotNull(dbVersion); + + final CloudStackVersion currentVersion = CloudStackVersion.parse("4.21.0.0"); + assertNotNull(currentVersion); + + final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); + final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); + + assertNotNull(upgrades); + assertEquals(3, upgrades.length); + assertTrue(upgrades[0] instanceof Upgrade42020to42030); + assertTrue(upgrades[1] instanceof Upgrade42030to42040); + assertTrue(upgrades[2] instanceof Upgrade42040to42100); + assertEquals(currentVersion.toString(), upgrades[2].getUpgradedVersion()); + } } diff --git a/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java b/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42040to42100Test.java similarity index 98% rename from engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java rename to engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42040to42100Test.java index 16908f6aaac0..5a2b55f03384 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42040to42100Test.java @@ -35,9 +35,9 @@ import com.cloud.utils.db.TransactionLegacy; @RunWith(MockitoJUnitRunner.class) -public class Upgrade42010to42100Test { +public class Upgrade42040to42100Test { @Spy - Upgrade42010to42100 upgrade; + Upgrade42040to42100 upgrade; @Mock private Connection conn; From 4d54125bb62de999cba3b3b05622a391fc564985 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Fri, 10 Jul 2026 08:32:36 -0400 Subject: [PATCH 176/205] CLVM: Fix volume mapping and disk path matching for storage migration (#13468) --- .../cloud/vm/VirtualMachineManagerImpl.java | 3 +- .../motion/AncientDataMotionStrategy.java | 21 ++++++++++ .../storage/volume/VolumeServiceImpl.java | 6 +++ .../volume/VolumeServiceImplClvmTest.java | 39 ++++++++++++++++++- .../resource/LibvirtComputingResource.java | 2 +- .../kvm/storage/KVMStorageProcessor.java | 6 ++- .../CloudStackPrimaryDataStoreDriverImpl.java | 14 ++----- 7 files changed, 76 insertions(+), 15 deletions(-) diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index a232bdd05daa..d5f7937a463f 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -3543,7 +3543,8 @@ protected void createStoragePoolMappingsForVolumes(VirtualMachineProfile profile protected boolean shouldMapVolume(VirtualMachineProfile profile, StoragePoolVO currentPool) { boolean isManaged = currentPool.isManaged(); boolean isNotKvm = HypervisorType.KVM != profile.getHypervisorType(); - return isNotKvm || isManaged; + boolean isClvm = ClvmPoolManager.isClvmPoolType(currentPool.getPoolType()); + return isNotKvm || isManaged || isClvm; } /** diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java index dd54dd580052..bbf775de27ad 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java @@ -445,6 +445,9 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) answer = new Answer(cmd, false, errMsg); } else { answer = ep.sendMessage(cmd); + if (answer != null && answer.getResult()) { + setClvmLockHostIdIfApplicable(destData, ep); + } } return answer; } @@ -500,6 +503,7 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) imageStore.delete(objOnImageStore); return answer; } + setClvmLockHostIdIfApplicable(destData, ep); } catch (Exception e) { if (imageStore.exists(objOnImageStore)) { objOnImageStore.processEvent(Event.OperationFailed); @@ -523,6 +527,9 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) answer = new Answer(cmd, false, errMsg); } else { answer = ep.sendMessage(cmd); + if (answer != null && answer.getResult()) { + setClvmLockHostIdIfApplicable(destData, ep); + } } // delete volume on cache store if (cacheData != null) { @@ -532,6 +539,17 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) } } + private void setClvmLockHostIdIfApplicable(DataObject destData, EndPoint ep) { + if (ep == null || !(destData instanceof VolumeInfo)) { + return; + } + VolumeInfo destVolume = (VolumeInfo) destData; + if (ClvmPoolManager.isClvmPoolType(destVolume.getStoragePoolType())) { + clvmPoolManager.setClvmLockHostId(destVolume.getId(), ep.getId()); + logger.debug("Set CLVM lock host {} for migrated volume {}", ep.getId(), destVolume.getUuid()); + } + } + private boolean canBypassSecondaryStorage(DataObject srcData, DataObject destData) { if (srcData instanceof VolumeInfo) { if (((VolumeInfo)srcData).isDirectDownload()) { @@ -650,6 +668,9 @@ protected Answer migrateVolumeToPool(DataObject srcData, DataObject destData) { if (destPool.getPoolType() == StoragePoolType.CLVM) { volumeVo.setFormat(ImageFormat.RAW); } + if (ClvmPoolManager.isClvmPoolType(destPool.getPoolType())) { + clvmPoolManager.setClvmLockHostId(volume.getId(), ep.getId()); + } // For SMB, pool credentials are also stored in the uri query string. We trim the query string // part here to make sure the credentials do not get stored in the db unencrypted. String folder = destPool.getPath(); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 8a0f93fe5fc3..f8d9cab56f73 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -3118,6 +3118,12 @@ public boolean isLockTransferRequired(VolumeInfo volumeToAttach, StoragePoolType } if (volumePoolId == null || !volumePoolId.equals(vmPoolId)) { + Long volumeLockHostId = findVolumeLockHost(volumeToAttach); + if (volumeLockHostId != null && vmHostId != null && !volumeLockHostId.equals(vmHostId)) { + logger.info("CLVM cross-pool lock transfer required: Volume {} on pool {} lock is on host {} but VM is on host {}", + volumeToAttach.getUuid(), volumePoolId, volumeLockHostId, vmHostId); + return true; + } return false; } diff --git a/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java index 38af2a7550b3..8d355263a6c3 100644 --- a/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java +++ b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java @@ -167,14 +167,49 @@ public void testIsLockTransferRequired_NonCLVMPool() { } @Test - public void testIsLockTransferRequired_DifferentPools() { + public void testIsLockTransferRequired_DifferentPools_LockOnDifferentHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_2); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_2, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentPools_LockOnSameHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_2, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentPools_NoLockHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + assertFalse(volumeService.isLockTransferRequired( volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, POOL_ID_1, POOL_ID_2, HOST_ID_1)); } @Test - public void testIsLockTransferRequired_NullPoolIds() { + public void testIsLockTransferRequired_NullPoolIds_LockOnDifferentHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_2); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + null, POOL_ID_1, HOST_ID_1)); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, null, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_NullPoolIds_NoLockHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + assertFalse(volumeService.isLockTransferRequired( volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, null, POOL_ID_1, HOST_ID_1)); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 41716881fa4a..acc4a878deaa 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -7038,7 +7038,7 @@ private static boolean isClvmVolume(DiskDef disk, VirtualMachineTO vmSpec) { continue; } VolumeObjectTO volumeTO = (VolumeObjectTO) diskTO.getData(); - if (!diskPath.equals(volumeTO.getPath()) && !diskPath.equals(diskTO.getPath())) { + if (!diskPath.substring(diskPath.lastIndexOf(File.separator) + 1).equals(volumeTO.getPath())) { continue; } DataStoreTO dataStore = volumeTO.getDataStore(); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 009e1decee2b..0f5c2f0913ba 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -570,7 +570,11 @@ public Answer cloneVolumeFromBaseTemplate(final CopyCommand cmd) { final VolumeObjectTO newVol = new VolumeObjectTO(); newVol.setPath(vol.getName()); - newVol.setSize(volume.getSize()); + if (StoragePoolType.CLVM_NG.equals(primaryStore.getPoolType()) && vol != null && vol.getVirtualSize() > 0) { + newVol.setSize(vol.getVirtualSize()); + } else { + newVol.setSize(volume.getSize()); + } if (vol.getQemuEncryptFormat() != null) { newVol.setEncryptFormat(vol.getQemuEncryptFormat().toString()); } diff --git a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java index 3e9fa8a5438d..a12836cdb965 100644 --- a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java @@ -436,17 +436,11 @@ public void revertSnapshot(SnapshotInfo snapshot, SnapshotInfo snapshotOnPrimary CommandResult result = new CommandResult(); try { EndPoint ep = null; - VolumeInfo volumeInfo = volFactory.getVolume(snapshot.getVolumeId(), DataStoreRole.Primary); - - StoragePoolVO storagePool = primaryStoreDao.findById(volumeInfo.getPoolId()); - if (storagePool != null && storagePool.getPoolType() == StoragePoolType.CLVM) { - ep = epSelector.select(volumeInfo); + if (snapshotOnPrimaryStore != null) { + ep = epSelector.select(snapshotOnPrimaryStore); } else { - if (snapshotOnPrimaryStore != null) { - ep = epSelector.select(snapshotOnPrimaryStore); - } else { - ep = epSelector.select(volumeInfo); - } + VolumeInfo volumeInfo = volFactory.getVolume(snapshot.getVolumeId(), DataStoreRole.Primary); + ep = epSelector.select(volumeInfo); } if ( ep == null ){ From 1c1611df2fd32ec160171de9f5f9031275f882bf Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 10 Jul 2026 15:28:33 +0200 Subject: [PATCH 177/205] Ubuntu 26.04: remove requiretty from /etc/sudoers.d/cloudstack and fix setup-sysvm-tmplt (#13476) * debian/rules: remove requiretty from /etc/sudoers.d/cloudstack * fix scripts/storage/secondary/setup-sysvm-tmplt /tmp is a tmpfs type of filesystem ``` root@mgmt1:~# /usr/share/cloudstack-common/scripts/storage/secondary/setup-sysvm-tmplt -u 002c593f-eac9-4fee-85dd-44887a3f2042 -f /usr/share/cloudstack-m +anagement/templates/systemvm/systemvmtemplate-4.22.0-x86_64-kvm.qcow2.bz2 -h kvm -d /tmp/tmp5782242889444060623/template/tmpl/1/3 .... Insufficient free disk space for target folder /tmp/tmp5782242889444060623/template/tmpl/1/3: avail=1739872k req=2120000k root@mgmt1:~# mount | grep /tmp tmpfs on /tmp type tmpfs (rw,nosuid,nodev,nr_inodes=1048576,inode64,usrquota) ``` * marvin: revert mysql-connector-python to 8.0.30 * Apply "mysql-connector-python >= 8.4.0", * register systemvm template via /var/tmp in java too * Revert "register systemvm template via /var/tmp in java too" This reverts commit c0147c5e90b49dfeefae9cd10fbe02c3023854af. * mgmt: add -Djava.io.tmpdir=/var/tmp to JAVA_OPTS * fix scripts/storage/secondary/createtmplt.sh --- debian/rules | 1 + packaging/systemd/cloudstack-management.default | 2 +- scripts/storage/secondary/createtmplt.sh | 2 +- scripts/storage/secondary/setup-sysvm-tmplt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/debian/rules b/debian/rules index 842fc2408af7..327447823308 100755 --- a/debian/rules +++ b/debian/rules @@ -95,6 +95,7 @@ override_dh_auto_install: # nast hack for a couple of configuration files mv $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/server/cloudstack-limits.conf $(DESTDIR)/$(SYSCONFDIR)/security/limits.d/ mv $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/server/cloudstack-sudoers $(DESTDIR)/$(SYSCONFDIR)/sudoers.d/$(PACKAGE) + sed -i '/requiretty/d' $(DESTDIR)/$(SYSCONFDIR)/sudoers.d/$(PACKAGE) chmod 0440 $(DESTDIR)/$(SYSCONFDIR)/sudoers.d/$(PACKAGE) install -D client/target/utilities/bin/cloud-update-xenserver-licenses $(DESTDIR)/usr/bin/cloudstack-update-xenserver-licenses diff --git a/packaging/systemd/cloudstack-management.default b/packaging/systemd/cloudstack-management.default index a41338beda68..dbb7fa7d4bc5 100644 --- a/packaging/systemd/cloudstack-management.default +++ b/packaging/systemd/cloudstack-management.default @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -JAVA_OPTS="-Djava.security.properties=/etc/cloudstack/management/java.security.ciphers -Djava.awt.headless=true -Xmx2G -XX:+UseParallelGC -XX:MaxGCPauseMillis=500 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:ErrorFile=/var/log/cloudstack/management/cloudstack-management.err --add-opens=java.base/java.lang=ALL-UNNAMED --add-exports=java.base/sun.security.x509=ALL-UNNAMED" +JAVA_OPTS="-Djava.security.properties=/etc/cloudstack/management/java.security.ciphers -Djava.awt.headless=true -Xmx2G -XX:+UseParallelGC -XX:MaxGCPauseMillis=500 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:ErrorFile=/var/log/cloudstack/management/cloudstack-management.err --add-opens=java.base/java.lang=ALL-UNNAMED --add-exports=java.base/sun.security.x509=ALL-UNNAMED -Djava.io.tmpdir=/var/tmp" CLASSPATH="/usr/share/cloudstack-management/lib/*:/etc/cloudstack/management:/usr/share/cloudstack-common:/usr/share/cloudstack-management/setup:/usr/share/cloudstack-management:/usr/share/cloudstack-mysql-ha/lib/*" diff --git a/scripts/storage/secondary/createtmplt.sh b/scripts/storage/secondary/createtmplt.sh index cfc4be28a012..280f57e70fb7 100755 --- a/scripts/storage/secondary/createtmplt.sh +++ b/scripts/storage/secondary/createtmplt.sh @@ -218,7 +218,7 @@ imgsize=$(ls -l $tmpltimg2| awk -F" " '{print $5}') if [ "$cloud" == "true" ] then create_from_file_user $tmpltfs $tmpltimg2 $tmpltname - tmpltfs=/tmp/cloud/templates/ + tmpltfs=/var/tmp/cloud/templates/ else create_from_file $tmpltfs $tmpltimg2 $tmpltname fi diff --git a/scripts/storage/secondary/setup-sysvm-tmplt b/scripts/storage/secondary/setup-sysvm-tmplt index 63006cc4e4c2..96939707b91a 100755 --- a/scripts/storage/secondary/setup-sysvm-tmplt +++ b/scripts/storage/secondary/setup-sysvm-tmplt @@ -105,7 +105,7 @@ if [[ "$destfiles" != "" ]]; then failed 2 "Data already exists at destination $destdir" fi -tmpfolder=/tmp/cloud/templates/ +tmpfolder=/var/tmp/cloud/templates/ mkdir -p $tmpfolder tmplfile=$tmpfolder/$localfile From ec2d3ea1e6d186964760c8d2ab33d3164715e1b5 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Fri, 10 Jul 2026 13:22:58 -0300 Subject: [PATCH 178/205] Fix `findHostsForMigration` never returning hosts from other clusters (#13452) --- .../cloud/server/ManagementServerImpl.java | 69 +++++++++++-------- .../server/ManagementServerImplTest.java | 61 ++++++++++++---- 2 files changed, 86 insertions(+), 44 deletions(-) diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index bd4c311e3cd5..1fc92ad0e8ee 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -1469,6 +1469,7 @@ protected boolean zoneWideVolumeRequiresStorageMotion(PrimaryDataStore volumeDat */ Ternary, Integer>, List, Map> getTechnicallyCompatibleHosts( final VirtualMachine vm, + final Host srcHost, final Long startIndex, final Long pageSize, final String keyword) { @@ -1479,31 +1480,6 @@ Ternary, Integer>, List, Map(new Pair<>(new ArrayList<>(), 0), new ArrayList<>(), new HashMap<>()); } - final long srcHostId = vm.getHostId(); - final Host srcHost = _hostDao.findById(srcHostId); - if (srcHost == null) { - if (logger.isDebugEnabled()) { - logger.debug("Unable to find the host with ID: " + srcHostId + " of this Instance: " + vm); - } - final InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find the host (with specified ID) of instance with specified ID"); - ex.addProxyObject(String.valueOf(srcHostId), "hostId"); - ex.addProxyObject(vm.getUuid(), "vmId"); - throw ex; - } - - String srcHostVersion = srcHost.getHypervisorVersion(); - if (HypervisorType.KVM.equals(srcHost.getHypervisorType()) && srcHostVersion == null) { - srcHostVersion = ""; - } - - // Check if the vm can be migrated with storage. - boolean canMigrateWithStorage = false; - - List hypervisorTypes = Arrays.asList(new HypervisorType[]{HypervisorType.VMware, HypervisorType.KVM}); - if (VirtualMachine.Type.User.equals(vm.getType()) || hypervisorTypes.contains(vm.getHypervisorType())) { - canMigrateWithStorage = _hypervisorCapabilitiesDao.isStorageMotionSupported(srcHost.getHypervisorType(), srcHostVersion); - } - // Check if the vm is using any disks on local storage. final VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(vm, null, _offeringDao.findById(vm.getId(), vm.getServiceOfferingId()), null, null); final List volumes = _volumeDao.findCreatedByInstance(vmProfile.getId()); @@ -1517,10 +1493,12 @@ Ternary, Integer>, List, Map, Integer> allHostsPair = null; List allHosts = null; @@ -1596,6 +1574,23 @@ Ternary, Integer>, List, Map(allHostsPairResult, filteredHosts, requiresStorageMotion); } + protected boolean isStorageMigrationSupported(final VirtualMachine vm, final Host srcHost) { + final List hypervisorTypes = Arrays.asList(HypervisorType.VMware, HypervisorType.KVM); + if (VirtualMachine.Type.User.equals(vm.getType()) || hypervisorTypes.contains(vm.getHypervisorType())) { + final String srcHostVersion = getHypervisorVersionOfHost(srcHost); + return _hypervisorCapabilitiesDao.isStorageMotionSupported(srcHost.getHypervisorType(), srcHostVersion); + } + return false; + } + + protected String getHypervisorVersionOfHost(final Host host) { + final String version = host.getHypervisorVersion(); + if (version == null && HypervisorType.KVM.equals(host.getHypervisorType())) { + return ""; + } + return version; + } + /** * Apply affinity group constraints and other exclusion rules for VM migration. * This builds an ExcludeList based on affinity groups, DPDK requirements, and dedicated resources. @@ -1692,9 +1687,19 @@ public Ternary, Integer>, List, Map, Integer>, List, Map> compatibilityResult = - getTechnicallyCompatibleHosts(vm, startIndex, pageSize, keyword); + getTechnicallyCompatibleHosts(vm, srcHost, startIndex, pageSize, keyword); Pair, Integer> allHostsPair = compatibilityResult.first(); List filteredHosts = compatibilityResult.second(); @@ -1707,9 +1712,7 @@ public Ternary, Integer>, List, Map, Integer>, List, Map(otherHosts, suitableHosts, requiresStorageMotion); } + protected DataCenterDeployment createDeploymentPlanForMigrationListing(final VirtualMachine vm, final Host srcHost) { + final boolean canMigrateWithStorage = isStorageMigrationSupported(vm, srcHost); + if (canMigrateWithStorage) { + return new DataCenterDeployment(srcHost.getDataCenterId(), srcHost.getPodId(), null, null, null, null); + } + return new DataCenterDeployment(srcHost.getDataCenterId(), srcHost.getPodId(), srcHost.getClusterId(), null, null, null); + } + /** * Add non DPDK enabled hosts to the avoid list */ diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index da2005f61368..2f3e97716a03 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -20,6 +20,7 @@ import com.cloud.dc.DataCenterVO; import com.cloud.dc.Vlan.VlanType; import com.cloud.dc.dao.DataCenterDao; +import com.cloud.deploy.DataCenterDeployment; import com.cloud.deploy.DeploymentPlanningManager; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; @@ -227,7 +228,7 @@ public void setup() throws IllegalAccessException, NoSuchFieldException { apiDBUtilsMock = Mockito.mockStatic(ApiDBUtils.class); // Return empty list to avoid architecture filtering in most tests apiDBUtilsMock.when(() -> ApiDBUtils.listZoneClustersArchs(Mockito.anyLong())) - .thenReturn(new ArrayList<>()); + .thenReturn(new ArrayList<>()); } @After @@ -246,7 +247,7 @@ private void overrideDefaultConfigValue(final ConfigKey configKey, final String } @Test(expected = InvalidParameterValueException.class) - public void testDuplicateRegistraitons(){ + public void testDuplicateRegistrations() { String accountName = "account"; String publicKeyString = "ssh-rsa very public"; String publicKeyMaterial = spy.getPublicKeyFromKeyKeyMaterial(publicKeyString); @@ -826,9 +827,13 @@ public void testListHostsForMigrationOfVMLxcUserVM() { @Test public void testListHostsForMigrationOfVMGpuEnabled() { VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + long hostId = vm.getHostId(); + HostVO srcHost = mockHost(hostId, 4L, 5L, 6L, HypervisorType.KVM); Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.doReturn(srcHost).when(hostDao).findById(hostId); // Mock GPU detail Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) @@ -888,7 +893,7 @@ public void testListHostsForMigrationOfVMWithSystemVM() { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify storage motion capability was checked - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.VMware, null); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); // Verify result structure and data Assert.assertNotNull(result); @@ -952,7 +957,7 @@ public void testListHostsForMigrationOfVMWithDomainRouter() { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify hypervisor capabilities were checked - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.KVM, ""); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.KVM, ""); // Verify result contains expected hosts Assert.assertNotNull(result); @@ -1097,7 +1102,7 @@ public void testListHostsForMigrationOfVMKVMWithNullHypervisorVersion() { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify KVM null version was converted to empty string - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.KVM, ""); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.KVM, ""); // Verify result data Assert.assertNotNull(result); @@ -1416,7 +1421,7 @@ public void testListHostsForMigrationOfVMStorageMotionCapabilityCheck() { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify storage motion capability was checked for User VM - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.VMware, null); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); // Verify response data Assert.assertNotNull(result); @@ -1481,7 +1486,7 @@ public void testListHostsForMigrationOfVMWithAllSupportedHypervisors() { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify hypervisor is in supported hypervisors list - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(hypervisorType, version); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(hypervisorType, version); // Verify validation passed for this hypervisor Assert.assertNotNull("Result should not be null for " + hypervisorType, result); @@ -1508,8 +1513,6 @@ public void testListHostsForMigrationOfVMSourceHostNotFound() { Account caller = mockRootAdminAccount(); Mockito.doReturn(caller).when(spy).getCaller(); Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); - Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) - .thenReturn(null); Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(null); spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); @@ -1589,7 +1592,7 @@ public void testListHostsForMigrationOfVMStorageMotionCheckForSystemVM() { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify that storage motion capability was checked for system VM (VMware is in hypervisorTypes list) - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.VMware, null); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); // Verify response structure Assert.assertNotNull(result); @@ -1642,7 +1645,7 @@ public void testListHostsForMigrationOfVMStorageMotionCheckForUserVM() { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify User VM can migrate with storage (User VM type always checks) - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.KVM, ""); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.KVM, ""); // Verify response data Assert.assertNotNull(result); @@ -1695,7 +1698,7 @@ public void testListHostsForMigrationOfVMWithoutStorageMotionClusterScope() { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify XenServer without storage motion was checked - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.XenServer, null); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.XenServer, null); // Verify cluster-scoped search was used (not zone-wide) Mockito.verify(spy).searchForServers( Mockito.eq(0L), Mockito.eq(20L), Mockito.isNull(), Mockito.any(Type.class), @@ -1845,14 +1848,14 @@ public void testListHostsForMigrationOfVMVmwareStorageMotionCheck() { Mockito.doReturn(caller).when(spy).getCaller(); Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) - .thenReturn(null); + .thenReturn(null); HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.VMware); Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); // VMware with DomainRouter should still check storage motion Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.VMware, null)) - .thenReturn(true); + .thenReturn(true); ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); @@ -1880,7 +1883,7 @@ public void testListHostsForMigrationOfVMVmwareStorageMotionCheck() { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify VMware always checks storage motion (hypervisorTypes list includes VMware) - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.VMware, null); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); // Verify response Assert.assertNotNull(result); @@ -2074,4 +2077,32 @@ private DiskOfferingVO mockSharedDiskOffering(Long id) { Mockito.when(diskOffering.isUseLocalStorage()).thenReturn(false); return diskOffering; } + + @Test + public void createDeploymentPlanForMigrationListingTestAllocatesInAnyClusterWhenStorageMigrationIsSupported() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + HostVO srcHost = mockHost(vm.getHostId(), 1L, 2L, 3L, HypervisorType.KVM); + + Mockito.doReturn(true).when(spy).isStorageMigrationSupported(vm, srcHost); + + DataCenterDeployment deploymentPlan = spy.createDeploymentPlanForMigrationListing(vm, srcHost); + + Assert.assertEquals(3L, deploymentPlan.getDataCenterId()); + Assert.assertEquals(2L, (long) deploymentPlan.getPodId()); + Assert.assertNull(deploymentPlan.getClusterId()); + } + + @Test + public void createDeploymentPlanForMigrationListingTestAllocatesInSourceClusterWhenStorageMigrationIsNotSupported() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.XenServer); + HostVO srcHost = mockHost(vm.getHostId(), 4L, 5L, 6L, HypervisorType.XenServer); + + Mockito.doReturn(false).when(spy).isStorageMigrationSupported(vm, srcHost); + + DataCenterDeployment deploymentPlan = spy.createDeploymentPlanForMigrationListing(vm, srcHost); + + Assert.assertEquals(6L, deploymentPlan.getDataCenterId()); + Assert.assertEquals(5L, (long) deploymentPlan.getPodId()); + Assert.assertEquals(4L, (long) deploymentPlan.getClusterId()); + } } From 8225668688d15b776182d3849cd2c94167678bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= <89930804+erikbocks@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:53:11 -0300 Subject: [PATCH 179/205] Fix QEMU convert command timeout for incremental snapshots (#13212) --- .../hypervisor/kvm/storage/KVMStorageProcessor.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index b5e5f5939dcf..bc82744dd857 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -2092,23 +2092,24 @@ protected void rebaseSnapshot(SnapshotObjectTO snapshotObjectTO, KVMStoragePool logger.debug("Rebasing snapshot [{}] with parent [{}].", snapshotName, parentSnapshotPath); + long snapshotTimeoutInMillis = wait * 1000L; try { - QemuImg qemuImg = new QemuImg(wait); + QemuImg qemuImg = new QemuImg(snapshotTimeoutInMillis); qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false); } catch (LibvirtException | QemuImgException e) { if (!StringUtils.contains(e.getMessage(), "Is another process using the image")) { logger.error("Exception while rebasing incremental snapshot [{}] due to: [{}].", snapshotName, e.getMessage(), e); throw new CloudRuntimeException(e); } - retryRebase(snapshotName, wait, e, snapshotFile, parentSnapshotFile); + retryRebase(snapshotName, snapshotTimeoutInMillis, e, snapshotFile, parentSnapshotFile); } } - private void retryRebase(String snapshotName, int wait, Exception e, QemuImgFile snapshotFile, QemuImgFile parentSnapshotFile) { + private void retryRebase(String snapshotName, long waitInMilliseconds, Exception e, QemuImgFile snapshotFile, QemuImgFile parentSnapshotFile) { logger.warn("Libvirt still has not released the lock, will wait [{}] milliseconds and try again later.", incrementalSnapshotRetryRebaseWait); try { Thread.sleep(incrementalSnapshotRetryRebaseWait); - QemuImg qemuImg = new QemuImg(wait); + QemuImg qemuImg = new QemuImg(waitInMilliseconds); qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false); } catch (LibvirtException | QemuImgException | InterruptedException ex) { logger.error("Unable to rebase snapshot [{}].", snapshotName, ex); From e8df87e89be3a1834e163c01ef395fe220a72027 Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Fri, 10 Jul 2026 21:02:07 +0200 Subject: [PATCH 180/205] flasharray: authenticate via REST 2.x api-token and discover API version (#13060) Signed-off-by: Eugenio Grosso Co-authored-by: Eugenio Grosso --- .../adapter/flasharray/FlashArrayAdapter.java | 184 +++++++++++++----- 1 file changed, 136 insertions(+), 48 deletions(-) diff --git a/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java b/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java index dd554af36fea..3ffbeb1f9a34 100644 --- a/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java +++ b/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java @@ -28,6 +28,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; @@ -63,6 +65,7 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -87,6 +90,10 @@ public class FlashArrayAdapter implements ProviderAdapter { private static final String API_LOGIN_VERSION_DEFAULT = "1.19"; private static final String API_VERSION_DEFAULT = "2.23"; + // URLs for which the legacy-auth deprecation WARN has already been emitted, + // so we don't spam the logs once per refresh per pool while it's still configured. + private static final Set WARNED_LEGACY_URLS = ConcurrentHashMap.newKeySet(); + static final ObjectMapper mapper = new ObjectMapper(); public String pod = null; public String hostgroup = null; @@ -588,6 +595,91 @@ private String getAccessToken() { return accessToken; } + /** + * Discover the latest supported Purity REST API version by hitting the unauthenticated + * {@code /api/api_version} endpoint (returns {@code {"version":["1.0",...,"2.36"]}}). + * The discovered version is stored on {@link #apiVersion}; on failure the caller-configured + * default remains in place. + */ + private void fetchApiVersionFromPurity(CloseableHttpClient client) { + HttpGet vReq = new HttpGet(url + "/api_version"); + CloseableHttpResponse vResp = null; + try { + vResp = client.execute(vReq); + if (vResp.getStatusLine().getStatusCode() == 200) { + JsonNode root = mapper.readTree(vResp.getEntity().getContent()); + JsonNode versions = root.get("version"); + if (versions != null && versions.isArray() && versions.size() > 0) { + apiVersion = versions.get(versions.size() - 1).asText(); + } + } else { + logger.warn("Unexpected HTTP " + vResp.getStatusLine().getStatusCode() + + " from FlashArray [" + url + "] /api_version, falling back to default " + + API_VERSION_DEFAULT); + } + } catch (Exception e) { + logger.warn("Failed to discover Purity REST API version from " + url + + "/api_version, falling back to default " + API_VERSION_DEFAULT, e); + } finally { + if (vResp != null) { + try { + vResp.close(); + } catch (IOException e) { + logger.debug("Error closing /api_version response from FlashArray [" + url + "]", e); + } + } + } + } + + /** + * Exchange the operator-configured username/password for a long-lived Purity api-token + * via REST 1.x {@code /auth/apitoken}. Emits the once-per-URL deprecation WARN. + * @return the api-token to feed into the REST 2.x /login exchange. + */ + private String getApiTokenUsingUserPass(CloseableHttpClient client) throws IOException { + if (WARNED_LEGACY_URLS.add(url)) { + logger.warn("FlashArray adapter at [" + url + "] is using deprecated username/password " + + "login against Purity REST 1.x. Replace with a pre-minted " + + ProviderAdapter.API_TOKEN_KEY + " detail; the username/password code path will be " + + "removed in a future release."); + } + HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken"); + ArrayList postParms = new ArrayList(); + postParms.add(new BasicNameValuePair("username", username)); + postParms.add(new BasicNameValuePair("password", password)); + request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8")); + CloseableHttpResponse response = null; + try { + response = client.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200 || statusCode == 201) { + FlashArrayApiToken legacyToken = mapper.readValue(response.getEntity().getContent(), + FlashArrayApiToken.class); + if (legacyToken == null || legacyToken.getApiToken() == null) { + throw new CloudRuntimeException( + "Authentication responded successfully but no api token was returned"); + } + return legacyToken.getApiToken(); + } else if (statusCode == 401 || statusCode == 403) { + throw new CloudRuntimeException( + "Authentication or Authorization to FlashArray [" + url + "] with user [" + username + + "] failed, unable to retrieve session token"); + } else { + throw new CloudRuntimeException( + "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode + + "] - " + response.getStatusLine().getReasonPhrase()); + } + } finally { + if (response != null) { + try { + response.close(); + } catch (IOException e) { + logger.debug("Error closing legacy auth/apitoken response from FlashArray [" + url + "]", e); + } + } + } + } + private synchronized void refreshSession(boolean force) { try { if (force || keyExpiration < System.currentTimeMillis()) { @@ -662,9 +754,11 @@ private void login() { } apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION); - if (apiVersion == null) { + boolean apiVersionExplicit = apiVersion != null; + if (!apiVersionExplicit) { apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION); - if (apiVersion == null) { + apiVersionExplicit = apiVersion != null; + if (!apiVersionExplicit) { apiVersion = API_VERSION_DEFAULT; } } @@ -731,72 +825,66 @@ private void login() { skipTlsValidation = true; } + // Resolve the long-lived API token. Prefer a pre-minted api_token (Purity REST 2.x flow); + // fall back to legacy username/password auth via Purity REST 1.x for backward compatibility. + String apiToken = connectionDetails.get(ProviderAdapter.API_TOKEN_KEY); + if (apiToken != null && apiToken.isEmpty()) { + apiToken = null; + } + boolean usingLegacyUserPass = apiToken == null; + if (usingLegacyUserPass && (username == null || password == null)) { + throw new CloudRuntimeException("FlashArray adapter requires either " + ProviderAdapter.API_TOKEN_KEY + + " (preferred) or both " + ProviderAdapter.API_USERNAME_KEY + " and " + + ProviderAdapter.API_PASSWORD_KEY + " in the connection details"); + } + + CloseableHttpClient client = getClient(); CloseableHttpResponse response = null; try { - HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken"); - // request.addHeader("Content-Type", "application/json"); - // request.addHeader("Accept", "application/json"); - ArrayList postParms = new ArrayList(); - postParms.add(new BasicNameValuePair("username", username)); - postParms.add(new BasicNameValuePair("password", password)); - request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8")); - CloseableHttpClient client = getClient(); - response = (CloseableHttpResponse) client.execute(request); - - int statusCode = response.getStatusLine().getStatusCode(); - FlashArrayApiToken apitoken = null; - if (statusCode == 200 | statusCode == 201) { - apitoken = mapper.readValue(response.getEntity().getContent(), FlashArrayApiToken.class); - if (apitoken == null) { - throw new CloudRuntimeException( - "Authentication responded successfully but no api token was returned"); - } - } else if (statusCode == 401 || statusCode == 403) { - throw new CloudRuntimeException( - "Authentication or Authorization to FlashArray [" + url + "] with user [" + username - + "] failed, unable to retrieve session token"); - } else { - throw new CloudRuntimeException( - "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode - + "] - " + response.getStatusLine().getReasonPhrase()); + // Discover the latest supported API version from the array unless one was explicitly configured. + // GET /api/api_version is unauthenticated and returns {"version":["1.0",...,"2.36"]}. + if (!apiVersionExplicit) { + fetchApiVersionFromPurity(client); } - // now we need to get the access token - request = new HttpPost(url + "/" + apiVersion + "/login"); - request.addHeader("api-token", apitoken.getApiToken()); - response = (CloseableHttpResponse) client.execute(request); + if (usingLegacyUserPass) { + apiToken = getApiTokenUsingUserPass(client); + } - statusCode = response.getStatusLine().getStatusCode(); - if (statusCode == 200 | statusCode == 201) { + // Exchange the long-lived api-token for a short-lived x-auth-token (REST 2.x). + HttpPost request = new HttpPost(url + "/" + apiVersion + "/login"); + request.addHeader("api-token", apiToken); + response = client.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200 || statusCode == 201) { Header[] headers = response.getHeaders("x-auth-token"); if (headers == null || headers.length == 0) { throw new CloudRuntimeException( - "Getting access token responded successfully but access token was not available"); + "FlashArray /login responded successfully but no x-auth-token header was returned"); } accessToken = headers[0].getValue(); } else if (statusCode == 401 || statusCode == 403) { throw new CloudRuntimeException( - "Authentication or Authorization to FlashArray [" + url + "] with user [" + username - + "] failed, unable to retrieve session token"); + "FlashArray [" + url + "] rejected the api-token at /" + apiVersion + "/login"); } else { throw new CloudRuntimeException( - "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode - + "] - " + response.getStatusLine().getReasonPhrase()); + "Unexpected HTTP response code from FlashArray [" + url + "] /" + apiVersion + + "/login - [" + statusCode + "] - " + + response.getStatusLine().getReasonPhrase()); } - } catch (UnsupportedEncodingException e) { - throw new CloudRuntimeException("Error creating input for login, check username/password encoding"); + throw new CloudRuntimeException("Error encoding login form for FlashArray [" + url + "]", e); } catch (UnsupportedOperationException e) { throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e); } catch (IOException e) { throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e); } finally { - try { - if (response != null) { + if (response != null) { + try { response.close(); + } catch (IOException e) { + logger.debug("Error closing response from login attempt to FlashArray", e); } - } catch (IOException e) { - logger.debug("Error closing response from login attempt to FlashArray", e); } } } @@ -964,7 +1052,7 @@ private T PATCH(String path, Object input, final TypeReference type) { request.setEntity(new StringEntity(data)); CloseableHttpClient client = getClient(); - response = (CloseableHttpResponse) client.execute(request); + response = client.execute(request); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 || statusCode == 201) { @@ -1019,7 +1107,7 @@ private T GET(String path, final TypeReference type) { request.addHeader("X-auth-token", getAccessToken()); CloseableHttpClient client = getClient(); - response = (CloseableHttpResponse) client.execute(request); + response = client.execute(request); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { try { @@ -1061,7 +1149,7 @@ private void DELETE(String path) { request.addHeader("X-auth-token", getAccessToken()); CloseableHttpClient client = getClient(); - response = (CloseableHttpResponse) client.execute(request); + response = client.execute(request); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 || statusCode == 404 || statusCode == 400) { // this means the volume was deleted successfully, or doesn't exist (effective From f2a1f839a7bb605302652992e9eeca99501980e7 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Mon, 13 Jul 2026 03:49:15 -0300 Subject: [PATCH 181/205] Quota UI rework (#13449) --- .../apache/cloudstack/api/ApiConstants.java | 1 + .../cloudstack/quota/dao/QuotaCreditsDao.java | 2 +- .../quota/dao/QuotaCreditsDaoImpl.java | 12 +- .../api/command/QuotaCreditsCmd.java | 64 +- .../api/command/QuotaCreditsListCmd.java | 30 +- .../api/response/QuotaResponseBuilder.java | 3 +- .../response/QuotaResponseBuilderImpl.java | 279 +++- .../QuotaStatementItemHistoryResponse.java | 68 + .../QuotaStatementItemResourceResponse.java | 10 + .../response/QuotaStatementItemResponse.java | 7 + .../api/command/QuotaCreditsCmdTest.java | 23 +- .../QuotaResponseBuilderImplTest.java | 291 +++- .../com/cloud/user/AccountManagerImpl.java | 2 +- ui/jest.config.js | 2 +- ui/package-lock.json | 1249 ++++++++++++++--- ui/package.json | 1 + ui/public/locales/en.json | 40 +- ui/public/locales/pt_BR.json | 47 +- ui/src/api/index.js | 1 + ui/src/components/view/DetailsTab.vue | 8 +- ui/src/components/view/InfoCard.vue | 14 +- ui/src/components/view/ListView.vue | 46 +- .../view/buttons/ExportToCsvButton.vue | 43 + ui/src/components/view/charts/BarChart.vue | 56 + .../view/stats/ResourceStatsLineChart.vue | 2 +- ui/src/config/section/plugin/quota.js | 70 +- ui/src/style/common/common.scss | 37 + ui/src/utils/chart.js | 70 + ui/src/utils/date.js | 13 +- ui/src/utils/quota.js | 79 +- ui/src/utils/units.js | 28 + ui/src/utils/util.js | 31 +- ui/src/views/AutogenView.vue | 27 +- .../compute/wizard/OwnershipSelection.vue | 6 +- ui/src/views/plugins/quota/AddQuotaCredit.vue | 163 +++ .../views/plugins/quota/CreateQuotaTariff.vue | 3 - .../views/plugins/quota/EditQuotaTariff.vue | 3 - .../plugins/quota/EditTariffValueWizard.vue | 145 -- .../plugins/quota/EmailTemplateDetails.vue | 30 +- .../quota/FilterQuotaDataByPeriodView.vue | 161 +++ ui/src/views/plugins/quota/QuotaBalance.vue | 173 --- .../views/plugins/quota/QuotaBalanceTab.vue | 204 +++ ui/src/views/plugins/quota/QuotaCreditTab.vue | 230 +++ ui/src/views/plugins/quota/QuotaSummary.vue | 65 - .../plugins/quota/QuotaSummaryResource.vue | 98 -- ui/src/views/plugins/quota/QuotaUsage.vue | 158 --- ui/src/views/plugins/quota/QuotaUsageTab.vue | 731 ++++++++++ 47 files changed, 3635 insertions(+), 1191 deletions(-) create mode 100644 plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemHistoryResponse.java create mode 100644 ui/src/components/view/buttons/ExportToCsvButton.vue create mode 100644 ui/src/components/view/charts/BarChart.vue create mode 100644 ui/src/style/common/common.scss create mode 100644 ui/src/utils/chart.js create mode 100644 ui/src/utils/units.js create mode 100644 ui/src/views/plugins/quota/AddQuotaCredit.vue delete mode 100644 ui/src/views/plugins/quota/EditTariffValueWizard.vue create mode 100644 ui/src/views/plugins/quota/FilterQuotaDataByPeriodView.vue delete mode 100644 ui/src/views/plugins/quota/QuotaBalance.vue create mode 100644 ui/src/views/plugins/quota/QuotaBalanceTab.vue create mode 100644 ui/src/views/plugins/quota/QuotaCreditTab.vue delete mode 100644 ui/src/views/plugins/quota/QuotaSummary.vue delete mode 100644 ui/src/views/plugins/quota/QuotaSummaryResource.vue delete mode 100644 ui/src/views/plugins/quota/QuotaUsage.vue create mode 100644 ui/src/views/plugins/quota/QuotaUsageTab.vue diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 5c53430388da..c15a4a800edc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -286,6 +286,7 @@ public class ApiConstants { public static final String HEALTH = "health"; public static final String HEADERS = "headers"; public static final String HIDE_IP_ADDRESS_USAGE = "hideipaddressusage"; + public static final String HISTORY = "history"; public static final String HOST_ID = "hostid"; public static final String HOST_IDS = "hostids"; public static final String HOST_IP = "hostip"; diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java index da36bc0b98c5..faf5ce363393 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java @@ -25,7 +25,7 @@ public interface QuotaCreditsDao extends GenericDao { - List findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive); + List findCredits(Long accountId, List domainIds, Date startDate, Date endDate); QuotaCreditsVO saveCredits(QuotaCreditsVO credits); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java index ce51177d0aec..ceb901ca30fd 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java @@ -21,7 +21,6 @@ import javax.inject.Inject; -import com.cloud.domain.dao.DomainDao; import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; @@ -39,8 +38,6 @@ @Component public class QuotaCreditsDaoImpl extends GenericDaoBase implements QuotaCreditsDao { - @Inject - DomainDao domainDao; @Inject QuotaBalanceDao quotaBalanceDao; @@ -50,19 +47,18 @@ public QuotaCreditsDaoImpl() { quotaCreditsVoSearch = createSearchBuilder(); quotaCreditsVoSearch.and("updatedOn", quotaCreditsVoSearch.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN); quotaCreditsVoSearch.and("accountId", quotaCreditsVoSearch.entity().getAccountId(), SearchCriteria.Op.EQ); - quotaCreditsVoSearch.and("domainId", quotaCreditsVoSearch.entity().getDomainId(), SearchCriteria.Op.IN); + quotaCreditsVoSearch.and("domainIds", quotaCreditsVoSearch.entity().getDomainId(), SearchCriteria.Op.IN); quotaCreditsVoSearch.done(); } @Override - public List findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive) { + public List findCredits(Long accountId, List domainIds, Date startDate, Date endDate) { SearchCriteria sc = quotaCreditsVoSearch.create(); Filter filter = new Filter(QuotaCreditsVO.class, "updatedOn", true, 0L, Long.MAX_VALUE); sc.setParametersIfNotNull("accountId", accountId); - if (domainId != null) { - List domainIds = recursive ? domainDao.getDomainAndChildrenIds(domainId) : List.of(domainId); - sc.setParameters("domainId", domainIds.toArray()); + if (domainIds != null) { + sc.setParameters("domainIds", domainIds.toArray()); } if (ObjectUtils.allNotNull(startDate, endDate)) { diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java index a6d1db41ddd2..0c99efd9a2d2 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java @@ -21,14 +21,13 @@ import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; -import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.QuotaCreditsResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; -import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.quota.QuotaService; import javax.inject.Inject; @@ -42,22 +41,35 @@ public class QuotaCreditsCmd extends BaseCmd { @Inject QuotaService _quotaService; - - - @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Account Id for which quota credits need to be added") + @Deprecated + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "Name of the Account for which Quota credits will be added. Deprecated, please use '" + + ApiConstants.ACCOUNT_ID + "' instead.") private String accountName; @ACL - @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "Domain for which quota credits need to be added") + @Deprecated + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "Domain of the Account specified by '" + ApiConstants.ACCOUNT + "' for which Quota credits will be added. " + + "Deprecated, please use '" + ApiConstants.ACCOUNT_ID + "' instead.") private Long domainId; - @Parameter(name = ApiConstants.VALUE, type = CommandType.DOUBLE, required = true, description = "Value of the credits to be added+, subtracted-") + @ACL + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "ID of the Account for which Quota credits will be added. Cannot be specified with '" + ApiConstants.PROJECT_ID + "'.") + private Long accountId; + + @ACL + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "ID of the Project for which Quota credits will be added. Cannot be specified with '" + ApiConstants.ACCOUNT_ID + "'.") + private Long projectId; + + @Parameter(name = ApiConstants.VALUE, type = CommandType.DOUBLE, required = true, description = "Amount of credits to be added (in case of a positive value) or subtracted (in case of a negative value).") private Double value; - @Parameter(name = "min_balance", type = CommandType.DOUBLE, required = false, description = "Minimum balance threshold of the Account") + @Parameter(name = "min_balance", type = CommandType.DOUBLE, description = "An email will be sent to the Account when the Quota credits get below this threshold.") private Double minBalance; - @Parameter(name = "quota_enforce", type = CommandType.BOOLEAN, required = false, description = "Account for which quota enforce is set to false will not be locked when there is no credit balance") + @Parameter(name = "quota_enforce", type = CommandType.BOOLEAN, description = "Whether to lock the Account when Quota credits are below zero.") private Boolean quotaEnforce; public Double getMinBalance() { @@ -100,31 +112,21 @@ public void setValue(Double value) { this.value = value; } + public Long getAccountId() { + return accountId; + } + + public Long getProjectId() { + return projectId; + } + public QuotaCreditsCmd() { super(); } @Override public void execute() { - Long accountId = null; - Account account = _accountService.getActiveAccountByName(accountName, domainId); - if (account != null) { - accountId = account.getAccountId(); - } - if (accountId == null) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "The Account does not exists or has been removed/disabled"); - } - if (getValue() == null) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please send a valid non-empty quota value"); - } - if (getQuotaEnforce() != null) { - _quotaService.setLockAccount(accountId, getQuotaEnforce()); - } - if (getMinBalance() != null) { - _quotaService.setMinBalance(accountId, getMinBalance()); - } - - final QuotaCreditsResponse response = _responseBuilder.addQuotaCredits(accountId, getDomainId(), getValue(), CallContext.current().getCallingUserId(), getQuotaEnforce()); + QuotaCreditsResponse response = _responseBuilder.addQuotaCredits(this); response.setResponseName(getCommandName()); response.setObjectName("quotacredits"); setResponseObject(response); @@ -132,10 +134,6 @@ public void execute() { @Override public long getEntityOwnerId() { - Account account = _accountService.getActiveAccountByName(accountName, domainId); - if (account != null) { - return account.getAccountId(); - } return Account.ACCOUNT_ID_SYSTEM; } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java index 48bb7ef79e70..7555923600d9 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java @@ -18,7 +18,7 @@ import com.cloud.utils.Pair; -import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; @@ -26,8 +26,10 @@ import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.QuotaCreditsResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.time.DateUtils; @@ -44,13 +46,16 @@ public class QuotaCreditsListCmd extends BaseCmd { @Inject QuotaResponseBuilder quotaResponseBuilder; - @ACL - @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which the credit statement will be generated.") + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "ID of the Account for which the credit statement will be generated. Cannot be specified with '" + ApiConstants.PROJECT_ID + "'.") private Long accountId; - @ACL - @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the domain for which credit statement will be generated. " + - "Available only for administrators.") + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "ID of the Project for which the credit statement will be generated. Cannot be specified with '" + ApiConstants.ACCOUNT_ID + "'.") + private Long projectId; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the Domain for which credit statement will be generated. " + + "Available only for administrators.", authorized = {RoleType.Admin, RoleType.DomainAdmin}) private Long domainId; @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "End date of the credit statement. If not provided, the current date will be " + @@ -97,14 +102,18 @@ public void setStartDate(Date startDate) { this.startDate = startDate; } - public Boolean getRecursive() { - return recursive; + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); } public void setRecursive(Boolean recursive) { this.recursive = recursive; } + public Long getProjectId() { + return projectId; + } + @Override public void execute() { Pair, Integer> responses = quotaResponseBuilder.createQuotaCreditsListResponse(this); @@ -116,7 +125,10 @@ public void execute() { @Override public long getEntityOwnerId() { - return -1; + if (ObjectUtils.allNull(accountId, projectId)) { + return -1; + } + return _accountService.finalizeAccountId(accountId, null, null, projectId); } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java index 63bf043f4fa9..c1a677da9355 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java @@ -19,6 +19,7 @@ import com.cloud.user.User; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; +import org.apache.cloudstack.api.command.QuotaCreditsCmd; import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; @@ -54,7 +55,7 @@ public interface QuotaResponseBuilder { Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd); - QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce); + QuotaCreditsResponse addQuotaCredits(QuotaCreditsCmd cmd); List listQuotaEmailTemplates(QuotaEmailTemplateListCmd cmd); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index b71057e64234..6727ab58141f 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -44,13 +44,23 @@ import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; import com.cloud.exception.InvalidParameterValueException; -import com.cloud.exception.PermissionDeniedException; +import com.cloud.network.VpnUserVO; import com.cloud.network.dao.IPAddressDao; import com.cloud.network.dao.IPAddressVO; +import com.cloud.network.dao.LoadBalancerDao; +import com.cloud.network.dao.LoadBalancerVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.VpnUserDao; +import com.cloud.network.rules.PortForwardingRuleVO; +import com.cloud.network.rules.dao.PortForwardingRulesDao; +import com.cloud.network.security.SecurityGroupVO; +import com.cloud.network.security.dao.SecurityGroupDao; +import com.cloud.network.vpc.VpcVO; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.offerings.NetworkOfferingVO; +import com.cloud.storage.BucketVO; +import com.cloud.storage.dao.BucketDao; import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.storage.dao.SnapshotDao; @@ -67,7 +77,9 @@ import com.cloud.utils.DateUtil; import com.cloud.utils.Pair; import com.cloud.utils.db.EntityManager; +import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.Ip; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.acl.ControlledEntity; @@ -77,6 +89,7 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; +import org.apache.cloudstack.api.command.QuotaCreditsCmd; import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; @@ -114,6 +127,7 @@ import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao; +import org.apache.cloudstack.quota.dao.VpcDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.apache.cloudstack.quota.vo.QuotaCreditsVO; @@ -180,6 +194,8 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Inject private NetworkDao networkDao; @Inject + private VpcDao vpcDao; + @Inject private NetworkOfferingDao networkOfferingDao; @Inject private SnapshotDao snapshotDao; @@ -190,6 +206,16 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Inject private VolumeDao volumeDao; @Inject + private BucketDao bucketDao; + @Inject + private VpnUserDao vpnUserDao; + @Inject + private LoadBalancerDao loadBalancerDao; + @Inject + private PortForwardingRulesDao portForwardingRulesDao; + @Inject + private SecurityGroupDao securityGroupDao; + @Inject private QuotaUsageJoinDao quotaUsageJoinDao; @Inject private QuotaTariffUsageDao quotaTariffUsageDao; @@ -412,45 +438,52 @@ protected QuotaStatementItemResponse createStatementItem(int usageType, List history = createQuotaConsumptionHistory(usageRecords, quotaUsed); + item.setHistory(history); + } + return item; } - protected void setStatementItemResources(QuotaStatementItemResponse statementItem, int usageType, List quotaUsageRecords, boolean showResources) { - if (!showResources) { - return; - } - + protected void setStatementItemResources(QuotaStatementItemResponse statementItem, int usageType, List quotaUsageRecords) { List itemDetails = new ArrayList<>(); - Map quotaUsagesValuesAggregatedById = quotaUsageRecords + Map> quotaUsagesAggregatedByResourceId = quotaUsageRecords .stream() .filter(quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType) != null) - .collect(Collectors.groupingBy( - quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType), - Collectors.reducing(new BigDecimal(0), QuotaUsageJoinVO::getQuotaUsed, BigDecimal::add) + .collect(Collectors.groupingBy(quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType) )); - for (Map.Entry entry : quotaUsagesValuesAggregatedById.entrySet()) { - QuotaStatementItemResourceResponse detail = new QuotaStatementItemResourceResponse(); - - detail.setQuotaUsed(entry.getValue()); - + for (Map.Entry> entry : quotaUsagesAggregatedByResourceId.entrySet()) { QuotaUsageResourceVO resource = getResourceFromIdAndType(entry.getKey(), usageType); + + QuotaStatementItemResourceResponse detail = new QuotaStatementItemResourceResponse(); if (resource != null) { detail.setResourceId(resource.getUuid()); detail.setDisplayName(resource.getName()); detail.setRemoved(resource.isRemoved()); } else { detail.setDisplayName(""); - } + BigDecimal quotaUsed = entry.getValue().stream() + .map(QuotaUsageJoinVO::getQuotaUsed) + .reduce(BigDecimal.ZERO, BigDecimal::add); + List history = createQuotaConsumptionHistory(entry.getValue(), quotaUsed); + detail.setQuotaUsed(quotaUsed); + detail.setHistory(history); + itemDetails.add(detail); } + statementItem.setResources(itemDetails); } @@ -470,6 +503,7 @@ protected QuotaUsageResourceVO getResourceFromIdAndType(long resourceId, int usa switch (usageType) { case QuotaTypes.ALLOCATED_VM: case QuotaTypes.RUNNING_VM: + case QuotaTypes.BACKUP: VMInstanceVO vmInstance = vmInstanceDao.findByIdIncludingRemoved(resourceId); if (vmInstance != null) { return new QuotaUsageResourceVO(vmInstance.getUuid(), vmInstance.getHostName(), vmInstance.getRemoved()); @@ -496,11 +530,18 @@ protected QuotaUsageResourceVO getResourceFromIdAndType(long resourceId, int usa break; case QuotaTypes.NETWORK_BYTES_SENT: case QuotaTypes.NETWORK_BYTES_RECEIVED: + case QuotaTypes.NETWORK: NetworkVO network = networkDao.findByIdIncludingRemoved(resourceId); if (network != null) { return new QuotaUsageResourceVO(network.getUuid(), network.getName(), network.getRemoved()); } break; + case QuotaTypes.VPC: + VpcVO vpc = vpcDao.findByIdIncludingRemoved(resourceId); + if (vpc != null) { + return new QuotaUsageResourceVO(vpc.getUuid(), vpc.getName(), vpc.getRemoved()); + } + break; case QuotaTypes.TEMPLATE: case QuotaTypes.ISO: VMTemplateVO vmTemplate = vmTemplateDao.findByIdIncludingRemoved(resourceId); @@ -520,10 +561,76 @@ protected QuotaUsageResourceVO getResourceFromIdAndType(long resourceId, int usa return new QuotaUsageResourceVO(ipAddress.getUuid(), ipAddress.getName(), ipAddress.getRemoved()); } break; + case QuotaTypes.BUCKET: + BucketVO bucket = bucketDao.findByIdIncludingRemoved(resourceId); + if (bucket != null) { + return new QuotaUsageResourceVO(bucket.getUuid(), bucket.getName(), bucket.getRemoved()); + } + break; + case QuotaTypes.VPN_USERS: + VpnUserVO vpnUser = vpnUserDao.findByIdIncludingRemoved(resourceId); + if (vpnUser != null) { + return new QuotaUsageResourceVO(vpnUser.getUuid(), vpnUser.getUsername(), null); + } + break; + case QuotaTypes.SECURITY_GROUP: + SecurityGroupVO securityGroup = securityGroupDao.findByIdIncludingRemoved(resourceId); + if (securityGroup != null) { + return new QuotaUsageResourceVO(securityGroup.getUuid(), securityGroup.getName(), null); + } + break; + case QuotaTypes.LOAD_BALANCER_POLICY: + LoadBalancerVO loadBalancer = loadBalancerDao.findByIdIncludingRemoved(resourceId); + if (loadBalancer != null) { + return new QuotaUsageResourceVO(loadBalancer.getUuid(), loadBalancer.getName(), loadBalancer.getRemoved()); + } + break; + case QuotaTypes.PORT_FORWARDING_RULE: + PortForwardingRuleVO portForwardingRule = portForwardingRulesDao.findByIdIncludingRemoved(resourceId); + if (portForwardingRule == null) { + return null; + } + IPAddressVO source = ipAddressDao.findByIdIncludingRemoved(portForwardingRule.getSourceIpAddressId()); + Ip destination = portForwardingRule.getDestinationIpAddress(); + if (ObjectUtils.anyNull(source, destination)) { + return null; + } + String displayName = String.format("%s:%s-%s to %s:%s-%s", source.getAddress(), portForwardingRule.getSourcePortStart(), + portForwardingRule.getSourcePortEnd(), destination, portForwardingRule.getDestinationPortStart(), + portForwardingRule.getDestinationPortEnd()); + return new QuotaUsageResourceVO(portForwardingRule.getUuid(), displayName, portForwardingRule.getRemoved()); } return null; } + protected List createQuotaConsumptionHistory(List quotaUsage, BigDecimal quotaUsed) { + if (quotaUsed.equals(BigDecimal.ZERO)) { + logger.debug("Not generating Quota consumption history because the item has not consumed any Quota in the period."); + return null; + } + + Map history = new HashMap<>(); + for (QuotaUsageJoinVO record : quotaUsage) { + if (ObjectUtils.anyNull(record.getUsageItemId(), record.getQuotaUsed())) { + continue; + } + + QuotaStatementItemHistoryResponse item = history.computeIfAbsent( + record.getEndDate(), + key -> new QuotaStatementItemHistoryResponse() + ); + if (item.getStartDate() == null || item.getStartDate().after(record.getStartDate())) { + item.setStartDate(record.getStartDate()); + } + item.setEndDate(record.getEndDate()); + item.setQuotaConsumed(item.getQuotaConsumed().add(record.getQuotaUsed())); + + history.put(record.getEndDate(), item); + } + + return history.values().stream().sorted(Comparator.comparing(QuotaStatementItemHistoryResponse::getEndDate)).collect(Collectors.toList()); + } + @Override public Pair, Integer> listQuotaTariffPlans(final QuotaTariffListCmd cmd) { Date startDate = cmd.getEffectiveDate(); @@ -661,49 +768,88 @@ protected void validateEndDateOnCreatingNewQuotaTariff(QuotaTariffVO newQuotaTar } @Override - public QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce) { - Date depositedOn = new Date(); - QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, depositedOn); + public QuotaCreditsResponse addQuotaCredits(QuotaCreditsCmd cmd) { + Double value = cmd.getValue(); + if (value == null) { + throw new InvalidParameterValueException("Please specify a valid amount of credits."); + } - if (qb != null) { - throw new InvalidParameterValueException(String.format("Incorrect deposit date [%s], as there are balance entries after this date.", - depositedOn)); + Long accountId = _accountMgr.finalizeAccountId(cmd.getAccountId(), cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); + AccountVO account = _accountDao.findById(accountId); + Long domainId = account.getDomainId(); + + Date depositedOn = new Date(); + boolean lockAccountEnforcement = "true".equalsIgnoreCase(QuotaConfig.QuotaEnableEnforcement.value()); + + QuotaCreditsVO result; + try (TransactionLegacy ignored = TransactionLegacy.open(TransactionLegacy.USAGE_DB)) { + QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, depositedOn); + if (qb != null) { + throw new InvalidParameterValueException(String.format("Incorrect deposit date [%s], as there are balance entries after this date.", + depositedOn)); + } + result = persistQuotaCredits(cmd, value, depositedOn, account, lockAccountEnforcement); + } finally { + // Swap back to cloud + TransactionLegacy.open(TransactionLegacy.CLOUD_DB).close(); } - QuotaCreditsVO credits = new QuotaCreditsVO(accountId, domainId, new BigDecimal(amount), updatedBy); + UserVO creditor = getCreditorForQuotaCredits(result); + return createQuotaCreditsResponse(result, creditor); + } + + protected QuotaCreditsVO persistQuotaCredits(QuotaCreditsCmd cmd, Double value, Date depositedOn, AccountVO account, boolean lockAccountEnforcement) { + Long accountId = account.getId(); + Long domainId = account.getDomainId(); + long callingUserId = CallContext.current().getCallingUserId(); + QuotaCreditsVO credits = new QuotaCreditsVO(accountId, domainId, new BigDecimal(value), callingUserId); credits.setUpdatedOn(depositedOn); QuotaCreditsVO result = quotaCreditsDao.saveCredits(credits); - if (result == null) { - logger.error("Unable to add credits to account ID [{}].", accountId); - throw new CloudRuntimeException("Unable to add credits to account."); - } - final AccountVO account = _accountDao.findById(accountId); - if (account == null) { - throw new InvalidParameterValueException("Account does not exist with account id " + accountId); - } - final boolean lockAccountEnforcement = "true".equalsIgnoreCase(QuotaConfig.QuotaEnableEnforcement.value()); - final BigDecimal currentAccountBalance = _quotaBalanceDao.getLastQuotaBalance(accountId, domainId); - logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", amount, + BigDecimal currentAccountBalance = _quotaBalanceDao.getLastQuotaBalance(accountId, domainId); + logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", value, DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), depositedOn), currentAccountBalance); - // update quota account with the balance _quotaService.saveQuotaAccount(account, currentAccountBalance, depositedOn); + + Boolean enforceQuota = cmd.getQuotaEnforce(); + if (enforceQuota != null) { + _quotaService.setLockAccount(accountId, enforceQuota); + } + + Double minBalance = cmd.getMinBalance(); + if (minBalance != null) { + _quotaService.setMinBalance(accountId, minBalance); + } + if (lockAccountEnforcement) { - if (currentAccountBalance.compareTo(new BigDecimal(0)) >= 0) { - if (account.getState() == Account.State.LOCKED) { - logger.info("UnLocking account " + account.getAccountName() + " , due to positive balance " + currentAccountBalance); - _accountMgr.enableAccount(account.getAccountName(), domainId, accountId); - } - } else { // currentAccountBalance < 0 then lock the account - if (_quotaManager.isLockable(account) && account.getState() == Account.State.ENABLED && enforce) { - logger.info("Locking account " + account.getAccountName() + " , due to negative balance " + currentAccountBalance); - _accountMgr.lockAccount(account.getAccountName(), domainId, accountId); - } + // Need to open a transaction for the cloud data base, and then swap back to cloud_usage + try (TransactionLegacy ignored = TransactionLegacy.open(TransactionLegacy.CLOUD_DB)) { + lockOrUnlockAccountIfRequired(currentAccountBalance, account, enforceQuota); + } finally { + TransactionLegacy.open(TransactionLegacy.USAGE_DB).close(); } } - UserVO creditor = getCreditorForQuotaCredits(result); - return createQuotaCreditsResponse(result, creditor); + return result; + } + + protected void lockOrUnlockAccountIfRequired(BigDecimal currentAccountBalance, AccountVO account, Boolean enforceQuota) { + Long accountId = account.getId(); + Long domainId = account.getDomainId(); + String accountName = account.getAccountName(); + + if (currentAccountBalance.compareTo(BigDecimal.ZERO) >= 0) { + if (account.getState() == Account.State.LOCKED) { + logger.info("Unlocking Account [{}] due to positive balance.", accountName); + _accountMgr.enableAccount(accountName, domainId, accountId); + } + return; + } + + if (Boolean.TRUE.equals(enforceQuota) && account.getState() == Account.State.ENABLED && _quotaManager.isLockable(account)) { + logger.info("Locking Account [{}] due to negative balance.", accountName); + _accountMgr.lockAccount(accountName, domainId, accountId); + } } private QuotaEmailTemplateResponse createQuotaEmailResponse(QuotaEmailTemplatesVO template) { @@ -1036,26 +1182,16 @@ public Pair, Integer> createQuotaCreditsListResponse( } protected List getCreditsForQuotaCreditsList(QuotaCreditsListCmd cmd) { - Long accountId = cmd.getAccountId(); - Long domainId = cmd.getDomainId(); + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), null); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); Date startDate = cmd.getStartDate(); Date endDate = cmd.getEndDate(); - boolean isRecursive = cmd.getRecursive(); - - if (ObjectUtils.allNull(accountId, domainId)) { - throw new InvalidParameterValueException("Please provide either account ID or domain ID."); - } if (startDate.after(endDate)) { throw new InvalidParameterValueException("The start date must be before the end date."); } - Account caller = CallContext.current().getCallingAccount(); - if (domainId != null && _accountMgr.isNormalUser(caller.getAccountId())) { - throw new PermissionDeniedException("Regular users are not allowed to generate domain statements."); - } - - return quotaCreditsDao.findCredits(accountId, domainId, startDate, endDate, isRecursive); + return quotaCreditsDao.findCredits(accountId, baseDomainAndFilteredDomains.second(), startDate, endDate); } /** @@ -1208,6 +1344,14 @@ public QuotaResourceStatementResponse createQuotaResourceStatement(QuotaResource return createQuotaResourceStatementResponse(resourceUuid, usageType, quotaResourceStatementItemResponseList, totalQuotaUsed); } + /** + * Determines the appropriate Account ID to use for Quota statement-related operations while ensuring correct permissions. + * + * @param providedAccountId the ID of the Account provided to the command. + * @param fallbackAccountId the ID of a fallback Account to use for User Accounts if no specific Account ID was provided. + * If null, then we fallback to the User Account itself. + * @return the account ID to be used for the Quota statement, or null if no specific Account limitation is required. + */ protected Long getAccountIdForQuotaStatement(long providedAccountId, Long fallbackAccountId) { Account caller = CallContext.current().getCallingAccount(); @@ -1235,6 +1379,17 @@ protected Long getAccountIdForQuotaStatement(long providedAccountId, Long fallba return caller.getAccountId(); } + /** + * Determines the Domains for which a Quota statement should be generated while ensuring correct permissions. + * + * @param finalAccountId the Account ID determined via org.apache.cloudstack.api.response.QuotaResponseBuilderImpl#getAccountIdForQuotaStatement(long, java.lang.Long). + * @param providedDomainId the Domain ID provided to the command. + * @param isRecursive the recursion flag provided to the command. + * @return A pair containing: + * - The base Domain's ID as the first element. This can be null if we are not limiting by Domain. + * - A list containing the base Domain's ID and optionally its children if + * the recursion flag is true. Also nullable. + */ protected Pair> getDomainIdsForQuotaStatement(Long finalAccountId, Long providedDomainId, boolean isRecursive) { if (finalAccountId != null) { // Access to the provided account has already been validated diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemHistoryResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemHistoryResponse.java new file mode 100644 index 000000000000..7799326ea54b --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemHistoryResponse.java @@ -0,0 +1,68 @@ +//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.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import java.math.BigDecimal; +import java.util.Date; + +public class QuotaStatementItemHistoryResponse extends BaseResponse { + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Start date of the item.") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "End date of the item.") + private Date endDate; + + @SerializedName(ApiConstants.QUOTA_CONSUMED) + @Param(description = "Amount of quota consumed.") + private BigDecimal quotaConsumed = BigDecimal.ZERO; + + public QuotaStatementItemHistoryResponse() { + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public BigDecimal getQuotaConsumed() { + return quotaConsumed; + } + + public void setQuotaConsumed(BigDecimal quotaConsumed) { + this.quotaConsumed = quotaConsumed; + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java index 3e052f733391..68585a90d393 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.response; import java.math.BigDecimal; +import java.util.List; import com.google.gson.annotations.SerializedName; @@ -43,6 +44,10 @@ public class QuotaStatementItemResourceResponse extends BaseResponse { @Param(description = "Indicates whether the resource is removed or active.") private boolean removed; + @SerializedName(ApiConstants.HISTORY) + @Param(description = "Quota consumption history.") + private List history; + public void setQuotaUsed(BigDecimal quotaUsed) { this.quotaUsed = quotaUsed; } @@ -58,4 +63,9 @@ public void setDisplayName(String displayName) { public void setRemoved(boolean removed) { this.removed = removed; } + + public void setHistory(List history) { + this.history = history; + } + } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java index 0747c5a9172d..f04fffdf7790 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java @@ -48,6 +48,10 @@ public class QuotaStatementItemResponse extends BaseResponse { @Param(description = "Item's resources.") private List resources; + @SerializedName(ApiConstants.HISTORY) + @Param(description = "Quota consumption history.") + private List history; + public QuotaStatementItemResponse(final int usageType) { this.usageType = usageType; } @@ -92,4 +96,7 @@ public void setResources(List resources) { this.resources = resources; } + public void setHistory(List history) { + this.history = history; + } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsCmdTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsCmdTest.java index 06dd57ad41d3..b02cffbd2f0a 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsCmdTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsCmdTest.java @@ -16,16 +16,9 @@ // under the License. package org.apache.cloudstack.api.command; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyDouble; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.nullable; - import java.lang.reflect.Field; -import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.QuotaCreditsResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.quota.QuotaService; @@ -78,23 +71,13 @@ public void testQuotaCreditsCmd() throws NoSuchFieldException, IllegalAccessExce AccountVO acc = new AccountVO(); acc.setId(2L); - Mockito.when(accountService.getActiveAccountByName(nullable(String.class), nullable(Long.class))).thenReturn(acc); - - Mockito.when(responseBuilder.addQuotaCredits(nullable(Long.class), nullable(Long.class), nullable(Double.class), nullable(Long.class), nullable(Boolean.class))).thenReturn(new QuotaCreditsResponse()); - - // No value provided test - try { - cmd.execute(); - } catch (ServerApiException e) { - assertTrue(e.getErrorCode().equals(ApiErrorCode.PARAM_ERROR)); - } + Mockito.when(responseBuilder.addQuotaCredits(cmd)).thenReturn(new QuotaCreditsResponse()); // With value provided test cmd.setValue(11.80); cmd.execute(); - Mockito.verify(quotaService, Mockito.times(0)).setLockAccount(anyLong(), anyBoolean()); - Mockito.verify(quotaService, Mockito.times(1)).setMinBalance(anyLong(), anyDouble()); - Mockito.verify(responseBuilder, Mockito.times(1)).addQuotaCredits(nullable(Long.class), nullable(Long.class), nullable(Double.class), nullable(Long.class), nullable(Boolean.class)); + + Mockito.verify(responseBuilder, Mockito.times(1)).addQuotaCredits(cmd); } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 37307db45629..bcc1c02cde74 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -44,6 +44,7 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; +import org.apache.cloudstack.api.command.QuotaCreditsCmd; import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; @@ -52,6 +53,7 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.discovery.ApiDiscoveryService; import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper; +import org.apache.cloudstack.quota.QuotaManager; import org.apache.cloudstack.quota.QuotaService; import org.apache.cloudstack.quota.QuotaStatement; import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariableDefinition; @@ -192,6 +194,12 @@ public class QuotaResponseBuilderImplTest extends TestCase { @Mock EntityManager entityManagerMock; + @Mock + QuotaManager quotaManagerMock; + + @Mock + QuotaBalanceVO quotaBalanceVoMock; + @Before public void setup() { CallContext.register(callerUserMock, callerAccountMock); @@ -243,28 +251,6 @@ public void createQuotaTariffResponseTestIfReturnsActivationRuleWithoutPermissio assertNull(tariffResponse.getActivationRule()); } - @Test - public void testAddQuotaCredits() { - final long accountId = 2L; - final long domainId = 1L; - final double amount = 11.0; - final long updatedBy = 2L; - - QuotaCreditsVO credit = new QuotaCreditsVO(); - credit.setCredit(new BigDecimal(amount)); - - Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenReturn(credit); - Mockito.when(quotaBalanceDaoMock.getLastQuotaBalance(Mockito.anyLong(), Mockito.anyLong())).thenReturn(new BigDecimal(111)); - Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(credit); - - AccountVO account = new AccountVO(); - account.setState(Account.State.LOCKED); - Mockito.when(accountDaoMock.findById(Mockito.anyLong())).thenReturn(account); - - QuotaCreditsResponse resp = quotaResponseBuilderSpy.addQuotaCredits(accountId, domainId, amount, updatedBy, true); - assertTrue(resp.getCredit().compareTo(credit.getCredit()) == 0); - } - @Test public void testListQuotaEmailTemplates() { QuotaEmailTemplateListCmd cmd = new QuotaEmailTemplateListCmd(); @@ -710,33 +696,18 @@ public void createQuotaCreditsListResponseTestReturnsObject() { } private QuotaCreditsListCmd createQuotaCreditsListCmdForTests() { - Mockito.doReturn(false).when(accountManagerMock).isNormalUser(Mockito.anyLong()); - QuotaCreditsListCmd cmd = new QuotaCreditsListCmd(); - cmd.setAccountId(1L); - cmd.setDomainId(2L); + QuotaCreditsListCmd cmd = Mockito.mock(QuotaCreditsListCmd.class); + Mockito.doReturn(1L).when(cmd).getEntityOwnerId(); + Mockito.doReturn(2L).when(cmd).getDomainId(); + Mockito.doReturn(new Date()).when(cmd).getStartDate(); + Mockito.doReturn(new Date()).when(cmd).getEndDate(); return cmd; } - @Test(expected = InvalidParameterValueException.class) - public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenBothAccountIdAndDomainIdAreNull() { - QuotaCreditsListCmd cmd = new QuotaCreditsListCmd(); - - quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); - } - @Test(expected = InvalidParameterValueException.class) public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenStartDateIsAfterEndDate() { QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests(); - cmd.setStartDate(new Date()); - cmd.setEndDate(DateUtils.addDays(new Date(), -1)); - - quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); - } - - @Test(expected = PermissionDeniedException.class) - public void getCreditsForQuotaCreditsListTestThrowsPermissionDeniedExceptionWhenDomainIdIsProvidedAndCallerIsNormalUser() { - QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests(); - Mockito.doReturn(true).when(accountManagerMock).isNormalUser(Mockito.anyLong()); + Mockito.doReturn(DateUtils.addDays(new Date(), -1)).when(cmd).getEndDate(); quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); } @@ -747,7 +718,7 @@ public void getCreditsForQuotaCreditsListTestReturnsData() { List expected = new ArrayList<>(); expected.add(new QuotaCreditsVO()); - Mockito.doReturn(expected).when(quotaCreditsDaoMock).findCredits(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.anyBoolean()); + Mockito.doReturn(expected).when(quotaCreditsDaoMock).findCredits(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); List result = quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); @@ -1007,7 +978,6 @@ private List getQuotaUsagesForTest() { @Test public void createStatementItemTestReturnItem() { List quotaUsages = getQuotaUsagesForTest(); - Mockito.doNothing().when(quotaResponseBuilderSpy).setStatementItemResources(Mockito.any(), Mockito.anyInt(), Mockito.any(), Mockito.anyBoolean()); QuotaStatementItemResponse result = quotaResponseBuilderSpy.createStatementItem(0, quotaUsages, false); @@ -1018,15 +988,6 @@ public void createStatementItemTestReturnItem() { Assert.assertEquals(quotaTypeExpected.getQuotaName(), result.getUsageName()); } - @Test - public void setStatementItemResourcesTestDoNotShowResourcesDoNothing() { - QuotaStatementItemResponse item = new QuotaStatementItemResponse(1); - - quotaResponseBuilderSpy.setStatementItemResources(item, 0, getQuotaUsagesForTest(), false); - - Assert.assertNull(item.getResources()); - } - @Test public void getAccountIdForQuotaStatementTestReturnsProvidedAccount() { long providedAccountId = 200L; @@ -1185,4 +1146,226 @@ public void retrieveResourceTestReturnsCorrectResource() { Assert.assertNotNull(result); Assert.assertEquals(mockResource, result); } + + @Test + public void lockOrUnlockAccountIfRequiredTestPositiveBalanceUnlocksAccount() { + Mockito.doReturn(Account.State.LOCKED).when(accountMock).getState(); + + quotaResponseBuilderSpy.lockOrUnlockAccountIfRequired(BigDecimal.TEN, accountMock, true); + + Mockito.verify(accountManagerMock).enableAccount(accountMock.getAccountName(), domainVoMock.getId(), accountMock.getId()); + Mockito.verify(accountManagerMock, Mockito.never()).lockAccount(Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void lockOrUnlockAccountIfRequiredTestNegativeBalanceLocksAccount() { + Mockito.doReturn(Account.State.ENABLED).when(accountMock).getState(); + Mockito.doReturn(true).when(quotaManagerMock).isLockable(accountMock); + + quotaResponseBuilderSpy.lockOrUnlockAccountIfRequired(BigDecimal.valueOf(-10), accountMock, true); + + Mockito.verify(accountManagerMock).lockAccount(accountMock.getAccountName(), domainVoMock.getId(), accountMock.getId()); + Mockito.verify(accountManagerMock, Mockito.never()).enableAccount(Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void addQuotaCreditsTestValidParameters() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Mockito.doReturn(10D).when(cmd).getValue(); + Mockito.doReturn(BigDecimal.TEN).when(quotaCreditsVoMock).getCredit(); + Mockito.doReturn(accountMock).when(accountDaoMock).findById(Mockito.anyLong()); + Mockito.doReturn(null).when(quotaBalanceDaoMock).findLaterBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), + Mockito.any()); + Mockito.doReturn(quotaCreditsVoMock).when(quotaResponseBuilderSpy).persistQuotaCredits(Mockito.any(), Mockito.anyDouble(), + Mockito.any(), Mockito.any(), Mockito.anyBoolean()); + Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(Mockito.any()); + + QuotaCreditsResponse response = quotaResponseBuilderSpy.addQuotaCredits(cmd); + + Assert.assertEquals(BigDecimal.TEN, response.getCredit()); + } + + @Test(expected = InvalidParameterValueException.class) + public void addQuotaCreditsTestThrowsExceptionWhenValueIsNull() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Mockito.doReturn(null).when(cmd).getValue(); + + quotaResponseBuilderSpy.addQuotaCredits(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void addQuotaCreditsTestThrowsExceptionWhenDepositDateIsIncorrect() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Mockito.doReturn(100.0).when(cmd).getValue(); + Mockito.doReturn(accountMock).when(accountDaoMock).findById(Mockito.anyLong()); + Mockito.doReturn(quotaBalanceVoMock).when(quotaBalanceDaoMock).findLaterBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); + + quotaResponseBuilderSpy.addQuotaCredits(cmd); + } + + @Test + public void persistQuotaCreditsTestSavesCreditsAndBalanceSuccessfully() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Long accountId = 1L; + Long domainId = 2L; + Double value = 10D; + Date depositedOn = new Date(); + AccountVO account = Mockito.mock(AccountVO.class); + BigDecimal currentBalance = BigDecimal.ZERO; + + Mockito.doReturn(accountId).when(account).getId(); + Mockito.doReturn(domainId).when(account).getDomainId(); + Mockito.doReturn(null).when(cmd).getQuotaEnforce(); + Mockito.doReturn(null).when(cmd).getMinBalance(); + Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + Mockito.when(quotaBalanceDaoMock.getLastQuotaBalance(accountId, domainId)).thenReturn(currentBalance); + + QuotaCreditsVO result = quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, false); + + Assert.assertNotNull(result); + Assert.assertEquals(BigDecimal.TEN, result.getCredit()); + Assert.assertEquals(accountId, result.getAccountId()); + Assert.assertEquals(depositedOn, result.getUpdatedOn()); + Mockito.verify(quotaServiceMock).saveQuotaAccount(account, currentBalance, depositedOn); + Mockito.verify(quotaServiceMock, Mockito.never()).setLockAccount(Mockito.anyLong(), Mockito.anyBoolean()); + Mockito.verify(quotaServiceMock, Mockito.never()).setMinBalance(Mockito.anyLong(), Mockito.anyDouble()); + Mockito.verify(quotaResponseBuilderSpy, Mockito.never()).lockOrUnlockAccountIfRequired(Mockito.any(), Mockito.any(), Mockito.anyBoolean()); + } + + @Test + public void persistQuotaCreditsTestCallsSetLockAccountWhenQuotaEnforceProvided() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Long accountId = 1L; + Double value = 100.0; + Date depositedOn = new Date(); + AccountVO account = Mockito.mock(AccountVO.class); + + Mockito.doReturn(accountId).when(account).getId(); + Mockito.when(cmd.getQuotaEnforce()).thenReturn(Boolean.TRUE); + Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, false); + + Mockito.verify(quotaServiceMock).setLockAccount(accountId, Boolean.TRUE); + } + + @Test + public void persistQuotaCreditsTestCallsSetMinBalanceWhenProvided() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Long accountId = 1L; + Double value = 100.0; + Date depositedOn = new Date(); + AccountVO account = Mockito.mock(AccountVO.class); + + Mockito.when(cmd.getMinBalance()).thenReturn(50.0); + Mockito.doReturn(accountId).when(account).getId(); + Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, false); + + Mockito.verify(quotaServiceMock).setMinBalance(accountId, 50.0); + } + + @Test + public void persistQuotaCreditsTestLocksOrUnlocksAccountWhenEnforcementIsEnabledGlobally() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Long accountId = 1L; + Long domainId = 2L; + Double value = 100.0; + Date depositedOn = new Date(); + AccountVO account = Mockito.mock(AccountVO.class); + BigDecimal currentBalance = BigDecimal.ZERO; + + Mockito.doReturn(accountId).when(account).getId(); + Mockito.doReturn(domainId).when(account).getDomainId(); + Mockito.when(cmd.getMinBalance()).thenReturn(50.0); + Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + Mockito.when(quotaBalanceDaoMock.getLastQuotaBalance(accountId, domainId)).thenReturn(currentBalance); + + quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, true); + + Mockito.verify(quotaResponseBuilderSpy).lockOrUnlockAccountIfRequired(currentBalance, account, false); + } + + @Test + public void createQuotaConsumptionHistoryTestReturnsNullForZeroQuotaUsed() { + List result = quotaResponseBuilderSpy.createQuotaConsumptionHistory(new ArrayList<>(), BigDecimal.ZERO); + + Assert.assertNull(result); + } + + @Test + public void createQuotaConsumptionHistoryTestIgnoresNullQuotaUsed() { + Date now = new Date(); + + List usageRecords = new ArrayList<>(); + QuotaUsageJoinVO record1 = new QuotaUsageJoinVO(); + record1.setStartDate(now); + record1.setEndDate(now); + record1.setQuotaUsed(null); + record1.setUsageItemId(10L); + + QuotaUsageJoinVO record2 = new QuotaUsageJoinVO(); + record2.setStartDate(new Date(now.getTime() + 1000)); + record2.setEndDate(new Date(now.getTime() + 1000)); + record2.setQuotaUsed(BigDecimal.valueOf(10)); + record2.setUsageItemId(11L); + + usageRecords.add(record1); + usageRecords.add(record2); + + BigDecimal totalQuotaUsed = BigDecimal.valueOf(10); + + List result = quotaResponseBuilderSpy.createQuotaConsumptionHistory(usageRecords, totalQuotaUsed); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(BigDecimal.valueOf(10), result.get(0).getQuotaConsumed()); + } + + @Test + public void createQuotaConsumptionHistoryTestCorrectlyAggregatesRecords() { + List usageRecords = new ArrayList<>(); + Date now = new Date(); + + QuotaUsageJoinVO record1 = new QuotaUsageJoinVO(); + record1.setStartDate(now); + record1.setEndDate(new Date(now.getTime() + 1000)); + record1.setQuotaUsed(BigDecimal.valueOf(5)); + record1.setUsageItemId(10L); + + QuotaUsageJoinVO record2 = new QuotaUsageJoinVO(); + record2.setStartDate(new Date(now.getTime() + 2000)); + record2.setEndDate(new Date(now.getTime() + 3000)); + record2.setQuotaUsed(BigDecimal.valueOf(15)); + record2.setUsageItemId(11L); + + QuotaUsageJoinVO record3 = new QuotaUsageJoinVO(); + record3.setStartDate(new Date(now.getTime() + 2000)); + record3.setEndDate(new Date(now.getTime() + 3000)); + record3.setQuotaUsed(BigDecimal.valueOf(5)); + record3.setUsageItemId(11L); + + usageRecords.add(record1); + usageRecords.add(record2); + usageRecords.add(record3); + + BigDecimal totalQuotaUsed = BigDecimal.valueOf(25); + + List result = quotaResponseBuilderSpy.createQuotaConsumptionHistory(usageRecords, totalQuotaUsed); + + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + + QuotaStatementItemHistoryResponse firstHistory = result.get(0); + QuotaStatementItemHistoryResponse secondHistory = result.get(1); + + Assert.assertEquals(BigDecimal.valueOf(5), firstHistory.getQuotaConsumed()); + Assert.assertEquals(record1.getStartDate(), firstHistory.getStartDate()); + Assert.assertEquals(record1.getEndDate(), firstHistory.getEndDate()); + + Assert.assertEquals(BigDecimal.valueOf(20), secondHistory.getQuotaConsumed()); + Assert.assertEquals(record2.getStartDate(), secondHistory.getStartDate()); + Assert.assertEquals(record2.getEndDate(), secondHistory.getEndDate()); + } } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 2a49680e8fd8..db9c1d1dafde 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -3964,7 +3964,7 @@ public Long finalizeAccountId(Long accountId, String accountName, Long domainId, if (getActiveAccountById(accountId) != null) { return accountId; } - throw new InvalidParameterValueException(String.format("Unable to find account with the specified ID.")); + throw new InvalidParameterValueException("Unable to find account with the specified ID."); } if (accountName == null && domainId == null) { diff --git a/ui/jest.config.js b/ui/jest.config.js index eb5f3f4db1b8..f67823eb9900 100644 --- a/ui/jest.config.js +++ b/ui/jest.config.js @@ -42,7 +42,7 @@ module.exports = { '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' ], transformIgnorePatterns: [ - '/node_modules/(?!ant-design-vue|vue|@babel/runtime|lodash-es|@ant-design|@vue-js-cron)' + '/node_modules/(?!ant-design-vue|vue|@babel/runtime|lodash-es|@ant-design|@vue-js-cron|prism)' ], collectCoverage: true, collectCoverageFrom: [ diff --git a/ui/package-lock.json b/ui/package-lock.json index c9f90b6552a4..8592158204b8 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cloudstack-ui", - "version": "4.19.0", + "version": "4.22.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -114,8 +114,7 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.5.4.tgz", "integrity": "sha512-shM3q7rUbNyXVVRkQJQseXv6bnYM3BUma/eZhwXR4xsuM+bqWnJKvW7SAfRjP7LuSCocrexa5AXhjjawNHrIlw==", - "dev": true, - "requires": {} + "dev": true }, "@apollographql/graphql-playground-html": { "version": "1.6.27", @@ -445,11 +444,15 @@ "@babel/types": "^7.18.6" } }, + "@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==" + }, "@babel/helper-validator-identifier": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", - "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", - "dev": true + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -544,9 +547,12 @@ } }, "@babel/parser": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.6.tgz", - "integrity": "sha512-uQVSa9jJUe/G/304lXspfWVpKpK4euFLgGiMQFOCpM/bgcAdeoHwi/OQz23O9GK2osz26ZiXRRV9aV+Yl1O8tw==" + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "requires": { + "@babel/types": "^7.29.7" + } }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -1489,13 +1495,12 @@ } }, "@babel/types": { - "version": "7.18.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.7.tgz", - "integrity": "sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" } }, "@ctrl/tinycolor": { @@ -1535,8 +1540,7 @@ "@fortawesome/vue-fontawesome": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.6.tgz", - "integrity": "sha512-akrL7lTroyNpPkoHtvK2UpsMzJr6jXdHaQ0YdcwqDsB8jdwlpNHZYijpOUd9KJsARr+VB3WXY4EyObepqJ4ytQ==", - "requires": {} + "integrity": "sha512-akrL7lTroyNpPkoHtvK2UpsMzJr6jXdHaQ0YdcwqDsB8jdwlpNHZYijpOUd9KJsARr+VB3WXY4EyObepqJ4ytQ==" }, "@gar/promisify": { "version": "1.1.3", @@ -1593,6 +1597,182 @@ "postcss": "^7.0.0" } }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "requires": { + "ansi-regex": "^6.2.2" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + } + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2186,6 +2366,64 @@ "fastq": "^1.6.0" } }, + "@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "dependencies": { + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + } + } + } + }, "@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -2250,8 +2488,34 @@ "@npmcli/promise-spawn": "^1.3.2", "node-gyp": "^7.1.0", "read-package-json-fast": "^2.0.1" + }, + "dependencies": { + "node-gyp": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", + "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.3", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "request": "^2.88.2", + "rimraf": "^3.0.2", + "semver": "^7.3.2", + "tar": "^6.0.2", + "which": "^2.0.2" + } + } } }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -3366,8 +3630,7 @@ "version": "4.5.19", "resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.19.tgz", "integrity": "sha512-DUmfdkG3pCdkP7Iznd87RfE9Qm42mgp2hcrNcYQYSru1W1gX2dG/JcW8bxmeGSa06lsxi9LEIc/QD1yPajSCZw==", - "dev": true, - "requires": {} + "dev": true }, "@vue/cli-service": { "version": "4.5.19", @@ -3878,8 +4141,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz", "integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==", - "dev": true, - "requires": {} + "dev": true }, "@vue/reactivity": { "version": "3.2.37", @@ -3938,8 +4200,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.0.0.tgz", "integrity": "sha512-zL5kygNq7hONrO1CzaUGprEAklAX+pH8J1MPMCU3Rd2xtSYkZ+PmKU3oEDRg8VAGdL5lNJHzDgrud5amFPtirw==", - "dev": true, - "requires": {} + "dev": true }, "@vue/web-component-wrapper": { "version": "1.3.0", @@ -3951,6 +4212,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, "requires": { "@webassemblyjs/helper-module-context": "1.9.0", "@webassemblyjs/helper-wasm-bytecode": "1.9.0", @@ -3960,22 +4222,26 @@ "@webassemblyjs/floating-point-hex-parser": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==" + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true }, "@webassemblyjs/helper-api-error": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==" + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true }, "@webassemblyjs/helper-buffer": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==" + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true }, "@webassemblyjs/helper-code-frame": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, "requires": { "@webassemblyjs/wast-printer": "1.9.0" } @@ -3983,12 +4249,14 @@ "@webassemblyjs/helper-fsm": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", - "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==" + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true }, "@webassemblyjs/helper-module-context": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0" } @@ -3996,12 +4264,14 @@ "@webassemblyjs/helper-wasm-bytecode": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==" + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true }, "@webassemblyjs/helper-wasm-section": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-buffer": "1.9.0", @@ -4013,6 +4283,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } @@ -4021,6 +4292,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, "requires": { "@xtuc/long": "4.2.2" } @@ -4028,12 +4300,14 @@ "@webassemblyjs/utf8": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==" + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true }, "@webassemblyjs/wasm-edit": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-buffer": "1.9.0", @@ -4049,6 +4323,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-wasm-bytecode": "1.9.0", @@ -4061,6 +4336,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-buffer": "1.9.0", @@ -4072,6 +4348,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-api-error": "1.9.0", @@ -4085,6 +4362,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/floating-point-hex-parser": "1.9.0", @@ -4098,6 +4376,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/wast-parser": "1.9.0", @@ -4116,12 +4395,14 @@ "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true }, "@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true }, "@zxing/text-encoding": { "version": "0.9.0", @@ -4153,7 +4434,8 @@ "acorn": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true }, "acorn-globals": { "version": "4.3.4", @@ -4169,8 +4451,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-walk": { "version": "6.2.0", @@ -4226,13 +4507,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "requires": {} + "dev": true }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} + "dev": true }, "alphanum-sort": { "version": "1.0.2", @@ -4399,14 +4680,14 @@ "resolved": "https://registry.npmjs.org/antd-theme-generator/-/antd-theme-generator-1.2.11.tgz", "integrity": "sha512-7A3lXyLb7eD7MXK7aSgZZ4DxQEdhZwyKhzIm70orUZPQJ8N8TWhZphyOWSGCe8yUqGQhi8PcpM2pLmTriZyKBw==", "requires": { - "glob": "*", - "hash.js": "*", - "less": "*", + "glob": "^7.1.3", + "hash.js": "^1.1.5", + "less": "^3.9.0", "less-bundle-promise": "^1.0.11", - "less-plugin-npm-import": "*", - "postcss": "*", + "less-plugin-npm-import": "^2.1.0", + "postcss": "^6.0.21", "postcss-less": "^3.1.4", - "strip-css-comments": "*" + "strip-css-comments": "^4.1.0" }, "dependencies": { "ansi-styles": { @@ -5129,8 +5410,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.5.0.tgz", "integrity": "sha512-lO5oTjgiC3vlVg2RKr3RiXIIQ5pGXBFxYGGUkKDhTud3jMIhs+gel8L8zsEjKaKxkjHhCQAA/bcEfYiKkGQIvA==", - "dev": true, - "requires": {} + "dev": true }, "apollo-server-express": { "version": "2.25.4", @@ -5247,17 +5527,20 @@ "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==" + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true }, "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true }, "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==" + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true }, "array-equal": { "version": "1.0.0", @@ -5313,7 +5596,8 @@ "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==" + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true }, "array.prototype.flat": { "version": "1.3.0", @@ -5376,6 +5660,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, "requires": { "object-assign": "^4.1.1", "util": "0.10.3" @@ -5384,12 +5669,14 @@ "inherits": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==" + "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", + "dev": true }, "util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==", + "dev": true, "requires": { "inherits": "2.0.1" } @@ -5404,7 +5691,8 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==" + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true }, "ast-types": { "version": "0.13.3", @@ -5469,7 +5757,8 @@ "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true }, "autoprefixer": { "version": "9.8.8", @@ -5557,8 +5846,7 @@ "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", - "dev": true, - "requires": {} + "dev": true }, "babel-eslint": { "version": "10.1.0", @@ -6167,6 +6455,7 @@ "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, "requires": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", @@ -6181,6 +6470,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -6189,6 +6479,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -6197,6 +6488,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -6205,6 +6497,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -6216,7 +6509,8 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true }, "batch": { "version": "0.6.1", @@ -6297,7 +6591,8 @@ "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true }, "bn.js": { "version": "5.2.1", @@ -6479,6 +6774,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -6496,6 +6792,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -6615,6 +6912,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, "requires": { "pako": "~1.0.5" } @@ -6689,7 +6987,8 @@ "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "buffer-indexof": { "version": "1.1.1", @@ -6711,7 +7010,8 @@ "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==" + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true }, "builtins": { "version": "1.0.3", @@ -6762,6 +7062,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, "requires": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", @@ -7008,8 +7309,7 @@ "chartjs-adapter-moment": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz", - "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==", - "requires": {} + "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==" }, "check-types": { "version": "8.0.3", @@ -7085,7 +7385,8 @@ "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true }, "ci-info": { "version": "2.0.0", @@ -7110,6 +7411,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, "requires": { "arr-union": "^3.1.0", "define-property": "^0.2.5", @@ -7121,6 +7423,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -7414,6 +7717,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, "requires": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" @@ -7490,12 +7794,14 @@ "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true }, "compressible": { "version": "2.0.18", @@ -7558,6 +7864,7 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -7628,7 +7935,8 @@ "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true }, "console-control-strings": { "version": "1.1.0", @@ -7647,7 +7955,8 @@ "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true }, "content-disposition": { "version": "0.5.4", @@ -7706,6 +8015,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, "requires": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", @@ -7719,6 +8029,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, "requires": { "minimist": "^1.2.6" } @@ -7727,6 +8038,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -7736,7 +8048,8 @@ "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==" + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true }, "copy-to-clipboard": { "version": "3.3.1", @@ -8339,7 +8652,8 @@ "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" + "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==", + "dev": true }, "dashdash": { "version": "1.14.1", @@ -8428,7 +8742,8 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==" + "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "dev": true }, "decompress": { "version": "4.2.1", @@ -8739,6 +9054,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, "requires": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" @@ -8748,6 +9064,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -8756,6 +9073,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -8764,6 +9082,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -8999,7 +9318,8 @@ "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true }, "domelementtype": { "version": "1.3.1", @@ -9247,6 +9567,7 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -9254,6 +9575,12 @@ "stream-shift": "^1.0.0" } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "easy-stack": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", @@ -9387,6 +9714,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, "requires": { "graceful-fs": "^4.1.2", "memory-fs": "^0.5.0", @@ -9397,6 +9725,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" @@ -9435,6 +9764,7 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, "requires": { "prr": "~1.0.1" } @@ -9735,8 +10065,7 @@ "version": "14.1.1", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz", "integrity": "sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.6", @@ -10102,8 +10431,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.1.0.tgz", "integrity": "sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-vue": { "version": "7.20.0", @@ -10138,6 +10466,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, "requires": { "esrecurse": "^4.1.0", "estraverse": "^4.1.1" @@ -10203,6 +10532,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "requires": { "estraverse": "^5.2.0" }, @@ -10210,14 +10540,16 @@ "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true } } }, "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true }, "estree-walker": { "version": "2.0.2", @@ -10251,7 +10583,8 @@ "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true }, "eventsource": { "version": "2.0.2", @@ -10302,6 +10635,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, "requires": { "debug": "^2.3.3", "define-property": "^0.2.5", @@ -10316,6 +10650,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -10324,6 +10659,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -10332,6 +10668,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -10339,7 +10676,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true } } }, @@ -10392,6 +10730,12 @@ } } }, + "exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true + }, "express": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", @@ -10522,6 +10866,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -10531,6 +10876,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -10539,6 +10885,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -10571,6 +10918,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, "requires": { "array-unique": "^0.3.2", "define-property": "^1.0.0", @@ -10586,6 +10934,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -10594,6 +10943,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -10602,6 +10952,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -10610,6 +10961,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -10618,6 +10970,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -10808,6 +11161,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -10819,6 +11173,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -10896,6 +11251,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, "requires": { "commondir": "^1.0.1", "make-dir": "^2.0.0", @@ -10906,6 +11262,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, "requires": { "pify": "^4.0.1", "semver": "^5.6.0" @@ -10914,12 +11271,14 @@ "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -10990,6 +11349,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, "requires": { "inherits": "^2.0.3", "readable-stream": "^2.3.6" @@ -11011,7 +11371,58 @@ "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==" + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true + }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } }, "forever-agent": { "version": "0.6.1", @@ -11038,6 +11449,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, "requires": { "map-cache": "^0.2.2" } @@ -11052,6 +11464,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -11098,6 +11511,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", "integrity": "sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==", + "dev": true, "requires": { "graceful-fs": "^4.1.2", "iferr": "^0.1.5", @@ -11245,7 +11659,8 @@ "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==" + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true }, "getpass": { "version": "0.1.7", @@ -11489,8 +11904,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==", - "dev": true, - "requires": {} + "dev": true }, "growly": { "version": "1.3.0", @@ -11614,6 +12028,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, "requires": { "get-value": "^2.0.6", "has-values": "^1.0.0", @@ -11624,6 +12039,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, "requires": { "is-number": "^3.0.0", "kind-of": "^4.0.0" @@ -11633,6 +12049,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -11910,9 +12327,9 @@ } }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" }, "http-deceiver": { "version": "1.2.7", @@ -12045,7 +12462,8 @@ "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true }, "https-proxy-agent": { "version": "5.0.1", @@ -12091,12 +12509,14 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true }, "iferr": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==" + "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==", + "dev": true }, "ignore": { "version": "4.0.6", @@ -12377,7 +12797,13 @@ "ip": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "dev": true + }, + "ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==" }, "ip-regex": { "version": "2.1.0", @@ -12401,6 +12827,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -12409,6 +12836,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -12503,6 +12931,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -12511,6 +12940,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -12530,6 +12960,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -12539,7 +12970,8 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true } } }, @@ -12558,7 +12990,8 @@ "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==" + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true }, "is-extglob": { "version": "2.1.1", @@ -12632,6 +13065,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -12640,6 +13074,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -12812,12 +13247,14 @@ "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true }, "is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==" + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true }, "is-yarn-global": { "version": "0.3.0", @@ -12843,7 +13280,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true }, "isstream": { "version": "0.1.2", @@ -13000,6 +13438,16 @@ "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", "dev": true }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "javascript-stringify": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-1.6.0.tgz", @@ -13908,8 +14356,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "24.9.0", @@ -14954,7 +15401,8 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true }, "json-parse-even-better-errors": { "version": "2.3.1", @@ -15050,7 +15498,8 @@ "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true }, "kleur": { "version": "3.0.3", @@ -15343,7 +15792,8 @@ "loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==" + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true }, "loader-utils": { "version": "2.0.2", @@ -15610,12 +16060,14 @@ "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==" + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, "requires": { "object-visit": "^1.0.0" } @@ -15656,6 +16108,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "integrity": "sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==", + "dev": true, "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" @@ -15704,6 +16157,7 @@ "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -16019,6 +16473,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, "requires": { "concat-stream": "^1.5.0", "duplexify": "^3.4.2", @@ -16041,6 +16496,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, "requires": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" @@ -16050,6 +16506,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -16058,6 +16515,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -16093,6 +16551,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "integrity": "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==", + "dev": true, "requires": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", @@ -16106,6 +16565,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, "requires": { "minimist": "^1.2.6" } @@ -16114,6 +16574,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -16164,21 +16625,21 @@ } }, "nan": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", - "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", - "dev": true, - "optional": true + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "dev": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==" }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -16254,7 +16715,8 @@ "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true }, "nested-error-stacks": { "version": "2.0.1", @@ -16358,20 +16820,211 @@ "dev": true }, "node-gyp": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", - "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.0.1.tgz", + "integrity": "sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==", + "dev": true, "requires": { "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.3", - "nopt": "^5.0.0", - "npmlog": "^4.1.2", - "request": "^2.88.2", - "rimraf": "^3.0.2", - "semver": "^7.3.2", - "tar": "^6.0.2", - "which": "^2.0.2" + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^4.0.0" + }, + "dependencies": { + "@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true + }, + "brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "requires": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + } + }, + "fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "requires": { + "minipass": "^7.0.3" + } + }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "requires": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "dependencies": { + "proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true + } + } + }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.2" + } + }, + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true + }, + "minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "requires": { + "minipass": "^7.0.3" + } + }, + "minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + }, + "nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "requires": { + "abbrev": "^2.0.0" + } + }, + "ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "requires": { + "minipass": "^7.0.3" + } + }, + "unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "requires": { + "unique-slug": "^4.0.0" + } + }, + "unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "requires": { + "isexe": "^3.1.1" + } + } } }, "node-int64": { @@ -16384,6 +17037,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, "requires": { "assert": "^1.1.1", "browserify-zlib": "^0.2.0", @@ -16414,6 +17068,7 @@ "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", @@ -16423,7 +17078,8 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true } } }, @@ -16694,6 +17350,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, "requires": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", @@ -16704,6 +17361,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -16712,6 +17370,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -16756,6 +17415,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, "requires": { "isobject": "^3.0.0" } @@ -16788,6 +17448,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -16963,7 +17624,8 @@ "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==" + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true }, "os-tmpdir": { "version": "1.0.2", @@ -17077,6 +17739,12 @@ } } }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "pacote": { "version": "11.3.5", "resolved": "https://registry.npmjs.org/pacote/-/pacote-11.3.5.tgz", @@ -17106,12 +17774,14 @@ "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true }, "parallel-transform": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, "requires": { "cyclist": "^1.0.1", "inherits": "^2.0.3", @@ -17209,12 +17879,14 @@ "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==" + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true }, "path-dirname": { "version": "1.0.2", @@ -17249,6 +17921,30 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true + } + } + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -17397,6 +18093,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, "requires": { "find-up": "^3.0.0" }, @@ -17405,6 +18102,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, "requires": { "locate-path": "^3.0.0" } @@ -17413,6 +18111,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -17422,6 +18121,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, "requires": { "p-limit": "^2.0.0" } @@ -17429,7 +18129,8 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true } } }, @@ -17482,7 +18183,8 @@ "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==" + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true }, "postcss": { "version": "7.0.39", @@ -18096,7 +18798,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true, "optional": true }, "pretty": { @@ -18175,6 +18876,11 @@ } } }, + "prism-es6": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prism-es6/-/prism-es6-1.2.0.tgz", + "integrity": "sha512-A8JV9G2zKM8PWksT7YJcmnaWtYO6C9hSfxM/xv0RxB2aNc8rjv30WakzIw1gWyqLi2eiqquo2KmS7orxqlm+yg==" + }, "prismjs": { "version": "1.28.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.28.0.tgz", @@ -18187,10 +18893,17 @@ "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", "dev": true }, + "proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true }, "process-exists": { "version": "3.1.0", @@ -18253,7 +18966,8 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true }, "ps-list": { "version": "4.1.0", @@ -18309,6 +19023,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, "requires": { "duplexify": "^3.6.0", "inherits": "^2.0.3", @@ -18319,6 +19034,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -18369,12 +19085,14 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "dev": true }, "querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==" + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true }, "querystringify": { "version": "2.2.0", @@ -18896,25 +19614,6 @@ "rc-util": "^5.36.0" } }, - "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - } - }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -19084,6 +19783,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, "requires": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" @@ -19245,12 +19945,14 @@ "repeat-element": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==" + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==" + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true }, "request": { "version": "2.88.2", @@ -19363,7 +20065,8 @@ "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==" + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "dev": true }, "responselike": { "version": "1.0.2", @@ -19386,7 +20089,8 @@ "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true }, "retry": { "version": "0.12.0", @@ -19463,6 +20167,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", + "dev": true, "requires": { "aproba": "^1.1.1" } @@ -19485,6 +20190,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, "requires": { "ret": "~0.1.10" } @@ -19614,15 +20320,6 @@ "xmlchars": "^2.1.1" } }, - "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, "schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -19686,12 +20383,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==" }, "semver-diff": { "version": "3.1.1", @@ -19788,6 +20482,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, "requires": { "randombytes": "^2.1.0" } @@ -19869,6 +20564,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -19880,6 +20576,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -19888,6 +20585,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -19897,7 +20595,8 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true }, "setprototypeof": { "version": "1.2.0", @@ -20079,6 +20778,7 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, "requires": { "base": "^0.11.1", "debug": "^2.2.0", @@ -20094,6 +20794,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -20102,6 +20803,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -20110,6 +20812,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -20117,12 +20820,14 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true } } }, @@ -20130,6 +20835,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, "requires": { "define-property": "^1.0.0", "isobject": "^3.0.0", @@ -20140,6 +20846,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -20148,6 +20855,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -20156,6 +20864,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -20164,6 +20873,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -20176,6 +20886,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, "requires": { "kind-of": "^3.2.0" }, @@ -20184,6 +20895,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -20234,11 +20946,11 @@ } }, "socks": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", - "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", "requires": { - "ip": "^1.1.5", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, @@ -20286,14 +20998,15 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, "requires": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0", @@ -20306,6 +21019,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -20314,7 +21028,8 @@ "source-map-url": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==" + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "dev": true }, "sourcemap-codec": { "version": "1.4.8", @@ -20407,6 +21122,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, "requires": { "extend-shallow": "^3.0.0" } @@ -20482,6 +21198,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, "requires": { "define-property": "^0.2.5", "object-copy": "^0.1.0" @@ -20491,6 +21208,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -20522,6 +21240,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, "requires": { "inherits": "~2.0.1", "readable-stream": "^2.0.2" @@ -20531,6 +21250,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, "requires": { "end-of-stream": "^1.1.0", "stream-shift": "^1.0.0" @@ -20540,6 +21260,7 @@ "version": "2.8.3", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, "requires": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.1", @@ -20551,7 +21272,8 @@ "stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true }, "streamsearch": { "version": "0.1.2", @@ -20565,14 +21287,6 @@ "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "dev": true }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", @@ -20637,6 +21351,14 @@ "es-abstract": "^1.19.5" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -20915,7 +21637,8 @@ "tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true }, "tar": { "version": "6.1.11", @@ -21100,6 +21823,7 @@ "version": "4.8.0", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, "requires": { "commander": "^2.20.0", "source-map": "~0.6.1", @@ -21109,7 +21833,8 @@ "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true } } }, @@ -21117,6 +21842,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "dev": true, "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", @@ -21133,6 +21859,7 @@ "version": "12.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, "requires": { "bluebird": "^3.5.5", "chownr": "^1.1.1", @@ -21154,12 +21881,14 @@ "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "requires": { "yallist": "^3.0.2" } @@ -21168,6 +21897,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, "requires": { "minimist": "^1.2.6" } @@ -21176,6 +21906,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -21184,6 +21915,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, "requires": { "ajv": "^6.1.0", "ajv-errors": "^1.0.0", @@ -21194,6 +21926,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", + "dev": true, "requires": { "figgy-pudding": "^3.5.1" } @@ -21202,6 +21935,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, "requires": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" @@ -21210,12 +21944,14 @@ "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -21309,6 +22045,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, "requires": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" @@ -21330,6 +22067,7 @@ "version": "2.0.12", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, "requires": { "setimmediate": "^1.0.4" } @@ -21363,7 +22101,8 @@ "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==" + "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==", + "dev": true }, "to-buffer": { "version": "1.1.1", @@ -21371,16 +22110,11 @@ "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", "dev": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -21389,6 +22123,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -21404,6 +22139,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, "requires": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", @@ -21415,6 +22151,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -21576,7 +22313,8 @@ "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==" + "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==", + "dev": true }, "tunnel-agent": { "version": "0.6.0", @@ -21618,7 +22356,8 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true }, "typedarray-to-buffer": { "version": "3.1.5", @@ -21835,6 +22574,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -21900,6 +22640,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, "requires": { "has-value": "^0.3.1", "isobject": "^3.0.0" @@ -21909,6 +22650,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, "requires": { "get-value": "^2.0.3", "has-values": "^0.1.4", @@ -21919,6 +22661,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, "requires": { "isarray": "1.0.0" } @@ -21928,7 +22671,8 @@ "has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==" + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true } } }, @@ -22004,12 +22748,14 @@ "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==" + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "dev": true }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "dev": true, "requires": { "punycode": "1.3.2", "querystring": "0.2.0" @@ -22018,7 +22764,8 @@ "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "dev": true } } }, @@ -22088,12 +22835,14 @@ "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, "requires": { "inherits": "2.0.3" }, @@ -22101,7 +22850,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true } } }, @@ -22196,7 +22946,8 @@ "vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true }, "vue": { "version": "3.2.37", @@ -22213,8 +22964,7 @@ "vue-chartjs": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-4.1.2.tgz", - "integrity": "sha512-QSggYjeFv/L4jFSBQpX8NzrAvX0B+Ha6nDgxkTG8tEXxYOOTwKI4phRLe+B4f+REnkmg7hgPY24R0cixZJyXBg==", - "requires": {} + "integrity": "sha512-QSggYjeFv/L4jFSBQpX8NzrAvX0B+Ha6nDgxkTG8tEXxYOOTwKI4phRLe+B4f+REnkmg7hgPY24R0cixZJyXBg==" }, "vue-clipboard2": { "version": "0.3.3", @@ -22224,6 +22974,57 @@ "clipboard": "^2.0.0" } }, + "vue-code-highlight": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/vue-code-highlight/-/vue-code-highlight-0.7.8.tgz", + "integrity": "sha512-jMt1W0DGisNZ3B2TASFCPLEswohVrwImX+TxzMIZINZyAUUNZ1Xth3soJw1xepTsMgxkIkhsmN/xVNpiI3bU4g==", + "requires": { + "prism-es6": "^1.2.0", + "vue": "^2.5.16" + }, + "dependencies": { + "@vue/compiler-sfc": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", + "requires": { + "@babel/parser": "^7.23.5", + "postcss": "^8.4.14", + "prettier": "^1.18.2 || ^2.0.0", + "source-map": "^0.6.1" + } + }, + "csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "requires": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "vue": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "requires": { + "@vue/compiler-sfc": "2.7.16", + "csstype": "^3.1.0" + } + } + } + }, "vue-codemod": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/vue-codemod/-/vue-codemod-0.0.5.tgz", @@ -22658,8 +23459,7 @@ "vue-web-storage": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/vue-web-storage/-/vue-web-storage-6.1.0.tgz", - "integrity": "sha512-Qsa6QkUyGP+Tj0oxLRc6vEATv6axY89LbwfX8eaMM3i7K/Nl9m0NTELtNp0s8Xhg9F0SkeEP4NC3+LQL3ygMQw==", - "requires": {} + "integrity": "sha512-Qsa6QkUyGP+Tj0oxLRc6vEATv6axY89LbwfX8eaMM3i7K/Nl9m0NTELtNp0s8Xhg9F0SkeEP4NC3+LQL3ygMQw==" }, "vue3-clipboard": { "version": "1.0.0", @@ -22672,8 +23472,7 @@ "vue3-google-login": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/vue3-google-login/-/vue3-google-login-2.0.26.tgz", - "integrity": "sha512-BuTSIeSjINNHNPs+BDF4COnjWvff27IfCBDxK6JPRqvm57lF8iK4B3+zcG8ud6BXfZdyuiDlxletbEDgg4/RFA==", - "requires": {} + "integrity": "sha512-BuTSIeSjINNHNPs+BDF4COnjWvff27IfCBDxK6JPRqvm57lF8iK4B3+zcG8ud6BXfZdyuiDlxletbEDgg4/RFA==" }, "vuedraggable": { "version": "4.1.0", @@ -22750,6 +23549,7 @@ "version": "1.7.5", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "dev": true, "requires": { "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", @@ -22904,6 +23704,7 @@ "version": "4.46.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -22934,6 +23735,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, "requires": { "minimist": "^1.2.0" } @@ -22942,6 +23744,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -22952,6 +23755,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, "requires": { "minimist": "^1.2.6" } @@ -22960,6 +23764,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, "requires": { "ajv": "^6.1.0", "ajv-errors": "^1.0.0", @@ -22970,6 +23775,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, "requires": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" @@ -23672,6 +24478,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, "requires": { "errno": "~0.1.7" } @@ -23761,8 +24568,7 @@ "version": "7.5.8", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "requires": {} + "dev": true }, "xdg-basedir": { "version": "4.0.0", @@ -23821,7 +24627,8 @@ "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true }, "y18n": { "version": "5.0.8", diff --git a/ui/package.json b/ui/package.json index 9801c1b18153..7ccd4916df2b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -65,6 +65,7 @@ "vue": "^3.2.31", "vue-chartjs": "^4.0.7", "vue-clipboard2": "^0.3.1", + "vue-code-highlight": "^0.7.8", "vue-cropper": "^1.0.2", "vue-i18n": "^9.1.6", "vue-loader": "^16.8.3", diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 0e062bcecc60..007ef04a6cac 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -258,6 +258,7 @@ "label.action.vmstoragesnapshot.create": "Take Instance volume Snapshot", "label.actions": "Actions", "label.active": "Active", +"label.activeaccounts": "Active accounts", "label.activate.project": "Activate project", "label.activeviewersessions": "Active sessions", "label.add": "Add", @@ -671,6 +672,7 @@ "label.consoleproxy": "Console proxy", "label.console.proxy": "Console proxy", "label.console.proxy.vm": "Console proxy VM", +"label.consumption": "Consumption", "label.contains": "Contains", "label.contents": "Contents", "label.continue": "Continue", @@ -743,6 +745,8 @@ "label.nameservers": "DNS Nameservers", "label.publicdomainsuffix": "Public domain suffix", "label.credit": "Credit", +"label.credits": "Credits", +"label.creditor": "Creditor", "label.cron": "Cron expression", "label.cron.mode": "Cron mode", "label.crosszones": "Cross Zones", @@ -1059,6 +1063,7 @@ "label.egressdefaultpolicy": "Default egress policy", "label.elastic": "Elastic", "label.email": "Email", +"label.emailtemplate": "Email Template", "label.enable.autoscale.vmgroup": "Enable AutoScaling Group", "label.enable.csi": "Enable CloudStack CSI Driver", "label.enable.custom.action": "Enable Custom Action", @@ -1130,6 +1135,9 @@ "label.expungevmgraceperiod": "Expunge Instance grace period (in sec)", "label.expunged": "Expunged", "label.expunging": "Expunging", +"label.export.data.csv": "Export data as CSV", +"label.export.details.csv": "Export details as CSV", +"label.export.resources.csv": "Export resources as CSV", "label.export.rules": "Export Rules", "label.ext.hostname.tooltip": "External Host Name or IP Address", "label.extension": "Extension", @@ -2100,23 +2108,38 @@ "label.quota": "Quota", "label.quota.add.credits": "Add credits", "label.quota.configuration": "Quota configuration", -"label.quota.credits": "Credits", +"label.quota.consumed": "Quota consumed", +"label.quota.current.balance": "Current balance", "label.quota.email.edit": "Edit Email Template", "label.quota.enforce": "Enforce Quota", +"label.quota.filter.period": "Filtering data from {startDate} to {endDate}", +"label.quota.last.balance": "Last balance at the day", +"label.quota.period.today": "Today", +"label.quota.period.this.week": "This week", +"label.quota.period.this.month": "This month", +"label.quota.period.last.month": "Last month", +"label.quota.period.this.year": "This year", +"label.quota.period.last.year": "Last year", +"label.quota.period.custom": "Custom", +"label.quota.select.period": "Select a period", "label.quota.statement": "Statement", -"label.quota.statement.balance": "Quota balance", -"label.quota.statement.quota": "Quota usage", +"label.quota.statement.history": "History (non-cumulative)", +"label.quota.statement.cumulative.history": "History (cumulative)", "label.quota.statement.tariff": "Quota tariff", "label.quota.summary": "Summary", "label.quota.tariff": "Tariff", "label.quota.tariff.activationrule": "Activation rule", "label.quota.tariff.effectivedate": "Effective date", +"label.quota.tariff.hasactivationrule": "Has activation rule?", "label.quota.tariff.position": "Position", "label.quota.tariff.value": "Tariff value", "label.quota.total": "Total", +"label.quota.total.consumption": "Total quota consumption", "label.quota.type.name": "Usage Type", "label.quota.type.unit": "Usage unit", -"label.quota.usage": "Quota consumption", +"label.quota.usage.types.summary": "Usage types summary", +"label.quota.usage.resources.by.type": "Resources by usage type", +"label.quota.usage.details.by.resource": "Details by resource", "label.quota.validate.activation.rule": "Validate activation rule", "label.quota.value": "Quota value", "label.quotastate": "Quota state", @@ -2212,6 +2235,7 @@ "label.remove.vpc": "Remove VPC", "label.remove.vpc.offering": "Remove VPC Offering", "label.removed": "Removed", +"label.removedaccounts": "Removed Accounts", "label.removing": "Removing", "label.replace": "Replace", "label.replace.acl": "Replace ACL", @@ -3143,6 +3167,10 @@ "message.action.primary.storage.scope.cluster": "Please confirm that you want to change the scope from Zone to the specified Cluster.
This operation will update the database and disconnect the storage pool from all hosts that were previously connected to the primary storage and are not part of the specified cluster.", "message.action.primary.storage.scope.zone": "Please confirm that you want to change the scope from Cluster to Zone.
This operation will update the database and connect the storage pool to all hosts of the zone running the same hypervisor as set on the storage pool.", "message.action.primarystorage.enable.maintenance.mode": "Warning: placing the primary storage into maintenance mode will cause all Instances using volumes from it to be stopped. Do you want to continue?", +"message.action.quota.credit.add.error.accountrequired": "Please, inform the account", +"message.action.quota.credit.add.error.domainidrequired": "Please, inform the domain", +"message.action.quota.credit.add.error.valuerequired": "Please, inform the amount of credits", +"message.action.quota.credit.add.success": "Successfully added {credit} credits to account \"{account}\"", "message.action.quota.tariff.create.error.namerequired": "Please, inform a name for the quota tariff.", "message.action.quota.tariff.create.error.usagetyperequired": "Please, select the usage type of the quota tariff.", "message.action.quota.tariff.create.error.valuerequired": "Please, inform a value for the quota tariff.", @@ -3893,6 +3921,7 @@ "message.public.traffic.in.basic.zone": "Public traffic is generated when Instances in the cloud access the Internet or provide services to clients over the Internet. Publicly accessible IPs must be allocated for this purpose. When a Instance is created, an IP from this set of Public IPs will be allocated to the Instance in addition to the guest IP address. Static 1-1 NAT will be set up automatically between the public IP and the guest IP. End Users can also use the CloudStack UI to acquire additional IPs to implement static NAT between their Instances and the public IP.", "message.quota.tariff.create.success": "Successfully created quota tariff \"{quotaTariff}\"", "message.quota.tariff.update.success": "Successfully updated quota tariff \"{quotaTariff}\"", +"message.quota.usage.resource.warn": "Resources that are tagged as do not have constant metadata (if removed, the data is deleted) and cannot be retrieved.", "message.read.accept.license.agreements": "Please read and accept the terms for the license agreements.", "message.read.admin.guide.scaling.up": "Please read the dynamic scaling section in the admin guide before scaling up.", "message.recover.vm": "Please confirm that you would like to recover this Instance.", @@ -3927,6 +3956,7 @@ "message.remove.sticky.policy.processing": "Removing sticky policy...", "message.remove.vpc": "Please confirm that you want to remove the VPC", "message.request.failed": "Request failed.", + "message.request.no.data": "There is no data to show.", "message.required.add.least.ip": "Please add at least 1 IP Range", "message.required.traffic.type": "All required traffic types should be added and with multiple physical networks each traffic type should have a label.", "message.required.tagged.physical.network": "There can only be one untagged physical network with guest traffic type.", @@ -4319,6 +4349,8 @@ "placeholder.dns.record.name": "e.g. www", "placeholder.dns.record.type": "Select record type", "placeholder.dns.record.contents": "Type a value and hit Enter", +"placeholder.quota.credit.add.min_balance": "Account's minimum balance", +"placeholder.quota.credit.add.value": "Amount of credits", "placeholder.quota.tariff.activationrule": "Quota tariff's activation rule", "placeholder.quota.tariff.description": "Quota tariff's description", "placeholder.quota.tariff.enddate": "Quota tariff's end date", diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index a022769a5b0f..fbcf61489b90 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -236,6 +236,7 @@ "label.action.vmstoragesnapshot.create": "Criar snapshot de volume da VM", "label.actions": "A\u00e7\u00f5es", "label.active": "Ativo", +"label.activeaccounts": "Contas ativas", "label.activate.project": "Ativar projeto", "label.activeviewersessions": "Sess\u00f5es ativas", "label.add": "Adicionar", @@ -606,6 +607,7 @@ "label.consoleproxy": "Console proxy", "label.console.proxy": "Console proxy", "label.console.proxy.vm": "VM da console proxy", +"label.consumption": "Consumo", "label.continue": "Continuar", "label.continue.install": "Continuar com a instala\u00e7\u00e3o", "label.controlnodes": "Controlar nodos", @@ -667,6 +669,8 @@ "label.creating": "Criando", "label.creating.iprange": "Criando intervalos de IP", "label.credit": "Cr\u00e9dito", +"label.credits": "Cr\u00e9ditos", +"label.creditor": "Credor", "label.cron": "Express\u00e3o Cron", "label.cron.mode": "Modo Cron", "label.crosszones": "Inter zonas", @@ -939,7 +943,8 @@ "label.egress.rules": "Regras de sa\u00edda", "label.egressdefaultpolicy": "Pol\u00edtica padr\u00e3o de egress\u00e3o", "label.elastic": "El\u00e1stico", -"label.email": "Email", +"label.email": "E-mail", +"label.emailtemplate": "Template de e-mail", "label.enable.host": "Habilita host", "label.enable.network.offering": "Habilita oferta de rede", "label.enable.oauth": "Ativar Login OAuth", @@ -1490,7 +1495,7 @@ "label.migrate.instance.single.storage": "Migrar todo(s) o(s) volume(s) da Inst\u00e2ncia para um \u00fanico armazenamento prim\u00e1rio", "label.migrate.instance.specific.storages": "Migrar volume(s) da Inst\u00e2ncia para armazenamentos prim\u00e1rios espec\u00edficos", "label.migrate.with.storage": "Migrar com armazenamento", -"label.min_balance": "Saldo m\u00edn", +"label.min_balance": "Saldo m\u00ednimo", "label.minimumsemanticversion": "Vers\u00e3o sem\u00e2ntica m\u00ednima", "label.minmembers": "M\u00edn membros", "label.minorsequence": "Sequ\u00eancia Menor", @@ -1836,27 +1841,43 @@ "label.quota": "Cota", "label.quota.add.credits": "Adicionar cr\u00e9ditos", "label.quota.configuration": "Configura\u00e7\u00e3o da cota", -"label.quota.credits": "Cr\u00e9ditos", -"label.quota.email.edit": "Editar template de email", +"label.quota.consumed": "Cota consumida", +"label.quota.current.balance": "Saldo atual", +"label.quota.email.edit": "Editar template de e-mail", +"label.quota.filter.period": "Filtrando dados entre {startDate} e {endDate}", +"label.quota.last.balance": "Hor\u00e1rio do \u00faltimo balan\u00e7o do dia", +"label.quota.period.today": "Hoje", +"label.quota.period.this.week": "Essa semana", +"label.quota.period.this.month": "Esse m\u00eas", +"label.quota.period.last.month": "M\u00eas passado", +"label.quota.period.this.year": "Esse ano", +"label.quota.period.last.year": "Ano passado", +"label.quota.period.custom": "Personalizado", "label.quota.enforce": "Impor cota", +"label.quota.select.period": "Selecione um per\u00edodo", "label.quota.statement": "Demonstrativo", -"label.quota.statement.balance": "Saldo", -"label.quota.statement.quota": "Utiliza\u00e7\u00e3o", +"label.quota.statement.history": "Hist\u00f3rico (n\u00e3o-cumulativo)", +"label.quota.statement.cumulative.history": "Hist\u00f3rico (cumulativo)", "label.quota.statement.tariff": "Tarifa", "label.quota.summary": "Relat\u00f3rios", -"label.quotastate": "Estado da cota", +"label.quotastate": "Estado do Quota", "label.quota_enforce": "Impor Cota", "label.summary": "Sum\u00e1rio", "label.quota.tariff": "Tarifa", "label.quota.tariff.activationrule": "Regra de ativa\u00e7\u00e3o", "label.quota.tariff.effectivedate": "Data efetiva", +"label.quota.tariff.hasactivationrule": "Possui regra de ativa\u00e7\u00e3o?", "label.quota.tariff.position": "Posi\u00e7\u00e3o", "label.quota.tariff.value": "Valor", "label.quota.total": "Total", +"label.quota.total.consumption": "Consumo total", "label.quota.type.name": "Tipo de uso", "label.quota.type.unit": "Unidade do uso", "label.action.update.object.storage" : "Atualizar Object Storage", "label.quota.usage": "Consumo da cota", +"label.quota.usage.types.summary": "Sum\u00e1rio dos tipos", +"label.quota.usage.resources.by.type": "Recursos por tipo", +"label.quota.usage.details.by.resource": "Detalhes por recurso", "label.quota.validate.activation.rule": "Validar regra de ativa\u00e7\u00e3o", "label.quota.value": "Valor", "label.rados.monitor": "Monitor RADOS", @@ -2298,14 +2319,12 @@ "label.tagged": "Etiquetado", "label.tags": "Etiquetas", "label.target.iqn": "IQN alvo", -"label.tariffactions": "A\u00e7\u00f5es", -"label.tariffvalue": "Valor da tarifa", "label.tcp": "TCP", "label.tcp.proxy": "TCP proxy", "label.template": "Template", "label.template.select.existing": "Selecione um template existente", "label.template.temporary.import": "Utilize um template tempor\u00e1rio para importar", -"label.templatebody": "Corpo do email", +"label.templatebody": "Corpo do e-mail", "label.templatefileupload": "Arquivo local", "label.templateid": "Selecione um template", "label.templateiso": "Template/ISO", @@ -2750,6 +2769,10 @@ "message.action.primary.storage.scope.cluster": "Por favor, confirme que voc\u00ea deseja alterar o escopo de zona para o cluster especificado.
Esta opera\u00e7\u00e3o atualizar\u00e1 o banco de dados e desconectar\u00e1 o pool de armazenamento de todos os hosts que estavam conectados anteriormente ao armazenamento prim\u00e1rio e n\u00e3o fazem parte do cluster especificado.", "message.action.primary.storage.scope.zone": "Por favor, confirme que voc\u00ea deseja alterar o escopo de cluster para zona.
Esta opera\u00e7\u00e3o atualizar\u00e1 o banco de dados e conectar\u00e1 o pool de armazenamento a todos os hosts da zona executando o mesmo hypervisor definido no pool de armazenamento.", "message.action.primarystorage.enable.maintenance.mode": "Aviso: colocar o armazenamento prim\u00e1rio em modo de manuten\u00e7\u00e3o ir\u00e1 causar a parada de todas as VMs hospedadas nesta unidade. Deseja continuar?", +"message.action.quota.credit.add.error.accountrequired": "Por favor, informe a conta", +"message.action.quota.credit.add.error.domainidrequired": "Por favor, informe o dom\u00ednio", +"message.action.quota.credit.add.error.valuerequired": "Por favor, informe a quantidade de cr\u00e9ditos", +"message.action.quota.credit.add.success": "{credit} cr\u00e9ditos adicionados para a conta \"{account}\"", "message.action.quota.tariff.create.error.namerequired": "Por favor, informe o nome da tarifa.", "message.action.quota.tariff.create.error.usagetyperequired": "Por favor, selecione o tipo da tarifa.", "message.action.quota.tariff.create.error.valuerequired": "Por favor, informe o valor da tarifa.", @@ -3408,6 +3431,7 @@ "message.public.traffic.in.basic.zone": "O tr\u00e1fego p\u00fablico \u00e9 gerado quando as VMs na nuvem acessam a internet ou prestam servi\u00e7os aos clientes atrav\u00e9s da internet. Os IPs acess\u00edveis ao p\u00fablico devem ser alocados para essa finalidade. Quando uma inst\u00e2ncia \u00e9 criada, um IP a partir deste conjunto de IPs P\u00fablicos ser\u00e3o destinados \u00e0 inst\u00e2ncia, al\u00e9m do endere\u00e7o IP guest. Um NAT est\u00e1tico 1-1 ser\u00e1 criada automaticamente entre o IP p\u00fablico e IP guest. Os usu\u00e1rios finais tamb\u00e9m podem usar a interface de usu\u00e1rio CloudStack para adquirir IPs adicionais afim de se implementar NAT est\u00e1tico entre suas inst\u00e2ncias e o IP p\u00fablico.", "message.quota.tariff.create.success": "Tarifa \"{quotaTariff}\" criada com sucesso", "message.quota.tariff.update.success": "Tarifa \"{quotaTariff}\" atualizada com sucesso", +"message.quota.usage.resource.warn": "Recursos que s\u00e3o marcados como n\u00e3o possuem metadados constantes (se removido, o dado \u00e9 completamente deletado) e n\u00e3o podem ser retornados.", "message.read.accept.license.agreements": "Leia e aceite os termos dos contratos de licen\u00e7a.", "message.read.admin.guide.scaling.up": "Por favor leia a sess\u00e3o sobre escalonamento din\u00e2mico no guia do administrador antes de escalonar.", "message.recover.vm": "Por favor, confirme a recupera\u00e7\u00e3o desta VM.", @@ -3442,6 +3466,7 @@ "message.remove.sticky.policy.processing": "Removendo sticky policy", "message.remove.vpc": "Favor confirmar que voc\u00ea deseja remover a VPC", "message.request.failed": "Falha na solicita\u00e7\u00e3o", +"message.request.no.data": "N\u00e3o h\u00e1 dados para mostrar.", "message.required.add.least.ip": "Por favor, adicionar pelo menos UM intervalo IP", "message.required.tagged.physical.network": "S\u00f3 pode haver uma rede f\u00edsica n\u00e3o marcada com tipo de tr\u00e1fego convidado.", "message.required.traffic.type": "Erro na configura\u00e7\u00e3o! Todos os tipos de tr\u00e1fego necess\u00e1rios devem ser adicionados e com m\u00faltiplas redes f\u00edsicas cada rede deve ter uma etiqueta.", @@ -3773,6 +3798,8 @@ "migrate.from": "Migrar de", "migrate.to": "Migrar para", "migrationPolicy": "Pol\u00edtica de migra\u00e7\u00e3o", +"placeholder.quota.credit.add.min_balance": "Saldo m\u00ednimo", +"placeholder.quota.credit.add.value": "Quantidade de cr\u00e9ditos", "placeholder.quota.tariff.activationrule": "Regra de ativa\u00e7\u00e3o", "placeholder.quota.tariff.description": "Descri\u00e7\u00e3o", "placeholder.quota.tariff.enddate": "Data de t\u00e9rmino", diff --git a/ui/src/api/index.js b/ui/src/api/index.js index 5ec73a0b20e1..7959947a053a 100644 --- a/ui/src/api/index.js +++ b/ui/src/api/index.js @@ -33,6 +33,7 @@ const additionalGetAPICommandsList = [ 'quotatarifflist', 'quotaisenabled', 'quotastatement', + 'quotaemailtemplatelist', 'verifyoauthcodeandgetuser' ] diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 8414d8934262..ff3810c13a86 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -100,6 +100,9 @@
{{ $toLocaleDate(dataResource[item]) }}
+ + {{ dataResource[item] }} +
{{ dataResource[item] }}
{{ decodeUserData(dataResource.userdata)}}
@@ -247,6 +250,8 @@ import ObjectListTable from '@/components/view/ObjectListTable' import ExternalConfigurationDetails from '@/views/extension/ExternalConfigurationDetails' import TooltipButton from '@/components/widgets/TooltipButton' import { genericCompare } from '@/utils/sort' +import CodeHighlight from 'vue-code-highlight/src/CodeHighlight.vue' +import 'vue-code-highlight/themes/prism-okaidia.css' export default { name: 'DetailsTab', @@ -256,7 +261,8 @@ export default { VmwareData, ObjectListTable, ExternalConfigurationDetails, - TooltipButton + TooltipButton, + CodeHighlight }, props: { resource: { diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index ba7ff0dc27bb..45ba348e4edf 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -548,7 +548,7 @@ {{ resource.project || resource.projectname || resource.projectid }} - {{ resource.projectname }} + {{ resource.projectname || resource.projectid }}
@@ -887,6 +887,18 @@ {{ resource.domain || resource.domainid }}
+
+
{{ $t('label.currency') }}
+
+ {{ resource.currency }} +
+
+
+
{{ $t('label.quota.current.balance') }}
+
+ {{ resource.balance }} +
+
{{ $t('label.payloadurl') }}
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 37abd43e46c7..d57100429b86 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -205,7 +205,7 @@ - - + + + + diff --git a/ui/src/components/view/charts/BarChart.vue b/ui/src/components/view/charts/BarChart.vue new file mode 100644 index 000000000000..a27069163d35 --- /dev/null +++ b/ui/src/components/view/charts/BarChart.vue @@ -0,0 +1,56 @@ +// 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. + + + + diff --git a/ui/src/components/view/stats/ResourceStatsLineChart.vue b/ui/src/components/view/stats/ResourceStatsLineChart.vue index 399e77bebd46..f1afbefd155d 100644 --- a/ui/src/components/view/stats/ResourceStatsLineChart.vue +++ b/ui/src/components/view/stats/ResourceStatsLineChart.vue @@ -217,7 +217,7 @@ export default { data: element.data.map(d => d.stat), hidden: this.hideLine(element.data.map(d => d.stat)), pointRadius: element.pointRadius, - fill: 'origin' + fill: element.fill || 'origin' } ) } diff --git a/ui/src/config/section/plugin/quota.js b/ui/src/config/section/plugin/quota.js index 630e42e4c042..162fb74f7e75 100644 --- a/ui/src/config/section/plugin/quota.js +++ b/ui/src/config/section/plugin/quota.js @@ -30,29 +30,38 @@ export default { title: 'label.quota.summary', icon: 'bars-outlined', permission: ['quotaSummary'], - columns: ['account', + customParamHandler: (params, query) => { return { ...params, ignoreproject: true } }, + tabs: [ + { + name: 'consumption', + component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaUsageTab.vue'))) + }, { - state: (record) => record.state.toLowerCase() + name: 'balance', + component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaBalanceTab.vue'))) }, { - quotastate: (record) => record.quotaenabled ? 'Enabled' : 'Disabled' - }, 'domain', 'currency', 'balance' + name: 'credits', + component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaCreditTab.vue'))) + } ], - columnNames: ['account', 'accountstate', 'quotastate', 'domain', 'currency', 'currentbalance'], - details: ['account', 'domain', 'state', 'currency', 'balance', 'quota', 'startdate', 'enddate'], - component: shallowRef(() => import('@/views/plugins/quota/QuotaSummary.vue')), - tabs: [ + columns: [ + 'account', { - name: 'details', - component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + field: 'state', + customTitle: 'accountState', + state: (record) => record.accountremoved || (record.projectid && record.projectremoved) ? 'disabled' : 'enabled' }, { - name: 'quota.statement.quota', - component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaUsage.vue'))) + field: 'quotastate', + customTitle: 'quotaState', + quotastate: (record) => !record.quotaenabled || record.accountremoved || (record.projectid && record.projectremoved) ? 'disabled' : 'enabled' }, + 'domain', + 'currency', { - name: 'quota.statement.balance', - component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/QuotaBalance.vue'))) + field: 'balance', + customTitle: 'quota.current.balance' } ], actions: [ @@ -61,16 +70,9 @@ export default { icon: 'plus-outlined', docHelp: 'plugins/quota.html#quota-credits', label: 'label.quota.add.credits', - dataView: true, - args: ['value', 'min_balance', 'quota_enforce'], - mapping: { - account: { - value: (record) => { return record.account } - }, - domainid: { - value: (record) => { return record.domainid } - } - } + listView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/plugins/quota/AddQuotaCredit.vue'))) } ] }, @@ -83,7 +85,7 @@ export default { customParamHandler: (params, query) => { params.listall = false - if (['all', 'removed'].includes(query.filter) || params.id) { + if (['all', 'removed'].includes(query.filter) || params.uuid) { params.listall = true } @@ -109,6 +111,11 @@ export default { field: 'tariffValue', customTitle: 'quota.tariff.value' }, + { + field: 'hasActivationRule', + customTitle: 'quota.tariff.hasactivationrule', + hasActivationRule: (record) => record.activationRule ? i18n.global.t('label.yes') : i18n.global.t('label.no') + }, { field: 'executionPosition', customTitle: 'quota.tariff.position', @@ -145,7 +152,11 @@ export default { field: 'endDate', customTitle: 'end.date' }, - 'removed' + 'removed', + { + field: 'activationRule', + customTitle: 'quota.tariff.activationrule' + } ], filters: ['all', 'active', 'removed'], searchFilters: ['usagetype'], @@ -173,13 +184,16 @@ export default { label: 'label.action.quota.tariff.remove', message: 'message.action.quota.tariff.remove', dataView: true, - show: (record) => !record.removed + show: (record) => !record.removed, + groupAction: true, + popup: true, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) } } ] }, { name: 'quotaemailtemplate', - title: 'label.templatetype', + title: 'label.emailtemplate', icon: 'mail-outlined', permission: ['quotaEmailTemplateList'], columns: ['templatetype', 'templatesubject', 'templatebody'], diff --git a/ui/src/style/common/common.scss b/ui/src/style/common/common.scss new file mode 100644 index 000000000000..39dcb46457e7 --- /dev/null +++ b/ui/src/style/common/common.scss @@ -0,0 +1,37 @@ +// 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. + +.w-100 { + width: 100%; +} + +.mt-10 { + margin-top: 10px; +} + +.mb-10 { + margin-bottom: 10px; +} + +.m-20-0 { + margin: 20px 0; +} + +.dotted-underline { + text-decoration: underline dotted; + cursor: default; +} diff --git a/ui/src/utils/chart.js b/ui/src/utils/chart.js new file mode 100644 index 000000000000..a4e78d896163 --- /dev/null +++ b/ui/src/utils/chart.js @@ -0,0 +1,70 @@ +// 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. + +import { TIME_UNITS } from './units' + +export const defaultDisplayFormats = { + day: 'DD MMM YYYY', + week: 'DD MMM YYYY', + month: 'MMM YYYY', + quarter: 'MMM YYYY', + year: 'YYYY' +} + +export const getUnitToTimeCartesianAxis = (baseUnit, dataLength) => { + const maxLabels = 15 + if (dataLength <= maxLabels) { + return baseUnit + } + + const units = [ + 'millisecond', + 'second', + 'minute', + 'hour', + 'day', + 'week', + 'month', + 'quarter', + 'year' + ] + + let index = units.indexOf(baseUnit) + + let unitToReturn = baseUnit + if (index >= 0 && index < units.length) { + let unitTime = 0 + for (index; index < units.length; index++) { + unitTime = TIME_UNITS[units[index]] + const nextUnitTime = TIME_UNITS[units[index + 1]] + + if ((dataLength / (nextUnitTime / unitTime)) <= maxLabels) { + return units[index + 1] + } + + unitToReturn = units[index] + } + } + + return unitToReturn +} + +export const getChartColorObject = (hexColor = '#1890FF') => ({ + backgroundColor: hexColor.concat('80'), + borderColor: hexColor, + borderWidth: 1.5 +}) diff --git a/ui/src/utils/date.js b/ui/src/utils/date.js index 216dfde1303b..bd2ea64e32fa 100644 --- a/ui/src/utils/date.js +++ b/ui/src/utils/date.js @@ -65,16 +65,21 @@ export function parseDateToDatePicker (value) { } export function toLocalDate ({ date, timezoneoffset = store.getters.timezoneoffset, usebrowsertimezone = store.getters.usebrowsertimezone }) { - if (usebrowsertimezone) { - // Since GMT+530 is returned as -330 (minutes to GMT) - timezoneoffset = new Date().getTimezoneOffset() / -60 - } + timezoneoffset = getTimezoneOffset({ timezoneoffset, usebrowsertimezone }) const milliseconds = Date.parse(date) // e.g. "Tue, 08 Jun 2010 19:13:49 GMT"; "Tue, 25 May 2010 12:07:01 UTC" return new Date(milliseconds + (timezoneoffset * 60 * 60 * 1000)) } +export function getTimezoneOffset ({ timezoneoffset = store.getters.timezoneoffset, usebrowsertimezone = store.getters.usebrowsertimezone }) { + if (!usebrowsertimezone) { + return timezoneoffset + } + // Since GMT+530 is returned as -330 (mins to GMT) + return new Date().getTimezoneOffset() / -60 +} + export function toLocaleDate ({ date, timezoneoffset = store.getters.timezoneoffset, usebrowsertimezone = store.getters.usebrowsertimezone, dateOnly = false, hourOnly = false }) { if (!date) { return null diff --git a/ui/src/utils/quota.js b/ui/src/utils/quota.js index b8adbb93518a..c23deb8d13b1 100644 --- a/ui/src/utils/quota.js +++ b/ui/src/utils/quota.js @@ -19,106 +19,135 @@ export const QUOTA_TYPES = [ { id: 1, - type: 'RUNNING_VM' + type: 'RUNNING_VM', + chartColor: '#1890ff' }, { id: 2, - type: 'ALLOCATED_VM' + type: 'ALLOCATED_VM', + chartColor: '#fadb14' }, { id: 3, - type: 'IP_ADDRESS' + type: 'IP_ADDRESS', + chartColor: '#ffd6e7' }, { id: 4, - type: 'NETWORK_BYTES_SENT' + type: 'NETWORK_BYTES_SENT', + chartColor: '#adc6ff' }, { id: 5, - type: 'NETWORK_BYTES_RECEIVED' + type: 'NETWORK_BYTES_RECEIVED', + chartColor: '#10239e' }, { id: 6, - type: 'VOLUME' + type: 'VOLUME', + chartColor: '#722ed1' }, { id: 7, - type: 'TEMPLATE' + type: 'TEMPLATE', + chartColor: '#08979c' }, { id: 8, - type: 'ISO' + type: 'ISO', + chartColor: '#87e8de' }, { id: 9, - type: 'SNAPSHOT' + type: 'SNAPSHOT', + chartColor: '#f5222d' }, { id: 10, - type: 'SECURITY_GROUP' + type: 'SECURITY_GROUP', + chartColor: '#d46b08' }, { id: 11, - type: 'LOAD_BALANCER_POLICY' + type: 'LOAD_BALANCER_POLICY', + chartColor: '#ffd666' }, { id: 12, - type: 'PORT_FORWARDING_RULE' + type: 'PORT_FORWARDING_RULE', + chartColor: '#7cb305' }, { id: 13, - type: 'NETWORK_OFFERING' + type: 'NETWORK_OFFERING', + chartColor: '#ffbb96' }, { id: 14, - type: 'VPN_USERS' + type: 'VPN_USERS', + chartColor: '#95de64' }, { id: 21, - type: 'VM_DISK_IO_READ' + type: 'VM_DISK_IO_READ', + chartColor: '#ffe7ba' }, { id: 22, - type: 'VM_DISK_IO_WRITE' + type: 'VM_DISK_IO_WRITE', + chartColor: '#5b8c00' }, { id: 23, - type: 'VM_DISK_BYTES_READ' + type: 'VM_DISK_BYTES_READ', + chartColor: '#0050b3' }, { id: 24, - type: 'VM_DISK_BYTES_WRITE' + type: 'VM_DISK_BYTES_WRITE', + chartColor: '#520339' }, { id: 25, - type: 'VM_SNAPSHOT' + type: 'VM_SNAPSHOT', + chartColor: '#9e1068' }, { id: 26, - type: 'VOLUME_SECONDARY' + type: 'VOLUME_SECONDARY', + chartColor: '#061178' }, { id: 27, - type: 'VM_SNAPSHOT_ON_PRIMARY' + type: 'VM_SNAPSHOT_ON_PRIMARY', + chartColor: '#ad2102' }, { id: 28, - type: 'BACKUP' + type: 'BACKUP', + chartColor: '#00474f' }, { id: 29, - type: 'BUCKET' + type: 'BUCKET', + chartColor: '#13a8a8' }, { id: 30, - type: 'NETWORK' + type: 'NETWORK', + chartColor: '#c75314' }, { id: 31, - type: 'VPC' + type: 'VPC', + chartColor: '#018391' } ] export const getQuotaTypes = () => { return QUOTA_TYPES.sort((a, b) => a.type.localeCompare(b.type)) } + +export const getQuotaTypeByName = (type) => { + return QUOTA_TYPES.find(quotaType => quotaType.type === type) +} diff --git a/ui/src/utils/units.js b/ui/src/utils/units.js new file mode 100644 index 000000000000..712b4d8426c3 --- /dev/null +++ b/ui/src/utils/units.js @@ -0,0 +1,28 @@ +// 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. + +export const TIME_UNITS = { + millisecond: 1, + second: 1000, + minute: 60 * 1000, + hour: 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000, + month: 30 * 24 * 60 * 60 * 1000, + quarter: 91 * 24 * 60 * 60 * 1000, + year: 365 * 24 * 60 * 60 * 1000 +} diff --git a/ui/src/utils/util.js b/ui/src/utils/util.js index 3c51096ac53e..94fbaa14c0af 100644 --- a/ui/src/utils/util.js +++ b/ui/src/utils/util.js @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +import dayjs from 'dayjs' import semver from 'semver' export function timeFix () { @@ -79,13 +80,13 @@ export function getParsedVersion (version) { return version } -export function toCsv ({ keys = null, data = null, columnDelimiter = ',', lineDelimiter = '\n' }) { - if (data === null || !data.length) { +export function toCsv ({ keys = null, data = null, columnDelimiter = ',', lineDelimiter = '\n', headers = null, dateFormat = undefined }) { + if (data === null || !data.length || keys === null || !keys.filter(key => key !== null && key !== '').length) { return null } let result = '' - result += keys.join(columnDelimiter) + result += (headers || keys).join(columnDelimiter) result += lineDelimiter data.forEach(item => { @@ -93,7 +94,15 @@ export function toCsv ({ keys = null, data = null, columnDelimiter = ',', lineDe if (item[key] === undefined) { item[key] = '' } - result += typeof item[key] === 'string' && item[key].includes(columnDelimiter) ? `"${item[key]}"` : item[key] + + if (typeof item[key] === 'string' && item[key].includes(columnDelimiter)) { + result += `"${item[key]}"` + } else if (dateFormat && dayjs.isDayjs(item[key])) { + result += `"${item[key].format(dateFormat)}"` + } else { + result += item[key] + } + result += columnDelimiter }) result = result.slice(0, -1) @@ -103,6 +112,20 @@ export function toCsv ({ keys = null, data = null, columnDelimiter = ',', lineDe return result } +export function downloadDataAsCsv ({ data = null, keys = null, headers = null, columnDelimiter = ',', lineDelimiter = '\n', fileName = 'data', dateFormat = undefined }) { + const dataParsed = toCsv({ keys, data, columnDelimiter, lineDelimiter, headers, dateFormat }) + if (dataParsed === null) { + return + } + + const hiddenElement = document.createElement('a') + hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(dataParsed) + hiddenElement.target = '_blank' + hiddenElement.download = `${fileName}.csv` + hiddenElement.click() + hiddenElement.remove() +} + export function isValidIPv4Cidr (rule, value) { return new Promise((resolve, reject) => { if (!value) { diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index bbf6c001a265..a22c71bf86fd 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -558,7 +558,7 @@
col.dataIndex === 'hasActivationRule') + if (index >= 0) { + this.columns.splice(index, 1) + } + } + this.loading = true if (this.$route.path.startsWith('/cniconfiguration')) { params.forcks = true @@ -1160,6 +1167,14 @@ export default { if (this.$route.path.startsWith('/tungstenfirewallpolicy/')) { params.firewallpolicyuuid = this.$route.params.id } + if (this.apiName === 'quotaSummary' && params.id) { + params.accountid = params.id + delete params.id + } + if (this.apiName === 'quotaEmailTemplateList' && params.id) { + params.templatetype = params.id + delete params.id + } } if (this.$store.getters.listAllProjects && !this.projectView) { @@ -1206,7 +1221,11 @@ export default { break } - if ('id' in this.$route.params && this.$route.params.id !== params.id) { + const idFromRouteMatchesApiParameter = this.$route.params.id === params.id || + this.apiName === 'quotaSummary' && this.$route.params.id === params.accountid || + this.apiName === 'quotaEmailTemplateList' && this.$route.params.id === params.templatetype + + if ('id' in this.$route.params && !idFromRouteMatchesApiParameter) { console.log('DEBUG - Discarding API response as its `id` does not match the uuid on the browser path') return } diff --git a/ui/src/views/compute/wizard/OwnershipSelection.vue b/ui/src/views/compute/wizard/OwnershipSelection.vue index 484ffdb690fc..decf196f8400 100644 --- a/ui/src/views/compute/wizard/OwnershipSelection.vue +++ b/ui/src/views/compute/wizard/OwnershipSelection.vue @@ -151,6 +151,10 @@ export default { props: { override: { type: Object + }, + accountState: { + type: String, + default: 'Enabled' } }, created () { @@ -199,7 +203,7 @@ export default { response: 'json', domainId: this.selectedDomain, showicon: true, - state: 'Enabled', + state: this.accountState, isrecursive: false }) .then((response) => { diff --git a/ui/src/views/plugins/quota/AddQuotaCredit.vue b/ui/src/views/plugins/quota/AddQuotaCredit.vue new file mode 100644 index 000000000000..51a2d84bb803 --- /dev/null +++ b/ui/src/views/plugins/quota/AddQuotaCredit.vue @@ -0,0 +1,163 @@ +// 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. + + + + + + diff --git a/ui/src/views/plugins/quota/CreateQuotaTariff.vue b/ui/src/views/plugins/quota/CreateQuotaTariff.vue index 18086aca2af0..3262c61b7351 100644 --- a/ui/src/views/plugins/quota/CreateQuotaTariff.vue +++ b/ui/src/views/plugins/quota/CreateQuotaTariff.vue @@ -62,7 +62,6 @@ @@ -85,7 +84,6 @@ @@ -94,7 +92,6 @@ @@ -61,7 +60,6 @@ @@ -70,7 +68,6 @@ - - - - - - - - - -
- {{ $t('label.cancel') }} - {{ $t('label.ok') }} -
-
-
- - - - - diff --git a/ui/src/views/plugins/quota/EmailTemplateDetails.vue b/ui/src/views/plugins/quota/EmailTemplateDetails.vue index 0644fa55b69d..a048cdd7188d 100644 --- a/ui/src/views/plugins/quota/EmailTemplateDetails.vue +++ b/ui/src/views/plugins/quota/EmailTemplateDetails.vue @@ -63,9 +63,14 @@ import { postAPI } from '@/api' export default { name: 'EmailTemplateDetails', + props: { + resource: { + type: Object, + required: true + } + }, data () { return { - resource: {}, formModel: { templatesubject: null, templatebody: null @@ -74,29 +79,10 @@ export default { } }, created () { - this.fetchData() + this.formModel.templatesubject = this.resource.templatesubject || null + this.formModel.templatebody = this.resource.templatebody || null }, methods: { - fetchData () { - this.loading = true - const params = {} - params.templatetype = this.$route.params.id - - postAPI('quotaEmailTemplateList', params).then(json => { - const listTemplates = json.quotaemailtemplatelistresponse.quotaemailtemplate || [] - this.resource = listTemplates && listTemplates.length > 0 ? listTemplates[0] : {} - this.preFillDataValues() - }).catch(e => { - this.$notifyError(e) - }).finally(() => { - this.loading = false - }) - }, - preFillDataValues () { - console.log(this.resource) - this.formModel.templatesubject = this.resource.templatesubject || null - this.formModel.templatebody = this.resource.templatebody || null - }, handleSubmit () { if (this.loading) return const params = {} diff --git a/ui/src/views/plugins/quota/FilterQuotaDataByPeriodView.vue b/ui/src/views/plugins/quota/FilterQuotaDataByPeriodView.vue new file mode 100644 index 000000000000..3e4839b21aa2 --- /dev/null +++ b/ui/src/views/plugins/quota/FilterQuotaDataByPeriodView.vue @@ -0,0 +1,161 @@ +// 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. + + + + + + diff --git a/ui/src/views/plugins/quota/QuotaBalance.vue b/ui/src/views/plugins/quota/QuotaBalance.vue deleted file mode 100644 index f1cf640f6aaa..000000000000 --- a/ui/src/views/plugins/quota/QuotaBalance.vue +++ /dev/null @@ -1,173 +0,0 @@ -// 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. - - - - diff --git a/ui/src/views/plugins/quota/QuotaBalanceTab.vue b/ui/src/views/plugins/quota/QuotaBalanceTab.vue new file mode 100644 index 000000000000..46c2e03d8fe9 --- /dev/null +++ b/ui/src/views/plugins/quota/QuotaBalanceTab.vue @@ -0,0 +1,204 @@ +// 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. + + + + + + diff --git a/ui/src/views/plugins/quota/QuotaCreditTab.vue b/ui/src/views/plugins/quota/QuotaCreditTab.vue new file mode 100644 index 000000000000..3ae32a02d526 --- /dev/null +++ b/ui/src/views/plugins/quota/QuotaCreditTab.vue @@ -0,0 +1,230 @@ +// 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. + + + + + + diff --git a/ui/src/views/plugins/quota/QuotaSummary.vue b/ui/src/views/plugins/quota/QuotaSummary.vue deleted file mode 100644 index 5bfa72a8abae..000000000000 --- a/ui/src/views/plugins/quota/QuotaSummary.vue +++ /dev/null @@ -1,65 +0,0 @@ -// 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. - - - - diff --git a/ui/src/views/plugins/quota/QuotaSummaryResource.vue b/ui/src/views/plugins/quota/QuotaSummaryResource.vue deleted file mode 100644 index c6acf30676fc..000000000000 --- a/ui/src/views/plugins/quota/QuotaSummaryResource.vue +++ /dev/null @@ -1,98 +0,0 @@ -// 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. - - - - - - diff --git a/ui/src/views/plugins/quota/QuotaUsage.vue b/ui/src/views/plugins/quota/QuotaUsage.vue deleted file mode 100644 index 6c55a728bd1c..000000000000 --- a/ui/src/views/plugins/quota/QuotaUsage.vue +++ /dev/null @@ -1,158 +0,0 @@ -// 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. - - - - diff --git a/ui/src/views/plugins/quota/QuotaUsageTab.vue b/ui/src/views/plugins/quota/QuotaUsageTab.vue new file mode 100644 index 000000000000..cefc169c8d4a --- /dev/null +++ b/ui/src/views/plugins/quota/QuotaUsageTab.vue @@ -0,0 +1,731 @@ +// 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. + + + + + + From 13822b1ac1f8295c12fb82acd4b74a1b5726196c Mon Sep 17 00:00:00 2001 From: Harikrishna Date: Mon, 13 Jul 2026 12:43:57 +0530 Subject: [PATCH 182/205] Support Firewall for public IPs in VPC (#12706) --- .../com/cloud/network/NetworkRuleApplier.java | 7 +- .../element/FirewallServiceProvider.java | 18 +- .../com/cloud/network/rules/FirewallRule.java | 4 +- .../firewall/CreateEgressFirewallRuleCmd.java | 2 +- .../user/firewall/CreateFirewallRuleCmd.java | 33 +- .../firewall/CreatePortForwardingRuleCmd.java | 4 +- .../user/nat/CreateIpForwardingRuleCmd.java | 9 +- .../api/response/FirewallResponse.java | 8 + .../firewall/CreateFirewallRuleCmdTest.java | 29 ++ .../api/routing/NetworkElementCommand.java | 1 + .../api/routing/SetFirewallRulesCommand.java | 10 + .../network/rules/StaticNatRuleImpl.java | 7 +- .../orchestration/NetworkOrchestrator.java | 1 + .../cloud/network/dao/FirewallRulesDao.java | 2 + .../network/dao/FirewallRulesDaoImpl.java | 17 + .../cloud/network/rules/FirewallRuleVO.java | 17 +- .../META-INF/db/schema-42210to42300.sql | 3 + .../cluster/KubernetesClusterManagerImpl.java | 3 +- .../KubernetesClusterManagerImplTest.java | 2 +- .../resource/PaloAltoResourceTest.java | 4 +- .../java/com/cloud/api/ApiResponseHelper.java | 34 +- .../ConfigurationManagerImpl.java | 4 +- .../cloud/network/IpAddressManagerImpl.java | 56 ++- .../com/cloud/network/NetworkModelImpl.java | 41 +- .../network/element/VirtualRouterElement.java | 7 +- .../element/VpcVirtualRouterElement.java | 48 ++- .../network/firewall/FirewallManagerImpl.java | 309 ++++++++++++-- .../network/router/CommandSetupHelper.java | 40 +- .../VirtualNetworkApplianceManagerImpl.java | 6 +- ...VpcVirtualNetworkApplianceManagerImpl.java | 25 ++ .../cloud/network/rules/FirewallRules.java | 11 + .../com/cloud/network/rules/RuleApplier.java | 12 + .../com/cloud/network/vpc/VpcManagerImpl.java | 2 +- .../network/RoutedIpv4ManagerImpl.java | 8 +- .../topology/BasicNetworkTopology.java | 117 ++++++ .../network/topology/BasicNetworkVisitor.java | 4 +- .../network/topology/NetworkTopology.java | 6 + .../cloud/network/IpAddressManagerTest.java | 148 +++++++ .../element/VpcVirtualRouterElementTest.java | 26 ++ .../network/firewall/FirewallManagerTest.java | 393 +++++++++++++++++- systemvm/debian/opt/cloud/bin/configure.py | 127 +++++- systemvm/debian/opt/cloud/bin/cs/CsAddress.py | 1 + systemvm/debian/opt/cloud/bin/cs/CsConfig.py | 3 + .../smoke/test_vpc_firewall_rules.py | 187 +++++++++ ui/src/views/network/PublicIpResource.vue | 43 +- ui/src/views/offering/AddNetworkOffering.vue | 3 + ui/src/views/offering/AddVpcOffering.vue | 7 +- ui/src/views/offering/CloneVpcOffering.vue | 1 + 48 files changed, 1744 insertions(+), 106 deletions(-) create mode 100644 test/integration/smoke/test_vpc_firewall_rules.py diff --git a/api/src/main/java/com/cloud/network/NetworkRuleApplier.java b/api/src/main/java/com/cloud/network/NetworkRuleApplier.java index b9942e71eb26..69b712bc6ca2 100644 --- a/api/src/main/java/com/cloud/network/NetworkRuleApplier.java +++ b/api/src/main/java/com/cloud/network/NetworkRuleApplier.java @@ -21,8 +21,13 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.rules.FirewallRule; +import com.cloud.network.vpc.Vpc; public interface NetworkRuleApplier { - public boolean applyRules(Network network, FirewallRule.Purpose purpose, List rules) throws ResourceUnavailableException; + default boolean applyRules(Network network, FirewallRule.Purpose purpose, List rules) throws ResourceUnavailableException { + return applyRules(network, null, purpose, rules); + } + + boolean applyRules(Network network, Vpc vpc, FirewallRule.Purpose purpose, List rules) throws ResourceUnavailableException; } diff --git a/api/src/main/java/com/cloud/network/element/FirewallServiceProvider.java b/api/src/main/java/com/cloud/network/element/FirewallServiceProvider.java index c091142d9353..6b0f932e8c22 100644 --- a/api/src/main/java/com/cloud/network/element/FirewallServiceProvider.java +++ b/api/src/main/java/com/cloud/network/element/FirewallServiceProvider.java @@ -21,14 +21,20 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.Network; import com.cloud.network.rules.FirewallRule; +import com.cloud.network.vpc.Vpc; public interface FirewallServiceProvider extends NetworkElement { /** - * Apply rules - * @param network - * @param rules - * @return - * @throws ResourceUnavailableException + * Apply firewall rules in a network context. */ - boolean applyFWRules(Network network, List rules) throws ResourceUnavailableException; + default boolean applyFWRules(Network network, List rules) throws ResourceUnavailableException { + return false; + } + + /** + * Apply firewall rules in a VPC context. + */ + default boolean applyFWRulesInVPC(Vpc vpc, List rules) throws ResourceUnavailableException { + return false; + } } diff --git a/api/src/main/java/com/cloud/network/rules/FirewallRule.java b/api/src/main/java/com/cloud/network/rules/FirewallRule.java index 369c6aa57eb8..38ba009163ce 100644 --- a/api/src/main/java/com/cloud/network/rules/FirewallRule.java +++ b/api/src/main/java/com/cloud/network/rules/FirewallRule.java @@ -69,7 +69,9 @@ enum TrafficType { State getState(); - long getNetworkId(); + Long getNetworkId(); + + Long getVpcId(); Long getSourceIpAddressId(); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateEgressFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateEgressFirewallRuleCmd.java index 3fd571b7a479..e2c84ac85c7a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateEgressFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateEgressFirewallRuleCmd.java @@ -212,7 +212,7 @@ public State getState() { } @Override - public long getNetworkId() { + public Long getNetworkId() { return networkId; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmd.java index bc65126f33bd..30dd1a2d015a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmd.java @@ -223,13 +223,9 @@ public State getState() { } @Override - public long getNetworkId() { - IpAddress ip = _entityMgr.findById(IpAddress.class, getIpAddressId()); - Long ntwkId = null; - - if (ip.getAssociatedWithNetworkId() != null) { - ntwkId = ip.getAssociatedWithNetworkId(); - } + public Long getNetworkId() { + IpAddress ip = getIp(); + Long ntwkId = isVpcIp(ip) ? getVpcNetworkIdForFirewallRule(ip) : getIsolatedNetworkIdForFirewallRule(ip); if (ntwkId == null) { throw new InvalidParameterValueException("Unable to create firewall rule for the IP address ID=" + ipAddressId + @@ -238,6 +234,12 @@ public long getNetworkId() { return ntwkId; } + @Override + public Long getVpcId() { + IpAddress ip = getIp(); + return isVpcIp(ip) ? ip.getVpcId() : null; + } + @Override public long getEntityOwnerId() { Account account = CallContext.current().getCallingAccount(); @@ -300,7 +302,21 @@ public String getSyncObjType() { @Override public Long getSyncObjId() { - return getIp().getAssociatedWithNetworkId(); + Long syncObjId = getIp().getAssociatedWithNetworkId(); + return syncObjId != null ? syncObjId : getNetworkId(); + } + + private boolean isVpcIp(IpAddress ip) { + return ip.getVpcId() != null; + } + + private Long getIsolatedNetworkIdForFirewallRule(IpAddress ip) { + return ip.getAssociatedWithNetworkId(); + } + + private Long getVpcNetworkIdForFirewallRule(IpAddress ip) { + // VPC flow is independent from tier association; manager resolves execution network. + return ip.getNetworkId(); } private IpAddress getIp() { @@ -311,6 +327,7 @@ private IpAddress getIp() { return ip; } + @Override public Integer getIcmpCode() { if (icmpCode != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java index 2bc5fc2ee68b..66fc118395ea 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java @@ -176,7 +176,7 @@ public Boolean getOpenFirewall() { } } - private Long getVpcId() { + public Long getVpcId() { if (ipAddressId != null) { IpAddress ipAddr = _networkService.getIp(ipAddressId); if (ipAddr == null || !ipAddr.readyToUse()) { @@ -275,7 +275,7 @@ public State getState() { } @Override - public long getNetworkId() { + public Long getNetworkId() { IpAddress ip = _entityMgr.findById(IpAddress.class, getIpAddressId()); Long ntwkId = _networkService.getPreferredNetworkIdForPublicIpRuleAssignment(ip, networkId); if (ntwkId == null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/nat/CreateIpForwardingRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/nat/CreateIpForwardingRuleCmd.java index 7963dfe5c7d3..98487ddeb19a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/nat/CreateIpForwardingRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/nat/CreateIpForwardingRuleCmd.java @@ -229,8 +229,13 @@ public FirewallRule.State getState() { } @Override - public long getNetworkId() { - return -1; + public Long getNetworkId() { + return -1L; + } + + @Override + public Long getVpcId() { + return null; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/response/FirewallResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/FirewallResponse.java index 5986c16dc8c0..f6cc9e5d9499 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/FirewallResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/FirewallResponse.java @@ -51,6 +51,10 @@ public class FirewallResponse extends BaseResponse { @Param(description = "The Network ID of the firewall rule") private String networkId; + @SerializedName(ApiConstants.VPC_ID) + @Param(description = "The VPC ID of the firewall rule") + private String vpcId; + @SerializedName(ApiConstants.IP_ADDRESS) @Param(description = "The public IP address for the firewall rule") private String publicIpAddress; @@ -115,6 +119,10 @@ public void setNetworkId(String networkId) { this.networkId = networkId; } + public void setVpcId(String vpcId) { + this.vpcId = vpcId; + } + public void setState(String state) { this.state = state; } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmdTest.java index c905974b2be9..504d3914b70e 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmdTest.java @@ -21,10 +21,15 @@ import java.util.Collections; import java.util.List; +import com.cloud.network.IpAddress; +import com.cloud.network.NetworkService; +import com.cloud.utils.db.EntityManager; import org.apache.commons.collections.CollectionUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; @@ -33,6 +38,12 @@ @RunWith(MockitoJUnitRunner.class) public class CreateFirewallRuleCmdTest { + @Mock + private EntityManager entityManager; + + @Mock + private NetworkService networkService; + private void validateAllIp4Cidr(final CreateFirewallRuleCmd cmd) { Assert.assertTrue(CollectionUtils.isNotEmpty(cmd.getSourceCidrList())); Assert.assertEquals(1, cmd.getSourceCidrList().size()); @@ -88,4 +99,22 @@ public void testGetSourceCidrList_EmptyFirstElementButMore() { Assert.assertEquals(2, cmd.getSourceCidrList().size()); Assert.assertEquals(cidr, cmd.getSourceCidrList().get(1)); } + + @Test + public void testGetNetworkIdVpcWithoutAssociatedNetworkUsesVpcFallbackAndSyncObjId() { + final CreateFirewallRuleCmd cmd = new CreateFirewallRuleCmd(); + final IpAddress ip = Mockito.mock(IpAddress.class); + + cmd._entityMgr = entityManager; + cmd._networkService = networkService; + ReflectionTestUtils.setField(cmd, "ipAddressId", 42L); + + Mockito.when(networkService.getIp(42L)).thenReturn(ip); + Mockito.when(ip.getAssociatedWithNetworkId()).thenReturn(null); + Mockito.when(ip.getVpcId()).thenReturn(100L); + Mockito.when(ip.getNetworkId()).thenReturn(2L); + + Assert.assertEquals(Long.valueOf(2L), cmd.getNetworkId()); + Assert.assertEquals(Long.valueOf(2L), cmd.getSyncObjId()); + } } diff --git a/core/src/main/java/com/cloud/agent/api/routing/NetworkElementCommand.java b/core/src/main/java/com/cloud/agent/api/routing/NetworkElementCommand.java index 400b6bb80917..20d2ea0a443d 100644 --- a/core/src/main/java/com/cloud/agent/api/routing/NetworkElementCommand.java +++ b/core/src/main/java/com/cloud/agent/api/routing/NetworkElementCommand.java @@ -37,6 +37,7 @@ public abstract class NetworkElementCommand extends Command { public static final String ZONE_NETWORK_TYPE = "zone.network.type"; public static final String GUEST_BRIDGE = "guest.bridge"; public static final String VPC_PRIVATE_GATEWAY = "vpc.gateway.private"; + public static final String VPC_ID = "vpc.id"; public static final String FIREWALL_EGRESS_DEFAULT = "firewall.egress.default"; public static final String NETWORK_PUB_LAST_IP = "network.public.last.ip"; public static final String HYPERVISOR_HOST_PRIVATE_IP = "hypervisor.private.ip"; diff --git a/core/src/main/java/com/cloud/agent/api/routing/SetFirewallRulesCommand.java b/core/src/main/java/com/cloud/agent/api/routing/SetFirewallRulesCommand.java index c56f8d20fbe6..ff81ab7749c5 100644 --- a/core/src/main/java/com/cloud/agent/api/routing/SetFirewallRulesCommand.java +++ b/core/src/main/java/com/cloud/agent/api/routing/SetFirewallRulesCommand.java @@ -32,6 +32,7 @@ */ public class SetFirewallRulesCommand extends NetworkElementCommand { FirewallRuleTO[] rules; + Long vpcId; protected SetFirewallRulesCommand() { } @@ -40,10 +41,19 @@ public SetFirewallRulesCommand(List rules) { this.rules = rules.toArray(new FirewallRuleTO[rules.size()]); } + public SetFirewallRulesCommand(List rules, Long vpcId) { + this.rules = rules.toArray(new FirewallRuleTO[rules.size()]); + this.vpcId = vpcId; + } + public FirewallRuleTO[] getRules() { return rules; } + public Long getVpcId() { + return vpcId; + } + public String[][] generateFwRules() { String[][] result = new String[2][]; Set toAdd = new HashSet(); diff --git a/engine/components-api/src/main/java/com/cloud/network/rules/StaticNatRuleImpl.java b/engine/components-api/src/main/java/com/cloud/network/rules/StaticNatRuleImpl.java index 4d8270ca078d..980606176024 100644 --- a/engine/components-api/src/main/java/com/cloud/network/rules/StaticNatRuleImpl.java +++ b/engine/components-api/src/main/java/com/cloud/network/rules/StaticNatRuleImpl.java @@ -80,10 +80,15 @@ public long getDomainId() { } @Override - public long getNetworkId() { + public Long getNetworkId() { return networkId; } + @Override + public Long getVpcId() { + return null; + } + @Override public long getId() { return id; diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 15f112c0a36a..84a397349cec 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -593,6 +593,7 @@ public boolean configure(final String name, final Map params) th defaultVPCOffProviders.put(Service.StaticNat, defaultProviders); defaultVPCOffProviders.put(Service.PortForwarding, defaultProviders); defaultVPCOffProviders.put(Service.Vpn, defaultProviders); + defaultVPCOffProviders.put(Service.Firewall, defaultProviders); Transaction.execute(new TransactionCallbackNoReturn() { @Override diff --git a/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDao.java b/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDao.java index 7f322ae6c037..3527ce84dcf6 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDao.java @@ -69,6 +69,8 @@ public interface FirewallRulesDao extends GenericDao { List listByNetworkPurposeTrafficType(long networkId, FirewallRule.Purpose purpose, FirewallRule.TrafficType trafficType); + List listByVpcPurposeTrafficType(long vpcId, FirewallRule.Purpose purpose, FirewallRule.TrafficType trafficType); + List listByIpAndPurposeWithState(Long addressId, FirewallRule.Purpose purpose, FirewallRule.State state); void loadSourceCidrs(FirewallRuleVO rule); diff --git a/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDaoImpl.java index 57d53f92572c..5a1e1aae6b66 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDaoImpl.java @@ -74,6 +74,7 @@ protected FirewallRulesDaoImpl() { AllFieldsSearch.and("domain", AllFieldsSearch.entity().getDomainId(), Op.EQ); AllFieldsSearch.and("id", AllFieldsSearch.entity().getId(), Op.EQ); AllFieldsSearch.and("networkId", AllFieldsSearch.entity().getNetworkId(), Op.EQ); + AllFieldsSearch.and("vpcId", AllFieldsSearch.entity().getVpcId(), Op.EQ); AllFieldsSearch.and("related", AllFieldsSearch.entity().getRelated(), Op.EQ); AllFieldsSearch.and("trafficType", AllFieldsSearch.entity().getTrafficType(), Op.EQ); AllFieldsSearch.done(); @@ -356,6 +357,22 @@ public List listByNetworkPurposeTrafficType(long networkId, Purp return listBy(sc); } + @Override + public List listByVpcPurposeTrafficType(long vpcId, Purpose purpose, TrafficType trafficType) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("vpcId", vpcId); + + if (purpose != null) { + sc.setParameters("purpose", purpose); + } + + if (trafficType != null) { + sc.setParameters("trafficType", trafficType); + } + + return listBy(sc); + } + @Override @DB public boolean remove(Long id) { diff --git a/engine/schema/src/main/java/com/cloud/network/rules/FirewallRuleVO.java b/engine/schema/src/main/java/com/cloud/network/rules/FirewallRuleVO.java index 6ce9e6a118b7..2b34e23fa4d4 100644 --- a/engine/schema/src/main/java/com/cloud/network/rules/FirewallRuleVO.java +++ b/engine/schema/src/main/java/com/cloud/network/rules/FirewallRuleVO.java @@ -91,6 +91,9 @@ public class FirewallRuleVO implements FirewallRule { @Column(name = "network_id") Long networkId; + @Column(name = "vpc_id") + Long vpcId; + @Column(name = "icmp_code") Integer icmpCode; @@ -196,10 +199,18 @@ public State getState() { } @Override - public long getNetworkId() { + public Long getNetworkId() { return networkId; } + public Long getVpcId() { + return vpcId; + } + + public void setVpcId(Long vpcId) { + this.vpcId = vpcId; + } + @Override public FirewallRuleType getType() { return type; @@ -217,7 +228,7 @@ protected FirewallRuleVO() { uuid = UUID.randomUUID().toString(); } - public FirewallRuleVO(String xId, Long ipAddressId, Integer portStart, Integer portEnd, String protocol, long networkId, long accountId, long domainId, + public FirewallRuleVO(String xId, Long ipAddressId, Integer portStart, Integer portEnd, String protocol, Long networkId, long accountId, long domainId, Purpose purpose, List sourceCidrs, Integer icmpCode, Integer icmpType, Long related, TrafficType trafficType) { this.xId = xId; if (xId == null) { @@ -261,7 +272,7 @@ public FirewallRuleVO(String xId, long ipAddressId, int port, String protocol, l } - public FirewallRuleVO(String xId, Long ipAddressId, Integer portStart, Integer portEnd, String protocol, long networkId, long accountId, long domainId, + public FirewallRuleVO(String xId, Long ipAddressId, Integer portStart, Integer portEnd, String protocol, Long networkId, long accountId, long domainId, Purpose purpose, List sourceCidrs, List destCidrs, Integer icmpCode, Integer icmpType, Long related, TrafficType trafficType) { this(xId,ipAddressId, portStart, portEnd, protocol, networkId, accountId, domainId, purpose, sourceCidrs, icmpCode, icmpType, related, trafficType); this.destinationCidrs = destCidrs; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 3240c431fc83..80293aaab353 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -588,3 +588,6 @@ CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone_network_map` ( CONSTRAINT `fk_dns_map__zone_id` FOREIGN KEY (`dns_zone_id`) REFERENCES `dns_zone` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_dns_map__network_id` FOREIGN KEY (`network_id`) REFERENCES `networks` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- This is part of allowing firewall rules on public IP addresses in VPC network +ALTER TABLE `cloud`.`firewall_rules` MODIFY COLUMN `network_id` BIGINT UNSIGNED NULL; diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java index fea20eb124fc..aa5ddf0cd006 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java @@ -2986,9 +2986,8 @@ private void createNetworkOfferingForKubernetes(String offeringName, String offe defaultKubernetesServiceNetworkOfferingProviders.put(Service.UserData, provider); if (forVpc) { defaultKubernetesServiceNetworkOfferingProviders.put(Service.NetworkACL, forNsx ? Network.Provider.Nsx : provider); - } else { - defaultKubernetesServiceNetworkOfferingProviders.put(Service.Firewall, forNsx ? Network.Provider.Nsx : provider); } + defaultKubernetesServiceNetworkOfferingProviders.put(Service.Firewall, forNsx ? Network.Provider.Nsx : provider); defaultKubernetesServiceNetworkOfferingProviders.put(Service.Lb, forNsx ? Network.Provider.Nsx : provider); defaultKubernetesServiceNetworkOfferingProviders.put(Service.SourceNat, forNsx ? Network.Provider.Nsx : provider); defaultKubernetesServiceNetworkOfferingProviders.put(Service.StaticNat, forNsx ? Network.Provider.Nsx : provider); diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java index 71949459c865..1fab5420c3c3 100644 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java @@ -155,7 +155,7 @@ public void validateIsolatedNetworkIpRulesNoRules() { } private FirewallRuleVO createRule(int startPort, int endPort) { - FirewallRuleVO rule = new FirewallRuleVO(null, null, startPort, endPort, "tcp", 1, 1, 1, FirewallRule.Purpose.Firewall, List.of("0.0.0.0/0"), null, null, null, FirewallRule.TrafficType.Ingress); + FirewallRuleVO rule = new FirewallRuleVO(null, null, startPort, endPort, "tcp", 1L, 1, 1, FirewallRule.Purpose.Firewall, List.of("0.0.0.0/0"), null, null, null, FirewallRule.TrafficType.Ingress); return rule; } diff --git a/plugins/network-elements/palo-alto/src/test/java/com/cloud/network/resource/PaloAltoResourceTest.java b/plugins/network-elements/palo-alto/src/test/java/com/cloud/network/resource/PaloAltoResourceTest.java index 58f962f13727..ae8520e005a7 100644 --- a/plugins/network-elements/palo-alto/src/test/java/com/cloud/network/resource/PaloAltoResourceTest.java +++ b/plugins/network-elements/palo-alto/src/test/java/com/cloud/network/resource/PaloAltoResourceTest.java @@ -290,7 +290,7 @@ public void addEgressFirewallRule() throws ConfigurationException, Exception { List rules = new ArrayList(); List cidrList = new ArrayList(); cidrList.add("0.0.0.0/0"); - FirewallRuleVO activeVO = new FirewallRuleVO(null, null, 80, 80, "tcp", 1, 1, 1, Purpose.Firewall, cidrList, null, null, null, FirewallRule.TrafficType.Egress); + FirewallRuleVO activeVO = new FirewallRuleVO(null, null, 80, 80, "tcp", 1L, 1, 1, Purpose.Firewall, cidrList, null, null, null, FirewallRule.TrafficType.Egress); FirewallRuleTO active = new FirewallRuleTO(activeVO, Long.toString(vlanId), null, Purpose.Firewall, FirewallRule.TrafficType.Egress); rules.add(active); @@ -319,7 +319,7 @@ public void removeEgressFirewallRule() throws ConfigurationException, Exception long vlanId = 3954; List rules = new ArrayList(); - FirewallRuleVO revokedVO = new FirewallRuleVO(null, null, 80, 80, "tcp", 1, 1, 1, Purpose.Firewall, null, null, null, null, FirewallRule.TrafficType.Egress); + FirewallRuleVO revokedVO = new FirewallRuleVO(null, null, 80, 80, "tcp", 1L, 1, 1, Purpose.Firewall, null, null, null, null, FirewallRule.TrafficType.Egress); revokedVO.setState(State.Revoke); FirewallRuleTO revoked = new FirewallRuleTO(revokedVO, Long.toString(vlanId), null, Purpose.Firewall, FirewallRule.TrafficType.Egress); rules.add(revoked); diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 49b0e342efc6..ab5f572021cd 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -2968,8 +2968,21 @@ public FirewallResponse createFirewallResponse(FirewallRule fwRule) { } } - Network network = ApiDBUtils.findNetworkById(fwRule.getNetworkId()); - response.setNetworkId(network.getUuid()); + Long networkId = fwRule.getNetworkId(); + if (networkId != null) { + Network network = ApiDBUtils.findNetworkById(networkId); + if (network != null) { + response.setNetworkId(network.getUuid()); + } + } + + Long vpcId = fwRule.getVpcId(); + if (vpcId != null) { + Vpc vpc = ApiDBUtils.findVpcById(vpcId); + if (vpc != null) { + response.setVpcId(vpc.getUuid()); + } + } FirewallRule.State state = fwRule.getState(); String stateToSet = state.toString(); @@ -5420,8 +5433,21 @@ public FirewallResponse createIpv6FirewallRuleResponse(FirewallRule fwRule) { response.setIcmpCode(fwRule.getIcmpCode()); response.setIcmpType(fwRule.getIcmpType()); - Network network = ApiDBUtils.findNetworkById(fwRule.getNetworkId()); - response.setNetworkId(network.getUuid()); + Long networkId = fwRule.getNetworkId(); + if (networkId != null) { + Network network = ApiDBUtils.findNetworkById(networkId); + if (network != null) { + response.setNetworkId(network.getUuid()); + } + } + + Long vpcId = fwRule.getVpcId(); + if (vpcId != null) { + Vpc vpc = ApiDBUtils.findVpcById(vpcId); + if (vpc != null) { + response.setVpcId(vpc.getUuid()); + } + } FirewallRule.State state = fwRule.getState(); String stateToSet = state.toString(); diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 1384b58075eb..daefdbbc4a59 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -7263,10 +7263,12 @@ public NetworkOffering createNetworkOffering(final NetworkOfferingBaseCmd cmd) { } if (forVpc == null) { - if (service == Service.SecurityGroup || service == Service.Firewall) { + if (service == Service.SecurityGroup) { forVpc = false; } else if (service == Service.NetworkACL) { forVpc = true; + } else if (service == Service.Firewall) { + forVpc = true; } } diff --git a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java index da84e5058603..3cd42c50c28d 100644 --- a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java +++ b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java @@ -659,28 +659,58 @@ public boolean applyRules(List rules, FirewallRule.Purpo } boolean success = true; - Network network = _networksDao.findById(rules.get(0).getNetworkId()); - FirewallRuleVO.TrafficType trafficType = rules.get(0).getTrafficType(); + FirewallRule firstRule = rules.get(0); + Long networkId = firstRule.getNetworkId(); + Long vpcId = firstRule.getVpcId(); + FirewallRuleVO.TrafficType trafficType = firstRule.getTrafficType(); List publicIps = new ArrayList(); - if (!(rules.get(0).getPurpose() == FirewallRule.Purpose.Firewall && trafficType == FirewallRule.TrafficType.Egress)) { - // get the list of public ip's owned by the network - List userIps = _ipAddressDao.listByAssociatedNetwork(network.getId(), null); - if (userIps != null && !userIps.isEmpty()) { - for (IPAddressVO userIp : userIps) { - PublicIp publicIp = PublicIp.createFromAddrAndVlan(userIp, _vlanDao.findById(userIp.getVlanId())); - publicIps.add(publicIp); + // For VPC firewall rules the networkId on the rule is null; resolve via VPC. + Network network = null; + Vpc vpc = null; + if (networkId != null) { + network = _networksDao.findById(networkId); + } else if (vpcId != null) { + vpc = _vpcDao.findById(vpcId); + } + + if (network == null) { + logger.warn("Unable to resolve network for firewall rules (networkId={}, vpcId={}); skipping IP association", networkId, vpcId); + } else if (!(firstRule.getPurpose() == FirewallRule.Purpose.Firewall && trafficType == FirewallRule.TrafficType.Egress)) { + // For VPC ingress rules, collect public IPs tied to the VPC rather than network association + if (vpcId != null && networkId == null) { + List vpcIps = _ipAddressDao.listByAssociatedVpc(vpcId, null); + if (vpcIps != null) { + for (IPAddressVO userIp : vpcIps) { + PublicIp publicIp = PublicIp.createFromAddrAndVlan(userIp, _vlanDao.findById(userIp.getVlanId())); + publicIps.add(publicIp); + } + } + } else { + // get the list of public ip's owned by the network + List userIps = _ipAddressDao.listByAssociatedNetwork(network.getId(), null); + if (userIps != null && !userIps.isEmpty()) { + for (IPAddressVO userIp : userIps) { + PublicIp publicIp = PublicIp.createFromAddrAndVlan(userIp, _vlanDao.findById(userIp.getVlanId())); + publicIps.add(publicIp); + } } } } - // rules can not programmed unless IP is associated with network service provider, so run IP assoication for + + // rules can not programmed unless IP is associated with network service provider, so run IP association for // the network so as to ensure IP is associated before applying rules (in add state) - if (checkIfIpAssocRequired(network, false, publicIps)) { + if (network != null && checkIfIpAssocRequired(network, false, publicIps)) { applyIpAssociations(network, false, continueOnError, publicIps); } try { - applier.applyRules(network, purpose, rules); + if (network != null || vpc != null) { + applier.applyRules(network, vpc, purpose, rules); + } else { + logger.warn("Skipping applyRules: no network or vpc resolved for rules (networkId={}, vpcId={})", networkId, vpcId); + success = false; + } } catch (ResourceUnavailableException e) { if (!continueOnError) { throw e; @@ -691,7 +721,7 @@ public boolean applyRules(List rules, FirewallRule.Purpo // if there are no active rules associated with a public IP, then public IP need not be associated with a provider. // This IPAssoc ensures, public IP is dis-associated after last active rule is revoked. - if (checkIfIpAssocRequired(network, true, publicIps)) { + if (network != null && checkIfIpAssocRequired(network, true, publicIps)) { applyIpAssociations(network, true, continueOnError, publicIps); } diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java index a9ca5004a06c..f47046cdc434 100644 --- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java @@ -107,9 +107,11 @@ import com.cloud.network.rules.dao.PortForwardingRulesDao; import com.cloud.network.vpc.Vpc; import com.cloud.network.vpc.VpcGatewayVO; +import com.cloud.network.vpc.VpcOfferingServiceMapVO; import com.cloud.network.vpc.dao.PrivateIpDao; import com.cloud.network.vpc.dao.VpcDao; import com.cloud.network.vpc.dao.VpcGatewayDao; +import com.cloud.network.vpc.dao.VpcOfferingServiceMapDao; import com.cloud.offering.NetworkOffering; import com.cloud.offering.NetworkOffering.Detail; import com.cloud.offerings.NetworkOfferingServiceMapVO; @@ -186,6 +188,8 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi NetworkPermissionDao _networkPermissionDao; @Inject VpcDao vpcDao; + @Inject + VpcOfferingServiceMapDao _vpcOffSvcMapDao; private List networkElements; @@ -510,12 +514,16 @@ public boolean canIpUsedForService(PublicIp publicIp, Service service, Long netw // We only support one provider for one service now Map> serviceToProviders = getServiceProvidersMap(networkId); // Since IP already has service to bind with, the oldProvider can't be null - Set newProviders = serviceToProviders.get(service); + Set newProviders = getProvidersForServiceWithVpcFallback(serviceToProviders, service, publicIp.getVpcId()); if (newProviders == null || newProviders.isEmpty()) { throw new InvalidParameterValueException("There is no new provider for IP " + publicIp.getAddress() + " of service " + service.getName() + "!"); } Provider newProvider = (Provider)newProviders.toArray()[0]; - Set oldProviders = serviceToProviders.get(services.toArray()[0]); + Service existingService = (Service) services.toArray()[0]; + Set oldProviders = getProvidersForServiceWithVpcFallback(serviceToProviders, existingService, publicIp.getVpcId()); + if (oldProviders == null || oldProviders.isEmpty()) { + throw new InvalidParameterValueException("There is no existing provider for IP " + publicIp.getAddress() + " of service " + existingService.getName() + "!"); + } Provider oldProvider = (Provider)oldProviders.toArray()[0]; Network network = _networksDao.findById(networkId); NetworkElement oldElement = getElementImplementingProvider(oldProvider.getName()); @@ -530,6 +538,35 @@ public boolean canIpUsedForService(PublicIp publicIp, Service service, Long netw return true; } + private Set getProvidersForServiceWithVpcFallback(Map> serviceToProviders, Service service, Long vpcId) { + Set providers = serviceToProviders.get(service); + if (providers != null && !providers.isEmpty()) { + return providers; + } + + if (vpcId == null || service != Service.Firewall) { + return providers; + } + + Set vpcProviders = new HashSet(); + Vpc vpc = vpcDao.findById(vpcId); + if (vpc == null) { + return vpcProviders; + } + + List offeringProviders = _vpcOffSvcMapDao.listProvidersForServiceForVpcOffering(vpc.getVpcOfferingId(), Service.Firewall); + if (offeringProviders != null) { + for (VpcOfferingServiceMapVO offeringProvider : offeringProviders) { + Provider provider = Provider.getProvider(offeringProvider.getProvider()); + if (provider != null) { + vpcProviders.add(provider); + } + } + } + + return vpcProviders; + } + Map> getProviderServicesMap(long networkId) { Map> map = new HashMap>(); List nsms = _ntwkSrvcDao.getServicesInNetwork(networkId); diff --git a/server/src/main/java/com/cloud/network/element/VirtualRouterElement.java b/server/src/main/java/com/cloud/network/element/VirtualRouterElement.java index e569904c9598..c08246c5a15b 100644 --- a/server/src/main/java/com/cloud/network/element/VirtualRouterElement.java +++ b/server/src/main/java/com/cloud/network/element/VirtualRouterElement.java @@ -210,6 +210,10 @@ protected boolean canHandle(final Network network, final Service service) { return true; } + protected boolean canHandle(final Vpc vpc, final Service service) { + return false; + } + @Override public boolean implement(final Network network, final NetworkOffering offering, final DeployDestination dest, final ReservationContext context) throws ResourceUnavailableException, ConcurrentOperationException, InsufficientCapacityException { @@ -279,7 +283,7 @@ public boolean applyFWRules(final Network network, final List routers = getRouters(network); if (routers == null || routers.isEmpty()) { - logger.debug("Virtual router element doesn't need to apply firewall rules on the backend; virtual router doesn't exist in the network {}", network); + logger.debug("Virtual router element doesn't need to apply firewall rules on the backend; virtual router doesn't exist in the network {}"); return true; } @@ -302,6 +306,7 @@ public boolean applyFWRules(final Network network, final List rules = new ArrayList(); diff --git a/server/src/main/java/com/cloud/network/element/VpcVirtualRouterElement.java b/server/src/main/java/com/cloud/network/element/VpcVirtualRouterElement.java index f393ef8a129d..99f8d8f697b2 100644 --- a/server/src/main/java/com/cloud/network/element/VpcVirtualRouterElement.java +++ b/server/src/main/java/com/cloud/network/element/VpcVirtualRouterElement.java @@ -52,6 +52,7 @@ import com.cloud.network.router.VirtualRouter.Role; import com.cloud.network.router.VpcNetworkHelperImpl; import com.cloud.network.router.VpcVirtualNetworkApplianceManager; +import com.cloud.network.rules.FirewallRule; import com.cloud.network.vpc.NetworkACLItem; import com.cloud.network.vpc.NetworkACLItemDao; import com.cloud.network.vpc.NetworkACLItemVO; @@ -148,6 +149,49 @@ protected boolean canHandle(final Network network, final Service service) { return true; } + @Override + protected boolean canHandle(final Vpc vpc, final Service service) { + if (vpc == null) { + return false; + } + + if (!_networkMdl.isProviderEnabledInZone(vpc.getZoneId(), Network.Provider.VPCVirtualRouter.getName())) { + return false; + } + + if (service != null && !_vpcMgr.isProviderSupportServiceInVpc(vpc.getId(), service, getProvider())) { + logger.trace("Element " + getProvider().getName() + " doesn't support service " + service.getName() + " in the vpc " + vpc); + return false; + } + + return true; + } + + @Override + public boolean applyFWRulesInVPC(final Vpc vpc, final List rules) throws ResourceUnavailableException { + boolean result = true; + if (canHandle(vpc, Service.Firewall)) { + final List routers = _routerDao.listByVpcId(vpc.getId()); + if (CollectionUtils.isEmpty(routers)) { + logger.debug("Virtual router element doesn't need to apply firewall rules on the backend; virtual router doesn't exist in the vpc"); + return true; + } + + Network network = null; + if (CollectionUtils.isNotEmpty(rules) && rules.get(0).getNetworkId() != null) { + network = _networkModel.getNetwork(rules.get(0).getNetworkId()); + } + + final DataCenterVO dcVO = _dcDao.findById(vpc.getZoneId()); + final NetworkTopology networkTopology = networkTopologyContext.retrieveNetworkTopology(dcVO); + + for (final DomainRouterVO domainRouterVO : routers) { + result = result && networkTopology.applyFirewallRulesInVPC(vpc, rules, domainRouterVO); + } + } + return result; + } + @Override public boolean implementVpc(final Vpc vpc, final DeployDestination dest, final ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { @@ -412,10 +456,6 @@ private static Map> setCapabilities() { vpnCapabilities.putAll(capabilities.get(Service.Vpn)); vpnCapabilities.put(Capability.VpnTypes, "s2svpn"); capabilities.put(Service.Vpn, vpnCapabilities); - - // remove firewall capability - capabilities.remove(Service.Firewall); - // add network ACL capability final Map networkACLCapabilities = new HashMap(); networkACLCapabilities.put(Capability.SupportedProtocols, "tcp,udp,icmp"); diff --git a/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java b/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java index 744d7f5158d1..dc6fc12a6110 100644 --- a/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java +++ b/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java @@ -203,25 +203,67 @@ public FirewallRule createEgressFirewallRule(FirewallRule rule) throws NetworkRu if (sourceCidrs != null && !sourceCidrs.isEmpty()) Collections.replaceAll(sourceCidrs, "0.0.0.0/0", network.getCidr()); - return createFirewallRule(null, caller, rule.getXid(), rule.getSourcePortStart(), rule.getSourcePortEnd(), rule.getProtocol(), sourceCidrs, rule.getDestinationCidrList(), - rule.getIcmpCode(), rule.getIcmpType(), null, rule.getType(), rule.getNetworkId(), rule.getTrafficType(), rule.isDisplay()); + return createFirewallRuleForNonVPC(null, caller, rule.getXid(), rule.getSourcePortStart(), rule.getSourcePortEnd(), rule.getProtocol(), sourceCidrs, + rule.getDestinationCidrList(), rule.getIcmpCode(), rule.getIcmpType(), null, rule.getType(), rule.getNetworkId(), rule.getTrafficType(), rule.isDisplay()); } @Override @ActionEvent(eventType = EventTypes.EVENT_FIREWALL_OPEN, eventDescription = "creating firewall rule", create = true) public FirewallRule createIngressFirewallRule(FirewallRule rule) throws NetworkRuleConflictException { - Account caller = CallContext.current().getCallingAccount(); + Account caller = CallContext.current().getCallingAccount(); Long sourceIpAddressId = rule.getSourceIpAddressId(); + IPAddressVO sourceIp = getSourceIpForIngressRule(sourceIpAddressId); + + if (sourceIp.getVpcId() != null) { + return createIngressFirewallRuleForVpcIp(rule, caller, sourceIp); + } + return createIngressFirewallRuleForIsolatedIp(rule, caller, sourceIp); + } + + protected IPAddressVO getSourceIpForIngressRule(Long sourceIpAddressId) { + if (sourceIpAddressId == null) { + return null; + } + IPAddressVO sourceIp = _ipAddressDao.findById(sourceIpAddressId); + if (sourceIp == null) { + throw new CloudRuntimeException("Unable to find IP address by id=" + sourceIpAddressId); + } + + return sourceIp; + } + + protected FirewallRule createIngressFirewallRuleForIsolatedIp(FirewallRule rule, Account caller, IPAddressVO sourceIp) + throws NetworkRuleConflictException { + return createFirewallRuleForNonVPC(rule.getSourceIpAddressId(), caller, rule.getXid(), rule.getSourcePortStart(), rule.getSourcePortEnd(), + rule.getProtocol(), rule.getSourceCidrList(), null, rule.getIcmpCode(), rule.getIcmpType(), null, rule.getType(), + rule.getNetworkId(), rule.getTrafficType(), rule.isDisplay()); + } - return createFirewallRule(sourceIpAddressId, caller, rule.getXid(), rule.getSourcePortStart(), rule.getSourcePortEnd(), rule.getProtocol(), - rule.getSourceCidrList(), null, rule.getIcmpCode(), rule.getIcmpType(), null, rule.getType(), rule.getNetworkId(), rule.getTrafficType(), rule.isDisplay()); + protected FirewallRule createIngressFirewallRuleForVpcIp(FirewallRule rule, Account caller, IPAddressVO sourceIp) + throws NetworkRuleConflictException { + Long vpcId = sourceIp != null ? sourceIp.getVpcId() : null; + return createFirewallRuleForVpc(rule.getSourceIpAddressId(), caller, rule.getXid(), rule.getSourcePortStart(), rule.getSourcePortEnd(), + rule.getProtocol(), rule.getSourceCidrList(), null, rule.getIcmpCode(), rule.getIcmpType(), null, rule.getType(), + vpcId, rule.getTrafficType(), rule.isDisplay()); } //Destination CIDR capability is currently implemented for egress rules only. For others, the field is passed as null. @DB protected FirewallRule createFirewallRule(final Long ipAddrId, Account caller, final String xId, final Integer portStart, final Integer portEnd, final String protocol, final List sourceCidrList, final List destCidrList, final Integer icmpCode, final Integer icmpType, final Long relatedRuleId, - final FirewallRule.FirewallRuleType type, final Long networkId, final FirewallRule.TrafficType trafficType, final Boolean forDisplay) throws NetworkRuleConflictException { + final FirewallRule.FirewallRuleType type, final Long networkId, final Long vpcId, final FirewallRule.TrafficType trafficType, final Boolean forDisplay) throws NetworkRuleConflictException { + if (vpcId != null) { + return createFirewallRuleForVpc(ipAddrId, caller, xId, portStart, portEnd, protocol, sourceCidrList, destCidrList, icmpCode, icmpType, relatedRuleId, + type, vpcId, trafficType, forDisplay); + } + return createFirewallRuleForNonVPC(ipAddrId, caller, xId, portStart, portEnd, protocol, sourceCidrList, destCidrList, icmpCode, icmpType, relatedRuleId, + type, networkId, trafficType, forDisplay); + } + + @DB + protected FirewallRule createFirewallRuleForNonVPC(final Long ipAddrId, Account caller, final String xId, final Integer portStart, final Integer portEnd, final String protocol, + final List sourceCidrList, final List destCidrList, final Integer icmpCode, final Integer icmpType, final Long relatedRuleId, + final FirewallRule.FirewallRuleType type, final Long networkId, final FirewallRule.TrafficType trafficType, final Boolean forDisplay) throws NetworkRuleConflictException { IPAddressVO ipAddress = null; try { // Validate ip address @@ -288,6 +330,161 @@ protected FirewallRule createFirewallRule(final Long ipAddrId, Account caller, f } } + @DB + protected FirewallRule createFirewallRuleForVpc(final Long ipAddrId, Account caller, final String xId, final Integer portStart, final Integer portEnd, final String protocol, + final List sourceCidrList, final List destCidrList, final Integer icmpCode, final Integer icmpType, + final Long relatedRuleId, final FirewallRuleType type, final Long vpcId, + final FirewallRule.TrafficType trafficType, final Boolean forDisplay) throws NetworkRuleConflictException { + IPAddressVO ipAddress = null; + try { + Long resolvedVpcId = vpcId; + if (ipAddrId != null) { + ipAddress = _ipAddressDao.acquireInLockTable(ipAddrId); + if (ipAddress == null) { + throw new InvalidParameterValueException("Unable to create firewall rule; " + "couldn't locate IP address by id in the system"); + } + resolvedVpcId = resolvedVpcId != null ? resolvedVpcId : ipAddress.getVpcId(); + } + + if (resolvedVpcId == null) { + throw new InvalidParameterValueException("Unable to create VPC firewall rule; couldn't locate VPC id"); + } + + validateFirewallRuleForVpc(caller, ipAddress, portStart, portEnd, protocol, Purpose.Firewall, type, resolvedVpcId, trafficType); + + if (!protocol.equalsIgnoreCase(NetUtils.ICMP_PROTO) && (icmpCode != null || icmpType != null)) { + throw new InvalidParameterValueException("Can specify icmpCode and icmpType for ICMP protocol only"); + } + + if (protocol.equalsIgnoreCase(NetUtils.ICMP_PROTO) && (portStart != null || portEnd != null)) { + throw new InvalidParameterValueException("Can't specify start/end port when protocol is ICMP"); + } + + Long accountId = null; + Long domainId = null; + + if (ipAddress != null) { + accountId = ipAddress.getAllocatedToAccountId(); + domainId = ipAddress.getAllocatedInDomainId(); + } else { + Vpc vpc = _vpcMgr.getActiveVpc(resolvedVpcId); + if (vpc == null) { + throw new InvalidParameterValueException("Unable to create VPC firewall rule; couldn't locate VPC by id=" + resolvedVpcId); + } + accountId = vpc.getAccountId(); + domainId = vpc.getDomainId(); + } + + final Long accountIdFinal = accountId; + final Long domainIdFinal = domainId; + final Long resolvedNetworkIdFinal = null; + final Long resolvedVpcIdFinal = resolvedVpcId; + return Transaction.execute((TransactionCallbackWithException) status -> { + FirewallRuleVO newRule = new FirewallRuleVO(xId, ipAddrId, portStart, portEnd, protocol.toLowerCase(), resolvedNetworkIdFinal, accountIdFinal, domainIdFinal, Purpose.Firewall, + sourceCidrList, destCidrList, icmpCode, icmpType, relatedRuleId, trafficType); + newRule.setVpcId(resolvedVpcIdFinal); + newRule.setType(type); + if (forDisplay != null) { + newRule.setDisplay(forDisplay); + } + newRule = _firewallDao.persist(newRule); + + if (type == FirewallRuleType.User) + detectRulesConflict(newRule); + + if (!_firewallDao.setStateToAdd(newRule)) { + throw new CloudRuntimeException("Unable to update the state to add for " + newRule); + } + CallContext.current().setEventDetails("Rule ID: " + newRule.getUuid()); + CallContext.current().putContextParameter(FirewallRule.class, newRule.getId()); + + return newRule; + }); + } finally { + if (ipAddrId != null) { + _ipAddressDao.releaseFromLockTable(ipAddrId); + } + } + } + + protected void validateFirewallRuleForVpc(Account caller, IPAddressVO ipAddress, Integer portStart, Integer portEnd, String proto, Purpose purpose, + FirewallRuleType type, Long vpcId, FirewallRule.TrafficType trafficType) { + if (portStart != null && !NetUtils.isValidPort(portStart)) { + throw new InvalidParameterValueException("publicPort is an invalid value: " + portStart); + } + if (portEnd != null && !NetUtils.isValidPort(portEnd)) { + throw new InvalidParameterValueException("Public port range is an invalid value: " + portEnd); + } + if (portStart != null && portEnd != null && portStart > portEnd) { + throw new InvalidParameterValueException("Start port can't be bigger than end port"); + } + + if (ipAddress == null && type == FirewallRuleType.System) { + return; + } + + if (vpcId == null) { + throw new InvalidParameterValueException("Unable to retrieve VPC id to validate the rule"); + } + + if (ipAddress != null) { + _accountMgr.checkAccess(caller, null, true, ipAddress); + } + + Vpc vpc = _vpcMgr.getActiveVpc(vpcId); + if (vpc == null) { + throw new InvalidParameterValueException("Unable to retrieve VPC to validate the rule by id=" + vpcId); + } + + Map caps = null; + if (purpose == Purpose.Firewall) { + caps = getFirewallServiceCapabilitiesForVpc(vpcId); + if (caps == null) { + throw new InvalidParameterValueException("Firewall service is not supported in VPC " + vpc); + } + } + + if (caps != null) { + String supportedTrafficTypes = null; + if (purpose == FirewallRule.Purpose.Firewall) { + supportedTrafficTypes = caps.get(Capability.SupportedTrafficDirection).toLowerCase(); + } + + String supportedProtocols; + if (purpose == FirewallRule.Purpose.Firewall && trafficType == FirewallRule.TrafficType.Egress) { + supportedProtocols = caps.get(Capability.SupportedEgressProtocols).toLowerCase(); + } else { + supportedProtocols = caps.get(Capability.SupportedProtocols).toLowerCase(); + } + + if (!supportedProtocols.contains(proto.toLowerCase())) { + throw new InvalidParameterValueException("Protocol " + proto + " is not supported in VPC " + vpcId); + } else if (proto.equalsIgnoreCase(NetUtils.ICMP_PROTO) && purpose != Purpose.Firewall) { + throw new InvalidParameterValueException("Protocol " + proto + " is currently supported only for rules with purpose " + Purpose.Firewall); + } else if (purpose == Purpose.Firewall && !supportedTrafficTypes.contains(trafficType.toString().toLowerCase())) { + throw new InvalidParameterValueException(String.format("Traffic Type %s is currently supported by Firewall in VPC %s", trafficType, vpc.getUuid())); + } + } + } + + protected Map getFirewallServiceCapabilitiesForVpc(Long vpcId) { + for (FirewallServiceProvider fwElement : _firewallElements) { + Network.Provider provider = fwElement.getProvider(); + if (_vpcMgr.isProviderSupportServiceInVpc(vpcId, Service.Firewall, provider)) { + Map> capabilities = fwElement.getCapabilities(); + if (capabilities != null && capabilities.get(Service.Firewall) != null) { + return capabilities.get(Service.Firewall); + } + } + } + return null; + } + + protected Long resolveIsolatedFirewallRuleNetworkId(IPAddressVO ipAddress, Long networkId) { + _networkModel.checkIpForService(ipAddress, Service.Firewall, networkId); + return ipAddress.getAssociatedWithNetworkId(); + } + @Override public Pair, Integer> listFirewallRules(IListFirewallRulesCmd cmd) { Long ipId = cmd.getIpAddressId(); @@ -404,9 +601,16 @@ public void detectRulesConflict(FirewallRule newRule) throws NetworkRuleConflict assert (rules.size() >= 1); } - NetworkVO newRuleNetwork = getNewRuleNetwork(newRule); - boolean newRuleIsOnVpcNetwork = newRuleNetwork.getVpcId() != null; - boolean vpcConserveModeEnabled = _vpcMgr.isNetworkOnVpcEnabledConserveMode(newRuleNetwork); + Long newRuleVpcId = newRule.getVpcId(); + boolean newRuleIsVpc = newRuleVpcId != null; + NetworkVO newRuleNetwork = null; + boolean newRuleIsOnVpcNetwork = false; + boolean vpcConserveModeEnabled = false; + if (!newRuleIsVpc) { + newRuleNetwork = getNewRuleNetwork(newRule); + newRuleIsOnVpcNetwork = newRuleNetwork.getVpcId() != null; + vpcConserveModeEnabled = newRuleIsOnVpcNetwork && _vpcMgr.isNetworkOnVpcEnabledConserveMode(newRuleNetwork); + } for (FirewallRuleVO rule : rules) { if (rule.getId() == newRule.getId()) { @@ -457,8 +661,8 @@ public void detectRulesConflict(FirewallRule newRule) throws NetworkRuleConflict // Checking if the rule applied is to the same network that is passed in the rule. // (except for VPCs with conserve mode = true) - if ((!newRuleIsOnVpcNetwork || !vpcConserveModeEnabled) - && rule.getNetworkId() != newRule.getNetworkId() && rule.getState() != State.Revoke) { + if (!newRuleIsVpc && (!newRuleIsOnVpcNetwork || !vpcConserveModeEnabled) + && !Objects.equals(rule.getNetworkId(), newRule.getNetworkId()) && rule.getState() != State.Revoke) { String errMsg = String.format("New rule is for a different network than what's specified in rule %s", rule.getXid()); if (newRuleIsOnVpcNetwork) { Vpc vpc = _vpcMgr.getActiveVpc(newRuleNetwork.getVpcId()); @@ -580,11 +784,9 @@ public void validateFirewallRule(Account caller, IPAddressVO ipAddress, Integer } if (ipAddress != null) { - if (ipAddress.getAssociatedWithNetworkId() == null) { - throw new InvalidParameterValueException("Unable to create firewall rule ; ip with specified id is not associated with any network"); - } else { - networkId = ipAddress.getAssociatedWithNetworkId(); - } + networkId = isVpcIpAddress(ipAddress) + ? validateFirewallRuleForVpcIp(ipAddress, networkId) + : validateFirewallRuleForIsolatedIp(ipAddress); // Validate ip address _accountMgr.checkAccess(caller, null, true, ipAddress); @@ -615,7 +817,7 @@ public void validateFirewallRule(Account caller, IPAddressVO ipAddress, Integer if (routedIpv4Manager.isVirtualRouterGateway(network)) { throw new CloudRuntimeException("Unable to create routing firewall rule. Please use routing firewall API instead."); } - caps = _networkModel.getNetworkServiceCapabilities(network.getId(), Service.Firewall); + caps = getFirewallServiceCapabilities(network); } if (caps != null) { @@ -655,6 +857,41 @@ public void validateFirewallRule(Account caller, IPAddressVO ipAddress, Integer } + protected boolean isVpcIpAddress(IPAddressVO ipAddress) { + return ipAddress.getVpcId() != null; + } + + protected Long validateFirewallRuleForIsolatedIp(IPAddressVO ipAddress) { + if (ipAddress.getAssociatedWithNetworkId() == null) { + throw new InvalidParameterValueException("Unable to create firewall rule ; ip with specified id is not associated with any network"); + } + return ipAddress.getAssociatedWithNetworkId(); + } + + protected Long validateFirewallRuleForVpcIp(IPAddressVO ipAddress, Long networkId) { + if (networkId == null) { + throw new InvalidParameterValueException("Unable to retrieve network id to validate the rule"); + } + return networkId; + } + + protected Map getFirewallServiceCapabilities(Network network) { + if (network.getVpcId() == null) { + return _networkModel.getNetworkServiceCapabilities(network.getId(), Service.Firewall); + } + + for (FirewallServiceProvider fwElement : _firewallElements) { + Network.Provider provider = fwElement.getProvider(); + if (_vpcMgr.isProviderSupportServiceInVpc(network.getVpcId(), Service.Firewall, provider)) { + Map> capabilities = fwElement.getCapabilities(); + if (capabilities != null && capabilities.get(Service.Firewall) != null) { + return capabilities.get(Service.Firewall); + } + } + } + return _networkModel.getNetworkServiceCapabilities(network.getId(), Service.Firewall); + } + @Override public boolean applyRules(List rules, boolean continueOnError, boolean updateRulesInDB) throws ResourceUnavailableException { boolean success = true; @@ -683,7 +920,7 @@ public boolean applyRules(List rules, boolean continueOn success = false; } else { removeRule(rule); - if (rule.getSourceIpAddressId() != null) { + if (rule.getSourceIpAddressId() != null && rule.getVpcId() == null) { //if the rule is the last one for the ip address assigned to VPC, unassign it from the network _vpcMgr.unassignIPFromVpcNetwork(rule.getSourceIpAddressId(), rule.getNetworkId()); } @@ -701,7 +938,7 @@ public boolean applyRules(List rules, boolean continueOn } @Override - public boolean applyRules(Network network, Purpose purpose, List rules) throws ResourceUnavailableException { + public boolean applyRules(Network network, Vpc vpc, Purpose purpose, List rules) throws ResourceUnavailableException { boolean handled = false; switch (purpose) { /* StaticNatRule would be applied by Firewall provider, since the incompatible of two object */ @@ -710,11 +947,26 @@ public boolean applyRules(Network network, Purpose purpose, List rules) throws ResourceUnavailableException { + return applyRules(network, null, purpose, rules); + } + @Override public void removeRule(FirewallRule rule) { @@ -817,8 +1074,10 @@ public boolean applyFirewallRules(List rules, boolean continueOn for (FirewallRuleVO rule : rules) { // validate rule - for NSX - long networkId = rule.getNetworkId(); - validateNsxConstraints(networkId, rule); + Long networkId = rule.getNetworkId(); + if (networkId != null) { + validateNsxConstraints(networkId, rule); + } // load cidrs if any rule.setSourceCidrList(_firewallCidrsDao.getSourceCidrs(rule.getId())); rule.setDestinationCidrsList(_firewallDcidrsDao.getDestCidrs(rule.getId())); @@ -1078,7 +1337,7 @@ public FirewallRule createRuleForAllCidrs(long ipAddrId, Account caller, Integer List oneCidr = new ArrayList(); oneCidr.add(NetUtils.ALL_IP4_CIDRS); return createFirewallRule(ipAddrId, caller, null, startPort, endPort, protocol, oneCidr, null, icmpCode, icmpType, relatedRuleId, FirewallRule.FirewallRuleType.User, - networkId, FirewallRule.TrafficType.Ingress, true); + networkId, null, FirewallRule.TrafficType.Ingress, true); } @Override @@ -1193,7 +1452,7 @@ public boolean addSystemFirewallRules(IPAddressVO ip, Account acct) { _firewallDao.loadSourceCidrs(rule); } createFirewallRule(ip.getId(), acct, rule.getXid(), rule.getSourcePortStart(), rule.getSourcePortEnd(), rule.getProtocol(), rule.getSourceCidrList(),null, - rule.getIcmpCode(), rule.getIcmpType(), rule.getRelated(), FirewallRuleType.System, rule.getNetworkId(), rule.getTrafficType(), true); + rule.getIcmpCode(), rule.getIcmpType(), rule.getRelated(), FirewallRuleType.System, rule.getNetworkId(), rule.getVpcId(), rule.getTrafficType(), true); } catch (Exception e) { logger.debug("Failed to add system wide firewall rule, due to:" + e.toString()); } diff --git a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java index 628c36826dec..58b2892e09c3 100644 --- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java +++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java @@ -463,7 +463,8 @@ public void createApplyStaticNatRulesCommands(final List rules, final VirtualRouter router, final Commands cmds, final long guestNetworkId) { + public void createApplyFirewallRulesCommands(final List rules, final VirtualRouter router, final Commands cmds, + final Long guestNetworkId, final Long vpcId) { final List rulesTO = new ArrayList<>(); String systemRule = null; Boolean defaultEgressPolicy = false; @@ -485,6 +486,10 @@ public void createApplyFirewallRulesCommands(final List final FirewallRuleTO ruleTO = new FirewallRuleTO(rule, null, srcIp, Purpose.Firewall, traffictype); rulesTO.add(ruleTO); } else if (rule.getTrafficType() == FirewallRule.TrafficType.Egress) { + if (guestNetworkId == null) { + logger.warn("Skipping egress firewall rule {} as guestNetworkId is null", rule.getUuid()); + continue; + } final NetworkVO network = _networkDao.findById(guestNetworkId); final NetworkOfferingVO offering = _networkOfferingDao.findById(network.getNetworkOfferingId()); defaultEgressPolicy = offering.isEgressDefaultPolicy(); @@ -495,9 +500,14 @@ public void createApplyFirewallRulesCommands(final List } } - final SetFirewallRulesCommand cmd = new SetFirewallRulesCommand(rulesTO); + final SetFirewallRulesCommand cmd = new SetFirewallRulesCommand(rulesTO, vpcId); cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, _routerControlHelper.getRouterControlIp(router.getId())); - cmd.setAccessDetail(NetworkElementCommand.ROUTER_GUEST_IP, _routerControlHelper.getRouterIpInNetwork(guestNetworkId, router.getId())); + if (guestNetworkId != null) { + cmd.setAccessDetail(NetworkElementCommand.ROUTER_GUEST_IP, _routerControlHelper.getRouterIpInNetwork(guestNetworkId, router.getId())); + } + if (vpcId != null) { + cmd.setAccessDetail(NetworkElementCommand.VPC_ID, String.valueOf(vpcId)); + } cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, router.getInstanceName()); final DataCenterVO dcVo = _dcDao.findById(router.getDataCenterId()); cmd.setAccessDetail(NetworkElementCommand.ZONE_NETWORK_TYPE, dcVo.getNetworkType().toString()); @@ -510,6 +520,10 @@ public void createApplyFirewallRulesCommands(final List cmds.addCommand(cmd); } + public void createApplyFirewallRulesCommands(final List rules, final VirtualRouter router, final Commands cmds, final long guestNetworkId) { + createApplyFirewallRulesCommands(rules, router, cmds, guestNetworkId, null); + } + public void createApplyIpv6FirewallRulesCommands(final List rules, final VirtualRouter router, final Commands cmds, final long guestNetworkId) { final List rulesTO = new ArrayList<>(); String systemRule = null; @@ -551,7 +565,8 @@ public void createApplyIpv6FirewallRulesCommands(final List rules, final VirtualRouter router, final Commands cmds, final long guestNetworkId) { + public void createFirewallRulesCommands(final List rules, final VirtualRouter router, final Commands cmds, final Long guestNetworkId, + final Long vpcId) { final List rulesTO = new ArrayList<>(); String systemRule = null; Boolean defaultEgressPolicy = false; @@ -573,6 +588,10 @@ public void createFirewallRulesCommands(final List rules final FirewallRuleTO ruleTO = new FirewallRuleTO(rule, null, srcIp, Purpose.Firewall, traffictype); rulesTO.add(ruleTO); } else if (rule.getTrafficType() == FirewallRule.TrafficType.Egress) { + if (guestNetworkId == null) { + logger.warn("Skipping egress firewall rule {} as guestNetworkId is null", rule.getUuid()); + continue; + } final NetworkVO network = _networkDao.findById(guestNetworkId); final NetworkOfferingVO offering = _networkOfferingDao.findById(network.getNetworkOfferingId()); defaultEgressPolicy = offering.isEgressDefaultPolicy(); @@ -583,9 +602,14 @@ public void createFirewallRulesCommands(final List rules } } - final SetFirewallRulesCommand cmd = new SetFirewallRulesCommand(rulesTO); + final SetFirewallRulesCommand cmd = new SetFirewallRulesCommand(rulesTO, vpcId); cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, _routerControlHelper.getRouterControlIp(router.getId())); - cmd.setAccessDetail(NetworkElementCommand.ROUTER_GUEST_IP, _routerControlHelper.getRouterIpInNetwork(guestNetworkId, router.getId())); + if (guestNetworkId != null) { + cmd.setAccessDetail(NetworkElementCommand.ROUTER_GUEST_IP, _routerControlHelper.getRouterIpInNetwork(guestNetworkId, router.getId())); + } + if (vpcId != null) { + cmd.setAccessDetail(NetworkElementCommand.VPC_ID, String.valueOf(vpcId)); + } cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, router.getInstanceName()); final DataCenterVO dcVo = _dcDao.findById(router.getDataCenterId()); cmd.setAccessDetail(NetworkElementCommand.ZONE_NETWORK_TYPE, dcVo.getNetworkType().toString()); @@ -598,6 +622,10 @@ public void createFirewallRulesCommands(final List rules cmds.addCommand(cmd); } + public void createFirewallRulesCommands(final List rules, final VirtualRouter router, final Commands cmds, final Long guestNetworkId) { + createFirewallRulesCommands(rules, router, cmds, guestNetworkId, router.getVpcId()); + } + public void createIpv6FirewallRulesCommands(final List rules, final VirtualRouter router, final Commands cmds, final long guestNetworkId) { final List rulesTO = new ArrayList<>(); String systemRule = null; diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index dd65719ad033..a166e894be1b 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -2018,6 +2018,8 @@ public boolean finalizeVirtualMachineProfile(final VirtualMachineProfile profile } else { buf.append(" has_public_network=false"); } + boolean isVpcFirewallEnabled = vpcManager.isProviderSupportServiceInVpc(vpc.getId(), Service.Firewall, Provider.VPCVirtualRouter); + buf.append(" vpc_firewall_enabled=").append(isVpcFirewallEnabled); } else if (!publicNetwork) { type = "dhcpsrvr"; } else { @@ -2497,7 +2499,7 @@ protected void finalizeNetworkRulesForNetwork(final Commands cmds, final DomainR // Re-apply firewall Egress rules logger.debug("Found " + firewallRulesEgress.size() + " firewall Egress rule(s) to apply as a part of domR " + router + " start."); if (!firewallRulesEgress.isEmpty()) { - _commandSetupHelper.createFirewallRulesCommands(firewallRulesEgress, router, cmds, guestNetworkId); + _commandSetupHelper.createFirewallRulesCommands(firewallRulesEgress, router, cmds, guestNetworkId, router.getVpcId()); } logger.debug(String.format("Found %d Ipv6 firewall rule(s) to apply as a part of domR %s start.", ipv6firewallRules.size(), router)); @@ -2572,7 +2574,7 @@ protected void finalizeNetworkRulesForNetwork(final Commands cmds, final DomainR // Re-apply firewall Ingress rules logger.debug("Found " + firewallRulesIngress.size() + " firewall Ingress rule(s) to apply as a part of domR " + router + " start."); if (!firewallRulesIngress.isEmpty()) { - _commandSetupHelper.createFirewallRulesCommands(firewallRulesIngress, router, cmds, guestNetworkId); + _commandSetupHelper.createFirewallRulesCommands(firewallRulesIngress, router, cmds, guestNetworkId, router.getVpcId()); } // Re-apply port forwarding rules diff --git a/server/src/main/java/com/cloud/network/router/VpcVirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VpcVirtualNetworkApplianceManagerImpl.java index 67cfe8458518..49c2243d1ecf 100644 --- a/server/src/main/java/com/cloud/network/router/VpcVirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VpcVirtualNetworkApplianceManagerImpl.java @@ -72,6 +72,7 @@ import com.cloud.network.dao.LoadBalancerVO; import com.cloud.network.dao.MonitoringServiceVO; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.rules.FirewallRule; import com.cloud.network.dao.RemoteAccessVpnVO; import com.cloud.network.dao.Site2SiteVpnConnectionVO; import com.cloud.network.lb.LoadBalancingRule; @@ -567,6 +568,10 @@ public boolean finalizeCommandsOnStart(final Commands cmds, final VirtualMachine finalizeMonitorService(cmds, profile, domainRouterVO, provider, publicNics.get(0).second().getId(), true, routerHealthCheckConfig); } + if (reprogramGuestNtwks) { + reapplyVpcFirewallIngressRules(cmds, domainRouterVO, provider); + } + for (final Pair nicNtwk : guestNics) { final Nic guestNic = nicNtwk.first(); final long guestNetworkId = guestNic.getNetworkId(); @@ -638,6 +643,26 @@ protected void finalizeNetworkRulesForNetwork(final Commands cmds, final DomainR } } + private void reapplyVpcFirewallIngressRules(final Commands cmds, final DomainRouterVO domainRouterVO, final Provider provider) { + final Long vpcId = domainRouterVO.getVpcId(); + if (vpcId == null) { + return; + } + + if (!_vpcMgr.isProviderSupportServiceInVpc(vpcId, Service.Firewall, provider)) { + return; + } + + final List firewallRulesIngress = new ArrayList<>( + _rulesDao.listByVpcPurposeTrafficType(vpcId, FirewallRule.Purpose.Firewall, FirewallRule.TrafficType.Ingress)); + if (firewallRulesIngress.isEmpty()) { + return; + } + + logger.debug("Found {} VPC firewall ingress rule(s) to apply as a part of domR {} start for VPC {}", firewallRulesIngress.size(), domainRouterVO, vpcId); + _commandSetupHelper.createFirewallRulesCommands(firewallRulesIngress, domainRouterVO, cmds, null, vpcId); + } + protected boolean sendNetworkRulesToRouter(final long routerId, final long networkId, final boolean reprogramNetwork) throws ResourceUnavailableException { final DomainRouterVO router = _routerDao.findById(routerId); final Commands cmds = new Commands(OnError.Continue); diff --git a/server/src/main/java/com/cloud/network/rules/FirewallRules.java b/server/src/main/java/com/cloud/network/rules/FirewallRules.java index e995f143e0e8..11922e676e1e 100644 --- a/server/src/main/java/com/cloud/network/rules/FirewallRules.java +++ b/server/src/main/java/com/cloud/network/rules/FirewallRules.java @@ -36,6 +36,7 @@ import com.cloud.network.router.VirtualRouter; import com.cloud.network.rules.FirewallRule.Purpose; import com.cloud.network.rules.LoadBalancerContainer.Scheme; +import com.cloud.network.vpc.Vpc; import com.cloud.utils.net.Ip; public class FirewallRules extends RuleApplier { @@ -50,6 +51,16 @@ public FirewallRules(final Network network, final List r _rules = rules; } + public FirewallRules(final Network network, final Vpc vpc, final List rules) { + super(network, vpc); + _rules = rules; + } + + public FirewallRules(final Vpc vpc, final List rules) { + super(null, vpc); + _rules = rules; + } + @Override public boolean accept(final NetworkTopologyVisitor visitor, final VirtualRouter router) throws ResourceUnavailableException { _router = router; diff --git a/server/src/main/java/com/cloud/network/rules/RuleApplier.java b/server/src/main/java/com/cloud/network/rules/RuleApplier.java index 73c3855361b0..baf4da324845 100644 --- a/server/src/main/java/com/cloud/network/rules/RuleApplier.java +++ b/server/src/main/java/com/cloud/network/rules/RuleApplier.java @@ -22,6 +22,7 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.Network; import com.cloud.network.router.VirtualRouter; +import com.cloud.network.vpc.Vpc; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -30,16 +31,27 @@ public abstract class RuleApplier { protected Logger logger = LogManager.getLogger(getClass()); protected Network _network; + protected Vpc _vpc; protected VirtualRouter _router; public RuleApplier(final Network network) { _network = network; + _vpc = null; + } + + public RuleApplier(final Network network, final Vpc vpc) { + _network = network; + _vpc = vpc; } public Network getNetwork() { return _network; } + public Vpc getVpc() { + return _vpc; + } + public VirtualRouter getRouter() { return _router; } diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java index e2083fac7ff8..bcf2c6176efe 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -336,7 +336,7 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis private final ScheduledExecutorService _executor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("VpcChecker")); private List vpcElements = null; - private final List nonSupportedServices = Arrays.asList(Service.SecurityGroup, Service.Firewall); + private final List nonSupportedServices = Arrays.asList(Service.SecurityGroup); private final List supportedProviders = Arrays.asList(Provider.VPCVirtualRouter, Provider.NiciraNvp, Provider.InternalLbVm, Provider.Netscaler, Provider.JuniperContrailVpcRouter, Provider.Ovs, Provider.BigSwitchBcf, Provider.ConfigDrive, Provider.Nsx, Provider.Netris); diff --git a/server/src/main/java/org/apache/cloudstack/network/RoutedIpv4ManagerImpl.java b/server/src/main/java/org/apache/cloudstack/network/RoutedIpv4ManagerImpl.java index a03db4d4a241..213d8ee0087b 100644 --- a/server/src/main/java/org/apache/cloudstack/network/RoutedIpv4ManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/network/RoutedIpv4ManagerImpl.java @@ -989,15 +989,15 @@ public boolean applyRoutingFirewallRule(long id) { @Override public boolean isVirtualRouterGateway(Network network) { return isRoutedNetwork(network) - && (networkServiceMapDao.canProviderSupportServiceInNetwork(network.getId(), Service.Gateway, Provider.VirtualRouter)) - || networkServiceMapDao.canProviderSupportServiceInNetwork(network.getId(), Service.Gateway, Provider.VPCVirtualRouter); + && (networkServiceMapDao.canProviderSupportServiceInNetwork(network.getId(), Service.Gateway, Provider.VirtualRouter) + || networkServiceMapDao.canProviderSupportServiceInNetwork(network.getId(), Service.Gateway, Provider.VPCVirtualRouter)); } @Override public boolean isVirtualRouterGateway(NetworkOffering networkOffering) { return NetworkOffering.NetworkMode.ROUTED.equals(networkOffering.getNetworkMode()) - && networkOfferingServiceMapDao.canProviderSupportServiceInNetworkOffering(networkOffering.getId(), Service.Gateway, Provider.VirtualRouter) - || networkOfferingServiceMapDao.canProviderSupportServiceInNetworkOffering(networkOffering.getId(), Service.Gateway, Provider.VPCVirtualRouter); + && (networkOfferingServiceMapDao.canProviderSupportServiceInNetworkOffering(networkOffering.getId(), Service.Gateway, Provider.VirtualRouter) + || networkOfferingServiceMapDao.canProviderSupportServiceInNetworkOffering(networkOffering.getId(), Service.Gateway, Provider.VPCVirtualRouter)); } @Override diff --git a/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkTopology.java b/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkTopology.java index a7000f702ec9..46a6886182b0 100644 --- a/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkTopology.java +++ b/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkTopology.java @@ -65,6 +65,7 @@ import com.cloud.network.vpc.NetworkACLItem; import com.cloud.network.vpc.PrivateGateway; import com.cloud.network.vpc.StaticRouteProfile; +import com.cloud.network.vpc.Vpc; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.DomainRouterVO; import com.cloud.vm.NicProfile; @@ -228,6 +229,26 @@ public boolean applyFirewallRules(final Network network, final List(firewallRules)); } + @Override + public boolean applyFirewallRulesInVPC(final Vpc vpc, final List rules, final VirtualRouter router) + throws ResourceUnavailableException { + if (rules == null || rules.isEmpty()) { + logger.debug("No firewall rules to be applied for vpc {}", vpc); + return true; + } + + logger.debug("APPLYING FIREWALL RULES"); + + final String typeString = "firewall rules"; + final boolean isPodLevelException = false; + final boolean failWhenDisconnect = false; + final Long podId = null; + + final FirewallRules firewallRules = new FirewallRules(vpc, rules); + + return applyRulesInVPC(vpc, router, typeString, isPodLevelException, podId, failWhenDisconnect, new RuleApplierWrapper(firewallRules)); + } + @Override public boolean applyStaticNats(final Network network, final List rules, final VirtualRouter router) throws ResourceUnavailableException { if (rules == null || rules.isEmpty()) { @@ -444,6 +465,102 @@ public boolean applyRules(final Network network, final VirtualRouter router, fin return result; } + @Override + public boolean applyRulesInVPC(final Vpc vpc, final VirtualRouter router, final String typeString, final boolean isPodLevelException, final Long podId, + final boolean failWhenDisconnect, final RuleApplierWrapper ruleApplierWrapper) throws ResourceUnavailableException { + + if (vpc == null) { + throw new CloudRuntimeException("Unable to apply " + typeString + " because VPC is null"); + } + + if (router == null) { + logger.warn("Unable to apply {}, virtual router doesn't exist in vpc {}", typeString, vpc); + final Long dcId = vpc.getZoneId(); + throw new ResourceUnavailableException("Unable to apply " + typeString, DataCenter.class, dcId); + } + + final RuleApplier ruleApplier = ruleApplierWrapper.getRuleType(); + + final Long dcId = vpc.getZoneId(); + final DataCenter dc = _dcDao.findById(dcId); + final boolean isZoneBasic = dc.getNetworkType() == NetworkType.Basic; + + // isPodLevelException and podId is only used for basic zone + assert !(!isZoneBasic && isPodLevelException || isZoneBasic && isPodLevelException && podId == null); + + final List connectedRouters = new ArrayList(); + final List disconnectedRouters = new ArrayList(); + boolean result = true; + final String msg = "Unable to apply " + typeString + " on disconnected router "; + if (router.getState() == State.Running) { + logger.debug("Applying {} in vpc {}", typeString, vpc); + + if (router.isStopPending()) { + if (_hostDao.findById(router.getHostId()).getState() == Status.Up) { + throw new ResourceUnavailableException("Unable to process due to the stop pending router " + router.getInstanceName() + + " haven't been stopped after it's host coming back!", DataCenter.class, router.getDataCenterId()); + } + logger.debug("Router {} is stop pending, so not sending apply {} commands to the backend", router, typeString); + return false; + } + + try { + result = ruleApplier.accept(getVisitor(), router); + connectedRouters.add(router); + } catch (final AgentUnavailableException e) { + logger.warn("{}{}", msg, router, e); + disconnectedRouters.add(router); + } + + // If rules fail to apply on one domR and not due to + // disconnection, no need to proceed with the rest + if (!result) { + if (isZoneBasic && isPodLevelException) { + throw new ResourceUnavailableException("Unable to apply " + typeString + " on router ", Pod.class, podId); + } + throw new ResourceUnavailableException("Unable to apply " + typeString + " on router ", DataCenter.class, router.getDataCenterId()); + } + + } else if (router.getState() == State.Stopped || router.getState() == State.Stopping) { + logger.debug("Router {} is in {}, so not sending apply {} commands to the backend", router, router.getState(), typeString); + } else { + logger.warn("Unable to apply " + typeString + ", virtual router is not in the right state " + router.getState()); + if (isZoneBasic && isPodLevelException) { + throw new ResourceUnavailableException("Unable to apply " + typeString + ", virtual router is not in the right state", Pod.class, podId); + } + throw new ResourceUnavailableException("Unable to apply " + typeString + ", virtual router is not in the right state", DataCenter.class, router.getDataCenterId()); + } + + if (!connectedRouters.isEmpty()) { + // Shouldn't we include this check inside the method? + if (!isZoneBasic && !disconnectedRouters.isEmpty()) { + // These disconnected redundant virtual routers are out of sync + // now, stop them for synchronization + for (final VirtualRouter virtualRouter : disconnectedRouters) { + // If we have at least 1 disconnected redundant router, callhandleSingleWorkingRedundantRouter(). + if (virtualRouter.getIsRedundantRouter()) { + _networkHelper.handleSingleWorkingRedundantRouter(connectedRouters, disconnectedRouters, msg); + break; + } + } + } + } else if (!disconnectedRouters.isEmpty()) { + if (logger.isDebugEnabled()) { + logger.debug("{}{}", msg, router); + } + if (isZoneBasic && isPodLevelException) { + throw new ResourceUnavailableException(msg, Pod.class, podId); + } + throw new ResourceUnavailableException(msg, DataCenter.class, disconnectedRouters.get(0).getDataCenterId()); + } + + result = true; + if (failWhenDisconnect) { + result = !connectedRouters.isEmpty(); + } + return result; + } + @Override public boolean removeDhcpEntry(Network network, NicProfile nic, VirtualMachineProfile profile, VirtualRouter virtualRouter) throws ResourceUnavailableException { logger.debug("REMOVING DHCP ENTRY RULE"); diff --git a/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkVisitor.java b/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkVisitor.java index 8702a58ad69e..17a6d826bd17 100644 --- a/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkVisitor.java +++ b/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkVisitor.java @@ -145,7 +145,9 @@ public boolean visit(final FirewallRules firewall) throws ResourceUnavailableExc } else if (purpose == Purpose.Firewall) { - _commandSetupHelper.createApplyFirewallRulesCommands(rules, router, cmds, network.getId()); + final Long guestNetworkId = network != null ? network.getId() : null; + final Long vpcId = network != null ? network.getVpcId() : router.getVpcId(); + _commandSetupHelper.createApplyFirewallRulesCommands(rules, router, cmds, guestNetworkId, vpcId); return _networkGeneralHelper.sendCommandsToRouter(router, cmds); diff --git a/server/src/main/java/org/apache/cloudstack/network/topology/NetworkTopology.java b/server/src/main/java/org/apache/cloudstack/network/topology/NetworkTopology.java index 176584780fed..58b0acd33d24 100644 --- a/server/src/main/java/org/apache/cloudstack/network/topology/NetworkTopology.java +++ b/server/src/main/java/org/apache/cloudstack/network/topology/NetworkTopology.java @@ -35,6 +35,7 @@ import com.cloud.network.vpc.NetworkACLItem; import com.cloud.network.vpc.PrivateGateway; import com.cloud.network.vpc.StaticRouteProfile; +import com.cloud.network.vpc.Vpc; import com.cloud.vm.DomainRouterVO; import com.cloud.vm.NicProfile; import com.cloud.vm.VirtualMachineProfile; @@ -72,6 +73,8 @@ boolean applyUserData(final Network network, final NicProfile nic, final Virtual boolean applyFirewallRules(final Network network, final List rules, final VirtualRouter router) throws ResourceUnavailableException; + boolean applyFirewallRulesInVPC(final Vpc vpc, final List rules, final VirtualRouter router) throws ResourceUnavailableException; + boolean applyStaticNats(final Network network, final List rules, final VirtualRouter router) throws ResourceUnavailableException; boolean associatePublicIP(final Network network, final List ipAddress, final VirtualRouter router) throws ResourceUnavailableException; @@ -89,6 +92,9 @@ boolean saveUserDataToRouter(final Network network, final NicProfile nic, final boolean applyRules(final Network network, final VirtualRouter router, final String typeString, final boolean isPodLevelException, final Long podId, final boolean failWhenDisconnect, RuleApplierWrapper ruleApplier) throws ResourceUnavailableException; + boolean applyRulesInVPC(final Vpc vpc, final VirtualRouter router, final String typeString, final boolean isPodLevelException, final Long podId, + final boolean failWhenDisconnect, RuleApplierWrapper ruleApplier) throws ResourceUnavailableException; + boolean removeDhcpEntry(final Network network, final NicProfile nic, final VirtualMachineProfile profile, final VirtualRouter virtualRouter) throws ResourceUnavailableException; boolean applyBgpPeers(final Network network, final List bpgPeers, final VirtualRouter virtualRouter) throws ResourceUnavailableException; diff --git a/server/src/test/java/com/cloud/network/IpAddressManagerTest.java b/server/src/test/java/com/cloud/network/IpAddressManagerTest.java index cf3a886ce99f..d12419b66e0a 100644 --- a/server/src/test/java/com/cloud/network/IpAddressManagerTest.java +++ b/server/src/test/java/com/cloud/network/IpAddressManagerTest.java @@ -19,10 +19,13 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -54,8 +57,13 @@ import com.cloud.network.dao.IPAddressVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.rules.FirewallRule; import com.cloud.network.rules.StaticNat; import com.cloud.network.rules.StaticNatImpl; +import com.cloud.network.vpc.dao.VpcDao; +import com.cloud.network.vpc.VpcVO; +import com.cloud.network.vpc.VpcManager; +import com.cloud.dc.dao.VlanDao; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.user.AccountVO; @@ -105,6 +113,15 @@ public class IpAddressManagerTest { @Mock AccountManager accountManagerMock; + @Mock + VpcManager vpcMgr; + + @Mock + VpcDao vpcDao; + + @Mock + VlanDao vlanDao; + final long dummyID = 1L; final String UUID = "uuid"; @@ -492,4 +509,135 @@ public void checkIfIpResourceCountShouldBeUpdatedTestIpIsAssociatedToVpcAndNotDe Assert.assertTrue(result); } + + + private FirewallRule makeRule(Long networkId, Long vpcId, FirewallRule.Purpose purpose, + FirewallRule.TrafficType trafficType) { + FirewallRule rule = mock(FirewallRule.class); + lenient().when(rule.getNetworkId()).thenReturn(networkId); + lenient().when(rule.getVpcId()).thenReturn(vpcId); + lenient().when(rule.getPurpose()).thenReturn(purpose); + lenient().when(rule.getTrafficType()).thenReturn(trafficType); + return rule; + } + + /** Stub the two IP-association helper methods so they are no-ops. */ + private void stubIpAssocHelpers() throws ResourceUnavailableException { + doReturn(false).when(ipAddressManager).checkIfIpAssocRequired(any(Network.class), anyBoolean(), any()); + } + + /** + * Test: Non-VPC rules still resolve via networkId (backward compatibility). + */ + @Test + public void applyRulesNonVpcRuleStillWorksViaNetworkId() throws ResourceUnavailableException { + long networkId = 10L; + NetworkVO network = mock(NetworkVO.class); + when(network.getId()).thenReturn(networkId); + when(networkDao.findById(networkId)).thenReturn(network); + + FirewallRule rule = makeRule(networkId, null, FirewallRule.Purpose.Firewall, FirewallRule.TrafficType.Ingress); + NetworkRuleApplier applier = mock(NetworkRuleApplier.class); + + when(ipAddressDao.listByAssociatedNetwork(networkId, null)).thenReturn(new ArrayList<>()); + stubIpAssocHelpers(); + + boolean result = ipAddressManager.applyRules( + Collections.singletonList(rule), FirewallRule.Purpose.Firewall, applier, false); + + assertTrue(result); + verify(networkDao).findById(networkId); + verify(applier).applyRules(network, null, FirewallRule.Purpose.Firewall, Collections.singletonList(rule)); + } + + /** + * Test: VPC rule resolves network via VpcManager.getVpcNetworks() + * when networkId is null but vpcId is set. + */ + @Test + public void applyRulesVpcRuleResolvesNetworkViaVpcManager() throws ResourceUnavailableException { + long vpcId = 20L; + VpcVO vpc = mock(VpcVO.class); + when(vpcDao.findById(vpcId)).thenReturn(vpc); + + FirewallRule rule = makeRule(null, vpcId, FirewallRule.Purpose.Firewall, FirewallRule.TrafficType.Ingress); + NetworkRuleApplier applier = mock(NetworkRuleApplier.class); + + stubIpAssocHelpers(); + + boolean result = ipAddressManager.applyRules( + Collections.singletonList(rule), FirewallRule.Purpose.Firewall, applier, false); + + assertTrue(result); + verify(vpcDao).findById(vpcId); + verify(applier).applyRules(null, vpc, FirewallRule.Purpose.Firewall, Collections.singletonList(rule)); + } + + + /** + * Test: For VPC egress firewall rules, IP collection should be skipped. + */ + @Test + public void applyRulesVpcEgressFirewallRuleSkipsIpCollection() throws ResourceUnavailableException { + long vpcId = 20L; + VpcVO vpc = mock(VpcVO.class); + when(vpcDao.findById(vpcId)).thenReturn(vpc); + + FirewallRule rule = makeRule(null, vpcId, FirewallRule.Purpose.Firewall, FirewallRule.TrafficType.Egress); + NetworkRuleApplier applier = mock(NetworkRuleApplier.class); + + stubIpAssocHelpers(); + + boolean result = ipAddressManager.applyRules( + Collections.singletonList(rule), FirewallRule.Purpose.Firewall, applier, false); + + assertTrue(result); + verify(ipAddressDao, never()).listByAssociatedVpc(anyLong(), any()); + verify(applier).applyRules(null, vpc, FirewallRule.Purpose.Firewall, Collections.singletonList(rule)); + } + + /** + * Test: VPC ingress firewall rules collect public IPs from VPC (listByAssociatedVpc), + * NOT from network (listByAssociatedNetwork). + */ + @Test + public void applyRulesVpcIngressRuleCollectsIpsFromVpcNotNetwork() throws ResourceUnavailableException { + long vpcId = 20L; + VpcVO vpc = mock(VpcVO.class); + when(vpcDao.findById(vpcId)).thenReturn(vpc); + + stubIpAssocHelpers(); + + NetworkRuleApplier applier = mock(NetworkRuleApplier.class); + FirewallRule rule = makeRule(null, vpcId, FirewallRule.Purpose.Firewall, FirewallRule.TrafficType.Ingress); + + ipAddressManager.applyRules(Collections.singletonList(rule), FirewallRule.Purpose.Firewall, applier, false); + + verify(applier).applyRules(null, vpc, FirewallRule.Purpose.Firewall, Collections.singletonList(rule)); + verify(ipAddressDao, never()).listByAssociatedVpc(vpcId, null); + verify(ipAddressDao, never()).listByAssociatedNetwork(anyLong(), any()); + } + + /** + * Test: Error handling respects continueOnError flag. + * When continueOnError=true, exceptions are caught and false is returned. + */ + @Test + public void applyRulesVpcRuleErrorHandlingWithContinueOnErrorTrue() throws ResourceUnavailableException { + long vpcId = 20L; + VpcVO vpc = mock(VpcVO.class); + when(vpcDao.findById(vpcId)).thenReturn(vpc); + + stubIpAssocHelpers(); + + NetworkRuleApplier applier = mock(NetworkRuleApplier.class); + when(applier.applyRules(any(), any(), any(), any())).thenThrow(new ResourceUnavailableException("test", Network.class, 0L)); + + FirewallRule rule = makeRule(null, vpcId, FirewallRule.Purpose.Firewall, FirewallRule.TrafficType.Ingress); + + boolean result = ipAddressManager.applyRules( + Collections.singletonList(rule), FirewallRule.Purpose.Firewall, applier, true); + + assertFalse(result); + } } diff --git a/server/src/test/java/com/cloud/network/element/VpcVirtualRouterElementTest.java b/server/src/test/java/com/cloud/network/element/VpcVirtualRouterElementTest.java index 20ddb39d9432..a9928c487870 100644 --- a/server/src/test/java/com/cloud/network/element/VpcVirtualRouterElementTest.java +++ b/server/src/test/java/com/cloud/network/element/VpcVirtualRouterElementTest.java @@ -19,10 +19,13 @@ import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.DataCenterDao; import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.Network; +import com.cloud.network.NetworkModel; import com.cloud.network.RemoteAccessVpn; import com.cloud.network.VpnUser; import com.cloud.network.router.VpcVirtualNetworkApplianceManagerImpl; import com.cloud.network.vpc.Vpc; +import com.cloud.network.vpc.VpcManager; import com.cloud.network.vpc.dao.VpcDao; import com.cloud.utils.db.EntityManager; import com.cloud.vm.DomainRouterVO; @@ -43,7 +46,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -60,6 +65,12 @@ public class VpcVirtualRouterElementTest { @Mock EntityManager _entityMgr; + @Mock + NetworkModel _networkMdl; + + @Mock + VpcManager _vpcMgr; + @Mock NetworkTopologyContext networkTopologyContext; @@ -188,4 +199,19 @@ public void testApplyVpnUsersException2() { verify(remoteAccessVpn, times(1)).getVpcId(); } + + @Test + public void testCanHandleFirewallUsesVpcCapability() { + final Network network = Mockito.mock(Network.class); + + when(_networkMdl.getPhysicalNetworkId(network)).thenReturn(1L); + when(network.getId()).thenReturn(200L); + when(network.getVpcId()).thenReturn(100L); + when(_networkMdl.isProviderEnabledInPhysicalNetwork(1L, Network.Provider.VPCVirtualRouter.getName())).thenReturn(true); + when(_networkMdl.isProviderSupportServiceInNetwork(200L, Network.Service.Firewall, Network.Provider.VPCVirtualRouter)).thenReturn(true); + + assertTrue(vpcVirtualRouterElement.canHandle(network, Network.Service.Firewall)); + verify(_networkMdl).isProviderSupportServiceInNetwork(200L, Network.Service.Firewall, Network.Provider.VPCVirtualRouter); + verify(_vpcMgr, never()).isProviderSupportServiceInVpc(100L, Network.Service.Firewall, Network.Provider.VPCVirtualRouter); + } } diff --git a/server/src/test/java/com/cloud/network/firewall/FirewallManagerTest.java b/server/src/test/java/com/cloud/network/firewall/FirewallManagerTest.java index bacef85479a2..e35fb479ba69 100644 --- a/server/src/test/java/com/cloud/network/firewall/FirewallManagerTest.java +++ b/server/src/test/java/com/cloud/network/firewall/FirewallManagerTest.java @@ -17,27 +17,36 @@ package com.cloud.network.firewall; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.NetworkRuleConflictException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.IpAddressManager; import com.cloud.network.Network; +import com.cloud.network.Network.Capability; +import com.cloud.network.Network.Service; import com.cloud.network.NetworkModel; import com.cloud.network.NetworkRuleApplier; import com.cloud.network.dao.FirewallRulesDao; +import com.cloud.network.dao.IPAddressDao; +import com.cloud.network.dao.IPAddressVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; import com.cloud.network.element.FirewallServiceProvider; import com.cloud.network.element.VirtualRouterElement; import com.cloud.network.element.VpcVirtualRouterElement; import com.cloud.network.rules.FirewallRule; +import com.cloud.network.rules.FirewallRule.FirewallRuleType; import com.cloud.network.rules.FirewallRule.Purpose; import com.cloud.network.rules.FirewallRuleVO; import com.cloud.network.vpc.Vpc; import com.cloud.network.vpc.VpcManager; +import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.DomainManager; import com.cloud.utils.component.ComponentContext; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -53,12 +62,18 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -79,9 +94,13 @@ public class FirewallManagerTest { @Mock IpAddressManager _ipAddrMgr; @Mock + RoutedIpv4Manager routedIpv4Manager; + @Mock FirewallRulesDao _firewallDao; @Mock NetworkDao _networkDao; + @Mock + IPAddressDao _ipAddressDao; @Spy @InjectMocks @@ -115,7 +134,7 @@ public void tearDown() throws Exception { } private FirewallRule createFirewallRule(int startPort, int endPort, Purpose purpose) { - return new FirewallRuleVO("xid", 1L, startPort, endPort, "TCP", 2, 3, 4, purpose, new ArrayList<>(), + return new FirewallRuleVO("xid", 1L, startPort, endPort, "TCP", 2L, 3, 4, purpose, new ArrayList<>(), new ArrayList<>(), 5, 6, null, FirewallRule.TrafficType.Ingress); } @@ -332,4 +351,376 @@ public void checkIfRulesHaveConflictingPortRangesTestBothRulesArePortForwardingA Assert.assertFalse(result); } + + @Test + public void testValidateFirewallRuleVpcWithoutAssociatedNetworkUsesVpcCapabilities() { + final Account caller = Mockito.mock(Account.class); + final IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + final NetworkVO network = Mockito.mock(NetworkVO.class); + final FirewallServiceProvider firewallServiceProvider = Mockito.mock(FirewallServiceProvider.class); + final Map firewallCaps = new HashMap<>(); + final Map> capabilities = new HashMap<>(); + + firewallCaps.put(Capability.SupportedTrafficDirection, "ingress, egress"); + firewallCaps.put(Capability.SupportedProtocols, "tcp,udp,icmp"); + firewallCaps.put(Capability.SupportedEgressProtocols, "tcp,udp,icmp"); + capabilities.put(Service.Firewall, firewallCaps); + + when(ipAddress.getVpcId()).thenReturn(10L); + when(_networkModel.getNetwork(2L)).thenReturn(network); + when(network.getVpcId()).thenReturn(10L); + when(routedIpv4Manager.isVirtualRouterGateway(network)).thenReturn(false); + when(firewallServiceProvider.getProvider()).thenReturn(Network.Provider.VPCVirtualRouter); + when(firewallServiceProvider.getCapabilities()).thenReturn(capabilities); + when(_vpcMgr.isProviderSupportServiceInVpc(10L, Service.Firewall, Network.Provider.VPCVirtualRouter)).thenReturn(true); + _firewallMgr._firewallElements = List.of(firewallServiceProvider); + + _firewallMgr.validateFirewallRule(caller, ipAddress, 80, 80, "tcp", Purpose.Firewall, FirewallRuleType.User, 2L, FirewallRule.TrafficType.Ingress); + + verify(_networkModel, Mockito.never()).getNetworkServiceCapabilities(Mockito.anyLong(), Mockito.eq(Service.Firewall)); + } + + @Test + public void testIsVpcIpAddressReturnsTrueWhenVpcIdPresent() { + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + when(ipAddress.getVpcId()).thenReturn(5L); + Assert.assertTrue(_firewallMgr.isVpcIpAddress(ipAddress)); + } + + @Test + public void testIsVpcIpAddressReturnsFalseWhenVpcIdNull() { + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + when(ipAddress.getVpcId()).thenReturn(null); + Assert.assertFalse(_firewallMgr.isVpcIpAddress(ipAddress)); + } + + @Test + public void testValidateFirewallRuleForIsolatedIpReturnsNetworkId() { + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + when(ipAddress.getAssociatedWithNetworkId()).thenReturn(42L); + Long result = _firewallMgr.validateFirewallRuleForIsolatedIp(ipAddress); + Assert.assertEquals(Long.valueOf(42L), result); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateFirewallRuleForIsolatedIpThrowsWhenNotAssociated() { + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + when(ipAddress.getAssociatedWithNetworkId()).thenReturn(null); + _firewallMgr.validateFirewallRuleForIsolatedIp(ipAddress); + } + + @Test + public void testValidateFirewallRuleForVpcIpReturnsNetworkId() { + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + Long result = _firewallMgr.validateFirewallRuleForVpcIp(ipAddress, 99L); + Assert.assertEquals(Long.valueOf(99L), result); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateFirewallRuleForVpcIpThrowsWhenNetworkIdNull() { + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + _firewallMgr.validateFirewallRuleForVpcIp(ipAddress, null); + } + + @Test + public void testGetFirewallServiceCapabilitiesForNonVpcNetworkUsesNetworkModel() { + NetworkVO network = Mockito.mock(NetworkVO.class); + when(network.getId()).thenReturn(1L); + when(network.getVpcId()).thenReturn(null); + Map caps = new HashMap<>(); + caps.put(Capability.SupportedProtocols, "tcp,udp"); + when(_networkModel.getNetworkServiceCapabilities(1L, Service.Firewall)).thenReturn(caps); + + Map result = _firewallMgr.getFirewallServiceCapabilities(network); + + Assert.assertEquals(caps, result); + verify(_networkModel, times(1)).getNetworkServiceCapabilities(1L, Service.Firewall); + } + + @Test + public void testGetFirewallServiceCapabilitiesForVpcNetworkUsesVpcProvider() { + NetworkVO network = Mockito.mock(NetworkVO.class); + FirewallServiceProvider fwProvider = Mockito.mock(FirewallServiceProvider.class); + Map firewallCaps = new HashMap<>(); + firewallCaps.put(Capability.SupportedProtocols, "tcp,udp,icmp"); + Map> providerCapabilities = new HashMap<>(); + providerCapabilities.put(Service.Firewall, firewallCaps); + + when(network.getVpcId()).thenReturn(10L); + when(fwProvider.getProvider()).thenReturn(Network.Provider.VPCVirtualRouter); + when(fwProvider.getCapabilities()).thenReturn(providerCapabilities); + when(_vpcMgr.isProviderSupportServiceInVpc(10L, Service.Firewall, Network.Provider.VPCVirtualRouter)).thenReturn(true); + _firewallMgr._firewallElements = List.of(fwProvider); + + Map result = _firewallMgr.getFirewallServiceCapabilities(network); + + Assert.assertEquals(firewallCaps, result); + verify(_networkModel, never()).getNetworkServiceCapabilities(Mockito.anyLong(), Mockito.eq(Service.Firewall)); + } + + @Test + public void testGetFirewallServiceCapabilitiesForVpcNetworkFallsBackToNetworkModelWhenNoProvider() { + NetworkVO network = Mockito.mock(NetworkVO.class); + FirewallServiceProvider fwProvider = Mockito.mock(FirewallServiceProvider.class); + Map fallbackCaps = new HashMap<>(); + + when(network.getId()).thenReturn(1L); + when(network.getVpcId()).thenReturn(10L); + when(fwProvider.getProvider()).thenReturn(Network.Provider.VPCVirtualRouter); + when(_vpcMgr.isProviderSupportServiceInVpc(10L, Service.Firewall, Network.Provider.VPCVirtualRouter)).thenReturn(false); + when(_networkModel.getNetworkServiceCapabilities(1L, Service.Firewall)).thenReturn(fallbackCaps); + _firewallMgr._firewallElements = List.of(fwProvider); + + Map result = _firewallMgr.getFirewallServiceCapabilities(network); + + Assert.assertEquals(fallbackCaps, result); + verify(_networkModel, times(1)).getNetworkServiceCapabilities(1L, Service.Firewall); + } + + @Test + public void testGetFirewallServiceCapabilitiesForVpcReturnsCapabilitiesWhenProviderSupports() { + FirewallServiceProvider fwProvider = Mockito.mock(FirewallServiceProvider.class); + Map firewallCaps = new HashMap<>(); + firewallCaps.put(Capability.SupportedProtocols, "tcp,udp"); + Map> caps = new HashMap<>(); + caps.put(Service.Firewall, firewallCaps); + + when(fwProvider.getProvider()).thenReturn(Network.Provider.VPCVirtualRouter); + when(fwProvider.getCapabilities()).thenReturn(caps); + when(_vpcMgr.isProviderSupportServiceInVpc(10L, Service.Firewall, Network.Provider.VPCVirtualRouter)).thenReturn(true); + _firewallMgr._firewallElements = List.of(fwProvider); + + Map result = _firewallMgr.getFirewallServiceCapabilitiesForVpc(10L); + + Assert.assertNotNull(result); + Assert.assertEquals("tcp,udp", result.get(Capability.SupportedProtocols)); + } + + @Test + public void testGetFirewallServiceCapabilitiesForVpcReturnsNullWhenNoProviderSupports() { + FirewallServiceProvider fwProvider = Mockito.mock(FirewallServiceProvider.class); + when(fwProvider.getProvider()).thenReturn(Network.Provider.VPCVirtualRouter); + when(_vpcMgr.isProviderSupportServiceInVpc(10L, Service.Firewall, Network.Provider.VPCVirtualRouter)).thenReturn(false); + _firewallMgr._firewallElements = List.of(fwProvider); + + Map result = _firewallMgr.getFirewallServiceCapabilitiesForVpc(10L); + + Assert.assertNull(result); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateFirewallRuleForVpcThrowsOnInvalidStartPort() { + Account caller = Mockito.mock(Account.class); + _firewallMgr.validateFirewallRuleForVpc(caller, null, -1, 80, "tcp", Purpose.Firewall, FirewallRuleType.User, 10L, FirewallRule.TrafficType.Ingress); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateFirewallRuleForVpcThrowsOnInvalidEndPort() { + Account caller = Mockito.mock(Account.class); + _firewallMgr.validateFirewallRuleForVpc(caller, null, 80, 70000, "tcp", Purpose.Firewall, FirewallRuleType.User, 10L, FirewallRule.TrafficType.Ingress); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateFirewallRuleForVpcThrowsWhenStartPortGreaterThanEndPort() { + Account caller = Mockito.mock(Account.class); + _firewallMgr.validateFirewallRuleForVpc(caller, null, 200, 100, "tcp", Purpose.Firewall, FirewallRuleType.User, 10L, FirewallRule.TrafficType.Ingress); + } + + @Test + public void testValidateFirewallRuleForVpcSystemTypeWithNullIpReturnsEarly() { + // System rule type + null IP should return without further validation + Account caller = Mockito.mock(Account.class); + // Should not throw even though vpcId checks come after this + _firewallMgr.validateFirewallRuleForVpc(caller, null, 80, 80, "tcp", Purpose.Firewall, FirewallRuleType.System, 10L, FirewallRule.TrafficType.Ingress); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateFirewallRuleForVpcThrowsWhenVpcIdNullAndNotSystemRule() { + Account caller = Mockito.mock(Account.class); + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + _firewallMgr.validateFirewallRuleForVpc(caller, ipAddress, 80, 80, "tcp", Purpose.Firewall, FirewallRuleType.User, null, FirewallRule.TrafficType.Ingress); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateFirewallRuleForVpcThrowsWhenActiveVpcNotFound() { + Account caller = Mockito.mock(Account.class); + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + when(_vpcMgr.getActiveVpc(10L)).thenReturn(null); + _firewallMgr.validateFirewallRuleForVpc(caller, ipAddress, 80, 80, "tcp", Purpose.Firewall, FirewallRuleType.User, 10L, FirewallRule.TrafficType.Ingress); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateFirewallRuleForVpcThrowsWhenFirewallServiceNotSupported() { + Account caller = Mockito.mock(Account.class); + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + Vpc vpc = Mockito.mock(Vpc.class); + when(_vpcMgr.getActiveVpc(10L)).thenReturn(vpc); + _firewallMgr._firewallElements = Collections.emptyList(); + + _firewallMgr.validateFirewallRuleForVpc(caller, ipAddress, 80, 80, "tcp", Purpose.Firewall, FirewallRuleType.User, 10L, FirewallRule.TrafficType.Ingress); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateFirewallRuleForVpcThrowsOnUnsupportedProtocol() { + Account caller = Mockito.mock(Account.class); + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + Vpc vpc = Mockito.mock(Vpc.class); + FirewallServiceProvider fwProvider = Mockito.mock(FirewallServiceProvider.class); + Map firewallCaps = new HashMap<>(); + firewallCaps.put(Capability.SupportedProtocols, "tcp,udp"); + firewallCaps.put(Capability.SupportedTrafficDirection, "ingress,egress"); + Map> caps = new HashMap<>(); + caps.put(Service.Firewall, firewallCaps); + + when(_vpcMgr.getActiveVpc(10L)).thenReturn(vpc); + when(fwProvider.getProvider()).thenReturn(Network.Provider.VPCVirtualRouter); + when(fwProvider.getCapabilities()).thenReturn(caps); + when(_vpcMgr.isProviderSupportServiceInVpc(10L, Service.Firewall, Network.Provider.VPCVirtualRouter)).thenReturn(true); + _firewallMgr._firewallElements = List.of(fwProvider); + + _firewallMgr.validateFirewallRuleForVpc(caller, ipAddress, 80, 80, "gre", Purpose.Firewall, FirewallRuleType.User, 10L, FirewallRule.TrafficType.Ingress); + } + + @Test + public void testValidateFirewallRuleForVpcSucceedsWithSupportedProtocolAndTrafficType() { + Account caller = Mockito.mock(Account.class); + IPAddressVO ipAddress = Mockito.mock(IPAddressVO.class); + Vpc vpc = Mockito.mock(Vpc.class); + FirewallServiceProvider fwProvider = Mockito.mock(FirewallServiceProvider.class); + Map firewallCaps = new HashMap<>(); + firewallCaps.put(Capability.SupportedProtocols, "tcp,udp,icmp"); + firewallCaps.put(Capability.SupportedTrafficDirection, "ingress,egress"); + Map> caps = new HashMap<>(); + caps.put(Service.Firewall, firewallCaps); + + when(_vpcMgr.getActiveVpc(10L)).thenReturn(vpc); + when(fwProvider.getProvider()).thenReturn(Network.Provider.VPCVirtualRouter); + when(fwProvider.getCapabilities()).thenReturn(caps); + when(_vpcMgr.isProviderSupportServiceInVpc(10L, Service.Firewall, Network.Provider.VPCVirtualRouter)).thenReturn(true); + _firewallMgr._firewallElements = List.of(fwProvider); + + // Should not throw + _firewallMgr.validateFirewallRuleForVpc(caller, ipAddress, 80, 80, "tcp", Purpose.Firewall, FirewallRuleType.User, 10L, FirewallRule.TrafficType.Ingress); + + verify(_accountMgr, times(1)).checkAccess(caller, null, true, ipAddress); + } + + @Test + public void testCreateFirewallRuleRoutesToVpcWhenVpcIdProvided() throws NetworkRuleConflictException { + Account caller = Mockito.mock(Account.class); + FirewallRule vpcRule = Mockito.mock(FirewallRule.class); + + doReturn(vpcRule).when(_firewallMgr).createFirewallRuleForVpc( + Mockito.anyLong(), Mockito.eq(caller), Mockito.any(), Mockito.anyInt(), Mockito.anyInt(), + Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(FirewallRuleType.class), Mockito.anyLong(), + Mockito.any(FirewallRule.TrafficType.class), Mockito.anyBoolean()); + + _firewallMgr.createFirewallRule(1L, caller, "xid", 80, 80, "tcp", + Collections.singletonList("0.0.0.0/0"), null, null, null, null, + FirewallRuleType.User, null, 10L, FirewallRule.TrafficType.Ingress, true); + + verify(_firewallMgr, times(1)).createFirewallRuleForVpc( + Mockito.anyLong(), Mockito.eq(caller), Mockito.any(), Mockito.anyInt(), Mockito.anyInt(), + Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(FirewallRuleType.class), Mockito.anyLong(), + Mockito.any(FirewallRule.TrafficType.class), Mockito.anyBoolean()); + + verify(_firewallMgr, never()).createFirewallRuleForNonVPC( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + @Test + public void testCreateFirewallRuleRoutesToNonVpcWhenVpcIdNull() throws NetworkRuleConflictException { + Account caller = Mockito.mock(Account.class); + FirewallRule nonVpcRule = Mockito.mock(FirewallRule.class); + + doReturn(nonVpcRule).when(_firewallMgr).createFirewallRuleForNonVPC( + Mockito.any(), Mockito.eq(caller), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(FirewallRuleType.class), Mockito.anyLong(), + Mockito.any(FirewallRule.TrafficType.class), Mockito.anyBoolean()); + + _firewallMgr.createFirewallRule(null, caller, "xid", 80, 80, "tcp", + Collections.singletonList("0.0.0.0/0"), null, null, null, null, + FirewallRuleType.User, 2L, null, FirewallRule.TrafficType.Ingress, true); + + verify(_firewallMgr, times(1)).createFirewallRuleForNonVPC( + Mockito.any(), Mockito.eq(caller), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(FirewallRuleType.class), Mockito.anyLong(), + Mockito.any(FirewallRule.TrafficType.class), Mockito.anyBoolean()); + + verify(_firewallMgr, never()).createFirewallRuleForVpc( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + @Test + public void testApplyRulesForVpcNetworkUsesVpcProviderCheck() throws ResourceUnavailableException { + FirewallManagerImpl firewallMgr = new FirewallManagerImpl(); + firewallMgr._networkModel = _networkModel; + firewallMgr._vpcMgr = _vpcMgr; + + Network network = Mockito.mock(Network.class); + FirewallServiceProvider fwProvider = Mockito.mock(FirewallServiceProvider.class); + List rules = new ArrayList<>(); + FirewallRuleVO rule = new FirewallRuleVO("rule1", 1L, 80, 80, "tcp", 1L, 2, 3, Purpose.Firewall, + Collections.emptyList(), Collections.emptyList(), null, null, null, FirewallRule.TrafficType.Ingress); + rules.add(rule); + + when(network.getVpcId()).thenReturn(10L); + when(fwProvider.getProvider()).thenReturn(Network.Provider.VPCVirtualRouter); + when(_vpcMgr.isProviderSupportServiceInVpc(10L, Service.Firewall, Network.Provider.VPCVirtualRouter)).thenReturn(true); + when(fwProvider.applyFWRules(Mockito.eq(network), Mockito.anyList())).thenReturn(true); + firewallMgr._firewallElements = List.of(fwProvider); + + boolean result = firewallMgr.applyRules(network, Purpose.Firewall, rules); + + Assert.assertTrue(result); + verify(_vpcMgr, times(1)).isProviderSupportServiceInVpc(10L, Service.Firewall, Network.Provider.VPCVirtualRouter); + verify(_networkModel, never()).isProviderSupportServiceInNetwork(Mockito.anyLong(), Mockito.eq(Service.Firewall), Mockito.any()); + } + + @Test + public void testApplyRulesForNonVpcNetworkUsesNetworkModelProviderCheck() throws ResourceUnavailableException { + FirewallManagerImpl firewallMgr = new FirewallManagerImpl(); + firewallMgr._networkModel = _networkModel; + firewallMgr._vpcMgr = _vpcMgr; + + Network network = Mockito.mock(Network.class); + FirewallServiceProvider fwProvider = Mockito.mock(FirewallServiceProvider.class); + List rules = new ArrayList<>(); + FirewallRuleVO rule = new FirewallRuleVO("rule1", 1L, 80, 80, "tcp", 1L, 2, 3, Purpose.Firewall, + Collections.emptyList(), Collections.emptyList(), null, null, null, FirewallRule.TrafficType.Ingress); + rules.add(rule); + + when(network.getId()).thenReturn(1L); + when(network.getVpcId()).thenReturn(null); + when(fwProvider.getProvider()).thenReturn(Network.Provider.VirtualRouter); + when(_networkModel.isProviderSupportServiceInNetwork(1L, Service.Firewall, Network.Provider.VirtualRouter)).thenReturn(true); + when(fwProvider.applyFWRules(Mockito.eq(network), Mockito.anyList())).thenReturn(true); + firewallMgr._firewallElements = List.of(fwProvider); + + boolean result = firewallMgr.applyRules(network, Purpose.Firewall, rules); + + Assert.assertTrue(result); + verify(_networkModel, times(1)).isProviderSupportServiceInNetwork(1L, Service.Firewall, Network.Provider.VirtualRouter); + verify(_vpcMgr, never()).isProviderSupportServiceInVpc(Mockito.anyLong(), Mockito.eq(Service.Firewall), Mockito.any()); + } + + @Test + public void testGetSourceIpForIngressRuleReturnsNullWhenIdIsNull() { + IPAddressVO result = _firewallMgr.getSourceIpForIngressRule(null); + Assert.assertNull(result); + } + + @Test(expected = CloudRuntimeException.class) + public void testGetSourceIpForIngressRuleReturnsNullWhenIpIsnotPresent() { + when(_ipAddressDao.findById(1L)).thenReturn(null); + _firewallMgr.getSourceIpForIngressRule(1L); + } } diff --git a/systemvm/debian/opt/cloud/bin/configure.py b/systemvm/debian/opt/cloud/bin/configure.py index bf48be66694c..77b56779d5b8 100755 --- a/systemvm/debian/opt/cloud/bin/configure.py +++ b/systemvm/debian/opt/cloud/bin/configure.py @@ -703,14 +703,139 @@ def process(self): self.add_routing_rules() return + desired_firewall_ips = set() + fw_chains_created = set() + if self.config.is_vpc() and self.config.is_vpc_firewall_enabled(): + desired_firewall_ips = self._get_desired_vpc_firewall_ips() + # Pre-create FIREWALL chains for ALL public IPs that have any active rule + # (static NAT, port forwarding, LB, or explicit firewall rule) so that the + # default DROP is always in place even before any explicit firewall rule exists. + self._ensure_vpc_firewall_chains(desired_firewall_ips, fw_chains_created) + for item in self.dbag: if item == "id": continue - if self.config.is_vpc(): + if self.config.is_vpc() and not ("purpose" in self.dbag[item] and self.dbag[item]["purpose"] == "Firewall"): self.AclDevice(self.dbag[item], self.config).create() else: + if self.config.is_vpc() and self.dbag[item].get("purpose") == "Firewall" and not self.config.is_vpc_firewall_enabled(): + continue + # Chain skeleton is already ensured by the pre-creation pass above; + # _ensure_vpc_firewall_chains is idempotent (skips IPs in fw_chains_created). + if self.config.is_vpc() and self.config.is_vpc_firewall_enabled() and self.dbag[item].get("purpose") == "Firewall": + src_ip = self.dbag[item].get("src_ip") + self._ensure_vpc_firewall_chains([src_ip], fw_chains_created) self.AclIP(self.dbag[item], self.config).create() + if self.config.is_vpc() and self.config.is_vpc_firewall_enabled(): + self._cleanup_removed_vpc_firewall_chains(desired_firewall_ips) + + def _get_desired_vpc_firewall_ips(self): + """ + Collect the full set of public IPs that should have a FIREWALL mangle chain + in a VPC with firewall capability. This includes IPs from explicit firewall + rules, forwarding/static-NAT rules, and load-balancer rules. + """ + if not self.config.is_vpc(): + return set() + + ips = set() + ips.update(self._get_firewall_rule_ips()) + ips.update(self._get_forwarding_rule_ips()) + ips.update(self._get_loadbalancer_ips()) + return ips + + def _get_firewall_rule_ips(self): + """Return public IPs that have explicit firewall rules in this data bag.""" + ips = set() + for item in self.dbag: + if item == "id": + continue + rule = self.dbag[item] + if rule.get("purpose") == "Firewall": + src_ip = rule.get("src_ip") + if src_ip: + ips.add(src_ip) + return ips + + def _get_forwarding_rule_ips(self): + """ + Return public IPs from the forwardingrules bag (static NAT and port forwarding). + That bag is keyed by public IP, so each key (other than 'id') is a public IP. + """ + ips = set() + try: + fwd_bag = CsDataBag("forwardingrules", self.config) + for public_ip in fwd_bag.get_bag(): + if public_ip == "id": + continue + ips.add(public_ip) + except Exception as e: + logging.debug("Could not load forwardingrules for VPC firewall chain collection: %s", e) + return ips + + def _get_loadbalancer_ips(self): + """ + Return public IPs from the loadbalancer bag. + add_rules entries are formatted as 'ip:port', so the IP is the first segment. + """ + ips = set() + try: + lb_bag = CsDataBag("loadbalancer", self.config) + lb_data = lb_bag.get_bag() + if "config" in lb_data and lb_data["config"]: + for rule_str in lb_data["config"][0].get("add_rules", []): + ip = rule_str.split(":")[0] + if ip: + ips.add(ip) + except Exception as e: + logging.debug("Could not load loadbalancer for VPC firewall chain collection: %s", e) + return ips + + def _ensure_vpc_firewall_chains(self, source_ips, fw_chains_created): + fw = self.config.get_fw() + for src_ip in source_ips: + if not src_ip or src_ip in fw_chains_created: + continue + fw.append(["mangle", "front", + "-A PREROUTING -d %s/32 -j FIREWALL_%s" % (src_ip, src_ip)]) + fw.append(["mangle", "front", + "-A FIREWALL_%s -m state --state RELATED,ESTABLISHED -j RETURN" % src_ip]) + fw.append(["mangle", "", + "-A FIREWALL_%s -j DROP" % src_ip]) + fw_chains_created.add(src_ip) + + def _cleanup_removed_vpc_firewall_chains(self, desired_firewall_ips): + try: + mangle_save = CsHelper.execute("iptables-save -t mangle") + existing_firewall_ips = [] + for line in mangle_save: + if line.startswith(":FIREWALL_"): + chain = line.split(" ")[0][1:] + existing_firewall_ips.append(chain.replace("FIREWALL_", "", 1)) + + for src_ip in existing_firewall_ips: + if src_ip in desired_firewall_ips: + continue + self._delete_vpc_firewall_chain(src_ip) + except Exception as e: + logging.debug("Failed VPC firewall chain cleanup: %s", e) + + def _delete_vpc_firewall_chain(self, src_ip): + chain = "FIREWALL_%s" % src_ip + try: + prerouting_rules = CsHelper.execute("iptables -t mangle -S PREROUTING") + for rule in prerouting_rules: + if ("-d %s/32" % src_ip) in rule and ("-j %s" % chain) in rule: + delete_rule = rule.replace("-A PREROUTING", "-D PREROUTING", 1) + CsHelper.execute2("iptables -t mangle %s" % delete_rule, False) + + CsHelper.execute2("iptables -t mangle -F %s" % chain, False) + CsHelper.execute2("iptables -t mangle -X %s" % chain, False) + logging.info("Deleted VPC firewall chain %s as last firewall rule was removed", chain) + except Exception as e: + logging.debug("Failed deleting VPC firewall chain %s: %s", chain, e) + class CsIpv6Firewall(CsDataBag): """ Deal with IPv6 Firewall diff --git a/systemvm/debian/opt/cloud/bin/cs/CsAddress.py b/systemvm/debian/opt/cloud/bin/cs/CsAddress.py index 37ca8979edc7..fe95808f7d32 100755 --- a/systemvm/debian/opt/cloud/bin/cs/CsAddress.py +++ b/systemvm/debian/opt/cloud/bin/cs/CsAddress.py @@ -680,6 +680,7 @@ def fw_vpcrouter(self): self.fw.append(["filter", "", "-P INPUT DROP"]) self.fw.append(["filter", "", "-P FORWARD DROP"]) + def fw_router_routing(self): if self.config.is_vpc() or not self.config.is_routed(): return diff --git a/systemvm/debian/opt/cloud/bin/cs/CsConfig.py b/systemvm/debian/opt/cloud/bin/cs/CsConfig.py index 549b08f75fc6..5218e8111161 100755 --- a/systemvm/debian/opt/cloud/bin/cs/CsConfig.py +++ b/systemvm/debian/opt/cloud/bin/cs/CsConfig.py @@ -155,3 +155,6 @@ def get_egress_table(self): def has_public_network(self): return self.cmdline().idata().get('has_public_network', 'true') == 'true' + + def is_vpc_firewall_enabled(self): + return self.cmdline().idata().get('vpc_firewall_enabled', 'false') == 'true' diff --git a/test/integration/smoke/test_vpc_firewall_rules.py b/test/integration/smoke/test_vpc_firewall_rules.py new file mode 100644 index 000000000000..6aae5f13fbcd --- /dev/null +++ b/test/integration/smoke/test_vpc_firewall_rules.py @@ -0,0 +1,187 @@ +# 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. + +"""Smoke tests for firewall rules on VPC public IPs.""" + +from nose.plugins.attrib import attr + +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import Account, FireWallRule, Network, NetworkOffering, PublicIPAddress, VPC, VpcOffering +from marvin.lib.common import get_domain, get_zone, list_publicIP +from marvin.lib.utils import cleanup_resources, wait_until + + +class TestVpcFirewallRules(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.testClient = super(TestVpcFirewallRules, cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.domain = get_domain(cls.apiclient) + cls._cleanup = [] + + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=cls.domain.id + ) + cls._cleanup.append(cls.account) + + cls.services["vpc_offering"]["supportedservices"] = ( + "Vpn,Dhcp,Dns,SourceNat,Lb,UserData,StaticNat," + "NetworkACL,PortForwarding,Firewall" + ) + cls.services["vpc_offering"]["serviceProviderList"] = { + "Vpn": "VpcVirtualRouter", + "Dhcp": "VpcVirtualRouter", + "Dns": "VpcVirtualRouter", + "SourceNat": "VpcVirtualRouter", + "Lb": "VpcVirtualRouter", + "UserData": "VpcVirtualRouter", + "StaticNat": "VpcVirtualRouter", + "NetworkACL": "VpcVirtualRouter", + "PortForwarding": "VpcVirtualRouter", + "Firewall": "VpcVirtualRouter" + } + + cls.vpc_offering = VpcOffering.create( + cls.apiclient, + cls.services["vpc_offering"] + ) + cls.vpc_offering.update(cls.apiclient, state="Enabled") + cls._cleanup.append(cls.vpc_offering) + + network_offering = NetworkOffering.list( + cls.apiclient, + name="DefaultIsolatedNetworkOfferingForVpcNetworks" + ) + cls.assertTrue( + network_offering is not None and len(network_offering) > 0, + "No VPC tier network offering found" + ) + cls.network_offering = network_offering[0] + cls.services["vpc"]["cidr"] = "10.20.30.0/24" + cls.vpc = VPC.create( + cls.apiclient, + cls.services["vpc"], + vpcofferingid=cls.vpc_offering.id, + zoneid=cls.zone.id, + account=cls.account.name, + domainid=cls.account.domainid + ) + + cls.tier = Network.create( + cls.apiclient, + services={ + "name": "vpc-fw-tier", + "displaytext": "vpc-fw-tier" + }, + accountid=cls.account.name, + domainid=cls.account.domainid, + networkofferingid=cls.network_offering.id, + zoneid=cls.zone.id, + vpcid=cls.vpc.id, + gateway="10.20.30.1", + netmask="255.255.255.0" + ) + + @classmethod + def tearDownClass(cls): + try: + cleanup_resources(cls.apiclient, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup: %s" % e) + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.cleanup = [] + + def tearDown(self): + cleanup_resources(self.apiclient, self.cleanup) + + def _wait_for_firewall_rule(self, rule_id): + rules = FireWallRule.list(self.apiclient, id=rule_id, listall=True) + if rules and len(rules) == 1: + return True, rules[0] + return False, None + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="false") + def test_01_create_firewall_rule_on_vpc_public_ip(self): + """Verify firewall rule can be created and listed on a dedicated VPC public IP.""" + public_ip = PublicIPAddress.create( + self.apiclient, + zoneid=self.zone.id, + accountid=self.account.name, + domainid=self.account.domainid, + vpcid=self.vpc.id + ) + self.cleanup.append(public_ip) + + firewall_rule = FireWallRule.create( + self.apiclient, + ipaddressid=public_ip.ipaddress.id, + protocol="tcp", + cidrlist=["0.0.0.0/0"], + startport=19090, + endport=19090, + vpcid=self.vpc.id + ) + self.cleanup.insert(0, firewall_rule) + + result, listed_rule = wait_until(2, 10, self._wait_for_firewall_rule, firewall_rule.id) + self.assertTrue(result, "Firewall rule was not listed for the VPC public IP") + self.assertEqual(listed_rule.id, firewall_rule.id) + self.assertEqual(listed_rule.ipaddressid, public_ip.ipaddress.id) + self.assertEqual(listed_rule.vpcid, self.vpc.id) + self.assertEqual(listed_rule.protocol.lower(), "tcp") + self.assertEqual(int(listed_rule.startport), 19090) + self.assertEqual(int(listed_rule.endport), 19090) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="false") + def test_02_create_firewall_rule_on_vpc_source_nat_ip(self): + """Verify firewall rule can be created and listed on the VPC source NAT IP.""" + source_nat_ips = list_publicIP( + self.apiclient, + vpcid=self.vpc.id, + listall=True, + issourcenat=True + ) + self.assertTrue(source_nat_ips is not None and len(source_nat_ips) > 0, + "No source NAT IP found for the VPC") + source_nat_ip = source_nat_ips[0] + + firewall_rule = FireWallRule.create( + self.apiclient, + ipaddressid=source_nat_ip.id, + protocol="tcp", + cidrlist=["0.0.0.0/0"], + startport=19443, + endport=19443, + vpcid=self.vpc.id + ) + self.cleanup.append(firewall_rule) + + result, listed_rule = wait_until(2, 10, self._wait_for_firewall_rule, firewall_rule.id) + self.assertTrue(result, "Firewall rule was not listed for the VPC source NAT IP") + self.assertEqual(listed_rule.id, firewall_rule.id) + self.assertEqual(listed_rule.ipaddressid, source_nat_ip.id) + self.assertEqual(listed_rule.vpcid, self.vpc.id) + self.assertEqual(listed_rule.protocol.lower(), "tcp") + self.assertEqual(int(listed_rule.startport), 19443) + self.assertEqual(int(listed_rule.endport), 19443) diff --git a/ui/src/views/network/PublicIpResource.vue b/ui/src/views/network/PublicIpResource.vue index 0540e7f292a8..15c423071575 100644 --- a/ui/src/views/network/PublicIpResource.vue +++ b/ui/src/views/network/PublicIpResource.vue @@ -136,23 +136,39 @@ export default { } if (this.resource && this.resource.vpcid) { const vpc = await this.fetchVpc() + const hasFirewallCapability = this.hasVpcFirewallCapability(vpc) // VPC IPs with source nat have only VPN when VPC offering conserve mode = false if (this.resource.issourcenat && vpc?.vpcofferingconservemode === false) { - this.tabs = this.defaultTabs.concat(this.$route.meta.tabs.filter(tab => tab.name === 'vpn')) + const tabs = this.defaultTabs.concat(this.$route.meta.tabs.filter(tab => tab.name === 'vpn')) + this.tabs = hasFirewallCapability ? this.addFirewallTab(tabs) : tabs return } - // VPC IPs with static nat have nothing + // VPC IPs with static nat keep existing VPN behavior; show firewall only when capability exists if (this.resource.isstaticnat) { - if (this.resource.virtualmachinetype === 'DomainRouter') { - this.tabs = this.defaultTabs.concat(this.$route.meta.tabs.filter(tab => tab.name === 'vpn')) + let tabs = this.$route.meta.tabs + if (hasFirewallCapability) { + tabs = this.addFirewallTab(tabs).map(tab => { + if (tab.name !== 'firewall') { + return tab + } + const staticNatFirewallTab = { ...tab } + delete staticNatFirewallTab.networkServiceFilter + return staticNatFirewallTab + }) + } else { + tabs = tabs.filter(tab => tab.name !== 'firewall') } + this.tabs = tabs return } - // VPC IPs don't have firewall - let tabs = this.$route.meta.tabs.filter(tab => tab.name !== 'firewall') + // VPC IPs have all tabs; firewall is shown only if VPC has firewall capability + let tabs = this.$route.meta.tabs + if (!hasFirewallCapability) { + tabs = tabs.filter(tab => tab.name !== 'firewall') + } const network = await this.fetchNetwork() if (network && network.networkofferingconservemode) { @@ -168,12 +184,12 @@ export default { this.portFWRuleCount = await this.fetchPortFWRule() this.loadBalancerRuleCount = await this.fetchLoadBalancerRule() - // VPC IPs with PF only have PF + // VPC IPs with PF only have PF (and firewall) if (this.portFWRuleCount > 0) { tabs = tabs.filter(tab => tab.name !== 'loadbalancing') } - // VPC IPs with LB rules only have LB + // VPC IPs with LB rules only have LB (and firewall) if (this.loadBalancerRuleCount > 0) { tabs = tabs.filter(tab => tab.name !== 'portforwarding') } @@ -200,6 +216,17 @@ export default { fetchAction () { this.actions = this.$route.meta.actions || [] }, + addFirewallTab (tabs) { + const firewallTab = this.$route.meta.tabs.find(tab => tab.name === 'firewall') + if (!firewallTab || tabs.some(tab => tab.name === 'firewall')) { + return tabs + } + return tabs.concat(firewallTab) + }, + hasVpcFirewallCapability (vpc) { + const services = vpc?.service || [] + return Array.isArray(services) && services.some(service => (service?.name || '').toLowerCase() === 'firewall') + }, fetchVpc () { if (!this.resource.vpcid) { return null diff --git a/ui/src/views/offering/AddNetworkOffering.vue b/ui/src/views/offering/AddNetworkOffering.vue index 1a89d2db1cb5..995b81ce68c6 100644 --- a/ui/src/views/offering/AddNetworkOffering.vue +++ b/ui/src/views/offering/AddNetworkOffering.vue @@ -946,6 +946,9 @@ export default { provider.enabled = self.isVpcCoreProvider(provider.name, svc.name) || !self.isBuiltInNetworkProvider(provider.name) } + if (svc.name === 'Firewall' && provider.name === 'VpcVirtualRouter') { + provider.enabled = false + } } else { // *** non-vpc *** provider.enabled = !['InternalLbVm', 'VpcVirtualRouter', 'Nsx', 'Netris'].includes(provider.name) } diff --git a/ui/src/views/offering/AddVpcOffering.vue b/ui/src/views/offering/AddVpcOffering.vue index 1efbfd4df1d6..780da6638f8e 100644 --- a/ui/src/views/offering/AddVpcOffering.vue +++ b/ui/src/views/offering/AddVpcOffering.vue @@ -432,6 +432,9 @@ export default { }) }, isVpcCoreProvider (providerName, serviceName) { + if (serviceName === 'Firewall') { + return ['VpcVirtualRouter'].includes(providerName) + } if (['VpcVirtualRouter', 'Netscaler', 'BigSwitchBcf', 'ConfigDrive'].includes(providerName)) { return true } @@ -540,7 +543,7 @@ export default { this.supportedServices = [] this.supportedServiceLoading = true getAPI('listSupportedNetworkServices').then(json => { - const vpcServices = ['Dhcp', 'Dns', 'Lb', 'Gateway', 'StaticNat', 'SourceNat', 'NetworkACL', 'PortForwarding', 'UserData', 'Vpn', 'Connectivity', 'CustomAction'] + const vpcServices = ['Dhcp', 'Dns', 'Lb', 'Gateway', 'StaticNat', 'SourceNat', 'NetworkACL', 'PortForwarding', 'UserData', 'Vpn', 'Connectivity', 'CustomAction', 'Firewall'] services = (json?.listsupportednetworkservicesresponse?.networkservice || []) .filter(service => vpcServices.includes(service.name)) .map(service => { @@ -575,7 +578,7 @@ export default { this.supportedServices = [] if (this.networkmode === 'ROUTED') { - services = services.filter(service => !['SourceNat', 'StaticNat', 'Lb', 'PortForwarding', 'Vpn'].includes(service.name)) + services = services.filter(service => !['SourceNat', 'StaticNat', 'Lb', 'PortForwarding', 'Vpn', 'Firewall'].includes(service.name)) } this.supportedServices = services }).catch(error => { diff --git a/ui/src/views/offering/CloneVpcOffering.vue b/ui/src/views/offering/CloneVpcOffering.vue index cecc0c600b57..6adbfafad611 100644 --- a/ui/src/views/offering/CloneVpcOffering.vue +++ b/ui/src/views/offering/CloneVpcOffering.vue @@ -523,6 +523,7 @@ export default { return [ { name: 'Dhcp', provider: [{ name: 'VpcVirtualRouter' }, { name: 'ConfigDrive' }] }, { name: 'Dns', provider: [{ name: 'VpcVirtualRouter' }, { name: 'ConfigDrive' }] }, + { name: 'Firewall', provider: [{ name: 'VpcVirtualRouter' }] }, { name: 'Lb', provider: [{ name: 'VpcVirtualRouter' }, { name: 'InternalLbVm' }] }, { name: 'Gateway', provider: [{ name: 'VpcVirtualRouter' }, { name: 'BigSwitchBcf' }] }, { name: 'StaticNat', provider: [{ name: 'VpcVirtualRouter' }, { name: 'BigSwitchBcf' }] }, From 4c0a3e14992ecf6412b064f34f2ce7d84e94e584 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 13 Jul 2026 09:40:20 +0200 Subject: [PATCH 183/205] marvin: use pycryptodome instead of PyCrypt (#13594) --- tools/marvin/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py index eb97894a9914..8360d00e5326 100644 --- a/tools/marvin/setup.py +++ b/tools/marvin/setup.py @@ -57,7 +57,7 @@ "ipmisim >= 0.7", "pytz", "retries", - "PyCrypt", + "pycryptodome", "kubernetes", "urllib3", "setuptools >= 40.3.0" From 63c142be261753314bb8d33446f20ab553aefec9 Mon Sep 17 00:00:00 2001 From: Nikolaus Eppinger Date: Mon, 13 Jul 2026 09:42:40 +0200 Subject: [PATCH 184/205] KVM: fix LUKS/volume-encryption detection for qemu-img >= 10.1.0 (#13587) qemu-img 10.1.0 changed the "qemu-img --help" supported-formats header from "Supported formats:" to "Supported image formats:". The regex in QemuImg.helpSupportsImageFormat() only matched the old header, so hostSupportsVolumeEncryption() returned false on affected hosts even though cryptsetup and the luks format were both available, blocking encrypted offerings. Make the "image" keyword optional in the regex so it matches both the legacy and current qemu-img help output. --- .../org/apache/cloudstack/utils/qemu/QemuImg.java | 5 ++++- .../apache/cloudstack/utils/qemu/QemuImgTest.java | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java index 80e44d8059a1..e51c80e521c7 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java @@ -927,7 +927,10 @@ public boolean supportsImageFormat(QemuImg.PhysicalDiskFormat format) { } protected static boolean helpSupportsImageFormat(String text, QemuImg.PhysicalDiskFormat format) { - Pattern pattern = Pattern.compile("Supported\\sformats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE); + // QEMU >= 10.1.0 changed the qemu-img --help header from + // "Supported formats:" to "Supported image formats:", so the word + // "image" must be treated as optional here. + Pattern pattern = Pattern.compile("Supported\\s(image\\s)?formats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE); return pattern.matcher(text).find(); } diff --git a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java index 5a0274257764..15f6785c1fd4 100644 --- a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java +++ b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java @@ -390,6 +390,21 @@ public void testHelpSupportsImageFormat() throws QemuImgException, LibvirtExcept Assert.assertFalse("should not support http", QemuImg.helpSupportsImageFormat(partialHelp, PhysicalDiskFormat.SHEEPDOG)); } + @Test + public void testHelpSupportsImageFormatQemu101Header() throws QemuImgException, LibvirtException { + // qemu-img 10.1.0 (e.g. RHEL 9.8: qemu-kvm-10.1.0-17.el9_8.3) changed the + // help header from "Supported formats:" to "Supported image formats:" + String help = "Supported image formats:\n" + + " blkdebug blklogwrites blkverify compress copy-before-write copy-on-read\n" + + " file ftp ftps host_cdrom host_device http https io_uring luks nbd null-aio\n" + + " null-co nvme nvme-io_uring preallocate qcow2 quorum raw rbd\n" + + " snapshot-access throttle vdi vhdx virtio-blk-vfio-pci\n" + + " virtio-blk-vhost-user virtio-blk-vhost-vdpa vmdk vpc\n"; + Assert.assertTrue("should support luks", QemuImg.helpSupportsImageFormat(help, PhysicalDiskFormat.LUKS)); + Assert.assertTrue("should support qcow2", QemuImg.helpSupportsImageFormat(help, PhysicalDiskFormat.QCOW2)); + Assert.assertFalse("should not support sheepdog", QemuImg.helpSupportsImageFormat(help, PhysicalDiskFormat.SHEEPDOG)); + } + @Test public void testCheckAndRepair() throws LibvirtException { String filename = "/tmp/" + UUID.randomUUID() + ".qcow2"; From 38b674f4426fa8a9ab72ce485b1bdad62e2f1e4f Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 13 Jul 2026 10:17:13 +0200 Subject: [PATCH 185/205] Add AGENTS.md + SECURITY.md wiring for security-model discoverability (#13554) --- AGENTS.md | 32 ++++++++++++++++++++++++++++++++ SECURITY.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 AGENTS.md create mode 100644 SECURITY.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..4469efa2f494 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ + + +# Agent Guide for Apache CloudStack + +This file is read by automated agents (security scanners, code +analyzers, AI assistants) operating on this repository. + +## Security + +Security model: [SECURITY.md](./SECURITY.md) + +Agents that scan this repository should consult `SECURITY.md` and the +threat model it links before reporting issues. + +The project-wide security threat model is linked from `SECURITY.md`. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..ba69c0860243 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ + + +# Security Policy + +## Reporting a Vulnerability + +`apache/cloudstack` follows the [Apache Software Foundation security process](https://www.apache.org/security/). Please report suspected +vulnerabilities privately to `security@apache.org`; do not open public GitHub issues or pull requests for security reports. + +For more details, see https://cloudstack.apache.org/security.html. + +## Threat Model + +What the project treats as in scope and out of scope, the security +properties it provides and disclaims, the adversary model, and how +findings are triaged are documented in the project-wide threat model: +[draft-THREAT-MODEL.md](draft-THREAT-MODEL.md). From 17e5947a6d2cea3ced8d892e13d04b4e838f8d8e Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 13 Jul 2026 11:00:37 +0200 Subject: [PATCH 186/205] server: add removed Tests for listHostsForMigrationOfVM and fix test failures --- .../server/ManagementServerImplTest.java | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index b0f274e6fc84..451e30005cf7 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -43,8 +43,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd; @@ -59,6 +61,7 @@ import org.apache.cloudstack.api.command.user.userdata.RegisterUserDataCmd; import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; import org.apache.cloudstack.framework.config.ConfigDepot; @@ -69,24 +72,45 @@ import org.apache.cloudstack.userdata.UserDataManager; import com.cloud.cpu.CPU; +import com.cloud.dc.DataCenterVO; import com.cloud.dc.Vlan.VlanType; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.deploy.DataCenterDeployment; +import com.cloud.deploy.DeploymentPlanningManager; import com.cloud.domain.dao.DomainDao; import com.cloud.api.ApiDBUtils; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.gpu.GPU; +import com.cloud.gpu.VgpuProfileVO; +import com.cloud.gpu.dao.VgpuProfileDao; import com.cloud.host.DetailVO; import com.cloud.host.Host; +import com.cloud.host.Host.Type; import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDetailsDao; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; +import com.cloud.hypervisor.kvm.dpdk.DpdkHelper; import com.cloud.network.IpAddress; import com.cloud.network.IpAddressManagerImpl; import com.cloud.network.dao.IPAddressVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.service.dao.ServiceOfferingDetailsDao; +import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOSCategoryVO; import com.cloud.storage.GuestOSVO; import com.cloud.storage.GuestOsCategory; import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.GuestOSCategoryDao; import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VolumeDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.SSHKeyPair; @@ -97,14 +121,18 @@ import com.cloud.user.dao.SSHKeyPairDao; import com.cloud.user.dao.UserDataDao; import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceDetailVO; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.agent.manager.allocator.HostAllocator; @@ -156,6 +184,42 @@ public class ManagementServerImplTest { @Mock HostDetailsDao hostDetailsDao; + @Mock + VMInstanceDao vmInstanceDao; + + @Mock + HostDao hostDao; + + @Mock + ServiceOfferingDetailsDao serviceOfferingDetailsDao; + + @Mock + VolumeDao volumeDao; + + @Mock + ServiceOfferingDao offeringDao; + + @Mock + DiskOfferingDao diskOfferingDao; + + @Mock + HypervisorCapabilitiesDao hypervisorCapabilitiesDao; + + @Mock + DataStoreManager dataStoreManager; + + @Mock + DpdkHelper dpdkHelper; + + @Mock + AffinityGroupVMMapDao affinityGroupVMMapDao; + + @Mock + DeploymentPlanningManager dpMgr; + + @Mock + DataCenterDao dcDao; + @Mock ConfigurationDao configDao; @@ -181,6 +245,12 @@ public class ManagementServerImplTest { @Mock HostAllocator hostAllocator; + @Mock + VgpuProfileDao vgpuProfileDao; + + @Mock + VgpuProfileVO vgpuProfileVO; + private AutoCloseable closeable; private MockedStatic apiDBUtilsMock; @@ -203,6 +273,9 @@ public void setup() throws IllegalAccessException, NoSuchFieldException { // Return empty list to avoid architecture filtering in most tests apiDBUtilsMock.when(() -> ApiDBUtils.listZoneClustersArchs(Mockito.anyLong())) .thenReturn(new ArrayList<>()); + + when(vgpuProfileDao.findById(any())).thenReturn(vgpuProfileVO); + when(vgpuProfileVO.getName()).thenReturn("test-vgpu-profile"); } @After @@ -1067,7 +1140,7 @@ public void testListHostsForMigrationOfVMNonRootAdmin() { mockRunningVM(1L, HypervisorType.KVM); Account caller = Mockito.mock(Account.class); Mockito.doReturn(caller).when(spy).getCaller(); - Mockito.when(_accountMgr.isRootAdmin(caller.getId())).thenReturn(false); + Mockito.when(accountManager.isRootAdmin(caller.getId())).thenReturn(false); spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); } @@ -1127,7 +1200,7 @@ public void testListHostsForMigrationOfVMGpuEnabled() { Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) .thenReturn(Mockito.mock(com.cloud.service.ServiceOfferingDetailsVO.class)); - Ternary, Integer>, List, java.util.Map> result = + Ternary, Integer>, List, Map> result = spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); Assert.assertNotNull(result); @@ -2327,7 +2400,7 @@ private VMInstanceVO mockVM(Long id, HypervisorType hypervisorType, State state) private Account mockRootAdminAccount() { Account account = Mockito.mock(Account.class); Mockito.when(account.getId()).thenReturn(1L); - Mockito.when(_accountMgr.isRootAdmin(1L)).thenReturn(true); + Mockito.when(accountManager.isRootAdmin(1L)).thenReturn(true); return account; } From 24fd90a24878c56fb48934b95ca6664c5f519828 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 13 Jul 2026 11:29:58 +0200 Subject: [PATCH 187/205] server: fix build failures after merge-forward --- .../java/com/cloud/server/ManagementServerImplTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index 1ac03258beee..d2c12b6c99dd 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -23,10 +23,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.cloud.deploy.DataCenterDeployment; -import com.cloud.deploy.DeploymentPlanner; -import com.cloud.deploy.DeploymentPlanningManager; -import com.cloud.vm.VirtualMachineProfile; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -80,6 +76,7 @@ import com.cloud.dc.Vlan.VlanType; import com.cloud.dc.dao.DataCenterDao; import com.cloud.deploy.DataCenterDeployment; +import com.cloud.deploy.DeploymentPlanner; import com.cloud.deploy.DeploymentPlanningManager; import com.cloud.domain.dao.DomainDao; import com.cloud.api.ApiDBUtils; @@ -134,6 +131,7 @@ import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; From d2a3bd1aa78a516f47d5cf683bbb1327a121a4b8 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 13 Jul 2026 16:00:54 +0200 Subject: [PATCH 188/205] Rename draft-THREAT-MODEL.md to THREAT_MODEL.md (#13599) The project-wide security threat model merged (apache/cloudstack#13293) as draft-THREAT-MODEL.md, but the canonical discoverability name that scanners and satellite-repo SECURITY.md pointers follow is THREAT_MODEL.md. This renames the file to THREAT_MODEL.md and updates the in-repo SECURITY.md reference, making the pointer in apache/cloudstack-cloudmonkey (which already targets .../blob/main/THREAT_MODEL.md) resolve. The document's own review-status wording is unchanged. Generated-by: Claude Opus 4.8 (1M context) --- SECURITY.md | 2 +- draft-THREAT-MODEL.md => THREAT_MODEL.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename draft-THREAT-MODEL.md => THREAT_MODEL.md (100%) diff --git a/SECURITY.md b/SECURITY.md index ba69c0860243..018f2fa8cb86 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -31,4 +31,4 @@ For more details, see https://cloudstack.apache.org/security.html. What the project treats as in scope and out of scope, the security properties it provides and disclaims, the adversary model, and how findings are triaged are documented in the project-wide threat model: -[draft-THREAT-MODEL.md](draft-THREAT-MODEL.md). +[THREAT_MODEL.md](THREAT_MODEL.md). diff --git a/draft-THREAT-MODEL.md b/THREAT_MODEL.md similarity index 100% rename from draft-THREAT-MODEL.md rename to THREAT_MODEL.md From 13742921d1ec3eacd529e0c361bde77071bcaa33 Mon Sep 17 00:00:00 2001 From: Harikrishna Date: Tue, 14 Jul 2026 15:47:09 +0530 Subject: [PATCH 189/205] Fix simulator test failures on main (#13608) --- .../com/cloud/configuration/ConfigurationManagerImpl.java | 2 -- test/integration/component/test_vpc_offerings.py | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index daefdbbc4a59..164193beeda0 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -7267,8 +7267,6 @@ public NetworkOffering createNetworkOffering(final NetworkOfferingBaseCmd cmd) { forVpc = false; } else if (service == Service.NetworkACL) { forVpc = true; - } else if (service == Service.Firewall) { - forVpc = true; } } diff --git a/test/integration/component/test_vpc_offerings.py b/test/integration/component/test_vpc_offerings.py index 25206cfe1569..a47bc119a88c 100644 --- a/test/integration/component/test_vpc_offerings.py +++ b/test/integration/component/test_vpc_offerings.py @@ -868,10 +868,10 @@ def test_06_vpc_off_invalid_services(self): # Validate the following # 1. Creating VPC Offering with services NOT supported by VPC - # like Firewall should not be allowed + # like SecurityGroup should not be allowed - self.logger.debug("Creating a VPC offering with Firewall") - self.services["vpc_offering"]["supportedservices"] = 'Dhcp,Dns,PortForwarding,Firewall,Vpn,SourceNat,Lb,UserData,StaticNat' + self.logger.debug("Creating a VPC offering with SecurityGroup") + self.services["vpc_offering"]["supportedservices"] = 'Dhcp,Dns,PortForwarding,SecurityGroup,Vpn,SourceNat,Lb,UserData,StaticNat' with self.assertRaises(Exception): VpcOffering.create( From 332800480dfc79a761c2252a36fbf21d3f87cd72 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 14 Jul 2026 12:18:24 +0200 Subject: [PATCH 190/205] debian/ubuntu: ignore pycompile errors (#13602) --- debian/cloudstack-common.postinst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/debian/cloudstack-common.postinst b/debian/cloudstack-common.postinst index aa99edaee064..b11e6a3fe502 100644 --- a/debian/cloudstack-common.postinst +++ b/debian/cloudstack-common.postinst @@ -19,14 +19,14 @@ set -e CLOUDUTILS_DIR="/usr/share/pyshared/" -DIST_DIR=$(python3 -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))") -if which pycompile >/dev/null 2>&1; then - pycompile -p cloudstack-common -fi +# distutils was removed in Python 3.12 (Ubuntu 24.04); the Debian/Ubuntu-patched +# sysconfig 'deb_system' scheme gives the same /usr/lib/python3/dist-packages path. +DIST_DIR=$(python3 -c "import sysconfig; print(sysconfig.get_path('platlib', 'deb_system'))") -if which pycompile >/dev/null 2>&1; then - pycompile -p cloudstack-common /usr/share/cloudstack-common +if command -v py3compile >/dev/null 2>&1; then + py3compile -p cloudstack-common 2>/dev/null || echo "Warning: py3compile failed for cloudstack-common" >&2 + py3compile -p cloudstack-common /usr/share/cloudstack-common 2>/dev/null || echo "Warning: py3compile failed for cloudstack-common (/usr/share/cloudstack-common)" >&2 fi cp $CLOUDUTILS_DIR/cloud_utils.py $DIST_DIR From 0e43c6a818e1a74056f464a87c40941f8fb7d191 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 14 Jul 2026 13:51:07 +0200 Subject: [PATCH 191/205] tools/docker: Build docker image cloudstack-simulator (#13597) * tools: Build docker image from ubuntu 24.04 * docker: run UI with NODE_OPTIONS=--openssl-legacy-provider * docker: add MAVEN_OPTS --- tools/docker/Dockerfile | 13 ++++++------- tools/docker/supervisord.conf | 2 ++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index dcadfb3fead6..9728f92515e4 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -17,7 +17,7 @@ # # CloudStack-simulator build -FROM ubuntu:22.04 +FROM ubuntu:24.04 LABEL Vendor="Apache.org" License="ApacheV2" Version="4.23.0.0-SNAPSHOT" Author="Apache CloudStack " @@ -34,7 +34,8 @@ RUN apt-get -y update && apt-get install -y \ ipmitool \ iproute2 \ maven \ - openjdk-11-jdk \ + openjdk-17-jre-headless \ + openjdk-17-jdk \ python3-dev \ python-is-python3 \ python3-setuptools \ @@ -61,12 +62,10 @@ RUN find /var/lib/mysql -type f -exec touch {} \; && \ mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password by ''" --connect-expired-password; \ mvn -Pdeveloper -pl developer -Ddeploydb; \ mvn -Pdeveloper -pl developer -Ddeploydb-simulator; \ - MARVIN_FILE=`find /root/tools/marvin/dist/ -name "Marvin*.tar.gz"`; \ - rm -rf /usr/bin/x86_64-linux-gnu-gcc && \ - ln -s /usr/bin/gcc-10 /usr/bin/x86_64-linux-gnu-gcc; \ - pip3 install $MARVIN_FILE + MARVIN_FILE=`find /root/tools/marvin/dist/ -name "[mM]arvin*.tar.gz"`; \ + pip3 install $MARVIN_FILE --break-system-packages -RUN curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -; \ +RUN curl -sL https://deb.nodesource.com/setup_24.x | sudo -E bash -; \ apt-get install -y nodejs; \ cd ui && npm rebuild node-sass && npm install diff --git a/tools/docker/supervisord.conf b/tools/docker/supervisord.conf index 1c14578c4cf3..4fe0b49546be 100644 --- a/tools/docker/supervisord.conf +++ b/tools/docker/supervisord.conf @@ -8,6 +8,7 @@ autorestart=true user=root [program:cloudstack] +environment=MAVEN_OPTS="--add-opens=java.base/java.lang=ALL-UNNAMED --add-exports=java.base/sun.security.x509=ALL-UNNAMED" command=/bin/bash -c "mvn -pl client jetty:run -Dsimulator -Dorg.eclipse.jetty.annotations.maxWait=120" directory=/root stdout_logfile=/dev/stdout @@ -16,6 +17,7 @@ redirect_stderr=true user=root [program:cloudstack-ui] +environment=NODE_OPTIONS="--openssl-legacy-provider" command=/bin/bash -c "npm run serve" directory=/root/ui stdout_logfile=/dev/stdout From 66132f83eaec000e3cb0ca94539aaa93bd6ba2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Jandre?= <48719461+JoaoJandre@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:19:31 -0300 Subject: [PATCH 192/205] Introduce new backup provider (KBOSS) (#12758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Jandre Co-authored-by: Bernardo De Marco Gonçalves Co-authored-by: Fabricio Duarte Co-authored-by: GaOrtiga <49285692+GaOrtiga@users.noreply.github.com> --- agent/conf/agent.properties | 12 + .../agent/properties/AgentProperties.java | 15 +- .../cloud/agent/api/to/DataObjectType.java | 2 +- .../main/java/com/cloud/event/EventTypes.java | 1 + .../com/cloud/hypervisor/HypervisorGuru.java | 5 +- .../main/java/com/cloud/storage/Storage.java | 3 +- .../main/java/com/cloud/storage/Volume.java | 11 +- .../com/cloud/storage/VolumeApiService.java | 4 +- .../com/cloud/user/ResourceLimitService.java | 8 +- .../main/java/com/cloud/vm/UserVmService.java | 3 +- .../java/com/cloud/vm/VirtualMachine.java | 19 +- .../java/com/cloud/vm/VmDetailConstants.java | 10 + .../apache/cloudstack/alert/AlertService.java | 3 + .../apache/cloudstack/api/ApiConstants.java | 33 +- .../vm/CreateVMFromBackupCmdByAdmin.java | 15 + .../admin/volume/DestroyVolumeCmdByAdmin.java | 2 +- .../command/user/backup/CreateBackupCmd.java | 10 + .../user/backup/CreateBackupOfferingCmd.java | 185 + .../user/backup/CreateBackupScheduleCmd.java | 10 + .../DownloadValidationScreenshotCmd.java | 94 + .../user/backup/FinishBackupChainCmd.java | 86 + .../user/backup/ListBackupServiceJobsCmd.java | 105 + .../command/user/backup/RestoreBackupCmd.java | 20 +- ...storeVolumeFromBackupAndAttachToVMCmd.java | 19 +- .../user/network/CreateNetworkCmd.java | 21 + .../user/vm/CreateVMFromBackupCmd.java | 9 + .../api/command/user/vm/DestroyVMCmd.java | 10 +- .../command/user/volume/DeleteVolumeCmd.java | 2 +- .../command/user/volume/DestroyVolumeCmd.java | 2 +- .../api/response/BackupOfferingResponse.java | 8 + .../api/response/BackupResponse.java | 36 + .../api/response/BackupScheduleResponse.java | 8 + .../response/BackupServiceJobResponse.java | 79 + .../api/response/UserVmResponse.java | 11 + .../org/apache/cloudstack/backup/Backup.java | 22 +- .../cloudstack/backup/BackupManager.java | 17 +- .../cloudstack/backup/BackupProvider.java | 28 +- .../cloudstack/backup/BackupSchedule.java | 1 + .../backup/InternalBackupProvider.java | 142 + .../backup/InternalBackupService.java | 54 + .../apache/cloudstack/query/QueryService.java | 6 +- .../secstorage/heuristics/HeuristicType.java | 4 +- client/pom.xml | 5 + ...ackupsBetweenSecondaryStoragesCommand.java | 41 + ...igrateBetweenSecondaryStoragesCommand.java | 48 + ...BetweenSecondaryStoragesCommandAnswer.java | 41 + .../com/cloud/agent/api/StartCommand.java | 14 +- .../CreateDiskOnlyVmSnapshotAnswer.java | 11 +- .../CreateDiskOnlyVmSnapshotCommand.java | 12 +- .../MergeDiskOnlyVmSnapshotCommand.java | 16 +- .../storage/resource/StorageProcessor.java | 4 + .../StorageSubsystemCommandHandlerBase.java | 2 + .../backup/CleanupKbossBackupErrorAnswer.java | 46 + .../CleanupKbossBackupErrorCommand.java | 82 + .../backup/CleanupKbossValidationCommand.java | 49 + .../backup/CompressBackupCommand.java | 78 + .../backup/ConsolidateVolumesAnswer.java | 37 + .../backup/ConsolidateVolumesCommand.java | 56 + .../FinalizeBackupCompressionCommand.java | 49 + .../backup/PrepareValidationCommand.java | 52 + .../backup/RestoreKbossBackupAnswer.java | 41 + .../backup/RestoreKbossBackupCommand.java | 66 + .../backup/TakeBackupHashCommand.java | 47 + .../backup/TakeKbossBackupAnswer.java | 59 + .../backup/TakeKbossBackupCommand.java | 92 + .../backup/ValidateKbossVmAnswer.java | 46 + .../backup/ValidateKbossVmCommand.java | 133 + .../storage/command/BackupDeleteAnswer.java | 36 + .../storage/command/DeleteCommand.java | 10 + .../command/RevertSnapshotCommand.java | 10 + .../cloudstack/storage/to/BackupDeltaTO.java | 102 + .../storage/to/DeltaMergeTreeTO.java} | 20 +- .../apache/cloudstack/storage/to/KbossTO.java | 110 + .../cloudstack/storage/to/VolumeObjectTO.java | 12 +- ...e-lifecycle-backup-context-inheritable.xml | 5 + .../spring-core-registry-core-context.xml | 4 + .../service/VolumeOrchestrationService.java | 2 +- .../com/cloud/storage/StorageManager.java | 3 + .../java/com/cloud/vm/VmWorkDeleteBackup.java | 38 + .../com/cloud/vm/VmWorkRestoreBackup.java | 45 + .../VmWorkRestoreVolumeBackupAndAttach.java | 55 + .../java/com/cloud/vm/VmWorkTakeBackup.java | 50 + .../cloud/vm/VirtualMachineManagerImpl.java | 16 +- .../orchestration/DataMigrationUtility.java | 89 +- .../orchestration/StorageOrchestrator.java | 148 +- .../orchestration/VolumeOrchestrator.java | 28 +- ...ring-engine-orchestration-core-context.xml | 1 + .../main/java/com/cloud/host/dao/HostDao.java | 4 + .../java/com/cloud/host/dao/HostDaoImpl.java | 18 + .../com/cloud/network/dao/NetworkDao.java | 2 + .../com/cloud/network/dao/NetworkDaoImpl.java | 12 + .../java/com/cloud/storage/SnapshotVO.java | 4 + .../cloud/storage/dao/SnapshotDaoImpl.java | 98 +- .../cloudstack/backup/BackupOfferingVO.java | 5 + .../cloudstack/backup/BackupScheduleVO.java | 16 +- .../apache/cloudstack/backup/BackupVO.java | 55 +- .../backup/InternalBackupDataStoreVO.java | 95 + .../backup/InternalBackupJoinVO.java | 211 ++ .../backup/InternalBackupServiceJobType.java | 21 + .../backup/InternalBackupServiceJobVO.java | 179 + .../backup/InternalBackupStoragePoolVO.java | 101 + .../cloudstack/backup/dao/BackupDao.java | 1 + .../cloudstack/backup/dao/BackupDaoImpl.java | 10 + .../backup/dao/BackupDetailsDao.java | 15 + .../backup/dao/BackupDetailsDaoImpl.java | 26 + .../backup/dao/BackupOfferingDaoImpl.java | 13 + .../dao/InternalBackupDataStoreDao.java | 33 + .../dao/InternalBackupDataStoreDaoImpl.java | 74 + .../backup/dao/InternalBackupJoinDao.java | 44 + .../backup/dao/InternalBackupJoinDaoImpl.java | 158 + .../dao/InternalBackupServiceJobDao.java | 39 + .../dao/InternalBackupServiceJobDaoImpl.java | 138 + .../dao/InternalBackupStoragePoolDao.java | 37 + .../dao/InternalBackupStoragePoolDaoImpl.java | 86 + .../datastore/db/SnapshotDataStoreDao.java | 2 + .../db/SnapshotDataStoreDaoImpl.java | 31 +- ...spring-engine-schema-core-daos-context.xml | 4 + .../META-INF/db/schema-42210to42300.sql | 55 + .../db/views/cloud.internal_backup_view.sql | 51 + .../StorageSystemDataMotionStrategy.java | 5 + .../storage/snapshot/SnapshotServiceImpl.java | 4 + ...KvmFileBasedStorageVmSnapshotStrategy.java | 249 +- .../vmsnapshot/StorageVMSnapshotStrategy.java | 16 + .../vmsnapshot/VMSnapshotStrategyKVMTest.java | 6 + .../storage/backup/BackupObject.java | 198 ++ .../storage/helper/VMSnapshotHelperImpl.java | 43 +- .../storage/vmsnapshot/VMSnapshotHelper.java | 3 + .../storage/volume/VolumeServiceImpl.java | 7 +- .../framework/jobs/impl/VmWorkJobVO.java | 8 + .../backup/DummyBackupProvider.java | 9 +- plugins/backup/kboss/pom.xml | 50 + .../backup/KbossBackupProvider.java | 3068 +++++++++++++++++ .../cloudstack/kboss/module.properties | 18 + .../kboss/spring-backup-kboss-context.xml | 26 + .../backup/KbossBackupProviderTest.java | 2993 ++++++++++++++++ .../cloudstack/backup/NASBackupProvider.java | 10 +- .../backup/NASBackupProviderTest.java | 6 +- .../backup/NetworkerBackupProvider.java | 9 +- .../backup/VeeamBackupProvider.java | 9 +- .../HypervResourceController.cs | 6 +- .../resource/HypervDirectConnectResource.java | 2 +- plugins/hypervisors/kvm/pom.xml | 15 + .../kvm/resource/BlockCommitListener.java | 12 +- .../resource/LibvirtComputingResource.java | 359 +- .../kvm/resource/LibvirtDomainXMLParser.java | 16 + ...grateResourceBetweenSecondaryStorages.java | 123 + .../LibvirtStorageVolumeXMLParser.java | 20 + .../hypervisor/kvm/resource/LibvirtVMDef.java | 9 + ...tCleanupKbossValidationCommandWrapper.java | 50 + ...irtCleanupKbossVmBackupCommandWrapper.java | 294 ++ .../LibvirtCompressBackupCommandWrapper.java | 150 + ...bvirtConsolidateVolumesCommandWrapper.java | 59 + ...reateDiskOnlyVMSnapshotCommandWrapper.java | 178 +- ...nalizeBackupCompressionCommandWrapper.java | 73 + .../LibvirtGetStorageStatsCommandWrapper.java | 2 +- ...virtGetVolumesOnStorageCommandWrapper.java | 2 +- ...MergeDiskOnlyVMSnapshotCommandWrapper.java | 99 +- ...etweenSecondaryStoragesCommandWrapper.java | 132 + ...ibvirtPrepareValidationCommandWrapper.java | 91 + ...bvirtRestoreKbossBackupCommandWrapper.java | 123 + .../LibvirtRevertSnapshotCommandWrapper.java | 15 +- .../wrapper/LibvirtStartCommandWrapper.java | 16 + .../LibvirtTakeBackupHashCommandWrapper.java | 72 + .../LibvirtTakeKbossBackupCommandWrapper.java | 394 +++ .../LibvirtValidateKbossVmCommandWrapper.java | 225 ++ .../kvm/storage/KVMStoragePoolManager.java | 9 +- .../kvm/storage/KVMStorageProcessor.java | 25 +- .../kvm/storage/LibvirtStorageAdaptor.java | 5 +- .../apache/cloudstack/utils/qemu/QemuImg.java | 104 +- .../LibvirtComputingResourceTest.java | 43 +- ...bvirTakeKbossBackupCommandWrapperTest.java | 377 ++ ...GetVolumesOnStorageCommandWrapperTest.java | 2 +- ...tRestoreKbossBackupCommandWrapperTest.java | 181 + ...bvirtRevertSnapshotCommandWrapperTest.java | 10 +- .../cloudstack/utils/qemu/QemuImgTest.java | 116 +- .../com/cloud/simulator/SimulatorGuru.java | 5 +- .../com/cloud/hypervisor/guru/VMwareGuru.java | 7 +- .../veeam/adapter/ServerAdapter.java | 4 +- .../veeam/adapter/ServerAdapterTest.java | 2 +- plugins/pom.xml | 1 + .../CloudStackImageStoreDriverImpl.java | 6 +- .../lifecycle/StorageVmSharedFSLifeCycle.java | 2 +- .../CloudStackPrimaryDataStoreDriverImpl.java | 14 +- .../java/com/cloud/api/ApiResponseHelper.java | 1 + .../com/cloud/api/query/QueryManagerImpl.java | 51 +- .../api/query/dao/UserVmJoinDaoImpl.java | 7 + .../consoleproxy/ConsoleProxyManagerImpl.java | 7 +- .../cloud/hypervisor/HypervisorGuruBase.java | 5 +- .../java/com/cloud/hypervisor/KVMGuru.java | 21 +- .../network/as/AutoScaleManagerImpl.java | 2 +- .../ResourceLimitManagerImpl.java | 62 +- .../com/cloud/storage/StorageManagerImpl.java | 5 +- .../cloud/storage/VolumeApiServiceImpl.java | 70 +- .../main/java/com/cloud/vm/UserVmManager.java | 15 +- .../java/com/cloud/vm/UserVmManagerImpl.java | 277 +- ...BackupCompressionServiceJobController.java | 241 ++ .../cloudstack/backup/BackupManagerImpl.java | 328 +- .../BackupValidationServiceJobController.java | 217 ++ .../backup/InternalBackupServiceImpl.java | 370 ++ .../InternalBackupServiceJobController.java | 307 ++ .../backup/to/BackupScreenshotObject.java | 113 + .../backup/to/BackupScreenshotTO.java | 59 + .../command/ReconcileCommandServiceImpl.java | 2 +- .../heuristics/HeuristicRuleHelper.java | 24 + .../heuristics/presetvariables/Backup.java | 40 + .../presetvariables/PresetVariables.java | 10 + .../vm/UnmanagedVMsManagerImpl.java | 4 +- .../spring-server-core-managers-context.xml | 5 + .../network/as/AutoScaleManagerImplTest.java | 5 +- .../ResourceLimitManagerImplTest.java | 64 +- .../storage/VolumeApiServiceImplTest.java | 40 +- .../template/TemplateManagerImplTest.java | 5 + .../com/cloud/vm/UserVmManagerImplTest.java | 37 +- .../vpc/MockResourceLimitManagerImpl.java | 10 +- .../com/cloud/vpc/dao/MockNetworkDaoImpl.java | 5 + ...upCompressionServiceJobControllerTest.java | 528 +++ .../cloudstack/backup/BackupManagerTest.java | 274 +- ...kupValidationServiceJobControllerTest.java | 150 + .../backup/InternalBackupServiceImplTest.java | 482 +++ .../heuristics/HeuristicRuleHelperTest.java | 19 + .../resource/NfsSecondaryStorageResource.java | 74 +- .../NfsSecondaryStorageResourceTest.java | 142 +- .../smoke/test_backup_recovery_kboss.py | 336 ++ .../integration/smoke/test_public_ip_range.py | 18 +- tools/apidoc/gen_toc.py | 3 +- tools/marvin/marvin/lib/base.py | 31 + ui/public/locales/en.json | 13 + ui/public/locales/pt_BR.json | 10 + ui/src/components/view/DetailsTab.vue | 15 +- ui/src/components/view/ListView.vue | 6 + ui/src/components/widgets/Status.vue | 8 + ui/src/config/section/compute.js | 15 + ui/src/config/section/offering.js | 9 +- ui/src/config/section/storage.js | 12 +- ui/src/core/lazy_lib/icons_use.js | 2 + ui/src/views/AutogenView.vue | 4 + ui/src/views/compute/InstanceTab.vue | 2 +- ui/src/views/compute/StartBackup.vue | 13 +- .../views/compute/backup/BackupSchedule.vue | 8 + ui/src/views/compute/backup/FormSchedule.vue | 13 +- .../views/offering/CreateBackupOffering.vue | 297 ++ ui/src/views/storage/CreateVMFromBackup.vue | 12 +- .../storage/RestoreAttachBackupVolume.vue | 74 +- .../main/java/com/cloud/utils/DateUtil.java | 6 + .../utils/exception/BackupException.java | 42 + .../exception/BackupProviderException.java | 33 + 246 files changed, 18950 insertions(+), 981 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupOfferingCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/backup/DownloadValidationScreenshotCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/backup/FinishBackupChainCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupServiceJobsCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/BackupServiceJobResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java create mode 100644 api/src/main/java/org/apache/cloudstack/backup/InternalBackupService.java create mode 100644 core/src/main/java/com/cloud/agent/api/MigrateBackupsBetweenSecondaryStoragesCommand.java create mode 100644 core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommand.java create mode 100644 core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommandAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/CleanupKbossValidationCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/CompressBackupCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/FinalizeBackupCompressionCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/PrepareValidationCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/TakeBackupHashCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/storage/command/BackupDeleteAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/storage/to/BackupDeltaTO.java rename core/src/main/java/{com/cloud/agent/api/storage/SnapshotMergeTreeTO.java => org/apache/cloudstack/storage/to/DeltaMergeTreeTO.java} (71%) create mode 100644 core/src/main/java/org/apache/cloudstack/storage/to/KbossTO.java create mode 100644 engine/components-api/src/main/java/com/cloud/vm/VmWorkDeleteBackup.java create mode 100644 engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreBackup.java create mode 100644 engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreVolumeBackupAndAttach.java create mode 100644 engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeBackup.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupDataStoreVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobType.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupStoragePoolVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDao.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDaoImpl.java create mode 100644 engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql create mode 100644 engine/storage/src/main/java/org/apache/cloudstack/storage/backup/BackupObject.java create mode 100644 plugins/backup/kboss/pom.xml create mode 100644 plugins/backup/kboss/src/main/java/org/apache/cloudstack/backup/KbossBackupProvider.java create mode 100644 plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/module.properties create mode 100644 plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/spring-backup-kboss-context.xml create mode 100644 plugins/backup/kboss/src/test/java/org/apache/cloudstack/backup/KbossBackupProviderTest.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtMigrateResourceBetweenSecondaryStorages.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossValidationCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossVmBackupCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCompressBackupCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConsolidateVolumesCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeBackupCompressionCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateBackupsBetweenSecondaryStoragesCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareValidationCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupHashCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeKbossBackupCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtValidateKbossVmCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirTakeKbossBackupCommandWrapperTest.java create mode 100644 plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapperTest.java create mode 100644 server/src/main/java/org/apache/cloudstack/backup/BackupCompressionServiceJobController.java create mode 100644 server/src/main/java/org/apache/cloudstack/backup/BackupValidationServiceJobController.java create mode 100644 server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobController.java create mode 100644 server/src/main/java/org/apache/cloudstack/backup/to/BackupScreenshotObject.java create mode 100644 server/src/main/java/org/apache/cloudstack/backup/to/BackupScreenshotTO.java create mode 100644 server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Backup.java create mode 100644 server/src/test/java/org/apache/cloudstack/backup/BackupCompressionServiceJobControllerTest.java create mode 100644 server/src/test/java/org/apache/cloudstack/backup/BackupValidationServiceJobControllerTest.java create mode 100644 server/src/test/java/org/apache/cloudstack/backup/InternalBackupServiceImplTest.java create mode 100644 test/integration/smoke/test_backup_recovery_kboss.py create mode 100644 ui/src/views/offering/CreateBackupOffering.vue create mode 100644 utils/src/main/java/com/cloud/utils/exception/BackupException.java create mode 100644 utils/src/main/java/com/cloud/utils/exception/BackupProviderException.java diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties index 2d244e00edaf..4e36eff4d75f 100644 --- a/agent/conf/agent.properties +++ b/agent/conf/agent.properties @@ -488,3 +488,15 @@ iscsi.session.cleanup.enabled=false # Optional vCenter SHA1 thumbprint for VMware to KVM conversion via VDDK, passed as # -io vddk-thumbprint=. If unset, CloudStack computes it on the KVM host via openssl. #vddk.thumbprint= + +# Timeout (in seconds) for QCOW2 delta merge operations, mainly used for classic volume snapshots, disk-only VM snapshots on file-based storage, and the KBOSS plugin. +# If a value of 0 or less is informed, the default will be used. +# qcow2.delta.merge.timeout=259200 + +# Maximum number of backup validation jobs that can be executed at the same time. Values lower than 0 remove the limit, meaning that as many validations as possible will be done at +# the same time. +# backup.validation.max.concurrent.operations.per.host= + +# Maximum number of backup compression jobs that can be executed at the same time. Values lower than 0 remove the limit, meaning that as many compressions as possible will be +# done at the same time. +# backup.compression.max.concurrent.operations.per.host= diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java index d47ded2aca79..e4775188d0ca 100644 --- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java +++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java @@ -170,7 +170,8 @@ public class AgentProperties{ public static final Property CMDS_TIMEOUT = new Property<>("cmds.timeout", 7200); /** - * The timeout (in seconds) for the snapshot merge operation, mainly used for classic volume snapshots and disk-only VM snapshots on file-based storage.
+ * The timeout (in seconds) for QCOW2 delta merge operations, mainly used for classic volume snapshots, disk-only VM snapshots on file-based storage, and the KBOSS plugin. + * If a value of 0 or less is informed, the default will be used.
* This configuration is only considered if libvirt.events.enabled is also true.
* Data type: Integer.
* Default value: 259200 @@ -953,6 +954,18 @@ public Property getWorkers() { public static final Property VM_NETWORK_MACIP_STATIC = new Property<>("vm.network.macip.static", false, Boolean.class); + /** + * Maximum number of backup validation jobs that can be executed at the same time. Values lower than 0 remove the limit, meaning that as many validations as possible will be done at + * the same time. + */ + public static final Property BACKUP_VALIDATION_MAX_CONCURRENT_OPERATIONS_PER_HOST = new Property<>("backup.validation.max.concurrent.operations.per.host", null, Integer.class); + + /** + * Maximum number of backup compression jobs that can be executed at the same time. Values lower than 0 remove the limit, meaning that as many compressions as possible will be + * done at the same time. + */ + public static final Property BACKUP_COMPRESSION_MAX_CONCURRENT_OPERATIONS_PER_HOST = new Property<>("backup.compression.max.concurrent.operations.per.host", null, Integer.class); + public static class Property { private String name; private T defaultValue; diff --git a/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java b/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java index 26294cfbb223..76a75e03ba55 100644 --- a/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java +++ b/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java @@ -19,5 +19,5 @@ package com.cloud.agent.api.to; public enum DataObjectType { - VOLUME, SNAPSHOT, TEMPLATE, ARCHIVE + VOLUME, SNAPSHOT, TEMPLATE, ARCHIVE, BACKUP } diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index ee6b010c064c..f7d13343d469 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -668,6 +668,7 @@ public class EventTypes { public static final String EVENT_VM_BACKUP_USAGE_METRIC = "BACKUP.USAGE.METRIC"; public static final String EVENT_VM_BACKUP_EDIT = "BACKUP.OFFERING.EDIT"; public static final String EVENT_VM_CREATE_FROM_BACKUP = "VM.CREATE.FROM.BACKUP"; + public static final String EVENT_SCREENSHOT_DOWNLOAD = "BACKUP.VALIDATION.SCREENSHOT.DOWNLOAD"; // external network device events public static final String EVENT_EXTERNAL_NVP_CONTROLLER_ADD = "PHYSICAL.NVPCONTROLLER.ADD"; diff --git a/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java b/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java index 0c821b4e36c0..67db19b7cc54 100644 --- a/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java +++ b/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java @@ -20,6 +20,7 @@ import java.util.Map; import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupProvider; import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.agent.api.Command; @@ -94,10 +95,10 @@ public interface HypervisorGuru extends Adapter { Map getClusterSettings(long vmId); VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, - String vmInternalName, Backup backup) throws Exception; + String vmInternalName, Backup backup, BackupProvider backupProvider) throws Exception; boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, - VirtualMachine vm, long poolId, Backup backup) throws Exception; + VirtualMachine vm, long poolId, Backup backup, BackupProvider backupProvider) throws Exception; /** * Will generate commands to migrate a vm to a pool. For now this will only work for stopped VMs on Vmware. * diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index ddf5978497ba..3511b4e88cb9 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -35,7 +35,8 @@ public static enum ImageFormat { VDI(true, true, false, "vdi"), TAR(false, false, false, "tar"), ZIP(false, false, false, "zip"), - DIR(false, false, false, "dir"); + DIR(false, false, false, "dir"), + PNG(false, false, false, "png"); private final boolean supportThinProvisioning; private final boolean supportSparse; diff --git a/api/src/main/java/com/cloud/storage/Volume.java b/api/src/main/java/com/cloud/storage/Volume.java index c7a13d5780d0..89298e04587f 100644 --- a/api/src/main/java/com/cloud/storage/Volume.java +++ b/api/src/main/java/com/cloud/storage/Volume.java @@ -60,7 +60,9 @@ enum State { UploadError(false, "Volume upload encountered some error"), UploadAbandoned(false, "Volume upload is abandoned since the upload was never initiated within a specified time"), Attaching(true, "The volume is attaching to a VM from Ready state."), - Restoring(true, "The volume is being restored from backup."); + Restoring(true, "The volume is being restored from backup."), + Consolidating(true, "The volume is being flattened."), + RestoreError(false, "The volume restore encountered an error."); boolean _transitional; @@ -153,6 +155,10 @@ public String getDescription() { s_fsm.addTransition(new StateMachine2.Transition(Destroy, Event.RestoreRequested, Restoring, null)); s_fsm.addTransition(new StateMachine2.Transition(Restoring, Event.RestoreSucceeded, Ready, null)); s_fsm.addTransition(new StateMachine2.Transition(Restoring, Event.RestoreFailed, Ready, null)); + s_fsm.addTransition(new StateMachine2.Transition<>(Ready, Event.ConsolidationRequested, Consolidating, null)); + s_fsm.addTransition(new StateMachine2.Transition<>(Consolidating, Event.OperationSucceeded, Ready, null)); + s_fsm.addTransition(new StateMachine2.Transition<>(Consolidating, Event.OperationFailed, RestoreError, null)); + s_fsm.addTransition(new StateMachine2.Transition<>(RestoreError, Event.RestoreFailed, RestoreError, null)); } } @@ -179,7 +185,8 @@ enum Event { OperationTimeout, RestoreRequested, RestoreSucceeded, - RestoreFailed; + RestoreFailed, + ConsolidationRequested } /** diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index d287cc335eed..372eb0385618 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -114,7 +114,7 @@ Volume allocVolume(long ownerId, Long zoneId, Long diskOfferingId, Long vmId, Lo Volume attachVolumeToVM(AttachVolumeCmd command); - Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean allowAttachForSharedFS); + Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean allowAttachForSharedFS, boolean allowAttachOnRestoring); Volume detachVolumeViaDestroyVM(long vmId, long volumeId); @@ -189,7 +189,7 @@ Volume updateVolume(long volumeId, String path, String state, Long storageId, boolean validateConditionsToReplaceDiskOfferingOfVolume(Volume volume, DiskOffering newDiskOffering, StoragePool destPool); - Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge); + Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge, Boolean countDisplayFalseInResourceCount); void destroyVolume(long volumeId); diff --git a/api/src/main/java/com/cloud/user/ResourceLimitService.java b/api/src/main/java/com/cloud/user/ResourceLimitService.java index 9c493fb383c9..89128f87829e 100644 --- a/api/src/main/java/com/cloud/user/ResourceLimitService.java +++ b/api/src/main/java/com/cloud/user/ResourceLimitService.java @@ -254,14 +254,14 @@ public interface ResourceLimitService { void updateTaggedResourceLimitsAndCountsForAccounts(List responses, String tag); void updateTaggedResourceLimitsAndCountsForDomains(List responses, String tag); void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException; - List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering); + List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering, Boolean enforceResourceLimitOnDisplayFalse); void checkVolumeResourceLimitForDiskOfferingChange(Account owner, Boolean display, Long currentSize, Long newSize, DiskOffering currentOffering, DiskOffering newOffering, List reservations) throws ResourceAllocationException; void checkPrimaryStorageResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException; void incrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); - void decrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); + void decrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering, Boolean countDisplayFalseInResourceCount); void updateVmResourceCountForTemplateChange(long accountId, Boolean display, ServiceOffering offering, VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate); @@ -276,8 +276,8 @@ void updateVolumeResourceCountForDiskOfferingChange(long accountId, Boolean disp void incrementVolumePrimaryStorageResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); void decrementVolumePrimaryStorageResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException; - void incrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template); - void decrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template); + void incrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Boolean countDisplayFalseInResourceLimit); + void decrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Boolean countDisplayFalseInResourceCount); void checkVmResourceLimitsForServiceOfferingChange(Account owner, Boolean display, Long currentCpu, Long newCpu, Long currentMemory, Long newMemory, ServiceOffering currentOffering, ServiceOffering newOffering, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException; diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index ffa00734d575..5864e91cd7f4 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -75,10 +75,11 @@ public interface UserVmService { * Destroys one virtual machine * * @param cmd the API Command Object containg the parameters to use for this service action + * @param checkExpunge * @throws ConcurrentOperationException * @throws ResourceUnavailableException */ - UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, ConcurrentOperationException; + UserVm destroyVm(DestroyVMCmd cmd, boolean checkExpunge) throws ResourceUnavailableException, ConcurrentOperationException; /** * Destroys one virtual machine diff --git a/api/src/main/java/com/cloud/vm/VirtualMachine.java b/api/src/main/java/com/cloud/vm/VirtualMachine.java index 41c9a864c9d0..3adcc85d28a1 100644 --- a/api/src/main/java/com/cloud/vm/VirtualMachine.java +++ b/api/src/main/java/com/cloud/vm/VirtualMachine.java @@ -58,7 +58,10 @@ public enum State { Error(false, "VM is in error"), Unknown(false, "VM state is unknown."), Shutdown(false, "VM state is shutdown from inside"), - Restoring(true, "VM is being restored from backup"); + Restoring(true, "VM is being restored from backup"), + BackingUp(true, "VM is being backed up"), + BackupError(false, "VM backup is in an inconsistent state. Operator should analyse the logs and restore the VM"), + RestoreError(false, "VM restore left the VM in an inconsistent state. Operator should analyse the logs and restore the VM"); private final boolean _transitional; String _description; @@ -134,6 +137,14 @@ public static StateMachine2 getStat s_fsm.addTransition(new Transition(State.Destroyed, Event.RestoringRequested, State.Restoring, null)); s_fsm.addTransition(new Transition(State.Restoring, Event.RestoringSuccess, State.Stopped, null)); s_fsm.addTransition(new Transition(State.Restoring, Event.RestoringFailed, State.Stopped, null)); + s_fsm.addTransition(new Transition<>(State.Running, Event.BackupRequested, State.BackingUp, null)); + s_fsm.addTransition(new Transition<>(State.Stopped, Event.BackupRequested, State.BackingUp, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.BackupSucceededRunning, State.Running, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.BackupSucceededStopped, State.Stopped, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.OperationFailedToError, State.BackupError, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.OperationFailedToRunning, State.Running, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.OperationFailedToStopped, State.Stopped, null)); + s_fsm.addTransition(new Transition(State.RestoreError, Event.RestoringFailed, State.RestoreError, null)); s_fsm.addTransition(new Transition(State.Starting, VirtualMachine.Event.FollowAgentPowerOnReport, State.Running, Arrays.asList(new Impact[]{Impact.USAGE}))); s_fsm.addTransition(new Transition(State.Stopping, VirtualMachine.Event.FollowAgentPowerOnReport, State.Running, null)); @@ -212,6 +223,8 @@ public enum Event { ExpungeOperation, OperationSucceeded, OperationFailed, + OperationFailedToRunning, + OperationFailedToStopped, OperationFailedToError, OperationRetry, AgentReportShutdowned, @@ -221,6 +234,10 @@ public enum Event { RestoringRequested, RestoringFailed, RestoringSuccess, + BackupRequested, + BackupSucceededStopped, + BackupSucceededRunning, + FinalizedBackupChain, // added for new VMSync logic FollowAgentPowerOnReport, diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index 33cc6da70812..877df55c6d67 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -136,4 +136,14 @@ public interface VmDetailConstants { String ACTIVE_CHECKPOINT_CREATE_TIME = "active.checkpoint.create.time"; String LAST_CHECKPOINT_ID = "last.checkpoint.id"; String LAST_CHECKPOINT_CREATE_TIME = "last.checkpoint.create.time"; + + // KBOSS specific + String LINKED_VOLUMES_SECONDARY_STORAGE_UUIDS = "linkedVolumesSecondaryStorageUuids"; + String VALIDATION_COMMAND = "backupValidationCommand"; + String VALIDATION_COMMAND_ARGUMENTS = "backupValidationCommandArguments"; + String VALIDATION_COMMAND_EXPECTED_RESULT = "backupValidationCommandExpectedResult"; + String VALIDATION_COMMAND_TIMEOUT = "backupValidationCommandTimeout"; + String VALIDATION_SCREENSHOT_WAIT = "backupValidationScreenshotWait"; + String VALIDATION_BOOT_TIMEOUT = "backupValidationBootTimeout"; + String LAST_KNOWN_STATE = "last_known_state"; } diff --git a/api/src/main/java/org/apache/cloudstack/alert/AlertService.java b/api/src/main/java/org/apache/cloudstack/alert/AlertService.java index fcc87908bd5d..a9c2abc11ce7 100644 --- a/api/src/main/java/org/apache/cloudstack/alert/AlertService.java +++ b/api/src/main/java/org/apache/cloudstack/alert/AlertService.java @@ -83,6 +83,9 @@ private AlertType(short type, String name, boolean isDefault) { public static final AlertType ALERT_TYPE_VPN_GATEWAY_OBSOLETE_PARAMETERS = new AlertType((short)34, "ALERT.S2S.VPN.GATEWAY.OBSOLETE.PARAMETERS", true, true); public static final AlertType ALERT_TYPE_BACKUP_STORAGE = new AlertType(Capacity.CAPACITY_TYPE_BACKUP_STORAGE, "ALERT.STORAGE.BACKUP", true); public static final AlertType ALERT_TYPE_OBJECT_STORAGE = new AlertType(Capacity.CAPACITY_TYPE_OBJECT_STORAGE, "ALERT.STORAGE.OBJECT", true); + public static final AlertType ALERT_TYPE_BACKUP_VALIDATION_FAILED = new AlertType((short)35, "ALERT.BACKUP.VALIDATION.FAILED", true, true); + public static final AlertType ALERT_TYPE_BACKUP_VALIDATION_UNABLE_TO_VALIDATE = new AlertType((short)36, "ALERT.BACKUP.VALIDATION.UNABLE.TO.VALIDATE", true, true); + public static final AlertType ALERT_TYPE_BACKUP_VALIDATION_CLEANUP_FAILED = new AlertType((short)37, "ALERT.BACKUP.VALIDATION.CLEANUP_FAILED", true, true); public short getType() { return type; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index c15a4a800edc..ac6acdf42516 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -63,6 +63,7 @@ public class ApiConstants { public static final String BACKUP_LIMIT = "backuplimit"; public static final String BACKUP_OFFERING_NAME = "backupofferingname"; public static final String BACKUP_OFFERING_ID = "backupofferingid"; + public static final String BACKUP_OFFERING_DETAILS = "backupofferingdetails"; public static final String BACKUP_STORAGE_AVAILABLE = "backupstorageavailable"; public static final String BACKUP_STORAGE_LIMIT = "backupstoragelimit"; public static final String BACKUP_STORAGE_TOTAL = "backupstoragetotal"; @@ -534,6 +535,7 @@ public class ApiConstants { public static final String QUALIFIERS = "qualifiers"; public static final String QUERY_FILTER = "queryfilter"; public static final String QUIESCE_VM = "quiescevm"; + public static final String QUICK_RESTORE = "quickrestore"; public static final String SCHEDULE = "schedule"; public static final String SCHEDULE_ID = "scheduleid"; public static final String SCOPE = "scope"; @@ -584,6 +586,8 @@ public class ApiConstants { public static final String STATE = "state"; public static final String STATS = "stats"; public static final String STATUS = "status"; + public static final String COMPRESSION_STATUS = "compressionstatus"; + public static final String VALIDATION_STATUS = "validationstatus"; public static final String STORAGE_TYPE = "storagetype"; public static final String STORAGE_POLICY = "storagepolicy"; public static final String STORAGE_MOTION_ENABLED = "storagemotionenabled"; @@ -682,6 +686,7 @@ public class ApiConstants { public static final String ETCD_SERVICE_OFFERING_NAME = "etcdofferingname"; public static final String REMOVE_VLAN = "removevlan"; public static final String VLAN_ID = "vlanid"; + public static final String ISOLATED = "isolated"; public static final String ISOLATED_PVLAN = "isolatedpvlan"; public static final String ISOLATED_PVLAN_TYPE = "isolatedpvlantype"; public static final String ISOLATION_URI = "isolationuri"; @@ -1206,6 +1211,7 @@ public class ApiConstants { public static final String CLEAN_UP_EXTRA_CONFIG = "cleanupextraconfig"; public static final String CLEAN_UP_PARAMETERS = "cleanupparameters"; public static final String VIRTUAL_SIZE = "virtualsize"; + public static final String UNCOMPRESSED_SIZE = "uncompressedsize"; public static final String NETSCALER_CONTROLCENTER_ID = "netscalercontrolcenterid"; public static final String NETSCALER_SERVICEPACKAGE_ID = "netscalerservicepackageid"; public static final String FETCH_ROUTER_HEALTH_CHECK_RESULTS = "fetchhealthcheckresults"; @@ -1342,7 +1348,7 @@ public class ApiConstants { public static final String IMPORT_SOURCE = "importsource"; public static final String TEMP_PATH = "temppath"; public static final String HEURISTIC_RULE = "heuristicrule"; - public static final String HEURISTIC_TYPE_VALID_OPTIONS = "Valid options are: ISO, SNAPSHOT, TEMPLATE and VOLUME."; + public static final String HEURISTIC_TYPE_VALID_OPTIONS = "Valid options are: ISO, SNAPSHOT, BACKUP, TEMPLATE and VOLUME."; public static final String MANAGEMENT = "management"; public static final String IS_VNF = "isvnf"; public static final String VNF_NICS = "vnfnics"; @@ -1438,6 +1444,10 @@ public class ApiConstants { public static final String VMWARE_DC = "vmwaredc"; + public static final String PARAMETER_DESCRIPTION_ISOLATED_BACKUPS = "Whether the backup will be isolated, defaults to false. " + + "Isolated backups are always created as full backups in independent chains. Therefore, they will never depend on any existing backup chain " + + "and no backup chain will depend on them. Currently only supported for the KBOSS provider."; + public static final String CSS = "css"; public static final String JSON_CONFIGURATION = "jsonconfiguration"; @@ -1459,6 +1469,27 @@ public class ApiConstants { public static final String OBSOLETE_PARAMETERS = "obsoleteparameters"; public static final String EXCLUDED_PARAMETERS = "excludedparameters"; + public static final String COMPRESS = "compress"; + + public static final String VALIDATE = "validate"; + + public static final String VALIDATION_STEPS = "validationsteps"; + + public static final String ALLOW_QUICK_RESTORE = "allowquickrestore"; + + public static final String ALLOW_EXTRACT_FILE = "allowextractfile"; + + public static final String BACKUP_CHAIN_SIZE = "backupchainsize"; + + public static final String COMPRESSION_LIBRARY = "compressionlibrary"; + public static final String ATTEMPTS = "attempts"; + + public static final String EXECUTING = "executing"; + + public static final String SCHEDULED = "scheduled"; + public static final String SCHEDULED_DATE = "scheduleddate"; + public static final String BACKUP_PROVIDER = "backupprovider"; + /** * This enum specifies IO Drivers, each option controls specific policies on I/O. * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0). diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CreateVMFromBackupCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CreateVMFromBackupCmdByAdmin.java index d95f17ef304c..280e4a8bab77 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CreateVMFromBackupCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CreateVMFromBackupCmdByAdmin.java @@ -45,6 +45,8 @@ public class CreateVMFromBackupCmdByAdmin extends CreateVMFromBackupCmd implemen @Parameter(name = ApiConstants.CLUSTER_ID, type = CommandType.UUID, entityType = ClusterResponse.class, description = "destination Cluster ID to deploy the VM to - parameter available for root admin only", since = "4.21") private Long clusterId; + private String instanceType; + public Long getPodId() { return podId; } @@ -52,4 +54,17 @@ public Long getPodId() { public Long getClusterId() { return clusterId; } + + @Override + public String getInstanceType() { + return instanceType; + } + + public CreateVMFromBackupCmdByAdmin(){} + + public CreateVMFromBackupCmdByAdmin(String hypervisor, String instanceType) { + this.displayVm = false; + this.hypervisor = hypervisor; + this.instanceType = instanceType; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java index 0840b4ce6f99..de90fee102de 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java @@ -40,7 +40,7 @@ public class DestroyVolumeCmdByAdmin extends DestroyVolumeCmd implements AdminCm @Override public void execute() { CallContext.current().setEventDetails("Volume Id: " + getId()); - Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false); + Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false, null); if (result != null) { VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Full, result); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java index 8a4053aa15da..9ce64c2294b1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java @@ -81,6 +81,12 @@ public class CreateBackupCmd extends BaseAsyncCreateCmd { since = "4.21.0") private Boolean quiesceVM; + @Parameter(name = ApiConstants.ISOLATED, + type = CommandType.BOOLEAN, + description = ApiConstants.PARAMETER_DESCRIPTION_ISOLATED_BACKUPS, + since = "4.23.0") + private boolean isolated; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -101,6 +107,10 @@ public Boolean getQuiesceVM() { return quiesceVM; } + public boolean isIsolated() { + return isolated; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupOfferingCmd.java new file mode 100644 index 000000000000..c5d29b615439 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupOfferingCmd.java @@ -0,0 +1,185 @@ +// 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.api.command.user.backup; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; + + +import javax.inject.Inject; +import java.util.List; + +@APICommand(name = "createBackupOffering", description = "Creates a backup offering", responseObject = BackupOfferingResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin}, since = "4.23.0") +public class CreateBackupOfferingCmd extends BaseCmd { + + @Inject + protected BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Backup offering name.", required = true) + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = true, + description = "The description of the backup offering") + private String description; + + @Parameter(name = ApiConstants.COMPRESS, type = CommandType.BOOLEAN, description = "Whether the backups should be compressed or not.") + private Boolean compress; + + @Parameter(name = ApiConstants.VALIDATE, type = CommandType.BOOLEAN, description = "Whether the backups should be validated or not.") + private Boolean validate; + + @Parameter(name = ApiConstants.VALIDATION_STEPS, type = CommandType.STRING, description = "Which validation steps should be performed. Accepts a comma-separated list of " + + "steps. Accepted values are: wait_for_boot, screenshot and execute_command.") + private String validationSteps; + + @Parameter(name = ApiConstants.ALLOW_QUICK_RESTORE, type = CommandType.BOOLEAN, description = "Whether quick restore is enabled for the backups or not.") + private Boolean allowQuickRestore; + + @Parameter(name = ApiConstants.ALLOW_EXTRACT_FILE, type = CommandType.BOOLEAN, description = "Whether files may be extracted from backups or not.") + private Boolean allowExtractFile; + + @Parameter(name = ApiConstants.BACKUP_CHAIN_SIZE, type = CommandType.INTEGER, description = "Backup chain size for backups created with this offering.") + private Integer backupChainSize; + + @Parameter(name = ApiConstants.COMPRESSION_LIBRARY, type = CommandType.STRING, description = "Compression library, for offerings that support compression. Accepted values " + + "are zstd and zlib. By default, zstd is used for images that support it. If the image only supports zlib, it will be used regardless of this parameter.") + private String compressionLibrary; + + @Parameter(name = ApiConstants.ZONE_ID, type = BaseCmd.CommandType.UUID, entityType = ZoneResponse.class, + description = "Restrict the backup offering to the Zone identified by this ID.", required = true) + private Long zoneId; + + @Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = CommandType.BOOLEAN, + description = "Whether users are allowed to create ad-hoc backups and backup schedules when using this offering.", required = true) + private Boolean userDrivenBackups; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = DomainResponse.class, + description = "Restrict the backup offering to the Domains identified by these IDs.") + private List domainIds; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public boolean isCompress() { + return Boolean.TRUE.equals(compress); + } + + public boolean isValidate() { + return Boolean.TRUE.equals(validate); + } + + public boolean isAllowQuickRestore() { + return Boolean.TRUE.equals(allowQuickRestore); + } + + public boolean isAllowExtractFile() { + return Boolean.TRUE.equals(allowExtractFile); + } + + public Integer getBackupChainSize() { + return backupChainSize; + } + + public Backup.CompressionLibrary getCompressionLibrary() { + if (compressionLibrary == null) { + return null; + } + try { + return Backup.CompressionLibrary.valueOf(compressionLibrary); + } catch (IllegalArgumentException e) { + throw new InvalidParameterValueException(String.format("Invalid compression library, accepted values are zstd and zlib, received [%s].", compressionLibrary)); + } + } + + public String getValidationSteps() { + if (validationSteps == null) { + return Backup.ValidationSteps.screenshot.name(); + } + StringBuilder sb = new StringBuilder(); + for (String step : validationSteps.strip().split(",")) { + try { + Backup.ValidationSteps enumStep = Backup.ValidationSteps.valueOf(step); + sb.append(enumStep.name()); + sb.append(","); + } catch (IllegalArgumentException ex) { + logger.error("Invalid validation step informed [{}].", step, ex); + throw new InvalidParameterValueException(String.format("Invalid validation step [%s] informed. Accepted values are: wait_for_boot, screenshot and execute_command.", step)); + } + } + sb.deleteCharAt(sb.lastIndexOf(",")); + return sb.toString(); + } + + public String getDescription() { + return description; + } + + public Long getZoneId() { + return zoneId; + } + + public List getDomainIds() { + return domainIds; + } + + public Boolean getUserDrivenBackups() { + return userDrivenBackups; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, + NetworkRuleConflictException { + BackupOffering offering = backupManager.createBackupOffering(this); + BackupOfferingResponse response = _responseGenerator.createBackupOfferingResponse(offering); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return 0; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java index f6e17a2b3908..41c530471da5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java @@ -88,6 +88,12 @@ public class CreateBackupScheduleCmd extends BaseCmd { since = "4.21.0") private Boolean quiesceVM; + @Parameter(name = ApiConstants.ISOLATED, + type = CommandType.BOOLEAN, + description = ApiConstants.PARAMETER_DESCRIPTION_ISOLATED_BACKUPS, + since = "4.23.0") + private boolean isolated; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -116,6 +122,10 @@ public Boolean getQuiesceVM() { return quiesceVM; } + public boolean isIsolated() { + return isolated; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DownloadValidationScreenshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DownloadValidationScreenshotCmd.java new file mode 100644 index 000000000000..997d0d24c6f0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DownloadValidationScreenshotCmd.java @@ -0,0 +1,94 @@ +// 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.api.command.user.backup; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.ExtractResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.InternalBackupService; + +import javax.inject.Inject; + +@APICommand(name = "downloadValidationScreenshot", description = "Download validation screenshot of given backup.", + responseObject = ExtractResponse.class, since = "4.23.0", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class DownloadValidationScreenshotCmd extends BaseAsyncCmd { + + @Inject + private InternalBackupService internalBackupService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL + @Parameter(name = ApiConstants.BACKUP_ID, type = CommandType.UUID, entityType = BackupResponse.class, required = true, + description = "ID of the backup.") + private Long backupId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getBackupId() { + return backupId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getEventType() { + return EventTypes.EVENT_SCREENSHOT_DOWNLOAD; + } + + @Override + public String getEventDescription() { + Backup backup = _entityMgr.findById(Backup.class, getBackupId()); + if (backup == null) { + throw new InvalidParameterValueException(String.format("Unable to find backup with ID [%s].", getBackupId())); + } + return "Downloading validation screenshot of backup " + backup.getUuid(); + } + + @Override + public void execute() { + ExtractResponse response = internalBackupService.downloadScreenshot(getBackupId()); + response.setResponseName(getCommandName()); + response.setObjectName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + Backup backup = _entityMgr.findById(Backup.class, getBackupId()); + if (backup != null) { + return backup.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/FinishBackupChainCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/FinishBackupChainCmd.java new file mode 100644 index 000000000000..575df7ae0831 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/FinishBackupChainCmd.java @@ -0,0 +1,86 @@ +// 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.api.command.user.backup; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.VirtualMachineResponse; +import org.apache.cloudstack.backup.InternalBackupService; + +import javax.inject.Inject; + +@APICommand(name = "finishBackupChain", description = "Finish the backup chain of VM. Currently only has effect on VMs with KBOSS backup offerings.", + responseObject = SuccessResponse.class, since = "4.23.0", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class FinishBackupChainCmd extends BaseCmd { + @Inject + private InternalBackupService internalBackupService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = VirtualMachineResponse.class, required = true, + description = "ID of the VM to finish the chain.") + private Long vmId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, + NetworkRuleConflictException { + boolean result = internalBackupService.finishBackupChain(getVmId()); + SuccessResponse response = new SuccessResponse(); + response.setSuccess(result); + response.setResponseName(getCommandName()); + response.setObjectName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, getVmId()); + if (vm != null) { + return vm.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupServiceJobsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupServiceJobsCmd.java new file mode 100644 index 000000000000..39ca444ae2ee --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupServiceJobsCmd.java @@ -0,0 +1,105 @@ +// 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.api.command.user.backup; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupServiceJobResponse; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ZoneResponse; + +@APICommand(name = "listBackupServiceJobs", description = "List backup service jobs", responseObject = BackupServiceJobResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin}, since = "4.23.0") +public class ListBackupServiceJobsCmd extends BaseListCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.LONG, entityType = BackupServiceJobResponse.class, description = "List only job with given ID.") + private Long id; + + @Parameter(name = ApiConstants.BACKUP_ID, type = CommandType.UUID, entityType = BackupResponse.class, description = "List jobs for the given backup.") + private Long backupId; + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "List jobs in the given host. When passing this parameter, only jobs that are currently executing will be returned.") + private Long hostId; + + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, description = "List jobs in the given zone.") + private Long zoneId; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "List jobs with the given type. Accepted values are StartCompression, FinalizeCompression and " + + "BackupValidation.") + private String type; + + @Parameter(name = ApiConstants.EXECUTING, type = CommandType.BOOLEAN, description = "List executing jobs.") + private Boolean executing; + + @Parameter(name = ApiConstants.SCHEDULED, type = CommandType.BOOLEAN, description = "List scheduled jobs.") + private Boolean scheduled; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public Long getBackupId() { + return backupId; + } + + public Long getHostId() { + return hostId; + } + + public Long getZoneId() { + return zoneId; + } + + public String getType() { + return type; + } + + public boolean getExecuting() { + return Boolean.TRUE.equals(executing); + } + + public boolean getScheduled() { + return Boolean.TRUE.equals(scheduled); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, + NetworkRuleConflictException { + ListResponse response = _queryService.listBackupServiceJobs(this); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java index c29d117161f2..9c67aa6a9f69 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java @@ -26,6 +26,7 @@ import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.backup.BackupManager; @@ -38,6 +39,7 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.commons.lang3.BooleanUtils; @APICommand(name = "restoreBackup", description = "Restores an existing stopped or deleted Instance using an Instance backup", @@ -59,6 +61,14 @@ public class RestoreBackupCmd extends BaseAsyncCmd { description = "ID of the backup") private Long backupId; + @Parameter(name = ApiConstants.QUICK_RESTORE, type = CommandType.BOOLEAN, entityType = BackupResponse.class, description = "Whether to use the quick restore process or not. " + + "Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0") + private Boolean quickRestore; + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "If quickrestore is true, which host to start the VM on;" + + " otherwise, ignored. Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0", authorized = {RoleType.Admin}) + private Long hostId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -67,6 +77,14 @@ public Long getBackupId() { return backupId; } + public boolean isQuickRestore() { + return BooleanUtils.isTrue(quickRestore); + } + + public Long getHostId() { + return hostId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -74,7 +92,7 @@ public Long getBackupId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { try { - boolean result = backupManager.restoreBackup(backupId); + boolean result = backupManager.restoreBackup(backupId, isQuickRestore(), getHostId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java index c15e6f8de684..e05845866c2b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.BackupResponse; @@ -78,6 +79,14 @@ public class RestoreVolumeFromBackupAndAttachToVMCmd extends BaseAsyncCmd { description = "ID of the Instance where to attach the restored volume") private Long vmId; + @Parameter(name = ApiConstants.QUICK_RESTORE, type = CommandType.BOOLEAN, description = "Whether to use the quick restore process or not. " + + "Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0") + private Boolean quickRestore; + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "If quickrestore is true, which host to start the VM on;" + + " otherwise, ignored. Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0", authorized = {RoleType.Admin}) + private Long hostId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -94,6 +103,14 @@ public Long getBackupId() { return backupId; } + public boolean isQuickRestore() { + return org.apache.commons.lang3.BooleanUtils.isTrue(quickRestore); + } + + public Long getHostId() { + return hostId; + } + @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); @@ -106,7 +123,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { try { - boolean result = backupManager.restoreBackupVolumeAndAttachToVM(volumeUuid, backupId, vmId); + boolean result = backupManager.restoreBackupVolumeAndAttachToVM(volumeUuid, backupId, vmId, isQuickRestore(), getHostId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java index ee5b8568e835..79fb5f6d01cf 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java @@ -419,6 +419,27 @@ public Long getAsNumber() { ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// + + public CreateNetworkCmd() { + } + + public CreateNetworkCmd(long networkOfferingId, String name, String displayText, String gateway, String netmask, String startIp, String endIp, long domainId, + String accountName, long zoneId, String aclType, boolean subdomainAccess, boolean displayNetwork) { + this.networkOfferingId = networkOfferingId; + this.name = name; + this.displayText = displayText; + this.gateway = gateway; + this.netmask = netmask; + this.startIp = startIp; + this.endIp = endIp; + this.domainId = domainId; + this.accountName = accountName; + this.zoneId = zoneId; + this.aclType = aclType; + this.subdomainAccess = subdomainAccess; + this.displayNetwork = displayNetwork; + } + @Override public String getCommandName() { return s_name; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMFromBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMFromBackupCmd.java index a719062e1bca..7743c031006b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMFromBackupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMFromBackupCmd.java @@ -36,6 +36,7 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.uservm.UserVm; import com.cloud.vm.VirtualMachine; +import org.apache.commons.lang3.ObjectUtils; @APICommand(name = "createVMFromBackup", description = "Creates and automatically starts a VM from a backup.", @@ -70,6 +71,10 @@ public class CreateVMFromBackupCmd extends BaseDeployVMCmd { @Parameter(name = ApiConstants.PRESERVE_IP, type = CommandType.BOOLEAN, description = "Use the same IP/MAC addresses as stored in the backup metadata. Works only if the original Instance is deleted and the IP/MAC address is available.") private Boolean preserveIp; + @Parameter(name = ApiConstants.QUICK_RESTORE, type = CommandType.BOOLEAN, entityType = BackupResponse.class, description = "Whether to use the quick restore process or not. " + + "Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0") + private Boolean quickRestore; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -90,6 +95,10 @@ public boolean getPreserveIp() { return (preserveIp != null) ? preserveIp : false; } + public Boolean getQuickRestore() { + return ObjectUtils.defaultIfNull(this.quickRestore, false); + } + @Override public void create() { UserVm vm; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java index aec0688f1779..0a3e510d3d53 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java @@ -98,6 +98,14 @@ public boolean isForced() { /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// + public DestroyVMCmd() { + } + + public DestroyVMCmd(Long id, Boolean expunge) { + this.id = id; + this.expunge = expunge; + } + @Override public String getCommandName() { return s_name; @@ -136,7 +144,7 @@ public Long getApiResourceId() { @Override public void execute() throws ResourceUnavailableException, ConcurrentOperationException { CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); - UserVm result = _userVmService.destroyVm(this); + UserVm result = _userVmService.destroyVm(this, true); UserVmResponse response = new UserVmResponse(); if (result != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java index e102d51f0378..ab6cda651b06 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java @@ -85,7 +85,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() throws ConcurrentOperationException { CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); - Volume result = _volumeService.destroyVolume(id, CallContext.current().getCallingAccount(), true, false); + Volume result = _volumeService.destroyVolume(id, CallContext.current().getCallingAccount(), true, false, null); if (result != null) { SuccessResponse response = new SuccessResponse(getCommandName()); setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java index 12a44f76ea15..e9e388436642 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java @@ -116,7 +116,7 @@ public Long getApiResourceId() { @Override public void execute() { CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); - Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false); + Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false, null); if (result != null) { VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Restricted, result); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java index c4f3ee31dadc..69bee63e652f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.response; import java.util.Date; +import java.util.Map; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; @@ -79,6 +80,10 @@ public class BackupOfferingResponse extends BaseResponse { @Param(description = "The date this backup offering was created") private Date created; + @SerializedName(ApiConstants.BACKUP_OFFERING_DETAILS) + @Param(description = "Details for the backup offering", since = "4.23.0") + private Map details; + public void setId(String id) { this.id = id; } @@ -127,4 +132,7 @@ public void setDomain(String domain) { this.domain = domain; } + public void setDetails(Map details) { + this.details = details; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java index 51fcaa9836e9..70db01445edd 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java @@ -71,10 +71,22 @@ public class BackupResponse extends BaseResponse { @Param(description = "Backup protected (virtual) size in bytes") private Long protectedSize; + @SerializedName(ApiConstants.UNCOMPRESSED_SIZE) + @Param(description = "Backup uncompressed size in bytes. Only defined if backup is compressed.") + private Long uncompressedSize; + @SerializedName(ApiConstants.STATUS) @Param(description = "Backup status") private Backup.Status status; + @SerializedName(ApiConstants.COMPRESSION_STATUS) + @Param(description = "Backup compression status.") + private Backup.CompressionStatus compressionStatus; + + @SerializedName(ApiConstants.VALIDATION_STATUS) + @Param(description = "Backup validation status.") + private Backup.ValidationStatus validationStatus; + @SerializedName(ApiConstants.VOLUMES) @Param(description = "Backed up volumes") private String volumes; @@ -219,6 +231,14 @@ public void setProtectedSize(Long protectedSize) { this.protectedSize = protectedSize; } + public Long getUncompressedSize() { + return uncompressedSize; + } + + public void setUncompressedSize(Long uncompressedSize) { + this.uncompressedSize = uncompressedSize; + } + public Backup.Status getStatus() { return status; } @@ -227,6 +247,22 @@ public void setStatus(Backup.Status status) { this.status = status; } + public Backup.CompressionStatus getCompressionStatus() { + return compressionStatus; + } + + public void setCompressionStatus(Backup.CompressionStatus compressionStatus) { + this.compressionStatus = compressionStatus; + } + + public Backup.ValidationStatus getValidationStatus() { + return validationStatus; + } + + public void setValidationStatus(Backup.ValidationStatus validationStatus) { + this.validationStatus = validationStatus; + } + public String getVolumes() { return volumes; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java index 13d0c5d8c562..5da07864603a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java @@ -56,6 +56,10 @@ public class BackupScheduleResponse extends BaseResponse { @Param(description = "maximum number of backups retained") private Integer maxBackups; + @SerializedName(ApiConstants.ISOLATED) + @Param(description = ApiConstants.PARAMETER_DESCRIPTION_ISOLATED_BACKUPS) + private boolean isolated; + public void setId(String id) { this.id = id; } @@ -111,4 +115,8 @@ public void setMaxBackups(Integer maxBackups) { public void setQuiesceVM(Boolean quiesceVM) { this.quiesceVM = quiesceVM; } + + public void setIsolated(boolean isolated) { + this.isolated = isolated; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupServiceJobResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupServiceJobResponse.java new file mode 100644 index 000000000000..0d4984fe34f5 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupServiceJobResponse.java @@ -0,0 +1,79 @@ +// 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.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import java.util.Date; + +public class BackupServiceJobResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "Compression job ID.") + private Long id; + + @SerializedName(ApiConstants.BACKUP_ID) + @Param(description = "Backup ID.") + private String backupId; + + @SerializedName(ApiConstants.HOST_ID) + @Param(description = "Host where the job is being executed.") + private String hostId; + + @SerializedName(ApiConstants.ZONE_ID) + @Param(description = "Zone where the job is being executed.") + private String zoneId; + + @SerializedName(ApiConstants.ATTEMPTS) + @Param(description = "Number of attempts already made to complete this job.") + private Integer attempts; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "Compression job type.") + private String type; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Compression job start date.") + private Date startDate; + + @SerializedName(ApiConstants.SCHEDULED_DATE) + @Param(description = "Compression job scheduled start date.") + private Date scheduledDate; + + @SerializedName(ApiConstants.REMOVED) + @Param(description = "Compression job scheduled removed date.") + private Date removed; + + public BackupServiceJobResponse(Long id, String backupId, String zoneId, Integer attempts, String type, Date startDate, Date scheduledDate, Date removed) { + super("backupservicejob"); + this.id = id; + this.backupId = backupId; + this.zoneId = zoneId; + this.attempts = attempts; + this.type = type; + this.startDate = startDate; + this.scheduledDate = scheduledDate; + this.removed = removed; + } + + public void setHostId(String hostId) { + this.hostId = hostId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index 4d6eae2fad23..e08c6019f150 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -234,6 +234,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "The name of the backup offering of the Instance", since = "4.14") private String backupOfferingName; + @SerializedName(ApiConstants.BACKUP_PROVIDER) + @Param(description = "The name of the backup provider of the offering attached to the Instance", since = "4.23.0") + private String backupProvider; + @SerializedName("forvirtualnetwork") @Param(description = "The virtual Network for the service offering") private Boolean forVirtualNetwork; @@ -1362,4 +1366,11 @@ public void setLeaseExpiryDate(Date leaseExpiryDate) { this.leaseExpiryDate = leaseExpiryDate; } + public String getBackupProvider() { + return backupProvider; + } + + public void setBackupProvider(String backupProvider) { + this.backupProvider = backupProvider; + } } diff --git a/api/src/main/java/org/apache/cloudstack/backup/Backup.java b/api/src/main/java/org/apache/cloudstack/backup/Backup.java index 865d657a7a48..2124423da108 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/Backup.java +++ b/api/src/main/java/org/apache/cloudstack/backup/Backup.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.backup; +import java.io.Serializable; import java.util.Date; import java.util.List; import java.util.Map; @@ -46,6 +47,22 @@ enum Status { Hidden } + enum CompressionStatus { + Uncompressed, Compressing, FinalizingCompression, Compressed, CompressionError + } + + enum ValidationStatus { + NotValidated, Validating, Valid, UnableToValidate, NotValid + } + + enum ValidationSteps { + wait_for_boot, screenshot, execute_command + } + + enum CompressionLibrary { + zstd, zlib + } + class Metric { private Long backupSize = 0L; private Long dataSize = 0L; @@ -132,7 +149,7 @@ public void setDataSize(Long dataSize) { } } - class VolumeInfo { + class VolumeInfo implements Serializable { private String uuid; private Volume.Type type; private Long size; @@ -201,11 +218,14 @@ public String toString() { String getType(); Date getDate(); Backup.Status getStatus(); + Backup.CompressionStatus getCompressionStatus(); + Backup.ValidationStatus getValidationStatus(); Long getSize(); Long getProtectedSize(); void setName(String name); String getDescription(); void setDescription(String description); + Long getUncompressedSize(); List getBackedUpVolumes(); long getZoneId(); Map getDetails(); diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index 3d520f306264..30fc2bbec40d 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -20,6 +20,8 @@ import java.util.List; import java.util.Map; +import com.cloud.storage.Volume; +import com.cloud.vm.VirtualMachine; import com.cloud.capacity.Capacity; import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.api.command.admin.backup.CloneBackupOfferingCmd; @@ -31,17 +33,16 @@ import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; +import org.apache.cloudstack.api.command.user.backup.CreateBackupOfferingCmd; import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.Network; -import com.cloud.storage.Volume; import com.cloud.utils.Pair; import com.cloud.utils.component.Manager; import com.cloud.utils.component.PluggableService; -import com.cloud.vm.VirtualMachine; import com.cloud.vm.VmDiskInfo; /** @@ -138,6 +139,12 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer */ BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd); + /** + * Create a new Backup and Recovery policy to CloudStack. Currently only supported for KBOSS. + * @param cmd create backup offering cmd + */ + BackupOffering createBackupOffering(final CreateBackupOfferingCmd cmd); + List getBackupOfferingDomains(final Long offeringId); /** @@ -210,7 +217,7 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer /** * Restore a full VM from backup */ - boolean restoreBackup(final Long backupId); + boolean restoreBackup(final Long backupId, boolean quickRestore, Long hostId); Map getIpToNetworkMapFromBackup(Backup backup, boolean preserveIps, List networkIds); @@ -221,12 +228,12 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer /** * Restore a backup to a new Instance */ - boolean restoreBackupToVM(Long backupId, Long vmId) throws ResourceUnavailableException; + boolean restoreBackupToVM(Long backupId, Long vmId, boolean quickrestore) throws ResourceUnavailableException; /** * Restore a backed up volume and attach it to a VM */ - boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, final Long backupId, final Long vmId) throws Exception; + boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, final Long backupId, final Long vmId, boolean isQuickRestore, Long hostId) throws Exception; /** * Deletes a backup diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java index 4ae9148113a4..ffc32b6c4d08 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java @@ -63,6 +63,17 @@ public interface BackupProvider { */ boolean removeVMFromBackupOffering(VirtualMachine vm); + /** + * Removes the specified backup schedule from a virtual machine. + * + * @param vm the virtual machine from which the schedule will be removed. + * @param backupSchedule the backup schedule to be removed. + * @return {@code true} if the operation was successful; {@code false} otherwise. + */ + default boolean removeVMBackupSchedule(VirtualMachine vm, BackupSchedule backupSchedule) { + return true; + } + /** * Whether the provider will delete backups on removal of VM from the offering * @return boolean result @@ -73,11 +84,14 @@ public interface BackupProvider { * Starts and creates an adhoc backup process * for a previously registered VM backup * - * @param vm the machine to make a backup of - * @param quiesceVM instance will be quiesced for checkpointing for backup. Applicable only to NAS plugin. + * @param vm + * the machine to make a backup of + * @param quiesceVM + * instance will be quiesced for checkpointing for backup. Applicable only to NAS plugin. + * @param isolated * @return the result and {code}Backup{code} {code}Object{code} */ - Pair takeBackup(VirtualMachine vm, Boolean quiesceVM); + Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated, Long backupScheduleId); /** * Delete an existing backup @@ -99,17 +113,18 @@ default boolean handlesChainDeleteResourceAccounting() { return false; } - Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid); + Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore); /** * Restore VM from backup */ - boolean restoreVMFromBackup(VirtualMachine vm, Backup backup); + boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId); /** * Restore a volume from a backup */ - Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState); + Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore); /** * Syncs backup metrics (backup size, protected size) from the plugin and stores it within the provider @@ -152,5 +167,4 @@ default boolean supportsMemoryVmSnapshot() { * @param zoneId the zone for which to return metrics */ void syncBackupStorageStats(Long zoneId); - } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java index 44fdf70c4c15..ddc823a14ff5 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java @@ -34,4 +34,5 @@ public interface BackupSchedule extends ControlledEntity, InternalIdentity { Boolean getQuiesceVM(); int getMaxBackups(); String getUuid(); + boolean isIsolated(); } diff --git a/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java new file mode 100644 index 000000000000..efd28385a3e8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java @@ -0,0 +1,142 @@ +// 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.backup; + +import com.cloud.storage.Volume; +import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.snapshot.VMSnapshot; +import org.apache.cloudstack.framework.config.ConfigKey; + +import java.util.Set; + +public interface InternalBackupProvider extends BackupProvider { + String VM_WORK_JOB_HANDLER = InternalBackupService.class.getSimpleName(); + + ConfigKey backupCompressionTimeout = new ConfigKey<>("Advanced", Integer.class, "backup.compression.timeout", "28800", "Backup compression timeout (in " + + "seconds). Will only start counting once the backup compression async job actually starts. This setting is currently only applicable to KBOSS.", true, + ConfigKey.Scope.Cluster); + + ConfigKey backupCompressionMinimumFreeStorage = new ConfigKey<>("Advanced", Double.class, "backup.compression.minimum.free.storage", "1", "The minimum " + + "amount of free storage that should be available to start the compression. This configuration uses a multiplier on the backup size, by default, it needs the same " + + "amount of free storage as the backup uses while uncompressed. This setting is currently only applicable to KBOSS.", true, ConfigKey.Scope.Zone); + + ConfigKey backupCompressionCoroutines = new ConfigKey<>("Advanced", Integer.class, "backup.compression.coroutines", "1", "Number of parallel coroutines " + + "for the compression process. This is translated to qemu-img '-m' parameter. This setting is currently only applicable to KBOSS.", true, ConfigKey.Scope.Cluster); + + ConfigKey backupCompressionRateLimit = new ConfigKey<>("Advanced", Integer.class, "backup.compression.rate.limit", "0", "Limit the compression rate to " + + "this configuration's value (in MB/s). Values lower than 1 disable the limit. This setting is currently only applicable to KBOSS.", true, ConfigKey.Scope.Cluster); + + ConfigKey backupValidationTimeout = new ConfigKey<>("Advanced", Integer.class, "backup.validation.timeout", "3600", "Backup validation job timeout (in " + + "seconds). Will only start counting once the backup validation async job actually starts. This setting is currently only applicable to KBOSS.", true, ConfigKey.Scope.Cluster); + + /** + * Actually execute the backup after being queued. + * */ + default Pair orchestrateTakeBackup(Backup backup, boolean quiesceVm, boolean isolated) { + return null; + } + + /** + * Actually delete the backup after being queued. + * */ + default Boolean orchestrateDeleteBackup(Backup backup, boolean forced) { + return null; + } + + /** + * Actually restore the backup after being queued. + * */ + default Boolean orchestrateRestoreVMFromBackup(Backup backup, VirtualMachine vm, boolean quickRestore, Long hostId, boolean sameVmAsBackup) { + return null; + } + + /** + * This method should be overwritten by any backup providers that want to schedule their backup restore jobs in the same queue as the VM jobs. + * Otherwise, just use the restoreBackedUpVolume method. + * */ + default Pair orchestrateRestoreBackedUpVolume(Backup backup, VirtualMachine vm, Backup.VolumeInfo backupVolumeInfo, String hostIp, boolean quickRestore) { + return null; + } + + /** + * This method should be overwritten by any native backup providers that want to allow backup compression through ACS.
+ * The compression is done in two steps:
+ * 1) Compress the backup to a different file;
+ * 2) Switch the old file for the newly compressed one.
+ *

+ * This method is supposed to execute step 1. + * + * @return + */ + default boolean startBackupCompression(long backupId, long hostId) { + return false; + } + + /** + * This method should be overwritten by any native backup providers that want to allow backup compression through ACS.
+ * The compression is done in two steps:
+ * 1) Compress the backup to a different file;
+ * 2) Switch the old file for the newly compressed one.
+ *

+ * This method is supposed to execute step 2. + * + * @return + */ + default boolean finalizeBackupCompression(long backupId, long hostId) { + return false; + } + + default boolean validateBackup(long backupId, long hostId) { + return false; + } + + /** + * This method should be overwritten by any native backup providers that allow volume detach but need to prepare it beforehand. + * */ + default void prepareVolumeForDetach(Volume volume, VirtualMachine virtualMachine) { + } + + /** + * This method should be overwritten by any native backup providers that allow volume migration but need to prepare it beforehand. + * */ + default void prepareVolumeForMigration(Volume volume, VirtualMachine virtualMachine) { + } + + /** + * This method should be overwritten by any native backup providers that must update metadata regarding a volume after certain operations (such as after a volume migration). + * */ + default void updateVolumeId(VirtualMachine virtualMachine, long oldVolumeId, long newVolumeId) { + } + + default Set getSecondaryStorageUrls(UserVm userVm) { + return Set.of(); + } + + /** + * This method should be overwritten by any native backup providers that are compatible with VM Snapshots but need to prepare the VM to be reverted. + * Currently, the only strategy that calls this method is the {@code KvmFileBasedStorageVmSnapshotStrategy}. + * */ + default void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot, VirtualMachine virtualMachine) { + } + + default boolean finishBackupChains(VirtualMachine virtualMachine) { + return false; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/InternalBackupService.java b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupService.java new file mode 100644 index 000000000000..76c71f0eb4ce --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupService.java @@ -0,0 +1,54 @@ +// 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.backup; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.DataTO; +import com.cloud.storage.Volume; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.snapshot.VMSnapshot; +import org.apache.cloudstack.api.response.ExtractResponse; + +import java.util.Set; + +public interface InternalBackupService { + + void configureChainInfo(DataTO volumeTo, Command cmd); + + void cleanupBackupMetadata(long volumeId); + + void prepareVolumeForDetach(Volume volume, VirtualMachine virtualMachine); + + void prepareVolumeForMigration(Volume volume); + + void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot); + + void updateVolumeId(long oldVolumeId, long newVolumeId); + + Set getSecondaryStorageUrls(UserVm userVm); + + boolean startBackupCompression(long backupId, long hostId, long zoneId); + + boolean finalizeBackupCompression(long backupId, long hostId, long zoneId); + + boolean validateBackup(long backupId, long hostId, long zoneId); + + ExtractResponse downloadScreenshot(long backupId); + + boolean finishBackupChain(long vmId); +} diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index 5b053aafd84b..b6362e9a9c9b 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -41,6 +41,7 @@ import org.apache.cloudstack.api.command.user.account.ListProjectAccountsCmd; import org.apache.cloudstack.api.command.user.address.ListQuarantinedIpsCmd; import org.apache.cloudstack.api.command.user.affinitygroup.ListAffinityGroupsCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupServiceJobsCmd; import org.apache.cloudstack.api.command.user.bucket.ListBucketsCmd; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.iso.ListIsosCmd; @@ -63,6 +64,7 @@ import org.apache.cloudstack.api.command.user.zone.ListZonesCmd; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.AsyncJobResponse; +import org.apache.cloudstack.api.response.BackupServiceJobResponse; import org.apache.cloudstack.api.response.BucketResponse; import org.apache.cloudstack.api.response.DetailOptionsResponse; import org.apache.cloudstack.api.response.DiskOfferingResponse; @@ -119,7 +121,7 @@ public interface QueryService { "Determines whether users can view certain VM settings. When set to empty, default value used is: rootdisksize, cpuOvercommitRatio, memoryOvercommitRatio, Message.ReservedCapacityFreed.Flag.", true, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.CSV, null); ConfigKey UserVMReadOnlyDetails = new ConfigKey<>(String.class, - "user.vm.readonly.details", "Advanced", "dataDiskController, rootDiskController", + "user.vm.readonly.details", "Advanced", "dataDiskController, rootDiskController, backupValidationCommandTimeout, backupValidationScreenshotWait, backupValidationBootTimeout", "List of read-only VM settings/details as comma separated string", true, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.CSV, null, ""); ConfigKey SortKeyAscending = new ConfigKey<>("Advanced", Boolean.class, "sortkey.algorithm", "true", @@ -224,4 +226,6 @@ public interface QueryService { ListResponse searchForObjectStores(ListObjectStoragePoolsCmd listObjectStoragePoolsCmd); ListResponse searchForBuckets(ListBucketsCmd listBucketsCmd); + + ListResponse listBackupServiceJobs(ListBackupServiceJobsCmd cmd); } diff --git a/api/src/main/java/org/apache/cloudstack/secstorage/heuristics/HeuristicType.java b/api/src/main/java/org/apache/cloudstack/secstorage/heuristics/HeuristicType.java index f23e4b0b633b..489e9bf54f23 100644 --- a/api/src/main/java/org/apache/cloudstack/secstorage/heuristics/HeuristicType.java +++ b/api/src/main/java/org/apache/cloudstack/secstorage/heuristics/HeuristicType.java @@ -18,8 +18,8 @@ /** * The type of the heuristic used in the allocation process of secondary storage resources. - * Valid options are: {@link #ISO}, {@link #SNAPSHOT}, {@link #TEMPLATE} and {@link #VOLUME} + * Valid options are: {@link #ISO}, {@link #SNAPSHOT}, {@link #TEMPLATE}, {@link #VOLUME} and {@link #BACKUP} */ public enum HeuristicType { - ISO, SNAPSHOT, TEMPLATE, VOLUME + ISO, SNAPSHOT, TEMPLATE, VOLUME, BACKUP } diff --git a/client/pom.xml b/client/pom.xml index 90b839112ce9..968f735af600 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -627,6 +627,11 @@ cloud-plugin-integrations-veeam-control-service ${project.version} + + org.apache.cloudstack + cloud-plugin-backup-kvm-backup-on-secondary-storage + ${project.version} + org.apache.cloudstack cloud-plugin-integrations-kubernetes-service diff --git a/core/src/main/java/com/cloud/agent/api/MigrateBackupsBetweenSecondaryStoragesCommand.java b/core/src/main/java/com/cloud/agent/api/MigrateBackupsBetweenSecondaryStoragesCommand.java new file mode 100644 index 000000000000..7794b4a53083 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/MigrateBackupsBetweenSecondaryStoragesCommand.java @@ -0,0 +1,41 @@ +// 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 com.cloud.agent.api; + +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; + +import java.util.List; + +public class MigrateBackupsBetweenSecondaryStoragesCommand extends MigrateBetweenSecondaryStoragesCommand { + + List> backupChain; + + public MigrateBackupsBetweenSecondaryStoragesCommand() { + } + + public MigrateBackupsBetweenSecondaryStoragesCommand(List> backupChain, DataStoreTO srcDataStore, DataStoreTO destDataStore) { + super(srcDataStore, destDataStore); + this.backupChain = backupChain; + } + + public List> getBackupChain() { + return backupChain; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommand.java b/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommand.java new file mode 100644 index 000000000000..48dc7e2c85f4 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommand.java @@ -0,0 +1,48 @@ +// 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 com.cloud.agent.api; + +import com.cloud.agent.api.to.DataStoreTO; + +public abstract class MigrateBetweenSecondaryStoragesCommand extends Command { + + DataStoreTO srcDataStore; + DataStoreTO destDataStore; + + public MigrateBetweenSecondaryStoragesCommand() { + } + + public MigrateBetweenSecondaryStoragesCommand(DataStoreTO srcDataStore, DataStoreTO destDataStore) { + this.srcDataStore = srcDataStore; + this.destDataStore = destDataStore; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public DataStoreTO getSrcDataStore() { + return srcDataStore; + } + + public DataStoreTO getDestDataStore() { + return destDataStore; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommandAnswer.java b/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommandAnswer.java new file mode 100644 index 000000000000..fd303093e092 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommandAnswer.java @@ -0,0 +1,41 @@ +// +// 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 com.cloud.agent.api; + +import com.cloud.utils.Pair; + +import java.util.List; + +public class MigrateBetweenSecondaryStoragesCommandAnswer extends Answer { + + List> migratedResourcesIdAndCheckpointPath; + + public MigrateBetweenSecondaryStoragesCommandAnswer() { + } + + public MigrateBetweenSecondaryStoragesCommandAnswer(MigrateBetweenSecondaryStoragesCommand cmd, boolean success, String result, List> migratedResourcesIdAndCheckpointPath) { + super(cmd, success, result); + this.migratedResourcesIdAndCheckpointPath = migratedResourcesIdAndCheckpointPath; + } + + public List> getMigratedResources() { + return migratedResourcesIdAndCheckpointPath; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/StartCommand.java b/core/src/main/java/com/cloud/agent/api/StartCommand.java index 24b0ac3787b5..ea763d6cd4bd 100644 --- a/core/src/main/java/com/cloud/agent/api/StartCommand.java +++ b/core/src/main/java/com/cloud/agent/api/StartCommand.java @@ -22,13 +22,15 @@ import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.host.Host; +import java.util.List; + /** */ public class StartCommand extends Command { VirtualMachineTO vm; String hostIp; boolean executeInSequence = false; - String secondaryStorage; + private List secondaryStorages; public VirtualMachineTO getVirtualMachine() { return vm; @@ -50,18 +52,18 @@ public StartCommand(VirtualMachineTO vm, Host host, boolean executeInSequence) { this.vm = vm; this.hostIp = host.getPrivateIpAddress(); this.executeInSequence = executeInSequence; - this.secondaryStorage = null; + this.secondaryStorages = null; } public String getHostIp() { return this.hostIp; } - public String getSecondaryStorage() { - return this.secondaryStorage; + public List getSecondaryStorages() { + return this.secondaryStorages; } - public void setSecondaryStorage(String secondary) { - this.secondaryStorage = secondary; + public void setSecondaryStorages(List secondary) { + this.secondaryStorages = secondary; } } diff --git a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java index 4d61249c7cbc..9a571e34c7e6 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java @@ -20,20 +20,19 @@ import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; -import com.cloud.utils.Pair; import java.util.Map; public class CreateDiskOnlyVmSnapshotAnswer extends Answer { - protected Map> mapVolumeToSnapshotSizeAndNewVolumePath; + protected Map mapVolumeToSnapshotSize; - public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map> mapVolumeToSnapshotSizeAndNewVolumePath) { + public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map mapVolumeToSnapshotSize) { super(command, success, details); - this.mapVolumeToSnapshotSizeAndNewVolumePath = mapVolumeToSnapshotSizeAndNewVolumePath; + this.mapVolumeToSnapshotSize = mapVolumeToSnapshotSize; } - public Map> getMapVolumeToSnapshotSizeAndNewVolumePath() { - return mapVolumeToSnapshotSizeAndNewVolumePath; + public Map getMapVolumeToSnapshotSize() { + return mapVolumeToSnapshotSize; } } diff --git a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java index 952bf0c971de..da1b420625f8 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java +++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java @@ -21,6 +21,7 @@ import com.cloud.agent.api.VMSnapshotBaseCommand; import com.cloud.agent.api.VMSnapshotTO; +import com.cloud.utils.Pair; import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -30,12 +31,19 @@ public class CreateDiskOnlyVmSnapshotCommand extends VMSnapshotBaseCommand { protected VirtualMachine.State vmState; - public CreateDiskOnlyVmSnapshotCommand(String vmName, VMSnapshotTO snapshot, List volumeTOs, String guestOSType, VirtualMachine.State vmState) { - super(vmName, snapshot, volumeTOs, guestOSType); + List> volumeTosAndNewPaths; + + public CreateDiskOnlyVmSnapshotCommand(String vmName, VMSnapshotTO snapshot, List> volumeTosAndNewPaths, String guestOSType, VirtualMachine.State vmState) { + super(vmName, snapshot, null, guestOSType); this.vmState = vmState; + this.volumeTosAndNewPaths = volumeTosAndNewPaths; } public VirtualMachine.State getVmState() { return vmState; } + + public List> getVolumeTosAndNewPaths() { + return volumeTosAndNewPaths; + } } diff --git a/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java index b6396c24d10a..1a47d97d5e25 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java +++ b/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java @@ -19,28 +19,28 @@ package com.cloud.agent.api.storage; import com.cloud.agent.api.Command; -import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; import java.util.List; public class MergeDiskOnlyVmSnapshotCommand extends Command { - private List snapshotMergeTreeToList; - private VirtualMachine.State vmState; + private List snapshotMergeTreeToList; + private boolean isVmRunning; private String vmName; - public MergeDiskOnlyVmSnapshotCommand(List snapshotMergeTreeToList, VirtualMachine.State vmState, String vmName) { + public MergeDiskOnlyVmSnapshotCommand(List snapshotMergeTreeToList, boolean isVmRunning, String vmName) { this.snapshotMergeTreeToList = snapshotMergeTreeToList; - this.vmState = vmState; + this.isVmRunning = isVmRunning; this.vmName = vmName; } - public List getSnapshotMergeTreeToList() { + public List getDeltaMergeTreeToList() { return snapshotMergeTreeToList; } - public VirtualMachine.State getVmState() { - return vmState; + public boolean isVmRunning() { + return isVmRunning; } public String getVmName() { diff --git a/core/src/main/java/com/cloud/storage/resource/StorageProcessor.java b/core/src/main/java/com/cloud/storage/resource/StorageProcessor.java index dd8e2abcd643..31c384eab3a0 100644 --- a/core/src/main/java/com/cloud/storage/resource/StorageProcessor.java +++ b/core/src/main/java/com/cloud/storage/resource/StorageProcessor.java @@ -85,4 +85,8 @@ public interface StorageProcessor { public Answer checkDataStoreStoragePolicyCompliance(CheckDataStoreStoragePolicyComplianceCommand cmd); public Answer syncVolumePath(SyncVolumePathCommand cmd); + + default Answer deleteBackup(DeleteCommand cmd) { + return new Answer(cmd, false, "Operation not implemented"); + } } diff --git a/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java b/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java index 318c069b0b0b..3d2608c0c4b8 100644 --- a/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java +++ b/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java @@ -154,6 +154,8 @@ protected Answer execute(DeleteCommand cmd) { answer = processor.deleteVolume(cmd); } else if (data.getObjectType() == DataObjectType.SNAPSHOT) { answer = processor.deleteSnapshot(cmd); + } else if (data.getObjectType() == DataObjectType.BACKUP) { + answer = processor.deleteBackup(cmd); } else { answer = new Answer(cmd, false, "unsupported type"); } diff --git a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java new file mode 100644 index 000000000000..042047b59358 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java @@ -0,0 +1,46 @@ +/* + * 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.backup; + +import java.util.Map; + +import org.apache.commons.collections4.MapUtils; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.utils.Pair; + +public class CleanupKbossBackupErrorAnswer extends Answer { + private Map> volumeIdToPathAndChainEnded; + private boolean vmRunning; + + public CleanupKbossBackupErrorAnswer(Command cmd, Map> volumeIdToPathAndChainEnded, boolean vmRunning) { + super(cmd, MapUtils.isNotEmpty(volumeIdToPathAndChainEnded), null); + this.volumeIdToPathAndChainEnded = volumeIdToPathAndChainEnded; + this.vmRunning = vmRunning; + } + + public Map> getVolumeIdToPathAndChainEnded() { + return volumeIdToPathAndChainEnded; + } + + public boolean isVmRunning() { + return vmRunning; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java new file mode 100644 index 000000000000..e5cd5a7f8150 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java @@ -0,0 +1,82 @@ +// 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.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.KbossTO; + +import java.util.List; + +public class CleanupKbossBackupErrorCommand extends Command { + + private boolean runningVM; + + private boolean errorOnCreate; + + private boolean endOfChain; + + private boolean isTopDelta; + + private String vmName; + + private String imageStoreUrl; + + private List kbossTOS; + + public CleanupKbossBackupErrorCommand(boolean runningVM, boolean errorOnCreate, boolean endOfChain, boolean isTopDelta, String vmName, String imageStoreUrl, List kbossTOS) { + this.errorOnCreate = errorOnCreate; + this.runningVM = runningVM; + this.endOfChain = endOfChain; + this.isTopDelta = isTopDelta; + this.vmName = vmName; + this.imageStoreUrl = imageStoreUrl; + this.kbossTOS = kbossTOS; + } + + public boolean isErrorOnCreate() { + return errorOnCreate; + } + + public boolean isEndOfChain() { + return endOfChain; + } + + public boolean isTopDelta() { + return isTopDelta; + } + + public boolean isRunningVM() { + return runningVM; + } + + public String getVmName() { + return vmName; + } + + public String getImageStoreUrl() { + return imageStoreUrl; + } + + public List getKbossTOs() { + return kbossTOS; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossValidationCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossValidationCommand.java new file mode 100644 index 000000000000..8a345aeba176 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossValidationCommand.java @@ -0,0 +1,49 @@ +// +// 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.backup; + +import com.cloud.agent.api.Command; + +import java.util.Set; + +public class CleanupKbossValidationCommand extends Command { + + private String vmName; + + private Set secondaryStorages; + + public CleanupKbossValidationCommand(String vmName, Set secondaryStorages) { + this.vmName = vmName; + this.secondaryStorages = secondaryStorages; + } + + public String getVmName() { + return vmName; + } + + public Set getSecondaryStorages() { + return secondaryStorages; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CompressBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CompressBackupCommand.java new file mode 100644 index 000000000000..a551f246a0bb --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CompressBackupCommand.java @@ -0,0 +1,78 @@ +// +// 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.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; + +import java.util.List; + +public class CompressBackupCommand extends Command { + + private List backupDeltasToCompress; + + private List backupChainImageStoreUrls; + + private long minFreeStorage; + + private Backup.CompressionLibrary compressionLib; + + private int coroutines; + + private int rateLimit; + + public CompressBackupCommand(List backupDeltasToCompress, List backupChainImageStoreUrls, long minFreeStorage, Backup.CompressionLibrary compressionLib, int coroutines, int rateLimit) { + this.backupChainImageStoreUrls = backupChainImageStoreUrls; + this.backupDeltasToCompress = backupDeltasToCompress; + this.minFreeStorage = minFreeStorage; + this.compressionLib = compressionLib; + this.coroutines = coroutines; + this.rateLimit = rateLimit; + } + + public List getBackupDeltasToCompress() { + return backupDeltasToCompress; + } + + public List getBackupChainImageStoreUrls() { + return backupChainImageStoreUrls; + } + + public long getMinFreeStorage() { + return minFreeStorage; + } + + public Backup.CompressionLibrary getCompressionLib() { + return compressionLib; + } + + public int getCoroutines() { + return coroutines; + } + + public int getRateLimit() { + return rateLimit; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesAnswer.java new file mode 100644 index 000000000000..9f230deb8d75 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesAnswer.java @@ -0,0 +1,37 @@ +// 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.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.List; + +public class ConsolidateVolumesAnswer extends Answer { + + private List successfullyConsolidatedVolumes; + + public ConsolidateVolumesAnswer(Command command, boolean success, String details, List successfullyConsolidatedVolumes) { + super(command, success, details); + this.successfullyConsolidatedVolumes = successfullyConsolidatedVolumes; + } + + public List getSuccessfullyConsolidatedVolumes() { + return successfullyConsolidatedVolumes; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesCommand.java b/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesCommand.java new file mode 100644 index 000000000000..7b2bc2245939 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesCommand.java @@ -0,0 +1,56 @@ +// 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.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.List; +import java.util.stream.Collectors; + +public class ConsolidateVolumesCommand extends Command { + + private List volumesToConsolidate; + + private List secondaryStorageUuids; + + private String vmName; + + public ConsolidateVolumesCommand(List volumesToConsolidate, List secondaryStorageUuids, String vmName) { + this.volumesToConsolidate = volumesToConsolidate.stream().map(vol -> (VolumeObjectTO)vol.getTO()).collect(Collectors.toList()); + this.secondaryStorageUuids = secondaryStorageUuids; + this.vmName = vmName; + } + + public List getVolumesToConsolidate() { + return volumesToConsolidate; + } + + public List getSecondaryStorageUuids() { + return secondaryStorageUuids; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/FinalizeBackupCompressionCommand.java b/core/src/main/java/org/apache/cloudstack/backup/FinalizeBackupCompressionCommand.java new file mode 100644 index 000000000000..a4cbb69611f7 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/FinalizeBackupCompressionCommand.java @@ -0,0 +1,49 @@ +// +// 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.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +import java.util.List; + +public class FinalizeBackupCompressionCommand extends Command { + private boolean cleanup; + + private List backupDeltaTO; + + public FinalizeBackupCompressionCommand(boolean cleanup, List backupDeltaTO) { + this.cleanup = cleanup; + this.backupDeltaTO = backupDeltaTO; + } + + public boolean isCleanup() { + return cleanup; + } + + public List getBackupDeltaTOList() { + return backupDeltaTO; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/PrepareValidationCommand.java b/core/src/main/java/org/apache/cloudstack/backup/PrepareValidationCommand.java new file mode 100644 index 000000000000..bae2fee95774 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/PrepareValidationCommand.java @@ -0,0 +1,52 @@ +// +// 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.backup; + +import com.cloud.agent.api.Command; +import com.cloud.utils.Pair; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.List; +import java.util.Set; + +public class PrepareValidationCommand extends Command { + + private List> backupToVolumeList; + + private Set imageStoreSet; + + public PrepareValidationCommand(List> backupToVolumeList, Set imageStoreSet) { + this.backupToVolumeList = backupToVolumeList; + this.imageStoreSet = imageStoreSet; + } + + public List> getBackupToVolumeList() { + return backupToVolumeList; + } + + public Set getImageStoreSet() { + return imageStoreSet; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupAnswer.java new file mode 100644 index 000000000000..925ceca6b3b3 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupAnswer.java @@ -0,0 +1,41 @@ +// 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.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +import java.util.Set; + +public class RestoreKbossBackupAnswer extends Answer { + + private Set secondaryStorageUuids; + + public RestoreKbossBackupAnswer(Command command, Set secondaryStorageUuids) { + super(command); + this.secondaryStorageUuids = secondaryStorageUuids; + } + + public RestoreKbossBackupAnswer(Command command, Exception e, Set secondaryStorageUuids) { + super(command, e); + this.secondaryStorageUuids = secondaryStorageUuids; + } + + public Set getSecondaryStorageUuids() { + return secondaryStorageUuids; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupCommand.java new file mode 100644 index 000000000000..d173aab324a2 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupCommand.java @@ -0,0 +1,66 @@ +// +// 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.backup; + +import com.cloud.agent.api.Command; +import com.cloud.utils.Pair; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.Set; + +public class RestoreKbossBackupCommand extends Command { + + private Set deltasToRemove; + + private Set> backupAndVolumePairs; + + private Set secondaryStorageUrls; + + private boolean quickRestore; + + public RestoreKbossBackupCommand(Set deltasToRemove, Set> backupAndVolumePairs, Set secondaryStorageUrls, + boolean quickRestore) { + this.deltasToRemove = deltasToRemove; + this.backupAndVolumePairs = backupAndVolumePairs; + this.secondaryStorageUrls = secondaryStorageUrls; + this.quickRestore = quickRestore; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public Set getDeltasToRemove() { + return deltasToRemove; + } + + public Set> getBackupAndVolumePairs() { + return backupAndVolumePairs; + } + + public Set getSecondaryStorageUrls() { + return secondaryStorageUrls; + } + + public boolean isQuickRestore() { + return quickRestore; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupHashCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupHashCommand.java new file mode 100644 index 000000000000..7effe40686b2 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupHashCommand.java @@ -0,0 +1,47 @@ +// 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.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +import java.util.List; + +public class TakeBackupHashCommand extends Command { + + private List backupDeltaTOList; + + private String backupUuid; + + public TakeBackupHashCommand(List backupDeltaTOList, String backupUuid) { + this.backupDeltaTOList = backupDeltaTOList; + this.backupUuid = backupUuid; + } + + public List getBackupDeltaTOList() { + return backupDeltaTOList; + } + + public String getBackupUuid() { + return backupUuid; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupAnswer.java new file mode 100644 index 000000000000..1827c766f573 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupAnswer.java @@ -0,0 +1,59 @@ +// +// 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.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; + +import java.util.Map; + +public class TakeKbossBackupAnswer extends Answer { + + private Map mapVolumeUuidToNewVolumePath; + private Map> mapVolumeUuidToDeltaPathOnSecondaryAndSize; + private boolean isVmConsistent = true; + + public TakeKbossBackupAnswer(Command command, boolean success, Map mapVolumeUuidToNewVolumePath, + Map> mapVolumeUuidToDeltaPathOnSecondaryAndSize) { + super(command, success, null); + this.mapVolumeUuidToNewVolumePath = mapVolumeUuidToNewVolumePath; + this.mapVolumeUuidToDeltaPathOnSecondaryAndSize = mapVolumeUuidToDeltaPathOnSecondaryAndSize; + } + + public TakeKbossBackupAnswer(Command command, Exception e) { + super(command, e); + if (e instanceof BackupException) { + this.isVmConsistent = ((BackupException)e).isVmConsistent(); + } + } + + public Map getMapVolumeUuidToNewVolumePath() { + return mapVolumeUuidToNewVolumePath; + } + + public Map> getMapVolumeUuidToDeltaPathOnSecondaryAndSize() { + return mapVolumeUuidToDeltaPathOnSecondaryAndSize; + } + + public boolean isVmConsistent() { + return isVmConsistent; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupCommand.java new file mode 100644 index 000000000000..b145fa6257b1 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupCommand.java @@ -0,0 +1,92 @@ +// +// 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.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.KbossTO; + +import java.util.List; + +public class TakeKbossBackupCommand extends Command { + + private boolean quiesceVm; + + private boolean runningVM; + + private boolean endChain; + + private String vmName; + + private String imageStoreUrl; + + private List backupChainImageStoreUrls; + + private List kbossTOS; + + private boolean isolated; + + public TakeKbossBackupCommand(boolean quiesceVm, boolean runningVM, boolean endChain, String vmName, String imageStoreUrl, List backupChainImageStoreUrls, List kbossTOS, boolean isolated) { + this.quiesceVm = quiesceVm; + this.runningVM = runningVM; + this.endChain = endChain; + this.vmName = vmName; + this.imageStoreUrl = imageStoreUrl; + this.backupChainImageStoreUrls = backupChainImageStoreUrls; + this.kbossTOS = kbossTOS; + this.isolated = isolated; + } + + public boolean isQuiesceVm() { + return quiesceVm; + } + + public boolean isRunningVM() { + return runningVM; + } + + public boolean isEndChain() { + return endChain; + } + + public String getVmName() { + return vmName; + } + + public String getImageStoreUrl() { + return imageStoreUrl; + } + + public List getBackupChainImageStoreUrls() { + return backupChainImageStoreUrls; + } + + public List getKbossTOs() { + return kbossTOS; + } + + public boolean isIsolated() { + return isolated; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmAnswer.java new file mode 100644 index 000000000000..fcff6a8cce8c --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmAnswer.java @@ -0,0 +1,46 @@ +// 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.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class ValidateKbossVmAnswer extends Answer { + + private boolean bootValidated; + private String screenshotPath; + private String scriptResult; + + public ValidateKbossVmAnswer(Command command, boolean bootValidated, String screenshotPath, String scriptResult) { + super(command); + this.bootValidated = bootValidated; + this.screenshotPath = screenshotPath; + this.scriptResult = scriptResult; + } + + public boolean isBootValidated() { + return bootValidated; + } + + public String getScreenshotPath() { + return screenshotPath; + } + + public String getScriptResult() { + return scriptResult; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmCommand.java b/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmCommand.java new file mode 100644 index 000000000000..9e10499f53d9 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmCommand.java @@ -0,0 +1,133 @@ +// +// 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.backup; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.VirtualMachineTO; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +public class ValidateKbossVmCommand extends Command { + + private VirtualMachineTO vm; + private BackupDeltaTO backupDeltaTO; + + private String scriptToExecute; + + private String scriptArguments; + private String expectedResult; + + private Integer scriptTimeout; + private Integer bootTimeout; + private Integer screenshotWait; + + private boolean takeScreenshot; + private boolean waitForBoot; + private boolean executeScript; + + public ValidateKbossVmCommand(VirtualMachineTO vm, BackupDeltaTO backupDeltaTO) { + this.vm = vm; + this.backupDeltaTO = backupDeltaTO; + } + + public void setScriptToExecute(String scriptToExecute) { + this.scriptToExecute = scriptToExecute; + } + + public void setScriptArguments(String scriptArguments) { + this.scriptArguments = scriptArguments; + } + + public void setExpectedResult(String expectedResult) { + this.expectedResult = expectedResult; + } + + public void setScriptTimeout(Integer scriptTimeout) { + this.scriptTimeout = scriptTimeout; + } + + public void setTakeScreenshot(boolean takeScreenshot) { + this.takeScreenshot = takeScreenshot; + } + + public void setWaitForBoot(boolean waitForBoot) { + this.waitForBoot = waitForBoot; + } + + public void setExecuteScript(boolean executeScript) { + this.executeScript = executeScript; + } + + public void setBootTimeout(Integer bootTimeout) { + this.bootTimeout = bootTimeout; + } + + public void setScreenshotWait(Integer screenshotWait) { + this.screenshotWait = screenshotWait; + } + + public VirtualMachineTO getVm() { + return vm; + } + + public BackupDeltaTO getBackupDeltaTO() { + return backupDeltaTO; + } + + public String getScriptToExecute() { + return scriptToExecute; + } + + public String getScriptArguments() { + return scriptArguments; + } + + public String getExpectedResult() { + return expectedResult; + } + + public Integer getScriptTimeout() { + return scriptTimeout; + } + + public Integer getBootTimeout() { + return bootTimeout; + } + + public Integer getScreenshotWait() { + return screenshotWait; + } + + public boolean isTakeScreenshot() { + return takeScreenshot; + } + + public boolean isWaitForBoot() { + return waitForBoot; + } + + public boolean isExecuteScript() { + return executeScript; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/BackupDeleteAnswer.java b/core/src/main/java/org/apache/cloudstack/storage/command/BackupDeleteAnswer.java new file mode 100644 index 000000000000..6cc48ea296d1 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/command/BackupDeleteAnswer.java @@ -0,0 +1,36 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +package org.apache.cloudstack.storage.command; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class BackupDeleteAnswer extends Answer { + + private long backupId; + + public BackupDeleteAnswer(Command command, boolean success, String details) { + super(command, success, details); + backupId = ((DeleteCommand) command).getData().getId(); + } + + public long getBackupId() { + return backupId; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/DeleteCommand.java b/core/src/main/java/org/apache/cloudstack/storage/command/DeleteCommand.java index 6f82fa97818d..9aa6f26b5d9c 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/command/DeleteCommand.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/DeleteCommand.java @@ -24,6 +24,8 @@ public final class DeleteCommand extends StorageSubSystemCommand { private DataTO data; + private boolean deleteChain; + public DeleteCommand(final DataTO data) { super(); this.data = data; @@ -42,6 +44,14 @@ public DataTO getData() { return data; } + public void setDeleteChain(boolean deleteChain) { + this.deleteChain = deleteChain; + } + + public boolean isDeleteChain() { + return deleteChain; + } + @Override public void setExecuteInSequence(final boolean inSeq) { diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/RevertSnapshotCommand.java b/core/src/main/java/org/apache/cloudstack/storage/command/RevertSnapshotCommand.java index 174302252a55..42926d37cdfe 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/command/RevertSnapshotCommand.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/RevertSnapshotCommand.java @@ -25,6 +25,8 @@ public final class RevertSnapshotCommand extends StorageSubSystemCommand { private SnapshotObjectTO dataOnPrimaryStorage; private boolean _executeInSequence = false; + private boolean deleteChain; + public RevertSnapshotCommand(SnapshotObjectTO data, SnapshotObjectTO dataOnPrimaryStorage) { super(); this.data = data; @@ -43,6 +45,14 @@ public SnapshotObjectTO getDataOnPrimaryStorage() { return dataOnPrimaryStorage; } + public boolean isDeleteChain() { + return deleteChain; + } + + public void setDeleteChain(boolean deleteChain) { + this.deleteChain = deleteChain; + } + @Override public void setExecuteInSequence(final boolean executeInSequence) { _executeInSequence = executeInSequence; diff --git a/core/src/main/java/org/apache/cloudstack/storage/to/BackupDeltaTO.java b/core/src/main/java/org/apache/cloudstack/storage/to/BackupDeltaTO.java new file mode 100644 index 000000000000..662a2d486560 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/to/BackupDeltaTO.java @@ -0,0 +1,102 @@ +// 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.storage.to; + +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.Storage; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class BackupDeltaTO implements DataTO { + private DataStoreTO dataStoreTO; + + private Hypervisor.HypervisorType hypervisorType; + + private String path; + + private String screenshotPath; + + private Storage.ImageFormat format; + + // When set, represents the Backup ID, not the delta ID. + private long id = 0; + + public BackupDeltaTO(DataStoreTO dataStoreTO, Hypervisor.HypervisorType hypervisorType, String path) { + this.dataStoreTO = dataStoreTO; + this.hypervisorType = hypervisorType; + this.path = path; + this.format = Storage.ImageFormat.QCOW2; + } + + public BackupDeltaTO(long id, DataStoreTO dataStoreTO, Hypervisor.HypervisorType hypervisorType, String path) { + this(dataStoreTO, hypervisorType, path); + this.id = id; + } + + @Override + public DataObjectType getObjectType() { + return DataObjectType.BACKUP; + } + + @Override + public DataStoreTO getDataStore() { + return dataStoreTO; + } + + @Override + public Hypervisor.HypervisorType getHypervisorType() { + return hypervisorType; + } + + @Override + public String getPath() { + return path; + } + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Storage.ImageFormat getFormat() { + return this.format; + } + + public void setScreenshotPath(String screenshotPath) { + this.screenshotPath = screenshotPath; + } + + public String getScreenshotPath() { + return screenshotPath; + } + + public void setPath(String path) { + this.path = path; + } + + @Override + public String toString() { + return new ReflectionToStringBuilder(this, ToStringStyle.JSON_STYLE).setExcludeFieldNames("id").toString(); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java b/core/src/main/java/org/apache/cloudstack/storage/to/DeltaMergeTreeTO.java similarity index 71% rename from core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java rename to core/src/main/java/org/apache/cloudstack/storage/to/DeltaMergeTreeTO.java index 78f23105e192..143cf6fe22bb 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java +++ b/core/src/main/java/org/apache/cloudstack/storage/to/DeltaMergeTreeTO.java @@ -16,28 +16,40 @@ * specific language governing permissions and limitations * under the License. */ -package com.cloud.agent.api.storage; +package org.apache.cloudstack.storage.to; import com.cloud.agent.api.to.DataTO; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import java.util.List; -public class SnapshotMergeTreeTO { +public class DeltaMergeTreeTO { + + VolumeObjectTO volumeObjectTO; DataTO parent; DataTO child; List grandChildren; - public SnapshotMergeTreeTO(DataTO parent, DataTO child, List grandChildren) { + public DeltaMergeTreeTO(VolumeObjectTO volumeObjectTO, DataTO parent, DataTO child, List grandChildren) { + this.volumeObjectTO = volumeObjectTO; this.parent = parent; this.child = child; this.grandChildren = grandChildren; } + public VolumeObjectTO getVolumeObjectTO() { + return volumeObjectTO; + } + public DataTO getParent() { return parent; } + public void setParent(DataTO parent) { + this.parent = parent; + } + public DataTO getChild() { return child; } @@ -52,6 +64,6 @@ public void addGrandChild(DataTO grandChild) { @Override public String toString() { - return ReflectionToStringBuilder.toString(this); + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); } } diff --git a/core/src/main/java/org/apache/cloudstack/storage/to/KbossTO.java b/core/src/main/java/org/apache/cloudstack/storage/to/KbossTO.java new file mode 100644 index 000000000000..4a3f53a6dad4 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/to/KbossTO.java @@ -0,0 +1,110 @@ +package org.apache.cloudstack.storage.to; +// 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. + +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class KbossTO { + + private String pathBackupParentOnSecondary; + private VolumeObjectTO volumeObjectTO; + private String deltaPathOnPrimary; + private String parentDeltaPathOnPrimary; + private String deltaPathOnSecondary; + private String oldVolumePath; + + private DeltaMergeTreeTO deltaMergeTreeTO; + + private List deltaPaths; + + public KbossTO(VolumeObjectTO volumeObjectTO, LinkedList deltaPaths) { + this.volumeObjectTO = volumeObjectTO; + this.deltaPaths = deltaPaths; + } + + public KbossTO(VolumeObjectTO volumeObjectTO, String deltaPathOnPrimary, String deltaPathOnSecondary, LinkedList deltaPaths) { + this.volumeObjectTO = volumeObjectTO; + this.deltaPathOnPrimary = deltaPathOnPrimary; + this.deltaPathOnSecondary = deltaPathOnSecondary; + this.deltaPaths = deltaPaths; + } + + public String getPathBackupParentOnSecondary() { + return pathBackupParentOnSecondary; + } + + public VolumeObjectTO getVolumeObjectTO() { + return volumeObjectTO; + } + + public DeltaMergeTreeTO getDeltaMergeTreeTO() { + return deltaMergeTreeTO; + } + + public List getDeltaPaths() { + return deltaPaths; + } + + public String getDeltaPathOnPrimary() { + return deltaPathOnPrimary; + } + + public String getDeltaPathOnSecondary() { + return deltaPathOnSecondary; + } + + public String getParentDeltaPathOnPrimary() { + return parentDeltaPathOnPrimary; + } + + public void setParentDeltaPathOnPrimary(String parentDeltaPathOnPrimary) { + this.parentDeltaPathOnPrimary = parentDeltaPathOnPrimary; + } + + public void setPathBackupParentOnSecondary(String pathBackupParentOnSecondary) { + this.pathBackupParentOnSecondary = pathBackupParentOnSecondary; + } + + public void setDeltaMergeTreeTO(DeltaMergeTreeTO deltaMergeTreeTO) { + this.deltaMergeTreeTO = deltaMergeTreeTO; + } + + public void setDeltaPathOnPrimary(String deltaPathOnPrimary) { + this.deltaPathOnPrimary = deltaPathOnPrimary; + } + + public void setDeltaPathOnSecondary(String deltaPathOnSecondary) { + this.deltaPathOnSecondary = deltaPathOnSecondary; + } + + public String getOldVolumePath() { + return oldVolumePath; + } + + public void setOldVolumePath(String oldVolumePath) { + this.oldVolumePath = oldVolumePath; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java b/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java index 827403ac5ef8..5b1d4c573b68 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java +++ b/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java @@ -32,11 +32,12 @@ import com.cloud.storage.Volume; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import java.io.Serializable; import java.util.Arrays; import java.util.List; import java.util.Set; -public class VolumeObjectTO extends DownloadableObjectTO implements DataTO { +public class VolumeObjectTO extends DownloadableObjectTO implements DataTO, Serializable { private String uuid; private Volume.Type volumeType; private DataStoreTO dataStore; @@ -80,6 +81,7 @@ public class VolumeObjectTO extends DownloadableObjectTO implements DataTO { private String encryptFormat; private List checkpointPaths; private Set checkpointImageStoreUrls; + private Set deltasToRemove; public VolumeObjectTO() { @@ -424,4 +426,12 @@ public Set getCheckpointImageStoreUrls() { public void setCheckpointImageStoreUrls(Set checkpointImageStoreUrls) { this.checkpointImageStoreUrls = checkpointImageStoreUrls; } + + public Set getDeltasToRemove() { + return deltasToRemove; + } + + public void setDeltasToRemove(Set deltasToRemove) { + this.deltasToRemove = deltasToRemove; + } } diff --git a/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml index 175d45e26752..fcbcb18c2bdf 100644 --- a/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml +++ b/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml @@ -29,4 +29,9 @@ + + + + + diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index 0a92e8a637bc..cf43b8527a97 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -339,6 +339,10 @@ class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry"> + + + diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java index 56624df1346b..141596407fe8 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java @@ -122,7 +122,7 @@ VolumeInfo moveVolume(VolumeInfo volume, long destPoolDcId, Long destPoolPodId, DiskProfile allocateRawVolume(Type type, String name, DiskOffering offering, Long size, Long minIops, Long maxIops, VirtualMachine vm, VirtualMachineTemplate template, Account owner, Long deviceId, Long kmsKeyId, boolean incrementResourceCount); - VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo volume, HypervisorType rootDiskHyperType, StoragePool storagePool) throws NoTransitionException; + VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo volume, HypervisorType rootDiskHyperType, StoragePool storagePool, Long clusterId, Long podId) throws NoTransitionException; void release(VirtualMachineProfile profile); diff --git a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java index d6604cffc40a..032fcbe76dce 100644 --- a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java +++ b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java @@ -241,6 +241,9 @@ public interface StorageManager extends StorageService { "while adding a new Secondary Storage. If the copy operation fails, the system falls back to downloading the template from the source URL.", true, ConfigKey.Scope.Zone, null); + ConfigKey AgentMaxDataMigrationWaitTime = new ConfigKey<>("Advanced", Integer.class, "agent.max.data.migration.wait.time", "3600", + "The maximum time (in seconds) that the secondary storage data migration command sent to the KVM Agent will be executed before a timeout occurs.", true, ConfigKey.Scope.Cluster); + /** * should we execute in sequence not involving any storages? * @return true if commands should execute in sequence diff --git a/engine/components-api/src/main/java/com/cloud/vm/VmWorkDeleteBackup.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkDeleteBackup.java new file mode 100644 index 000000000000..b9d2907ef780 --- /dev/null +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkDeleteBackup.java @@ -0,0 +1,38 @@ +// 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 com.cloud.vm; + +public class VmWorkDeleteBackup extends VmWork { + + private long backupId; + + private boolean forced; + + public VmWorkDeleteBackup(long userId, long accountId, long vmId, String handlerName, long backupId, boolean forced) { + super(userId, accountId, vmId, handlerName); + this.backupId = backupId; + this.forced = forced; + } + + public long getBackupId() { + return backupId; + } + + public boolean isForced() { + return forced; + } +} diff --git a/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreBackup.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreBackup.java new file mode 100644 index 000000000000..421430cfbe9d --- /dev/null +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreBackup.java @@ -0,0 +1,45 @@ +// 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 com.cloud.vm; + +public class VmWorkRestoreBackup extends VmWork { + + private long backupId; + + private boolean quickRestore; + + private Long hostId; + + public VmWorkRestoreBackup(long userId, long accountId, long vmId, String handlerName, long backupId, boolean quickRestore, Long hostId) { + super(userId, accountId, vmId, handlerName); + this.backupId = backupId; + this.quickRestore = quickRestore; + this.hostId = hostId; + } + + public long getBackupId() { + return backupId; + } + + public boolean isQuickRestore() { + return quickRestore; + } + + public Long getHostId() { + return hostId; + } +} diff --git a/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreVolumeBackupAndAttach.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreVolumeBackupAndAttach.java new file mode 100644 index 000000000000..a34b11abbdb4 --- /dev/null +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreVolumeBackupAndAttach.java @@ -0,0 +1,55 @@ +// 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 com.cloud.vm; + +import org.apache.cloudstack.backup.Backup; + +public class VmWorkRestoreVolumeBackupAndAttach extends VmWork { + + private long backupId; + + private Backup.VolumeInfo backupVolumeInfo; + + private String hostIp; + + private boolean quickRestore; + + public VmWorkRestoreVolumeBackupAndAttach(long userId, long accountId, long vmId, String handlerName, long backupId, Backup.VolumeInfo backupVolumeInfo, + String hostIp, boolean quickRestore) { + super(userId, accountId, vmId, handlerName); + this.backupId = backupId; + this.backupVolumeInfo = backupVolumeInfo; + this.hostIp = hostIp; + this.quickRestore = quickRestore; + } + + public long getBackupId() { + return backupId; + } + + public Backup.VolumeInfo getBackupVolumeInfo() { + return backupVolumeInfo; + } + + public String getHostIp() { + return hostIp; + } + + public boolean isQuickRestore() { + return quickRestore; + } +} diff --git a/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeBackup.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeBackup.java new file mode 100644 index 000000000000..57367d368b86 --- /dev/null +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeBackup.java @@ -0,0 +1,50 @@ +// 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 com.cloud.vm; + +public class VmWorkTakeBackup extends VmWork { + + private long backupId; + + private boolean quiesceVm; + + private boolean isolated; + + public VmWorkTakeBackup(long userId, long accountId, long vmId, long backupId, String handlerName, boolean quiesceVm, boolean isolated) { + super(userId, accountId, vmId, handlerName); + this.quiesceVm = quiesceVm; + this.backupId = backupId; + this.isolated = isolated; + } + + public boolean isQuiesceVm() { + return quiesceVm; + } + + public long getBackupId() { + return backupId; + } + + public boolean isIsolated() { + return isolated; + } + + @Override + public String toString() { + return super.toStringAfterRemoveParams(null, null); + } +} diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index d5f7937a463f..364db685c9de 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -499,7 +499,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac static final ConfigKey ClusterVMMetaDataSyncInterval = new ConfigKey("Advanced", Integer.class, "vmmetadata.sync.interval", "180", "Cluster VM metadata sync interval in seconds", false); - static final ConfigKey VmJobCheckInterval = new ConfigKey("Advanced", + public static final ConfigKey VmJobCheckInterval = new ConfigKey("Advanced", Long.class, "vm.job.check.interval", "3000", "Interval in milliseconds to check if the job is complete", false); static final ConfigKey VmJobTimeout = new ConfigKey("Advanced", @@ -1029,7 +1029,7 @@ public Ternary doInTransaction(final if (stateTransitTo(vm, Event.StartRequested, null, work.getId())) { logger.debug("Successfully transitioned to start state for {} reservation id = {}", vm, work.getId()); if (VirtualMachine.Type.User.equals(vm.type) && ResourceCountRunningVMsonly.value()) { - _resourceLimitMgr.incrementVmResourceCount(owner.getAccountId(), vm.isDisplay(), offering, template); + _resourceLimitMgr.incrementVmResourceCount(owner.getAccountId(), vm.isDisplay(), offering, template, null); } return new Ternary<>(vm, context, work); } @@ -1396,6 +1396,7 @@ public void orchestrateStart(final String vmUuid, final Map templates, List snapshots, List volumes) { + public boolean filesReadyToMigrate(Long srcDataStoreId, List templates, List snapshots, List volumes, List backups) { State[] validStates = {State.Ready, State.Allocated, State.Destroying, State.Destroyed, State.Failed}; boolean isReady = true; for (TemplateDataStoreVO template : templates) { @@ -109,14 +117,48 @@ public boolean filesReadyToMigrate(Long srcDataStoreId, List backups) { + List invalidBackupStates = Arrays.asList(Backup.Status.BackingUp, Backup.Status.Restoring); + List invalidBackupCompressionStatus = Arrays.asList(Backup.CompressionStatus.Compressing, Backup.CompressionStatus.FinalizingCompression); + + List> backupChains; + Set backupIdsAlreadyInChain = new HashSet<>(); + + for (InternalBackupJoinVO backup : backups) { + if (backup.getStatus() == Backup.Status.BackedUp && !backupIdsAlreadyInChain.contains(backup.getId())) { + backupChains = createBackupChain(backup); + backupChains.forEach(list -> backupIdsAlreadyInChain.add(list.stream().map(BackupObject::getId).findFirst().get())); + + for (List backupVolumeChain : backupChains) { + BackupObject backupObject = backupVolumeChain.get(0); + + if (invalidBackupStates.contains(backupObject.getStatus())) { + logger.debug("Migration is not possible because backup {} is in {} state.", backupObject.getUuid(), backupObject.getStatus()); + return false; + } + + if (invalidBackupCompressionStatus.contains(backupObject.getCompressionStatus())) { + logger.debug("Migration is not possible because backup {} is currently being compressed. Current compression status: {}.", backupObject.getUuid(), backupObject.getCompressionStatus()); + return false; + } + } + } + } + + return true; + } + private boolean filesReadyToMigrate(Long srcDataStoreId) { List templates = templateDataStoreDao.listByStoreId(srcDataStoreId); List snapshots = snapshotDataStoreDao.listByStoreId(srcDataStoreId, DataStoreRole.Image); List volumes = volumeDataStoreDao.listByStoreId(srcDataStoreId); - return filesReadyToMigrate(srcDataStoreId, templates, snapshots, volumes); + List backups = internalBackupJoinDao.listByImageStoreId(srcDataStoreId); + + return filesReadyToMigrate(srcDataStoreId, templates, snapshots, volumes, backups); } protected void checkIfCompleteMigrationPossible(ImageStoreService.MigrationPolicy policy, Long srcDataStoreId) { @@ -175,19 +217,58 @@ protected List getSortedValidSourcesList(DataStore srcDataStore, Map return files; } - protected List getSortedValidSourcesList(DataStore srcDataStore, Map, Long>> snapshotChains, - Map, Long>> childTemplates) { + Map, Long>> childTemplates, Map>, Long>> backupChains) { List files = new ArrayList<>(); files.addAll(getAllReadyTemplates(srcDataStore, childTemplates)); files.addAll(getAllReadySnapshotsAndChains(srcDataStore, snapshotChains)); files.addAll(getAllReadyVolumes(srcDataStore)); + files.addAll(getAllReadyBackupsAndChains(srcDataStore, backupChains)); files = sortFilesOnSize(files, snapshotChains); return files; } + protected List getAllReadyBackupsAndChains(DataStore srcDataStore, Map>, Long>> backupChains) { + List backups = internalBackupJoinDao.listByImageStoreId(srcDataStore.getId()); + return getAllReadyBackupsAndChains(backupChains, backups); + } + + private List getAllReadyBackupsAndChains(Map>, Long>> backupsChains, List backups) { + Set backupIdsToMigrate = backups.stream().map(InternalBackupJoinVO::getId).collect(Collectors.toSet()); + List> backupChains; + Set backupIdsAlreadyInChain = new HashSet<>(); + List files = new LinkedList<>(); + + for (InternalBackupJoinVO backup : backups) { + long backupId = backup.getId(); + + if (backup.getStatus() == Backup.Status.BackedUp && !backupIdsAlreadyInChain.contains(backupId)) { + backupChains = createBackupChain(backup); + backupChains.forEach(list -> backupIdsAlreadyInChain.add(list.stream().map(BackupObject::getId).findFirst().get())); + BackupObject parent = backupChains.get(0).get(0); + files.add(parent); + backupsChains.put(parent, new Pair<>(backupChains, backupChains.stream().map(list -> getTotalChainSize(list.stream() + .filter(back -> backupIdsToMigrate.contains(parent.getId())).collect(Collectors.toList())) + ).reduce(Long::sum).get())); + } + } + + return (List) (List) files; + } + + private List> createBackupChain(InternalBackupJoinVO backup) { + List> chain = new LinkedList<>(); + BackupObject backupObject = BackupObject.getBackupObject(backup); + + chain.addAll(backupObject.getParents(backup.getParentId())); + chain.add(internalBackupJoinDao.listById(backup.getId()).stream().map(BackupObject::getBackupObject).collect(Collectors.toList())); + chain.addAll(backupObject.getChildren()); + + return chain; + } + protected List sortFilesOnSize(List files, Map, Long>> snapshotChains) { Collections.sort(files, new Comparator() { @Override diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java index 933b4e0c5ce6..403e83bccf99 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java @@ -25,9 +25,13 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -36,10 +40,22 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.MigrateBackupsBetweenSecondaryStoragesCommand; +import com.cloud.agent.api.MigrateBetweenSecondaryStoragesCommandAnswer; import com.cloud.dc.dao.DataCenterDao; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.dao.VMTemplateDao; import com.cloud.template.TemplateManager; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; import org.apache.cloudstack.api.response.MigrationResponse; +import org.apache.cloudstack.backup.BackupDetailVO; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -57,6 +73,7 @@ import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.storage.ImageStoreService.MigrationPolicy; +import org.apache.cloudstack.storage.backup.BackupObject; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; @@ -115,6 +132,12 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra TemplateDataFactory templateDataFactory; @Inject DataCenterDao dcDao; + @Inject + AgentManager agentManager; + @Inject + HostDao hostDao; + @Inject + BackupDetailsDao backupDetailDao; ConfigKey ImageStoreImbalanceThreshold = new ConfigKey<>("Advanced", Double.class, @@ -128,6 +151,7 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra private final Map zoneExecutorMap = new HashMap<>(); private final Map zonePendingWorkCountMap = new HashMap<>(); + private final Map zoneKvmIncrementalExecutorMap = new ConcurrentHashMap<>(); @Override public String getConfigComponentName() { @@ -171,7 +195,9 @@ public MigrationResponse migrateData(Long srcDataStoreId, List destDatasto DataStore srcDatastore = dataStoreManager.getDataStore(srcDataStoreId, DataStoreRole.Image); Map, Long>> snapshotChains = new HashMap<>(); Map, Long>> childTemplates = new HashMap<>(); - files = migrationHelper.getSortedValidSourcesList(srcDatastore, snapshotChains, childTemplates); + Map>, Long>> backupChains = new HashMap<>(); + files = migrationHelper.getSortedValidSourcesList(srcDatastore, snapshotChains, childTemplates, backupChains); + if (files.isEmpty()) { return new MigrationResponse(String.format("No files in Image store: %s to migrate", srcDatastore), migrationPolicy.toString(), true); @@ -227,7 +253,7 @@ public MigrationResponse migrateData(Long srcDataStoreId, List destDatasto } if (shouldMigrate(chosenFileForMigration, srcDatastore.getId(), destDatastoreId, storageCapacities, snapshotChains, childTemplates, migrationPolicy)) { - storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, srcDatastore, destDatastoreId, futures); + storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, backupChains, srcDatastore, destDatastoreId, futures); } else { if (migrationPolicy == MigrationPolicy.BALANCE) { continue; @@ -256,7 +282,7 @@ public MigrationResponse migrateResources(Long srcImgStoreId, Long destImgStoreI List templates = templateDataStoreDao.listByStoreIdAndTemplateIds(srcImgStoreId, templateIdList); List snapshots = snapshotDataStoreDao.listByStoreAndSnapshotIds(srcImgStoreId, DataStoreRole.Image, snapshotIdList); - if (!migrationHelper.filesReadyToMigrate(srcImgStoreId, templates, snapshots, Collections.emptyList())) { + if (!migrationHelper.filesReadyToMigrate(srcImgStoreId, templates, snapshots, Collections.emptyList(), Collections.emptyList())) { throw new CloudRuntimeException("Migration failed as there are data objects which are not Ready - i.e, they may be in Migrating, creating, copying, etc. states"); } files = migrationHelper.getSortedValidSourcesList(srcDatastore, snapshotChains, childTemplates, templates, snapshots); @@ -291,7 +317,7 @@ public MigrationResponse migrateResources(Long srcImgStoreId, Long destImgStoreI } if (storageCapacityBelowThreshold(storageCapacities, destImgStoreId)) { - storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, srcDatastore, destImgStoreId, futures); + storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, null, srcDatastore, destImgStoreId, futures); } else { message = "Migration failed. Destination store doesn't have enough capacity for migration"; success = false; @@ -355,15 +381,89 @@ protected Map> migrateAway( Map, Long>> snapshotChains, Map, Long>> templateChains, + Map>, Long>> backupChains, DataStore srcDatastore, Long destDatastoreId, List> futures) { Long fileSize = migrationHelper.getFileSize(chosenFileForMigration, snapshotChains, templateChains); storageCapacities = assumeMigrate(storageCapacities, srcDatastore.getId(), destDatastoreId, fileSize); + DataStore destDataStore = dataStoreManager.getDataStore(destDatastoreId, DataStoreRole.Image); + + boolean isKvmIncrementalBackup = backupChains != null && chosenFileForMigration instanceof BackupObject && backupChains.containsKey(chosenFileForMigration); + + if (isKvmIncrementalBackup) { + MigrateKvmIncrementalBackupTask task = new MigrateKvmIncrementalBackupTask(chosenFileForMigration, backupChains, srcDatastore, destDataStore); + futures.add(submitKvmIncrementalMigration(srcDatastore.getScope().getScopeId(), task)); + logger.debug("Incremental backup migration {} submitted to incremental pool.", chosenFileForMigration.getUuid()); + } else { + createMigrateDataTask(chosenFileForMigration, snapshotChains, templateChains, srcDatastore, destDataStore, futures); + } + + return storageCapacities; + } + + private void migrateKvmIncrementalBackupChain(DataObject chosenFileForMigration, Map>, Long>> backupChains, DataStore srcDatastore, DataStore destDataStore) { + Transaction.execute((TransactionCallback) status -> { + MigrateBetweenSecondaryStoragesCommandAnswer answer = null; + + try { + List> backupChain = backupChains.get(chosenFileForMigration).first(); + MigrateBackupsBetweenSecondaryStoragesCommand migrateBetweenSecondaryStoragesCmd = new MigrateBackupsBetweenSecondaryStoragesCommand(backupChain.stream().map(list -> list.stream().map(BackupObject::getTO).collect(Collectors.toList())) + .collect(Collectors.toList()), srcDatastore.getTO(), destDataStore.getTO()); + + HostVO host = getAvailableHost(((BackupObject) chosenFileForMigration).getZoneId()); + if (host == null) { + throw new CloudRuntimeException("No hosts found to send migrate command."); + } + + migrateBetweenSecondaryStoragesCmd.setWait(StorageManager.AgentMaxDataMigrationWaitTime.valueIn(host.getClusterId())); + answer = (MigrateBetweenSecondaryStoragesCommandAnswer) agentManager.send(host.getId(), migrateBetweenSecondaryStoragesCmd); + if (answer == null || !answer.getResult()) { + logger.warn("Unable to migrate backups [{}].", backupChain); + throw new CloudRuntimeException("Unable to migrate KVM incremental backups to another secondary storage"); + } + + } catch (final OperationTimedoutException | AgentUnavailableException e) { + throw new CloudRuntimeException("Error while migrating KVM incremental backup chain. Check the logs for more information.", e); + } finally { + if (answer != null) { + updateBackupsReference(destDataStore, answer); + } + } + return answer.getResult(); + }); + } + + private void updateBackupsReference(DataStore destDataStore, MigrateBetweenSecondaryStoragesCommandAnswer answer) { + for (Pair backupIdAndUpdatedCheckpointPath : answer.getMigratedResources()) { + Long backupId = backupIdAndUpdatedCheckpointPath.first(); + BackupDetailVO backupDetail = backupDetailDao.findDetail(backupId, BackupDetailsDao.IMAGE_STORE_ID); + String destDataStoreId = String.valueOf(destDataStore.getId()); + + if (backupDetail == null) { + logger.warn("No details found for backup [{}]. Creating new entry with image store ID [{}].", backupId, destDataStoreId); + backupDetailDao.addDetail(backupId, BackupDetailsDao.IMAGE_STORE_ID, destDataStoreId, false); + continue; + } + + backupDetail.setValue(destDataStoreId); + backupDetailDao.update(backupDetail.getId(), backupDetail); + } + } - MigrateDataTask task = new MigrateDataTask(chosenFileForMigration, srcDatastore, dataStoreManager.getDataStore(destDatastoreId, DataStoreRole.Image)); - if (chosenFileForMigration instanceof SnapshotInfo ) { + private HostVO getAvailableHost(long zoneId) throws AgentUnavailableException, OperationTimedoutException { + List hosts = hostDao.listByDataCenterIdAndHypervisorType(zoneId, Hypervisor.HypervisorType.KVM); + if (CollectionUtils.isNotEmpty(hosts)) { + return hosts.get(new Random().nextInt(hosts.size())); + } + + return null; + } + + private void createMigrateDataTask(DataObject chosenFileForMigration, Map, Long>> snapshotChains, Map, Long>> templateChains, DataStore srcDatastore, DataStore destDataStore, List> futures) { + MigrateDataTask task = new MigrateDataTask(chosenFileForMigration, srcDatastore, destDataStore); + if (chosenFileForMigration instanceof SnapshotInfo) { task.setSnapshotChains(snapshotChains); } if (chosenFileForMigration instanceof TemplateInfo) { @@ -371,7 +471,6 @@ protected Map> migrateAway( } futures.add(submit(srcDatastore.getScope().getScopeId(), task)); logger.debug("Migration of {}: {} is initiated.", chosenFileForMigration.getType().name(), chosenFileForMigration.getUuid()); - return storageCapacities; } protected Future submit(Long zoneId, Callable task) { @@ -390,6 +489,13 @@ protected Future submit(Long zoneId, Callable task) { } + protected synchronized Future submitKvmIncrementalMigration(Long zoneId, Callable task) { + if (!zoneKvmIncrementalExecutorMap.containsKey(zoneId)) { + zoneKvmIncrementalExecutorMap.put(zoneId, Executors.newSingleThreadExecutor()); + } + return zoneKvmIncrementalExecutorMap.get(zoneId).submit(task); + } + protected void scaleExecutorIfNecessary(Long zoneId) { long activeSsvms = migrationHelper.activeSSVMCount(zoneId); long totalJobs = activeSsvms * numConcurrentCopyTasksPerSSVM; @@ -666,4 +772,32 @@ public TemplateApiResult call() { return result; } } + + private class MigrateKvmIncrementalBackupTask implements Callable { + private final DataObject chosenFile; + private final Map>, Long>> backupChains; + private final DataStore srcDataStore; + private final DataStore destDataStore; + + public MigrateKvmIncrementalBackupTask(DataObject chosenFile, Map>, Long>> backupChains, DataStore srcDataStore, DataStore destDataStore) { + this.chosenFile = chosenFile; + this.backupChains = backupChains; + this.srcDataStore = srcDataStore; + this.destDataStore = destDataStore; + } + + @Override + public DataObjectResult call() { + try { + migrateKvmIncrementalBackupChain(chosenFile, backupChains, srcDataStore, destDataStore); + return new DataObjectResult(chosenFile); + } catch (Exception e) { + logger.warn("Failed migrating incremental backup {} due to {}.", chosenFile.getUuid(), e); + DataObjectResult result = new DataObjectResult(chosenFile); + result.setResult(e.toString()); + return result; + } + } + } + } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index b4c104da07e7..f4198819dd16 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -53,6 +53,7 @@ import org.apache.cloudstack.api.command.admin.vm.MigrateVMCmd; import org.apache.cloudstack.api.command.admin.volume.MigrateVolumeCmdByAdmin; import org.apache.cloudstack.api.command.user.volume.MigrateVolumeCmd; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; @@ -301,6 +302,9 @@ public enum UserVmCloneType { @Inject private KMSWrappedKeyDao kmsWrappedKeyDao; + @Inject + private InternalBackupService internalBackupService; + private final StateMachine2 _volStateMachine; protected List _storagePoolAllocators; @@ -1358,21 +1362,27 @@ private VolumeInfo copyVolume(StoragePool rootDiskPool, VolumeInfo volumeInfo, V } @Override - public VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo volumeInfo, HypervisorType rootDiskHyperType, StoragePool storagePool) throws NoTransitionException { + public VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo volumeInfo, HypervisorType rootDiskHyperType, StoragePool storagePool, Long clusterId, Long podId) + throws NoTransitionException { String volumeToString = getReflectOnlySelectedFields(volumeInfo.getVolume()); VirtualMachineTemplate rootDiskTmplt = _entityMgr.findById(VirtualMachineTemplate.class, vm.getTemplateId()); DataCenter dcVO = _entityMgr.findById(DataCenter.class, vm.getDataCenterId()); - logger.trace("storage-pool {}/{} is associated with pod {}",storagePool.getName(), storagePool.getUuid(), storagePool.getPodId()); - Long podId = storagePool.getPodId() != null ? storagePool.getPodId() : vm.getPodIdToDeployIn(); + + if (storagePool != null) { + logger.trace("storage-pool {}/{} is associated with pod {}", storagePool.getName(), storagePool.getUuid(), storagePool.getPodId()); + podId = storagePool.getPodId() != null ? storagePool.getPodId() : vm.getPodIdToDeployIn(); + clusterId = storagePool.getClusterId(); + logger.trace("storage-pool {}/{} is associated with cluster {}",storagePool.getName(), storagePool.getUuid(), clusterId); + } + Pod pod = _entityMgr.findById(Pod.class, podId); ServiceOffering svo = _entityMgr.findById(ServiceOffering.class, vm.getServiceOfferingId()); DiskOffering diskVO = _entityMgr.findById(DiskOffering.class, volumeInfo.getDiskOfferingId()); - Long clusterId = storagePool.getClusterId(); - logger.trace("storage-pool {}/{} is associated with cluster {}",storagePool.getName(), storagePool.getUuid(), clusterId); + Long hostId = vm.getHostId(); - if (hostId == null && (storagePool.isLocal() || ClvmPoolManager.isClvmPoolType(storagePool.getPoolType()))) { + if (hostId == null && storagePool != null && (storagePool.isLocal() || ClvmPoolManager.isClvmPoolType(storagePool.getPoolType()))) { if (ClvmPoolManager.isClvmPoolType(storagePool.getPoolType())) { hostId = getClvmLockHostFromVmVolumes(vm.getId()); if (hostId != null) { @@ -1632,6 +1642,7 @@ public Volume migrateVolume(Volume volume, StoragePool destPool) throws StorageU _snapshotDao.updateVolumeIds(vol.getId(), result.getVolume().getId()); _snapshotDataStoreDao.updateVolumeIds(vol.getId(), result.getVolume().getId()); } + internalBackupService.updateVolumeId(vol.getId(), result.getVolume().getId()); // For CLVM volumes attached to a VM, update the CLVM_LOCK_HOST_ID after migration updateClvmLockHostAfterMigration(result.getVolume(), destPool, "migrated"); @@ -1695,6 +1706,8 @@ public void migrateVolumes(VirtualMachine vm, VirtualMachineTO vmTo, Host srcHos throw new CloudRuntimeException(String.format("Failed to find the destination storage pool [%s] to migrate the volume [%s] to.", storagePoolToString, volumeToString)); } + internalBackupService.prepareVolumeForMigration(volume); + volumeMap.put(volFactory.getVolume(volume.getId()), (DataStore)destPool); } @@ -2531,7 +2544,8 @@ public void destroyVolume(Volume volume) { if (volume.getState() == Volume.State.Allocated) { _volsDao.remove(volume.getId()); stateTransitTo(volume, Volume.Event.DestroyRequested); - _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), volume.isDisplay(), volume.getSize(), diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId())); + _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), volume.isDisplay(), volume.getSize(), diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId()), + null); } else { destroyVolumeInContext(volume); } diff --git a/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml b/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml index 8f93ae5b35a3..e17302e68a1f 100644 --- a/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml +++ b/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml @@ -91,6 +91,7 @@ + diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java index 5c0d04fb2be8..d8bdabc3dcbb 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java @@ -121,6 +121,8 @@ public interface HostDao extends GenericDao, StateDao listIdsForUpEnabledByZoneAndHypervisor(Long zoneId, HypervisorType hypervisorType); + List findRoutingByClusterId(Long clusterId); + List findByClusterIdAndEncryptionSupport(Long clusterId); /** @@ -139,6 +141,8 @@ public interface HostDao extends GenericDao, StateDao listAllHostsByZoneAndHypervisorType(long zoneId, HypervisorType hypervisorType); + List listAllRoutingHostsByZoneAndHypervisorType(long zoneId, HypervisorType hypervisorType); + List listAllHostsThatHaveNoRuleTag(Host.Type type, Long clusterId, Long podId, Long dcId); HostVO findByPublicIp(String publicIp); diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java index 5b8a38b8e5b4..15727d9d8e66 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java @@ -1341,6 +1341,14 @@ public List listIdsForUpEnabledByZoneAndHypervisor(Long zoneId, Hypervisor return listIdsBy(null, Status.Up, ResourceState.Enabled, hypervisorType, zoneId, null, null); } + @Override + public List findRoutingByClusterId(Long clusterId) { + SearchCriteria sc = ClusterSearch.create(); + sc.setParameters("clusterId", clusterId); + sc.setParameters("type", Type.Routing); + return listBy(sc); + } + @Override public List findByClusterIdAndEncryptionSupport(Long clusterId) { SearchBuilder hostCapabilitySearch = _detailsDao.createSearchBuilder(); @@ -1456,6 +1464,16 @@ public List listAllHostsByZoneAndHypervisorType(long zoneId, HypervisorT return listBy(sc); } + @Override + public List listAllRoutingHostsByZoneAndHypervisorType(long zoneId, HypervisorType hypervisorType) { + SearchCriteria sc = DcSearch.create(); + sc.setParameters("dc", zoneId); + sc.setParameters("hypervisorType", hypervisorType.toString()); + sc.setParameters("type", Type.Routing); + + return listBy(sc); + } + @Override public List listAllHostsThatHaveNoRuleTag(Type type, Long clusterId, Long podId, Long dcId) { SearchCriteria sc = searchBuilderFindByIdTypeClusterIdPodIdDcIdAndWithoutRuleTag.create(); diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java index 8406f80ed09d..b656d896e37f 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java @@ -141,4 +141,6 @@ List listByZonesTrafficTypeAndOwners(List zoneIds, final Traffi List listByPhysicalNetworkPvlan(long physicalNetworkId, String broadcastUri); List getAllPersistentNetworksFromZone(long dataCenterId); + + NetworkVO findByZoneIdAndAccountIdAndGuestTypeAndName(long zoneId, long accountId, GuestType guestType, String name); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java index 22b186567f84..fbf6a2ac2261 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java @@ -943,4 +943,16 @@ public List listByPhysicalNetworkPvlan(long physicalNetworkId, String return overlappingNetworks; } + + @Override + public NetworkVO findByZoneIdAndAccountIdAndGuestTypeAndName(long zoneId, long accountId, GuestType guestType, String name) { + SearchCriteria sc = AllFieldsSearch.create(); + + sc.setParameters("datacenter", zoneId); + sc.setParameters("account", accountId); + sc.setParameters("guestType", guestType); + sc.setParameters("name", name); + + return findOneBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/SnapshotVO.java b/engine/schema/src/main/java/com/cloud/storage/SnapshotVO.java index 4a504333344f..be7ba2843321 100644 --- a/engine/schema/src/main/java/com/cloud/storage/SnapshotVO.java +++ b/engine/schema/src/main/java/com/cloud/storage/SnapshotVO.java @@ -248,6 +248,10 @@ public Date getRemoved() { return removed; } + public void setRemoved(Date removed) { + this.removed = removed; + } + @Override public State getState() { return state; diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java index f167b5731878..a5596ed840cc 100755 --- a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java @@ -56,10 +56,18 @@ public class SnapshotDaoImpl extends GenericDaoBase implements // TODO: we should remove these direct sqls private static final String GET_LAST_SNAPSHOT = "SELECT snapshots.id FROM snapshot_store_ref, snapshots where snapshots.id = snapshot_store_ref.snapshot_id AND snapshosts.volume_id = ? AND snapshot_store_ref.role = ? ORDER BY created DESC"; - private static final String VOLUME_ID = "volumeId"; - private static final String NOT_TYPE = "notType"; + private static final String TYPE = "type"; private static final String STATUS = "status"; + private static final String VERSION = "version"; + private static final String ACCOUNT_ID = "accountId"; + private static final String REMOVED = "removed"; + private static final String NOT_TYPE = "notType"; + private static final String ID = "id"; + private static final String INSTANCE_ID = "instanceId"; + private static final String STATE = "state"; + private static final String INSTANCE_VOLUMES = "instanceVolumes"; + private static final String INSTANCE_SNAPSHOTS = "instanceSnapshots"; private SearchBuilder snapshotIdsSearch; private SearchBuilder VolumeIdSearch; @@ -83,9 +91,9 @@ public class SnapshotDaoImpl extends GenericDaoBase implements @Override public List listByVolumeIdTypeNotDestroyed(long volumeId, Type type) { SearchCriteria sc = VolumeIdTypeNotDestroyedSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("type", type.ordinal()); - sc.setParameters("status", State.Destroyed); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(TYPE, type.ordinal()); + sc.setParameters(STATUS, State.Destroyed); return listBy(sc, null); } @@ -102,28 +110,28 @@ public List listByVolumeId(long volumeId) { @Override public List listByVolumeId(Filter filter, long volumeId) { SearchCriteria sc = VolumeIdSearch.create(); - sc.setParameters("volumeId", volumeId); + sc.setParameters(VOLUME_ID, volumeId); return listBy(sc, filter); } @Override public List listByVolumeIdIncludingRemoved(long volumeId) { SearchCriteria sc = VolumeIdSearch.create(); - sc.setParameters("volumeId", volumeId); + sc.setParameters(VOLUME_ID, volumeId); return listIncludingRemovedBy(sc, null); } public List listByVolumeIdType(Filter filter, long volumeId, Type type) { SearchCriteria sc = VolumeIdTypeSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("type", type.ordinal()); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(TYPE, type.ordinal()); return listBy(sc, filter); } public List listByVolumeIdVersion(Filter filter, long volumeId, String version) { SearchCriteria sc = VolumeIdVersionSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("version", version); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(VERSION, version); return listBy(sc, filter); } @@ -133,61 +141,61 @@ public SnapshotDaoImpl() { @PostConstruct protected void init() { VolumeIdSearch = createSearchBuilder(); - VolumeIdSearch.and("volumeId", VolumeIdSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdSearch.and(VOLUME_ID, VolumeIdSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); VolumeIdSearch.done(); VolumeIdTypeSearch = createSearchBuilder(); - VolumeIdTypeSearch.and("volumeId", VolumeIdTypeSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - VolumeIdTypeSearch.and("type", VolumeIdTypeSearch.entity().getSnapshotType(), SearchCriteria.Op.EQ); + VolumeIdTypeSearch.and(VOLUME_ID, VolumeIdTypeSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdTypeSearch.and(TYPE, VolumeIdTypeSearch.entity().getSnapshotType(), SearchCriteria.Op.EQ); VolumeIdTypeSearch.done(); VolumeIdTypeNotDestroyedSearch = createSearchBuilder(); - VolumeIdTypeNotDestroyedSearch.and("volumeId", VolumeIdTypeNotDestroyedSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - VolumeIdTypeNotDestroyedSearch.and("type", VolumeIdTypeNotDestroyedSearch.entity().getSnapshotType(), SearchCriteria.Op.EQ); - VolumeIdTypeNotDestroyedSearch.and("status", VolumeIdTypeNotDestroyedSearch.entity().getState(), SearchCriteria.Op.NEQ); + VolumeIdTypeNotDestroyedSearch.and(VOLUME_ID, VolumeIdTypeNotDestroyedSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdTypeNotDestroyedSearch.and(TYPE, VolumeIdTypeNotDestroyedSearch.entity().getSnapshotType(), SearchCriteria.Op.EQ); + VolumeIdTypeNotDestroyedSearch.and(STATUS, VolumeIdTypeNotDestroyedSearch.entity().getState(), SearchCriteria.Op.NEQ); VolumeIdTypeNotDestroyedSearch.done(); VolumeIdVersionSearch = createSearchBuilder(); - VolumeIdVersionSearch.and("volumeId", VolumeIdVersionSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - VolumeIdVersionSearch.and("version", VolumeIdVersionSearch.entity().getVersion(), SearchCriteria.Op.EQ); + VolumeIdVersionSearch.and(VOLUME_ID, VolumeIdVersionSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdVersionSearch.and(VERSION, VolumeIdVersionSearch.entity().getVersion(), SearchCriteria.Op.EQ); VolumeIdVersionSearch.done(); AccountIdSearch = createSearchBuilder(); - AccountIdSearch.and("accountId", AccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountIdSearch.and(ACCOUNT_ID, AccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); AccountIdSearch.done(); StatusSearch = createSearchBuilder(); - StatusSearch.and("volumeId", StatusSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - StatusSearch.and("status", StatusSearch.entity().getState(), SearchCriteria.Op.IN); + StatusSearch.and(VOLUME_ID, StatusSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + StatusSearch.and(STATUS, StatusSearch.entity().getState(), SearchCriteria.Op.IN); StatusSearch.done(); notInStatusSearch = createSearchBuilder(); - notInStatusSearch.and("volumeId", notInStatusSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - notInStatusSearch.and("status", notInStatusSearch.entity().getState(), SearchCriteria.Op.NOTIN); + notInStatusSearch.and(VOLUME_ID, notInStatusSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + notInStatusSearch.and(STATUS, notInStatusSearch.entity().getState(), SearchCriteria.Op.NOTIN); notInStatusSearch.done(); CountSnapshotsByAccount = createSearchBuilder(Long.class); CountSnapshotsByAccount.select(null, Func.COUNT, null); - CountSnapshotsByAccount.and("account", CountSnapshotsByAccount.entity().getAccountId(), SearchCriteria.Op.EQ); - CountSnapshotsByAccount.and("status", CountSnapshotsByAccount.entity().getState(), SearchCriteria.Op.NIN); + CountSnapshotsByAccount.and(ACCOUNT_ID, CountSnapshotsByAccount.entity().getAccountId(), SearchCriteria.Op.EQ); + CountSnapshotsByAccount.and(STATUS, CountSnapshotsByAccount.entity().getState(), SearchCriteria.Op.NIN); CountSnapshotsByAccount.and("snapshotTypeNEQ", CountSnapshotsByAccount.entity().getSnapshotType(), SearchCriteria.Op.NIN); - CountSnapshotsByAccount.and("removed", CountSnapshotsByAccount.entity().getRemoved(), SearchCriteria.Op.NULL); + CountSnapshotsByAccount.and(REMOVED, CountSnapshotsByAccount.entity().getRemoved(), SearchCriteria.Op.NULL); CountSnapshotsByAccount.done(); InstanceIdSearch = createSearchBuilder(); - InstanceIdSearch.and("status", InstanceIdSearch.entity().getState(), SearchCriteria.Op.IN); + InstanceIdSearch.and(STATUS, InstanceIdSearch.entity().getState(), SearchCriteria.Op.IN); snapshotIdsSearch = createSearchBuilder(); - snapshotIdsSearch.and("id", snapshotIdsSearch.entity().getId(), SearchCriteria.Op.IN); + snapshotIdsSearch.and(ID, snapshotIdsSearch.entity().getId(), SearchCriteria.Op.IN); SearchBuilder instanceSearch = _instanceDao.createSearchBuilder(); - instanceSearch.and("instanceId", instanceSearch.entity().getId(), SearchCriteria.Op.EQ); + instanceSearch.and(INSTANCE_ID, instanceSearch.entity().getId(), SearchCriteria.Op.EQ); SearchBuilder volumeSearch = _volumeDao.createSearchBuilder(); - volumeSearch.and("state", volumeSearch.entity().getState(), SearchCriteria.Op.EQ); - volumeSearch.join("instanceVolumes", instanceSearch, instanceSearch.entity().getId(), volumeSearch.entity().getInstanceId(), JoinType.INNER); + volumeSearch.and(STATE, volumeSearch.entity().getState(), SearchCriteria.Op.EQ); + volumeSearch.join(INSTANCE_VOLUMES, instanceSearch, instanceSearch.entity().getId(), volumeSearch.entity().getInstanceId(), JoinType.INNER); - InstanceIdSearch.join("instanceSnapshots", volumeSearch, volumeSearch.entity().getId(), InstanceIdSearch.entity().getVolumeId(), JoinType.INNER); + InstanceIdSearch.join(INSTANCE_SNAPSHOTS, volumeSearch, volumeSearch.entity().getId(), InstanceIdSearch.entity().getVolumeId(), JoinType.INNER); InstanceIdSearch.done(); volumeIdAndTypeNotInSearch = createSearchBuilder(); @@ -219,8 +227,8 @@ public long getLastSnapshot(long volumeId, DataStoreRole role) { @Override public Long countSnapshotsForAccount(long accountId) { SearchCriteria sc = CountSnapshotsByAccount.create(); - sc.setParameters("account", accountId); - sc.setParameters("status", State.Error, State.Destroyed); + sc.setParameters(ACCOUNT_ID, accountId); + sc.setParameters(STATUS, State.Error, State.Destroyed); sc.setParameters("snapshotTypeNEQ", Snapshot.Type.GROUP.ordinal()); return customSearch(sc, null).get(0); } @@ -230,19 +238,19 @@ public List listByInstanceId(long instanceId, Snapshot.State... stat SearchCriteria sc = InstanceIdSearch.create(); if (status != null && status.length != 0) { - sc.setParameters("status", (Object[])status); + sc.setParameters(STATUS, (Object[])status); } - sc.setJoinParameters("instanceSnapshots", "state", Volume.State.Ready); - sc.setJoinParameters("instanceVolumes", "instanceId", instanceId); + sc.setJoinParameters(INSTANCE_SNAPSHOTS, STATE, Volume.State.Ready); + sc.setJoinParameters(INSTANCE_VOLUMES, INSTANCE_ID, instanceId); return listBy(sc, null); } @Override public List listByStatus(long volumeId, Snapshot.State... status) { SearchCriteria sc = StatusSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("status", (Object[])status); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(STATUS, (Object[])status); return listBy(sc, null); } @@ -263,7 +271,7 @@ public boolean remove(Long id) { @Override public List listAllByStatus(Snapshot.State... status) { SearchCriteria sc = StatusSearch.create(); - sc.setParameters("status", (Object[])status); + sc.setParameters(STATUS, (Object[])status); return listBy(sc, null); } @@ -277,7 +285,7 @@ public List listAllByStatusIncludingRemoved(Snapshot.State... status @Override public List listByIds(Object... ids) { SearchCriteria sc = snapshotIdsSearch.create(); - sc.setParameters("id", ids); + sc.setParameters(ID, ids); return listBy(sc, null); } @@ -295,7 +303,7 @@ public boolean updateState(State currentState, Event event, State nextState, Sna @Override public void updateVolumeIds(long oldVolId, long newVolId) { SearchCriteria sc = VolumeIdSearch.create(); - sc.setParameters("volumeId", oldVolId); + sc.setParameters(VOLUME_ID, oldVolId); SnapshotVO snapshot = createForUpdate(); snapshot.setVolumeId(newVolId); UpdateBuilder ub = getUpdateBuilder(snapshot); @@ -305,8 +313,8 @@ public void updateVolumeIds(long oldVolId, long newVolId) { @Override public List listByStatusNotIn(long volumeId, Snapshot.State... status) { SearchCriteria sc = this.notInStatusSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("status", (Object[]) status); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(STATUS, (Object[]) status); return listBy(sc, null); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java index ebeb7d4a2d59..9156f8f5ff84 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java @@ -83,6 +83,11 @@ public BackupOfferingVO(final long zoneId, final String externalId, final String this.created = new Date(); } + public BackupOfferingVO(final long zoneId, final String provider, final String name, final String description, final boolean userDrivenBackupAllowed) { + this(zoneId, null, provider, name, description, userDrivenBackupAllowed); + this.externalId = this.uuid; + } + public String getUuid() { return uuid; } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java index 1ee2cff78b65..aa02bb077163 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java @@ -74,10 +74,14 @@ public class BackupScheduleVO implements BackupSchedule { @Column(name = "domain_id") Long domainId; + @Column(name = "isolated") + private boolean isolated; + public BackupScheduleVO() { } - public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String schedule, String timezone, Date scheduledTimestamp, int maxBackups, Boolean quiesceVM, Long accountId, Long domainId) { + public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String schedule, String timezone, Date scheduledTimestamp, int maxBackups, Boolean quiesceVM, + Long accountId, Long domainId, boolean isolated) { this.vmId = vmId; this.scheduleType = (short) scheduleType.ordinal(); this.schedule = schedule; @@ -87,6 +91,7 @@ public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String sc this.quiesceVM = quiesceVM; this.accountId = accountId; this.domainId = domainId; + this.isolated = isolated; } @Override @@ -197,4 +202,13 @@ public void setAccountId(Long accountId) { public void setDomainId(Long domainId) { this.domainId = domainId; } + + @Override + public boolean isIsolated() { + return isolated; + } + + public void setIsolated(boolean isolated) { + this.isolated = isolated; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java index 7754c2440c09..c2b091700edc 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java @@ -19,7 +19,6 @@ import com.cloud.utils.db.GenericDao; import com.google.gson.Gson; - import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.lang3.StringUtils; @@ -81,6 +80,9 @@ public class BackupVO implements Backup { @Column(name = "protected_size") private Long protectedSize; + @Column(name = "uncompressed_size") + private Long uncompressedSize; + @Enumerated(value = EnumType.STRING) @Column(name = "status") private Backup.Status status; @@ -103,6 +105,12 @@ public class BackupVO implements Backup { @Column(name = "backup_schedule_id") private Long backupScheduleId; + @Column(name = "compression_status") + private CompressionStatus compressionStatus; + + @Column(name = "validation_status") + private ValidationStatus validationStatus; + @Column(name = "from_checkpoint_id") private String fromCheckpointId; @@ -120,6 +128,24 @@ public class BackupVO implements Backup { public BackupVO() { this.uuid = UUID.randomUUID().toString(); + this.compressionStatus = CompressionStatus.Uncompressed; + this.validationStatus = ValidationStatus.NotValidated; + } + + public BackupVO(String name, long vmId, long backupOfferingId, long accountId, long domainId, long zoneId, long virtualSize, Status status, Long backupScheduleId) { + this.name = name; + this.vmId = vmId; + this.backupOfferingId = backupOfferingId; + this.accountId = accountId; + this.domainId = domainId; + this.zoneId = zoneId; + this.protectedSize = virtualSize; + this.status = status; + this.setType("FULL"); + this.uuid = UUID.randomUUID().toString(); + this.backupScheduleId = backupScheduleId; + this.compressionStatus = CompressionStatus.Uncompressed; + this.validationStatus = ValidationStatus.NotValidated; } @Override @@ -156,6 +182,7 @@ public void setExternalId(String externalId) { this.externalId = externalId; } + @Override public String getType() { return type; } @@ -301,6 +328,32 @@ public void setBackupScheduleId(Long backupScheduleId) { this.backupScheduleId = backupScheduleId; } + @Override + public CompressionStatus getCompressionStatus() { + return compressionStatus; + } + + public void setCompressionStatus(CompressionStatus compressionStatus) { + this.compressionStatus = compressionStatus; + } + + @Override + public ValidationStatus getValidationStatus() { + return validationStatus; + } + + public void setValidationStatus(ValidationStatus validationStatus) { + this.validationStatus = validationStatus; + } + + public Long getUncompressedSize() { + return uncompressedSize; + } + + public void setUncompressedSize(Long uncompressedSize) { + this.uncompressedSize = uncompressedSize; + } + @Override public String getFromCheckpointId() { return fromCheckpointId; diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupDataStoreVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupDataStoreVO.java new file mode 100644 index 000000000000..5266a4129905 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupDataStoreVO.java @@ -0,0 +1,95 @@ +//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 +//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.backup; + +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.persistence.Column; +import javax.persistence.Entity; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + + +@Entity +@Table(name = "internal_backup_store_ref") +public class InternalBackupDataStoreVO implements InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "backup_id") + private long backupId; + + @Column(name = "volume_id") + private long volumeId; + + @Column (name = "device_id") + private long deviceId; + + @Column(name = "path") + private String backupPath; + + public InternalBackupDataStoreVO() { + } + + public InternalBackupDataStoreVO(long backupId, long volumeId, long deviceId, String backupPath) { + this.backupId = backupId; + this.volumeId = volumeId; + this.deviceId = deviceId; + this.backupPath = backupPath; + } + + @Override + public long getId() { + return id; + } + + public long getBackupId() { + return backupId; + } + + public long getVolumeId() { + return volumeId; + } + + public long getDeviceId() { + return deviceId; + } + + public String getBackupPath() { + return backupPath; + } + + public void setVolumeId(long volumeId) { + this.volumeId = volumeId; + } + + public void setBackupPath(String backupPath) { + this.backupPath = backupPath; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java new file mode 100644 index 000000000000..e9e232ef2023 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java @@ -0,0 +1,211 @@ +// 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.backup; + +import com.google.gson.Gson; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.commons.lang3.StringUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +@Entity +@Table(name = "internal_backup_view") +public class InternalBackupJoinVO { + + @Id + @Column(name="id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "vm_id") + private long vmId; + + @Column(name = "backed_volumes", length = 65535) + private String backedUpVolumes; + + @Column(name = "backup_offering_id") + private long backupOfferingId; + + @Column(name = "image_store_id") + private long imageStoreId; + + @Column(name = "parent_id") + private long parentId; + + @Column(name = "type") + private String type; + + @Column(name = "date") + @Temporal(value = TemporalType.DATE) + private Date date; + + @Enumerated(value = EnumType.STRING) + @Column(name = "status") + private Backup.Status status; + + @Enumerated(value = EnumType.STRING) + @Column(name = "compression_status") + private Backup.CompressionStatus compressionStatus; + + @Column(name = "end_of_chain") + private Boolean endOfChain; + + @Column(name = "current") + private Boolean current; + + @Column(name = "image_store_path") + private String imageStorePath; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "size") + private long size; + + @Column(name = "protected_size") + private long protectedSize; + + @Column(name = "volume_id") + private long volumeId; + + @Column(name = "isolated") + private Boolean isolated; + + @Column(name = "storage_pool_delta_path") + private String storagePoolDeltaPath; + + @Column(name = "storage_pool_parent_path") + private String storagePoolParentPath; + + @Column(name = "schedule_id") + private Long scheduleId; + + public InternalBackupJoinVO() { + } + + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public long getVmId() { + return vmId; + } + + public List getBackedUpVolumes() { + if (StringUtils.isEmpty(this.backedUpVolumes)) { + return Collections.emptyList(); + } + return Arrays.asList(new Gson().fromJson(this.backedUpVolumes, Backup.VolumeInfo[].class)); + } + + public long getBackupOfferingId() { + return backupOfferingId; + } + + public long getImageStoreId() { + return imageStoreId; + } + + public long getParentId() { + return parentId; + } + + public String getType() { + return type; + } + + public Date getDate() { + return date; + } + + public Backup.Status getStatus() { + return status; + } + + public Boolean getEndOfChain() { + return BooleanUtils.isTrue(endOfChain); + } + + public Boolean getCurrent() { + return BooleanUtils.isTrue(current); + } + + public String getImageStorePath() { + return imageStorePath; + } + + public long getZoneId() { + return zoneId; + } + + public long getSize() { + return size; + } + + public long getProtectedSize() { + return protectedSize; + } + + public long getVolumeId() { + return volumeId; + } + + public Boolean getIsolated() { + return BooleanUtils.isTrue(isolated); + } + + public Backup.CompressionStatus getCompressionStatus() { + return compressionStatus; + } + + public String getStoragePoolDeltaPath() { + return storagePoolDeltaPath; + } + + public String getStoragePoolParentPath() { + return storagePoolParentPath; + } + + public Long getScheduleId() { + return scheduleId; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobType.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobType.java new file mode 100644 index 000000000000..287b64c2e6fc --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobType.java @@ -0,0 +1,21 @@ +// 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.backup; + +public enum InternalBackupServiceJobType { + StartCompression, FinalizeCompression, BackupValidation +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobVO.java new file mode 100644 index 000000000000..87b4a53cb615 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobVO.java @@ -0,0 +1,179 @@ +// 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.backup; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; + +@Entity +@Table(name = "internal_backup_service_job") +public class InternalBackupServiceJobVO implements InternalIdentity, Comparable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "backup_id") + private long backupId; + + @Column(name = "instance_id") + private long instanceId; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "host_id") + private Long hostId; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "attempts") + private int attempts; + + @Column (name = "type") + private InternalBackupServiceJobType type; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = "scheduled_start_time") + @Temporal(value = TemporalType.TIMESTAMP) + private Date scheduledStartTime; + + @Column(name = "start_time") + @Temporal(value = TemporalType.TIMESTAMP) + private Date startTime; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + public InternalBackupServiceJobVO() { + } + + public InternalBackupServiceJobVO(long backupId, long zoneId, long instanceId, long accountId, InternalBackupServiceJobType type) { + this.created = new Date(); + this.backupId = backupId; + this.zoneId = zoneId; + this.instanceId = instanceId; + this.accountId = accountId; + this.type = type; + this.scheduledStartTime = this.created; + } + + public InternalBackupServiceJobVO(long backupId, long zoneId, long instanceId, long accountId, InternalBackupServiceJobType type, Date scheduledStartTime) { + this(backupId, zoneId, instanceId, accountId, type); + this.scheduledStartTime = scheduledStartTime; + } + + @Override + public long getId() { + return id; + } + + public long getBackupId() { + return backupId; + } + + public long getInstanceId() { + return instanceId; + } + + public long getAccountId() { + return accountId; + } + + public Long getHostId() { + return hostId; + } + + public void setHostId(Long hostId) { + this.hostId = hostId; + } + + public long getZoneId() { + return zoneId; + } + + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public int getAttempts() { + return attempts; + } + + public void setAttempts(int attempts) { + this.attempts = attempts; + } + + public InternalBackupServiceJobType getType() { + return type; + } + + public Date getCreated() { + return created; + } + + public Date getScheduledStartTime() { + return scheduledStartTime; + } + + public void setScheduledStartTime(Date scheduledStartTime) { + this.scheduledStartTime = scheduledStartTime; + } + + public Date getStartTime() { + return startTime; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + @Override + public int compareTo(InternalBackupServiceJobVO that) { + return this.created.compareTo(that.created); + } + + @Override + public String toString() { + return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "backupId", "zoneId", "hostId", "created", "scheduledStartTime", "startTime", "attempts", + "type"); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupStoragePoolVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupStoragePoolVO.java new file mode 100644 index 000000000000..f6d4cd10ffb7 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupStoragePoolVO.java @@ -0,0 +1,101 @@ +//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 +//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.backup; + +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "internal_backup_pool_ref") +public class InternalBackupStoragePoolVO implements InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "backup_id") + private long backupId; + + @Column(name = "storage_pool_id") + private long storagePoolId; + + @Column(name = "volume_id") + private long volumeId; + + @Column(name = "backup_delta_path") + private String backupDeltaPath; + + @Column(name = "backup_parent_path") + private String backupDeltaParentPath; + + public InternalBackupStoragePoolVO() { + } + + public InternalBackupStoragePoolVO(long backupId, long storagePoolId, long volumeId, String backupDeltaPath, String backupDeltaParentPath) { + this.backupId = backupId; + this.storagePoolId = storagePoolId; + this.volumeId = volumeId; + this.backupDeltaPath = backupDeltaPath; + this.backupDeltaParentPath = backupDeltaParentPath; + } + + @Override + public long getId() { + return id; + } + + public long getBackupId() { + return backupId; + } + + public long getStoragePoolId() { + return storagePoolId; + } + + public long getVolumeId() { + return volumeId; + } + + public String getBackupDeltaPath() { + return backupDeltaPath; + } + + public String getBackupDeltaParentPath() { + return backupDeltaParentPath; + } + + public void setBackupDeltaPath(String backupDeltaPath) { + this.backupDeltaPath = backupDeltaPath; + } + + public void setBackupDeltaParentPath(String backupDeltaParentPath) { + this.backupDeltaParentPath = backupDeltaParentPath; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java index e60e49e1a0c2..3829777d9d8f 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java @@ -42,4 +42,5 @@ public interface BackupDao extends GenericDao { void loadDetails(BackupVO backup); void saveDetails(BackupVO backup); List listBySchedule(Long backupScheduleId); + BackupVO findLatestByStatusAndVmId(Backup.Status status, long vmId); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java index fd29da72c718..9859f29701b7 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java @@ -90,6 +90,7 @@ protected void init() { backupSearch.and("external_id", backupSearch.entity().getExternalId(), SearchCriteria.Op.EQ); backupSearch.and("backup_offering_id", backupSearch.entity().getBackupOfferingId(), SearchCriteria.Op.EQ); backupSearch.and("zone_id", backupSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + backupSearch.and("status", backupSearch.entity().getStatus(), SearchCriteria.Op.IN); backupSearch.done(); backupVmSearchInZone = createSearchBuilder(Long.class); @@ -280,4 +281,13 @@ public List listVmIdsWithBackupsInZone(Long zoneId) { sc.setParameters("zone_id", zoneId); return customSearchIncludingRemoved(sc, null); } + + @Override + public BackupVO findLatestByStatusAndVmId(Backup.Status status, long vmId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters("vm_id", vmId); + sc.setParameters("status", status); + Filter filter = new Filter(BackupVO.class, "date", false, 0L, 1L); + return findOneBy(sc, filter); + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDao.java index 664650074bce..c6c8fc3a322e 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDao.java @@ -23,4 +23,19 @@ public interface BackupDetailsDao extends GenericDao, ResourceDetailsDao { + String END_OF_CHAIN = "end_of_chain"; + + String CURRENT = "current"; + + String IMAGE_STORE_ID = "image_store_id"; + + String PARENT_ID = "parent_id"; + + String ISOLATED = "isolated"; + + String SCREENSHOT_PATH = "screenshot_path"; + + String BACKUP_HASH = "backup_hash"; + + void removeDetailsExcept(long backupId, String exception); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDaoImpl.java index 08c7192af909..5f257c23892c 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDaoImpl.java @@ -17,13 +17,39 @@ package org.apache.cloudstack.backup.dao; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; import org.apache.cloudstack.backup.BackupDetailVO; import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; + @Component public class BackupDetailsDaoImpl extends ResourceDetailsDaoBase implements BackupDetailsDao { + private SearchBuilder backupDetailSearch; + + private static final String BACKUP_ID = "backup_id"; + + private static final String KEY = "key"; + + @PostConstruct + protected void init() { + backupDetailSearch = createSearchBuilder(); + backupDetailSearch.and(BACKUP_ID, backupDetailSearch.entity().getResourceId(), SearchCriteria.Op.EQ); + backupDetailSearch.and(KEY, backupDetailSearch.entity().getName(), SearchCriteria.Op.NEQ); + backupDetailSearch.done(); + } + + @Override + public void removeDetailsExcept(long backupId, String exception) { + SearchCriteria sc = backupDetailSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + sc.setParameters(KEY, exception); + super.expunge(sc); + } + @Override public void addDetail(long resourceId, String key, String value, boolean display) { super.addDetail(new BackupDetailVO(resourceId, key, value, display)); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java index 708faeef4643..1d7a55d7caf7 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java @@ -22,8 +22,10 @@ import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.response.BackupOfferingResponse; import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.BackupOfferingDetailsVO; import org.apache.cloudstack.backup.BackupOfferingVO; import com.cloud.dc.DataCenterVO; @@ -32,7 +34,9 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class BackupOfferingDaoImpl extends GenericDaoBase implements BackupOfferingDao { @@ -62,6 +66,7 @@ public BackupOfferingResponse newBackupOfferingResponse(BackupOffering offering, DataCenterVO zone = dataCenterDao.findById(offering.getZoneId()); List domainIds = backupOfferingDetailsDao.findDomainIds(offering.getId()); + List details = backupOfferingDetailsDao.listDetails(offering.getId()); BackupOfferingResponse response = new BackupOfferingResponse(); response.setId(offering.getUuid()); response.setName(offering.getName()); @@ -88,6 +93,14 @@ public BackupOfferingResponse newBackupOfferingResponse(BackupOffering offering, if (crossZoneInstanceCreation) { response.setCrossZoneInstanceCreation(true); } + details.removeIf(backupOfferingDetailsVO -> ApiConstants.DOMAIN_ID.equals(backupOfferingDetailsVO.getName())); + Map detailString = new HashMap<>(); + for (BackupOfferingDetailsVO detail : details) { + detailString.put(detail.getName(), detail.getValue()); + } + if (!detailString.isEmpty()) { + response.setDetails(detailString); + } response.setCreated(offering.getCreated()); response.setObjectName("backupoffering"); return response; diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java new file mode 100644 index 000000000000..e71ffe5ebde9 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java @@ -0,0 +1,33 @@ +//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 +//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.backup.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.backup.InternalBackupDataStoreVO; + +import java.util.List; + +public interface InternalBackupDataStoreDao extends GenericDao { + + List listByBackupId(long backupId); + + InternalBackupDataStoreVO findByBackupIdAndVolumeId(long backupId, long volumeId); + + void expungeByBackupId(long backupId); + + void updateVolumeId(long oldVolumeId, long newVolumeId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java new file mode 100644 index 000000000000..34a064ff62a2 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java @@ -0,0 +1,74 @@ +//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 +//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.backup.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.UpdateBuilder; +import org.apache.cloudstack.backup.InternalBackupDataStoreVO; + +import javax.annotation.PostConstruct; +import java.util.List; + +public class InternalBackupDataStoreDaoImpl extends GenericDaoBase implements InternalBackupDataStoreDao { + + private SearchBuilder backupSearch; + + private static final String BACKUP_ID = "backup_id"; + private static final String VOLUME_ID = "volume_id"; + + @PostConstruct + protected void init() { + backupSearch = createSearchBuilder(); + backupSearch.and(BACKUP_ID, backupSearch.entity().getBackupId(), SearchCriteria.Op.EQ); + backupSearch.and(VOLUME_ID, backupSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + backupSearch.done(); + } + + @Override + public List listByBackupId(long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + return listBy(sc); + } + + @Override + public InternalBackupDataStoreVO findByBackupIdAndVolumeId(long backupId, long volumeId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + sc.setParameters(VOLUME_ID, volumeId); + return findOneBy(sc); + } + + @Override + public void expungeByBackupId(long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + expunge(sc); + } + + @Override + public void updateVolumeId(long oldVolumeId, long newVolumeId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VOLUME_ID, oldVolumeId); + InternalBackupDataStoreVO delta = createForUpdate(); + delta.setVolumeId(newVolumeId); + UpdateBuilder ub = getUpdateBuilder(delta); + update(ub, sc, null); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDao.java new file mode 100644 index 000000000000..3cb6ea241041 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDao.java @@ -0,0 +1,44 @@ +//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 +//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.backup.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.backup.InternalBackupJoinVO; + +import java.util.Date; +import java.util.List; + +public interface InternalBackupJoinDao extends GenericDao { + + List listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(long vmId, Long scheduleId, Date date, boolean before, boolean ascending); + + List listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(long vmId, Long scheduleId, Date beforeDate); + + InternalBackupJoinVO findCurrent(long vmId, Long scheduleId); + + List listCurrents(long vmId, boolean descending); + + List listCurrentsByVolumeIdDesc(long volumeId); + + InternalBackupJoinVO findByParentId(long parentId); + + List listByImageStoreId(long imageStoreId); + + List listById(long id); + + List listByParentId(long parentId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDaoImpl.java new file mode 100644 index 000000000000..bbc91db13e83 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDaoImpl.java @@ -0,0 +1,158 @@ +// 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.backup.dao; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.InternalBackupJoinVO; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class InternalBackupJoinDaoImpl extends GenericDaoBase implements InternalBackupJoinDao { + + private static final String ID = "id"; + private static final String VM_ID = "vm_id"; + private static final String STATUS = "status"; + private static final String CREATED_BEFORE = "created_before"; + private static final String CREATED_AFTER = "created_after"; + private static final String CURRENT = "current"; + private static final String ISOLATED = "isolated"; + private static final String PARENT_ID = "parent_id"; + private static final String IMAGE_STORE_ID = "image_store_id"; + private static final String SCHEDULE_ID = "schedule_id"; + private static final String VOLUME_ID = "volume_id"; + private SearchBuilder backupSearch; + private SearchBuilder allBackupsSearch; + + @PostConstruct + protected void init() { + backupSearch = createSearchBuilder(); + backupSearch.and(VM_ID, backupSearch.entity().getVmId(), SearchCriteria.Op.EQ); + backupSearch.and(STATUS, backupSearch.entity().getStatus(), SearchCriteria.Op.IN); + backupSearch.and(CREATED_BEFORE, backupSearch.entity().getDate(), SearchCriteria.Op.LT); + backupSearch.and(CREATED_AFTER, backupSearch.entity().getDate(), SearchCriteria.Op.GT); + backupSearch.and(CURRENT, backupSearch.entity().getCurrent(), SearchCriteria.Op.EQ); + backupSearch.and(PARENT_ID, backupSearch.entity().getParentId(), SearchCriteria.Op.EQ); + backupSearch.and(ISOLATED, backupSearch.entity().getIsolated(), SearchCriteria.Op.EQ); + backupSearch.and(SCHEDULE_ID, backupSearch.entity().getScheduleId(), SearchCriteria.Op.EQ); + backupSearch.groupBy(backupSearch.entity().getId()); + backupSearch.done(); + + allBackupsSearch = createSearchBuilder(); + allBackupsSearch.and(ID, allBackupsSearch.entity().getId(), SearchCriteria.Op.EQ); + allBackupsSearch.and(STATUS, allBackupsSearch.entity().getStatus(), SearchCriteria.Op.IN); + allBackupsSearch.and(PARENT_ID, allBackupsSearch.entity().getParentId(), SearchCriteria.Op.EQ); + allBackupsSearch.and(IMAGE_STORE_ID, allBackupsSearch.entity().getImageStoreId(), SearchCriteria.Op.EQ); + allBackupsSearch.and(VM_ID, allBackupsSearch.entity().getVmId(), SearchCriteria.Op.EQ); + allBackupsSearch.and(VOLUME_ID, allBackupsSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + allBackupsSearch.and(CURRENT, allBackupsSearch.entity().getCurrent(), SearchCriteria.Op.EQ); + allBackupsSearch.done(); + } + + @Override + public List listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(long vmId, Long scheduleId, Date date, boolean before, boolean ascending) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VM_ID, vmId); + sc.setParameters(STATUS, Backup.Status.BackedUp); + if (before) { + sc.setParameters(CREATED_BEFORE, date); + } else { + sc.setParameters(CREATED_AFTER, date); + } + sc.setParameters(ISOLATED, Boolean.FALSE.toString()); + sc.setParameters(SCHEDULE_ID, scheduleId); + Filter filter = new Filter(InternalBackupJoinVO.class, "date", ascending); + return new ArrayList<>(listBy(sc, filter)); + } + + @Override + public List listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(long vmId, Long scheduleId, Date beforeDate) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VM_ID, vmId); + sc.setParameters(STATUS, Backup.Status.BackedUp, Backup.Status.Removed); + sc.setParameters(CREATED_BEFORE, beforeDate); + sc.setParameters(ISOLATED, Boolean.FALSE.toString()); + sc.setParameters(SCHEDULE_ID, scheduleId); + Filter filter = new Filter(InternalBackupJoinVO.class, "date", false); + return new ArrayList<>(listIncludingRemovedBy(sc, filter)); + } + + @Override + public InternalBackupJoinVO findCurrent(long vmId, Long scheduleId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VM_ID, vmId); + sc.setParameters(CURRENT, Boolean.TRUE.toString()); + sc.setParameters(SCHEDULE_ID, scheduleId); + return findOneBy(sc); + } + + @Override + public InternalBackupJoinVO findByParentId(long parentId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(PARENT_ID, parentId); + return findOneIncludingRemovedBy(sc); + } + + @Override + public List listByImageStoreId(long imageStoreId) { + SearchCriteria sc = allBackupsSearch.create(); + sc.setParameters(IMAGE_STORE_ID, imageStoreId); + sc.setParameters(STATUS, Backup.Status.BackedUp); + return listBy(sc); + } + + @Override + public List listById(long id) { + SearchCriteria sc = allBackupsSearch.create(); + sc.setParameters(ID, id); + sc.setParameters(STATUS, Backup.Status.BackedUp); + return listBy(sc); + } + + @Override + public List listByParentId(long parentId) { + SearchCriteria sc = allBackupsSearch.create(); + sc.setParameters(PARENT_ID, parentId); + sc.setParameters(STATUS, Backup.Status.BackedUp); + return listBy(sc); + } + + @Override + public List listCurrents(long vmId, boolean descending) { + SearchCriteria sc = allBackupsSearch.create(); + sc.setParameters(VM_ID, vmId); + sc.setParameters(CURRENT, Boolean.TRUE.toString()); + Filter filter = new Filter(InternalBackupJoinVO.class, "date", !descending); + + return listBy(sc, filter); + } + + @Override + public List listCurrentsByVolumeIdDesc(long volumeId) { + SearchCriteria sc = allBackupsSearch.create(); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(CURRENT, Boolean.TRUE.toString()); + Filter filter = new Filter(InternalBackupJoinVO.class, "date", false); + return listBy(sc, filter); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDao.java new file mode 100644 index 000000000000..c80734b9cb3c --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDao.java @@ -0,0 +1,39 @@ +//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 +//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.backup.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.backup.InternalBackupServiceJobType; +import org.apache.cloudstack.backup.InternalBackupServiceJobVO; + +import java.util.Date; +import java.util.List; + +public interface InternalBackupServiceJobDao extends GenericDao { + + List listExecutingJobsByZoneIdAndJobType(long zoneId, InternalBackupServiceJobType... jobTypes); + + List listWaitingJobsAndScheduledToBeforeNow(long zoneId, InternalBackupServiceJobType... jobTypes); + + List listExecutingJobsByHostsAndStartTimeBeforeAndTypeIn(Object[] hostIds, Date date, InternalBackupServiceJobType... jobTypes); + + Pair, Integer> searchAndCountForListApi(Long id, Long backupId, Long hostId, Long zoneId, InternalBackupServiceJobType type, boolean executing, + boolean scheduled, Long startIndex, Long pageSize); + + void update(InternalBackupServiceJobVO job); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDaoImpl.java new file mode 100644 index 000000000000..32e81261838d --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDaoImpl.java @@ -0,0 +1,138 @@ +//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 +//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.backup.dao; + +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.backup.InternalBackupServiceJobType; +import org.apache.cloudstack.backup.InternalBackupServiceJobVO; + +import javax.annotation.PostConstruct; +import java.util.Date; +import java.util.List; + +public class InternalBackupServiceJobDaoImpl extends GenericDaoBase implements InternalBackupServiceJobDao { + private static final String ID = "id"; + private static final String BACKUP_ID = "backup_id"; + private SearchBuilder executingBeforeAndHostInAndTypeInSearch; + private SearchBuilder scheduledAndNotStartedSearch; + + private SearchBuilder executingAndZoneIdAndTypeSearch; + + private static final String HOST_ID = "host_id"; + private static final String TYPE = "type"; + private static final String START_TIME = "start_time"; + private static final String SCHEDULED = "scheduled"; + private static final String ZONE_ID = "zone_id"; + + @PostConstruct + protected void init() { + executingBeforeAndHostInAndTypeInSearch = createSearchBuilder(); + executingBeforeAndHostInAndTypeInSearch.and(HOST_ID, executingBeforeAndHostInAndTypeInSearch.entity().getHostId(), SearchCriteria.Op.IN); + executingBeforeAndHostInAndTypeInSearch.and(START_TIME, executingBeforeAndHostInAndTypeInSearch.entity().getStartTime(), SearchCriteria.Op.LTEQ); + executingBeforeAndHostInAndTypeInSearch.and(TYPE, executingBeforeAndHostInAndTypeInSearch.entity().getType(), SearchCriteria.Op.IN); + executingBeforeAndHostInAndTypeInSearch.done(); + + scheduledAndNotStartedSearch = createSearchBuilder(); + scheduledAndNotStartedSearch.and(SCHEDULED, scheduledAndNotStartedSearch.entity().getScheduledStartTime(), SearchCriteria.Op.LTEQ); + scheduledAndNotStartedSearch.and(START_TIME, scheduledAndNotStartedSearch.entity().getStartTime(), SearchCriteria.Op.NULL); + scheduledAndNotStartedSearch.and(ZONE_ID, scheduledAndNotStartedSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + scheduledAndNotStartedSearch.and(TYPE, scheduledAndNotStartedSearch.entity().getType(), SearchCriteria.Op.IN); + scheduledAndNotStartedSearch.done(); + + executingAndZoneIdAndTypeSearch = createSearchBuilder(); + executingAndZoneIdAndTypeSearch.and(START_TIME, executingAndZoneIdAndTypeSearch.entity().getStartTime(), SearchCriteria.Op.NNULL); + executingAndZoneIdAndTypeSearch.and(ZONE_ID, executingAndZoneIdAndTypeSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + executingAndZoneIdAndTypeSearch.and(TYPE, executingAndZoneIdAndTypeSearch.entity().getType(), SearchCriteria.Op.IN); + executingAndZoneIdAndTypeSearch.done(); + } + + @Override + public List listExecutingJobsByZoneIdAndJobType(long zoneId, InternalBackupServiceJobType... jobTypes) { + SearchCriteria sc = executingAndZoneIdAndTypeSearch.create(); + sc.setParameters(TYPE, (Object[]) jobTypes); + sc.setParameters(ZONE_ID, zoneId); + + return listBy(sc); + } + + @Override + public List listWaitingJobsAndScheduledToBeforeNow(long zoneId, InternalBackupServiceJobType... jobTypes) { + SearchCriteria sc = scheduledAndNotStartedSearch.create(); + + sc.setParameters(SCHEDULED, DateUtil.now()); + sc.setParameters(ZONE_ID, zoneId); + sc.setParameters(TYPE, (Object[]) jobTypes); + + Filter filter = new Filter(InternalBackupServiceJobVO.class, "scheduledStartTime", true); + return listBy(sc, filter); + } + + @Override + public List listExecutingJobsByHostsAndStartTimeBeforeAndTypeIn(Object[] hostIds, Date date, InternalBackupServiceJobType... jobTypes) { + SearchCriteria sc = executingBeforeAndHostInAndTypeInSearch.create(); + sc.setParameters(HOST_ID, hostIds); + sc.setParameters(START_TIME, date); + sc.setParameters(TYPE, (Object[]) jobTypes); + + return listBy(sc); + } + + @Override + public Pair, Integer> searchAndCountForListApi(Long id, Long backupId, Long hostId, Long zoneId, InternalBackupServiceJobType type, boolean executing, + boolean scheduled, Long startIndex, Long pageSize) { + SearchBuilder sb = createSearchBuilder(); + + sb.and(ID, sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and(BACKUP_ID, sb.entity().getBackupId(), SearchCriteria.Op.EQ); + sb.and(HOST_ID, sb.entity().getHostId(), SearchCriteria.Op.EQ); + sb.and(ZONE_ID, sb.entity().getZoneId(), SearchCriteria.Op.EQ); + sb.and(TYPE, sb.entity().getType(), SearchCriteria.Op.EQ); + + boolean removed = !executing && !scheduled; + if (executing && !scheduled) { + sb.and("executing", sb.entity().getStartTime(), SearchCriteria.Op.NNULL); + } else if (scheduled && !executing) { + sb.and("scheduled", sb.entity().getStartTime(), SearchCriteria.Op.NULL); + } + + SearchCriteria sc = sb.create(); + + sc.setParametersIfNotNull(ID, id); + sc.setParametersIfNotNull(BACKUP_ID, backupId); + sc.setParametersIfNotNull(HOST_ID, hostId); + sc.setParametersIfNotNull(ZONE_ID, zoneId); + if (type != null) { + sc.setParameters(TYPE, type); + } + + Filter filter = new Filter(InternalBackupServiceJobVO.class, "created", false, startIndex, pageSize); + + return searchAndCount(sc, filter, removed); + } + + @Override + @DB + public void update(InternalBackupServiceJobVO job) { + super.update(job.getId(), job); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDao.java new file mode 100644 index 000000000000..e6628f78af8c --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDao.java @@ -0,0 +1,37 @@ +//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 +//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.backup.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.backup.InternalBackupStoragePoolVO; + +import java.util.List; + +public interface InternalBackupStoragePoolDao extends GenericDao { + + List listByBackupId(long backupId); + + List listByVolumeId(long volumeId); + + InternalBackupStoragePoolVO findOneByVolumeIdAndBackupId(long volumeId, long backupId); + + void expungeByBackupId(long backupId); + + void expungeByVolumeId(long volumeId); + + void expungeByVolumeIdAndBackupId(long volumeId, long backupId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDaoImpl.java new file mode 100644 index 000000000000..41002aeb51d2 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDaoImpl.java @@ -0,0 +1,86 @@ +//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 +//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.backup.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.backup.InternalBackupStoragePoolVO; + +import javax.annotation.PostConstruct; +import java.util.List; + +public class InternalBackupStoragePoolDaoImpl extends GenericDaoBase implements InternalBackupStoragePoolDao { + + private SearchBuilder backupSearch; + + private static final String BACKUP_ID = "backup_id"; + + private static final String VOLUME_ID = "volume_id"; + + @PostConstruct + protected void init() { + backupSearch = createSearchBuilder(); + backupSearch.and(BACKUP_ID, backupSearch.entity().getBackupId(), SearchCriteria.Op.EQ); + backupSearch.and(VOLUME_ID, backupSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + backupSearch.done(); + } + + @Override + public List listByBackupId(long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + return listBy(sc); + } + + @Override + public List listByVolumeId(long volumeId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VOLUME_ID, volumeId); + return listBy(sc); + } + + @Override + public InternalBackupStoragePoolVO findOneByVolumeIdAndBackupId(long volumeId, long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(BACKUP_ID, backupId); + return findOneBy(sc); + } + + @Override + public void expungeByBackupId(long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + expunge(sc); + } + + @Override + public void expungeByVolumeId(long volumeId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VOLUME_ID, volumeId); + expunge(sc); + } + + @Override + public void expungeByVolumeIdAndBackupId(long volumeId, long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(BACKUP_ID, backupId); + expunge(sc); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java index 3329983d711e..2d337fe07fc7 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java @@ -99,6 +99,8 @@ public interface SnapshotDataStoreDao extends GenericDao findByVolume(long snapshotId, long volumeId, DataStoreRole role); + void expungeBySnapshotIdAndStoreRole(long snapshotId, DataStoreRole role); + /** * List all snapshots in 'snapshot_store_ref' by volume and data store role. Therefore, it is possible to list all snapshots that are in the primary storage or in the secondary storage. */ diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java index 8b7a2b78de7e..cb88e21cf9d6 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java @@ -63,7 +63,7 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase searchFilteringStoreIdEqStoreRoleEqStateNeqRefCntNeq; protected SearchBuilder searchFilteringStoreIdEqStateEqStoreRoleEqIdEqUpdateCountEqSnapshotIdEqVolumeIdEq; private SearchBuilder stateSearch; @@ -76,7 +76,9 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase storeSnapshotDownloadStatusSearch; private SearchBuilder searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqKVMCheckpointNotNull; private SearchBuilder searchFilterStateAndDownloadUrlNotNullAndDownloadUrlCreatedBefore; - private SearchBuilder searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq; + private SearchBuilder searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq; + + private SearchBuilder searchFilteringVolumeIdAndStateAndCreatedAfter; private SearchBuilder searchBySnapshotId; @@ -198,17 +200,24 @@ public boolean configure(String name, Map params) throws Configu searchFilterStateAndDownloadUrlNotNullAndDownloadUrlCreatedBefore.and(URL_CREATED_BEFORE, searchFilterStateAndDownloadUrlNotNullAndDownloadUrlCreatedBefore.entity().getExtractUrlCreated(), SearchCriteria.Op.LT); searchFilterStateAndDownloadUrlNotNullAndDownloadUrlCreatedBefore.done(); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq = createSearchBuilder(); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(STATE, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getState(), SearchCriteria.Op.EQ); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(VOLUME_ID, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getVolumeId(), SearchCriteria.Op.EQ); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(STORE_ROLE, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getRole(), SearchCriteria.Op.EQ); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(STORE_ID, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getDataStoreId(), SearchCriteria.Op.IN); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq = createSearchBuilder(); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(STATE, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getState(), SearchCriteria.Op.EQ); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(VOLUME_ID, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getVolumeId(), SearchCriteria.Op.EQ); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(STORE_ROLE, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getRole(), SearchCriteria.Op.EQ); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(STORE_ID, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getDataStoreId(), SearchCriteria.Op.IN); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(INSTALL_PATH, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getInstallPath(), SearchCriteria.Op.EQ); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.done(); searchBySnapshotId = createSearchBuilder(); searchBySnapshotId.and(SNAPSHOT_ID, searchBySnapshotId.entity().getSnapshotId(), SearchCriteria.Op.EQ); searchBySnapshotId.and(STATE, searchBySnapshotId.entity().getState(), SearchCriteria.Op.EQ); searchBySnapshotId.done(); + searchFilteringVolumeIdAndStateAndCreatedAfter = createSearchBuilder(); + searchFilteringVolumeIdAndStateAndCreatedAfter.and(STATE, searchFilteringVolumeIdAndStateAndCreatedAfter.entity().getState(), SearchCriteria.Op.EQ); + searchFilteringVolumeIdAndStateAndCreatedAfter.and(VOLUME_ID, searchFilteringVolumeIdAndStateAndCreatedAfter.entity().getVolumeId(), SearchCriteria.Op.EQ); + searchFilteringVolumeIdAndStateAndCreatedAfter.and(CREATED, searchFilteringVolumeIdAndStateAndCreatedAfter.entity().getCreated(), SearchCriteria.Op.GT); + searchFilteringVolumeIdAndStateAndCreatedAfter.done(); return true; } @@ -356,7 +365,7 @@ public SnapshotDataStoreVO findParent(DataStoreRole role, Long storeId, Long zon if (kvmIncrementalSnapshot && Hypervisor.HypervisorType.KVM.equals(hypervisorType)) { sc = searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqKVMCheckpointNotNull.create(); } else { - sc = searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.create(); + sc = searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.create(); } sc.setParameters(VOLUME_ID, volumeId); @@ -469,6 +478,12 @@ public SnapshotDataStoreVO findBySnapshotIdInAnyState(long snapshotId, DataStore return findOneBy(sc); } + @Override + public void expungeBySnapshotIdAndStoreRole(long snapshotId, DataStoreRole role) { + SearchCriteria sc = createSearchCriteriaBySnapshotIdAndStoreRole(snapshotId, role); + expunge(sc); + } + @Override public List listAllByVolumeAndDataStore(long volumeId, DataStoreRole role) { SearchCriteria sc = searchFilteringStoreIdEqStateEqStoreRoleEqIdEqUpdateCountEqSnapshotIdEqVolumeIdEq.create(); diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index d6c4935e8ddf..932db538f30b 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -272,6 +272,10 @@ + + + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 80293aaab353..ab5ac7b2b875 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -591,3 +591,58 @@ CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone_network_map` ( -- This is part of allowing firewall rules on public IP addresses in VPC network ALTER TABLE `cloud`.`firewall_rules` MODIFY COLUMN `network_id` BIGINT UNSIGNED NULL; + +-- KBOSS + +CREATE TABLE IF NOT EXISTS `cloud`.`internal_backup_pool_ref` ( + `id` bigint NOT NULL UNIQUE AUTO_INCREMENT, + `backup_id` bigint unsigned NOT NULL COMMENT 'The backup ID. Foreign key that points to the backups table.', + `storage_pool_id` bigint unsigned NOT NULL COMMENT 'The storage ID. Foreign key that points to the storage_pool table.', + `volume_id` bigint unsigned NOT NULL COMMENT 'The volumes ID. Foreign key that points to the volumes table.', + `backup_delta_path` varchar(255) COMMENT 'Path of the created delta.', + `backup_parent_path` varchar(255) COMMENT 'Path of the created delta parent.', + PRIMARY KEY (`id`), + CONSTRAINT `fk_internal_backup_pool_ref__backup_id` FOREIGN KEY (`backup_id`) REFERENCES `backups`(`id`), + CONSTRAINT `fk_internal_backup_pool_ref__storage_pool_id` FOREIGN KEY (`storage_pool_id`) REFERENCES `storage_pool`(`id`), + CONSTRAINT `fk_internal_backup_pool_ref__volume_id` FOREIGN KEY (`volume_id`) REFERENCES `volumes`(`id`) + ); + +CREATE TABLE IF NOT EXISTS `cloud`.`internal_backup_store_ref` ( + `id` bigint NOT NULL UNIQUE AUTO_INCREMENT, + `backup_id` bigint unsigned NOT NULL COMMENT 'The backup ID. Foreign key that points to the backups table.', + `volume_id` bigint unsigned NOT NULL COMMENT 'The volume ID. Foreign key that points to the volumes table.', + `device_id` bigint unsigned COMMENT 'device ID of the volume', + `path` varchar(255) COMMENT 'Path of the backup.', + PRIMARY KEY (`id`), + CONSTRAINT `fk_internal_backup_store_ref__backup_id` FOREIGN KEY (`backup_id`) REFERENCES `backups`(`id`), + CONSTRAINT `fk_internal_backup_store_ref__volume_id` FOREIGN KEY (`volume_id`) REFERENCES `volumes`(`id`) + ); + +CREATE TABLE IF NOT EXISTS `cloud`.`internal_backup_service_job` ( + `id` bigint NOT NULL UNIQUE AUTO_INCREMENT, + `backup_id` bigint unsigned NOT NULL COMMENT 'The backup ID. Foreign key that points to the backups table.', + `instance_id` bigint unsigned NOT NULL COMMENT 'The instance ID. Foreign key that points to the vm_instance table.', + `account_id` bigint(20) unsigned COMMENT 'Account ID of the owner of the VM.', + `host_id` bigint unsigned COMMENT 'The host ID that is executing the compression. Foreign key that points to the host table.', + `zone_id` bigint unsigned NOT NULL COMMENT 'The zone ID of the where the VM is. Foreign key that points to the data_center table', + `attempts` int(32) unsigned NOT NULL DEFAULT 0, + `type` varchar(55) NOT NULL, + `created` datetime NOT NULL, + `scheduled_start_time` datetime NOT NULL, + `start_time` datetime, + `removed` datetime, + PRIMARY KEY (`id`), + CONSTRAINT `fk_internal_backup_service_job__backup_id` FOREIGN KEY (`backup_id`) REFERENCES `backups`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_internal_backup_service_job__instance_id` FOREIGN KEY (`instance_id`) REFERENCES `vm_instance`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_internal_backup_service_job__host_id` FOREIGN KEY (`host_id`) REFERENCES `host`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_internal_backup_service_job__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_internal_backup_service_job__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE + ); + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'uncompressed_size', 'bigint unsigned'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'compression_status', 'varchar(55)'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'validation_status', 'varchar(55)'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule', 'isolated', 'TINYINT(1) NOT NULL DEFAULT 0 COMMENT "Whether the scheduled backups will be isolated or not."'); + +UPDATE `cloud`.`configuration` SET `value`=CONCAT(`value`, ', backupValidationCommandTimeout, backupValidationScreenshotWait, backupValidationBootTimeout') +WHERE `name`='user.vm.readonly.details' AND `value` IS NOT NULL; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql new file mode 100644 index 000000000000..9e6be1fc5b7f --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql @@ -0,0 +1,51 @@ +-- 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. + +-- VIEW `cloud`.`internal_backup_view`; + +DROP VIEW IF EXISTS `cloud`.`internal_backup_view`; +CREATE VIEW `cloud`.`internal_backup_view` AS +SELECT b.id, + b.uuid, + b.vm_id, + b.backed_volumes, + b.type, + b.date, + b.status, + b.compression_status, + b.backup_offering_id, + b.size, + b.protected_size, + b.zone_id, + MAX(CASE WHEN bd.name = 'image_store_id' THEN bd.value END) image_store_id, + MAX(CASE WHEN bd.name = 'parent_id' THEN bd.value END) parent_id, + MAX(CASE WHEN bd.name = 'end_of_chain' THEN bd.value END) end_of_chain, + MAX(CASE WHEN bd.name = 'current' THEN bd.value END) current, + COALESCE(MAX(CASE WHEN bd.name = 'isolated' THEN bd.value END), 'false') isolated, + nbpr.volume_id, + nbpr.backup_delta_path storage_pool_delta_path, + nbpr.backup_parent_path storage_pool_parent_path, + nbsr.path image_store_path, + bs.id schedule_id +FROM backups b +LEFT JOIN backup_details bd ON b.id = bd.backup_id +LEFT JOIN backup_offering bo ON b.backup_offering_id = bo.id +LEFT JOIN internal_backup_store_ref nbsr ON b.id = nbsr.backup_id +LEFT JOIN internal_backup_pool_ref nbpr ON nbpr.volume_id = nbsr.volume_id and nbpr.backup_id = b.id +LEFT JOIN backup_schedule bs ON bs.id = b.backup_schedule_id +WHERE bo.provider='kboss' +GROUP BY b.id, nbsr.volume_id; diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java index 2be0b981455d..7674f1ce25a1 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java @@ -37,6 +37,7 @@ import com.cloud.agent.api.PrepareForMigrationAnswer; import com.cloud.resource.ResourceManager; import com.cloud.storage.clvm.ClvmPoolManager; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; @@ -212,6 +213,9 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { @Inject private ClvmPoolManager clvmPoolManager; + @Inject + private InternalBackupService internalBackupService; + @Override public StrategyPriority canHandle(DataObject srcData, DataObject destData) { if (srcData instanceof SnapshotInfo) { @@ -2527,6 +2531,7 @@ private void handlePostMigration(boolean success, Map sr _snapshotDao.updateVolumeIds(srcVolumeInfo.getId(), destVolumeInfo.getId()); _snapshotDataStoreDao.updateVolumeIds(srcVolumeInfo.getId(), destVolumeInfo.getId()); } + internalBackupService.updateVolumeId(srcVolumeInfo.getId(), destVolumeInfo.getId()); } else { try { diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java index 95345bdf9e0e..a94c04ff4aae 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java @@ -30,6 +30,7 @@ import com.cloud.storage.Volume; import com.cloud.storage.snapshot.SnapshotManager; import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionService; @@ -116,6 +117,8 @@ public class SnapshotServiceImpl implements SnapshotService { ConfigurationDao _configDao; @Inject HostDao hostDao; + @Inject + private InternalBackupService internalBackupService; @Inject private HeuristicRuleHelper heuristicRuleHelper; @@ -603,6 +606,7 @@ protected Void revertSnapshotCallback(AsyncCallbackDispatcher volumeInfoToSnapshotObjectMap = new HashMap<>(); @@ -131,18 +153,20 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { List volumeSnapshotVos = new ArrayList<>(); if (isCurrent && numberOfChildren == 0) { - volumeSnapshotVos = mergeCurrentDeltaOnSnapshot(vmSnapshotBeingDeleted, userVm, hostId, volumeTOs); + volumeSnapshotVos = mergeSucceedingDeltaOnSnapshot(vmSnapshotBeingDeleted, userVm, hostId, volumeTOs); } else if (numberOfChildren == 0) { logger.debug("Deleting VM snapshot [{}] as no snapshots/volumes depend on it.", vmSnapshot.getUuid()); volumeSnapshotVos = deleteSnapshot(vmSnapshotBeingDeleted, hostId); mergeOldSiblingWithOldParentIfOldParentIsDead(vmSnapshotDao.findByIdIncludingRemoved(vmSnapshotBeingDeleted.getParent()), userVm, hostId, volumeTOs); } else if (!isCurrent && numberOfChildren == 1) { VMSnapshotVO childSnapshot = snapshotChildren.get(0); - volumeSnapshotVos = mergeSnapshots(vmSnapshotBeingDeleted, childSnapshot, userVm, volumeTOs, hostId); + volumeSnapshotVos = mergeSnapshots(vmSnapshotBeingDeleted, childSnapshot, userVm, hostId); } + Date removedDate = DateUtil.now(); for (SnapshotVO snapshotVO : volumeSnapshotVos) { snapshotVO.setState(Snapshot.State.Destroyed); + snapshotVO.setRemoved(removedDate); snapshotDao.update(snapshotVO.getId(), snapshotVO); } @@ -180,7 +204,9 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) { transitStateWithoutThrow(vmSnapshotBeingReverted, VMSnapshot.Event.RevertRequested); - List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotBeingReverted); + internalBackupService.prepareVmForSnapshotRevert(vmSnapshot); + + List volumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotBeingReverted.getId()); List volumeSnapshotTos = volumeSnapshots.stream() .map(snapshot -> (SnapshotObjectTO) snapshotDataFactory.getSnapshot(snapshot.getSnapshotId(), snapshot.getDataStoreId(), DataStoreRole.Primary).getTO()) .collect(Collectors.toList()); @@ -252,7 +278,7 @@ private void mergeOldSiblingWithOldParentIfOldParentIsDead(VMSnapshotVO oldParen List snapshotVos; if (oldParent.getCurrent()) { - snapshotVos = mergeCurrentDeltaOnSnapshot(oldParent, userVm, hostId, volumeTOs); + snapshotVos = mergeSucceedingDeltaOnSnapshot(oldParent, userVm, hostId, volumeTOs); } else { List oldSiblings = vmSnapshotDao.listByParentAndStateIn(oldParent.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden); @@ -269,7 +295,7 @@ private void mergeOldSiblingWithOldParentIfOldParentIsDead(VMSnapshotVO oldParen VMSnapshotVO oldSibling = oldSiblings.get(0); logger.debug("Merging VM snapshot [{}] with [{}] as the former was hidden and only the latter depends on it.", oldParent.getUuid(), oldSibling.getUuid()); - snapshotVos = mergeSnapshots(oldParent, oldSibling, userVm, volumeTOs, hostId); + snapshotVos = mergeSnapshots(oldParent, oldSibling, userVm, hostId); } for (SnapshotVO snapshotVO : snapshotVos) { @@ -344,8 +370,8 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe } BackupOfferingVO backupOffering = backupOfferingDao.findById(vm.getBackupOfferingId()); - if (backupOffering != null) { - logger.debug("{} as the VM has a backup offering. This strategy does not support snapshots on VMs with current backup providers.", cantHandleLog); + if (backupOffering != null && !backupOffering.getProvider().equals(BackupManagerImpl.KBOSS_BACKUP_PROVIDER)) { + logger.debug("{} as the VM has a backup offering for a provider that is not supported. This strategy only supports the KBOSS backup provider.", cantHandleLog); return StrategyPriority.CANT_HANDLE; } @@ -353,7 +379,7 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe } private List deleteSnapshot(VMSnapshotVO vmSnapshotVO, Long hostId) { - List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVO); + List volumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot((vmSnapshotVO.getId())); List volumeSnapshotTOList = volumeSnapshots.stream() .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) .collect(Collectors.toList()); @@ -374,7 +400,7 @@ private List deleteSnapshot(VMSnapshotVO vmSnapshotVO, Long hostId) return snapshotVOList; } - private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO childSnapshot, UserVmVO userVm, List volumeObjectTOS, Long hostId) { + private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO childSnapshot, UserVmVO userVm, Long hostId) { logger.debug("Merging VM snapshot [{}] with its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid()); List snapshotGrandChildren = vmSnapshotDao.listByParentAndStateIn(childSnapshot.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden); @@ -384,18 +410,10 @@ private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO removeCurrentBackingChainSnapshotFromVmSnapshotList(snapshotGrandChildren, userVm); } - List snapshotMergeTreeToList = generateSnapshotMergeTrees(vmSnapshotVO, childSnapshot, snapshotGrandChildren); - - if (childSnapshot.getCurrent() && !VirtualMachine.State.Running.equals(userVm.getState())) { - for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) { - snapshotMergeTreeToList.stream().filter(snapshotTree -> Objects.equals(((SnapshotObjectTO) snapshotTree.getParent()).getVolume().getId(), volumeObjectTO.getId())) - .findFirst() - .orElseThrow(() -> new CloudRuntimeException(String.format("Failed to find volume snapshot for volume [%s].", volumeObjectTO.getUuid()))) - .addGrandChild(volumeObjectTO); - } - } + List deltaMergeTreeTOs = generateDeltaMergeTrees(vmSnapshotVO, childSnapshot, snapshotGrandChildren, + !userVm.getState().equals(VirtualMachine.State.Running)); - MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(snapshotMergeTreeToList, userVm.getState(), userVm.getName()); + MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(deltaMergeTreeTOs, userVm.getState().equals(VirtualMachine.State.Running), userVm.getName()); Answer answer = agentMgr.easySend(hostId, mergeDiskOnlyVMSnapshotCommand); if (answer == null || !answer.getResult()) { throw new CloudRuntimeException(String.format("Failed to merge VM snapshot [%s] due to %s.", vmSnapshotVO.getUuid(), answer != null ? answer.getDetails() : "Communication failure")); @@ -403,15 +421,23 @@ private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO logger.debug("Updating metadata of VM snapshot [{}] and its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid()); List snapshotVOList = new ArrayList<>(); - for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeToList) { - SnapshotObjectTO childTO = (SnapshotObjectTO) snapshotMergeTreeTO.getChild(); - SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotMergeTreeTO.getParent(); - - SnapshotDataStoreVO childSnapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotIdInAnyState(childTO.getId(), DataStoreRole.Primary); - childSnapshotDataStoreVO.setInstallPath(parentTO.getPath()); - snapshotDataStoreDao.update(childSnapshotDataStoreVO.getId(), childSnapshotDataStoreVO); + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOs) { + DataTO childTO = deltaMergeTreeTO.getChild(); + SnapshotObjectTO parentTO = (SnapshotObjectTO) deltaMergeTreeTO.getParent(); + + if (childTO instanceof BackupDeltaTO) { + InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeIdAndBackupId(parentTO.getVolume().getVolumeId(), childTO.getId()); + backupDelta.setBackupDeltaParentPath(parentTO.getPath()); + logger.debug("The child was also a KBOSS backup delta, will update the backup delta metadata. Updating backupDeltaParentPath of backupDelta [{}] to [{}].", backupDelta.getId(), parentTO.getPath()); + internalBackupStoragePoolDao.update(backupDelta.getId(), backupDelta); + } else { + SnapshotDataStoreVO childSnapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotIdInAnyState(childTO.getId(), DataStoreRole.Primary); + childSnapshotDataStoreVO.setInstallPath(parentTO.getPath()); + logger.debug("Updating the child path [{}] to [{}].", childSnapshotDataStoreVO.getId(), parentTO.getPath()); + snapshotDataStoreDao.update(childSnapshotDataStoreVO.getId(), childSnapshotDataStoreVO); + } - snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(parentTO.getId(), childSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary); + snapshotDataStoreDao.expungeBySnapshotIdAndStoreRole(parentTO.getId(), DataStoreRole.Primary); snapshotVOList.add(snapshotDao.findById(parentTO.getId())); } @@ -421,20 +447,35 @@ private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO return snapshotVOList; } - private List mergeCurrentDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, UserVmVO userVmVO, Long hostId, List volumeObjectTOS) { - logger.debug("Merging VM snapshot [{}] with the current volume delta.", vmSnapshotVo.getUuid()); - List snapshotMergeTreeTOList = new ArrayList<>(); - List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVo); + private List mergeSucceedingDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, UserVmVO userVmVO, Long hostId, List volumeObjectTOS) { + logger.debug(String.format("Merging VM snapshot [%s] with the succeeding delta.", vmSnapshotVo.getUuid())); + List deltaMergeTreeTOs = new ArrayList<>(); + List volumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotVo.getId()); + Map volumeIdAndSucceedingBackupMap = getVolumeIdAndSucceedingBackupMap(vmSnapshotVo); for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) { - SnapshotDataStoreVO volumeParentSnapshot = volumeSnapshots.stream().filter(snapshot -> Objects.equals(snapshot.getVolumeId(), volumeObjectTO.getId())) + Long volumeId = volumeObjectTO.getId(); + SnapshotDataStoreVO volumeParentSnapshot = volumeSnapshots.stream().filter(snapshot -> Objects.equals(snapshot.getVolumeId(), volumeId)) .findFirst() .orElseThrow(() -> new CloudRuntimeException(String.format("Failed to find volume snapshot for volume [%s].", volumeObjectTO.getUuid()))); DataTO parentSnapshot = snapshotDataFactory.getSnapshot(volumeParentSnapshot.getSnapshotId(), volumeParentSnapshot.getDataStoreId(), DataStoreRole.Primary).getTO(); - snapshotMergeTreeTOList.add(new SnapshotMergeTreeTO(parentSnapshot, volumeObjectTO, new ArrayList<>())); + + if (volumeIdAndSucceedingBackupMap.containsKey(volumeId)) { + InternalBackupJoinVO succeedingBackup = volumeIdAndSucceedingBackupMap.get(volumeId); + logger.debug("The succeeding delta is also a KNIB backup delta. Will merge the snapshot delta of volume [{}] with the parent backup delta at [{}].", + volumeObjectTO.getUuid(), succeedingBackup.getStoragePoolParentPath()); + BackupDeltaTO childTo = new BackupDeltaTO(succeedingBackup.getId(), volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, succeedingBackup.getStoragePoolParentPath()); + ArrayList grandChildren = new ArrayList<>(); + if (userVmVO.getState().equals(VirtualMachine.State.Stopped)) { + grandChildren.add(new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, succeedingBackup.getStoragePoolDeltaPath())); + } + deltaMergeTreeTOs.add(new DeltaMergeTreeTO(volumeObjectTO, parentSnapshot, childTo, grandChildren)); + } else { + deltaMergeTreeTOs.add(new DeltaMergeTreeTO(volumeObjectTO, parentSnapshot, volumeObjectTO, new ArrayList<>())); + } } - MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(snapshotMergeTreeTOList, userVmVO.getState(), userVmVO.getName()); + MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(deltaMergeTreeTOs, userVmVO.getState().equals(VirtualMachine.State.Running), userVmVO.getName()); Answer answer = agentMgr.easySend(hostId, mergeDiskOnlyVMSnapshotCommand); if (answer == null || !answer.getResult()) { @@ -443,13 +484,20 @@ private List mergeCurrentDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, logger.debug("Updating metadata of VM snapshot [{}].", vmSnapshotVo.getUuid()); List snapshotVOList = new ArrayList<>(); - for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeTOList) { - VolumeObjectTO volumeObjectTO = (VolumeObjectTO) snapshotMergeTreeTO.getChild(); - SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotMergeTreeTO.getParent(); - - VolumeVO volumeVO = volumeDao.findById(volumeObjectTO.getId()); - volumeVO.setPath(parentTO.getPath()); - volumeDao.update(volumeVO.getId(), volumeVO); + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOs) { + DataTO dataTO = deltaMergeTreeTO.getChild(); + SnapshotObjectTO parentTO = (SnapshotObjectTO) deltaMergeTreeTO.getParent(); + VolumeVO volumeVO = volumeDao.findById(parentTO.getVolume().getId()); + + if (dataTO instanceof BackupDeltaTO) { + logger.debug("The child of deltaMergeTree [{}] is a backupDeltaTO, thus, we will update the backup delta metadata.", deltaMergeTreeTO); + InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeIdAndBackupId(parentTO.getVolume().getVolumeId(), dataTO.getId()); + backupDelta.setBackupDeltaParentPath(parentTO.getPath()); + internalBackupStoragePoolDao.update(backupDelta.getId(), backupDelta); + } else { + volumeVO.setPath(parentTO.getPath()); + volumeDao.update(volumeVO.getId(), volumeVO); + } snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(parentTO.getId(), volumeVO.getPoolId(), DataStoreRole.Primary); snapshotVOList.add(snapshotDao.findById(parentTO.getId())); @@ -495,11 +543,12 @@ protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map> volumeTosAndNewPaths = volumeTOs.stream().map(volume -> new Pair<>(volume, UUID.randomUUID().toString())).collect(Collectors.toList()); + long virtualSize = createVolumeSnapshotMetadataAndCalculateVirtualSize(vmSnapshot, volumeInfoToSnapshotObjectMap, volumeTosAndNewPaths); VMSnapshotTO target = new VMSnapshotTO(vmSnapshot.getId(), vmSnapshot.getName(), vmSnapshot.getType(), null, vmSnapshot.getDescription(), false, parentSnapshotTo, quiesceVm); - CreateDiskOnlyVmSnapshotCommand ccmd = new CreateDiskOnlyVmSnapshotCommand(userVm.getInstanceName(), target, volumeTOs, null, userVm.getState()); + CreateDiskOnlyVmSnapshotCommand ccmd = new CreateDiskOnlyVmSnapshotCommand(userVm.getInstanceName(), target, volumeTosAndNewPaths, null, userVm.getState()); logger.info("Sending disk-only VM snapshot creation of VM Snapshot [{}] command for host [{}].", vmSnapshot.getUuid(), hostId); Answer answer = agentMgr.easySend(hostId, ccmd); @@ -519,12 +568,12 @@ protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map volumeInfoToSnapshotObjectMap, CreateDiskOnlyVmSnapshotAnswer answer, UserVm userVm, VMSnapshotVO vmSnapshotVO, long virtualSize, VMSnapshotVO parentSnapshotVo) throws NoTransitionException { logger.debug("Processing CreateDiskOnlyVMSnapshotCommand answer for disk-only VM snapshot [{}].", vmSnapshot.getUuid()); - Map> volumeUuidToSnapshotSizeAndNewVolumePathMap = answer.getMapVolumeToSnapshotSizeAndNewVolumePath(); + Map volumeUuidToSnapshotSize = answer.getMapVolumeToSnapshotSize(); long vmSnapshotSize = 0; for (VolumeInfo volumeInfo : volumeInfoToSnapshotObjectMap.keySet()) { VolumeVO volumeVO = (VolumeVO) volumeInfo.getVolume(); - Pair snapSizeAndNewVolumePath = volumeUuidToSnapshotSizeAndNewVolumePathMap.get(volumeVO.getUuid()); + Long snapSize = volumeUuidToSnapshotSize.get(volumeVO.getUuid()); SnapshotObject snapshot = volumeInfoToSnapshotObjectMap.get(volumeInfo); snapshot.markBackedUp(); @@ -532,14 +581,15 @@ private VMSnapshot processCreateVmSnapshotAnswer(VMSnapshot vmSnapshot, Map volumeInfoToSnapshotObjectMap, List volumeTOs) throws NoTransitionException { + private long createVolumeSnapshotMetadataAndCalculateVirtualSize(VMSnapshot vmSnapshot, Map volumeInfoToSnapshotObjectMap, + List> volumeToAndNewPaths) throws NoTransitionException { long virtualSize = 0; - for (VolumeObjectTO volumeObjectTO : volumeTOs) { + for (Pair volumeToAndPath : volumeToAndNewPaths) { + VolumeObjectTO volumeObjectTO = volumeToAndPath.first(); VolumeInfo volumeInfo = volumeDataFactory.getVolume(volumeObjectTO.getId()); volumeInfo.stateTransit(Volume.Event.SnapshotRequested); virtualSize += volumeInfo.getSize(); @@ -584,61 +636,78 @@ private long createVolumeSnapshotMetadataAndCalculateVirtualSize(VMSnapshot vmSn snapshotOnPrimary.processEvent(Snapshot.Event.CreateRequested); snapshotOnPrimary.processEvent(ObjectInDataStoreStateMachine.Event.CreateOnlyRequested); + SnapshotDataStoreVO snapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotId(snapshot.getId()).get(0); + snapshotDataStoreVO.setInstallPath(volumeToAndPath.second()); + snapshotDataStoreDao.update(snapshotDataStoreVO.getId(), snapshotDataStoreVO); volumeInfoToSnapshotObjectMap.put(volumeInfo, snapshotOnPrimary); } return virtualSize; } - private List generateSnapshotMergeTrees(VMSnapshotVO parent, VMSnapshotVO child, List grandChildren) throws NoSuchElementException { + /** + * Generates the delta merge trees, taking internal backups into account. + * */ + private List generateDeltaMergeTrees(VMSnapshotVO parent, VMSnapshotVO child, List grandChildren, boolean stoppedVm) throws NoSuchElementException { logger.debug("Generating list of Snapshot Merge Trees for the merge process of VM Snapshot [{}].", parent.getUuid()); - List snapshotMergeTrees = new ArrayList<>(); - List parentVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(parent); - List childVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(child); + List snapshotMergeTrees = new ArrayList<>(); + List parentVolumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(parent.getId()); + List childVolumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(child.getId()); List grandChildrenVolumeSnapshots = new ArrayList<>(); + Map volumeIdAndSucceedingBackupMap = getVolumeIdAndSucceedingBackupMap(parent); for (VMSnapshotVO grandChild : grandChildren) { - grandChildrenVolumeSnapshots.addAll(getVolumeSnapshotsAssociatedWithVmSnapshot(grandChild)); + grandChildrenVolumeSnapshots.addAll(vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(grandChild.getId())); } for (SnapshotDataStoreVO parentSnapshotDataStoreVO : parentVolumeSnapshots) { - DataTO parentTO = snapshotDataFactory.getSnapshot(parentSnapshotDataStoreVO.getSnapshotId(), parentSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO(); + SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotDataFactory.getSnapshot(parentSnapshotDataStoreVO.getSnapshotId(), parentSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO(); + VolumeObjectTO volumeObjectTO = parentTO.getVolume(); + InternalBackupJoinVO succeedingBackup = volumeIdAndSucceedingBackupMap.get(volumeObjectTO.getId()); - DataTO childTO = childVolumeSnapshots.stream() + SnapshotDataStoreVO childVO = childVolumeSnapshots.stream() .filter(childSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), childSnapshot.getVolumeId())) - .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) .findFirst().orElseThrow(() -> new CloudRuntimeException(String.format("Could not find child snapshot of parent [%s].", parentSnapshotDataStoreVO.getSnapshotId()))); - List grandChildrenTOList = grandChildrenVolumeSnapshots.stream() - .filter(grandChildSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), grandChildSnapshot.getVolumeId())) - .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) - .collect(Collectors.toList()); + List grandChildrenTOList = new ArrayList<>(); + DataTO childTO = getChildAndGrandChildren(child, stoppedVm, parentSnapshotDataStoreVO, succeedingBackup, childVO, volumeObjectTO, grandChildrenTOList, + grandChildrenVolumeSnapshots); - snapshotMergeTrees.add(new SnapshotMergeTreeTO(parentTO, childTO, grandChildrenTOList)); + snapshotMergeTrees.add(new DeltaMergeTreeTO(volumeObjectTO, parentTO, childTO, grandChildrenTOList)); } - logger.debug("Generated the following list of Snapshot Merge Trees for the VM snapshot [{}]: [{}].", parent.getUuid(), snapshotMergeTrees); + logger.debug(String.format("Generated the following list of Snapshot Merge Trees for the VM snapshot [%s]: [%s].", parent.getUuid(), snapshotMergeTrees)); return snapshotMergeTrees; } /** - * For a given {@code VMSnapshotVO}, populates the {@code associatedVolumeSnapshots} list with all the volume snapshots that are - * part of the VMSnapshot. - * @param vmSnapshot the VMSnapshotVO that will have its size calculated - * @return the list that will be populated with the volume snapshots associated with the VM snapshot. + * Gets the correct children and grandchildren, taking KBOSS backups into account. * */ - private List getVolumeSnapshotsAssociatedWithVmSnapshot(VMSnapshotVO vmSnapshot) { - List associatedVolumeSnapshots = new ArrayList<>(); - List snapshotDetailList = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT); - for (VMSnapshotDetailsVO vmSnapshotDetailsVO : snapshotDetailList) { - SnapshotDataStoreVO snapshot = snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary); - if (snapshot == null) { - throw new CloudRuntimeException(String.format("Could not find snapshot for VM snapshot [%s].", vmSnapshot.getUuid())); + private DataTO getChildAndGrandChildren(VMSnapshotVO childSnapshot, boolean stoppedVm, SnapshotDataStoreVO parentSnapshotDataStoreVO, InternalBackupJoinVO childBackup, + SnapshotDataStoreVO childVO, VolumeObjectTO volumeObjectTO, List grandChildrenTOList, List grandChildrenVolumeSnapshots) { + + DataTO childTO; + if (childBackup != null && childBackup.getDate().before(childSnapshot.getCreated())) { + logger.debug("The child snapshot delta is also a backup delta. We will set the backup delta parent path [{}] as the child and the backup delta path [{}] " + + "as the grand-child.", parentSnapshotDataStoreVO.getInstallPath(), childBackup.getStoragePoolDeltaPath()); + childTO = new BackupDeltaTO(childBackup.getId(), volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, childBackup.getStoragePoolParentPath()); + if (stoppedVm) { + grandChildrenTOList.add(new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, childBackup.getStoragePoolDeltaPath())); } - associatedVolumeSnapshots.add(snapshot); + } else { + childTO = snapshotDataFactory.getSnapshot(childVO.getSnapshotId(), childVO.getDataStoreId(), DataStoreRole.Primary).getTO(); + grandChildrenTOList.addAll(grandChildrenVolumeSnapshots.stream() + .filter(grandChildSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), grandChildSnapshot.getVolumeId())) + .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) + .collect(Collectors.toList())); } - return associatedVolumeSnapshots; + + if (childSnapshot.getCurrent() && stoppedVm && grandChildrenTOList.isEmpty()) { + grandChildrenTOList.add(volumeObjectTO); + } + + return childTO; } /** @@ -693,4 +762,26 @@ private void transitStateWithoutThrow(VMSnapshot vmSnapshot, VMSnapshot.Event ev throw new CloudRuntimeException(msg, e); } } + + + private Map getVolumeIdAndSucceedingBackupMap(VMSnapshotVO vmSnapshotVO) { + Map volumeIdAndSucceedingBackupMap = new HashMap<>(); + if (vmSnapshotVO == null) { + return volumeIdAndSucceedingBackupMap; + } + + List currents = internalBackupJoinDao.listCurrents(vmSnapshotVO.getVmId(), false) + .stream().filter(internalBackupJoinVO -> internalBackupJoinVO.getDate().after(vmSnapshotVO.getCreated())).collect(Collectors.toList()); + if (currents.isEmpty()) { + logger.debug("No backups created after the VM snapshot [{}] were found, returning.", vmSnapshotVO.getUuid()); + return volumeIdAndSucceedingBackupMap; + } + + InternalBackupJoinVO succeedingBackup = currents.get(0); + volumeIdAndSucceedingBackupMap = currents.stream().filter(b -> succeedingBackup.getId() == b.getId()) + .collect(Collectors.toMap(InternalBackupJoinVO::getVolumeId, internalBackupJoinVO -> internalBackupJoinVO)); + logger.debug("Found the following backups that succeeds the VM snapshot [{}]: [{}].", vmSnapshotVO.getUuid(), volumeIdAndSucceedingBackupMap.values()); + + return volumeIdAndSucceedingBackupMap; + } } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java index 4ae6e26fbd96..fd306414ad49 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java @@ -37,6 +37,8 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.commons.collections.CollectionUtils; @@ -95,6 +97,9 @@ public class StorageVMSnapshotStrategy extends DefaultVMSnapshotStrategy { @Inject VMSnapshotDetailsDao vmSnapshotDetailsDao; + @Inject + private SnapshotDataStoreDao snapshotDataStoreDao; + @Override public boolean configure(String name, Map params) throws ConfigurationException { return super.configure(name, params); @@ -374,6 +379,17 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe return StrategyPriority.CANT_HANDLE; } + for (VolumeVO volume : volumeDao.findByInstance(vmId)) { + List snapshots = snapshotDataStoreDao.listReadyByVolumeIdAndCheckpointPathNotNull(volume.getId()); + if (CollectionUtils.isNotEmpty(snapshots)) { + logger.debug( + "{} as VM has a volume with incremental snapshots {}. Incremental volume snapshots and StorageVmSnapshotStrategy are not compatible," + + " as restoring VM snapshots will erase the bitmaps and destroy snapshot chains.", + cantHandleLog, snapshots); + return StrategyPriority.CANT_HANDLE; + } + } + if (SnapshotManager.VmStorageSnapshotKvm.value() && !snapshotMemory) { return StrategyPriority.HYPERVISOR; } diff --git a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java index 7d5d3c786e87..7584ae1c986c 100644 --- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java +++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java @@ -44,6 +44,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.test.utils.SpringUtils; @@ -442,5 +443,10 @@ public BackupOfferingDao backupOfferingDao() { public BackupManager backupManager() { return Mockito.mock(BackupManager.class); } + + @Bean + public SnapshotDataStoreDao snapshotDataStoreDao() { + return Mockito.mock(SnapshotDataStoreDao.class); + } } } diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/backup/BackupObject.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/backup/BackupObject.java new file mode 100644 index 000000000000..7475f5619528 --- /dev/null +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/backup/BackupObject.java @@ -0,0 +1,198 @@ +// +// 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.storage.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.DataStoreRole; +import com.cloud.utils.component.ComponentContext; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.InternalBackupJoinVO; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.commons.collections.CollectionUtils; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +public class BackupObject implements DataObject { + + private long id; + private String uuid; + private Long zoneId; + private Long size; + private long physicalSize; + private DataStore dataStore; + private String imageStorePath; + private Backup.Status status; + private Backup.CompressionStatus compressionStatus; + + @Inject + InternalBackupJoinDao internalBackupJoinDao; + @Inject + DataStoreManager storeManager; + + public BackupObject() { + + } + + public static BackupObject getBackupObject(InternalBackupJoinVO internalBackupJoinVO) { + BackupObject backupObject = ComponentContext.inject(BackupObject.class); + backupObject.configure(internalBackupJoinVO); + return backupObject; + } + + private void configure(InternalBackupJoinVO internalBackupJoin) { + this.id = internalBackupJoin.getId(); + this.uuid = internalBackupJoin.getUuid(); + this.zoneId = internalBackupJoin.getZoneId(); + this.size = internalBackupJoin.getProtectedSize(); + this.physicalSize = internalBackupJoin.getSize(); + this.imageStorePath = internalBackupJoin.getImageStorePath(); + this.status = internalBackupJoin.getStatus(); + this.compressionStatus = internalBackupJoin.getCompressionStatus(); + this.dataStore = storeManager.getDataStore(internalBackupJoin.getImageStoreId(), DataStoreRole.Image); + } + + public List> getChildren() { + List> children = new ArrayList<>(); + + List backups = internalBackupJoinDao.listByParentId(id); + while (CollectionUtils.isNotEmpty(backups)) { + children.add(backups.stream().map(BackupObject::getBackupObject).collect(Collectors.toList())); + backups = internalBackupJoinDao.listByParentId(backups.get(0).getId()); + } + + return children; + } + + public List> getParents(long parentId) { + LinkedList> parents = new LinkedList<>(); + + List backups = internalBackupJoinDao.listById(parentId); + while (CollectionUtils.isNotEmpty(backups)) { + parents.addFirst(backups.stream().map(BackupObject::getBackupObject).collect(Collectors.toList())); + backups = internalBackupJoinDao.listById(backups.get(0).getParentId()); + } + + return parents; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUri() { + return ""; + } + + @Override + public DataTO getTO() { + DataTO to = dataStore.getDriver().getTO(this); + if (to == null) { + return new BackupDeltaTO(id, dataStore.getTO(), Hypervisor.HypervisorType.KVM, imageStorePath); + } + return to; + } + + @Override + public DataStore getDataStore() { + return dataStore; + } + + @Override + public Long getSize() { + return size; + } + + @Override + public long getPhysicalSize() { + return physicalSize; + } + + @Override + public DataObjectType getType() { + return DataObjectType.BACKUP; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public void processEvent(ObjectInDataStoreStateMachine.Event event) { + } + + @Override + public void processEvent(ObjectInDataStoreStateMachine.Event event, Answer answer) { + } + + @Override + public void incRefCount() { + } + + @Override + public void decRefCount() { + } + + @Override + public Long getRefCount() { + return 0L; + } + + @Override + public String getName() { + return ""; + } + + public Long getZoneId() { + return zoneId; + } + + @Override + public String toString() { + return uuid; + } + + public Backup.CompressionStatus getCompressionStatus() { + return compressionStatus; + } + + public Backup.Status getStatus() { + return status; + } +} diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/helper/VMSnapshotHelperImpl.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/helper/VMSnapshotHelperImpl.java index 55551772a08a..d5b96be71ee8 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/helper/VMSnapshotHelperImpl.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/helper/VMSnapshotHelperImpl.java @@ -26,9 +26,16 @@ import javax.inject.Inject; import com.cloud.uservm.UserVm; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.VolumeApiServiceImpl; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.storage.vmsnapshot.VMSnapshotHelper; @@ -64,6 +71,12 @@ public class VMSnapshotHelperImpl implements VMSnapshotHelper { @Inject VolumeDataFactory volumeDataFactory; + @Inject + private VMSnapshotDetailsDao vmSnapshotDetailsDao; + + @Inject + private SnapshotDataStoreDao snapshotDataStoreDao; + StateMachine2 _vmSnapshottateMachine; public VMSnapshotHelperImpl() { @@ -115,10 +128,14 @@ public List getVolumeTOList(Long vmId) { List volumeTOs = new ArrayList(); List volumeVos = volumeDao.findByInstance(vmId); VolumeInfo volumeInfo = null; - for (VolumeVO volume : volumeVos) { - volumeInfo = volumeDataFactory.getVolume(volume.getId()); + try { + for (VolumeVO volume : volumeVos) { + volumeInfo = volumeDataFactory.getVolume(volume.getId()); - volumeTOs.add((VolumeObjectTO)volumeInfo.getTO()); + volumeTOs.add((VolumeObjectTO)volumeInfo.getTO()); + } + } catch (NullPointerException npe) { + throw new CloudRuntimeException(String.format("Unable to get list of volumeTOs for VM [%s]. Have the volumes already been created on the storage?", vmId), npe); } return volumeTOs; } @@ -150,6 +167,26 @@ public VMSnapshotTO getSnapshotWithParents(VMSnapshotVO snapshot) { return result; } + /** + * For a given {@code vmSnapshotId}, gets the list with all the volume snapshots that are part of the VMSnapshot. + * + * @param vmSnapshotId the id of the VM snapshot; + * @return the list that will be populated with the volume snapshots associated with the VM snapshot. + */ + @Override + public List getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(long vmSnapshotId) { + List associatedVolumeSnapshots = new ArrayList<>(); + List snapshotDetailList = vmSnapshotDetailsDao.findDetails(vmSnapshotId, VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT); + for (VMSnapshotDetailsVO vmSnapshotDetailsVO : snapshotDetailList) { + SnapshotDataStoreVO snapshot = snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary); + if (snapshot == null) { + throw new CloudRuntimeException(String.format("Could not find snapshot for VM snapshot [%s].", vmSnapshotId)); + } + associatedVolumeSnapshots.add(snapshot); + } + return associatedVolumeSnapshots; + } + @Override public StoragePoolVO getStoragePoolForVM(UserVm vm) { List rootVolumes = volumeDao.findReadyRootVolumesByInstance(vm.getId()); diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotHelper.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotHelper.java index 6d6cb7b70a93..868d634a30a6 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotHelper.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotHelper.java @@ -22,6 +22,7 @@ import com.cloud.uservm.UserVm; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import com.cloud.agent.api.VMSnapshotTO; @@ -39,6 +40,8 @@ public interface VMSnapshotHelper { VMSnapshotTO getSnapshotWithParents(VMSnapshotVO snapshot); + List getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(long vmSnapshotId); + StoragePoolVO getStoragePoolForVM(UserVm vm); Storage.StoragePoolType getStoragePoolType(Long poolId); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index f8d9cab56f73..d16d5c373009 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -38,6 +38,7 @@ import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.engine.cloud.entity.api.VolumeEntity; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; @@ -227,6 +228,8 @@ public class VolumeServiceImpl implements VolumeService { protected DiskOfferingDao diskOfferingDao; @Inject ClvmPoolManager clvmPoolManager; + @Inject + private InternalBackupService internalBackupService; @Inject private KMSManager kmsManager; @@ -525,6 +528,8 @@ public Void deleteVolumeCallback(AsyncCallbackDispatcher snapStoreVOs = _snapshotStoreDao.listAllByVolumeAndDataStore(vo.getId(), DataStoreRole.Primary); for (SnapshotDataStoreVO snapStoreVo : snapStoreVOs) { @@ -1696,7 +1701,7 @@ public void destroyVolume(long volumeId) { if (vol.getAttachedVM() == null || vol.getAttachedVM().getType() == VirtualMachine.Type.User) { // Decrement the resource count for volumes and primary storage belonging user VM's only - _resourceLimitMgr.decrementVolumeResourceCount(vol.getAccountId(), vol.isDisplay(), vol.getSize(), diskOfferingDao.findById(vol.getDiskOfferingId())); + _resourceLimitMgr.decrementVolumeResourceCount(vol.getAccountId(), vol.isDisplay(), vol.getSize(), diskOfferingDao.findById(vol.getDiskOfferingId()), null); } } diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/VmWorkJobVO.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/VmWorkJobVO.java index 41eaac598bf3..050fe4e5215c 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/VmWorkJobVO.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/VmWorkJobVO.java @@ -69,6 +69,14 @@ public VmWorkJobVO(String related) { setRelated(related); } + public VmWorkJobVO(String related, long userId, long accountId, String cmd, Long instanceId, VirtualMachine.Type vmType, Step step) { + super(null, userId, accountId, cmd, null, instanceId, null, null); + setRelated(related); + this.vmType = vmType; + this.step = step; + this.vmInstanceId = instanceId; + } + public Step getStep() { return step; } diff --git a/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java index b228a9f8ce05..cf02de1f6c18 100644 --- a/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java +++ b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java @@ -90,13 +90,14 @@ public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backup } @Override - public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { logger.debug("Restoring vm {} from backup {} on the Dummy Backup Provider", vm, backup); return true; } @Override - public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { final VolumeVO volume = volumeDao.findByUuid(backupVolumeInfo.getUuid()); final StoragePoolHostVO dataStore = storagePoolHostDao.findByUuid(dataStoreUuid); final DiskOffering diskOffering = diskOfferingDao.findByUuid(backupVolumeInfo.getDiskOfferingId()); @@ -153,7 +154,7 @@ public boolean willDeleteBackupsOnOfferingRemoval() { } @Override - public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM) { + public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated, Long backupScheduleId) { logger.debug("Starting backup for VM {} on Dummy provider", vm); BackupVO backup = new BackupVO(); @@ -204,7 +205,7 @@ public void syncBackupStorageStats(Long zoneId) { } @Override - public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore) { return new Pair<>(true, null); } } diff --git a/plugins/backup/kboss/pom.xml b/plugins/backup/kboss/pom.xml new file mode 100644 index 000000000000..eb8cbd03efa1 --- /dev/null +++ b/plugins/backup/kboss/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + cloud-plugin-backup-kvm-backup-on-secondary-storage + Apache CloudStack Plugin - KVM Backup On Secondary Storage + + cloudstack-plugins + org.apache.cloudstack + 4.23.0.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-plugin-hypervisor-kvm + ${project.version} + + + org.apache.cloudstack + cloud-engine-components-api + ${project.version} + compile + + + org.apache.cloudstack + cloud-engine-orchestration + ${project.version} + compile + + + diff --git a/plugins/backup/kboss/src/main/java/org/apache/cloudstack/backup/KbossBackupProvider.java b/plugins/backup/kboss/src/main/java/org/apache/cloudstack/backup/KbossBackupProvider.java new file mode 100644 index 000000000000..27f4d9b343e6 --- /dev/null +++ b/plugins/backup/kboss/src/main/java/org/apache/cloudstack/backup/KbossBackupProvider.java @@ -0,0 +1,3068 @@ +// 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.backup; + +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.BACKUP_HASH; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.CURRENT; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.END_OF_CHAIN; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.IMAGE_STORE_ID; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.ISOLATED; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.PARENT_ID; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.SCREENSHOT_PATH; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.cloudstack.alert.AlertService; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; +import org.apache.cloudstack.backup.dao.InternalBackupDataStoreDao; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.backup.dao.InternalBackupServiceJobDao; +import org.apache.cloudstack.backup.dao.InternalBackupStoragePoolDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.jobs.AsyncJob; +import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.framework.jobs.Outcome; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +import org.apache.cloudstack.framework.jobs.impl.OutcomeImpl; +import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; +import org.apache.cloudstack.jobs.JobInfo; +import org.apache.cloudstack.secstorage.heuristics.HeuristicType; +import org.apache.cloudstack.storage.command.BackupDeleteAnswer; +import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.heuristics.HeuristicRuleHelper; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.storage.vmsnapshot.VMSnapshotHelper; +import org.apache.cloudstack.storage.volume.VolumeObject; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.storage.MergeDiskOnlyVmSnapshotCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.manager.Commands; +import com.cloud.alert.AlertManager; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.resource.ResourceState; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.Storage; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeApiService; +import com.cloud.storage.VolumeApiServiceImpl; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.uservm.UserVm; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.Predicate; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.exception.BackupException; +import com.cloud.utils.exception.BackupProviderException; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmManager; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceDetailVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VirtualMachineManagerImpl; +import com.cloud.vm.VirtualMachineProfileImpl; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.VmWork; +import com.cloud.vm.VmWorkConstants; +import com.cloud.vm.VmWorkDeleteBackup; +import com.cloud.vm.VmWorkRestoreBackup; +import com.cloud.vm.VmWorkRestoreVolumeBackupAndAttach; +import com.cloud.vm.VmWorkSerializer; +import com.cloud.vm.VmWorkTakeBackup; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; +import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; + +public class KbossBackupProvider extends AdapterBase implements InternalBackupProvider, Configurable { + protected ConfigKey backupChainSize = new ConfigKey<>("Advanced", Integer.class, "backup.chain.size", "8", "Determines the max size of a backup chain." + + " Currently only used by the KBOSS provider. If cloud admins set it to 1 , all the backups will be full backups. With values lower than 1, the backup chain will be " + + "unlimited, unless it is stopped by another process. Please note that unlimited backup chains have a higher chance of getting corrupted, as new backups will be" + + " dependant on all of the older ones.", true, ConfigKey.Scope.Zone); + + protected ConfigKey backupTimeout = new ConfigKey<>("Advanced", Integer.class, "kboss.timeout", "43200", "Timeout, in seconds, to execute KBOSS commands. After the " + + "command times out, the Management Server will still wait for another kboss.timeout seconds to receive a response from the Agent.", true, ConfigKey.Scope.Zone); + + @Inject + private AsyncJobManager jobManager; + @Inject + private EntityManager entityManager; + + @Inject + private VirtualMachineManager virtualMachineManager; + + @Inject + private UserVmDao userVmDao; + + @Inject + private VMInstanceDetailsDao vmInstanceDetailsDao; + + @Inject + private VMSnapshotHelper vmSnapshotHelper; + + @Inject + private SnapshotDataStoreDao snapshotDataStoreDao; + + @Inject + private VMSnapshotDao vmSnapshotDao; + + @Inject + private VMSnapshotDetailsDao vmSnapshotDetailsDao; + + @Inject + private BackupDao backupDao; + + @Inject + private InternalBackupJoinDao internalBackupJoinDao; + + @Inject + private BackupDetailsDao backupDetailDao; + + @Inject + private InternalBackupStoragePoolDao internalBackupStoragePoolDao; + + @Inject + private InternalBackupDataStoreDao internalBackupDataStoreDao; + + @Inject + private BackupOfferingDao backupOfferingDao; + + @Inject + private BackupOfferingDetailsDao backupOfferingDetailsDao; + + @Inject + private HeuristicRuleHelper heuristicRuleHelper; + + @Inject + private DataStoreManager dataStoreManager; + + @Inject + private AgentManager agentManager; + + @Inject + private EndPointSelector endPointSelector; + + @Inject + private VolumeDao volumeDao; + + @Inject + private ImageStoreDao imageStoreDao; + + @Inject + private VolumeApiService volumeApiService; + + @Inject + private PrimaryDataStoreDao storagePoolDao; + + @Inject + private HostDao hostDao; + + @Inject + private UserVmManager userVmManager; + + @Inject + private VolumeOrchestrationService volumeOrchestrationService; + + @Inject + private VolumeDataFactory volumeDataFactory; + @Inject + private InternalBackupServiceJobDao internalBackupServiceJobDao; + + @Inject + private BackupManager backupManager; + + @Inject + private DiskOfferingDao diskOfferingDao; + + @Inject + private HypervisorGuruManager hypervisorGuruManager; + + @Inject + private NicDao nicDao; + + @Inject + private AlertManager alertManager; + + protected final List validChildStatesToRemoveBackup = List.of(Backup.Status.Expunged, Backup.Status.Error, Backup.Status.Failed); + + private final List supportedStoragePoolTypes = List.of(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem, + Storage.StoragePoolType.SharedMountPoint); + + private final List allowedBackupStatesToRemove = List.of(Backup.Status.BackedUp, Backup.Status.Failed, Backup.Status.Error); + + private final List allowedBackupStatesToCompress = List.of(Backup.Status.BackedUp, Backup.Status.Restoring); + + private final List allowedBackupStatesToValidate = List.of(Backup.Status.BackedUp, Backup.Status.Restoring); + + private final List allowedVmStates = Arrays.asList(VirtualMachine.State.Running, VirtualMachine.State.Stopped); + + @Override + public String getName() { + return "kboss"; + } + + @Override + public String getDescription() { + return "KVM Backup on Secondary Storage"; + } + + @Override + public List listBackupOfferings(Long zoneId) { + return List.of(); + } + + @Override + public boolean isValidProviderOffering(Long zoneId, String uuid) { + return true; + } + + @Override + public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backupOffering) { + logger.debug("Assigning VM [{}] to KBOSS backup offering with name:[{}], uuid: [{}].", vm.getUuid(), backupOffering.getName(), backupOffering.getUuid()); + if (!Hypervisor.HypervisorType.KVM.equals(vm.getHypervisorType())) { + logger.error("KVM Backup on Secondary Storage provider is only supported for KVM."); + return false; + } + + for (VMSnapshotVO vmSnapshotVO : vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) { + List vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId()); + if (!vmSnapshotDetails.stream().allMatch(vmSnapshotDetailsVO -> vmSnapshotDetailsVO.getName().equals(VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT))) { + logger.error("KBOSS is only supported with disk-only VM snapshots using [{}] strategy. Found a disk-only VM snapshot using another strategy for the VM.", + VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT); + logger.debug("Found VM snapshot details [{}].", () -> vmSnapshotDetails.stream().map(VMSnapshotDetailsVO::getName).collect(Collectors.toList())); + return false; + } + } + + return CollectionUtils.isEmpty(vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.DiskAndMemory)); + } + + @Override + public boolean removeVMFromBackupOffering(VirtualMachine vm) { + logger.info("Removing VM [{}] from KBOSS backup offering.", vm.getUuid()); + + validateVmState(vm, "remove backup offering", VirtualMachine.State.Expunging, VirtualMachine.State.Destroyed); + List currents = internalBackupJoinDao.listCurrents(vm.getId(), true); + + return finishAllChains(vm, currents); + } + + @Override + public boolean removeVMBackupSchedule(VirtualMachine vm, BackupSchedule backupSchedule) { + logger.info("Removing VM [{}] from KBOSS backup schedule.", vm.getUuid()); + + if (endBackupChain(vm, backupSchedule.getId())) { + return true; + } + UserVmVO vmVO = userVmDao.findById(vm.getId()); + logger.error("Failed to merge deltas for VM [{}] during backup schedule removal process. Changing its state to [{}].", vm, VirtualMachine.State.BackupError); + BackupVO backupVO = backupDao.findById(internalBackupJoinDao.findCurrent(vm.getId(), backupSchedule.getId()).getId()); + backupVO.setStatus(Backup.Status.Error); + backupDao.update(backupVO.getId(), backupVO); + vmInstanceDetailsDao.addDetail(vm.getId(), VmDetailConstants.LAST_KNOWN_STATE, vmVO.getState().name(), false); + vmVO.setState(VirtualMachine.State.BackupError); + userVmDao.update(vmVO.getId(), vmVO); + + return false; + } + + @Override + public boolean willDeleteBackupsOnOfferingRemoval() { + return false; + } + + @Override + public Pair takeBackup(VirtualMachine vm, Boolean quiesceVm, boolean isolated, Long backupScheduleId) { + logger.debug("Queueing backup on VM [{}].", vm.getUuid()); + Outcome outcome = createBackupThroughJobQueue(vm, ObjectUtils.defaultIfNull(quiesceVm, false), isolated, backupScheduleId); + + try { + outcome.get(); + } catch (InterruptedException | ExecutionException e) { + throw new CloudRuntimeException(String.format("Unable to retrieve result from job takeBackup due to [%s]. VM [%s].", e.getMessage(), vm.getUuid()), e); + } + + Object jobResult = jobManager.unmarshallResultObject(outcome.getJob()); + + if (jobResult instanceof BackupProviderException) { + throw (BackupProviderException) jobResult; + } else if (jobResult instanceof Throwable) { + throw new CloudRuntimeException(String.format("Exception while taking KBOSS backup for VM [%s]. Check the logs for more information.", vm.getUuid())); + } + + Pair result = (Pair)jobResult; + Pair returnValue = new Pair<>(result.first(), null); + if (result.first()) { + returnValue.second(backupDao.findById(result.second())); + } + return returnValue; + } + + @Override + public Pair orchestrateTakeBackup(Backup backup, boolean quiesceVm, boolean isolated) { + BackupVO backupVO = (BackupVO) backup; + long vmId = backup.getVmId(); + VirtualMachine userVm = virtualMachineManager.findById(vmId); + Long hostId = vmSnapshotHelper.pickRunningHost(vmId); + HostVO hostVO = hostDao.findById(hostId); + + if (hostVO.getStatus() != Status.Up || hostVO.getResourceState() != ResourceState.Enabled) { + backupVO.setStatus(Backup.Status.Failed); + backupDao.update(backupVO.getId(), backupVO); + + logger.error("No available host found to create backup [{}] of VM [{}]. Setting the backup as Failed.", backupVO.getUuid(), userVm.getUuid()); + return new Pair<>(Boolean.FALSE, backup.getId()); + } + + List volumeTOs; + try { + validateVmState(userVm, "take backup"); + volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId()); + validateStorages(volumeTOs, userVm.getUuid()); + } catch (Exception e) { + backupVO.setStatus(Backup.Status.Failed); + backupDao.update(backupVO.getId(), backupVO); + throw e; + } + + logger.info("Starting VM backup process for VM [{}].", userVm.getUuid()); + + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + + backupVO.setDate(new Date()); + List backupChain = getBackupJoinParents(backupVO, true); + InternalBackupJoinVO parentBackup = null; + if (isolated) { + setBackupAsIsolated(backupVO); + } else { + parentBackup = getParentAndSetEndOfChain(backupVO, backupChain, backupOfferingVO); + } + InternalBackupJoinVO newBackupJoin = internalBackupJoinDao.findById(backup.getId()); + boolean fullBackup = parentBackup == null; + List parentBackupDeltasOnPrimary = new ArrayList<>(); + List parentBackupDeltasOnSecondary = new ArrayList<>(); + List chainImageStoreUrls = null; + List kbossTOs = new ArrayList<>(); + HashMap volumeUuidToDeltaPrimaryRef = new HashMap<>(); + HashMap volumeUuidToDeltaSecondaryRef = new HashMap<>(); + + boolean runningVm = userVm.getState() == VirtualMachine.State.Running; + transitVmState(userVm, VirtualMachine.Event.BackupRequested, hostId); + updateBackupStatusToBackingUp(volumeTOs, backupVO); + + DataStore imageStore = getImageStoreForBackup(userVm.getDataCenterId(), backupVO); + createBasicBackupDetails(imageStore.getId(), fullBackup ? 0L : parentBackup.getId(), backupVO); + + List succeedingBackupList = getSucceedingBackupList(parentBackup); + InternalBackupJoinVO succeedingBackup = succeedingBackupList.isEmpty() ? null : succeedingBackupList.get(0); + + List succeedingVmSnapshotList = getSucceedingVmSnapshotList(parentBackup); + VMSnapshotVO succeedingVmSnapshot = succeedingVmSnapshotList.isEmpty() ? null : succeedingVmSnapshotList.get(0); + + if (!fullBackup) { + parentBackupDeltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(parentBackup.getId()); + parentBackupDeltasOnSecondary = internalBackupDataStoreDao.listByBackupId(parentBackup.getId()); + + chainImageStoreUrls = getChainImageStoreUrls(backupChain); + } + + Map> volumeIdToSnapshotDataStoreAndBackupPathList = mapVolumesToVmSnapshotAndBackupReferences(volumeTOs, succeedingVmSnapshotList, succeedingBackupList); + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + KbossTO kbossTO = new KbossTO(volumeObjectTO, volumeIdToSnapshotDataStoreAndBackupPathList.getOrDefault(volumeObjectTO.getId(), new LinkedList<>())); + kbossTOs.add(kbossTO); + createDeltaReferences(fullBackup, runningVm, backup, parentBackupDeltasOnSecondary, + parentBackupDeltasOnPrimary, volumeUuidToDeltaPrimaryRef, volumeUuidToDeltaSecondaryRef, succeedingVmSnapshot, kbossTO); + } + + TakeKbossBackupCommand command = new TakeKbossBackupCommand(quiesceVm, runningVm, newBackupJoin.getEndOfChain(), userVm.getInstanceName(), imageStore.getUri(), + chainImageStoreUrls, kbossTOs, isolated); + + Answer answer = sendBackupCommand(hostId, command); + + if (answer == null || !answer.getResult()) { + processBackupFailure(answer, userVm, hostId, runningVm, backupVO); + return new Pair<>(Boolean.FALSE, null); + } + + processBackupSuccess(runningVm, volumeTOs, volumeUuidToDeltaPrimaryRef, volumeUuidToDeltaSecondaryRef, (TakeKbossBackupAnswer)answer, parentBackupDeltasOnPrimary, + succeedingVmSnapshot, backupVO, fullBackup, userVm, hostId, newBackupJoin.getEndOfChain(), isolated, succeedingBackup); + + if (!isolated) { + updateCurrentBackup(newBackupJoin); + } + + if (offeringSupportsCompression(newBackupJoin)) { + compressBackupAsync(newBackupJoin, backup.getZoneId(), userVm.getAccountId()); + } else { + validateBackupAsyncIfHasOfferingSupport(newBackupJoin, backup.getZoneId(), userVm.getAccountId()); + } + return new Pair<>(Boolean.TRUE, backupVO.getId()); + } + + @Override + public boolean deleteBackup(Backup backup, boolean forced) { + logger.debug("Queueing backup [{}] deletion.", backup.getUuid()); + Outcome outcome = deleteBackupThroughJobQueue(backup, forced); + + try { + outcome.get(); + } catch (InterruptedException | ExecutionException e) { + throw new CloudRuntimeException(String.format("Unable to retrieve result from job deleteBackup due to [%s]. Backup [%s].", e.getMessage(), backup.getUuid()), e); + } + + Object jobResult = jobManager.unmarshallResultObject(outcome.getJob()); + + if (jobResult instanceof Throwable) { + if (jobResult instanceof BackupProviderException) { + throw (BackupProviderException) jobResult; + } + throw new CloudRuntimeException(String.format("Exception while deleting KBOSS backup [%s]. Check the logs for more information.", backup.getUuid())); + } + + return BooleanUtils.isTrue((Boolean) jobResult); + } + + @Override + public Boolean orchestrateDeleteBackup(Backup backup, boolean forced) { + BackupVO backupVO = (BackupVO) backup; + + VirtualMachine virtualMachine = virtualMachineManager.findById(backup.getVmId()); + + if (virtualMachine != null) { + validateVmState(virtualMachine, "delete backup", VirtualMachine.State.Destroyed); + } + + logger.info("Starting delete process for backup [{}].", backupVO); + + if (!validateBackupStateForRemoval(backupVO.getId())) { + return false; + } + + checkErrorBackup(backupVO, virtualMachine); + if (deleteFailedBackup(backupVO)) { + return true; + } + + InternalBackupJoinVO childBackup = internalBackupJoinDao.findByParentId(backup.getId()); + + if (childBackup != null && !validChildStatesToRemoveBackup.contains(childBackup.getStatus())) { + logger.debug("Backup [{}] has children that are not in one of the following states [{}]; will mark it as removed on the database but the files will not be deleted " + + "from secondary storage until the children are also expunged.", backup.getUuid(), validChildStatesToRemoveBackup); + backupVO.setStatus(Backup.Status.Removed); + backupDao.update(backupVO.getId(), backupVO); + return true; + } + + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backup.getId()); + if (backupJoinVO.getCurrent()) { + if (!mergeCurrentBackupDeltas(backupJoinVO)) { + return false; + } + InternalBackupJoinVO parent = internalBackupJoinDao.findById(backupJoinVO.getParentId()); + if (parent != null && parent.getStatus() == Backup.Status.BackedUp) { + backupDetailDao.persist(new BackupDetailVO(parent.getId(), END_OF_CHAIN, Boolean.TRUE.toString(), false)); + } + } + + Commands deleteCommands = new Commands(Command.OnError.Continue); + + DataStore dataStore = addBackupDeltasToDeleteCommand(backup.getId(), deleteCommands); + Pair, InternalBackupJoinVO> backupParentsToBeRemovedAndLastAliveBackup = getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVO, + deleteCommands); + + EndPoint endPoint = endPointSelector.select(dataStore); + if (endPoint == null) { + logger.error("Unable to find SSVM to delete backup [{}]. Check if SSVM is up for the zone.", backup); + throw new CloudRuntimeException(String.format("Unable to delete backup [%s]. Please check the logs.", backup.getUuid())); + } + Answer[] deleteAnswers; + try { + deleteAnswers = sendBackupCommands(endPoint.getId(), deleteCommands); + } catch (AgentUnavailableException | OperationTimedoutException e) { + throw new CloudRuntimeException(e); + } + + List removedBackupIds = backupParentsToBeRemovedAndLastAliveBackup.first().stream().map(InternalBackupJoinVO::getId).collect(Collectors.toList()); + removedBackupIds.add(backup.getId()); + + boolean isFailedSetEmpty = processRemoveBackupFailures(forced, deleteAnswers, removedBackupIds, backupJoinVO, virtualMachine); + + processRemovedBackups(removedBackupIds); + + if (backupParentsToBeRemovedAndLastAliveBackup.second() != null) { + backupDetailDao.persist(new BackupDetailVO(backupParentsToBeRemovedAndLastAliveBackup.second().getId(), END_OF_CHAIN, Boolean.TRUE.toString(), false)); + } + + return isFailedSetEmpty; + } + + @Override + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { + logger.debug("Queueing backup [{}] restore for VM [{}].", backup.getUuid(), vm.getUuid()); + validateQuickRestore(backup, quickRestore); + + Outcome outcome = restoreVMFromBackupThroughJobQueue(vm, backup, quickRestore, hostId); + + try { + outcome.get(); + } catch (InterruptedException | ExecutionException e) { + throw new CloudRuntimeException(String.format("Unable to retrieve result from job restoreVMFromBackup due to [%s]. Backup [%s].", e.getMessage(), backup.getUuid()), e); + } finally { + BackupVO backupVO = backupDao.findById(backup.getId()); + backupVO.setStatus(Backup.Status.BackedUp); + backupDao.update(backupVO.getId(), backupVO); + } + + Object jobResult = jobManager.unmarshallResultObject(outcome.getJob()); + + handleRestoreException(backup, vm, jobResult); + + return BooleanUtils.isTrue((Boolean) jobResult); + } + + @Override + public Boolean orchestrateRestoreVMFromBackup(Backup backup, VirtualMachine vm, boolean quickRestore, Long hostId, boolean sameVmAsBackup) { + logger.info("Starting restore backup process for VM [{}] and backup [{}].", vm.getUuid(), backup); + validateNoVmSnapshots(vm); + validateQuickRestore(backup, quickRestore); + long backupId = backup.getId(); + Pair isValidStateAndBackupVo = validateCompressionStateForRestoreAndGetBackup(backupId); + + if (!isValidStateAndBackupVo.first()) { + return false; + } + + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backupId); + List currentBackups = sameVmAsBackup ? internalBackupJoinDao.listCurrents(vm.getId(), false) : List.of(); + List deltasOnPrimary = new ArrayList<>(); + for (InternalBackupJoinVO currentBackup : currentBackups) { + deltasOnPrimary.addAll(0, internalBackupStoragePoolDao.listByBackupId(currentBackup.getId())); + } + List deltasOnSecondary = internalBackupDataStoreDao.listByBackupId(backupId); + List volumeTOs = vmSnapshotHelper.getVolumeTOList(vm.getId()); + + Set deltasToRemove = new HashSet<>(); + + List backupsWithoutVolumes = sameVmAsBackup ? getBackupsWithoutVolumes(deltasOnSecondary, volumeTOs) : List.of(); + + HostVO host; + try { + host = getHostToRestore(vm, quickRestore, hostId); + } catch (AgentUnavailableException e) { + throw new CloudRuntimeException(e); + } + + BackupVO backupVO = isValidStateAndBackupVo.second(); + List volumeInfos = backupVO.getBackedUpVolumes(); + if (sameVmAsBackup) { + createAndAttachVolumes(volumeInfos, backupsWithoutVolumes, vm, host); + // Get new volume references + volumeTOs = vmSnapshotHelper.getVolumeTOList(vm.getId()); + } + + Set> backupAndVolumePairs = generateBackupAndVolumePairsToRestore(deltasOnSecondary, volumeTOs, backupJoinVO, sameVmAsBackup); + + List deltasToBeMerged = List.of(); + if (sameVmAsBackup) { + List volumesNotPartOfTheBackup = getVolumesThatAreNotPartOfTheBackup(volumeTOs, deltasOnSecondary); + deltasToBeMerged = populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(deltasOnPrimary, deltasToRemove, volumeTOs, volumesNotPartOfTheBackup, + vm.getUuid()); + } + Set secondaryStorageUrls = getParentSecondaryStorageUrls(backupVO); + + Commands commands = new Commands(Command.OnError.Stop); + commands.addCommand(new RestoreKbossBackupCommand(deltasToRemove, backupAndVolumePairs, secondaryStorageUrls, quickRestore)); + commands.addCommand(new MergeDiskOnlyVmSnapshotCommand(deltasToBeMerged, vm.getState().equals(VirtualMachine.State.Running), vm.getInstanceName())); + + Answer[] answers; + + try { + answers = sendBackupCommands(host.getId(), commands); + } catch (OperationTimedoutException | AgentUnavailableException e) { + throw new CloudRuntimeException(e); + } + + if (answers == null) { + logger.error("Failed to restore backup [{}] due to no answer from host.", backup); + return false; + } + + if (!processRestoreAnswers(vm, answers, quickRestore)) { + return false; + } + + updateVolumePathsAndSizeIfNeeded(vm, volumeTOs, volumeInfos, deltasToBeMerged, sameVmAsBackup); + + for (InternalBackupJoinVO currentBackup : currentBackups) { + internalBackupStoragePoolDao.expungeByBackupId(currentBackup.getId()); + setEndOfChainAndRemoveCurrentForBackup(currentBackup); + } + + if (quickRestore) { + List volumesToConsolidate = getVolumesToConsolidate(vm, deltasOnSecondary, volumeTOs, host.getId(), sameVmAsBackup); + return finalizeQuickRestore(vm, volumesToConsolidate, host.getId()); + } + + return true; + } + + @Override + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { + logger.debug("Queueing backup [{}] volume [{}] restore for VM [{}].", backup.getUuid(), backupVolumeInfo, vm.getUuid()); + validateQuickRestore(backup, quickRestore); + Outcome outcome = restoreBackedUpVolumeThroughJobQueue(vm, backup, backupVolumeInfo, hostIp, quickRestore); + + try { + outcome.get(); + } catch (InterruptedException | ExecutionException e) { + throw new CloudRuntimeException(String.format("Unable to retrieve result from job restoreBackedUpVolume due to [%s]. Backup [%s].", e.getMessage(), backup.getUuid()), e); + } finally { + BackupVO backupVO = backupDao.findById(backup.getId()); + backupVO.setStatus(Backup.Status.BackedUp); + backupDao.update(backupVO.getId(), backupVO); + } + + Object jobResult = jobManager.unmarshallResultObject(outcome.getJob()); + + handleRestoreException(backup, vm, jobResult); + + if (!(jobResult instanceof Pair)) { + throw new CloudRuntimeException(String.format("Unexpected answer from restoreBackupVolume job. Got [%s].", jobResult)); + } + return (Pair) jobResult; + } + + @Override + public Pair orchestrateRestoreBackedUpVolume(Backup backup, VirtualMachine vm, Backup.VolumeInfo backupVolumeInfo, String hostIp, boolean quickRestore) { + BackupVO backupVO = (BackupVO) backup; + Pair isValidStateAndBackupVo = validateCompressionStateForRestoreAndGetBackup(backup.getId()); + + if (!isValidStateAndBackupVo.first()) { + return new Pair<>(false, null); + } + + VolumeVO backedUpVolume = volumeDao.findByUuidIncludingRemoved(backupVolumeInfo.getUuid()); + HostVO hostVo = hostDao.findByIp(hostIp); + VolumeInfo volumeInfo = duplicateAndCreateVolume(vm, hostVo, backupVolumeInfo); + + VolumeObjectTO volumeObjectTO = (VolumeObjectTO) volumeInfo.getTO(); + InternalBackupDataStoreVO deltaOnSecondary = internalBackupDataStoreDao.findByBackupIdAndVolumeId(backup.getId(), backedUpVolume.getId()); + InternalBackupJoinVO internalBackupJoinVO = internalBackupJoinDao.findById(backup.getId()); + Pair backupAndVolumePair = generateBackupAndVolumePairForSingleNewVolume(deltaOnSecondary, volumeObjectTO, internalBackupJoinVO); + Set secondaryStorageUrls = getParentSecondaryStorageUrls(backupVO); + + RestoreKbossBackupCommand cmd = new RestoreKbossBackupCommand(Set.of(), Set.of(backupAndVolumePair), secondaryStorageUrls, quickRestore); + + Answer answer = sendBackupCommand(hostVo.getId(), cmd); + + if (!processRestoreAnswers(vm, new Answer[] {answer}, quickRestore)) { + throw new CloudRuntimeException("Bad answer from agent"); + } + + VolumeVO newVolume = (VolumeVO)volumeInfo.getVolume(); + volumeDao.update(newVolume.getId(), newVolume); + + Volume attachedVolume = volumeApiService.attachVolumeToVM(vm.getId(), newVolume.getId(), null, false, false); + + if (quickRestore) { + ArrayList volumeToConsolidate = new ArrayList<>(); + volumeToConsolidate.add(volumeDataFactory.getVolume(attachedVolume.getId())); + return new Pair<>(finalizeQuickRestore(vm, volumeToConsolidate, hostVo.getId()), attachedVolume.getUuid()); + } + + return new Pair<>(true, attachedVolume.getUuid()); + } + + @Override + public boolean startBackupCompression(long backupId, long hostId) { + Pair validCompressAndBackupVO = validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + if (!validCompressAndBackupVO.first()) { + return false; + } + + InternalBackupJoinVO backup = internalBackupJoinDao.findById(backupId); + InternalBackupJoinVO parentBackup = internalBackupJoinDao.findById(backup.getParentId()); + + List backupDeltas = internalBackupDataStoreDao.listByBackupId(backupId); + List parentBackupDeltas = parentBackup != null ? internalBackupDataStoreDao.listByBackupId(backup.getParentId()) : List.of(); + + DataStoreTO imageStoreTo = dataStoreManager.getDataStore(backup.getImageStoreId(), DataStoreRole.Image).getTO(); + DataStoreTO parentStoreTo = parentBackup != null ? dataStoreManager.getDataStore(parentBackup.getImageStoreId(), DataStoreRole.Image).getTO() : null; + + List deltasToCompressAndParents = new ArrayList<>(); + for (InternalBackupDataStoreVO delta : backupDeltas) { + BackupDeltaTO backupDeltaTO = new BackupDeltaTO(imageStoreTo, Hypervisor.HypervisorType.KVM, delta.getBackupPath()); + InternalBackupDataStoreVO parentDataStore = parentBackupDeltas.stream().filter(parent -> parent.getVolumeId() == delta.getVolumeId()).findFirst().orElse(null); + BackupDeltaTO parentDeltaTO = parentDataStore != null ? new BackupDeltaTO(parentStoreTo, Hypervisor.HypervisorType.KVM, parentDataStore.getBackupPath()) : null; + deltasToCompressAndParents.add(new DeltaMergeTreeTO(null, parentDeltaTO, backupDeltaTO, null)); + } + + HostVO hostVO = hostDao.findById(hostId); + BackupVO backupVO = validCompressAndBackupVO.second(); + + long minFreeStorage = Math.round(backupVO.getSize() * backupCompressionMinimumFreeStorage.valueIn(hostVO.getDataCenterId())); + + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backupVO.getBackupOfferingId()); + BackupOfferingDetailsVO detail = backupOfferingDetailsDao.findDetail(backupOfferingVO.getId(), ApiConstants.COMPRESSION_LIBRARY); + List backupChain = getBackupJoinParents(backupVO, true); + List chainImageStoreUrls = getChainImageStoreUrls(backupChain); + CompressBackupCommand cmd = new CompressBackupCommand(deltasToCompressAndParents, chainImageStoreUrls, minFreeStorage, detail == null ? null : + Backup.CompressionLibrary.valueOf(detail.getValue()), backupCompressionCoroutines.valueIn(hostVO.getClusterId()), + backupCompressionRateLimit.valueIn(hostVO.getClusterId())); + cmd.setWait(backupCompressionTimeout.valueIn(hostVO.getClusterId())); + Answer answer = agentManager.easySend(hostId, cmd); + + if (answer == null || !answer.getResult()) { + logger.error("Failed to compress backup [{}] due to {}.", backup.getUuid(), answer == null ? "no answer" : answer.getDetails()); + backupVO.setCompressionStatus(Backup.CompressionStatus.CompressionError); + backupDao.update(backupId, backupVO); + return false; + } + + logger.info("Successfully completed the first step of the backup compression process for backup [{}]. Will launch a new compression job to finalize the compression.", + backup.getUuid()); + + internalBackupServiceJobDao.persist(new InternalBackupServiceJobVO(backupVO.getId(), backupVO.getZoneId(), backupVO.getVmId(), backupVO.getAccountId(), + InternalBackupServiceJobType.FinalizeCompression)); + + return true; + } + + @Override + public boolean finalizeBackupCompression(long backupId, long hostId) { + Pair shouldContinueProcessAndBackupVo = validateBackupStateForFinalizeCompression(backupId); + if (!shouldContinueProcessAndBackupVo.first()) { + return false; + } + BackupVO backupVO = shouldContinueProcessAndBackupVo.second(); + + List deltaTOs = getBackupDeltaTOList(backupId); + + FinalizeBackupCompressionCommand cmd = new FinalizeBackupCompressionCommand(backupVO.getStatus() != Backup.Status.BackedUp, deltaTOs); + HostVO hostVO = hostDao.findById(hostId); + cmd.setWait(backupCompressionTimeout.valueIn(hostVO.getClusterId())); + Answer answer = agentManager.easySend(hostId, cmd); + + if (answer == null || !answer.getResult()) { + logger.error("Failed to finish compression of backup [{}] due to {}.", backupVO.getUuid(), answer == null ? "no answer" : answer.getDetails()); + backupVO.setCompressionStatus(Backup.CompressionStatus.CompressionError); + backupDao.update(backupId, backupVO); + return false; + } + + if (cmd.isCleanup()) { + logger.info("Successfully cleaned up backup compression of backup [{}].", backupVO); + return true; + } + + backupVO.setCompressionStatus(Backup.CompressionStatus.Compressed); + backupVO.setUncompressedSize(backupVO.getSize()); + backupVO.setSize(Long.parseLong(answer.getDetails())); + backupDao.update(backupVO.getId(), backupVO); + + logger.info("Finalized compression for backup [{}], old size was [{}], compressed size is [{}].", backupVO.getUuid(), backupVO.getUncompressedSize(), backupVO.getSize()); + + validateBackupAsyncIfHasOfferingSupport(internalBackupJoinDao.findById(backupId), backupVO.getZoneId(), backupVO.getAccountId()); + return true; + } + + @Override + public boolean validateBackup(long backupId, long hostId) { + if (!validateBackupStateForValidation(backupId)) { + return false; + } + BackupVO backupVO = backupDao.findById(backupId); + backupVO.setValidationStatus(Backup.ValidationStatus.Validating); + backupDao.update(backupId, backupVO); + BackupDetailVO hashDetail = backupDetailDao.findDetail(backupId, BACKUP_HASH); + if (hashDetail != null) { + return validateWithHash(backupId, backupVO, hashDetail); + } else { + return validateWithValidationVm(backupId, hostId, backupVO); + } + } + + @Override + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickRestore) { + Pair shouldRestoreAndOldStatus = validateBackupStateForRestoreBackupToVM(backup.getId()); + if (!shouldRestoreAndOldStatus.first()) { + return new Pair<>(false, "Backup is not in the right state."); + } + + boolean result = false; + try { + result = orchestrateRestoreVMFromBackup(backup, vm, quickRestore, null, false); + } catch (Exception exception) { + handleRestoreException(backup, vm, exception); + } finally { + Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback) transactionStatus -> { + BackupVO backupVO = backupDao.findById(backup.getId()); + backupVO.setStatus(shouldRestoreAndOldStatus.second()); + backupDao.update(backupVO.getId(), backupVO); + return true; + }); + } + + return new Pair<>(result, null); + } + + @Override + public boolean finishBackupChains(VirtualMachine virtualMachine) { + UserVmVO vm = userVmDao.findById(virtualMachine.getId()); + List currents = internalBackupJoinDao.listCurrents(vm.getId(), true); + if (allowedVmStates.contains(vm.getState())) { + return finishAllChains(vm, currents); + } + if (vm.getState() != VirtualMachine.State.BackupError) { + logger.error("VM [{}] is not in the right state to finish backup chain. It can only be in states [Running, Stopped and BackupError].", vm.getUuid()); + return false; + } + return normalizeBackupErrorAndFinishChain(vm); + } + + @Override + public void syncBackupMetrics(Long zoneId) { + } + + @Override + public Backup createNewBackupEntryForRestorePoint(Backup.RestorePoint rp, VirtualMachine vm) { + return null; + } + + @Override + public Pair getBackupStorageStats(Long zoneId) { + return new Pair<>(0L, 0L); + } + + @Override + public void syncBackupStorageStats(Long zoneId) { + } + + @Override + public boolean supportsInstanceFromBackup() { + return true; + } + + @Override + public boolean supportsMemoryVmSnapshot() { + return false; + } + + @Override + public void prepareVolumeForDetach(Volume volume, VirtualMachine virtualMachine) { + logger.info("Preparing volume [{}] for detach.", volume.getUuid()); + mergeCurrentDeltasIntoVolume(volume, virtualMachine, "detach", virtualMachine.getState().equals(VirtualMachine.State.Running)); + } + + @Override + public void prepareVolumeForMigration(Volume volume, VirtualMachine vm) { + if (VirtualMachine.State.Migrating.equals(vm.getState())) { + logger.info("Preparing volume [{}] for live migration.", volume.getUuid()); + mergeCurrentDeltasIntoVolume(volume, vm, "live migration", true); + } + } + + @Override + public void updateVolumeId(VirtualMachine virtualMachine, long oldVolumeId, long newVolumeId) { + internalBackupDataStoreDao.updateVolumeId(oldVolumeId, newVolumeId); + } + + @Override + public void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot, VirtualMachine virtualMachine) { + List currentBackups = internalBackupJoinDao.listCurrents(virtualMachine.getId(), true); + + if (currentBackups.isEmpty()) { + logger.debug("There is no current backup delta, the VM [{}] is already prepared for VM snapshot revert.", virtualMachine.getUuid()); + return; + } + currentBackups = currentBackups.stream().filter(backup -> backup.getDate().after(vmSnapshot.getCreated())).collect(Collectors.toList()); + if (currentBackups.isEmpty()) { + logger.debug("Existing backup deltas [{}] were created before the target VM snapshot [{}]. No preparation needed for VM [{}].", + currentBackups, vmSnapshot.getCreated(), virtualMachine.getUuid()); + return; + } + + logger.debug("Preparing VM [{}] for VM snapshot reversion.", virtualMachine.getUuid()); + + List volumeObjectTOs = vmSnapshotHelper.getVolumeTOList(virtualMachine.getId()); + + List deltaMergeTreeTOList = new ArrayList<>(); + Commands commands = new Commands(Command.OnError.Stop); + List deletedDeltas = new ArrayList<>(); + + Map backupVmSnapshotMap = new HashMap<>(); + + for (InternalBackupJoinVO currentBackup : currentBackups) { + VMSnapshotVO vmSnapshotSucceedingCurrentBackup = getSucceedingVmSnapshot(currentBackup); + + createDeleteCommandsAndMergeTrees(volumeObjectTOs, commands, deletedDeltas, vmSnapshotSucceedingCurrentBackup, deltaMergeTreeTOList, currentBackup); + backupVmSnapshotMap.put(currentBackup, vmSnapshotSucceedingCurrentBackup); + } + + if (CollectionUtils.isNotEmpty(deltaMergeTreeTOList)) { + commands.addCommand(new MergeDiskOnlyVmSnapshotCommand(deltaMergeTreeTOList, false, virtualMachine.getInstanceName())); + } + + Long hostId = vmSnapshotHelper.pickRunningHost(virtualMachine.getId()); + + Answer[] answers; + try { + answers = sendBackupCommands(hostId, commands); + } catch (AgentUnavailableException | OperationTimedoutException e) { + throw new CloudRuntimeException(e); + } + + if (answers == null || Arrays.stream(answers).anyMatch(answer -> !answer.getResult())) { + logger.error("Error while trying to prepare VM [{}] for VM snapshot reversion. Got [{}] as answers from host.", virtualMachine.getUuid(), + answers != null ? Arrays.stream(answers).filter(answer -> !answer.getResult()).map(Answer::getDetails) : null); + throw new CloudRuntimeException(String.format("Unable to prepare VM [%s] for VM snapshot reversion.", virtualMachine.getUuid())); + } + + for (Map.Entry backupAndVmSnapshot : backupVmSnapshotMap.entrySet()) { + InternalBackupJoinVO backup = backupAndVmSnapshot.getKey(); + VMSnapshotVO vmSnapshotSucceedingBackup = backupAndVmSnapshot.getValue(); + + List snapRefsSucceedingCurrentBackup = new ArrayList<>(); + + if (vmSnapshotSucceedingBackup != null) { + snapRefsSucceedingCurrentBackup = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotSucceedingBackup.getId()); + } + + updateReferencesAfterPrepareForSnapshotRevert(deltaMergeTreeTOList, snapRefsSucceedingCurrentBackup, deletedDeltas, backup); + } + } + + /** + * Get the secondary storage URLs of the backups that are backing files of this VM. This is only useful for Validation VMs currently, which are created with backing files on + * the secondary storage. + * */ + @Override + public Set getSecondaryStorageUrls(UserVm userVm) { + VMInstanceDetailVO detailVO = vmInstanceDetailsDao.findDetail(userVm.getId(), ApiConstants.BACKUP_ID); + if (detailVO == null) { + return Set.of(); + } + BackupVO backupVO = backupDao.findByUuid(detailVO.getValue()); + Set secondaryStorageUrls = getParentSecondaryStorageUrls(backupVO); + InternalBackupJoinVO internalBackupJoinVO = internalBackupJoinDao.findById(backupVO.getId()); + secondaryStorageUrls.add(imageStoreDao.findById(internalBackupJoinVO.getImageStoreId()).getUrl()); + return secondaryStorageUrls; + } + + @Override + public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) { + return false; + } + + @Override + public List listRestorePoints(VirtualMachine vm) { + return null; + } + + @Override + public String getConfigComponentName() { + return BackupService.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {backupChainSize, backupTimeout, backupCompressionTimeout, backupCompressionMinimumFreeStorage, backupCompressionRateLimit, + backupCompressionCoroutines}; + } + + protected Outcome createBackupThroughJobQueue(VirtualMachine vm, boolean quiesceVm, boolean isolated, Long backupScheduleId) { + final CallContext context = CallContext.current(); + long userId = context.getCallingUser().getId(); + long accountId = context.getCallingAccount().getAccountId(); + long vmId = vm.getId(); + + BackupVO backup = new BackupVO(String.format("%s-%s", vm.getHostName(), DateUtil.getDateInSystemTimeZone()), vmId, vm.getBackupOfferingId(), accountId, + vm.getDomainId(), vm.getDataCenterId(), 0, Backup.Status.Queued, backupScheduleId); + + VmWorkJobVO workJob = new VmWorkJobVO(AsyncJobExecutionContext.getOriginJobId(), userId, accountId, VmWorkTakeBackup.class.getName(), vmId, VirtualMachine.Type.Instance, + VmWorkJobVO.Step.Starting); + VmWorkTakeBackup workInfo = new VmWorkTakeBackup(userId, accountId, vmId, backupDao.persist(backup).getId(), VM_WORK_JOB_HANDLER, quiesceVm, isolated); + + workJob.setDispatcher(VmWorkConstants.VM_WORK_JOB_DISPATCHER); + workJob.setCmdInfo(VmWorkSerializer.serialize(workInfo)); + + jobManager.submitAsyncJob(workJob, VmWorkConstants.VM_WORK_QUEUE, vmId); + AsyncJobExecutionContext.getCurrentExecutionContext().joinJob(workJob.getId()); + + return new OutcomeImpl<>(Pair.class, workJob, VirtualMachineManagerImpl.VmJobCheckInterval.value(), new Predicate() { + @Override + public boolean checkCondition() { + AsyncJobVO jobVo = entityManager.findById(AsyncJobVO.class, workJob.getId()); + return jobVo == null || jobVo.getStatus() != JobInfo.Status.IN_PROGRESS; + } + }, AsyncJob.Topics.JOB_STATE); + } + + protected Outcome deleteBackupThroughJobQueue(Backup backup, boolean forced) { + final CallContext context = CallContext.current(); + long userId = context.getCallingUser().getId(); + long accountId = context.getCallingAccount().getAccountId(); + VirtualMachine userVm = userVmDao.findByIdIncludingRemoved(backup.getVmId()); + long vmId = userVm.getId(); + + VmWorkJobVO workJob = new VmWorkJobVO(AsyncJobExecutionContext.getOriginJobId(), userId, accountId, VmWorkDeleteBackup.class.getName(), vmId, VirtualMachine.Type.Instance, + VmWorkJobVO.Step.Starting); + VmWorkDeleteBackup workInfo = new VmWorkDeleteBackup(userId, accountId, vmId, VM_WORK_JOB_HANDLER, backup.getId(), forced); + + return submitWorkJob(workJob, workInfo, vmId); + } + + protected Outcome restoreVMFromBackupThroughJobQueue(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { + final CallContext context = CallContext.current(); + long userId = context.getCallingUser().getId(); + long accountId = context.getCallingAccount().getAccountId(); + long vmId = vm.getId(); + + VmWorkJobVO workJob = new VmWorkJobVO(AsyncJobExecutionContext.getOriginJobId(), userId, accountId, VmWorkRestoreBackup.class.getName(), vmId, VirtualMachine.Type.Instance, + VmWorkJobVO.Step.Starting); + VmWorkRestoreBackup workInfo = new VmWorkRestoreBackup(userId, accountId, vmId, VM_WORK_JOB_HANDLER, backup.getId(), quickRestore, hostId); + + return submitWorkJob(workJob, workInfo, vmId); + } + + protected Outcome restoreBackedUpVolumeThroughJobQueue(VirtualMachine vm, Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, boolean quickRestore) { + final CallContext context = CallContext.current(); + long userId = context.getCallingUser().getId(); + long accountId = context.getCallingAccount().getAccountId(); + long vmId = vm.getId(); + + VmWorkJobVO workJob = new VmWorkJobVO(AsyncJobExecutionContext.getOriginJobId(), userId, accountId, VmWorkRestoreVolumeBackupAndAttach.class.getName(), vmId, + VirtualMachine.Type.Instance, VmWorkJobVO.Step.Starting); + VmWorkRestoreVolumeBackupAndAttach workInfo = new VmWorkRestoreVolumeBackupAndAttach(userId, accountId, vmId, VM_WORK_JOB_HANDLER, backup.getId(), + backupVolumeInfo, hostIp, quickRestore); + + return submitWorkJob(workJob, workInfo, vmId); + } + + protected OutcomeImpl submitWorkJob(VmWorkJobVO workJob, VmWork workInfo, long vmId) { + workJob.setDispatcher(VmWorkConstants.VM_WORK_JOB_DISPATCHER); + workJob.setCmdInfo(VmWorkSerializer.serialize(workInfo)); + + jobManager.submitAsyncJob(workJob, VmWorkConstants.VM_WORK_QUEUE, vmId); + AsyncJobExecutionContext.getCurrentExecutionContext().joinJob(workJob.getId()); + + return new OutcomeImpl<>(Boolean.class, workJob, VirtualMachineManagerImpl.VmJobCheckInterval.value(), new Predicate() { + @Override + public boolean checkCondition() { + AsyncJobVO jobVo = entityManager.findById(AsyncJobVO.class, workJob.getId()); + return jobVo == null || jobVo.getStatus() != JobInfo.Status.IN_PROGRESS; + } + }, AsyncJob.Topics.JOB_STATE); + } + + protected void validateBackupAsyncIfHasOfferingSupport(InternalBackupJoinVO backupJoinVO, long zoneId, long accountId) { + if (!offeringSupportsValidation(backupJoinVO)) { + return; + } + + logger.info("Queuing backup validation job for backup [{}].", backupJoinVO.getUuid()); + internalBackupServiceJobDao.persist(new InternalBackupServiceJobVO(backupJoinVO.getId(), zoneId, backupJoinVO.getVmId(), accountId, InternalBackupServiceJobType.BackupValidation)); + } + + protected void compressBackupAsync(InternalBackupJoinVO backupJoinVO, long zoneId, long accountId) { + logger.info("Queuing backup compression job for backup [{}].", backupJoinVO.getUuid()); + internalBackupServiceJobDao.persist(new InternalBackupServiceJobVO(backupJoinVO.getId(), zoneId, backupJoinVO.getVmId(), accountId, InternalBackupServiceJobType.StartCompression)); + } + + protected boolean finalizeQuickRestore(VirtualMachine vm, List volumesToConsolidate, long hostId) { + logger.info("Finalizing quick restore for VM [{}].", vm.getUuid()); + + UserVmVO userVmVO = userVmDao.findById(vm.getId()); + if (userVmVO.getState() == VirtualMachine.State.Stopped) { + try { + logger.info("Starting VM [{}] as part of the quick restore process.", vm.getName()); + userVmManager.startVirtualMachine(userVmVO.getId(), hostId, new HashMap<>(), null, true); + } catch (Exception e) { + logger.error("Caught [{}] while trying to quick restore VM [{}]. Throwing BackupException.", e, vm); + throw new BackupException(String.format("Exception while trying to start VM [%s] as part of the quick restore process.", userVmVO.getUuid()), e, false); + } + } + + return consolidateVolumes(vm, hostId, volumesToConsolidate); + } + + protected boolean validateWithHash(long backupId, BackupVO backupVO, BackupDetailVO hashDetail) { + List backupDeltaTOList = getBackupDeltaTOList(backupId); + TakeBackupHashCommand hashCommand = new TakeBackupHashCommand(backupDeltaTOList, backupVO.getUuid()); + List hosts = hostDao.listAllHostsUpByZoneAndHypervisor(backupVO.getZoneId(), Hypervisor.HypervisorType.KVM); + String message; + if (CollectionUtils.isEmpty(hosts)) { + message = String.format("No Up and Enabled host found in zone [%s]. Cannot validate backup [%s]. Will try again later.", backupVO.getZoneId(), backupVO.getUuid()); + logger.error(message); + setBackupUnableToValidateAndSendAlert(backupVO, message); + return false; + } + Collections.shuffle(hosts); + Answer answer = sendBackupCommand(hosts.get(0).getId(), hashCommand); + if (!answer.getResult()) { + message = String.format("Unable to get hash of backup [%s] due to [%s]. Will try again later.", backupVO.getUuid(), answer.getDetails()); + logger.warn(message); + setBackupUnableToValidateAndSendAlert(backupVO, message); + return false; + } + + if (!hashDetail.getValue().equals(answer.getDetails())) { + message = String.format("Current xxHash128 of backup [%s] is different from previous validated hash. This backup has changed and might be corrupt." + + "The old hash is [%s]; the new hash is [%s].", backupVO.getUuid(), hashDetail.getValue(), answer.getDetails()); + logger.error(message); + setBackupAsInvalidAndSendAlert(backupVO, message); + return false; + } + + logger.info("xxHash128 of backup [{}] is the same as when it was validated. This backup is still valid.", backupVO.getUuid()); + backupVO.setValidationStatus(Backup.ValidationStatus.Valid); + backupDao.update(backupId, backupVO); + return true; + } + + protected boolean validateWithValidationVm(long backupId, long hostId, BackupVO backupVO) { + boolean startedVm = false; + UserVmVO validationVm = null; + List volumeVOs = List.of(); + try { + validationVm = allocateValidationVm(backupId, backupVO); + if (validationVm == null) { + return false; + } + + HostVO hostVo = hostDao.findById(hostId); + List volumeToList = new ArrayList<>(); + volumeVOs = volumeDao.findByInstance(validationVm.getId()); + createValidationVolumesOnPrimaryStorage(volumeVOs, validationVm, backupVO, hostVo, volumeToList); + + List backupDeltas = internalBackupDataStoreDao.listByBackupId(backupId); + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backupId); + Set> backupDeltaAndVolumePairs = generateBackupAndVolumePairsToRestore(backupDeltas, volumeToList, backupJoinVO, false); + if (!prepareForValidation(hostId, backupDeltaAndVolumePairs, backupVO, validationVm)) { + return false; + } + + userVmManager.startVirtualMachine(validationVm, null); + startedVm = true; + //refresh info + validationVm = userVmDao.findById(validationVm.getId()); + hostVo = hostDao.findById(validationVm.getHostId()); + + HypervisorGuru hvGuru = hypervisorGuruManager.getGuru(validationVm.getHypervisorType()); + VirtualMachineProfileImpl profile = new VirtualMachineProfileImpl(validationVm); + VirtualMachineTO vmTO = hvGuru.implement(profile); + + if (!validateBackup(backupId, vmTO, backupDeltaAndVolumePairs, backupVO, validationVm, hostVo)) { + endBackupChainIfConfigured(backupVO); + return false; + } + calculateAndSaveHash(backupDeltaAndVolumePairs, backupVO, hostVo.getId()); + return true; + } catch (Exception ex) { + logger.error("Encountered an exception during the validation process of backup [{}]. Will cleanup now.", backupVO.getUuid(), ex); + setBackupUnableToValidateAndSendAlert(backupVO, "Failed to validate due to unexpected exception: " + ex.getMessage()); + return false; + } finally { + cleanupValidation(startedVm, validationVm, backupVO, volumeVOs); + } + } + + /** + * If backupValidationEndChainOnFail is true for the account, and the backup being validated is part of the current chain, we end the current chain. + * */ + protected void endBackupChainIfConfigured(BackupVO backupVO) { + if (!getValidationEndChainOnFail(backupVO)) { + return; + } + List backupChildren = getBackupJoinChildren(backupVO); + + // Get updated record + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backupVO.getId()); + if (backupJoinVO.getCurrent() || (!backupChildren.isEmpty() && backupChildren.get(backupChildren.size() - 1).getCurrent())) { + logger.info("As [{}] is true, we are ending the backup chain of schedule [{}] for VM [{}]. The next backup will be a full backup.", + backupVO.getBackupScheduleId(), BackupValidationServiceJobController.backupValidationEndChainOnFail.toString()); + endBackupChain(userVmDao.findById(backupVO.getVmId()), backupVO.getBackupScheduleId()); + } + } + + /** + * This method was created to facilitate testing + * */ + protected Boolean getValidationEndChainOnFail(BackupVO backupVO) { + return BackupValidationServiceJobController.backupValidationEndChainOnFail.valueIn(backupVO.getAccountId()); + } + + protected boolean normalizeBackupErrorAndFinishChain(UserVmVO userVmVO) { + VMInstanceDetailVO detail = vmInstanceDetailsDao.findDetail(userVmVO.getId(), VmDetailConstants.LAST_KNOWN_STATE); + boolean runningVM = detail == null || VirtualMachine.State.valueOf(detail.getValue()) == VirtualMachine.State.Running; + + BackupVO backupVO = backupDao.findLatestByStatusAndVmId(Backup.Status.Error, userVmVO.getId()); + InternalBackupJoinVO currentOnThisChain = internalBackupJoinDao.findCurrent(userVmVO.getId(), backupVO.getBackupScheduleId()); + InternalBackupJoinVO errorBackup = internalBackupJoinDao.findById(backupVO.getId()); + + boolean errorOnBackupCreation = currentOnThisChain == null || currentOnThisChain.getId() != errorBackup.getId(); + + List succeedingBackupList = getSucceedingBackupList(currentOnThisChain); + List succeedingVmSnapshotList = getSucceedingVmSnapshotList(currentOnThisChain); + List volumeTOs = vmSnapshotHelper.getVolumeTOList(userVmVO.getId()); + + Map> volumeToDeltasAfterCurrent = mapVolumesToVmSnapshotAndBackupReferences(volumeTOs, succeedingVmSnapshotList, succeedingBackupList); + + List kbossTOS = new ArrayList<>(); + List deltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(errorBackup.getId()); + InternalBackupJoinVO parent = internalBackupJoinDao.findById(errorBackup.getParentId()); + + // There is a possibility that the cleanup step of the backup creation was executed, and thus we would have to merge with the old parent's parent + List parentDeltasOnPrimary = new ArrayList<>(); + if (parent != null) { + parentDeltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(parent.getId()); + } + + List deltasOnSecondary = internalBackupDataStoreDao.listByBackupId(errorBackup.getId()); + ImageStoreVO imageStoreVO = imageStoreDao.findById(errorBackup.getImageStoreId()); + configureKbossTosForCleanup(userVmVO, deltasOnPrimary, volumeToDeltasAfterCurrent, deltasOnSecondary, parentDeltasOnPrimary, kbossTOS, errorOnBackupCreation); + CleanupKbossBackupErrorCommand command = new CleanupKbossBackupErrorCommand(runningVM, errorOnBackupCreation, errorBackup.getEndOfChain(), succeedingBackupList.isEmpty(), + userVmVO.getInstanceName(), imageStoreVO.getUrl(), kbossTOS); + + long hostId = userVmVO.getHostId() != null ? userVmVO.getHostId() : vmSnapshotHelper.pickRunningHost(userVmVO.getId()); + Answer answer = sendBackupCommand(hostId, command); + if (answer == null || !answer.getResult()) { + logger.error("Unable to finish backup chain for VM [{}]. The host [{}] logs will have more information on why this happened.", userVmVO.getUuid(), hostId); + return false; + } + + boolean chainAlreadyEnded = processCleanupBackupErrorAnswer(userVmVO, answer, errorBackup, currentOnThisChain, succeedingBackupList); + + if (!chainAlreadyEnded) { + mergeCurrentBackupDeltas(errorBackup); + } + + if (currentOnThisChain != null) { + internalBackupStoragePoolDao.expungeByBackupId(currentOnThisChain.getId()); + setEndOfChainAndRemoveCurrentForBackup(currentOnThisChain); + } + + return finishBackupChains(userVmVO); + } + + protected boolean processCleanupBackupErrorAnswer(UserVmVO userVmVO, Answer answer, InternalBackupJoinVO errorBackup, InternalBackupJoinVO currentBackup, + List succeedingBackups) { + boolean runningVM; + CleanupKbossBackupErrorAnswer cleanAnswer = (CleanupKbossBackupErrorAnswer) answer; + logger.info("Successfully finished chain for VM [{}] and normalizing the BackupError state. Cleaning up metadata.", userVmVO.getUuid()); + + boolean chainAlreadyEnded = true; + for (Map.Entry> entry : cleanAnswer.getVolumeIdToPathAndChainEnded().entrySet()) { + VolumeVO volumeVO = volumeDao.findByUuid(entry.getKey()); + if (!entry.getValue().first().equals(volumeVO.getPath())) { + volumeVO.setPath(entry.getValue().first()); + volumeDao.update(volumeVO.getId(), volumeVO); + if (!entry.getValue().second()) { + chainAlreadyEnded = false; + continue; + } + internalBackupStoragePoolDao.expungeByVolumeIdAndBackupId(volumeVO.getId(), errorBackup.getId()); + } + } + updateSucceedingBackupIfNeeded(currentBackup, succeedingBackups); + + runningVM = cleanAnswer.isVmRunning(); + userVmVO.setState(runningVM ? VirtualMachine.State.Running : VirtualMachine.State.Stopped); + userVmDao.update(userVmVO.getId(), userVmVO); + vmInstanceDetailsDao.removeDetail(userVmVO.getId(), VmDetailConstants.LAST_KNOWN_STATE); + return chainAlreadyEnded; + } + + private void updateSucceedingBackupIfNeeded(InternalBackupJoinVO currentBackup, List succeedingBackups) { + if (currentBackup == null || succeedingBackups.isEmpty()) { + return; + } + InternalBackupJoinVO succeedingBackup = succeedingBackups.get(0); + for (InternalBackupStoragePoolVO deltaRef : internalBackupStoragePoolDao.listByBackupId(currentBackup.getId())) { + InternalBackupStoragePoolVO succeedingDelta = internalBackupStoragePoolDao.findOneByVolumeIdAndBackupId(deltaRef.getVolumeId(), succeedingBackup.getId()); + if (succeedingDelta != null) { + succeedingDelta.setBackupDeltaParentPath(deltaRef.getBackupDeltaParentPath()); + internalBackupStoragePoolDao.update(succeedingDelta.getId(), succeedingDelta); + } + } + } + + protected void calculateAndSaveHash(Set> backupDeltaAndVolumePairs, BackupVO backupVO, long hostId) { + TakeBackupHashCommand cmd = new TakeBackupHashCommand(backupDeltaAndVolumePairs.stream().map(Pair::first).collect(Collectors.toList()), backupVO.getUuid()); + Answer answer = sendBackupCommand(hostId, cmd); + + if (answer.getResult() && answer.getDetails() != null) { + logger.debug("Got xxHash128 [{}] of backup [{}].", answer.getDetails(), backupVO.getUuid()); + backupDetailDao.addDetail(backupVO.getId(), BackupDetailsDao.BACKUP_HASH, answer.getDetails(), false); + return; + } + logger.warn("Unable to get hash of backup [{}] due to [{}].", backupVO.getUuid(), answer.getDetails()); + } + + + /** + * Return a list of BackupDeltaTO of the given backup. + * */ + protected List getBackupDeltaTOList(long backupId) { + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backupId); + DataStoreTO imageStoreTo = dataStoreManager.getDataStore(backupJoinVO.getImageStoreId(), DataStoreRole.Image).getTO(); + List deltas = internalBackupDataStoreDao.listByBackupId(backupId); + + return deltas.stream() + .map(delta -> new BackupDeltaTO(imageStoreTo, Hypervisor.HypervisorType.KVM, delta.getBackupPath())) + .collect(Collectors.toList()); + } + + protected void cleanupValidation(boolean startedVm, UserVmVO validationVm, BackupVO backupVO, List volumeVOs) { + if (validationVm == null) { + return; + } + if (startedVm) { + userVmManager.stopVirtualMachine(validationVm.getId(), true); + } + DestroyVMCmd destroyVMCmd = new DestroyVMCmd(validationVm.getId(), true); + StringBuilder errorMessage = new StringBuilder("Cleanup failed due to:"); + boolean sendMail = false; + try { + userVmManager.destroyVm(destroyVMCmd, false); + } catch (Exception e) { + errorMessage.append("\nGot an unexpected exception while trying to destroy validation VM."); + sendMail = true; + logger.error("Got an error while trying to cleanup validation of backup [{}].", backupVO.getUuid(), e); + } + for (VolumeVO volume : volumeVOs) { + if (volume.getVolumeType() == Volume.Type.ROOT) { + continue; + } + Volume vol = volumeApiService.destroyVolume(volume.getId(), CallContext.current().getCallingAccount(), true, true, null); + if (vol == null) { + sendMail = true; + errorMessage.append(String.format("\nWe were unable to destroy volume [%s].", volume.getUuid())); + } + } + + if (startedVm) { + CleanupKbossValidationCommand cleanupKbossValidationCommand = new CleanupKbossValidationCommand(validationVm.getName(), getSecondaryStorageUrls(validationVm)); + Answer answer = agentManager.easySend(validationVm.getHostId(), cleanupKbossValidationCommand); + if (answer == null || !answer.getResult()) { + logger.error("Failed to cleanup post validation of backup [{}]. Got answer [{}]", backupVO.getUuid(), answer == null ? null : answer.getDetails()); + HostVO host = hostDao.findById(validationVm.getHostId()); + sendMail = true; + errorMessage.append(String.format("\nFailed to cleanup secondary storage mount at host [%s].", host != null ? host.getUuid() : "null")); + } + } + + if (sendMail) { + sendCleanupFailedEmail(backupVO, errorMessage.toString()); + } + } + + protected boolean validateBackup(long backupId, VirtualMachineTO vmTO, Set> backupDeltaAndVolumePairs, BackupVO backupVO, UserVmVO validationVm, + HostVO hostVo) { + Answer answer; + ValidateKbossVmCommand validateKbossVmCommand = new ValidateKbossVmCommand(vmTO, backupDeltaAndVolumePairs.stream().findFirst().get().first()); + configureValidationSteps(validateKbossVmCommand, backupVO); + answer = agentManager.easySend(validationVm.getHostId(), validateKbossVmCommand); + + boolean result = processValidationAnswer(answer, backupVO, validationVm, hostVo, validateKbossVmCommand); + if (result) { + backupVO.setValidationStatus(Backup.ValidationStatus.Valid); + backupDao.update(backupId, backupVO); + } + return result; + } + + protected boolean prepareForValidation(long hostId, Set> backupDeltaAndVolumePairs, BackupVO backupVO, UserVmVO validationVm) { + PrepareValidationCommand prepareCommand = new PrepareValidationCommand(new ArrayList<>(backupDeltaAndVolumePairs), getParentSecondaryStorageUrls(backupVO)); + + Answer answer = agentManager.easySend(hostId, prepareCommand); + + if (answer == null || !answer.getResult()) { + String msg = String.format("Failed to prepare dummy VM [%s] for validation of %s. %s", validationVm.getName(), backupVO.getUuid(), answer != null ? + "Details: "+ answer.getDetails(): ""); + logger.error(msg); + setBackupUnableToValidateAndSendAlert(backupVO, msg); + return false; + } + return true; + } + + protected void createValidationVolumesOnPrimaryStorage(List volumeVOs, UserVmVO validationVm, BackupVO backupVO, HostVO hostVo, List volumeToList) throws NoTransitionException { + for (VolumeVO volume : volumeVOs) { + logger.debug("Creating validation volume [{}] for validation VM [{}].", volume.getUuid(), validationVm.getUuid()); + VolumeInfo volumeInfo = volumeDataFactory.getVolume(volume.getId()); + volumeInfo = volumeOrchestrationService.createVolumeOnPrimaryStorage(validationVm, volumeInfo, Hypervisor.HypervisorType.KVM, null, hostVo.getClusterId(), + hostVo.getPodId()); + validateCorrectStorageType(backupVO, volume, volumeInfo); + volumeToList.add((VolumeObjectTO)volumeInfo.getTO()); + } + } + + protected UserVmVO allocateValidationVm(long backupId, BackupVO backupVO) { + UserVmVO validationVm; + try { + validationVm = (UserVmVO) userVmManager.allocateVMForValidation(backupId, Hypervisor.HypervisorType.KVM); + NicVO nic = nicDao.findDefaultNicForVM(validationVm.getId()); + virtualMachineManager.updateVmNic(validationVm, nic, false); + validationVm.setDataCenterId(backupVO.getZoneId()); + } catch (InsufficientCapacityException | ResourceAllocationException | ResourceUnavailableException e) { + String msg = String.format("Unable to allocate dummy VM to validate %s due to %s.", backupVO.getUuid(), e.getMessage()); + logger.error(msg, e); + setBackupUnableToValidateAndSendAlert(backupVO, msg); + return null; + } + return validationVm; + } + + protected List getVolumesToConsolidate(VirtualMachine vm, List deltasOnSecondary, List volumeObjectTOS, long hostId, + boolean sameVmAsBackup) { + List volumesToConsolidate = new ArrayList<>(); + + transitVmState(vm, VirtualMachine.Event.RestoringSuccess, hostId); + for (VolumeObjectTO volume : volumeObjectTOS) { + VolumeInfo volumeInfo = volumeDataFactory.getVolume(volume.getVolumeId()); + transitVolumeState(volumeInfo.getVolume(), Volume.Event.RestoreSucceeded); + + if (!sameVmAsBackup || deltasOnSecondary.stream().anyMatch(delta -> delta.getVolumeId() == volume.getVolumeId())) { + volumesToConsolidate.add(volumeInfo); + } + } + return volumesToConsolidate; + } + + protected boolean consolidateVolumes(VirtualMachine vm, long hostId, List volumesToConsolidate) { + for (VolumeInfo volumeInfo : volumesToConsolidate) { + transitVolumeState(volumeInfo.getVolume(), Volume.Event.ConsolidationRequested); + } + + VMInstanceDetailVO uuids = vmInstanceDetailsDao.findDetail(vm.getId(), VmDetailConstants.LINKED_VOLUMES_SECONDARY_STORAGE_UUIDS); + List secondaryStorageUuids = uuids != null ? List.of(uuids.getValue().split(",")) : List.of(); + ConsolidateVolumesCommand cmd = new ConsolidateVolumesCommand(volumesToConsolidate, secondaryStorageUuids, vm.getInstanceName()); + Answer answer = sendBackupCommand(hostId, cmd); + + String logError = String.format("Failed to consolidate volumes [%s] of VM [%s]. Answer details: [%s].", + volumesToConsolidate, vm.getName(), answer != null ? answer.getDetails() : "null"); + if (!(answer instanceof ConsolidateVolumesAnswer)) { + logger.error(logError); + throw new BackupException(logError, false); + } + ConsolidateVolumesAnswer cAnswer = (ConsolidateVolumesAnswer)answer; + processConsolidateAnswer(cAnswer, volumesToConsolidate, vm); + + logger.info("Volume consolidation answer: [{}].", cAnswer.getResult()); + return cAnswer.getResult(); + } + + /** + * Validates the Backup status:
+ * - If it is Error and The VM is in BackupError, will throw an exception;
+ * - If it is in Error but the VM is not in BackupError, will set the backup as Failed so that it may be removed with {@code deleteFailedBackup(BackupVO backupVO)};
+ * - If it is not in Error, does nothing. + * */ + protected void checkErrorBackup(BackupVO backupVO, VirtualMachine virtualMachine) { + if (backupVO.getStatus() != Backup.Status.Error) { + return; + } + if (virtualMachine != null && virtualMachine.getState() == VirtualMachine.State.BackupError) { + logger.error("Unable to delete backup [{}] as it is in Error state and the associated VM [{}] is in BackupError state. You must read the backup creation logs," + + " normalize the VM's volumes in the hypervisor/storage and update the VM state in the database before trying to delete the backup. Try again when the VM is not " + + "in this state.", backupVO, virtualMachine.getUuid()); + throw new InvalidParameterValueException(String.format("Unable to delete backup [%s]. Please check the logs.", backupVO.getUuid())); + } + logger.debug("Assuming VM and storage are normalized and setting backup [{}] as failed so its metadata is deleted."); + backupVO.setStatus(Backup.Status.Failed); + } + + /** + * Deletes a Failed backup metadata and sets the backup as Expunged. + * */ + protected boolean deleteFailedBackup(BackupVO backupVO) { + if (backupVO.getStatus() != Backup.Status.Failed) { + return false; + } + long backupId = backupVO.getId(); + + backupVO.setStatus(Backup.Status.Expunged); + backupDao.update(backupId, backupVO); + internalBackupStoragePoolDao.expungeByBackupId(backupId); + internalBackupDataStoreDao.expungeByBackupId(backupId); + backupDetailDao.removeDetails(backupId); + return true; + } + + /** + * Creates the necessary delta references on both primary and secondary storage. Also maps the volume to the parent delta backup and create the delta merge tree. + * */ + protected void createDeltaReferences(boolean fullBackup, boolean runningVm, Backup backup, + List parentBackupDeltasOnSecondary, List parentBackupDeltasOnPrimary, + HashMap volumeUuidToDeltaPrimaryRef, HashMap volumeUuidToDeltaSecondaryRef, + VMSnapshotVO succeedingVmSnapshot, KbossTO kbossTO) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + logger.debug("Creating delta references for backup [{}] of volume [{}].", backup.getUuid(), volumeObjectTO.getUuid()); + + String filename = UUID.randomUUID().toString(); + String relativePathOnSecondary = String.format("%s%s%s%s%s%s%s", "backups", File.separator, volumeObjectTO.getAccountId(), File.separator, volumeObjectTO.getId(), + File.separator, filename); + kbossTO.setDeltaPathOnPrimary(filename); + kbossTO.setDeltaPathOnSecondary(relativePathOnSecondary); + + InternalBackupDataStoreVO deltaSecondaryRef = new InternalBackupDataStoreVO(backup.getId(), volumeObjectTO.getVolumeId(), volumeObjectTO.getDeviceId(), relativePathOnSecondary); + if (!fullBackup) { + InternalBackupStoragePoolVO parentDeltaOnPrimary = createDeltaMergeTreeForVolume(false, runningVm, parentBackupDeltasOnPrimary, succeedingVmSnapshot, kbossTO, + new ArrayList<>()); + findAndSetParentBackupPath(parentBackupDeltasOnSecondary, parentDeltaOnPrimary, kbossTO); + } + + InternalBackupDataStoreVO referenceOnSecondary = internalBackupDataStoreDao.persist(deltaSecondaryRef); + logger.trace("Created reference [{}] for backup [{}] of volume [{}].", referenceOnSecondary, backup, volumeObjectTO); + volumeUuidToDeltaSecondaryRef.put(volumeObjectTO.getUuid(), referenceOnSecondary); + + InternalBackupStoragePoolVO deltaPrimaryRef = new InternalBackupStoragePoolVO(backup.getId(), volumeObjectTO.getPoolId(), volumeObjectTO.getVolumeId(), filename, + volumeObjectTO.getPath()); + + if (kbossTO.getDeltaMergeTreeTO() != null && CollectionUtils.isEmpty(kbossTO.getDeltaPaths())) { + deltaPrimaryRef.setBackupDeltaParentPath(kbossTO.getDeltaMergeTreeTO().getParent().getPath()); + } + + InternalBackupStoragePoolVO referenceOnPrimary = internalBackupStoragePoolDao.persist(deltaPrimaryRef); + logger.trace("Created reference [{}] for backup [{}] of volume [{}].", referenceOnPrimary, backup, volumeObjectTO); + volumeUuidToDeltaPrimaryRef.put(volumeObjectTO.getUuid(), referenceOnPrimary); + } + + /** + * Merges the current delta on primary storage, if any, into the given volume. If the backup has no more deltas on primary storage, will set the backup as end_of_chain. + * */ + protected void mergeCurrentDeltasIntoVolume(Volume volume, VirtualMachine virtualMachine, String operation, boolean isVmRunning) { + List currents = internalBackupJoinDao.listCurrentsByVolumeIdDesc(volume.getId()); + if (currents.isEmpty()) { + logger.debug("Volume [{}] has no deltas to merge, doing nothing.", volume.getUuid()); + return; + } + + for (InternalBackupJoinVO current : currents) { + InternalBackupStoragePoolVO delta = internalBackupStoragePoolDao.findOneByVolumeIdAndBackupId(volume.getId(), current.getId()); + + DataStore store = dataStoreManager.getDataStore(volume.getPoolId(), DataStoreRole.Primary); + VolumeObject volumeObject = VolumeObject.getVolumeObject(store, (VolumeVO)volume); + + DeltaMergeTreeTO deltaMergeTreeTO = createDeltaMergeTree(true, isVmRunning, delta, (VolumeObjectTO)volumeObject.getTO(), null, new ArrayList<>()); + MergeDiskOnlyVmSnapshotCommand cmd = new MergeDiskOnlyVmSnapshotCommand(List.of(deltaMergeTreeTO), isVmRunning, virtualMachine.getInstanceName()); + + Answer answer = sendBackupCommand(vmSnapshotHelper.pickRunningHost(virtualMachine.getId()), cmd); + + if (answer == null || !answer.getResult()) { + logger.error("Error while trying to prepare volume [{}] for {}. Got [{}] as answer from host.", volume.getUuid(), operation, answer != null ? answer.getDetails() : null); + throw new CloudRuntimeException(String.format("Unable to prepare volume [%s] for [%s].", volume.getUuid(), operation)); + } + VolumeVO volumeVO = volumeDao.findById(volumeObject.getId()); + volumeVO.setPath(deltaMergeTreeTO.getParent().getPath()); + volumeDao.update(volumeVO.getId(), volumeVO); + + volume = volumeVO; + + List deltaOnPrimary = List.of(delta); + expungeOldDeltasAndUpdateVmSnapshotOrBackup(deltaOnPrimary, null, null); + + List backupDeltas = internalBackupStoragePoolDao.listByBackupId(delta.getBackupId()); + if (backupDeltas.isEmpty()) { + logger.debug("Backup [{}] has no more deltas on primary storage due to prepare volume [{}] for {} operation. Will set it as end of chain and not current.", + current.getUuid(), volume.getUuid(), operation); + setEndOfChainAndRemoveCurrentForBackup(current); + } + } + } + + protected HostVO getHostToRestore(VirtualMachine vm, boolean quickRestore, Long hostId) throws AgentUnavailableException { + HostVO host; + if (quickRestore) { + if (hostId == null) { + hostId = vm.getLastHostId(); + } + if (hostId == null) { + logger.error("Cannot quick restore if the VM has no last host and no hostId was informed. You may try to start it in an available host and stop it before quick" + + " restoring. Otherwise, use the normal restore."); + throw new AgentUnavailableException(String.format("No host found to quick restore VM [%s]. Please check the logs.", vm.getUuid()), -1); + } + host = hostDao.findByIdIncludingRemoved(hostId); + if (host.getStatus() != Status.Up || host.isInMaintenanceStates() || host.getResourceState() != ResourceState.Enabled) { + logger.error("Cannot quick restore if the VM's last host is in maintenance, not Up, or disabled. You may try to start it in an available host and stop it before quick" + + " restoring. Otherwise, use the normal restore."); + throw new AgentUnavailableException(String.format("No host found to quick restore VM [%s]. Please check the logs.", vm.getUuid()), -1); + } + } else { + hostId = vmSnapshotHelper.pickRunningHost(vm.getId()); + host = hostDao.findByIdIncludingRemoved(hostId); + } + return host; + } + + /** + * Returns ordered list of disk-only VM snapshots taken after the last backup. The list is ordered from oldest to newest. + * */ + protected List getSucceedingVmSnapshotList(InternalBackupJoinVO backup) { + List vmSnapshotVOs = new ArrayList<>(); + if (backup == null) { + return vmSnapshotVOs; + } + + VMSnapshotVO currentSnapshotVO = vmSnapshotDao.findCurrentSnapshotByVmId(backup.getVmId()); + if (currentSnapshotVO == null || currentSnapshotVO.getCreated().before(backup.getDate())) { + return vmSnapshotVOs; + } + vmSnapshotVOs.add(0, currentSnapshotVO); + + while (currentSnapshotVO.getParent() != null && currentSnapshotVO.getParent() != 0) { + VMSnapshotVO parentSnap = vmSnapshotDao.findById(currentSnapshotVO.getParent()); + if (parentSnap.getCreated().before(backup.getDate())){ + break; + } + currentSnapshotVO = parentSnap; + vmSnapshotVOs.add(0, currentSnapshotVO); + } + + logger.debug("Found the following VM snapshots that succeed the backup [{}]: [{}].", backup.getUuid(), vmSnapshotVOs); + + return vmSnapshotVOs; + } + + /** + * Returns the disk-only VM snapshot taken after the last backup, if any. + * */ + protected VMSnapshotVO getSucceedingVmSnapshot(InternalBackupJoinVO backup) { + List snaps = getSucceedingVmSnapshotList(backup); + if (snaps.isEmpty()) { + return null; + } + return snaps.get(0); + } + + /** + * Returns ordered list of backups taken after the last backup. The list is ordered from oldest to newest. + * */ + protected List getSucceedingBackupList(InternalBackupJoinVO backup) { + List internalBackupJoinVOS = new ArrayList<>(); + if (backup == null) { + return internalBackupJoinVOS; + } + + List currentBackups = internalBackupJoinDao.listCurrents(backup.getVmId(), false); + if (currentBackups.isEmpty()) { + return internalBackupJoinVOS; + } + + internalBackupJoinVOS = currentBackups.stream().filter(internalBackupJoinVO -> internalBackupJoinVO.getDate().after(backup.getDate())).collect(Collectors.toList()); + logger.debug("Found the following backups that succeed the backup [{}]: [{}].", backup.getUuid(), internalBackupJoinVOS); + + return internalBackupJoinVOS; + } + + /** + * Given a list of volumes and VM snapshots/backups, maps the volumes to the delta references of the VM snapshots/backups. + * */ + protected Map> mapVolumesToVmSnapshotAndBackupReferences(List volumeObjectTOs, List vmSnapshotVOList, List internalBackupJoinVOList) { + Map> volumeToSnapshotAndBackupRefs = new HashMap<>(); + if (vmSnapshotVOList.isEmpty() && internalBackupJoinVOList.isEmpty()) { + logger.trace("No VM snapshot nor backup to map to any volume, returning."); + return volumeToSnapshotAndBackupRefs; + } + + List> volumeIdAndResourcePathAndCreatedDateList = new ArrayList<>(); + for (InternalBackupJoinVO internalBackupJoinVO : internalBackupJoinVOList) { + volumeIdAndResourcePathAndCreatedDateList.add(new Ternary<>(internalBackupJoinVO.getVolumeId(), internalBackupJoinVO.getStoragePoolDeltaPath(), internalBackupJoinVO.getDate())); + } + + for (VMSnapshotVO vmSnapshotVO : vmSnapshotVOList) { + vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotVO.getId()) + .forEach(snapshotDataStoreVO -> volumeIdAndResourcePathAndCreatedDateList.add(new Ternary<>(snapshotDataStoreVO.getVolumeId(), snapshotDataStoreVO.getInstallPath(), snapshotDataStoreVO.getCreated()))); + } + + volumeIdAndResourcePathAndCreatedDateList.sort(Comparator.comparing(Ternary::third)); + + for (Ternary volumeIdAndResourcePathAndCreatedDate : volumeIdAndResourcePathAndCreatedDateList) { + long volumeId = volumeIdAndResourcePathAndCreatedDate.first(); + String resourcePath = volumeIdAndResourcePathAndCreatedDate.second(); + + volumeToSnapshotAndBackupRefs.computeIfAbsent(volumeId, k -> new LinkedList<>()).addLast(resourcePath); + } + + logger.trace("Given volume objects [{}], VM snapshots [{}] and backups [{}], created the following map [{}].", volumeObjectTOs, vmSnapshotVOList, internalBackupJoinVOList, volumeToSnapshotAndBackupRefs); + return volumeToSnapshotAndBackupRefs; + } + + + protected void mapVolumesToSnapshotReferences(List volumeObjectTOs, List snapshotDataStoreVOS, Map> volumeToSnapshotRefs) { + for (VolumeObjectTO volumeObjectTO : volumeObjectTOs) { + List associatedSnapshots = snapshotDataStoreVOS.stream() + .filter(snapRef -> Objects.equals(snapRef.getVolumeId(), volumeObjectTO.getVolumeId())) + .collect(Collectors.toList()); + volumeToSnapshotRefs.put(volumeObjectTO.getId(), associatedSnapshots); + } + } + + /** + * Updates the necessary references on the database. Also calculates the backup's physical size. + * */ + protected long updateDeltaReferencesAndCalculateBackupPhysicalSize(VolumeObjectTO volumeObjectTO, HashMap volumeUuidToDeltaPrimaryRef, + HashMap volumeUuidToDeltaSecondaryRef, TakeKbossBackupAnswer answer, long physicalBackupSize, boolean endChain, boolean isolated, + BackupVO backupVO) { + String volumeUuid = volumeObjectTO.getUuid(); + InternalBackupStoragePoolVO deltaPrimaryRef = volumeUuidToDeltaPrimaryRef.get(volumeUuid); + if (endChain || isolated) { + logger.trace("Since backup [{}] is [{}]. We will delete the delta reference on primary at [{}] as it does not exist anymore.", backupVO.getUuid(), endChain ? + "end of chain" : "isolated", deltaPrimaryRef.getBackupDeltaPath()); + internalBackupStoragePoolDao.expunge(deltaPrimaryRef.getId()); + } + + InternalBackupDataStoreVO deltaSecondaryRef = volumeUuidToDeltaSecondaryRef.get(volumeUuid); + + String newVolumePath = answer.getMapVolumeUuidToNewVolumePath().get(volumeUuid); + + VolumeVO volumeVO = volumeDao.findById(volumeObjectTO.getId()); + volumeVO.setPath(newVolumePath); + logger.trace("Updating volume [{}] path to [{}].", volumeVO.getUuid(), newVolumePath); + volumeDao.update(volumeVO.getId(), volumeVO); + + Pair deltaPathOnSecondaryAndSize = answer.getMapVolumeUuidToDeltaPathOnSecondaryAndSize().get(volumeUuid); + logger.trace("Updating delta reference on secondary [{}] path to [{}].", deltaSecondaryRef, deltaPathOnSecondaryAndSize.first()); + deltaSecondaryRef.setBackupPath(deltaPathOnSecondaryAndSize.first()); + internalBackupDataStoreDao.update(deltaSecondaryRef.getId(), deltaSecondaryRef); + + physicalBackupSize += deltaPathOnSecondaryAndSize.second(); + return physicalBackupSize; + } + + /** + * Expunge the old backup deltas and if there were disk-only VM snapshot or backup deltas after the last backup, update their paths. + * */ + protected void expungeOldDeltasAndUpdateVmSnapshotOrBackupIfNeeded(List oldDeltasOnPrimary, VMSnapshot vmSnapshot, + InternalBackupJoinVO lastBackup) { + List snapshotRefs = vmSnapshot == null ? List.of() : vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshot.getId()); + List newBackupDeltas = new ArrayList<>(); + Map volumeIdNewBackupDeltaMap = new HashMap<>(); + + if (lastBackup != null) { + newBackupDeltas = internalBackupStoragePoolDao.listByBackupId(lastBackup.getId()); + volumeIdNewBackupDeltaMap = newBackupDeltas.stream().collect(Collectors.toMap(InternalBackupStoragePoolVO::getVolumeId, nbsp -> nbsp)); + } + + for (InternalBackupStoragePoolVO oldBackupDelta : oldDeltasOnPrimary) { + logger.trace("Expunging old backup delta [{}].", oldBackupDelta); + internalBackupStoragePoolDao.expunge(oldBackupDelta.getId()); + SnapshotDataStoreVO snapshotDataStoreVO = snapshotRefs.stream().filter(ref -> ref.getVolumeId() == oldBackupDelta.getVolumeId()).findFirst().orElse(null); + if (snapshotDataStoreVO != null) { + snapshotDataStoreVO.setInstallPath(oldBackupDelta.getBackupDeltaParentPath()); + logger.debug("Updating snapshot delta [{}] path to [{}].", snapshotDataStoreVO.getId(), oldBackupDelta.getBackupDeltaParentPath()); + snapshotDataStoreDao.update(snapshotDataStoreVO.getId(), snapshotDataStoreVO); + continue; + } + if (lastBackup != null) { + InternalBackupStoragePoolVO newBackupDelta = volumeIdNewBackupDeltaMap.get(oldBackupDelta.getVolumeId()); + newBackupDelta.setBackupDeltaParentPath(oldBackupDelta.getBackupDeltaParentPath()); + logger.debug("Updating backup delta [{}] path to [{}].", newBackupDelta.getId(), oldBackupDelta.getBackupDeltaParentPath()); + internalBackupStoragePoolDao.update(newBackupDelta.getId(), newBackupDelta); + } + } + } + + /** + * Expunges old deltas on primary storage and updates the metadata for either + * the succeeding VM snapshot or the succeeding backup based on their chronological order. + * If only one (or neither) is provided, it proceeds with the available entities. + * + * @param oldDeltasOnPrimary The list of delta references on the primary storage to be removed; + * @param succeedingVmSnapshotVO The VM snapshot that follows the deltas being expunged; + * @param succeedingBackup The backup entity that follows the deltas being expunged. + */ + protected void expungeOldDeltasAndUpdateVmSnapshotOrBackup(List oldDeltasOnPrimary, VMSnapshot succeedingVmSnapshotVO, + InternalBackupJoinVO succeedingBackup) { + if (ObjectUtils.allNotNull(succeedingVmSnapshotVO, succeedingBackup)) { + if (succeedingVmSnapshotVO.getCreated().before(succeedingBackup.getDate())) { + expungeOldDeltasAndUpdateVmSnapshotOrBackupIfNeeded(oldDeltasOnPrimary, succeedingVmSnapshotVO, null); + } else { + expungeOldDeltasAndUpdateVmSnapshotOrBackupIfNeeded(oldDeltasOnPrimary, null, succeedingBackup); + } + } else { + expungeOldDeltasAndUpdateVmSnapshotOrBackupIfNeeded(oldDeltasOnPrimary, succeedingVmSnapshotVO, succeedingBackup); + } + } + + + /** + * Create a {@link DeltaMergeTreeTO} for the volume if it has a delta on primary and add it to the list. + * + * @return the delta on primary of the volume. Null if no delta. + * */ + protected InternalBackupStoragePoolVO createDeltaMergeTreeForVolume(boolean childIsVolume, boolean runningVm, List deltasOnPrimary, VMSnapshotVO succeedingVmSnapshot, + KbossTO kbossTO, List succeedingBackupList) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + + InternalBackupStoragePoolVO deltaOnPrimary = deltasOnPrimary.stream() + .filter(delta -> delta.getVolumeId() == volumeObjectTO.getVolumeId()) + .findFirst() + .orElse(null); + if (deltaOnPrimary == null) { + logger.debug("Volume [{}] has no delta on primary storage.", volumeObjectTO); + return null; + } + + logger.debug("Volume [{}] has a backup delta on primary storage [{}].", volumeObjectTO.getUuid(), deltaOnPrimary); + + kbossTO.setDeltaMergeTreeTO(createDeltaMergeTree(childIsVolume, runningVm, deltaOnPrimary, volumeObjectTO, succeedingVmSnapshot, succeedingBackupList)); + return deltaOnPrimary; + } + + protected DeltaMergeTreeTO createDeltaMergeTree(boolean childIsVolume, boolean runningVm, InternalBackupStoragePoolVO deltaOnPrimary, + VolumeObjectTO volumeObjectTO, VMSnapshotVO succeedingVmSnapshot, List succeedingBackupsList) { + DataStore store = dataStoreManager.getDataStore(deltaOnPrimary.getStoragePoolId(), DataStoreRole.Primary); + DataTO deltaChild; + if (childIsVolume) { + deltaChild = volumeObjectTO; + } else { + deltaChild = new BackupDeltaTO(store.getTO(), Hypervisor.HypervisorType.KVM, deltaOnPrimary.getBackupDeltaPath()); + } + + BackupDeltaTO deltaParent = new BackupDeltaTO(store.getTO(), Hypervisor.HypervisorType.KVM, deltaOnPrimary.getBackupDeltaParentPath()); + List succeedingSnapshotList = succeedingVmSnapshot != null ? vmSnapshotDao.listByParent(succeedingVmSnapshot.getId()) : new ArrayList<>(); + + List succeedingDeltaPaths = new ArrayList<>(); + if (succeedingVmSnapshot != null || CollectionUtils.isNotEmpty(succeedingBackupsList)) { + succeedingDeltaPaths = mapVolumesToVmSnapshotAndBackupReferences(List.of(volumeObjectTO), succeedingSnapshotList, succeedingBackupsList) + .getOrDefault(volumeObjectTO.getVolumeId(), new LinkedList<>()); + + if (!childIsVolume && !runningVm && succeedingDeltaPaths.isEmpty()) { + succeedingDeltaPaths = List.of(volumeObjectTO.getPath()); + logger.debug("Since the last backup delta of volume [{}] is succeeded by a snapshot and the delta created by this snapshot is also the volume, it will have to be" + + " rebased. Setting it as the grand-child.", volumeObjectTO.getUuid()); + } + } + + List deltaGrandchildren = succeedingDeltaPaths.stream() + .map(deltaPath -> new BackupDeltaTO(store.getTO(), Hypervisor.HypervisorType.KVM, deltaPath)) + .collect(Collectors.toList()); + + DeltaMergeTreeTO deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, deltaParent, deltaChild, deltaGrandchildren); + + logger.debug("Mapped the following delta merge tree for volume [{}]: [{}].", volumeObjectTO.getUuid(), deltaMergeTreeTO); + return deltaMergeTreeTO; + } + + /** + * Sets on the {@code kbossTO} the backupParentOnSecondary path based on the list of InternalBackupDataStoreVO. + * + * @param parentBackupDeltasOnSecondary + * List of deltas on secondary; + * @param parentDeltaOnPrimary + * @param kbossTO + * KbossTO to be configured; + */ + protected void findAndSetParentBackupPath(List parentBackupDeltasOnSecondary, InternalBackupStoragePoolVO parentDeltaOnPrimary, KbossTO kbossTO) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + if (parentDeltaOnPrimary == null) { + logger.debug("Volume [{}] has no parent on primary, thus its backup cannot be incremental.", volumeObjectTO); + return; + } + + InternalBackupDataStoreVO parentOnSecondary = parentBackupDeltasOnSecondary.stream() + .filter(backupDataStoreVo -> volumeObjectTO.getVolumeId() == backupDataStoreVo.getVolumeId()) + .findFirst() + .orElse(null); + + if (parentOnSecondary == null) { + return; + } + + logger.debug("Volume [{}] already has a backup [{}].", volumeObjectTO.getUuid(), parentOnSecondary.getBackupId()); + + kbossTO.setPathBackupParentOnSecondary(parentOnSecondary.getBackupPath()); + } + + /** + * Verify if the data center has heuristic rules for allocating backups; if there is then returns the {@link DataStore} returned by the JS script. + * Otherwise, returns a {@link DataStore} with free capacity. + */ + protected DataStore getImageStoreForBackup(Long dataCenterId, BackupVO backupVO) { + DataStore imageStore = heuristicRuleHelper.getImageStoreIfThereIsHeuristicRule(dataCenterId, HeuristicType.BACKUP, backupVO); + + if (imageStore == null) { + imageStore = dataStoreManager.getImageStoreWithFreeCapacity(dataCenterId); + } + + if (imageStore == null) { + backupVO.setStatus(Backup.Status.Failed); + backupDao.update(backupVO.getId(), backupVO); + throw new CloudRuntimeException(String.format("Unable to find secondary storage for backup [%s].", backupVO)); + } + + logger.debug("Backup [{}] will use secondary storage [{}].", backupVO.getUuid(), imageStore.getUuid()); + return imageStore; + } + + protected void setBackupAsIsolated(BackupVO backup) { + logger.debug("Setting backup [{}] as isolated.", backup.getUuid()); + backupDetailDao.persist(new BackupDetailVO(backup.getId(), ISOLATED, Boolean.TRUE.toString(), true)); + } + + /** + * Gets the parent for newBackup. Will set the newBackup as the end of chain if needed.
+ * - If no backups are found, returns null.
+ * - If the last backup was the end of the chain, returns null.
+ * + * @param newBackup the new backup being created. + * @param backupChain newBackup's ancestors. + * */ + protected InternalBackupJoinVO getParentAndSetEndOfChain(BackupVO newBackup, List backupChain, BackupOfferingVO offering) { + int chainSize = getChainSizeForBackup(offering, newBackup.getZoneId()); + if (CollectionUtils.isEmpty(backupChain)) { + setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(chainSize, chainSize, newBackup.getId(), newBackup.getUuid()); + return null; + } + + int remainingChainSize = chainSize - backupChain.size(); + setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(remainingChainSize, chainSize, newBackup.getId(), newBackup.getUuid()); + + InternalBackupJoinVO parent = backupChain.get(0); + return parent.getStatus().equals(Backup.Status.BackedUp) ? parent : null; + } + + /** + * For every restore point, maps a volume to it. + * @throws CloudRuntimeException If cannot map restore point to any volume. + * */ + protected Set> generateBackupAndVolumePairsToRestore(List backupDeltas, List volumeTOs, + InternalBackupJoinVO backupJoinVO, boolean sameVmAsBackup) { + Set> backupAndVolumePairs = new HashSet<>(); + DataStore dataStore = dataStoreManager.getDataStore(backupJoinVO.getImageStoreId(), DataStoreRole.Image); + for (InternalBackupDataStoreVO backupDataStoreVO : backupDeltas) { + VolumeObjectTO volumeObjectTO = volumeTOs.stream() + .filter(volumeTO -> sameVmAsBackup ? volumeTO.getVolumeId() == backupDataStoreVO.getVolumeId() : volumeTO.getDeviceId() == backupDataStoreVO.getDeviceId()) + .findFirst() + .orElse(null); + + if (volumeObjectTO == null) { + logger.error("All backups should have a corresponding volume at this point, however, backup delta [{}] does not.", backupDataStoreVO.getId()); + throw new CloudRuntimeException("Error while restoring backup. Please check the logs."); + } + + backupAndVolumePairs.add(new Pair<>(new BackupDeltaTO(dataStore.getTO(), Hypervisor.HypervisorType.KVM, backupDataStoreVO.getBackupPath()), volumeObjectTO)); + } + logger.debug("Generated the following list of pairs of backup deltas and volumes: [{}].", backupAndVolumePairs); + return backupAndVolumePairs; + } + + protected Pair generateBackupAndVolumePairForSingleNewVolume(InternalBackupDataStoreVO backupDeltaVo, VolumeObjectTO volumeTO, + InternalBackupJoinVO backupJoinVO) { + DataStore dataStore = dataStoreManager.getDataStore(backupJoinVO.getImageStoreId(), DataStoreRole.Image); + Pair backupAndVolumePair = new Pair<>(new BackupDeltaTO(dataStore.getTO(), Hypervisor.HypervisorType.KVM, backupDeltaVo.getBackupPath()), volumeTO); + + logger.debug("Paired volume [{}] with backup delta [{}].", volumeTO, backupAndVolumePair.first()); + return backupAndVolumePair; + } + + /** + * For every volume, maps deltas that should be deleted, if there are any. If a volume has a delta but is not part of backup being restored, it will be mapped to be merged. + * + * @return List of deltas to be merged. + * */ + protected List populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(List deltasOnPrimary, Set deltasToRemove, List volumeTOs, + List volumesNotPartOfTheBackupBeingRestored, String vmUuid) { + List deltasToBeMerged = new ArrayList<>(); + for (InternalBackupStoragePoolVO deltaOnPrimary : deltasOnPrimary) { + Optional optional = volumeTOs.stream().filter(volumeTO -> volumeTO.getVolumeId() == deltaOnPrimary.getVolumeId()).findFirst(); + if (optional.isEmpty()) { + logger.error("Failed to find volume that matches delta [{}] with path [{}]. Please check for inconsistencies on the database or if there are leftover" + + " deltas on storage.", deltaOnPrimary.getId(), deltaOnPrimary.getBackupDeltaPath()); + throw new CloudRuntimeException(String.format("Failed to restore VM [%s]. Please check the logs.", vmUuid)); + } + VolumeObjectTO volumeObjectTO = optional.get(); + + if (volumesNotPartOfTheBackupBeingRestored.contains(volumeObjectTO)) { + deltasToBeMerged.add(createDeltaMergeTree(true, false, deltaOnPrimary, volumeObjectTO, null, new ArrayList<>())); + continue; + } + + DataStore dataStore = dataStoreManager.getDataStore(deltaOnPrimary.getStoragePoolId(), DataStoreRole.Primary); + BackupDeltaTO backupDeltaTO = new BackupDeltaTO(dataStore.getTO(), Hypervisor.HypervisorType.KVM, deltaOnPrimary.getBackupDeltaPath()); + logger.debug("Mapped the following backup delta on primary to be removed since the volume [{}] is not part of the backup being restored [{}].", + volumeObjectTO.getUuid(), backupDeltaTO); + deltasToRemove.add(backupDeltaTO); + volumeObjectTO.setPath(deltaOnPrimary.getBackupDeltaParentPath()); + } + if (!deltasToBeMerged.isEmpty()) { + logger.debug("The following deltaMergeTrees [{}] were created to merge volumes [{}] that have no backups.", deltasToBeMerged, volumesNotPartOfTheBackupBeingRestored); + } + return deltasToBeMerged; + } + + protected void updateVolumePathsAndSizeIfNeeded(VirtualMachine vm, List volumeTOs, List volumeInfos, + List deltaMergeTreeTOList, boolean sameVmAsBackup) { + List volumeVOs = volumeDao.findByInstance(vm.getId()); + + for (VolumeVO volumeVO : volumeVOs) { + VolumeObjectTO volumeTO = volumeTOs.stream().filter(volumeObjectTO -> volumeObjectTO.getVolumeId() == volumeVO.getId()).findFirst().get(); + + String log = "Volume [%s] path was updated as part of the backup restore process. New path: [%s]."; + DeltaMergeTreeTO deltaMergeTreeTO = deltaMergeTreeTOList.stream().filter(delta -> delta.getChild().getId() == volumeTO.getId()).findFirst().orElse(null); + if (!volumeVO.getPath().equals(volumeTO.getPath())) { + volumeVO.setPath(volumeTO.getPath()); + logger.debug(() -> String.format(log, volumeVO.getUuid(), volumeVO.getPath())); + } else if (deltaMergeTreeTO != null) { + volumeVO.setPath(deltaMergeTreeTO.getParent().getPath()); + logger.debug(() -> String.format(log, volumeVO.getUuid(), volumeVO.getPath())); + } + + Backup.VolumeInfo volumeInfo = volumeInfos.stream() + .filter(info -> sameVmAsBackup ? volumeVO.getUuid().equals(info.getUuid()) : volumeVO.getDeviceId().equals(info.getDeviceId())) + .findFirst().orElse(null); + if (volumeInfo != null && !Objects.equals(volumeInfo.getSize(), volumeVO.getSize())) { + logger.debug("Volume [{}] size was restored as part of the backup restore process. Old size is [{}] new size is [{}].", volumeVO.getUuid(), + volumeVO.getSize(), volumeInfo.getSize()); + volumeVO.setSize(volumeInfo.getSize()); + } + + volumeDao.update(volumeVO.getId(), volumeVO); + } + } + + protected void createAndAttachVolumes(List volumeInfos, List backupDeltas, VirtualMachine vm, HostVO host) { + logger.info("Found the following backup deltas that have no volume correspondence [{}]. Will create new volumes and attach them to VM [{}].", backupDeltas.stream() + .map(InternalBackupDataStoreVO::getId).collect(Collectors.toList()), vm.getUuid()); + for (InternalBackupDataStoreVO delta : backupDeltas) { + VolumeVO volumeVO = volumeDao.findByIdIncludingRemoved(delta.getVolumeId()); + Backup.VolumeInfo backupVolumeInfo = volumeInfos.stream().filter(info -> volumeVO.getUuid().equals(info.getUuid())).findFirst().orElseThrow(); + VolumeInfo volumeInfo = duplicateAndCreateVolume(vm, host, backupVolumeInfo); + Volume volume = volumeApiService.attachVolumeToVM(vm.getId(), volumeInfo.getId(), null, false, true); + transitVolumeState(volume, Volume.Event.RestoreRequested); + delta.setVolumeId(volume.getId()); + } + } + + protected VolumeInfo duplicateAndCreateVolume(VirtualMachine vm, HostVO hostVo, Backup.VolumeInfo backupVolumeInfo) { + VolumeVO newVolume = duplicateVolume(backupVolumeInfo); + VolumeInfo volumeInfo = volumeDataFactory.getVolume(newVolume.getId()); + + try { + volumeInfo = volumeOrchestrationService.createVolumeOnPrimaryStorage(vm, volumeInfo, Hypervisor.HypervisorType.KVM, null, hostVo.getClusterId(), hostVo.getPodId()); + validateCorrectStorageType(null, newVolume, volumeInfo); + } catch (NoTransitionException ex) { + logger.error("Exception while creating volume to restore.", ex); + throw new CloudRuntimeException(ex); + } + + return volumeInfo; + } + + protected VolumeVO duplicateVolume(Backup.VolumeInfo backupVolumeInfo) { + VolumeVO volumeVO = volumeDao.findByUuidIncludingRemoved(backupVolumeInfo.getUuid()); + VolumeVO duplicateVO = new VolumeVO(volumeVO); + DiskOfferingVO diskOfferingVO = diskOfferingDao.findByUuidIncludingRemoved(backupVolumeInfo.getDiskOfferingId()); + duplicateVO.setDiskOfferingId(diskOfferingVO.getId()); + duplicateVO.setSize(backupVolumeInfo.getSize()); + duplicateVO.setMinIops(backupVolumeInfo.getMinIops()); + duplicateVO.setMaxIops(backupVolumeInfo.getMaxIops()); + duplicateVO.setAttached(null); + duplicateVO.setVolumeType(Volume.Type.DATADISK); + duplicateVO.setInstanceId(null); + duplicateVO.setPoolId(null); + duplicateVO.setPath(null); + return volumeDao.persist(duplicateVO); + } + + protected List getBackupsWithoutVolumes(List backups, List volumes) { + List deltasOnSecondaryWithNoVolumes = new ArrayList<>(); + for (InternalBackupDataStoreVO backup : backups) { + VolumeObjectTO volumeObjectTO = volumes.stream().filter(volumeTO -> volumeTO.getVolumeId() == backup.getVolumeId()) + .findFirst() + .orElse(null); + + if (volumeObjectTO == null) { + deltasOnSecondaryWithNoVolumes.add(backup); + } + } + return deltasOnSecondaryWithNoVolumes; + } + + protected List getVolumesThatAreNotPartOfTheBackup(List volumeObjectTOS, List deltasOnSecondary) { + List volumesWithNoBackups = new ArrayList<>(); + for (VolumeObjectTO volume : volumeObjectTOS) { + if (deltasOnSecondary.stream().noneMatch(delta -> delta.getVolumeId() == volume.getVolumeId())) { + volumesWithNoBackups.add(volume); + } + } + logger.debug("Found the following volumes that are not part of the backup being restored [{}].", volumesWithNoBackups); + return volumesWithNoBackups; + } + + protected void processBackupSuccess(boolean runningVm, List volumeTOs, HashMap volumeUuidToDeltaPrimaryRef, + HashMap volumeUuidToDeltaSecondaryRef, TakeKbossBackupAnswer answer, List parentBackupDeltasOnPrimary, + VMSnapshotVO succeedingVmSnapshot, BackupVO backupVO, boolean fullBackup, VirtualMachine userVm, Long hostId, boolean endChain, boolean isolated, + InternalBackupJoinVO succeedingBackup) { + long physicalBackupSize = 0; + logger.debug("Processing backup [{}] success.", backupVO.getUuid()); + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + physicalBackupSize = updateDeltaReferencesAndCalculateBackupPhysicalSize(volumeObjectTO, volumeUuidToDeltaPrimaryRef, volumeUuidToDeltaSecondaryRef, answer, + physicalBackupSize, endChain, isolated, backupVO); + } + + expungeOldDeltasAndUpdateVmSnapshotOrBackup(parentBackupDeltasOnPrimary, succeedingVmSnapshot, succeedingBackup); + + backupVO.setSize(physicalBackupSize); + backupVO.setStatus(Backup.Status.BackedUp); + backupVO.setBackedUpVolumes(backupManager.createVolumeInfoFromVolumes(new ArrayList<>(volumeDao.findByInstance(userVm.getId())))); + backupDao.loadDetails(backupVO); + backupVO.getDetails().putAll(backupManager.getBackupDetailsFromVM(userVm)); + backupVO.setType(fullBackup ? "FULL" : "INCREMENTAL"); + backupDao.update(backupVO.getId(), backupVO); + + transitVmState(userVm, runningVm ? VirtualMachine.Event.BackupSucceededRunning : VirtualMachine.Event.BackupSucceededStopped, hostId); + } + + protected void processBackupFailure(Answer answer, VirtualMachine vm, long hostId, boolean runningVm, BackupVO backupVO) { + if (answer instanceof TakeKbossBackupAnswer && ((TakeKbossBackupAnswer) answer).isVmConsistent()) { + logger.info("Backup [{}] of VM [{}] failed. However, the VM is still consistent, so we will roll back its state.", backupVO.getUuid(), vm.getUuid()); + backupVO.setStatus(Backup.Status.Failed); + + transitVmState(vm, runningVm ? VirtualMachine.Event.OperationFailedToRunning : VirtualMachine.Event.OperationFailedToStopped, hostId); + } else { + logger.info("Backup [{}] of VM [{}] ended in error. We are not sure if the VM is consistent; thus, we will set it as BackupError.", backupVO.getUuid(), vm.getUuid()); + transitVmState(vm, VirtualMachine.Event.OperationFailedToError, hostId); + vmInstanceDetailsDao.addDetail(vm.getId(), VmDetailConstants.LAST_KNOWN_STATE, runningVm ? VirtualMachine.State.Running.name() : VirtualMachine.State.Stopped.name(), false); + backupVO.setStatus(Backup.Status.Error); + } + + backupDao.update(backupVO.getId(), backupVO); + } + + protected void processRemovedBackups(List removedBackupIds) { + for (Long removedBackupId : removedBackupIds) { + BackupVO removedBackupVO = backupDao.findByIdIncludingRemoved(removedBackupId); + removedBackupVO.setStatus(Backup.Status.Expunged); + backupDao.update(removedBackupId, removedBackupVO); + internalBackupDataStoreDao.expungeByBackupId(removedBackupId); + backupDetailDao.removeDetailsExcept(removedBackupId, END_OF_CHAIN); + } + } + + /** + * For every backup, except for the one which the command was issued, will set them as Expunged regardless and hope operators will look + * at the logs. For the current one, if forced=false, will set it as error, otherwise, will set it as Expunged as well. + * */ + protected boolean processRemoveBackupFailures(boolean forced, Answer[] deleteAnswers, List removedBackupIds, InternalBackupJoinVO backupJoinVO, VirtualMachine vm) { + List failures = Arrays.stream(deleteAnswers).filter(answer -> !answer.getResult()).collect(Collectors.toList()); + Set failedToRemoveBackupIdSet = new HashSet<>(); + if (CollectionUtils.isNotEmpty(failures)) { + StringBuilder failureStringBuilder = new StringBuilder("Encountered the following failures during backup removal, all will be marked as Expunged and need to be" + + " manually deleted from storage. "); + for (Answer answer : failures) { + failedToRemoveBackupIdSet.add(((BackupDeleteAnswer)answer).getBackupId()); + failureStringBuilder.append(answer.getDetails()); + } + logger.error(failureStringBuilder.toString()); + } + + removedBackupIds.removeAll(failedToRemoveBackupIdSet); + + boolean result = failedToRemoveBackupIdSet.isEmpty(); + if (!forced && failedToRemoveBackupIdSet.remove(backupJoinVO.getId())) { + BackupVO failedVO = backupDao.findByIdIncludingRemoved(backupJoinVO.getId()); + logger.info("Since backup delete command was not forced, will not set the main backup [{}] as Expunged, will set it as error instead.", failedVO.getUuid()); + failedVO.setStatus(Backup.Status.Error); + backupDao.update(failedVO.getId(), failedVO); + vmInstanceDetailsDao.addDetail(vm.getId(), VmDetailConstants.LAST_KNOWN_STATE, vm.getState().name(), false); + } + + for (Long failedToRemove : failedToRemoveBackupIdSet) { + BackupVO failedVO = backupDao.findByIdIncludingRemoved(failedToRemove); + failedVO.setStatus(Backup.Status.Expunged); + logger.error("Setting backup [{}] as expunged, even though there was an error when deleting it from storage. Please look at the logs and check if it was deleted from" + + " storage.", failedVO.getUuid()); + backupDao.update(failedToRemove, failedVO); + } + + return result; + } + + protected void processConsolidateAnswer(ConsolidateVolumesAnswer cAnswer, List volumesToConsolidate, VirtualMachine vm) { + for (VolumeObjectTO volumeObjectTO : cAnswer.getSuccessfullyConsolidatedVolumes()) { + VolumeInfo volumeInfo = volumesToConsolidate.stream().filter(vol -> vol.getId() == volumeObjectTO.getVolumeId()).findFirst().orElseThrow(); + transitVolumeState(volumeInfo.getVolume(), Volume.Event.OperationSucceeded); + volumesToConsolidate.remove(volumeInfo); + } + volumesToConsolidate.forEach(volumeInfo -> transitVolumeState(volumeInfo, Volume.Event.OperationFailed)); + if (cAnswer.getResult()) { + vmInstanceDetailsDao.removeDetail(vm.getId(), VmDetailConstants.LINKED_VOLUMES_SECONDARY_STORAGE_UUIDS); + } else { + throw new BackupException(String.format("Failed to consolidate all volumes necessary of VM [%s]. Missing volumes are [%s].", vm.getUuid(), volumesToConsolidate), false); + } + } + + protected boolean processRestoreAnswers(VirtualMachine vm, Answer[] answers, boolean quickRestore) { + boolean cmdSucceeded = true; + for (Answer answer : answers) { + if (answer == null || !answer.getResult()) { + cmdSucceeded = false; + logger.error("Failed to restore backup due to: [{}].", answer == null ? "null answer" : answer.getDetails()); + } + if (answer instanceof RestoreKbossBackupAnswer && quickRestore) { + RestoreKbossBackupAnswer restoreAnswer = (RestoreKbossBackupAnswer) answer; + vmInstanceDetailsDao.addDetail(vm.getId(), VmDetailConstants.LINKED_VOLUMES_SECONDARY_STORAGE_UUIDS, StringUtils.join(restoreAnswer.getSecondaryStorageUuids(), ","), false); + } + } + return cmdSucceeded; + } + + protected boolean processValidationAnswer(Answer answer, BackupVO backupVO, UserVmVO validationVm, HostVO hostVo, ValidateKbossVmCommand validateKbossVmCommand) { + if (answer == null) { + String msg = String.format("Backup [%s] was validated using dummy VM [%s]. The backup was deemed invalid due to: Null answer from host [%s]", backupVO.getUuid(), + validationVm.getName(), hostVo.getName()); + logger.error(msg); + setBackupAsInvalidAndSendAlert(backupVO, msg); + return false; + } + if (!answer.getResult()) { + String msg = String.format("Backup [%s] was validated using dummy VM [%s]. The backup was deemed invalid due to: %s", backupVO.getUuid(), + validationVm.getName(), answer.getDetails()); + logger.error(msg); + setBackupAsInvalidAndSendAlert(backupVO, msg); + return false; + } + if (!(answer instanceof ValidateKbossVmAnswer)) { + return false; + } + ValidateKbossVmAnswer validateKbossVmAnswer = (ValidateKbossVmAnswer)answer; + boolean result = true; + String msg = String.format("Backup [%s] was validated using dummy VM [%s]. The backup was deemed invalid due to: ", backupVO.getUuid(), validationVm.getName()); + if (validateKbossVmCommand.isWaitForBoot() && !validateKbossVmAnswer.isBootValidated()) { + result = false; + msg += "\n - The VM did not boot within the expected time."; + } + if (validateKbossVmCommand.isExecuteScript() && validateKbossVmAnswer.getScriptResult() != null) { + result = false; + msg += "\n - The script did not output the expected output. Captured output: " + validateKbossVmAnswer.getScriptResult(); + } + if (validateKbossVmCommand.isTakeScreenshot() && validateKbossVmAnswer.getScreenshotPath() == null) { + result = false; + msg += "\n - We were unable to take a screenshot of the VM."; + } else if (validateKbossVmCommand.isTakeScreenshot()) { + logger.debug("Saving validation screenshot path [{}] to the backup details of backup [{}].", validateKbossVmAnswer.getScreenshotPath(), backupVO.getUuid()); + backupDetailDao.addDetail(backupVO.getId(), SCREENSHOT_PATH, validateKbossVmAnswer.getScreenshotPath(), false); + } + if (!result) { + setBackupAsInvalidAndSendAlert(backupVO, msg); + } + + return result; + } + + protected void handleBackupExceptionInRestore(VirtualMachine vm, BackupException jobResult) { + if (!jobResult.isVmConsistent()) { + UserVmVO vmVO = userVmDao.findById(vm.getId()); + vmVO.setState(VirtualMachine.State.RestoreError); + userVmDao.update(vmVO.getId(), vmVO); + for (VolumeVO vol : volumeDao.findByInstance(vmVO.getId())) { + vol.setState(Volume.State.RestoreError); + volumeDao.update(vol.getId(), vol); + } + } + } + + protected void handleRestoreException(Backup backup, VirtualMachine vm, Object jobResult) { + if (!(jobResult instanceof Throwable)) { + return; + } + if (jobResult instanceof BackupException) { + handleBackupExceptionInRestore(vm, (BackupException)jobResult); + } else if (jobResult instanceof BackupProviderException) { + throw (BackupProviderException) jobResult; + } + throw new CloudRuntimeException(String.format("Exception while restoring KVM internal incremental backup [%s]. Check the logs for more information.", backup.getUuid()), ((Throwable)jobResult).getCause()); + } + + protected boolean finishAllChains(VirtualMachine vm, List currents) { + if (currents.isEmpty()) { + logger.debug("There is no current active chain, no need to do anything."); + return true; + } + + for (InternalBackupJoinVO current : currents) { + if (!mergeCurrentBackupDeltas(current)) { + UserVmVO vmVO = userVmDao.findById(vm.getId()); + logger.error("Failed to merge deltas for VM [{}] during backup offering removal process. Changing its state to [{}].", vm, VirtualMachine.State.BackupError); + BackupVO backupVO = backupDao.findById(current.getId()); + backupVO.setStatus(Backup.Status.Error); + backupDao.update(backupVO.getId(), backupVO); + vmVO.setState(VirtualMachine.State.BackupError); + userVmDao.update(vmVO.getId(), vmVO); + + return false; + } + setEndOfChainAndRemoveCurrentForBackup(current); + } + return true; + } + + protected boolean endBackupChain(VirtualMachine vm, Long backupScheduleId) { + InternalBackupJoinVO current = internalBackupJoinDao.findCurrent(vm.getId(), backupScheduleId); + if (current == null) { + logger.debug("There is no current active chain, no need to do anything."); + return true; + } + + validateVmState(vm, "end backup chain"); + + if (mergeCurrentBackupDeltas(current)) { + setEndOfChainAndRemoveCurrentForBackup(current); + return true; + } + return false; + } + + /** + * Merges the backup deltas related to the passed {@code InternalBackupJoinVO}. + * + * @return true if the merge was successful and false otherwise. + * */ + protected boolean mergeCurrentBackupDeltas(InternalBackupJoinVO backupJoinVO) { + VirtualMachine userVm = userVmDao.findById(backupJoinVO.getVmId()); + + List succeedingBackupList = getSucceedingBackupList(backupJoinVO); + InternalBackupJoinVO succeedingBackup = succeedingBackupList.isEmpty() ? null : succeedingBackupList.get(0); + VMSnapshotVO succeedingVmSnapshot = getSucceedingVmSnapshot(backupJoinVO); + MergeDiskOnlyVmSnapshotCommand cmd = buildMergeDiskOnlyVmSnapshotCommandForCurrentBackup(backupJoinVO, userVm, succeedingVmSnapshot, succeedingBackupList); + Long hostId = vmSnapshotHelper.pickRunningHost(backupJoinVO.getVmId()); + + Answer answer = sendBackupCommand(hostId, cmd); + if (answer == null || !answer.getResult()) { + logger.error("Failed to remove backup [{}]. Tried to merge the current deltas to cleanup the VM but failed due to [{}].", + backupJoinVO.getUuid(), answer != null ? answer.getDetails() : "no answer"); + return false; + } + + List deltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(backupJoinVO.getId()); + expungeOldDeltasAndUpdateVmSnapshotOrBackup(deltasOnPrimary, succeedingVmSnapshot, succeedingBackup); + + if (ObjectUtils.anyNotNull(succeedingVmSnapshot, succeedingBackup)) { + return true; + } + + for (DeltaMergeTreeTO deltaMergeTreeTO : cmd.getDeltaMergeTreeToList()) { + VolumeVO volumeVO = volumeDao.findById(deltaMergeTreeTO.getVolumeObjectTO().getVolumeId()); + volumeVO.setPath(deltaMergeTreeTO.getParent().getPath()); + logger.debug("Updating volume [{}] path to [{}] as part of the backup delete cleanup process.", volumeVO.getUuid(), volumeVO.getPath()); + volumeDao.update(volumeVO.getId(), volumeVO); + } + + return true; + } + + protected void createDeleteCommandsAndMergeTrees(List volumeObjectTOs, Commands commands, List deletedDeltas, + VMSnapshotVO vmSnapshotSucceedingCurrentBackup, List deltaMergeTreeTOList, InternalBackupJoinVO currentBackup) { + for (VolumeObjectTO volumeObjectTO : volumeObjectTOs) { + InternalBackupStoragePoolVO delta = internalBackupStoragePoolDao.findOneByVolumeIdAndBackupId(volumeObjectTO.getVolumeId(), currentBackup.getId()); + if (delta == null) { + continue; + } + if (vmSnapshotSucceedingCurrentBackup == null) { + commands.addCommand(new DeleteCommand(new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, delta.getBackupDeltaParentPath()))); + deletedDeltas.add(delta); + logger.debug("Volume [{}] has a backup delta that will be deleted as part of the preparation to revert a VM snapshot.", volumeObjectTO.getUuid()); + } else { + deltaMergeTreeTOList.add(createDeltaMergeTree(false, false, delta, volumeObjectTO, vmSnapshotSucceedingCurrentBackup, new ArrayList<>())); + } + } + } + + /*** + * Gets the list of parents that should be expunged. Will also create delete commands for them and add them to the list deleteCommands object. + * + * @param backupVO backup being expunged + * @param deleteCommands Commands object that will be appended with the delete commands for the parent backups. + * @return A pair which contains the list of backups that will be expunged, and the reference to the last backup of the chain that is still alive, if it exists. + */ + protected Pair, InternalBackupJoinVO> getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(BackupVO backupVO, Commands deleteCommands) { + logger.debug("Searching for removed parents of [{}] that should be expunged.", backupVO); + List backupParents = getBackupJoinParents(backupVO, true); + List backupParentsToBeExpunged = null; + InternalBackupJoinVO lastAliveBackup = null; + for (int i = 0; i < backupParents.size(); i++) { + InternalBackupJoinVO backupParent = backupParents.get(i); + if (Backup.Status.Removed.equals(backupParent.getStatus())) { + addBackupDeltasToDeleteCommand(backupParent.getId(), deleteCommands); + } else { + backupParentsToBeExpunged = backupParents.subList(0, i); + lastAliveBackup = backupParents.get(i); + break; + } + } + if (backupParentsToBeExpunged == null) { + backupParentsToBeExpunged = backupParents; + } + logger.debug("Found [{}] removed parents of [{}] that should be expunged: [{}].", backupParentsToBeExpunged.size(), backupVO, backupParentsToBeExpunged); + return new Pair<>(backupParentsToBeExpunged, lastAliveBackup); + } + + private MergeDiskOnlyVmSnapshotCommand buildMergeDiskOnlyVmSnapshotCommandForCurrentBackup(InternalBackupJoinVO backupJoinVO, VirtualMachine userVm, VMSnapshotVO vmSnapshot, + List succeedingBackupList) { + List deltaMergeTreeTOs = new ArrayList<>(); + + List volumeTOs = vmSnapshotHelper.getVolumeTOList(backupJoinVO.getVmId()); + List deltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(backupJoinVO.getId()); + + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + KbossTO kbossTO = new KbossTO(volumeObjectTO, new LinkedList<>()); + boolean childIsVolume = vmSnapshot == null && succeedingBackupList.isEmpty(); + createDeltaMergeTreeForVolume(childIsVolume, userVm.getState() == VirtualMachine.State.Running, deltasOnPrimary, vmSnapshot, kbossTO, succeedingBackupList); + if (kbossTO.getDeltaMergeTreeTO() != null) { + deltaMergeTreeTOs.add(kbossTO.getDeltaMergeTreeTO()); + } else { + logger.debug("Volume [{}] does not have any deltas to merge as part of the backup delete process.", volumeObjectTO.getUuid()); + } + } + + return new MergeDiskOnlyVmSnapshotCommand(deltaMergeTreeTOs, userVm.getState().equals(VirtualMachine.State.Running), userVm.getInstanceName()); + } + + protected DataStore addBackupDeltasToDeleteCommand(long backupId, Commands deleteCommands) { + InternalBackupJoinVO internalBackupJoinVO = internalBackupJoinDao.findById(backupId); + List internalBackupDataStoreVOS = internalBackupDataStoreDao.listByBackupId(backupId); + DataStore dataStore = dataStoreManager.getDataStore(internalBackupJoinVO.getImageStoreId(), DataStoreRole.Image); + DataStoreTO dataStoreTO = dataStore.getTO(); + BackupDetailVO screenshotPath = backupDetailDao.findDetail(backupId, SCREENSHOT_PATH); + for (InternalBackupDataStoreVO internalBackupDataStoreVO : internalBackupDataStoreVOS) { + BackupDeltaTO backupDeltaTO = new BackupDeltaTO(dataStoreTO, Hypervisor.HypervisorType.KVM, internalBackupDataStoreVO.getBackupPath()); + backupDeltaTO.setId(backupId); + if (screenshotPath != null) { + backupDeltaTO.setScreenshotPath(screenshotPath.getValue()); + screenshotPath = null; + } + DeleteCommand deleteCommand = new DeleteCommand(backupDeltaTO); + deleteCommands.addCommand(deleteCommand); + } + return dataStore; + } + + protected Set getParentSecondaryStorageUrls(BackupVO backupVO) { + List parentBackups = getBackupJoinParents(backupVO, true); + Set secondaryStorageIds = parentBackups.stream().map(InternalBackupJoinVO::getImageStoreId).collect(Collectors.toSet()); + return secondaryStorageIds.stream().map(id -> imageStoreDao.findById(id).getUrl()).collect(Collectors.toSet()); + } + + protected List getChainImageStoreUrls(List backupChain) { + List chainImageStoreUrls; + LinkedHashSet imageStoreIdSet = backupChain.stream().map(InternalBackupJoinVO::getImageStoreId).collect(Collectors.toCollection(LinkedHashSet::new)); + chainImageStoreUrls = imageStoreIdSet.stream().map(id -> imageStoreDao.findById(id).getUrl()).collect(Collectors.toList()); + return chainImageStoreUrls; + } + + /** + * Gets the list of backup parents of a given BackupVO. + * @param backupVO the backup in question. + * @param includeRemoved whether to include removed (but not expunged) parents or not. + * @return list of parents, or an empty list if no parents found. + * */ + protected List getBackupJoinParents(BackupVO backupVO, boolean includeRemoved) { + List ancestorBackups; + + if (includeRemoved) { + ancestorBackups = internalBackupJoinDao.listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(backupVO.getVmId(), backupVO.getBackupScheduleId(), + backupVO.getDate()); + } else { + ancestorBackups = internalBackupJoinDao.listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(backupVO.getVmId(), backupVO.getBackupScheduleId(), backupVO.getDate(), true, + false); + } + + for (int i = 0; i < ancestorBackups.size(); i++) { + if (ancestorBackups.get(i).getEndOfChain()) { + return ancestorBackups.subList(0, i); + } + } + + logger.debug("Found the following backup chain ancestors of backup [{}]: [{}].", backupVO, ancestorBackups); + return ancestorBackups; + } + + protected int getChainSizeForBackup(BackupOfferingVO offering, long zoneId) { + BackupOfferingDetailsVO detailsVO = backupOfferingDetailsDao.findDetail(offering.getId(), ApiConstants.BACKUP_CHAIN_SIZE); + if (detailsVO != null) { + return Integer.parseInt(detailsVO.getValue()); + } + return backupChainSize.valueIn(zoneId); + } + + /** + * Gets the list of backup children of a given backupVO. In ascending created order. + * + * @return list of children, or and empty list if no children found. + * */ + protected List getBackupJoinChildren(BackupVO backupVO) { + List children = internalBackupJoinDao.listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(backupVO.getVmId(), backupVO.getBackupScheduleId(), + backupVO.getDate(), false, true); + + long parentId = backupVO.getId(); + for (int i = 0; i < children.size(); i++) { + if (children.get(i).getParentId() != parentId) { + return children.subList(0, i); + } + parentId = children.get(i).getId(); + } + + return children; + } + + /** + * Creates a detail for the given BackupVO if the remaining chain size is one or less and the value of backupChainSize is greater than 0. + * */ + protected void setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(int remainingChainSize, int chainSize, long backupId, String backupUuid) { + if (remainingChainSize <= 1 && chainSize > 0) { + logger.debug("Setting backup [{}] as end of chain.", backupUuid); + backupDetailDao.persist(new BackupDetailVO(backupId, END_OF_CHAIN, Boolean.TRUE.toString(), true)); + } + } + + protected void setBackupVirtualSize(List volumeTOs, BackupVO backupVO) { + long virtualSize = 0; + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + virtualSize += volumeObjectTO.getSize(); + } + + backupVO.setProtectedSize(virtualSize); + } + + protected void updateBackupStatusToBackingUp(List volumeTOs, BackupVO backupVO) { + setBackupVirtualSize(volumeTOs, backupVO); + backupVO.setStatus(Backup.Status.BackingUp); + backupDao.update(backupVO.getId(), backupVO); + } + + /** + * Retrieves the current backup and removes the CURRENT detail. If the informed backup is not the end of chain, sets it as the new CURRENT + * */ + protected void updateCurrentBackup(InternalBackupJoinVO backup) { + InternalBackupJoinVO current = internalBackupJoinDao.findCurrent(backup.getVmId(), backup.getScheduleId()); + + if (current != null) { + backupDetailDao.removeDetail(current.getId(), CURRENT); + } + + if (!backup.getEndOfChain()) { + backupDetailDao.persist(new BackupDetailVO(backup.getId(), CURRENT, Boolean.TRUE.toString(), true)); + } + } + + /** + * Given a backup, removes the CURRENT detail, and if the snapshot is not set as END_OF_CHAIN, sets it as END_OF_CHAIN. + * */ + protected void setEndOfChainAndRemoveCurrentForBackup(InternalBackupJoinVO currentBackup) { + backupDetailDao.removeDetail(currentBackup.getId(), CURRENT); + if (!currentBackup.getEndOfChain()) { + backupDetailDao.persist(new BackupDetailVO(currentBackup.getId(), END_OF_CHAIN, Boolean.TRUE.toString(), true)); + } + } + + protected void setBackupUnableToValidateAndSendAlert(BackupVO backupVO, String msg) { + backupVO.setValidationStatus(Backup.ValidationStatus.UnableToValidate); + backupDao.update(backupVO.getId(), backupVO); + alertManager.sendAlert(AlertService.AlertType.ALERT_TYPE_BACKUP_VALIDATION_UNABLE_TO_VALIDATE, backupVO.getZoneId(), null, String.format("Unable to validate backup [%s]", + backupVO.getName()), msg); + } + + protected void sendCleanupFailedEmail(BackupVO backupVO, String msg) { + alertManager.sendAlert(AlertService.AlertType.ALERT_TYPE_BACKUP_VALIDATION_CLEANUP_FAILED, backupVO.getZoneId(), null, String.format("Cleanup of validation of backup " + + "[%s] failed", + backupVO.getName()), msg); + } + + protected void setBackupAsInvalidAndSendAlert(BackupVO backupVO, String msg) { + backupVO.setValidationStatus(Backup.ValidationStatus.NotValid); + backupDao.update(backupVO.getId(), backupVO); + alertManager.sendAlert(AlertService.AlertType.ALERT_TYPE_BACKUP_VALIDATION_FAILED, backupVO.getZoneId(), null, String.format("Backup [%s] is not valid", + backupVO.getName()), msg); + } + + protected void configureKbossTosForCleanup(UserVmVO userVmVO, List deltasOnPrimary, Map> volumeIdToDeltasAfterCurrent, + List deltasOnSecondary, List parentDeltasOnPrimary, List kbossTOS, boolean errorOnCreation) { + for (VolumeObjectTO volumeObjectTO : vmSnapshotHelper.getVolumeTOList(userVmVO.getId())) { + InternalBackupStoragePoolVO deltaOnPrimary = deltasOnPrimary.stream() + .filter(delta -> delta.getVolumeId() == volumeObjectTO.getVolumeId()).findFirst().orElseThrow(); + InternalBackupDataStoreVO deltaOnSecondary = + deltasOnSecondary.stream().filter(delta -> delta.getVolumeId() == volumeObjectTO.getVolumeId()).findFirst().orElseThrow(); + KbossTO kbossTO; + if (errorOnCreation) { + InternalBackupStoragePoolVO parent = parentDeltasOnPrimary.stream().filter(delta -> delta.getVolumeId() == volumeObjectTO.getVolumeId()).findFirst().orElse(null); + kbossTO = new KbossTO(volumeObjectTO, parent == null ? deltaOnPrimary.getBackupDeltaParentPath() : parent.getBackupDeltaPath(), deltaOnSecondary.getBackupPath(), + volumeIdToDeltasAfterCurrent.get(volumeObjectTO.getId())); + if (parent != null) { + kbossTO.setParentDeltaPathOnPrimary(parent.getBackupDeltaParentPath()); + } + kbossTO.setOldVolumePath(volumeObjectTO.getPath()); + volumeObjectTO.setPath(deltaOnPrimary.getBackupDeltaPath()); + } else { + kbossTO = new KbossTO(volumeObjectTO, deltaOnPrimary.getBackupDeltaPath(), deltaOnSecondary.getBackupPath(), + volumeIdToDeltasAfterCurrent.get(volumeObjectTO.getId())); + kbossTO.setParentDeltaPathOnPrimary(deltaOnPrimary.getBackupDeltaParentPath()); + } + + kbossTOS.add(kbossTO); + } + } + + protected void configureValidationSteps(ValidateKbossVmCommand cmd, BackupVO backup) { + BackupOfferingVO offeringVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + BackupOfferingDetailsVO detailsVO = backupOfferingDetailsDao.findDetail(offeringVO.getId(), ApiConstants.VALIDATION_STEPS); + String validationSteps = detailsVO.getValue(); + for (String step : validationSteps.split(",")) { + Backup.ValidationSteps enumStep = Backup.ValidationSteps.valueOf(step); + switch (enumStep) { + case screenshot: + cmd.setTakeScreenshot(true); + VMInstanceDetailVO screenshotWait = vmInstanceDetailsDao.findDetail(backup.getVmId(), VmDetailConstants.VALIDATION_SCREENSHOT_WAIT); + cmd.setScreenshotWait(screenshotWait != null ? Integer.valueOf(screenshotWait.getValue()) : + BackupValidationServiceJobController.backupValidationScreenshotDefaultWait.valueIn(backup.getAccountId())); + break; + case wait_for_boot: + cmd.setWaitForBoot(true); + VMInstanceDetailVO bootTimeout = vmInstanceDetailsDao.findDetail(backup.getVmId(), VmDetailConstants.VALIDATION_BOOT_TIMEOUT); + cmd.setBootTimeout(bootTimeout != null ? Integer.valueOf(bootTimeout.getValue()) : + BackupValidationServiceJobController.backupValidationBootDefaultTimeout.valueIn(backup.getAccountId())); + break; + case execute_command: + configureValidationScript(cmd, backup); + break; + } + } + } + + protected void configureValidationScript(ValidateKbossVmCommand cmd, BackupVO backupVO) { + long vmId = backupVO.getVmId(); + VMInstanceDetailVO script = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND); + if (script == null) { + logger.warn("Execute command step was configured but no script given. Ignoring this step for backup [{}].", backupVO.getUuid()); + return; + } + cmd.setExecuteScript(true); + cmd.setScriptToExecute(script.getValue()); + VMInstanceDetailVO scriptArguments = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND_ARGUMENTS); + cmd.setScriptArguments(scriptArguments != null ? scriptArguments.getValue() : null); + VMInstanceDetailVO scriptExpectedResult = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND_EXPECTED_RESULT); + cmd.setExpectedResult(scriptExpectedResult != null ? scriptExpectedResult.getValue() : "0"); + VMInstanceDetailVO scriptTimeout = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND_TIMEOUT); + cmd.setScriptTimeout(scriptTimeout != null ? Integer.valueOf(scriptTimeout.getValue()) : + BackupValidationServiceJobController.backupValidationScriptDefaultTimeout.valueIn(backupVO.getId())); + } + + protected void createBasicBackupDetails(Long imageStoreId, Long parentId, BackupVO backupVO) { + backupDetailDao.persist(new BackupDetailVO(backupVO.getId(), IMAGE_STORE_ID, imageStoreId.toString(), false)); + backupDetailDao.persist(new BackupDetailVO(backupVO.getId(), PARENT_ID, parentId.toString(), false)); + } + + protected void updateReferencesAfterPrepareForSnapshotRevert(List deltaMergeTreeTOList, List snapRefsSucceedingCurrentBackup, + List deletedDeltas, InternalBackupJoinVO backupVO) { + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOList) { + SnapshotDataStoreVO snapshotRef = snapRefsSucceedingCurrentBackup.stream() + .filter(ref -> Objects.equals(ref.getVolumeId(), deltaMergeTreeTO.getVolumeObjectTO().getVolumeId())) + .findFirst() + .orElse(null); + if (snapshotRef != null) { + snapshotRef.setInstallPath(deltaMergeTreeTO.getParent().getPath()); + logger.debug("Updating snapshot reference [{}] path to [{}] as part of the preparation to restore a VM snapshot.", snapshotRef.getId(), snapshotRef.getInstallPath()); + snapshotDataStoreDao.update(snapshotRef.getId(), snapshotRef); + } + internalBackupStoragePoolDao.expungeByVolumeIdAndBackupId(deltaMergeTreeTO.getVolumeObjectTO().getVolumeId(), backupVO.getId()); + } + + for (InternalBackupStoragePoolVO delta : deletedDeltas) { + internalBackupStoragePoolDao.expungeByVolumeIdAndBackupId(delta.getVolumeId(), delta.getBackupId()); + } + + setEndOfChainAndRemoveCurrentForBackup(backupVO); + } + + protected Answer sendBackupCommand(long hostId, Command cmd) { + cmd.setWait(backupTimeout.value()); + return agentManager.easySend(hostId, cmd); + } + + protected Answer[] sendBackupCommands(Long hostId, Commands cmds) throws OperationTimedoutException, AgentUnavailableException { + for (Command cmd : cmds) { + cmd.setWait(backupTimeout.value()); + } + return agentManager.send(hostId, cmds); + } + + protected void validateCorrectStorageType(BackupVO backupVO, VolumeVO volume, VolumeInfo volumeInfo) { + StoragePoolVO storagePoolVO = storagePoolDao.findById(volumeInfo.getDataStore().getId()); + if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) { + logger.error("Error while trying to create volume [{}]. It was created in a storage that is not supported. Make sure that the disk offerings of VMs with backup " + + "offerings can only be allocated to file-based storages ({}).", backupVO, volume, supportedStoragePoolTypes); + throw new CloudRuntimeException(String.format("Unable to create volume [%s] due to a failure to allocate the volume. Please check the logs.", backupVO.getUuid())); + } + } + + protected void validateQuickRestore(Backup backup, boolean quickRestore) { + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + BackupOfferingDetailsVO detail = backupOfferingDetailsDao.findDetail(backupOfferingVO.getId(), ApiConstants.ALLOW_QUICK_RESTORE); + if (quickRestore && (detail == null || !Boolean.parseBoolean(detail.getValue()))) { + throw new BackupProviderException(String.format("Unable to quick restore backup [%s] using offering [%s] as the offering does not support quick restoration.", + backup.getUuid(), backupOfferingVO.getUuid())); + } + } + + protected boolean offeringSupportsValidation(InternalBackupJoinVO backup) { + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + BackupOfferingDetailsVO detail = backupOfferingDetailsDao.findDetail(backupOfferingVO.getId(), ApiConstants.VALIDATE); + if (detail == null || !Boolean.parseBoolean(detail.getValue())) { + logger.debug("Backup [{}] will not be validated as offering [{}] does not support it.", backup, backupOfferingVO.getUuid()); + return false; + } + return true; + } + + protected boolean offeringSupportsCompression(InternalBackupJoinVO backup) { + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + BackupOfferingDetailsVO detail = backupOfferingDetailsDao.findDetail(backupOfferingVO.getId(), ApiConstants.COMPRESS); + if (detail == null || !Boolean.parseBoolean(detail.getValue())) { + logger.debug("Backup [{}] will not be compressed as offering [{}] does not support it.", backup, backupOfferingVO.getUuid()); + return false; + } + return true; + } + + protected void validateVmState(VirtualMachine vm, String operation, VirtualMachine.State... additionalStates) { + List allowedStates = new ArrayList<>(this.allowedVmStates); + allowedStates.addAll(Arrays.asList(additionalStates)); + if (!allowedStates.contains(vm.getState())) { + throw new BackupProviderException(String.format("VM [%s] is not in the right state to %s. It must be in one of these states: %s", vm.getUuid(), operation, + allowedStates)); + } + } + + protected Pair validateCompressionStateForRestoreAndGetBackup(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback>) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to get lock on backup [{}]. Cannot restore it.", backupId); + return new Pair<>(false, null); + } + + if (backupVO.getCompressionStatus() == Backup.CompressionStatus.FinalizingCompression) { + logger.error("We cannot restore backups that are finalizing the compression process. Please wait for the process to end and try again later.", + allowedBackupStatesToCompress, backupVO.getStatus()); + return new Pair<>(false, null); + } + backupVO.setStatus(Backup.Status.Restoring); + backupDao.update(backupId, backupVO); + return new Pair<>(true, backupVO); + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + /** + * Validates that the backup is in a valid state. This is synchronized with the backup compression check. We get a new backup reference to make sure the compression has not + * changed the backup compression state. + * */ + protected boolean validateBackupStateForRemoval(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to acquire lock for backup [{}]. Cannot remove it.", backupId); + return false; + } + + if (!allowedBackupStatesToRemove.contains(backupVO.getStatus())) { + logger.error("Backup [{}] is not in a state allowed to be removed. Current state is [{}]; allowed states are [{}]", backupVO, backupVO.getStatus(), + allowedBackupStatesToRemove); + return false; + } + + if (Backup.CompressionStatus.Compressing.equals(backupVO.getCompressionStatus())) { + logger.error("Backup [{}] is being compressed, we cannot delete it. Please wait for the compress process to end and try again later.", backupVO.getUuid()); + return false; + } + + if (Backup.ValidationStatus.Validating.equals(backupVO.getValidationStatus())) { + logger.error("Backup [{}] is being validated, we cannot delete it. Please wait for the validation process to end and try again later."); + return false; + } + return true; + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + /** + * Validates that the backup is in a valid state to start the compression. This is synchronized with the backup removal check. We get a new backup reference to make sure the + * delete process has not changed the backup state. + * */ + protected Pair validateBackupStateForStartCompressionAndUpdateCompressionStatus(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback>) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to get lock on backup [{}]. Will abort the start of the compression process. We might try again later.", backupId); + return new Pair<>(false, null); + } + + if (!allowedBackupStatesToCompress.contains(backupVO.getStatus())) { + logger.error("We can only compress backups that are on states [{}]. Current backup state is [{}].", allowedBackupStatesToCompress, backupVO.getStatus()); + return new Pair<>(false, null); + } + + logger.info("Compressing backup [{}].", backupVO.getUuid()); + backupVO.setCompressionStatus(Backup.CompressionStatus.Compressing); + backupDao.update(backupVO.getId(), backupVO); + return new Pair<>(true, backupVO); + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + /** + * Validates that the backup is in a valid state to finalize the compression. This is synchronized with the backup restore check. We get a new backup reference to make sure + * the restore process has not changed the backup state. + * */ + protected Pair validateBackupStateForFinalizeCompression(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback>) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to get lock on backup [{}]. Will abort the finalize compression process. We might try again later.", backupId); + return new Pair<>(false, null); + } + + List children = getBackupJoinChildren(backupVO); + if (Backup.Status.Restoring == backupVO.getStatus() || children.stream().anyMatch(backup -> backup.getStatus() == Backup.Status.Restoring)) { + logger.warn( + "Backup [{}] not in right state to finish compression. We can only finish compression process if backup is in [{}] state and no children are being " + "restored. Will try again later", + backupVO, Backup.Status.BackedUp); + return new Pair<>(false, null); + } + + if (Backup.Status.BackedUp == backupVO.getStatus()) { + logger.info("Backup [{}] is in the right state to finish compression. Will start the process.", backupVO.getUuid()); + backupVO.setCompressionStatus(Backup.CompressionStatus.FinalizingCompression); + backupDao.update(backupId, backupVO); + } else { + logger.warn("Backup [{}] is in [{}] state. Aborting compression and cleaning up compressed data. We can only finish compression process if backup is in [{}] " + + "state.", backupVO.getUuid(), backupVO.getStatus(), Backup.Status.BackedUp); + backupVO.setCompressionStatus(Backup.CompressionStatus.CompressionError); + backupDao.update(backupId, backupVO); + } + return new Pair<>(true, backupVO); + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + protected Pair validateBackupStateForRestoreBackupToVM(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback>) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to get lock on backup [{}]. Cannot create VM from this backup right now.", backupId); + return new Pair<>(false, null); + } + + if (Backup.Status.BackedUp == backupVO.getStatus() || Backup.Status.Restoring == backupVO.getStatus()) { + logger.debug("Backup [{}] is in the right state to create VM from it. Will start the process.", backupVO.getUuid()); + Backup.Status oldStatus = backupVO.getStatus(); + backupVO.setStatus(Backup.Status.Restoring); + backupDao.update(backupId, backupVO); + return new Pair<>(true, oldStatus); + } else { + logger.warn("Backup [{}] is in [{}] state. Aborting VM creation from backup. We can only create VM from backup if backup is in [{}] state.", + backupVO.getUuid(), backupVO.getStatus(), List.of(Backup.Status.BackedUp, Backup.Status.Restoring)); + return new Pair<>(false, null); + } + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + /** + * Validates that the backup is in a valid state to validate. This is synchronized with the backup removal check. We get a new backup reference to make sure the removal process + * has not changed the backup state. + * */ + protected boolean validateBackupStateForValidation(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to acquire lock for backup [{}]. Cannot validate it. It might have been removed.", backupId); + return false; + } + + if (!allowedBackupStatesToValidate.contains(backupVO.getStatus())) { + logger.error("Backup [{}] is not in a state allowed to be validated. Current state is [{}]; allowed states are [{}]", backupVO, backupVO.getStatus(), + allowedBackupStatesToValidate); + return false; + } + return true; + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + protected void validateStorages(List volumeTOs, String vmUuid) { + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + StoragePoolVO storagePoolVO = storagePoolDao.findById(volumeObjectTO.getPoolId()); + if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) { + logger.error("Only able to take backups of VMs with volumes in the following storage types [{}]. Throwing an exception.", supportedStoragePoolTypes); + throw new BackupProviderException(String.format("Unable to take backup of VM [%s], please check the logs.", vmUuid)); + } + } + } + + protected void validateNoVmSnapshots(VirtualMachine vm) { + List vmSnapshotVOs = vmSnapshotDao.findByVm(vm.getId()); + if (!vmSnapshotVOs.isEmpty()) { + throw new BackupProviderException(String.format("Restoring VM [%s] would remove the current VM snapshots it has. Please remove the VM snapshots [%s] before" + + " restoring the backup.", vm.getUuid(), vmSnapshotVOs.stream().map(VMSnapshotVO::getUuid).collect(Collectors.toList()))); + } + } + + protected BackupVO lockBackup(long backupId) { + return backupDao.acquireInLockTable(backupId, 300); + } + + protected void releaseBackup(long backupId) { + backupDao.releaseFromLockTable(backupId); + } + + protected void transitVmState(VirtualMachine vm, VirtualMachine.Event event, long hostId) { + try { + virtualMachineManager.stateTransitTo(vm, event, hostId); + } catch (NoTransitionException e) { + String msg = String.format("Failed to change VM [%s] state with event [%s].", vm.getUuid(), event.toString()); + logger.error(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + + protected void transitVolumeState(Volume volume, Volume.Event event) { + try { + volumeApiService.stateTransitTo(volume, event); + } catch (NoTransitionException e) { + throw new CloudRuntimeException(e); + } + } +} diff --git a/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/module.properties b/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/module.properties new file mode 100644 index 000000000000..33c86662ed69 --- /dev/null +++ b/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/module.properties @@ -0,0 +1,18 @@ +# 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. +name=kboss +parent=backup diff --git a/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/spring-backup-kboss-context.xml b/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/spring-backup-kboss-context.xml new file mode 100644 index 000000000000..da6600e6a7d2 --- /dev/null +++ b/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/spring-backup-kboss-context.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/plugins/backup/kboss/src/test/java/org/apache/cloudstack/backup/KbossBackupProviderTest.java b/plugins/backup/kboss/src/test/java/org/apache/cloudstack/backup/KbossBackupProviderTest.java new file mode 100644 index 000000000000..79276bb96e22 --- /dev/null +++ b/plugins/backup/kboss/src/test/java/org/apache/cloudstack/backup/KbossBackupProviderTest.java @@ -0,0 +1,2993 @@ +// 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.backup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; +import org.apache.cloudstack.backup.dao.InternalBackupDataStoreDao; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.backup.dao.InternalBackupServiceJobDao; +import org.apache.cloudstack.backup.dao.InternalBackupStoragePoolDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.secstorage.heuristics.HeuristicType; +import org.apache.cloudstack.storage.command.BackupDeleteAnswer; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.heuristics.HeuristicRuleHelper; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.storage.vmsnapshot.VMSnapshotHelper; +import org.apache.cloudstack.storage.volume.VolumeObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.manager.Commands; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.resource.ResourceState; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeApiService; +import com.cloud.storage.VolumeApiServiceImpl; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; +import com.cloud.utils.exception.BackupProviderException; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.UserVmManager; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceDetailVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; +import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; + +@RunWith(MockitoJUnitRunner.class) +public class KbossBackupProviderTest { + + @Mock + private VirtualMachine virtualMachineMock; + + @Mock + private BackupOfferingVO backupOfferingMock; + + @Mock + private VolumeDao volumeDaoMock; + + @Mock + private VolumeVO volumeVoMock; + + @Mock + private VMSnapshotDao vmSnapshotDaoMock; + + @Mock + private VMSnapshotVO vmSnapshotVoMock; + + @Mock + private VMSnapshotDetailsDao vmSnapshotDetailsDaoMock; + + @Mock + private VMSnapshotDetailsVO vmSnapshotDetailsVoMock; + + @Mock + private InternalBackupJoinDao internalBackupJoinDaoMock; + + @Mock + private InternalBackupJoinVO internalBackupJoinVoMock; + + @Mock + private InternalBackupDataStoreDao internalBackupDataStoreDaoMock; + + @Mock + private InternalBackupDataStoreVO internalBackupDataStoreVoMock; + + @Mock + private InternalBackupStoragePoolDao internalBackupStoragePoolDaoMock; + + @Mock + private BackupVO backupVoMock; + + @Mock + private BackupDetailsDao backupDetailDaoMock; + + @Mock + private BackupDetailVO backupDetailVoMock; + + @Mock + private DataStoreManager dataStoreManagerMock; + + @Mock + private DataStore dataStoreMock; + + @Mock + private HeuristicRuleHelper heuristicRuleHelperMock; + + @Mock + private VMSnapshotHelper vmSnapshotHelperMock; + + @Mock + private BackupDao backupDaoMock; + + @Mock + private VolumeObjectTO volumeObjectToMock; + + @Mock + private VirtualMachineManager virtualMachineManagerMock; + + @Mock + private HostDao hostDaoMock; + + @Mock + private HostVO hostVOMock; + + @Mock + private UserVmDao userVmDaoMock; + + @Mock + private VMInstanceDetailsDao vmInstanceDetailsDaoMock; + + @Mock + private VMInstanceDetailVO vmInstanceDetailVoMock; + + @Mock + private UserVmVO userVmVOMock; + + @Mock + private BackupOfferingDao backupOfferingDaoMock; + + @Mock + private AgentManager agentManagerMock; + + @Mock + private TakeKbossBackupAnswer takeKbossBackupAnswerMock; + + @Mock + private EndPointSelector endPointSelectorMock; + + @Mock + private EndPoint endPointMock; + + @Mock + private Backup.VolumeInfo backupVolumeInfoMock; + + @Mock + private VolumeInfo volumeInfoMock; + + @Mock + private VolumeApiService volumeApiServiceMock; + + @Mock + private VolumeDataFactory volumeDataFactoryMock; + + @Mock + private BackupOfferingDetailsDao backupOfferingDetailsDaoMock; + + @Mock + private BackupOfferingDetailsVO backupOfferingDetailsVoMock; + + @Mock + private Answer answerMock; + + @Mock + private InternalBackupServiceJobDao internalBackupServiceJobDaoMock; + + @Mock + private UserVmManager userVmManagerMock; + + @Mock + private HypervisorGuruManager hypervisorGuruManagerMock; + @Mock + private HypervisorGuru hypervisorGuruMock; + @Mock + private VirtualMachineTO virtualMachineToMock; + @Mock + private ImageStoreDao imageStoreDaoMock; + + @Mock + private VolumeObject volumeObjectMock; + + @Mock + private InternalBackupStoragePoolVO internalBackupStoragePoolVoMock; + + @Mock + private BackupDeltaTO backupDeltaToMock; + + @Mock + private DeltaMergeTreeTO deltaMergeTreeToMock; + + @Spy + @InjectMocks + private KbossBackupProvider kbossBackupProviderSpy; + + private long vmId = 319832; + private long volumeId = 41; + + private Long backupId = 312L; + + @Before + public void setup() { + doReturn(vmId).when(virtualMachineMock).getId(); + doReturn(vmId).when(backupVoMock).getVmId(); + doReturn(vmId).when(internalBackupJoinVoMock).getVmId(); + doReturn(vmId).when(userVmVOMock).getId(); + doReturn(backupId).when(backupVoMock).getId(); + } + + + @Test + public void assignVMToBackupOfferingTestNotKvm() { + doReturn(Hypervisor.HypervisorType.Any).when(virtualMachineMock).getHypervisorType(); + boolean result = kbossBackupProviderSpy.assignVMToBackupOffering(virtualMachineMock, backupOfferingMock); + assertFalse(result); + } + + @Test + public void assignVMToBackupOfferingTestKvmWithUnsupportedDiskOnlyVmSnapshot() { + doReturn(Hypervisor.HypervisorType.KVM).when(virtualMachineMock).getHypervisorType(); + doReturn(List.of(vmSnapshotVoMock)).when(vmSnapshotDaoMock).findByVmAndByType(vmId, VMSnapshot.Type.Disk); + long vmSnapId = 921; + doReturn(vmSnapId).when(vmSnapshotVoMock).getId(); + doReturn(List.of(vmSnapshotDetailsVoMock)).when(vmSnapshotDetailsDaoMock).listDetails(vmSnapId); + doReturn("Anything").when(vmSnapshotDetailsVoMock).getName(); + + boolean result = kbossBackupProviderSpy.assignVMToBackupOffering(virtualMachineMock, backupOfferingMock); + assertFalse(result); + } + + @Test + public void assignVMToBackupOfferingTestKvmWithSupportedDiskOnlyVmSnapshotAndDiskAndMemoryVmSnapshot() { + doReturn(Hypervisor.HypervisorType.KVM).when(virtualMachineMock).getHypervisorType(); + doReturn(List.of(vmSnapshotVoMock)).when(vmSnapshotDaoMock).findByVmAndByType(vmId, VMSnapshot.Type.Disk); + long vmSnapId = 921; + doReturn(vmSnapId).when(vmSnapshotVoMock).getId(); + doReturn(List.of(vmSnapshotDetailsVoMock)).when(vmSnapshotDetailsDaoMock).listDetails(vmSnapId); + doReturn(VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT).when(vmSnapshotDetailsVoMock).getName(); + doReturn(List.of(vmSnapshotVoMock)).when(vmSnapshotDaoMock).findByVmAndByType(vmId, VMSnapshot.Type.DiskAndMemory); + + boolean result = kbossBackupProviderSpy.assignVMToBackupOffering(virtualMachineMock, backupOfferingMock); + assertFalse(result); + } + + + @Test + public void assignVMToBackupOfferingTestKvmWithSupportedDiskOnlyVmSnapshotAndNoDiskAndMemoryVmSnapshot() { + doReturn(Hypervisor.HypervisorType.KVM).when(virtualMachineMock).getHypervisorType(); + doReturn(List.of(vmSnapshotVoMock)).when(vmSnapshotDaoMock).findByVmAndByType(vmId, VMSnapshot.Type.Disk); + long vmSnapId = 921; + doReturn(vmSnapId).when(vmSnapshotVoMock).getId(); + doReturn(List.of(vmSnapshotDetailsVoMock)).when(vmSnapshotDetailsDaoMock).listDetails(vmSnapId); + doReturn(VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT).when(vmSnapshotDetailsVoMock).getName(); + + boolean result = kbossBackupProviderSpy.assignVMToBackupOffering(virtualMachineMock, backupOfferingMock); + assertTrue(result); + } + + @Test + public void removeVMFromBackupOfferingTestNoActiveChain() { + doReturn(VirtualMachine.State.Running).when(virtualMachineMock).getState(); + + boolean result = kbossBackupProviderSpy.removeVMFromBackupOffering(virtualMachineMock); + + verify(kbossBackupProviderSpy, Mockito.never()).mergeCurrentBackupDeltas(any()); + assertTrue(result); + } + + @Test + public void removeVMFromBackupOfferingTestWithActiveChain() { + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(vmId, true); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + + boolean result = kbossBackupProviderSpy.removeVMFromBackupOffering(virtualMachineMock); + + verify(kbossBackupProviderSpy, Mockito.times(1)).mergeCurrentBackupDeltas(any()); + assertTrue(result); + } + + @Test + public void getBackupJoinParentsTestIncludeRemovedEmptyList() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + doReturn(null).when(backupVoMock).getBackupScheduleId(); + doReturn(new ArrayList<>()).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, null, date); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); + + assertTrue(result.isEmpty()); + } + + @Test + public void getBackupJoinParentsTestIncludeRemovedAncestorIsEndOfChain() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + doReturn(null).when(backupVoMock).getBackupScheduleId(); + doReturn(true).when(internalBackupJoinVoMock).getEndOfChain(); + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, null, date); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); + + assertTrue(result.isEmpty()); + } + + @Test + public void getBackupJoinParentsTestIncludeRemovedAncestorMultipleAncestors() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + doReturn(null).when(backupVoMock).getBackupScheduleId(); + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock1).getEndOfChain(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock2).getEndOfChain(); + doReturn(true).when(internalBackupJoinVoMock).getEndOfChain(); + doReturn(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, null, date); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); + + assertEquals(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2), result); + } + + @Test + public void getBackupJoinParentsTestIncludeRemovedAncestorMultipleAncestorsNoEndOfChain() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + doReturn(null).when(backupVoMock).getBackupScheduleId(); + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock1).getEndOfChain(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock2).getEndOfChain(); + doReturn(false).when(internalBackupJoinVoMock).getEndOfChain(); + doReturn(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, null, date); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); + + assertEquals(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock), result); + } + + @Test + public void getBackupJoinParentsTestNoRemovedAncestorMultipleAncestorsNoEndOfChain() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + doReturn(null).when(backupVoMock).getBackupScheduleId(); + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock1).getEndOfChain(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock2).getEndOfChain(); + doReturn(false).when(internalBackupJoinVoMock).getEndOfChain(); + doReturn(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(vmId, null, date, true, + false); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, false); + + assertEquals(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock), result); + } + + @Test + public void setEndOfChainTrueIfRemainingChainSizeIsOneTestChainSizeLowerThanOneAndConfigIsZero() { + int chainSize = 0; + kbossBackupProviderSpy.setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(-1, chainSize, 1, "uuid"); + + verify(backupDetailDaoMock, Mockito.never()).persist(any()); + } + + @Test + public void setEndOfChainTrueIfRemainingChainSizeIsOneTestChainSizeLowerThanOneAndConfigBiggerThanZero() { + int chainSize = 1; + kbossBackupProviderSpy.setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(-1, chainSize, 1, "uuid"); + + verify(backupDetailDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void setEndOfChainTrueIfRemainingChainSizeIsOneTestChainSizeBiggerThanOne() { + kbossBackupProviderSpy.setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(2, 0, 1, "uuid"); + + verify(backupDetailDaoMock, Mockito.never()).persist(any()); + } + + @Test + public void setEndOfChainTrueIfRemainingChainSizeIsOneTestChainSizeIsOne() { + int chainSize = 2; + kbossBackupProviderSpy.setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(1, chainSize, 1, "uuid"); + + verify(backupDetailDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void getParentAndSetEndOfChainTestBackupChainIsEmpty() { + int chainSize = 2; + doReturn(chainSize).when(kbossBackupProviderSpy).getChainSizeForBackup(any(), Mockito.anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + + InternalBackupJoinVO result = kbossBackupProviderSpy.getParentAndSetEndOfChain(backupVoMock, List.of(), null); + + assertNull(result); + verify(kbossBackupProviderSpy, Mockito.times(1)).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), + any()); + } + + @Test + public void getParentAndSetEndOfChainTestBackupChainIsBiggerThanChainSize() { + int chainSize = 2; + doReturn(chainSize).when(kbossBackupProviderSpy).getChainSizeForBackup(any(), Mockito.anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock1).getStatus(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + InternalBackupJoinVO result = kbossBackupProviderSpy.getParentAndSetEndOfChain(backupVoMock, List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2), null); + + assertEquals(internalBackupJoinVoMock1, result); + verify(kbossBackupProviderSpy, Mockito.times(1)).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + } + + @Test + public void getParentAndSetEndOfChainTestBackupChainIsSmallerThanChainSize() { + int chainSize = 3; + doReturn(chainSize).when(kbossBackupProviderSpy).getChainSizeForBackup(any(), Mockito.anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock1).getStatus(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + InternalBackupJoinVO result = kbossBackupProviderSpy.getParentAndSetEndOfChain(backupVoMock, List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2), null); + + assertEquals(internalBackupJoinVoMock1, result); + verify(kbossBackupProviderSpy, Mockito.times(1)).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + } + + @Test + public void getParentAndSetEndOfChainTestBackupChainIsNotEmptyParentIsRemoved() { + int chainSize = 2; + doReturn(chainSize).when(kbossBackupProviderSpy).getChainSizeForBackup(any(), Mockito.anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.Removed).when(internalBackupJoinVoMock1).getStatus(); + InternalBackupJoinVO result = kbossBackupProviderSpy.getParentAndSetEndOfChain(backupVoMock, List.of(internalBackupJoinVoMock1), null); + + assertNull(result); + verify(kbossBackupProviderSpy, Mockito.times(1)).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + } + + @Test + public void getImageStoreForBackupTestNoHeuristic() { + long zoneId = 2; + doReturn(null).when(heuristicRuleHelperMock).getImageStoreIfThereIsHeuristicRule(zoneId, HeuristicType.BACKUP, backupVoMock); + doReturn(dataStoreMock).when(dataStoreManagerMock).getImageStoreWithFreeCapacity(zoneId); + + DataStore result = kbossBackupProviderSpy.getImageStoreForBackup(zoneId, backupVoMock); + + assertEquals(dataStoreMock, result); + } + + @Test + public void getImageStoreForBackupTestWithHeuristic() { + long zoneId = 2; + doReturn(dataStoreMock).when(heuristicRuleHelperMock).getImageStoreIfThereIsHeuristicRule(zoneId, HeuristicType.BACKUP, backupVoMock); + + DataStore result = kbossBackupProviderSpy.getImageStoreForBackup(zoneId, backupVoMock); + + assertEquals(dataStoreMock, result); + verify(dataStoreManagerMock, Mockito.never()).getImageStoreWithFreeCapacity(Mockito.anyLong()); + } + + @Test (expected = CloudRuntimeException.class) + public void getImageStoreForBackupTestNoStorageFound() { + kbossBackupProviderSpy.getImageStoreForBackup(0L, backupVoMock); + } + + @Test + public void getSucceedingVmSnapshotListTestBackupIsNull() { + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(null); + + assertTrue(result.isEmpty()); + } + + @Test + public void getSucceedingVmSnapshotListTestNoCurrentSnapshotVo() { + doReturn(null).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertTrue(result.isEmpty()); + } + + @Test + public void getSucceedingVmSnapshotListTestCurrentCreatedBeforeBackup() { + doReturn(vmSnapshotVoMock).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + Date before = DateUtil.now(); + before.setTime(before.getTime()-10000); + Date now = DateUtil.now(); + doReturn(before).when(vmSnapshotVoMock).getCreated(); + doReturn(now).when(internalBackupJoinVoMock).getDate(); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertTrue(result.isEmpty()); + } + + @Test + public void getSucceedingVmSnapshotListTestCurrentVmSnapshotHasNoParent() { + doReturn(vmSnapshotVoMock).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + Date before = DateUtil.now(); + before.setTime(before.getTime()-10000); + Date now = DateUtil.now(); + doReturn(now).when(vmSnapshotVoMock).getCreated(); + doReturn(before).when(internalBackupJoinVoMock).getDate(); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertEquals(1, result.size()); + assertEquals(vmSnapshotVoMock, result.get(0)); + } + + @Test + public void getSucceedingVmSnapshotListTestCurrentVmSnapshotHasParentsCreatedAfter() { + doReturn(vmSnapshotVoMock).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + Date before = DateUtil.now(); + before.setTime(before.getTime()-10000); + Date now = DateUtil.now(); + doReturn(now).when(vmSnapshotVoMock).getCreated(); + doReturn(before).when(internalBackupJoinVoMock).getDate(); + long snapParentId = 909; + doReturn(snapParentId).when(vmSnapshotVoMock).getParent(); + VMSnapshotVO vmSnapshotVoMock1 = Mockito.mock(VMSnapshotVO.class); + doReturn(now).when(vmSnapshotVoMock1).getCreated(); + doReturn(vmSnapshotVoMock1).when(vmSnapshotDaoMock).findById(snapParentId); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertEquals(List.of(vmSnapshotVoMock1, vmSnapshotVoMock), result); + } + + + @Test + public void getSucceedingVmSnapshotListTestCurrentVmSnapshotHasParentsCreatedBefore() { + doReturn(vmSnapshotVoMock).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + Date before = DateUtil.now(); + before.setTime(before.getTime() - 10000); + Date now = DateUtil.now(); + doReturn(now).when(vmSnapshotVoMock).getCreated(); + doReturn(before).when(internalBackupJoinVoMock).getDate(); + long snapParentId = 909; + doReturn(snapParentId).when(vmSnapshotVoMock).getParent(); + VMSnapshotVO vmSnapshotVoMock1 = Mockito.mock(VMSnapshotVO.class); + Date evenBefore = new Date(before.getTime() - 10000); + doReturn(evenBefore).when(vmSnapshotVoMock1).getCreated(); + doReturn(vmSnapshotVoMock1).when(vmSnapshotDaoMock).findById(snapParentId); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertEquals(List.of(vmSnapshotVoMock), result); + } + + @Test + public void mapVolumesToVmSnapshotReferencesTestVmSnapshotAndBackupVOListIsEmpty() { + kbossBackupProviderSpy.mapVolumesToVmSnapshotAndBackupReferences(List.of(), List.of(), List.of()); + + verify(vmSnapshotHelperMock, Mockito.never()).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(1); + } + + @Test + public void mapVolumesToVmSnapshotAndBackupReferencesTestVmSnapshotAndBackupVOListHasTwoElements() { + VMSnapshotVO vmSnapshotVoMock1 = Mockito.mock(VMSnapshotVO.class); + doReturn(1L).when(vmSnapshotVoMock).getId(); + doReturn(2L).when(vmSnapshotVoMock1).getId(); + + kbossBackupProviderSpy.mapVolumesToVmSnapshotAndBackupReferences(List.of(), List.of(vmSnapshotVoMock, vmSnapshotVoMock1), List.of()); + + verify(vmSnapshotHelperMock, times(1)).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(1); + verify(vmSnapshotHelperMock, times(1)).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(2); + } + + @Test + public void createDeltaReferencesTestFullBackupEndOfChain() { + doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); + + kbossBackupProviderSpy.createDeltaReferences(true, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, new KbossTO(volumeObjectToMock, + new LinkedList<>())); + + verify(internalBackupDataStoreDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void createDeltaReferencesTestIsolatedBackup() { + doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); + + kbossBackupProviderSpy.createDeltaReferences(true, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, new KbossTO(volumeObjectToMock, + new LinkedList<>())); + + verify(internalBackupDataStoreDaoMock, Mockito.times(1)).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(0)).findAndSetParentBackupPath(any(), any(), any()); + verify(kbossBackupProviderSpy, Mockito.times(0)).findAndSetParentBackupPath(any(), any(), any()); + verify(internalBackupStoragePoolDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void createDeltaReferencesTestNotFullBackupEndOfChain() { + doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); + KbossTO kbossTO = new KbossTO(volumeObjectToMock, new LinkedList<>()); + doReturn(null).when(kbossBackupProviderSpy).createDeltaMergeTreeForVolume(false, true, List.of(), null, kbossTO, List.of()); + doNothing().when(kbossBackupProviderSpy).findAndSetParentBackupPath(List.of(), null, kbossTO); + + kbossBackupProviderSpy.createDeltaReferences(false, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, kbossTO); + + verify(internalBackupDataStoreDaoMock, Mockito.times(1)).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).findAndSetParentBackupPath(List.of(), null, kbossTO); + } + + @Test + public void createDeltaReferencesTestFullBackupNotEndOfChainDoesNotHaveVmSnapshotSucceedingLastBackup() { + doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); + + kbossBackupProviderSpy.createDeltaReferences(true, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, new KbossTO(volumeObjectToMock, + new LinkedList<>())); + + verify(internalBackupDataStoreDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void orchestrateTakeBackupTestHostIsDownReturnFalse() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Down); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, false); + assertFalse(result.first()); + } + + @Test + public void orchestrateTakeBackupTestHostIsDisconnectedReturnFalse() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Disconnected); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, false); + assertFalse(result.first()); + } + + @Test (expected = BackupProviderException.class) + public void orchestrateTakeBackupTestInitialValidationThrowException() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Up); + Mockito.when(hostVOMock.getResourceState()).thenReturn(ResourceState.Enabled); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any()); + Mockito.doThrow(new BackupProviderException("tst")).when(kbossBackupProviderSpy).validateStorages(any(), any()); + + kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, false); + assertEquals(Backup.Status.Failed, backupVoMock.getStatus()); + Mockito.verify(backupDaoMock, Mockito.times(1)).update(Mockito.anyLong(), any()); + } + + @Test + public void orchestrateTakeBackupTestIsolatedBackupFailed() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Up); + Mockito.when(hostVOMock.getResourceState()).thenReturn(ResourceState.Enabled); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any()); + + VolumeObjectTO vol1 = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO vol2 = Mockito.mock(VolumeObjectTO.class); + doReturn(List.of(vol1, vol2)).when(vmSnapshotHelperMock).getVolumeTOList(Mockito.anyLong()); + + doNothing().when(kbossBackupProviderSpy).validateStorages(any(), any()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(any()); + doReturn(dataStoreMock).when(kbossBackupProviderSpy).getImageStoreForBackup(any(), any()); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, true); + assertFalse(result.first()); + assertNull(result.second()); + verify(kbossBackupProviderSpy, Mockito.times(1)).setBackupAsIsolated(backupVoMock); + verify(kbossBackupProviderSpy, Mockito.times(2)).createDeltaReferences(Mockito.anyBoolean(), Mockito.anyBoolean(), any(), any(), any(), any(), any(), any(), any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).processBackupFailure(any(), any(), Mockito.anyLong(), Mockito.anyBoolean(), any()); + } + + @Test + public void orchestrateTakeBackupTestIsolatedBackupSuccessWithCompression() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Up); + Mockito.when(hostVOMock.getResourceState()).thenReturn(ResourceState.Enabled); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any()); + + VolumeObjectTO vol1 = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO vol2 = Mockito.mock(VolumeObjectTO.class); + doReturn(List.of(vol1, vol2)).when(vmSnapshotHelperMock).getVolumeTOList(Mockito.anyLong()); + + doNothing().when(kbossBackupProviderSpy).validateStorages(any(), any()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(any()); + doReturn(dataStoreMock).when(kbossBackupProviderSpy).getImageStoreForBackup(any(), any()); + doReturn(takeKbossBackupAnswerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(takeKbossBackupAnswerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).processBackupSuccess(anyBoolean(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any(), + anyLong(), anyBoolean(), anyBoolean(), any()); + doReturn(true).when(kbossBackupProviderSpy).offeringSupportsCompression(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).compressBackupAsync(internalBackupJoinVoMock, 0, 0); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, true); + assertTrue(result.first()); + assertEquals(backupId, result.second()); + verify(kbossBackupProviderSpy, Mockito.times(1)).setBackupAsIsolated(backupVoMock); + verify(kbossBackupProviderSpy, Mockito.times(2)).createDeltaReferences(Mockito.anyBoolean(), Mockito.anyBoolean(), any(), any(), any(), any(), any(), any(), any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).processBackupSuccess(anyBoolean(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any(), + anyLong(), anyBoolean(), anyBoolean(), any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).compressBackupAsync(internalBackupJoinVoMock, 0, 0); + } + + @Test + public void orchestrateTakeBackupTestBackupSuccessWithValidation() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Up); + Mockito.when(hostVOMock.getResourceState()).thenReturn(ResourceState.Enabled); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any()); + + VolumeObjectTO vol1 = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO vol2 = Mockito.mock(VolumeObjectTO.class); + doReturn(List.of(vol1, vol2)).when(vmSnapshotHelperMock).getVolumeTOList(Mockito.anyLong()); + + doNothing().when(kbossBackupProviderSpy).validateStorages(any(), any()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(any()); + InternalBackupJoinVO parentMock = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentMock).when(kbossBackupProviderSpy).getParentAndSetEndOfChain(any(), any(), any()); + doReturn(dataStoreMock).when(kbossBackupProviderSpy).getImageStoreForBackup(any(), any()); + doReturn(takeKbossBackupAnswerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(takeKbossBackupAnswerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).processBackupSuccess(anyBoolean(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any(), + anyLong(), anyBoolean(), anyBoolean(), any()); + doReturn(false).when(kbossBackupProviderSpy).offeringSupportsCompression(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).validateBackupAsyncIfHasOfferingSupport(any(), anyLong(), anyLong()); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, false); + assertTrue(result.first()); + assertEquals(backupId, result.second()); + verify(internalBackupStoragePoolDaoMock).listByBackupId(0); + verify(internalBackupDataStoreDaoMock).listByBackupId(0); + verify(kbossBackupProviderSpy, Mockito.times(2)).createDeltaReferences(Mockito.anyBoolean(), Mockito.anyBoolean(), any(), any(), any(), any(), any(), any(), any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).processBackupSuccess(anyBoolean(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any(), + anyLong(), anyBoolean(), anyBoolean(), any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).validateBackupAsyncIfHasOfferingSupport(internalBackupJoinVoMock, 0, 0); + } + + @Test + public void orchestrateDeleteBackupTestBackupStateIsNotOk() { + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteFailedBackup() throws OperationTimedoutException, AgentUnavailableException { + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(true).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + verify(kbossBackupProviderSpy, never()).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteBackupWithLiveChildren() throws OperationTimedoutException, AgentUnavailableException { + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findByParentId(anyLong()); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock).getStatus(); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupVoMock).setStatus(Backup.Status.Removed); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy, never()).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenFailedToMerge() throws OperationTimedoutException, AgentUnavailableException { + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(false).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(kbossBackupProviderSpy, never()).sendBackupCommands(anyLong(), any()); + verify(kbossBackupProviderSpy).mergeCurrentBackupDeltas(any()); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithParentNoEndPoint() throws OperationTimedoutException, AgentUnavailableException { + long parentBackupId = 12; + doReturn(parentBackupId).when(internalBackupJoinVoMock).getParentId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentBackupId); + doReturn(Backup.Status.BackedUp).when(parentVo).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + doReturn(null).when(kbossBackupProviderSpy).getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(any(), any()); + doReturn(null).when(endPointSelectorMock).select((DataStore)null); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupDetailDaoMock).persist(any()); + verify(kbossBackupProviderSpy, never()).sendBackupCommands(anyLong(), any()); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithParentTimedoutException() throws OperationTimedoutException, AgentUnavailableException { + long parentBackupId = 12; + doReturn(parentBackupId).when(internalBackupJoinVoMock).getParentId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentBackupId); + doReturn(Backup.Status.BackedUp).when(parentVo).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + doReturn(null).when(kbossBackupProviderSpy).getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(any(), any()); + doReturn(endPointMock).when(endPointSelectorMock).select((DataStore)null); + doThrow(OperationTimedoutException.class).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupDetailDaoMock).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithParentFailedSetNotEmpty() throws OperationTimedoutException, AgentUnavailableException { + long parentBackupId = 12; + doReturn(parentBackupId).when(internalBackupJoinVoMock).getParentId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentBackupId); + doReturn(Backup.Status.BackedUp).when(parentVo).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + doReturn(new Pair<>(List.of(), parentVo)).when(kbossBackupProviderSpy).getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(any(), any()); + doReturn(endPointMock).when(endPointSelectorMock).select((DataStore)null); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + doReturn(false).when(kbossBackupProviderSpy).processRemoveBackupFailures(anyBoolean(), any(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).processRemovedBackups(any()); + + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupDetailDaoMock, Mockito.times(2)).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithParentFailedSetEmpty() throws OperationTimedoutException, AgentUnavailableException { + long parentBackupId = 12; + doReturn(parentBackupId).when(internalBackupJoinVoMock).getParentId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentBackupId); + doReturn(Backup.Status.BackedUp).when(parentVo).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + doReturn(new Pair<>(List.of(), parentVo)).when(kbossBackupProviderSpy).getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(any(), any()); + doReturn(endPointMock).when(endPointSelectorMock).select((DataStore)null); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + doReturn(true).when(kbossBackupProviderSpy).processRemoveBackupFailures(anyBoolean(), any(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).processRemovedBackups(any()); + + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupDetailDaoMock, Mockito.times(2)).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateRestoreVMFromBackupTestInvalidState() { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(false, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, false); + + assertFalse(result); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateRestoreVMFromBackupTestCurrentBackupNoHostToRestore() throws AgentUnavailableException { // fixxxxxxx + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + doThrow(AgentUnavailableException.class).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, false, null); + + kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, false); + + verify(internalBackupStoragePoolDaoMock).listByBackupId(currentBackupId); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupTimeOut() throws AgentUnavailableException, OperationTimedoutException { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + InternalBackupJoinVO currentBackup = Mockito.mock(InternalBackupJoinVO.class); + doReturn(hostVOMock).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, false, null); + doNothing().when(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), anyBoolean()); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesThatAreNotPartOfTheBackup(any(), any()); + doReturn(List.of()).when(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + doThrow(OperationTimedoutException.class).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, true); + + verify(internalBackupStoragePoolDaoMock).listByBackupId(currentBackupId); + verify(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + verify(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + } + + @Test + public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupNullAnswers() throws AgentUnavailableException, OperationTimedoutException { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + InternalBackupJoinVO currentBackup = Mockito.mock(InternalBackupJoinVO.class); + doReturn(hostVOMock).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, false, null); + doNothing().when(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), anyBoolean()); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesThatAreNotPartOfTheBackup(any(), any()); + doReturn(List.of()).when(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, true); + + verify(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + verify(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + assertFalse(result); + } + + @Test + public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupAnswerFalse() throws AgentUnavailableException, OperationTimedoutException { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + InternalBackupJoinVO currentBackup = Mockito.mock(InternalBackupJoinVO.class); + doReturn(hostVOMock).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, false, null); + doNothing().when(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), anyBoolean()); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesThatAreNotPartOfTheBackup(any(), any()); + doReturn(List.of()).when(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + doReturn(new Answer[]{Mockito.mock(Answer.class)}).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + doReturn(false).when(kbossBackupProviderSpy).processRestoreAnswers(any(), any(), anyBoolean()); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, true); + + verify(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + verify(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + assertFalse(result); + } + + @Test + public void orchestrateRestoreVMFromBackupTestSameVmQuickRestoreCurrentBackupAnswerTrue() throws AgentUnavailableException, OperationTimedoutException { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, true); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + InternalBackupJoinVO currentBackup = Mockito.mock(InternalBackupJoinVO.class); + doReturn(hostVOMock).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, true, null); + doNothing().when(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), anyBoolean()); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesThatAreNotPartOfTheBackup(any(), any()); + doReturn(List.of()).when(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + doReturn(new Answer[]{Mockito.mock(Answer.class)}).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + doReturn(true).when(kbossBackupProviderSpy).processRestoreAnswers(any(), any(), anyBoolean()); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesToConsolidate(any(), any(), any(), anyLong(), anyBoolean()); + doReturn(true).when(kbossBackupProviderSpy).finalizeQuickRestore(any(), anyList(), anyLong()); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, true, null, true); + + verify(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + verify(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + verify(kbossBackupProviderSpy).updateVolumePathsAndSizeIfNeeded(any(), any(), anyList(), anyList(), anyBoolean()); + verify(kbossBackupProviderSpy).finalizeQuickRestore(any(), anyList(), anyLong()); + assertTrue(result); + } + + @Test + public void orchestrateRestoreBackedUpVolumeTestInvalidState() { + doReturn(new Pair<>(false, null)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + + Pair result = kbossBackupProviderSpy.orchestrateRestoreBackedUpVolume(backupVoMock, virtualMachineMock, null, null, false); + + assertFalse(result.first()); + verify(kbossBackupProviderSpy, Mockito.never()).sendBackupCommand(anyLong(), any()); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateRestoreBackedUpVolumeTestAnswerFalse() { + doReturn(new Pair<>(true, null)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + doReturn(volumeVoMock).when(volumeDaoMock).findByUuidIncludingRemoved(any()); + doReturn(hostVOMock).when(hostDaoMock).findByIp(any()); + doReturn(volumeInfoMock).when(kbossBackupProviderSpy).duplicateAndCreateVolume(virtualMachineMock, hostVOMock, backupVolumeInfoMock); + doReturn(volumeObjectToMock).when(volumeInfoMock).getTO(); + doReturn(new Pair(null, null)).when(kbossBackupProviderSpy).generateBackupAndVolumePairForSingleNewVolume(any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).getParentSecondaryStorageUrls(backupVoMock); + doReturn(false).when(kbossBackupProviderSpy).processRestoreAnswers(any(), any(), anyBoolean()); + + Pair result = kbossBackupProviderSpy.orchestrateRestoreBackedUpVolume(backupVoMock, virtualMachineMock, backupVolumeInfoMock, null, false); + + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommand(anyLong(), any()); + } + + @Test + public void orchestrateRestoreBackedUpVolumeTestQuickRestoreAnswerTrue() { + doReturn(new Pair<>(true, null)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + doReturn(volumeVoMock).when(volumeDaoMock).findByUuidIncludingRemoved(any()); + doReturn(hostVOMock).when(hostDaoMock).findByIp(any()); + doReturn(volumeInfoMock).when(kbossBackupProviderSpy).duplicateAndCreateVolume(virtualMachineMock, hostVOMock, backupVolumeInfoMock); + doReturn(volumeObjectToMock).when(volumeInfoMock).getTO(); + doReturn(new Pair(null, null)).when(kbossBackupProviderSpy).generateBackupAndVolumePairForSingleNewVolume(any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).getParentSecondaryStorageUrls(backupVoMock); + doReturn(true).when(kbossBackupProviderSpy).processRestoreAnswers(any(), any(), anyBoolean()); + doReturn(volumeVoMock).when(volumeInfoMock).getVolume(); + doReturn(volumeVoMock).when(volumeApiServiceMock).attachVolumeToVM(anyLong(), anyLong(), any(), anyBoolean(), anyBoolean()); + doReturn(true).when(kbossBackupProviderSpy).finalizeQuickRestore(any(), anyList(), anyLong()); + + Pair result = kbossBackupProviderSpy.orchestrateRestoreBackedUpVolume(backupVoMock, virtualMachineMock, backupVolumeInfoMock, null, true); + + assertTrue(result.first()); + + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommand(anyLong(), any()); + verify(volumeApiServiceMock).attachVolumeToVM(anyLong(), anyLong(), any(), anyBoolean(), anyBoolean()); + verify(kbossBackupProviderSpy).finalizeQuickRestore(any(), anyList(), anyLong()); + } + + @Test + public void startBackupCompressionTestInvalidState() { + doReturn(new Pair<>(false, null)).when(kbossBackupProviderSpy).validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + boolean result = kbossBackupProviderSpy.startBackupCompression(backupId, 0); + + assertFalse(result); + } + + @Test + public void startBackupCompressionTestNullAnswer() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + long parentId = 1332; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of(internalBackupDataStoreVoMock)).when(internalBackupDataStoreDaoMock).listByBackupId(backupId); + InternalBackupDataStoreVO parentDelta = mock(InternalBackupDataStoreVO.class); + doReturn(List.of(parentDelta)).when(internalBackupDataStoreDaoMock).listByBackupId(parentId); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(anyLong()); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(anyLong(), any()); + doReturn("zstd").when(backupOfferingDetailsVoMock).getValue(); + doReturn(List.of(parentVo)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(null).when(kbossBackupProviderSpy).getChainImageStoreUrls(any()); + doReturn(null).when(agentManagerMock).easySend(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.startBackupCompression(backupId, 0); + + assertFalse(result); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.CompressionError); + verify(backupDaoMock).update(backupId, backupVoMock); + } + + @Test + public void startBackupCompressionTestSuccess() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + long parentId = 1332; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of(internalBackupDataStoreVoMock)).when(internalBackupDataStoreDaoMock).listByBackupId(backupId); + InternalBackupDataStoreVO parentDelta = mock(InternalBackupDataStoreVO.class); + doReturn(List.of(parentDelta)).when(internalBackupDataStoreDaoMock).listByBackupId(parentId); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(anyLong()); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(anyLong(), any()); + doReturn("zstd").when(backupOfferingDetailsVoMock).getValue(); + doReturn(List.of(parentVo)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(null).when(kbossBackupProviderSpy).getChainImageStoreUrls(any()); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + + boolean result = kbossBackupProviderSpy.startBackupCompression(backupId, 0); + + assertTrue(result); + verify(internalBackupServiceJobDaoMock).persist(any()); + } + + @Test + public void finalizeBackupCompressionTestInvalidState() { + doReturn(new Pair<>(false, null)).when(kbossBackupProviderSpy).validateBackupStateForFinalizeCompression(backupId); + + boolean result = kbossBackupProviderSpy.finalizeBackupCompression(backupId, 0); + + assertFalse(result); + } + + @Test + public void finalizeBackupCompressionTestNullAnswer() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForFinalizeCompression(backupId); + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(hostVOMock).when(hostDaoMock).findById(0L); + doReturn(null).when(agentManagerMock).easySend(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.finalizeBackupCompression(backupId, 0); + + assertFalse(result); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.CompressionError); + verify(backupDaoMock).update(backupId, backupVoMock); + } + + @Test + public void finalizeBackupCompressionTestCleanup() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForFinalizeCompression(backupId); + doReturn(Backup.Status.Removed).when(backupVoMock).getStatus(); + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(hostVOMock).when(hostDaoMock).findById(0L); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + + boolean result = kbossBackupProviderSpy.finalizeBackupCompression(backupId, 0); + + assertTrue(result); + verify(backupVoMock, Mockito.never()).setCompressionStatus(Backup.CompressionStatus.Compressed); + verify(backupDaoMock, Mockito.never()).update(backupId, backupVoMock); + } + + @Test + public void finalizeBackupCompressionTestSuccess() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForFinalizeCompression(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(hostVOMock).when(hostDaoMock).findById(0L); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn("1").when(answerMock).getDetails(); + doNothing().when(kbossBackupProviderSpy).validateBackupAsyncIfHasOfferingSupport(any(), anyLong(), anyLong()); + + boolean result = kbossBackupProviderSpy.finalizeBackupCompression(backupId, 0); + + assertTrue(result); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.Compressed); + verify(backupDaoMock).update(backupId, backupVoMock); + } + + @Test + public void validateBackupTestInvalidState() { + doReturn(false).when(kbossBackupProviderSpy).validateBackupStateForValidation(backupId); + + boolean result = kbossBackupProviderSpy.validateBackup(backupId, 0); + + assertFalse(result); + } + + @Test + public void validateBackupTestValidateWithHash() { + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForValidation(backupId); + doReturn(backupVoMock).when(backupDaoMock).findById(backupId); + doReturn(backupDetailVoMock).when(backupDetailDaoMock).findDetail(backupId, BackupDetailsDao.BACKUP_HASH); + doReturn(true).when(kbossBackupProviderSpy).validateWithHash(backupId, backupVoMock, backupDetailVoMock); + + boolean result = kbossBackupProviderSpy.validateBackup(backupId, 0); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateWithHash(backupId, backupVoMock, backupDetailVoMock); + } + + @Test + public void validateBackupTestValidateWithValidationVm() { + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForValidation(backupId); + doReturn(backupVoMock).when(backupDaoMock).findById(backupId); + doReturn(null).when(backupDetailDaoMock).findDetail(backupId, BackupDetailsDao.BACKUP_HASH); + doReturn(true).when(kbossBackupProviderSpy).validateWithValidationVm(backupId, 0, backupVoMock); + + boolean result = kbossBackupProviderSpy.validateBackup(backupId, 0); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateWithValidationVm(backupId, 0, backupVoMock); + } + + @Test + public void finishBackupChainsTestInvalidState() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(VirtualMachine.State.Migrating).when(userVmVOMock).getState(); + + boolean result = kbossBackupProviderSpy.finishBackupChains(virtualMachineMock); + + assertFalse(result); + } + + @Test + public void finishBackupChainsTestRunningVm() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(VirtualMachine.State.Running).when(userVmVOMock).getState(); + doReturn(true).when(kbossBackupProviderSpy).finishAllChains(eq(userVmVOMock), any()); + + boolean result = kbossBackupProviderSpy.finishBackupChains(virtualMachineMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).finishAllChains(eq(userVmVOMock), any()); + } + + @Test + public void finishBackupChainTestBackupError() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(VirtualMachine.State.BackupError).when(userVmVOMock).getState(); + doReturn(true).when(kbossBackupProviderSpy).normalizeBackupErrorAndFinishChain(userVmVOMock); + + boolean result = kbossBackupProviderSpy.finishBackupChains(virtualMachineMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).normalizeBackupErrorAndFinishChain(userVmVOMock); + } + + @Test + public void prepareVmForSnapshotRevertTestNoCurrentBackup() { + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy, never()).getSucceedingVmSnapshot(any()); + } + + @Test + public void prepareVmForSnapshotRevertTestCurrentBackupBeforeVmSnapshot() { + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(anyLong(), anyBoolean()); + doReturn(Date.from(Instant.EPOCH)).when(internalBackupJoinVoMock).getDate(); + doReturn(Date.from(Instant.now())).when(vmSnapshotVoMock).getCreated(); + + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy, never()).getSucceedingVmSnapshot(any()); + } + + @Test (expected = CloudRuntimeException.class) + public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotTimeout() throws OperationTimedoutException, AgentUnavailableException { + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(anyLong(), anyBoolean()); + doReturn(Date.from(Instant.now())).when(internalBackupJoinVoMock).getDate(); + doReturn(Date.from(Instant.EPOCH)).when(vmSnapshotVoMock).getCreated(); + doReturn(List.of()).when(vmSnapshotHelperMock).getVolumeTOList(vmId); + doReturn(vmSnapshotVoMock).when(kbossBackupProviderSpy).getSucceedingVmSnapshot(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).createDeleteCommandsAndMergeTrees(any(), any(), any(), any(), anyList(), any()); + doThrow(OperationTimedoutException.class).when(kbossBackupProviderSpy).sendBackupCommands(any(), any()); + + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy, never()).updateReferencesAfterPrepareForSnapshotRevert(any(), any(), any(), any()); + } + + @Test (expected = CloudRuntimeException.class) + public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotNullAnswer() throws OperationTimedoutException, AgentUnavailableException { + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(anyLong(), anyBoolean()); + doReturn(Date.from(Instant.now())).when(internalBackupJoinVoMock).getDate(); + doReturn(Date.from(Instant.EPOCH)).when(vmSnapshotVoMock).getCreated(); + doReturn(List.of()).when(vmSnapshotHelperMock).getVolumeTOList(vmId); + doReturn(vmSnapshotVoMock).when(kbossBackupProviderSpy).getSucceedingVmSnapshot(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).createDeleteCommandsAndMergeTrees(any(), any(), any(), any(), anyList(), any()); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommands(any(), any()); + + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy, never()).updateReferencesAfterPrepareForSnapshotRevert(any(), any(), any(), any()); + } + + @Test + public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotSuccess() throws OperationTimedoutException, AgentUnavailableException { + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(anyLong(), anyBoolean()); + doReturn(Date.from(Instant.now())).when(internalBackupJoinVoMock).getDate(); + doReturn(Date.from(Instant.EPOCH)).when(vmSnapshotVoMock).getCreated(); + doReturn(List.of()).when(vmSnapshotHelperMock).getVolumeTOList(vmId); + doReturn(vmSnapshotVoMock).when(kbossBackupProviderSpy).getSucceedingVmSnapshot(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).createDeleteCommandsAndMergeTrees(any(), any(), any(), any(), anyList(), any()); + doReturn(new Answer[]{}).when(kbossBackupProviderSpy).sendBackupCommands(any(), any()); + doNothing().when(kbossBackupProviderSpy).updateReferencesAfterPrepareForSnapshotRevert(any(), any(), any(), any()); + + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy).updateReferencesAfterPrepareForSnapshotRevert(any(), any(), any(), any()); + } + + @Test (expected = BackupException.class) + public void finalizeQuickRestoreTestStoppedVmStartException() throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(VirtualMachine.State.Stopped).when(userVmVOMock).getState(); + doThrow(CloudRuntimeException.class).when(userVmManagerMock).startVirtualMachine(anyLong(), any(), any(), any(), anyBoolean()); + + kbossBackupProviderSpy.finalizeQuickRestore(virtualMachineMock, List.of(), 0); + } + + @Test + public void finalizeQuickRestoreTestStoppedVmStartSuccess() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(true).when(kbossBackupProviderSpy).consolidateVolumes(any(), anyLong(), anyList()); + + boolean result = kbossBackupProviderSpy.finalizeQuickRestore(virtualMachineMock, List.of(), 0); + + assertTrue(result); + verify(kbossBackupProviderSpy).consolidateVolumes(any(), anyLong(), anyList()); + } + + @Test + public void validateWithHashTestNoHosts() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(null).when(hostDaoMock).listAllHostsUpByZoneAndHypervisor(anyLong(), any()); + doNothing().when(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithHash(backupId, backupVoMock, null); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + } + + @Test + public void validateWithHashTestAnswerResultFalse() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(List.of(hostVOMock)).when(hostDaoMock).listAllHostsUpByZoneAndHypervisor(anyLong(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(false).when(answerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithHash(backupId, backupVoMock, null); + + assertFalse(result); + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + } + + @Test + public void validateWithHashTestDifferentHash() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(List.of(hostVOMock)).when(hostDaoMock).listAllHostsUpByZoneAndHypervisor(anyLong(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn("wrongHash").when(answerMock).getDetails(); + doReturn("correctHash").when(backupDetailVoMock).getValue(); + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithHash(backupId, backupVoMock, backupDetailVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(any(), any()); + verify(backupDaoMock, Mockito.never()).update(anyLong(), any()); + } + + @Test + public void validateWithHashTestSameHash() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(List.of(hostVOMock)).when(hostDaoMock).listAllHostsUpByZoneAndHypervisor(anyLong(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn("correctHash").when(answerMock).getDetails(); + doReturn("correctHash").when(backupDetailVoMock).getValue(); + + boolean result = kbossBackupProviderSpy.validateWithHash(backupId, backupVoMock, backupDetailVoMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(kbossBackupProviderSpy, never()).setBackupAsInvalidAndSendAlert(any(), any()); + verify(kbossBackupProviderSpy, never()).setBackupUnableToValidateAndSendAlert(any(), any()); + verify(backupDaoMock).update(anyLong(), any()); + } + + + @Test + public void validateWithValidationVmTestValidationVmIsNull() { + doReturn(null).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).cleanupValidation(anyBoolean(), any(), any(), any()); + } + + @Test + public void validateWithValidationVmTestPrepareForValidationFails() throws NoTransitionException { + doReturn(userVmVOMock).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(List.of()).when(volumeDaoMock).findByInstance(anyLong()); + doNothing().when(kbossBackupProviderSpy).createValidationVolumesOnPrimaryStorage(any(), any(), any(), any(), any()); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), eq(false)); + doReturn(false).when(kbossBackupProviderSpy).prepareForValidation(anyLong(), any(), any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).cleanupValidation(eq(false), eq(userVmVOMock), eq(backupVoMock), any()); + } + + @Test + public void validateWithValidationVmTestValidateBackupFails() throws NoTransitionException { + doReturn(userVmVOMock).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + doReturn(2L).when(userVmVOMock).getHostId(); + doReturn(Hypervisor.HypervisorType.KVM).when(userVmVOMock).getHypervisorType(); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(userVmVOMock).when(userVmDaoMock).findById(anyLong()); + doReturn(List.of()).when(volumeDaoMock).findByInstance(anyLong()); + doNothing().when(kbossBackupProviderSpy).createValidationVolumesOnPrimaryStorage(any(), any(), any(), any(), any()); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), eq(false)); + doReturn(true).when(kbossBackupProviderSpy).prepareForValidation(anyLong(), any(), any(), any()); + doReturn(hypervisorGuruMock).when(hypervisorGuruManagerMock).getGuru(any()); + doReturn(virtualMachineToMock).when(hypervisorGuruMock).implement(any()); + doReturn(false).when(kbossBackupProviderSpy).validateBackup(anyLong(), any(), any(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).endBackupChainIfConfigured(backupVoMock); + verify(kbossBackupProviderSpy).cleanupValidation(eq(true), eq(userVmVOMock), eq(backupVoMock), any()); + } + + @Test + public void validateWithValidationVmTestSuccessfulValidation() throws NoTransitionException { + doReturn(userVmVOMock).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + doReturn(2L).when(userVmVOMock).getHostId(); + doReturn(Hypervisor.HypervisorType.KVM).when(userVmVOMock).getHypervisorType(); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(2L).when(hostVOMock).getId(); + doReturn(userVmVOMock).when(userVmDaoMock).findById(anyLong()); + doReturn(List.of()).when(volumeDaoMock).findByInstance(anyLong()); + doNothing().when(kbossBackupProviderSpy).createValidationVolumesOnPrimaryStorage(any(), any(), any(), any(), any()); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), eq(false)); + doReturn(true).when(kbossBackupProviderSpy).prepareForValidation(anyLong(), any(), any(), any()); + doReturn(hypervisorGuruMock).when(hypervisorGuruManagerMock).getGuru(any()); + doReturn(virtualMachineToMock).when(hypervisorGuruMock).implement(any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackup(anyLong(), any(), any(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).calculateAndSaveHash(any(), any(), anyLong()); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).calculateAndSaveHash(any(), eq(backupVoMock), anyLong()); + verify(kbossBackupProviderSpy).cleanupValidation(eq(true), eq(userVmVOMock), eq(backupVoMock), any()); + } + + @Test + public void validateWithValidationVmTestExceptionHandling() throws NoTransitionException { + doReturn(userVmVOMock).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(List.of()).when(volumeDaoMock).findByInstance(anyLong()); + doThrow(new RuntimeException("boom")).when(kbossBackupProviderSpy).createValidationVolumesOnPrimaryStorage(any(), any(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(eq(backupVoMock), contains("boom")); + verify(kbossBackupProviderSpy).cleanupValidation(eq(false), eq(userVmVOMock), eq(backupVoMock), any()); + } + + + @Test + public void setBackupAsIsolatedTestPersistIsolatedDetail() { + kbossBackupProviderSpy.setBackupAsIsolated(backupVoMock); + verify(backupDetailDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void endBackupChainIfConfiguredTestFeatureDisabled() { + doReturn(false).when(kbossBackupProviderSpy).getValidationEndChainOnFail(backupVoMock); + + kbossBackupProviderSpy.endBackupChainIfConfigured(backupVoMock); + + verify(kbossBackupProviderSpy, never()).endBackupChain(any(), any()); + } + + @Test + public void endBackupChainIfConfiguredTestNotCurrentAndNoCurrentChildren() { + doReturn(true).when(kbossBackupProviderSpy).getValidationEndChainOnFail(backupVoMock); + doReturn(false).when(internalBackupJoinVoMock).getCurrent(); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + InternalBackupJoinVO child = mock(InternalBackupJoinVO.class); + doReturn(false).when(child).getCurrent(); + doReturn(List.of(child)).when(kbossBackupProviderSpy).getBackupJoinChildren(any()); + + kbossBackupProviderSpy.endBackupChainIfConfigured(backupVoMock); + + verify(kbossBackupProviderSpy, never()).endBackupChain(any(), any()); + } + + @Test + public void endBackupChainIfConfiguredTestBackupIsCurrent() { + doReturn(true).when(kbossBackupProviderSpy).getValidationEndChainOnFail(backupVoMock); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupJoinChildren(any()); + doReturn(userVmVOMock).when(userVmDaoMock).findById(anyLong()); + doReturn(true).when(kbossBackupProviderSpy).endBackupChain(any(), any()); + + kbossBackupProviderSpy.endBackupChainIfConfigured(backupVoMock); + + verify(kbossBackupProviderSpy, times(1)).endBackupChain(eq(userVmVOMock), anyLong()); + } + + @Test + public void endBackupChainIfConfiguredTestLastChildIsCurrent() { + doReturn(true).when(kbossBackupProviderSpy).getValidationEndChainOnFail(backupVoMock); + doReturn(false).when(internalBackupJoinVoMock).getCurrent(); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + InternalBackupJoinVO child = mock(InternalBackupJoinVO.class); + doReturn(true).when(child).getCurrent(); + doReturn(List.of(child)).when(kbossBackupProviderSpy).getBackupJoinChildren(any()); + doReturn(userVmVOMock).when(userVmDaoMock).findById(anyLong()); + doReturn(true).when(kbossBackupProviderSpy).endBackupChain(any(), any()); + + kbossBackupProviderSpy.endBackupChainIfConfigured(backupVoMock); + + verify(kbossBackupProviderSpy, times(1)).endBackupChain(eq(userVmVOMock), anyLong()); + } + + + @Test + public void normalizeBackupErrorAndFinishChainTestAnswerNull() { + doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); + doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(mock(ImageStoreVO.class)).when(imageStoreDaoMock).findById(anyLong()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(anyLong()); + long parentId = 9382; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + doReturn(null).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), any(), any(), any(),anyBoolean()); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); + + assertFalse(result); + } + + @Test + public void normalizeBackupErrorAndFinishChainTestAnswerFailed() { + doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); + doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(mock(ImageStoreVO.class)).when(imageStoreDaoMock).findById(anyLong()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(anyLong()); + long parentId = 9382; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + doReturn(null).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), any(), any(), any(), anyBoolean()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(false).when(answerMock).getResult(); + + boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); + + assertFalse(result); + } + + @Test + public void normalizeBackupErrorAndFinishChainTestSuccessCallsEndChain() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(any()); + doReturn(VirtualMachine.State.Running).when(userVmVOMock).getState(); + doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); + doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(mock(ImageStoreVO.class)).when(imageStoreDaoMock).findById(anyLong()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(anyLong()); + long parentId = 9382; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + doReturn(null).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), any(), any(), any(), anyBoolean()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn(false).when(kbossBackupProviderSpy).processCleanupBackupErrorAnswer(any(), any(), any(), any(), any()); + + boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + verify(kbossBackupProviderSpy).finishBackupChains(userVmVOMock); + } + + + @Test + public void normalizeBackupErrorAndFinishChainTestChainAlreadyEnded() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(any()); + doReturn(VirtualMachine.State.Running).when(userVmVOMock).getState(); + doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); + doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(mock(ImageStoreVO.class)).when(imageStoreDaoMock).findById(anyLong()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(anyLong()); + long parentId = 9382; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + doReturn(null).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), any(), any(), any(), anyBoolean()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn(true).when(kbossBackupProviderSpy).processCleanupBackupErrorAnswer(any(), any(), any(), any(), any()); + InternalBackupJoinVO current = mock(InternalBackupJoinVO.class); + doReturn(current).when(internalBackupJoinDaoMock).findCurrent(anyLong(), any()); + doNothing().when(internalBackupStoragePoolDaoMock).expungeByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); + + boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); + + assertTrue(result); + verify(internalBackupStoragePoolDaoMock).expungeByBackupId(anyLong()); + verify(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); + } + + @Test + public void cleanupValidationTestStartedVmFalseAndValidationNotPreparedAndFailedToDestroyVolume() throws ResourceUnavailableException { + VolumeVO dataVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.DATADISK).when(dataVolume).getVolumeType(); + doReturn(52L).when(dataVolume).getId(); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + CallContext callContextMock = Mockito.mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + kbossBackupProviderSpy.cleanupValidation(false, userVmVOMock, backupVoMock, List.of(dataVolume)); + + verify(userVmManagerMock, Mockito.never()).stopVirtualMachine(anyLong(), anyBoolean()); + verify(userVmManagerMock, Mockito.times(1)).destroyVm(any(DestroyVMCmd.class), Mockito.eq(false)); + verify(volumeApiServiceMock, Mockito.times(1)).destroyVolume(Mockito.eq(52L), any(), Mockito.eq(true), Mockito.eq(true), Mockito.isNull()); + verify(agentManagerMock, Mockito.never()).easySend(anyLong(), any()); + } + } + + @Test + public void cleanupValidationTestDestroyVmThrowsAndCleanupMailIsSent() throws ResourceUnavailableException { + VolumeVO dataVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.DATADISK).when(dataVolume).getVolumeType(); + doReturn(88L).when(userVmVOMock).getHostId(); + doReturn(Set.of("secondary-storage-1")).when(kbossBackupProviderSpy).getSecondaryStorageUrls(userVmVOMock); + doThrow(new RuntimeException("boom")).when(userVmManagerMock).destroyVm(any(DestroyVMCmd.class), Mockito.eq(false)); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + CallContext callContextMock = Mockito.mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + kbossBackupProviderSpy.cleanupValidation(true, userVmVOMock, backupVoMock, List.of(dataVolume)); + + verify(userVmManagerMock, Mockito.times(1)).destroyVm(any(DestroyVMCmd.class), Mockito.eq(false)); + verify(kbossBackupProviderSpy, Mockito.times(1)).sendCleanupFailedEmail(eq(backupVoMock), contains("Got an unexpected exception while trying to destroy validation VM.")); + verify(agentManagerMock, Mockito.times(1)).easySend(Mockito.eq(88L), any(CleanupKbossValidationCommand.class)); + } + } + + @Test + public void cleanupValidationTestCleanupCommandFails() { + VolumeVO dataVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.DATADISK).when(dataVolume).getVolumeType(); + doReturn(88L).when(userVmVOMock).getHostId(); + doReturn(Set.of("secondary-storage-1")).when(kbossBackupProviderSpy).getSecondaryStorageUrls(userVmVOMock); + doReturn(null).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(88L); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + CallContext callContextMock = Mockito.mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + kbossBackupProviderSpy.cleanupValidation(true, userVmVOMock, backupVoMock, List.of(dataVolume)); + + verify(agentManagerMock, Mockito.times(1)).easySend(Mockito.eq(88L), any(CleanupKbossValidationCommand.class)); + verify(hostDaoMock, Mockito.times(1)).findById(88L); + verify(kbossBackupProviderSpy, times(1)).sendCleanupFailedEmail(any(), any()); + } + } + + @Test + public void cleanupValidationTestNoEmailSentAndAllStepsExecuted() throws ResourceUnavailableException { + VolumeVO rootVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.ROOT).when(rootVolume).getVolumeType(); + + VolumeVO dataVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.DATADISK).when(dataVolume).getVolumeType(); + doReturn(52L).when(dataVolume).getId(); + + doReturn(77L).when(userVmVOMock).getId(); + doReturn(88L).when(userVmVOMock).getHostId(); + doReturn(Set.of("secondary-storage-1")).when(kbossBackupProviderSpy).getSecondaryStorageUrls(userVmVOMock); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn(volumeVoMock).when(volumeApiServiceMock).destroyVolume(Mockito.eq(52L), any(), Mockito.eq(true), Mockito.eq(true), Mockito.isNull()); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + CallContext callContextMock = Mockito.mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + kbossBackupProviderSpy.cleanupValidation(true, userVmVOMock, backupVoMock, List.of(rootVolume, dataVolume)); + + verify(userVmManagerMock, Mockito.times(1)).stopVirtualMachine(77L, true); + verify(userVmManagerMock, Mockito.times(1)).destroyVm(any(DestroyVMCmd.class), Mockito.eq(false)); + verify(volumeApiServiceMock, Mockito.times(1)).destroyVolume(Mockito.eq(52L), any(), Mockito.eq(true), Mockito.eq(true), Mockito.isNull()); + verify(agentManagerMock, Mockito.times(1)).easySend(Mockito.eq(88L), any(CleanupKbossValidationCommand.class)); + verify(kbossBackupProviderSpy, Mockito.never()).sendCleanupFailedEmail(any(), any()); + verify(hostDaoMock, Mockito.never()).findById(anyLong()); + } + } + + @Test + public void getVolumesToConsolidateTestSameVmAsBackupFalse() { + VolumeObjectTO volumeObjectTO1 = Mockito.mock(VolumeObjectTO.class); + doReturn(11L).when(volumeObjectTO1).getVolumeId(); + VolumeInfo volumeInfo1 = Mockito.mock(VolumeInfo.class); + doReturn(volumeVoMock).when(volumeInfo1).getVolume(); + doReturn(volumeInfo1).when(volumeDataFactoryMock).getVolume(11L); + + VolumeObjectTO volumeObjectTO2 = Mockito.mock(VolumeObjectTO.class); + doReturn(22L).when(volumeObjectTO2).getVolumeId(); + VolumeInfo volumeInfo2 = Mockito.mock(VolumeInfo.class); + doReturn(Mockito.mock(VolumeVO.class)).when(volumeInfo2).getVolume(); + doReturn(volumeInfo2).when(volumeDataFactoryMock).getVolume(22L); + + doNothing().when(kbossBackupProviderSpy).transitVmState(any(), any(), anyLong()); + doNothing().when(kbossBackupProviderSpy).transitVolumeState(any(), any()); + + List result = kbossBackupProviderSpy.getVolumesToConsolidate(virtualMachineMock, List.of(), List.of(volumeObjectTO1, volumeObjectTO2), 99L, false); + + assertEquals(List.of(volumeInfo1, volumeInfo2), result); + verify(kbossBackupProviderSpy, times(1)).transitVmState(virtualMachineMock, VirtualMachine.Event.RestoringSuccess, 99L); + verify(volumeDataFactoryMock, times(1)).getVolume(11L); + verify(volumeDataFactoryMock, times(1)).getVolume(22L); + verify(kbossBackupProviderSpy, times(2)).transitVolumeState(any(), eq(Volume.Event.RestoreSucceeded)); + } + + @Test + public void getVolumesToConsolidateTestSameVmAsBackupTrueFiltersBySecondaryDeltas() { + VolumeObjectTO volumeObjectTO1 = Mockito.mock(VolumeObjectTO.class); + doReturn(11L).when(volumeObjectTO1).getVolumeId(); + VolumeInfo volumeInfo1 = Mockito.mock(VolumeInfo.class); + doReturn(volumeVoMock).when(volumeInfo1).getVolume(); + doReturn(volumeInfo1).when(volumeDataFactoryMock).getVolume(11L); + + VolumeObjectTO volumeObjectTO2 = Mockito.mock(VolumeObjectTO.class); + doReturn(22L).when(volumeObjectTO2).getVolumeId(); + VolumeInfo volumeInfo2 = Mockito.mock(VolumeInfo.class); + doReturn(Mockito.mock(VolumeVO.class)).when(volumeInfo2).getVolume(); + doReturn(volumeInfo2).when(volumeDataFactoryMock).getVolume(22L); + + InternalBackupDataStoreVO delta = Mockito.mock(InternalBackupDataStoreVO.class); + doReturn(33L).when(delta).getVolumeId(); + + doNothing().when(kbossBackupProviderSpy).transitVmState(any(), any(), anyLong()); + doNothing().when(kbossBackupProviderSpy).transitVolumeState(any(), any()); + + List result = kbossBackupProviderSpy.getVolumesToConsolidate(virtualMachineMock, List.of(delta), List.of(volumeObjectTO1, volumeObjectTO2), 99L, true); + + assertEquals(List.of(), result); + verify(kbossBackupProviderSpy, times(1)).transitVmState(virtualMachineMock, VirtualMachine.Event.RestoringSuccess, 99L); + verify(volumeDataFactoryMock, times(1)).getVolume(11L); + verify(volumeDataFactoryMock, times(1)).getVolume(22L); + verify(kbossBackupProviderSpy, times(2)).transitVolumeState(any(), eq(Volume.Event.RestoreSucceeded)); + } + + @Test + public void checkErrorBackupTestNonErrorStatusDoesNothing() { + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + kbossBackupProviderSpy.checkErrorBackup(backupVoMock, virtualMachineMock); + + verify(backupVoMock, never()).setStatus(Backup.Status.Failed); + } + + @Test + public void checkErrorBackupTestErrorStatusAndVmIsNullSetsBackupFailed() { + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + + kbossBackupProviderSpy.checkErrorBackup(backupVoMock, null); + + verify(backupVoMock).setStatus(Backup.Status.Failed); + } + + @Test + public void checkErrorBackupTestErrorStatusAndVmNotBackupErrorSetsBackupFailed() { + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + doReturn(VirtualMachine.State.Running).when(virtualMachineMock).getState(); + + kbossBackupProviderSpy.checkErrorBackup(backupVoMock, virtualMachineMock); + + verify(backupVoMock).setStatus(Backup.Status.Failed); + } + + @Test(expected = InvalidParameterValueException.class) + public void checkErrorBackupTestErrorStatusAndVmInBackupErrorThrows() { + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + doReturn(VirtualMachine.State.BackupError).when(virtualMachineMock).getState(); + + kbossBackupProviderSpy.checkErrorBackup(backupVoMock, virtualMachineMock); + + verify(backupVoMock, never()).setStatus(Backup.Status.Failed); + } + + @Test + public void deleteFailedBackupTestFailedBackupIsExpungedAndCleanedUp() { + long backupId = 123L; + doReturn(backupId).when(backupVoMock).getId(); + doReturn(Backup.Status.Failed).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.deleteFailedBackup(backupVoMock); + + assertTrue(result); + verify(backupVoMock).setStatus(Backup.Status.Expunged); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(internalBackupStoragePoolDaoMock).expungeByBackupId(backupId); + verify(internalBackupDataStoreDaoMock).expungeByBackupId(backupId); + verify(backupDetailDaoMock).removeDetails(backupId); + } + + @Test + public void deleteFailedBackupTestNonFailedBackupDoesNothing() { + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.deleteFailedBackup(backupVoMock); + + assertFalse(result); + verify(backupVoMock, never()).setStatus(Backup.Status.Expunged); + verify(backupDaoMock, never()).update(anyLong(), any()); + verify(internalBackupStoragePoolDaoMock, never()).expungeByBackupId(anyLong()); + verify(internalBackupDataStoreDaoMock, never()).expungeByBackupId(anyLong()); + verify(backupDetailDaoMock, never()).removeDetails(anyLong()); + } + + @Test + public void mergeCurrentDeltaIntoVolumeTestNoDeltaDoesNothing() { + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn(List.of()).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(volumeId); + + kbossBackupProviderSpy.mergeCurrentDeltasIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + + verify(internalBackupJoinDaoMock, times(1)).listCurrentsByVolumeIdDesc(volumeId); + verify(internalBackupJoinDaoMock, never()).findById(anyLong()); + } + + @Test (expected = CloudRuntimeException.class) + public void mergeCurrentDeltaIntoVolumeTestNullAnswer() { + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(volumeId); + doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(anyBoolean(), anyBoolean(), any(), any(), any(), any()); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + try (MockedStatic volumeObjectMockedStatic = Mockito.mockStatic(VolumeObject.class)) { + when(VolumeObject.getVolumeObject(any(), any())).thenReturn(volumeObjectMock); + + kbossBackupProviderSpy.mergeCurrentDeltasIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(kbossBackupProviderSpy, never()).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + } + } + + @Test + public void mergeCurrentDeltaIntoVolumeTestNoSucceedingSnapshot() { + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(volumeId); + doReturn(backupId).when(internalBackupStoragePoolVoMock).getBackupId(); + doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(anyBoolean(), anyBoolean(), any(), any(), any(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn(volumeVoMock).when(volumeDaoMock).findById(anyLong()); + doReturn(backupDeltaToMock).when(deltaMergeTreeToMock).getParent(); + doNothing().when(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(backupId); + doNothing().when(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeIdAndBackupId(anyLong(), anyLong()); + + try (MockedStatic volumeObjectMockedStatic = Mockito.mockStatic(VolumeObject.class)) { + when(VolumeObject.getVolumeObject(any(), any())).thenReturn(volumeObjectMock); + + kbossBackupProviderSpy.mergeCurrentDeltasIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + verify(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + verify(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); + } + } + + @Test + public void mergeCurrentDeltaIntoVolumeTestWithSucceedingSnapshotWithMoreDeltas() { + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(volumeId); + doReturn(backupId).when(internalBackupStoragePoolVoMock).getBackupId(); + doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(anyBoolean(), anyBoolean(), any(), any(), any(), any()); + doReturn(backupDeltaToMock).when(deltaMergeTreeToMock).getParent(); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + doReturn(List.of(internalBackupStoragePoolVoMock)).when(internalBackupStoragePoolDaoMock).listByBackupId(backupId); + doReturn(volumeVoMock).when(volumeDaoMock).findById(anyLong()); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeIdAndBackupId(anyLong(), anyLong()); + + try (MockedStatic volumeObjectMockedStatic = Mockito.mockStatic(VolumeObject.class)) { + when(VolumeObject.getVolumeObject(any(), any())).thenReturn(volumeObjectMock); + + kbossBackupProviderSpy.mergeCurrentDeltasIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + verify(kbossBackupProviderSpy, never()).setEndOfChainAndRemoveCurrentForBackup(any()); + } + } + + @Test + public void getHostToRestoreTestNonQuickRestoreUsesRunningHost() throws AgentUnavailableException { + doReturn(vmId).when(virtualMachineMock).getId(); + doReturn(77L).when(vmSnapshotHelperMock).pickRunningHost(vmId); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(77L); + + HostVO result = kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, false, null); + + assertEquals(hostVOMock, result); + verify(vmSnapshotHelperMock, times(1)).pickRunningHost(vmId); + verify(hostDaoMock, times(1)).findByIdIncludingRemoved(77L); + } + + @Test + public void getHostToRestoreTestQuickRestoreUsesProvidedHostId() throws AgentUnavailableException { + doReturn(Status.Up).when(hostVOMock).getStatus(); + doReturn(false).when(hostVOMock).isInMaintenanceStates(); + doReturn(ResourceState.Enabled).when(hostVOMock).getResourceState(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(55L); + + HostVO result = kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, 55L); + + assertEquals(hostVOMock, result); + verify(vmSnapshotHelperMock, never()).pickRunningHost(anyLong()); + verify(hostDaoMock, times(1)).findByIdIncludingRemoved(55L); + } + + @Test + public void getHostToRestoreTestQuickRestoreUsesVmLastHostIdWhenHostIdIsNull() throws AgentUnavailableException { + doReturn(99L).when(virtualMachineMock).getLastHostId(); + doReturn(Status.Up).when(hostVOMock).getStatus(); + doReturn(false).when(hostVOMock).isInMaintenanceStates(); + doReturn(ResourceState.Enabled).when(hostVOMock).getResourceState(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(99L); + + HostVO result = kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, null); + + assertEquals(hostVOMock, result); + verify(hostDaoMock, times(1)).findByIdIncludingRemoved(99L); + } + + @Test(expected = AgentUnavailableException.class) + public void getHostToRestoreTestQuickRestoreWithNoHostIdAndNoLastHostThrows() throws AgentUnavailableException { + doReturn(null).when(virtualMachineMock).getLastHostId(); + + kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, null); + } + + @Test(expected = AgentUnavailableException.class) + public void getHostToRestoreTestQuickRestoreWithHostDownThrows() throws AgentUnavailableException { + doReturn(Status.Down).when(hostVOMock).getStatus(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(55L); + + kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, 55L); + } + + @Test(expected = AgentUnavailableException.class) + public void getHostToRestoreTestQuickRestoreWithHostInMaintenanceThrows() throws AgentUnavailableException { + doReturn(Status.Up).when(hostVOMock).getStatus(); + doReturn(true).when(hostVOMock).isInMaintenanceStates(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(55L); + + kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, 55L); + } + + @Test(expected = AgentUnavailableException.class) + public void getHostToRestoreTestQuickRestoreWithHostDisabledThrows() throws AgentUnavailableException { + doReturn(Status.Up).when(hostVOMock).getStatus(); + doReturn(false).when(hostVOMock).isInMaintenanceStates(); + doReturn(ResourceState.Disabled).when(hostVOMock).getResourceState(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(55L); + + kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, 55L); + } + + @Test + public void createDeltaMergeTreeTestChildIsVolumeWithoutSucceedingSnapshot() { + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + + DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(true, true, internalBackupStoragePoolVoMock, + volumeObjectToMock, null, null); + + assertEquals(volumeObjectToMock, result.getVolumeObjectTO()); + assertTrue(result.getGrandChildren().isEmpty()); + assertEquals(volumeObjectToMock, result.getChild()); + assertEquals("parent-path", result.getParent().getPath()); + } + + @Test + public void createDeltaMergeTreeTestChildIsDeltaWithoutSucceedingSnapshot() { + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + doReturn("child-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); + + DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(false, true, internalBackupStoragePoolVoMock, + volumeObjectToMock, null, null); + + assertEquals(volumeObjectToMock, result.getVolumeObjectTO()); + assertEquals("parent-path", result.getParent().getPath()); + assertEquals("child-path", result.getChild().getPath()); + assertTrue(result.getGrandChildren().isEmpty()); + } + + @Test + public void createDeltaMergeTreeTestChildIsDeltaWithSucceedingSnapshotReferences() { + SnapshotDataStoreVO snapshotRefMock = Mockito.mock(SnapshotDataStoreVO.class); + + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + doReturn("child-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn("path").when(volumeObjectToMock).getPath(); + + DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(false, false, internalBackupStoragePoolVoMock, + volumeObjectToMock, vmSnapshotVoMock, List.of()); + + assertEquals("child-path", result.getChild().getPath()); + assertEquals(1, result.getGrandChildren().size()); + assertEquals("path", result.getGrandChildren().get(0).getPath()); + } + + @Test + public void createDeltaMergeTreeTestChildIsDeltaWithSucceedingSnapshotButNoReferencesRebasesToVolumePath() { + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + doReturn("child-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); + doReturn("/volume/path").when(volumeObjectToMock).getPath(); + + DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(false, false, internalBackupStoragePoolVoMock, + volumeObjectToMock, vmSnapshotVoMock, List.of()); + + assertEquals(1, result.getGrandChildren().size()); + assertEquals("/volume/path", result.getGrandChildren().get(0).getPath()); + } + + @Test + public void generateBackupAndVolumePairsToRestoreTestSameVmAsBackupMatchesByVolumeId() { + doReturn(77L).when(internalBackupJoinVoMock).getImageStoreId(); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(77L, DataStoreRole.Image); + doReturn(10L).when(internalBackupDataStoreVoMock).getVolumeId(); + doReturn("delta-path").when(internalBackupDataStoreVoMock).getBackupPath(); + doReturn(10L).when(volumeObjectToMock).getVolumeId(); + + Set> result = kbossBackupProviderSpy.generateBackupAndVolumePairsToRestore(List.of(internalBackupDataStoreVoMock), + List.of(volumeObjectToMock), internalBackupJoinVoMock, true); + + assertEquals(1, result.size()); + Pair pair = result.iterator().next(); + assertEquals(volumeObjectToMock, pair.second()); + assertEquals("delta-path", pair.first().getPath()); + } + + @Test + public void generateBackupAndVolumePairsToRestoreTestDifferentVmMatchesByDeviceId() { + doReturn(77L).when(internalBackupJoinVoMock).getImageStoreId(); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(77L, DataStoreRole.Image); + doReturn(5L).when(internalBackupDataStoreVoMock).getDeviceId(); + doReturn("delta-path").when(internalBackupDataStoreVoMock).getBackupPath(); + doReturn(5L).when(volumeObjectToMock).getDeviceId(); + + Set> result = kbossBackupProviderSpy.generateBackupAndVolumePairsToRestore(List.of(internalBackupDataStoreVoMock), + List.of(volumeObjectToMock), internalBackupJoinVoMock, false); + + assertEquals(1, result.size()); + Pair pair = result.iterator().next(); + assertEquals(volumeObjectToMock, pair.second()); + assertEquals("delta-path", pair.first().getPath()); + } + + @Test(expected = CloudRuntimeException.class) + public void generateBackupAndVolumePairsToRestoreTestThrowsWhenNoMatchingVolumeExists() { + doReturn(77L).when(internalBackupJoinVoMock).getImageStoreId(); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(77L, DataStoreRole.Image); + doReturn(10L).when(internalBackupDataStoreVoMock).getVolumeId(); + doReturn(123L).when(internalBackupDataStoreVoMock).getId(); + + kbossBackupProviderSpy.generateBackupAndVolumePairsToRestore(List.of(internalBackupDataStoreVoMock), List.of(volumeObjectToMock), internalBackupJoinVoMock, true); + } + + @Test + public void populateDeltasToRemoveAndToMergeAndUpdateVolumePathsTestVolumeIsPartOfBackupAddsDeltaToRemoveAndUpdatesPath() { + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(11L, DataStoreRole.Primary); + doReturn(10L).when(internalBackupStoragePoolVoMock).getVolumeId(); + doReturn(11L).when(internalBackupStoragePoolVoMock).getStoragePoolId(); + doReturn("delta-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + doReturn(10L).when(volumeObjectToMock).getVolumeId(); + + Set deltasToRemove = new java.util.HashSet<>(); + + List result = kbossBackupProviderSpy.populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(List.of(internalBackupStoragePoolVoMock), deltasToRemove, + List.of(volumeObjectToMock), List.of(), "vm-uuid"); + + assertTrue(result.isEmpty()); + assertEquals(1, deltasToRemove.size()); + verify(volumeObjectToMock, times(1)).setPath("parent-path"); + verify(dataStoreManagerMock, times(1)).getDataStore(11L, DataStoreRole.Primary); + } + + @Test + public void populateDeltasToRemoveAndToMergeAndUpdateVolumePathsTestVolumeIsPartOfBackupCreatesMergeTree() { + doReturn(volumeId).when(internalBackupStoragePoolVoMock).getVolumeId(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + + Set deltasToRemove = new java.util.HashSet<>(); + + doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(eq(true), eq(false), eq(internalBackupStoragePoolVoMock), eq(volumeObjectToMock), eq(null), + eq(new ArrayList<>())); + + List result = kbossBackupProviderSpy.populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(List.of(internalBackupStoragePoolVoMock), deltasToRemove, + List.of(volumeObjectToMock), List.of(volumeObjectToMock), "vm-uuid"); + + assertEquals(List.of(deltaMergeTreeToMock), result); + assertTrue(deltasToRemove.isEmpty()); + verify(kbossBackupProviderSpy, times(1)).createDeltaMergeTree(eq(true), eq(false), eq(internalBackupStoragePoolVoMock), eq(volumeObjectToMock), eq(null), + eq(new ArrayList<>())); + verify(dataStoreManagerMock, never()).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + } + + @Test(expected = CloudRuntimeException.class) + public void populateDeltasToRemoveAndToMergeAndUpdateVolumePathsTestThrowsWhenNoMatchingVolumeExists() { + kbossBackupProviderSpy.populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(List.of(internalBackupStoragePoolVoMock), Set.of(), List.of(), List.of(), "vm-uuid"); + } + + @Test + public void updateVolumePathsAndSizeIfNeededTestUpdatesPathAndSizeWhenVolumePathChanged() { + doReturn(List.of(volumeVoMock)).when(volumeDaoMock).findByInstance(vmId); + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn("old-path").when(volumeVoMock).getPath(); + doReturn(5L).when(volumeVoMock).getDeviceId(); + doReturn(100L).when(volumeVoMock).getSize(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn("new-path").when(volumeObjectToMock).getPath(); + doReturn(150L).when(backupVolumeInfoMock).getSize(); + doReturn(5L).when(backupVolumeInfoMock).getDeviceId(); + + kbossBackupProviderSpy.updateVolumePathsAndSizeIfNeeded(virtualMachineMock, List.of(volumeObjectToMock), List.of(backupVolumeInfoMock), List.of(), false); + + verify(volumeVoMock).setPath("new-path"); + verify(volumeVoMock).setSize(150L); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + } + + @Test + public void updateVolumePathsAndSizeIfNeededTestUsesMergeTreeParentPathWhenVolumePathDidNotChange() { + doReturn(List.of(volumeVoMock)).when(volumeDaoMock).findByInstance(vmId); + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn("same-path").when(volumeVoMock).getPath(); + doReturn(5L).when(volumeVoMock).getDeviceId(); + doReturn(100L).when(volumeVoMock).getSize(); + doReturn("same-path").when(volumeObjectToMock).getPath(); + doReturn(volumeId).when(volumeObjectToMock).getId(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn(150L).when(backupVolumeInfoMock).getSize(); + doReturn(5L).when(backupVolumeInfoMock).getDeviceId(); + doReturn(volumeObjectToMock).when(deltaMergeTreeToMock).getChild(); + doReturn(backupDeltaToMock).when(deltaMergeTreeToMock).getParent(); + doReturn("parent-path").when(backupDeltaToMock).getPath(); + + kbossBackupProviderSpy.updateVolumePathsAndSizeIfNeeded(virtualMachineMock, List.of(volumeObjectToMock), List.of(backupVolumeInfoMock), List.of(deltaMergeTreeToMock), + false); + + verify(volumeVoMock).setPath("parent-path"); + verify(volumeVoMock).setSize(150L); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + } + + @Test + public void updateVolumePathsAndSizeIfNeededTestLeavesSizeUntouchedWhenRestoreSizeMatchesCurrentSize() { + doReturn(List.of(volumeVoMock)).when(volumeDaoMock).findByInstance(vmId); + doReturn(vmId).when(virtualMachineMock).getId(); + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn("same-path").when(volumeVoMock).getPath(); + doReturn(5L).when(volumeVoMock).getDeviceId(); + doReturn(100L).when(volumeVoMock).getSize(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn("same-path").when(volumeObjectToMock).getPath(); + doReturn(100L).when(backupVolumeInfoMock).getSize(); + doReturn(5L).when(backupVolumeInfoMock).getDeviceId(); + + kbossBackupProviderSpy.updateVolumePathsAndSizeIfNeeded(virtualMachineMock, List.of(volumeObjectToMock), List.of(backupVolumeInfoMock), List.of(), false); + + verify(volumeVoMock, never()).setSize(anyLong()); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + } + + @Test + public void updateVolumePathsAndSizeIfNeededTestMatchesByUuidWhenSameVmAsBackup() { + doReturn(List.of(volumeVoMock)).when(volumeDaoMock).findByInstance(vmId); + doReturn(vmId).when(virtualMachineMock).getId(); + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn("same-path").when(volumeVoMock).getPath(); + doReturn("vm-uuid").when(volumeVoMock).getUuid(); + doReturn(100L).when(volumeVoMock).getSize(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn("same-path").when(volumeObjectToMock).getPath(); + doReturn("vm-uuid").when(backupVolumeInfoMock).getUuid(); + doReturn(120L).when(backupVolumeInfoMock).getSize(); + + kbossBackupProviderSpy.updateVolumePathsAndSizeIfNeeded(virtualMachineMock, List.of(volumeObjectToMock), List.of(backupVolumeInfoMock), List.of(), true); + + verify(volumeVoMock).setSize(120L); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + } + + + @Test + public void processRemoveBackupFailuresTestNoFailuresReturnsTrueAndRemovesNothing() { + doReturn(backupId).when(internalBackupJoinVoMock).getId(); + + Answer[] deleteAnswers = new Answer[] {answerMock}; + doReturn(true).when(answerMock).getResult(); + + List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); + + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, deleteAnswers, removedBackupIds, internalBackupJoinVoMock, virtualMachineMock); + + assertTrue(result); + assertEquals(List.of(backupId, 200L), removedBackupIds); + verify(backupDaoMock, never()).findByIdIncludingRemoved(anyLong()); + verify(backupDaoMock, never()).update(anyLong(), any()); + } + + @Test + public void processRemoveBackupFailuresTestFailureOnCurrentBackupNotForcedSetsError() { + doReturn(backupId).when(internalBackupJoinVoMock).getId(); + + BackupDeleteAnswer failedCurrentBackupAnswer = Mockito.mock(BackupDeleteAnswer.class); + doReturn(false).when(failedCurrentBackupAnswer).getResult(); + doReturn(backupId).when(failedCurrentBackupAnswer).getBackupId(); + doReturn("delete failed").when(failedCurrentBackupAnswer).getDetails(); + + doReturn(backupId).when(backupVoMock).getId(); + doReturn(backupVoMock).when(backupDaoMock).findByIdIncludingRemoved(backupId); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + + List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); + + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, new Answer[]{failedCurrentBackupAnswer}, removedBackupIds, internalBackupJoinVoMock, virtualMachineMock); + + assertFalse(result); + assertEquals(List.of(200L), removedBackupIds); + verify(backupVoMock).setStatus(Backup.Status.Error); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(backupDaoMock, never()).findByIdIncludingRemoved(200L); + } + + @Test + public void processRemoveBackupFailuresTestFailureOnCurrentBackupForcedSetBackupAsExpunged() { + BackupDeleteAnswer failedCurrentBackupAnswer = Mockito.mock(BackupDeleteAnswer.class); + doReturn(false).when(failedCurrentBackupAnswer).getResult(); + doReturn(backupId).when(failedCurrentBackupAnswer).getBackupId(); + doReturn("delete failed").when(failedCurrentBackupAnswer).getDetails(); + doReturn(backupVoMock).when(backupDaoMock).findByIdIncludingRemoved(backupId); + + List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); + + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(true, new Answer[]{failedCurrentBackupAnswer}, removedBackupIds, internalBackupJoinVoMock, virtualMachineMock); + + assertFalse(result); + assertEquals(List.of(200L), removedBackupIds); + verify(backupVoMock).setStatus(Backup.Status.Expunged); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(backupDaoMock, never()).findByIdIncludingRemoved(200L); + } + + @Test + public void processRemoveBackupFailuresTestFailureOnOtherBackupMarksItExpunged() { + doReturn(backupId).when(internalBackupJoinVoMock).getId(); + + BackupDeleteAnswer failedOtherBackupAnswer = Mockito.mock(BackupDeleteAnswer.class); + doReturn(false).when(failedOtherBackupAnswer).getResult(); + doReturn(200L).when(failedOtherBackupAnswer).getBackupId(); + doReturn("delete failed").when(failedOtherBackupAnswer).getDetails(); + + BackupVO failedBackup = Mockito.mock(BackupVO.class); + doReturn(failedBackup).when(backupDaoMock).findByIdIncludingRemoved(200L); + + List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); + + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, new Answer[]{failedOtherBackupAnswer}, removedBackupIds, internalBackupJoinVoMock, virtualMachineMock); + + assertFalse(result); + assertEquals(List.of(backupId), removedBackupIds); + verify(failedBackup).setStatus(Backup.Status.Expunged); + verify(backupDaoMock).update(200L, failedBackup); + verify(backupDaoMock, never()).findByIdIncludingRemoved(backupId); + } + + @Test + public void processValidationAnswerTestNullAnswer() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(null, backupVoMock, userVmVOMock, hostVOMock, null); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("Null answer from host")); + } + + @Test + public void processValidationAnswerTestFalseAnswer() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + doReturn(false).when(answerMock).getResult(); + doReturn("fail-reason").when(answerMock).getDetails(); + boolean result = kbossBackupProviderSpy.processValidationAnswer(answerMock, backupVoMock, userVmVOMock, hostVOMock, null); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("fail-reason")); + } + + @Test + public void processValidationAnswerTestBootNotValidated() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + ValidateKbossVmAnswer answer = Mockito.mock(ValidateKbossVmAnswer.class); + doReturn(true).when(answer).getResult(); + doReturn(false).when(answer).isBootValidated(); + + ValidateKbossVmCommand cmd = Mockito.mock(ValidateKbossVmCommand.class); + doReturn(true).when(cmd).isWaitForBoot(); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(answer, backupVoMock, userVmVOMock, hostVOMock, cmd); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The VM did not boot within the expected time")); + } + + @Test + public void processValidationAnswerTestBootNotValidatedScriptResultFalse() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + ValidateKbossVmAnswer answer = Mockito.mock(ValidateKbossVmAnswer.class); + doReturn(true).when(answer).getResult(); + doReturn(false).when(answer).isBootValidated(); + doReturn("false").when(answer).getScriptResult(); + + ValidateKbossVmCommand cmd = Mockito.mock(ValidateKbossVmCommand.class); + doReturn(true).when(cmd).isWaitForBoot(); + doReturn(true).when(cmd).isExecuteScript(); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(answer, backupVoMock, userVmVOMock, hostVOMock, cmd); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The VM did not boot within the expected time")); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The script did not output the expected output.")); + } + + @Test + public void processValidationAnswerTestBootNotValidatedScriptResultFalseScreenshotPathNull() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + ValidateKbossVmAnswer answer = Mockito.mock(ValidateKbossVmAnswer.class); + doReturn(true).when(answer).getResult(); + doReturn(false).when(answer).isBootValidated(); + doReturn("false").when(answer).getScriptResult(); + doReturn(null).when(answer).getScreenshotPath(); + + ValidateKbossVmCommand cmd = Mockito.mock(ValidateKbossVmCommand.class); + doReturn(true).when(cmd).isWaitForBoot(); + doReturn(true).when(cmd).isExecuteScript(); + doReturn(true).when(cmd).isTakeScreenshot(); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(answer, backupVoMock, userVmVOMock, hostVOMock, cmd); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The VM did not boot within the expected time")); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The script did not output the expected output.")); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("We were unable to take a screenshot of the VM.")); + } + + @Test + public void processValidationAnswerTestBootValidatedScriptResultTrueScreenshotPathNotNull() { + ValidateKbossVmAnswer answer = Mockito.mock(ValidateKbossVmAnswer.class); + doReturn(true).when(answer).getResult(); + doReturn(true).when(answer).isBootValidated(); + doReturn(null).when(answer).getScriptResult(); + doReturn("snap-path").when(answer).getScreenshotPath(); + + ValidateKbossVmCommand cmd = Mockito.mock(ValidateKbossVmCommand.class); + doReturn(true).when(cmd).isWaitForBoot(); + doReturn(true).when(cmd).isExecuteScript(); + doReturn(true).when(cmd).isTakeScreenshot(); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(answer, backupVoMock, userVmVOMock, hostVOMock, cmd); + + assertTrue(result); + verify(kbossBackupProviderSpy, never()).setBackupAsInvalidAndSendAlert(any(), any()); + verify(backupDetailDaoMock).addDetail(eq(backupId), eq(BackupDetailsDao.SCREENSHOT_PATH), eq("snap-path"), eq(false)); + } + + @Test + public void getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommandsTestNoParents() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + + Pair, InternalBackupJoinVO> result = + kbossBackupProviderSpy.getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVoMock, new Commands(Command.OnError.Stop)); + + assertEquals(List.of(), result.first()); + assertNull(result.second()); + } + + @Test + public void getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommandsTestAliveParents() { + doReturn(List.of(internalBackupJoinVoMock)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock).getStatus(); + + Pair, InternalBackupJoinVO> result = + kbossBackupProviderSpy.getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVoMock, new Commands(Command.OnError.Stop)); + + assertEquals(List.of(), result.first()); + assertEquals(internalBackupJoinVoMock, result.second()); + } + + @Test + public void getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommandsTestDeadParents() { + InternalBackupJoinVO deadParent = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.Removed).when(deadParent).getStatus(); + doReturn(backupId).when(deadParent).getId(); + + doReturn(List.of(deadParent)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + Commands commands = new Commands(Command.OnError.Stop); + + Pair, InternalBackupJoinVO> result = + kbossBackupProviderSpy.getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVoMock, commands); + + assertEquals(List.of(deadParent), result.first()); + assertNull(result.second()); + verify(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(backupId, commands); + } + + @Test + public void getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommandsTestDeadAndAliveParents() { + InternalBackupJoinVO deadParent = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.Removed).when(deadParent).getStatus(); + doReturn(backupId).when(deadParent).getId(); + + doReturn(List.of(deadParent, internalBackupJoinVoMock)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + Commands commands = new Commands(Command.OnError.Stop); + + Pair, InternalBackupJoinVO> result = + kbossBackupProviderSpy.getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVoMock, commands); + + assertEquals(List.of(deadParent), result.first()); + assertEquals(internalBackupJoinVoMock, result.second()); + verify(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(backupId, commands); + } + + @Test + public void configureValidationStepsTestScreenshot() { + long backupOfferingId = 32L; + + doReturn(backupOfferingId).when(backupVoMock).getBackupOfferingId(); + doReturn(backupOfferingId).when(backupOfferingMock).getId(); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(backupOfferingId); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(backupOfferingId, ApiConstants.VALIDATION_STEPS); + doReturn("screenshot").when(backupOfferingDetailsVoMock).getValue(); + + ValidateKbossVmCommand cmd = new ValidateKbossVmCommand(null, null); + + kbossBackupProviderSpy.configureValidationSteps(cmd, backupVoMock); + + assertTrue(cmd.isTakeScreenshot()); + assertFalse(cmd.isWaitForBoot()); + assertFalse(cmd.isExecuteScript()); + } + + @Test + public void configureValidationStepsTestWaitForBoot() { + long backupOfferingId = 32L; + + doReturn(backupOfferingId).when(backupVoMock).getBackupOfferingId(); + doReturn(backupOfferingId).when(backupOfferingMock).getId(); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(backupOfferingId); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(backupOfferingId, ApiConstants.VALIDATION_STEPS); + doReturn("wait_for_boot").when(backupOfferingDetailsVoMock).getValue(); + + ValidateKbossVmCommand cmd = new ValidateKbossVmCommand(null, null); + + kbossBackupProviderSpy.configureValidationSteps(cmd, backupVoMock); + + assertFalse(cmd.isTakeScreenshot()); + assertTrue(cmd.isWaitForBoot()); + assertFalse(cmd.isExecuteScript()); + } + + @Test + public void configureValidationStepsTestExecuteCommand() { + long backupOfferingId = 32L; + + doReturn(backupOfferingId).when(backupVoMock).getBackupOfferingId(); + doReturn(backupOfferingId).when(backupOfferingMock).getId(); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(backupOfferingId); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(backupOfferingId, ApiConstants.VALIDATION_STEPS); + doReturn("execute_command").when(backupOfferingDetailsVoMock).getValue(); + doReturn(vmInstanceDetailVoMock).when(vmInstanceDetailsDaoMock).findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND); + doReturn(vmId).when(backupVoMock).getVmId(); + + ValidateKbossVmCommand cmd = new ValidateKbossVmCommand(null, null); + + kbossBackupProviderSpy.configureValidationSteps(cmd, backupVoMock); + + assertFalse(cmd.isTakeScreenshot()); + assertFalse(cmd.isWaitForBoot()); + assertTrue(cmd.isExecuteScript()); + } + + @Test + public void configureValidationStepsTestAllSteps() { + long backupOfferingId = 32L; + + doReturn(backupOfferingId).when(backupVoMock).getBackupOfferingId(); + doReturn(backupOfferingId).when(backupOfferingMock).getId(); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(backupOfferingId); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(backupOfferingId, ApiConstants.VALIDATION_STEPS); + doReturn("screenshot,wait_for_boot,execute_command").when(backupOfferingDetailsVoMock).getValue(); + doReturn(vmInstanceDetailVoMock).when(vmInstanceDetailsDaoMock).findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND); + doReturn(vmId).when(backupVoMock).getVmId(); + + ValidateKbossVmCommand cmd = new ValidateKbossVmCommand(null, null); + + kbossBackupProviderSpy.configureValidationSteps(cmd, backupVoMock); + + assertTrue(cmd.isTakeScreenshot()); + assertTrue(cmd.isWaitForBoot()); + assertTrue(cmd.isExecuteScript()); + } + + @Test + public void validateCompressionStateForRestoreAndGetBackupTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + Pair result = kbossBackupProviderSpy.validateCompressionStateForRestoreAndGetBackup(backupId); + + assertFalse(result.first()); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateCompressionStateForRestoreAndGetBackupTestFinalizingCompression() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.CompressionStatus.FinalizingCompression).when(backupVoMock).getCompressionStatus(); + + Pair result = kbossBackupProviderSpy.validateCompressionStateForRestoreAndGetBackup(backupId); + + assertFalse(result.first()); + verify(backupVoMock).getCompressionStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateCompressionStateForRestoreAndGetBackupTestValidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.CompressionStatus.Compressed).when(backupVoMock).getCompressionStatus(); + + Pair result = kbossBackupProviderSpy.validateCompressionStateForRestoreAndGetBackup(backupId); + + assertTrue(result.first()); + assertEquals(backupVoMock, result.second()); + verify(backupVoMock).getCompressionStatus(); + verify(backupVoMock).setStatus(Backup.Status.Restoring); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertFalse(result); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestInvalidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Restoring).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertFalse(result); + verify(backupVoMock, Mockito.atLeast(1)).getStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestCompressing() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(Backup.CompressionStatus.Compressing).when(backupVoMock).getCompressionStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertFalse(result); + verify(backupVoMock).getCompressionStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestValidating() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(Backup.CompressionStatus.Compressed).when(backupVoMock).getCompressionStatus(); + doReturn(Backup.ValidationStatus.Validating).when(backupVoMock).getValidationStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertFalse(result); + verify(backupVoMock).getValidationStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestValidStates() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(Backup.CompressionStatus.Compressed).when(backupVoMock).getCompressionStatus(); + doReturn(Backup.ValidationStatus.UnableToValidate).when(backupVoMock).getValidationStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertTrue(result); + verify(backupVoMock).getStatus(); + verify(backupVoMock).getValidationStatus(); + verify(backupVoMock).getCompressionStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForStartCompressionAndUpdateCompressionStatusTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + Pair result = kbossBackupProviderSpy.validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForStartCompressionAndUpdateCompressionStatusTestInvalidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + assertFalse(result.first()); + verify(backupVoMock, Mockito.atLeastOnce()).getStatus(); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForStartCompressionAndUpdateCompressionStatusTestValidStates() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + assertTrue(result.first()); + assertEquals(backupVoMock, result.second()); + verify(backupVoMock).getStatus(); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.Compressing); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestRestoringBackup() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Restoring).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestRestoringChildBackup() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(List.of(internalBackupJoinVoMock)).when(kbossBackupProviderSpy).getBackupJoinChildren(backupVoMock); + doReturn(Backup.Status.Restoring).when(internalBackupJoinVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestAllBackedUp() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(List.of(internalBackupJoinVoMock)).when(kbossBackupProviderSpy).getBackupJoinChildren(backupVoMock); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertTrue(result.first()); + assertEquals(backupVoMock, result.second()); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.FinalizingCompression); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestRemovedBackup() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Removed).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertTrue(result.first()); + assertEquals(backupVoMock, result.second()); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.CompressionError); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRestoreBackupToVMTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + Pair result = kbossBackupProviderSpy.validateBackupStateForRestoreBackupToVM(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRestoreBackupToVMTestBackedUp() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForRestoreBackupToVM(backupId); + + assertTrue(result.first()); + assertEquals(Backup.Status.BackedUp, result.second()); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRestoreBackupToVMTestRestoring() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Restoring).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForRestoreBackupToVM(backupId); + + assertTrue(result.first()); + assertEquals(Backup.Status.Restoring, result.second()); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRestoreBackupToVMTestError() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForRestoreBackupToVM(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + + @Test + public void validateBackupStateForValidationTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + boolean result = kbossBackupProviderSpy.validateBackupStateForValidation(backupId); + + assertFalse(result); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForValidationTestInvalidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Removed).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForValidation(backupId); + + assertFalse(result); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForValidationTestValidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForValidation(backupId); + + assertTrue(result); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + +} diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index f5fecc5de6f1..d73efc51be1a 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -51,7 +51,6 @@ import com.cloud.vm.snapshot.dao.VMSnapshotDao; import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; - import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupRepositoryDao; @@ -551,7 +550,7 @@ protected Host getVMHypervisorHostForBackup(VirtualMachine vm) { } @Override - public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM) { + public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM, boolean isolated, Long scheduleId) { final Host host = getVMHypervisorHostForBackup(vm); final BackupRepository backupRepository = backupRepositoryDao.findByBackupOfferingId(vm.getBackupOfferingId()); @@ -680,12 +679,12 @@ private BackupVO createBackupObject(VirtualMachine vm, String backupPath, String } @Override - public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore) { return restoreVMBackup(vm, backup); } @Override - public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { return restoreVMBackup(vm, backup).first(); } @@ -786,7 +785,8 @@ private String getVolumePathSuffix(StoragePoolVO storagePool) { } @Override - public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { final VolumeVO volume = volumeDao.findByUuid(backupVolumeInfo.getUuid()); final DiskOffering diskOffering = diskOfferingDao.findByUuid(backupVolumeInfo.getDiskOfferingId()); final StoragePoolVO pool = primaryDataStoreDao.findByUuid(dataStoreUuid); diff --git a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java index cd08378b926a..09eb877e0ab1 100644 --- a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java +++ b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java @@ -251,7 +251,7 @@ public void takeBackupSuccessfully() throws AgentUnavailableException, Operation Mockito.when(backupDao.persist(Mockito.any(BackupVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); Mockito.when(backupDao.update(Mockito.anyLong(), Mockito.any(BackupVO.class))).thenReturn(true); - Pair result = nasBackupProvider.takeBackup(vm, false); + Pair result = nasBackupProvider.takeBackup(vm, false, false, null); Assert.assertTrue(result.first()); Assert.assertNotNull(result.second()); @@ -569,7 +569,7 @@ public void restoreClearsActiveCheckpointDetail() throws AgentUnavailableExcepti Mockito.when(existing.getValue()).thenReturn("backup-1715000000"); Mockito.when(vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID)).thenReturn(existing); - boolean ok = nasBackupProvider.restoreVMFromBackup(vm, backup); + boolean ok = nasBackupProvider.restoreVMFromBackup(vm, backup, false, null); Assert.assertTrue(ok); Mockito.verify(vmInstanceDetailsDao).removeDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); } @@ -636,7 +636,7 @@ public void restoreBackedUpVolumeClearsTargetVmActiveCheckpoint() Mockito.when(vmInstanceDetailsDao.findDetail(targetVmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID)).thenReturn(existing); Pair result = nasBackupProvider.restoreBackedUpVolume( - backup, backedUp, hostIp, dsUuid, new Pair<>(targetVmName, VirtualMachine.State.Stopped)); + backup, backedUp, hostIp, dsUuid, new Pair<>(targetVmName, VirtualMachine.State.Stopped), null, false); Assert.assertTrue(result.first()); Mockito.verify(vmInstanceDetailsDao).removeDetail(targetVmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); diff --git a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java index 4cf4bd111ef1..31186385d578 100644 --- a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java +++ b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java @@ -357,7 +357,7 @@ public boolean removeVMFromBackupOffering(VirtualMachine vm) { } @Override - public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { String networkerServer; HostVO hostVO; @@ -407,7 +407,8 @@ public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { } @Override - public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { String networkerServer; VolumeVO volume = volumeDao.findByUuid(backupVolumeInfo.getUuid()); final DiskOffering diskOffering = diskOfferingDao.findByUuid(backupVolumeInfo.getDiskOfferingId()); @@ -491,7 +492,7 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI } @Override - public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM) { + public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated, Long scheduleId) { String networkerServer; String clusterName; @@ -648,7 +649,7 @@ public void syncBackupStorageStats(Long zoneId) { public boolean willDeleteBackupsOnOfferingRemoval() { return false; } @Override - public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore) { return new Pair<>(true, null); } } diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index 39970dab3427..361b3349b011 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -219,7 +219,7 @@ public boolean willDeleteBackupsOnOfferingRemoval() { } @Override - public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM) { + public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM, boolean isolated, Long scheduleId) { final VeeamClient client = getClient(vm.getDataCenterId()); Boolean result = client.startBackupJob(vm.getBackupExternalId()); return new Pair<>(result, null); @@ -256,7 +256,7 @@ public boolean deleteBackup(Backup backup, boolean forced) { } @Override - public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { final String restorePointId = backup.getExternalId(); try { return getClient(vm.getDataCenterId()).restoreFullVM(vm.getInstanceName(), restorePointId); @@ -291,7 +291,8 @@ private void prepareForBackupRestoration(VirtualMachine vm) { } @Override - public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { final Long zoneId = backup.getZoneId(); final String restorePointId = backup.getExternalId(); return getClient(zoneId).restoreVMToDifferentLocation(restorePointId, null, hostIp, dataStoreUuid); @@ -337,7 +338,7 @@ public List listRestorePoints(VirtualMachine vm) { } @Override - public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore) { final Long zoneId = backup.getZoneId(); final String restorePointId = backup.getExternalId(); final String restoreLocation = vm.getInstanceName(); diff --git a/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/HypervResourceController.cs b/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/HypervResourceController.cs index 7e31ced3e389..84c67d7a9650 100644 --- a/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/HypervResourceController.cs +++ b/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/HypervResourceController.cs @@ -1159,7 +1159,11 @@ public JContainer StartCommand([FromBody]dynamic cmd) try { string systemVmIsoPath = null; - String uriStr = (String)cmd.secondaryStorage; + String uriStr; + foreach (var item in cmd.secondaryStorages) + { + uriStr = item; + } if (!String.IsNullOrEmpty(uriStr)) { NFSTO share = new NFSTO(); diff --git a/plugins/hypervisors/hyperv/src/main/java/com/cloud/hypervisor/hyperv/resource/HypervDirectConnectResource.java b/plugins/hypervisors/hyperv/src/main/java/com/cloud/hypervisor/hyperv/resource/HypervDirectConnectResource.java index 6ad06f426a79..12d6c42fd7e6 100644 --- a/plugins/hypervisors/hyperv/src/main/java/com/cloud/hypervisor/hyperv/resource/HypervDirectConnectResource.java +++ b/plugins/hypervisors/hyperv/src/main/java/com/cloud/hypervisor/hyperv/resource/HypervDirectConnectResource.java @@ -455,7 +455,7 @@ public final Answer executeRequest(final Command cmd) { if (s_hypervMgr != null) { final String secondary = s_hypervMgr.prepareSecondaryStorageStore(Long.parseLong(zoneId)); if (secondary != null) { - ((StartCommand)cmd).setSecondaryStorage(secondary); + ((StartCommand)cmd).setSecondaryStorages(List.of(secondary)); } } else { logger.error("Hyperv manager isn't available. Couldn't check and copy the System VM ISO."); diff --git a/plugins/hypervisors/kvm/pom.xml b/plugins/hypervisors/kvm/pom.xml index 255ada09ef4f..a00530268e83 100644 --- a/plugins/hypervisors/kvm/pom.xml +++ b/plugins/hypervisors/kvm/pom.xml @@ -62,6 +62,21 @@ rados ${cs.rados-java.version}
+ + com.github.jai-imageio + jai-imageio-core + 1.4.0 + + + com.dynatrace.hash4j + hash4j + 0.29.0 + + + com.mikesamuel + json-sanitizer + 1.2.3 + com.linbit.linstor.api java-linstor diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java index d360aa481372..ab4513642efa 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java @@ -27,18 +27,14 @@ import org.libvirt.event.BlockJobStatus; import org.libvirt.event.BlockJobType; -import java.util.concurrent.Semaphore; - public class BlockCommitListener implements BlockJobListener { - private Semaphore semaphore; private String result; private String vmName; private Logger logger; private String logid; - protected BlockCommitListener(Semaphore semaphore, String vmName, String logid) { - this.semaphore = semaphore; + protected BlockCommitListener(String vmName, String logid) { this.vmName = vmName; this.logid = logid; logger = LogManager.getLogger(getClass()); @@ -54,24 +50,22 @@ public void onEvent(Domain domain, String diskPath, BlockJobType type, BlockJobS return; } + ThreadContext.put("logcontextid", logid); + logger.debug("Received status [{}] on disk [{}] while listening for block commit of VM [{}].", status, diskPath, vmName); switch (status) { case COMPLETED: result = null; - semaphore.release(); return; case READY: try { - ThreadContext.put("logcontextid", logid); logger.debug("Pivoting disk [{}] of VM [{}].", diskPath, vmName); domain.blockJobAbort(diskPath, Domain.BlockJobAbortFlags.PIVOT); } catch (LibvirtException ex) { result = String.format("Failed to pivot disk due to [%s].", ex.getMessage()); - semaphore.release(); } return; default: result = String.format("Failed to block commit disk with status [%s].", status); - semaphore.release(); } } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index acc4a878deaa..4281036d9456 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -36,6 +36,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; @@ -51,8 +52,6 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -74,6 +73,9 @@ import javax.xml.xpath.XPathFactory; import com.cloud.agent.api.to.VirtualMachineMetadataTO; +import com.cloud.utils.exception.BackupException; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import com.cloud.agent.api.to.DataObjectType; import org.apache.cloudstack.api.ApiConstants.IoDriverPolicy; import org.apache.cloudstack.command.CommandInfo; import org.apache.cloudstack.command.ReconcileCommandService; @@ -394,6 +396,20 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static final int IMAGE_SERVER_DEFAULT_PORT = 54322; public static final String IMAGE_SERVER_SYSTEMD_UNIT_NAME = "cloudstack-image-server"; + private static final String BLOCK_PULL_COMMAND = "virsh blockpull --domain %s --path %s"; + + private static final String SNAPSHOT_XML = "\n" + + "%s\n" + + "\n" + + " \n" + + "%s" + + " \n" + + ""; + + private static final String TAG_DISK_SNAPSHOT = "\n" + + "\n" + + "\n"; + protected int qcow2DeltaMergeTimeout; private String modifyVlanPath; @@ -594,6 +610,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static final String CGROUP_V2 = "cgroup2fs"; + public static final String AGENT_IS_NOT_CONNECTED = "QEMU guest agent is not connected"; + /** * Virsh command to merge (blockcommit) snapshot into the base file.

* 1st parameter: VM's name;
@@ -612,7 +630,7 @@ public long getHypervisorQemuVersion() { @Override public synchronized void registerStatusUpdater(AgentStatusUpdater updater) { - if (AgentPropertiesFileHandler.getPropertyValue(AgentProperties.LIBVIRT_EVENTS_ENABLED)) { + if (isLibvirtEventsEnabled()) { try { Connect conn = LibvirtConnection.getConnection(); if (libvirtDomainListener != null) { @@ -2358,7 +2376,7 @@ public String startVM(final Connect conn, final String vmName, final String doma public boolean stop() { try { final Connect conn = LibvirtConnection.getConnection(); - if (AgentPropertiesFileHandler.getPropertyValue(AgentProperties.LIBVIRT_EVENTS_ENABLED) && libvirtDomainListener != null) { + if (isLibvirtEventsEnabled() && libvirtDomainListener != null) { LOGGER.debug("Clearing old domain listener"); conn.removeLifecycleListener(libvirtDomainListener); } @@ -4409,6 +4427,7 @@ public StartupCommand[] initialize() { if (hostSupportsOvfExport()) { cmd.getHostDetails().put(HOST_OVFTOOL_VERSION, getHostOvfToolVersion()); } + addBackupJobDetails(cmd.getHostDetails()); HealthCheckResult healthCheckResult = getHostHealthCheckResult(); if (healthCheckResult != HealthCheckResult.IGNORE) { cmd.setHostHealthCheckResult(healthCheckResult == HealthCheckResult.SUCCESS); @@ -4442,6 +4461,18 @@ public StartupCommand[] initialize() { return startupCommandsArray; } + private void addBackupJobDetails(Map details) { + Integer maxCompressionOperations = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.BACKUP_COMPRESSION_MAX_CONCURRENT_OPERATIONS_PER_HOST); + if (maxCompressionOperations != null) { + details.put(AgentProperties.BACKUP_COMPRESSION_MAX_CONCURRENT_OPERATIONS_PER_HOST.getName(), maxCompressionOperations.toString()); + } + + Integer maxValidationOperations = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.BACKUP_VALIDATION_MAX_CONCURRENT_OPERATIONS_PER_HOST); + if (maxValidationOperations != null) { + details.put(AgentProperties.BACKUP_VALIDATION_MAX_CONCURRENT_OPERATIONS_PER_HOST.getName(), maxValidationOperations.toString()); + } + } + protected List getHostTags() { List hostTagsList = new ArrayList<>(); String hostTags = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_TAGS); @@ -5147,7 +5178,7 @@ public DiskDef getDiskWithPathOfVolumeObjectTO(List disks, VolumeObject return disks.stream() .filter(diskDef -> diskDef.getDiskPath() != null && diskDef.getDiskPath().contains(vol.getPath())) .findFirst() - .orElseThrow(() -> new CloudRuntimeException(String.format("Unable to find volume [%s].", vol.getUuid()))); + .orElseThrow(() -> new CloudRuntimeException(String.format("Unable to find volume [%s] with path [%s].", vol.getUuid(), vol.getPath()))); } protected String getDiskPathFromDiskDef(DiskDef disk) { @@ -6579,7 +6610,7 @@ public static String generateSecretUUIDFromString(String seed) { } /** - * Merges the snapshot into base file. + * Merges the delta into a base file. * * @param vm Domain of the VM; * @param diskLabel Disk label to manage snapshot and base file; @@ -6591,15 +6622,19 @@ public static String generateSecretUUIDFromString(String seed) { * @param conn Libvirt connection; * @throws LibvirtException */ - public void mergeSnapshotIntoBaseFile(Domain vm, String diskLabel, String baseFilePath, String topFilePath, boolean active, String snapshotName, VolumeObjectTO volume, + public void mergeDeltaIntoBaseFile(Domain vm, String diskLabel, String baseFilePath, String topFilePath, boolean active, String snapshotName, VolumeObjectTO volume, Connect conn) throws LibvirtException { - if (AgentPropertiesFileHandler.getPropertyValue(AgentProperties.LIBVIRT_EVENTS_ENABLED)) { + if (isLibvirtEventsEnabled()) { mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(vm, diskLabel, baseFilePath, topFilePath, active, snapshotName, volume, conn); } else { mergeSnapshotIntoBaseFileWithoutEvents(vm, diskLabel, baseFilePath, topFilePath, active, snapshotName, volume, conn); } } + protected Boolean isLibvirtEventsEnabled() { + return AgentPropertiesFileHandler.getPropertyValue(AgentProperties.LIBVIRT_EVENTS_ENABLED); + } + /** * This method only works if LIBVIRT_EVENTS_ENABLED is true. * */ @@ -6616,40 +6651,26 @@ protected void mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(Domain commitFlags |= Domain.BlockCommitFlags.ACTIVE; } - Semaphore semaphore = getSemaphoreToWaitForMerge(); - BlockCommitListener blockCommitListener = getBlockCommitListener(semaphore, vmName); - vm.addBlockJobListener(blockCommitListener); - - logger.info("Starting block commit of snapshot [{}] of VM [{}]. Using parameters: diskLabel [{}]; baseFilePath [{}]; topFilePath [{}]; commitFlags [{}]", snapshotName, - vmName, diskLabel, baseFilePath, topFilePath, commitFlags); + BlockCommitListener blockCommitListener = getBlockCommitListener(vmName); + try { + vm.addBlockJobListener(blockCommitListener); - vm.blockCommit(diskLabel, baseFilePath, topFilePath, 0, commitFlags); + logger.info("Starting block commit of QCOW2 delta [{}] of VM [{}]. Using parameters: diskLabel [{}]; baseFilePath [{}]; topFilePath [{}]; commitFlags [{}]", + snapshotName, + vmName, diskLabel, baseFilePath, topFilePath, commitFlags); - Thread checkProgressThread = new Thread(() -> checkBlockCommitProgress(vm, diskLabel, vmName, snapshotName, topFilePath, baseFilePath)); - checkProgressThread.start(); + vm.blockCommit(diskLabel, baseFilePath, topFilePath, 0, commitFlags); - String errorMessage = String.format("the block commit of top file [%s] into base file [%s] for snapshot [%s] of VM [%s]." + - " The job will be left running to avoid data corruption, but ACS will return an error and volume [%s] will need to be normalized manually. If the commit" + - " involved the active image, the pivot will need to be manually done.", topFilePath, baseFilePath, snapshotName, vmName, volume); - try { - if (!semaphore.tryAcquire(qcow2DeltaMergeTimeout, TimeUnit.SECONDS)) { - throw new CloudRuntimeException("Timed out while waiting for " + errorMessage); - } - } catch (InterruptedException e) { - throw new CloudRuntimeException("Interrupted while waiting for " + errorMessage); + checkBlockCommitProgress(vm, diskLabel, vmName, snapshotName, topFilePath, baseFilePath); } finally { vm.removeBlockJobListener(blockCommitListener); } String mergeResult = blockCommitListener.getResult(); - try { - checkProgressThread.join(); - } catch (InterruptedException ex) { - throw new CloudRuntimeException(String.format("Exception while running wait block commit task of snapshot [%s] and VM [%s].", snapshotName, vmName)); - } - if (mergeResult != null) { - String commitError = String.format("Failed %s The failure occurred due to [%s].", errorMessage, mergeResult); + String commitError = String.format("Failed the block commit of top file [%s] into base file [%s] for snapshot [%s] of VM [%s]. The job will be left running to avoid" + + " data corruption, but ACS will return an error and volume [%s] will need to be normalized manually. If the commit involved the active image, the pivot will" + + " need to be manually done. The failure occurred due to [%s].", topFilePath, baseFilePath, snapshotName, vmName, volume, mergeResult); logger.error(commitError); throw new CloudRuntimeException(commitError); } @@ -6706,15 +6727,8 @@ protected String buildMergeCommand(String vmName, String diskLabel, String baseF /** * This was created to facilitate testing. * */ - protected BlockCommitListener getBlockCommitListener(Semaphore semaphore, String vmName) { - return new BlockCommitListener(semaphore, vmName, ThreadContext.get("logcontextid")); - } - - /** - * This was created to facilitate testing. - * */ - protected Semaphore getSemaphoreToWaitForMerge() { - return new Semaphore(0); + protected BlockCommitListener getBlockCommitListener(String vmName) { + return new BlockCommitListener(vmName, ThreadContext.get("logcontextid")); } protected void checkBlockCommitProgress(Domain vm, String diskLabel, String vmName, String snapshotName, String topFilePath, String baseFilePath) { @@ -6730,8 +6744,8 @@ protected void checkBlockCommitProgress(Domain vm, String diskLabel, String vmNa try { Thread.sleep(1000); } catch (InterruptedException ex) { - logger.debug("Thread that was tracking the progress {} was interrupted.", partialLog, ex); - return; + logger.trace("Thread that was tracking the progress for the block commit job {} was interrupted. Ignoring.", partialLog, ex); + continue; } try { @@ -7141,4 +7155,261 @@ static boolean checkIfVolumeGroupIsClustered(String vgName) { return false; } + + public Map createDiskOnlyVmSnapshotForRunningVm(List> volumeTosAndNewPaths, String vmName, String snapshotName, + boolean quiesceVm) throws BackupException { + logger.info("Taking disk-only VM snapshot of running VM [{}].", vmName); + + Domain dm = null; + try { + LibvirtUtilitiesHelper libvirtUtilitiesHelper = getLibvirtUtilitiesHelper(); + Connect conn = libvirtUtilitiesHelper.getConnection(); + List disks = getDisks(conn, vmName); + + dm = getDomain(conn, vmName); + + if (dm == null) { + throw new BackupException(String.format("Creation of disk-only VM snapshot failed as we could not find the VM [%s].", vmName), true); + } + + Pair> snapshotXmlAndVolumeToNewPathMap = createSnapshotXmlAndNewVolumePathMap(volumeTosAndNewPaths, disks, snapshotName); + + int flagsToUseForRunningVmSnapshotCreation = getFlagsToUseForRunningVmSnapshotCreation(quiesceVm); + String snapshotXml = snapshotXmlAndVolumeToNewPathMap.first(); + + logger.info("Creating disk-only VM snapshot for VM [{}] using parameters: snapshotXml [{}]; flags [{}].", vmName, snapshotXml, flagsToUseForRunningVmSnapshotCreation); + + dm.snapshotCreateXML(snapshotXml, flagsToUseForRunningVmSnapshotCreation); + + return snapshotXmlAndVolumeToNewPathMap.second(); + } catch (LibvirtException e) { + String errorMsg = String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage()); + boolean isVmConsistent = false; + if (e.getMessage().contains(AGENT_IS_NOT_CONNECTED)) { + errorMsg = "QEMU guest agent is not connected. If the VM has been recently started, it might connect soon. Otherwise the VM does not have the" + + " guest agent installed; thus the QuiesceVM parameter is not supported."; + isVmConsistent = true; + } + logger.error(errorMsg, e); + throw new BackupException(errorMsg, isVmConsistent); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException l) { + logger.trace("Ignoring Libvirt error.", l); + } + } + } + } + + public Map createDiskOnlyVMSnapshotOfStoppedVm(List> volumeTosAndNewPaths, String vmName) { + logger.info("Creating volume deltas for stopped VM [{}].", vmName); + + Map mapVolumeToSnapshotSize = new HashMap<>(); + try { + for (Pair volumeObjectTOAndNewPath : volumeTosAndNewPaths) { + VolumeObjectTO volumeObjectTO = volumeObjectTOAndNewPath.first(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); + KVMStoragePool kvmStoragePool = getStoragePoolMgr().getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + + String snapshotPath = volumeObjectTOAndNewPath.second(); + String snapshotFullPath = kvmStoragePool.getLocalPathFor(snapshotPath); + QemuImgFile newDelta = new QemuImgFile(snapshotFullPath, QemuImg.PhysicalDiskFormat.QCOW2); + + String currentDeltaFullPath = kvmStoragePool.getLocalPathFor(volumeObjectTO.getPath()); + QemuImgFile currentDelta = new QemuImgFile(currentDeltaFullPath, QemuImg.PhysicalDiskFormat.QCOW2); + + QemuImg qemuImg = new QemuImg(0); + + logger.debug("Creating new delta [{}] for volume [{}] as part of the delta creation process for VM [{}].", newDelta, volumeObjectTO.getUuid(), vmName); + qemuImg.create(newDelta, currentDelta); + + mapVolumeToSnapshotSize.put(volumeObjectTO.getUuid(), getFileSize(currentDeltaFullPath)); + } + } catch (Exception e) { + logger.error("Exception while creating volume delta for VM [{}]. Deleting leftover deltas.", vmName, e); + cleanupLeftoverDeltas(volumeTosAndNewPaths, mapVolumeToSnapshotSize); + throw new BackupException(String.format("An exception was caught during the delta creation for VM [%s]. The leftover deltas have been deleted.", vmName), true); + } + + return mapVolumeToSnapshotSize; + } + + protected void cleanupLeftoverDeltas(List> volumeTosAndNewPaths, Map mapVolumeToSnapshotSize) { + for (Pair volumeObjectTOAndNewPath : volumeTosAndNewPaths) { + VolumeObjectTO volumeObjectTO = volumeObjectTOAndNewPath.first(); + Long volSize = mapVolumeToSnapshotSize.get(volumeObjectTO.getUuid()); + if (volSize == null) { + continue; + } + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); + KVMStoragePool kvmStoragePool = getStoragePoolMgr().getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + try { + Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volumeObjectTOAndNewPath.second()))); + } catch (IOException ex) { + logger.warn("Tried to delete leftover delta at [{}]. Failed.", volumeObjectTOAndNewPath.second(), ex); + } + } + } + + public void mergeDeltaForStoppedVm(DeltaMergeTreeTO deltaMergeTreeTO) throws QemuImgException, IOException, LibvirtException { + logger.debug("Merging delta [{}] for stopped VM.", deltaMergeTreeTO); + + QemuImg qemuImg = new QemuImg(qcow2DeltaMergeTimeout * 1000); + DataTO parentTo = deltaMergeTreeTO.getParent(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) parentTo.getDataStore(); + KVMStoragePool storagePool = storagePoolManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + String childLocalPath = storagePool.getLocalPathFor(deltaMergeTreeTO.getChild().getPath()); + + QemuImgFile parent = new QemuImgFile(storagePool.getLocalPathFor(parentTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile child = new QemuImgFile(childLocalPath, QemuImg.PhysicalDiskFormat.QCOW2); + + logger.debug("Committing child delta [{}] into parent delta [{}].", parentTo, deltaMergeTreeTO.getChild()); + qemuImg.commit(child, parent, true); + + List grandChildren = deltaMergeTreeTO.getGrandChildren().stream() + .map(deltaTo -> new QemuImgFile(storagePool.getLocalPathFor(deltaTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2)) + .collect(Collectors.toList()); + + logger.debug("Rebasing grand-children [{}] into parent at [{}].", grandChildren, parent.getFileName()); + for (QemuImgFile grandChild : grandChildren) { + qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false); + } + + logger.debug("Deleting child at [{}] as it is useless.", childLocalPath); + + Files.deleteIfExists(Path.of(childLocalPath)); + } + + public void mergeDeltaForRunningVm(DeltaMergeTreeTO mergeTreeTO, String vmName, VolumeObjectTO volumeObjectTO) throws LibvirtException, QemuImgException { + logger.debug("Merging delta [{}] for running VM [{}].", mergeTreeTO, vmName); + + QemuImg qemuImg = new QemuImg(qcow2DeltaMergeTimeout * 1000); + Connect conn = libvirtUtilitiesHelper.getConnection(); + Domain domain = getDomain(conn, vmName); + List disks = getDisks(conn, vmName); + + DataTO childTO = mergeTreeTO.getChild(); + DataTO parentSnapshotTO = mergeTreeTO.getParent(); + KVMStoragePool storagePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(volumeObjectTO, storagePoolManager); + + boolean active = DataObjectType.VOLUME.equals(childTO.getObjectType()); + String label = getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO).getDiskLabel(); + String parentSnapshotLocalPath = storagePool.getLocalPathFor(parentSnapshotTO.getPath()); + String childDeltaPath = storagePool.getLocalPathFor(childTO.getPath()); + + logger.debug("Found label [{}] for [{}]. Will merge delta at [{}] into delta at [{}].", label, volumeObjectTO, parentSnapshotLocalPath, childDeltaPath); + + mergeDeltaIntoBaseFile(domain, label, parentSnapshotLocalPath, childDeltaPath, active, childTO.getPath(), volumeObjectTO, conn); + + QemuImgFile parent = new QemuImgFile(parentSnapshotLocalPath, QemuImg.PhysicalDiskFormat.QCOW2); + + logger.debug("Rebasing grand-children [{}] into parent at [{}].", mergeTreeTO.getGrandChildren(), parentSnapshotLocalPath); + for (DataTO grandChildTo : mergeTreeTO.getGrandChildren()) { + if (checkIfFileIsInActiveChainForVm(domain, grandChildTo)) { + logger.debug("Grand-child [{}] is on the active chain of VM [{}], thus Libvirt has already rebased it, will ignore it.", grandChildTo, vmName); + continue; + } + QemuImgFile grandChild = new QemuImgFile(storagePool.getLocalPathFor(grandChildTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2); + qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false); + } + } + + private boolean checkIfFileIsInActiveChainForVm(Domain vm, DataTO dataTO) throws LibvirtException { + String xml = vm.getXMLDesc(0); + KVMStoragePool storagePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(dataTO, storagePoolManager); + return xml.contains(storagePool.getLocalPathFor(dataTO.getPath())); + } + + public int getFlagsToUseForRunningVmSnapshotCreation(boolean quiesceVm) { + int flags = quiesceVm ? Domain.SnapshotCreateFlags.QUIESCE : 0; + flags += Domain.SnapshotCreateFlags.DISK_ONLY + + Domain.SnapshotCreateFlags.ATOMIC + + Domain.SnapshotCreateFlags.NO_METADATA; + return flags; + } + + public Pair> createSnapshotXmlAndNewVolumePathMap(List> volumeTosAndNewPaths, List disks, String snapshotName) { + StringBuilder stringBuilder = new StringBuilder(); + Map volumeObjectToNewPathMap = new HashMap<>(); + + for (Pair volumeObjectTOAndPath : volumeTosAndNewPaths) { + LibvirtVMDef.DiskDef diskdef = getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTOAndPath.first()); + String newPath = volumeObjectTOAndPath.second(); + stringBuilder.append(String.format(TAG_DISK_SNAPSHOT, diskdef.getDiskLabel(), getSnapshotTemporaryPath(diskdef.getDiskPath(), newPath))); + + long snapSize = getFileSize(diskdef.getDiskPath()); + + volumeObjectToNewPathMap.put(volumeObjectTOAndPath.first().getUuid(), snapSize); + } + + String snapshotXml = String.format(SNAPSHOT_XML, snapshotName, stringBuilder); + return new Pair<>(snapshotXml, volumeObjectToNewPathMap); + } + + public long getFileSize(String path) { + return new File(path).length(); + } + + public boolean pullVolumeBackingFile(VolumeObjectTO volumeObjectTO, String vmName) throws LibvirtException { + Connect conn = libvirtUtilitiesHelper.getConnection(); + + Domain vm = getDomain(conn, vmName); + List disks = getDisks(conn, vmName); + DiskDef diskDef = getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO); + + String diskLabel = diskDef.getDiskLabel(); + Script.runSimpleBashScript(String.format(BLOCK_PULL_COMMAND, vmName, diskLabel)); + + boolean result = checkBlockPullProgress(vm, diskLabel, vmName, volumeObjectTO.getUuid()); + + if (!result) { + logger.warn("Failed to block pull volume [{}] of VM [{}], aborting.", volumeObjectTO, vmName); + vm.blockJobAbort(diskLabel, Domain.BlockJobAbortFlags.ASYNC); + } + return result; + } + + protected Boolean checkBlockPullProgress(Domain vm, String diskLabel, String vmName, String volumeUuid) { + int timeout = qcow2DeltaMergeTimeout; + DomainBlockJobInfo result; + long lastCommittedBytes = 0; + long endBytes = 0; + String partialLog = String.format("for volume [%s] of VM [%s]", volumeUuid, vmName); + while (timeout > 0) { + timeout -= 1; + + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + logger.trace("Thread that was tracking the block pull progress {} was interrupted. Ignoring.", partialLog, ex); + continue; + } + + try { + result = vm.getBlockJobInfo(diskLabel, 0); + } catch (LibvirtException ex) { + logger.warn("Exception while getting block job info {}: [{}].", partialLog, ex.getMessage(), ex); + return false; + } + + if (result == null || result.type == 0 && result.end == 0 && result.cur == 0) { + logger.debug("Block pull job {} has finished.", partialLog); + return true; + } + + long currentCommittedBytes = result.cur; + if (currentCommittedBytes > lastCommittedBytes) { + logger.debug("The block pull {} is at [{}] of [{}].", partialLog, currentCommittedBytes, result.end); + } + lastCommittedBytes = currentCommittedBytes; + endBytes = result.end; + } + logger.warn(String.format("Block pull %s has timed out after waiting at least %s seconds. The progress of the operation was [%s] of [%s].", partialLog, + qcow2DeltaMergeTimeout, lastCommittedBytes, endBytes)); + return false; + } + + } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java index e114669b8b56..1a4d23af1c90 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java @@ -131,6 +131,7 @@ public boolean parseDomainXML(String domXML) { fmt = DiskDef.DiskFmtType.valueOf(diskFmtType.toUpperCase()); } def.defFileBasedDisk(diskFile, diskLabel, DiskDef.DiskBus.valueOf(bus.toUpperCase()), fmt); + parseBackingFiles(disk, def); } else if (device.equalsIgnoreCase("cdrom")) { def.defISODisk(diskFile, i+1, diskLabel, DiskDef.DiskType.FILE); } @@ -405,6 +406,21 @@ public boolean parseDomainXML(String domXML) { return false; } + private void parseBackingFiles(Element disk, DiskDef def) { + NodeList backingStoreNodeList = disk.getElementsByTagName("backingStore"); + List backingStoreList = new ArrayList<>(); + while (backingStoreNodeList.getLength() > 0) { + Element backingStore = (Element)backingStoreNodeList.item(0); + String path = getAttrValue("source", "file", backingStore); + if (StringUtils.isEmpty(path)) { + break; + } + backingStoreList.add(path.substring(path.lastIndexOf(File.separator))+1); + backingStoreNodeList = backingStore.getElementsByTagName("backingStore"); + } + def.setBackingStoreList(backingStoreList); + } + /** * Parse the memballoon tag. * @param devices the devices tag. diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtMigrateResourceBetweenSecondaryStorages.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtMigrateResourceBetweenSecondaryStorages.java new file mode 100644 index 000000000000..a3dd2040cfc8 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtMigrateResourceBetweenSecondaryStorages.java @@ -0,0 +1,123 @@ +// +// 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 com.cloud.hypervisor.kvm.resource; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.resource.CommandWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; +import com.cloud.utils.script.Script; +import org.apache.cloudstack.utils.qemu.QemuImageOptions; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.libvirt.LibvirtException; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +public abstract class LibvirtMigrateResourceBetweenSecondaryStorages extends CommandWrapper { + + protected static final String BACKUP = "backup"; + protected static final String SNAPSHOT = "snapshot"; + + protected Set filesToRemove; + protected List> resourcesToUpdate; + protected String resourceType; + protected int wait; + + public String copyResourceToDestDataStore(DataTO resource, String resourceCurrentPath, KVMStoragePool destImagePool, String resourceParentPath) throws QemuImgException, LibvirtException { + String resourceDestDataStoreFullPath = destImagePool.getLocalPathFor(resource.getPath()); + String resourceDestCheckpointPath = resourceDestDataStoreFullPath.replace("snapshots", "checkpoints"); + + QemuImgFile resourceOrigin = new QemuImgFile(resourceCurrentPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile resourceDestination = new QemuImgFile(resourceDestDataStoreFullPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile parentResource = null; + + if (resourceParentPath != null) { + parentResource = new QemuImgFile(resourceParentPath, QemuImg.PhysicalDiskFormat.QCOW2); + } + + logger.debug("Migrating {} [{}] to [{}] with {}", resourceType, resourceOrigin, resourceDestination, parentResource == null ? "no parent." : String.format("parent [%s].", parentResource)); + + long resourceId = resource.getId(); + + createDirsIfNeeded(resourceDestDataStoreFullPath, resourceId); + + QemuImg qemuImg = new QemuImg(wait); + qemuImg.convert(resourceOrigin, resourceDestination, parentResource, null, null, new QemuImageOptions(resourceOrigin.getFormat(), resourceOrigin.getFileName(), null), + null, true, false, false, false, null, null); + + filesToRemove.add(resourceCurrentPath); + + if (SNAPSHOT.equals(resourceType)) { + String resourceCurrentCheckpointPath = resourceCurrentPath.replace("snapshots", "checkpoints"); + createDirsIfNeeded(resourceDestCheckpointPath, resourceId); + migrateCheckpointFile(resourceCurrentPath, resourceDestDataStoreFullPath); + filesToRemove.add(resourceCurrentCheckpointPath); + resourcesToUpdate.add(new Pair<>(resourceId, resourceDestCheckpointPath)); + } + + return resourceDestDataStoreFullPath; + } + + private void migrateCheckpointFile(String resourceCurrentPath, String resourceDestDataStoreFullPath) { + resourceCurrentPath = resourceCurrentPath.replace("snapshots", "checkpoints"); + resourceDestDataStoreFullPath = resourceDestDataStoreFullPath.replace("snapshots", "checkpoints"); + + String copyCommand = String.format("cp %s %s", resourceCurrentPath, resourceDestDataStoreFullPath); + Script.runSimpleBashScript(copyCommand); + } + + public void removeResourceFromSourceDataStore(String resourcePath) { + logger.debug("Removing file [{}].", resourcePath); + try { + Files.deleteIfExists(Path.of(resourcePath)); + } catch (IOException ex) { + logger.error("Failed to remove {} [{}].", resourceType, resourcePath, ex); + } + } + + public String rebaseResourceToNewParentPath(String resourcePath, String parentResourcePath) throws LibvirtException, QemuImgException { + QemuImgFile resource = new QemuImgFile(resourcePath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile parentResource = new QemuImgFile(parentResourcePath, QemuImg.PhysicalDiskFormat.QCOW2); + + QemuImg qemuImg = new QemuImg(wait); + qemuImg.rebase(resource, parentResource, parentResource.getFormat().toString(), false); + + return resourcePath; + } + + private void createDirsIfNeeded(String resourceFullPath, Long resourceId) { + String dirs = resourceFullPath.substring(0, resourceFullPath.lastIndexOf(File.separator)); + try { + Files.createDirectories(Path.of(dirs)); + } catch (IOException e) { + throw new BackupException(String.format("Error while creating directories for migration of %s [%s].", resourceType, resourceId), e, true); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStorageVolumeXMLParser.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStorageVolumeXMLParser.java index 1b6f73039ca5..00126a07cd3a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStorageVolumeXMLParser.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStorageVolumeXMLParser.java @@ -61,6 +61,26 @@ public LibvirtStorageVolumeDef parseStorageVolumeXML(String volXML) { return null; } + public String getBackingFileNameIfExists(String volXML) { + try { + DocumentBuilder builder = ParserUtils.getSaferDocumentBuilderFactory().newDocumentBuilder(); + + InputSource is = new InputSource(); + is.setCharacterStream(new StringReader(volXML)); + Document doc = builder.parse(is); + + Element rootElement = doc.getDocumentElement(); + Element backingStore = (Element)rootElement.getElementsByTagName("backingStore").item(0); + if (backingStore != null) { + String[] paths = getTagValue("path", backingStore).split("/"); + return paths[paths.length-1]; + } + } catch (ParserConfigurationException | SAXException | IOException e) { + logger.error(e.toString(), e); + } + return null; + } + private static String getTagValue(String tag, Element eElement) { NodeList nlList = eElement.getElementsByTagName(tag).item(0).getChildNodes(); Node nValue = nlList.item(0); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java index 7f6725b6d152..74529d9d5fa2 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java @@ -975,6 +975,7 @@ public String toString() { private BlockIOSize logicalBlockIOSize = null; private BlockIOSize physicalBlockIOSize = null; private DiskGeometry geometry = null; + private List backingStoreList = null; // Ordered list of backing stores, the first in the list is the immediate backing store, and the last in the list is the base public DiscardType getDiscard() { return _discard; @@ -1346,6 +1347,14 @@ public String getSourcePath() { return _sourcePath; } + public List getBackingStoreList() { + return backingStoreList; + } + + public void setBackingStoreList(List backingStoreList) { + this.backingStoreList = backingStoreList; + } + @Override public String toString() { StringBuilder diskBuilder = new StringBuilder(); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossValidationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossValidationCommandWrapper.java new file mode 100644 index 000000000000..c23a3ef0c979 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossValidationCommandWrapper.java @@ -0,0 +1,50 @@ +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import org.apache.cloudstack.backup.CleanupKbossValidationCommand; + +@ResourceWrapper(handles = CleanupKbossValidationCommand.class) +public class LibvirtCleanupKbossValidationCommandWrapper extends CommandWrapper { + @Override + public Answer execute(CleanupKbossValidationCommand command, LibvirtComputingResource serverResource) { + KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + cleanupSecondaryStorages(command, storagePoolMgr); + return new Answer(command); + } + + /** + * The objective of this command is to remove the secondary storage references after the validation VM was stopped. + * Since the getStoragePoolByURI and deleteStoragePool have a reference counter, where the first method increases the count and the second one + * decreases the count, we must call the deleteStoragePool twice so that the command is count negative. + * */ + private void cleanupSecondaryStorages(CleanupKbossValidationCommand command, KVMStoragePoolManager storagePoolMgr) { + logger.info("Cleaning up secondary storage references after backup validation process using VM [{}].", command.getVmName()); + for (String secondaryUrl : command.getSecondaryStorages()) { + logger.debug("Cleaning up secondary storage reference for secondary at [{}].", secondaryUrl); + KVMStoragePool secondary = storagePoolMgr.getStoragePoolByURI(secondaryUrl); + storagePoolMgr.deleteStoragePool(secondary.getType(), secondary.getUuid()); + storagePoolMgr.deleteStoragePool(secondary.getType(), secondary.getUuid()); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossVmBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossVmBackupCommandWrapper.java new file mode 100644 index 000000000000..8ca17fc0c6cf --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossVmBackupCommandWrapper.java @@ -0,0 +1,294 @@ +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.backup.CleanupKbossBackupErrorAnswer; +import org.apache.cloudstack.backup.CleanupKbossBackupErrorCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.commons.lang3.StringUtils; +import org.libvirt.Domain; +import org.libvirt.Error; +import org.libvirt.LibvirtException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; + +@ResourceWrapper(handles = CleanupKbossBackupErrorCommand.class) +public class LibvirtCleanupKbossVmBackupCommandWrapper extends CommandWrapper { + @Override + public Answer execute(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { + List kbossTOS = command.getKbossTOs(); + KVMStoragePoolManager storagePoolManager = serverResource.getStoragePoolMgr(); + + logger.info("Cleaning up backup error for VM [{}].", command.getVmName()); + cleanupBackupDeltasOnSecondary(command, storagePoolManager, kbossTOS); + + if (command.isRunningVM()) { + Pair>, Boolean> volumeTosAndIsVmRunning = cleanupRunningVm(command, serverResource); + return new CleanupKbossBackupErrorAnswer(command, volumeTosAndIsVmRunning.first(), volumeTosAndIsVmRunning.second()); + } + + return new CleanupKbossBackupErrorAnswer(command, mergeDeltasForStoppedVmIfNeeded(command, serverResource), false); + } + + private Pair>, Boolean> cleanupRunningVm(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { + Domain dm = null; + try { + dm = serverResource.getDomain(serverResource.getLibvirtUtilitiesHelper().getConnection(), command.getVmName()); + return new Pair<>(mergeDeltasForRunningVmIfNeeded(command, serverResource, dm), true); + } catch (LibvirtException e) { + if (e.getError().getCode() == Error.ErrorNumber.VIR_ERR_NO_DOMAIN && isVmReallyStopped(command, serverResource)) { + return new Pair<>(mergeDeltasForStoppedVmIfNeeded(command, serverResource), false); + } + logger.error("Error while trying to get VM [{}]. Aborting the process.", command.getVmName(), e); + return new Pair<>(Map.of(), false); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException e) { + logger.warn("Ignoring Libvirt exception.", e); + } + } + } + } + + private Map> mergeDeltasForStoppedVmIfNeeded(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { + HashMap> volumeToChainEnded = new HashMap<>(); + for (KbossTO kbossTO : command.getKbossTOs()) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO)volumeObjectTO.getDataStore(); + KVMStoragePool kvmStoragePool = serverResource.getStoragePoolMgr().getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + boolean volumePathMissing = !Files.exists(Path.of(kvmStoragePool.getLocalPathFor(volumeObjectTO.getPath()))); + boolean deltaPathMissing = !Files.exists(Path.of(kvmStoragePool.getLocalPathFor(kbossTO.getDeltaPathOnPrimary()))); + boolean basePathMissing = kbossTO.getParentDeltaPathOnPrimary() != null && !Files.exists(Path.of(kvmStoragePool.getLocalPathFor(kbossTO.getParentDeltaPathOnPrimary()))); + List grandchildren = kbossTO.getDeltaPaths().isEmpty() ? List.of() : List.of(new BackupDeltaTO(volumeObjectTO.getDataStore(), + Hypervisor.HypervisorType.KVM, kbossTO.getDeltaPaths().get(0))); + + Boolean chainEnded = mergeDeltaIfNeeded(serverResource, kbossTO, volumeObjectTO, grandchildren, volumePathMissing, deltaPathMissing, basePathMissing, + command.isErrorOnCreate(), false, command.isTopDelta(), command.isEndOfChain()); + volumeToChainEnded.put(volumeObjectTO.getUuid(), new Pair<>(volumeObjectTO.getPath(), chainEnded)); + } + return volumeToChainEnded; + } + + private Map> mergeDeltasForRunningVmIfNeeded(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource, Domain dm) throws LibvirtException { + HashMap> volumeIdToPathAndChainEnded = new HashMap<>(); + String xmlDesc = dm.getXMLDesc(0); + LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + parser.parseDomainXML(xmlDesc); + for (KbossTO kbossTO : command.getKbossTOs()) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + String volumePath = volumeObjectTO.getPath(); + LibvirtVMDef.DiskDef diskDef = parser.getDisks().stream() + .filter(disk -> hasPath(disk, volumePath, kbossTO.getDeltaPathOnPrimary(), kbossTO.getParentDeltaPathOnPrimary())) + .findFirst().orElse(null); + + if (diskDef == null) { + logger.warn("Volume [{}] does not match any record we have. This must be manually normalized.", volumeObjectTO.getUuid()); + return Map.of(); + } + + List backingStoreList = diskDef.getBackingStoreList(); + backingStoreList.add(0, diskDef.getDiskPath()); + + boolean volumePathMissing = true; + boolean deltaPathMissing = true; + boolean basePathMissing = kbossTO.getParentDeltaPathOnPrimary() != null; + for (String delta : backingStoreList) { + if (StringUtils.contains(delta, volumePath)) { + volumePathMissing = false; + } + if (StringUtils.contains(delta, kbossTO.getDeltaPathOnPrimary())) { + deltaPathMissing = false; + } + if (StringUtils.contains(delta, kbossTO.getParentDeltaPathOnPrimary())) { + basePathMissing = false; + } + } + + Boolean chainEnded = mergeDeltaIfNeeded(serverResource, kbossTO, volumeObjectTO, List.of(), volumePathMissing, deltaPathMissing, basePathMissing, + command.isErrorOnCreate(), true, command.isTopDelta(), command.isEndOfChain()); + volumeIdToPathAndChainEnded.put(volumeObjectTO.getUuid(), new Pair<>(volumeObjectTO.getPath(), chainEnded)); + } + + return volumeIdToPathAndChainEnded; + } + + private boolean hasPath(LibvirtVMDef.DiskDef diskDef, String... paths) { + List chain = diskDef.getBackingStoreList(); + chain = chain != null ? chain : new ArrayList<>(); + chain.add(diskDef.getDiskPath()); + for (String delta : chain) { + if (Arrays.stream(paths).anyMatch(path -> StringUtils.contains(delta, path))) { + return true; + } + } + return false; + } + + /** + * @return True if error chain is already ended, false otherwise. + * */ + private boolean mergeDeltaIfNeeded(LibvirtComputingResource serverResource, KbossTO kbossTO, VolumeObjectTO volumeObjectTO, List grandChildren, + boolean volumePathMissing, boolean deltaPathMissing, boolean basePathMissing, boolean errorOnCreate, boolean runningVm, boolean isTopDelta, boolean isEndOfChain) { + String errorMessage = String.format("Volume [%s] is inconsistent in an anomalous way. We cannot normalize it automatically.", volumeObjectTO.getUuid()); + if (!errorOnCreate) { + // Base should never be missing if it is not an error from creation. If the volume path is missing and it is not the delta that was being removed, it is an anomaly as well. + if (basePathMissing || (volumePathMissing && !isTopDelta)) { + logger.warn(errorMessage); + throw new CloudRuntimeException(String.format ("Unable to find the base delta or the volume path was not found. We cannot normalize it automatically. At least " + + "one of these should exist: volume [%s]; base path [%s].", volumeObjectTO.getPath(), kbossTO.getParentDeltaPathOnPrimary())); + } + // This means that the delta merge likely succeeded but the host was unable to reply to the Management Server + if (deltaPathMissing) { + // This is if the delta being merged was the top delta. Then we must update its path. + if (volumePathMissing) { + volumeObjectTO.setPath(kbossTO.getParentDeltaPathOnPrimary()); + } + logger.debug("Volume [{}] is already consistent. Its path is [{}].", volumeObjectTO.getUuid(), volumeObjectTO.getPath()); + return true; + } + return false; + } + + DeltaMergeTreeTO deltaMergeTreeTO; + boolean errorChainFinished; + if (volumePathMissing && !deltaPathMissing) { // The process was not started for this volume + DataTO child; + // If it is the top delta, we should set the volume path as the delta path on primary, as it is the real path. This will get updated later after being merged. + if (isTopDelta) { + volumeObjectTO.setPath(kbossTO.getDeltaPathOnPrimary()); + child = volumeObjectTO; + } else { // Otherwise, we set it as the old path of the volume. In this case, this will be its final path. + volumeObjectTO.setPath(kbossTO.getOldVolumePath()); + child = new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, kbossTO.getDeltaPathOnPrimary()); + } + logger.debug("Volume [{}] is consistent, the backup process for it was not started. Its current path is [{}]. We will merge the old backup chain.", + volumeObjectTO.getUuid(), volumeObjectTO.getPath()); + deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, + kbossTO.getParentDeltaPathOnPrimary()), child, grandChildren); + errorChainFinished = true; + } else if (!isEndOfChain && !volumePathMissing && (deltaPathMissing || kbossTO.getParentDeltaPathOnPrimary() == null)) { // The process was completed for this volume + logger.debug("Volume [{}] is consistent, the backup process was completed for it. Its current path is [{}].", volumeObjectTO.getUuid(), volumeObjectTO.getPath()); + return false; + } else if (isEndOfChain && volumePathMissing && !basePathMissing) { // The process was completed for this volume + volumeObjectTO.setPath(kbossTO.getParentDeltaPathOnPrimary()); + logger.debug("Volume [{}] is consistent, the backup process was completed for it. Its current path is [{}].", volumeObjectTO.getUuid(), volumeObjectTO.getPath()); + return true; + } else if (!volumePathMissing && !deltaPathMissing) { // The process stopped midway + logger.debug("Volume [{}] is inconsistent, but we can normalize it. We will merge the delta created by the last backup with the base volume.", + volumeObjectTO.getUuid()); + deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, + kbossTO.getParentDeltaPathOnPrimary()), new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, kbossTO.getDeltaPathOnPrimary()), + grandChildren); + errorChainFinished = false; + isTopDelta = false; + } else { + logger.warn(errorMessage); + throw new CloudRuntimeException(errorMessage + " Maybe it is a good idea to open an issue to get help on this."); + } + + try { + if (runningVm) { + serverResource.mergeDeltaForRunningVm(deltaMergeTreeTO, volumeObjectTO.getVmName(), volumeObjectTO); + } else { + serverResource.mergeDeltaForStoppedVm(deltaMergeTreeTO); + } + if (isTopDelta) { + volumeObjectTO.setPath(deltaMergeTreeTO.getParent().getPath()); + } + return errorChainFinished; + } catch (QemuImgException | IOException | LibvirtException ex) { + logger.error("Got an exception while trying to merge delta for volume [{}].", volumeObjectTO.getUuid(), ex); + throw new CloudRuntimeException(ex); + } + } + + /** + * Checks if the VM is really stopped by checking if its root volume has had any writes on the last 30 seconds. + * */ + private boolean isVmReallyStopped(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { + VolumeObjectTO volume = command.getKbossTOs().stream() + .filter(kbossTO -> kbossTO.getVolumeObjectTO().getDeviceId() == 0) + .map(KbossTO::getVolumeObjectTO).findFirst().orElseThrow(); + PrimaryDataStoreTO primary = (PrimaryDataStoreTO)volume.getDataStore(); + KVMStoragePool storage = serverResource.getStoragePoolMgr().getStoragePool(primary.getPoolType(), primary.getUuid()); + KVMPhysicalDisk disk = storage.getPhysicalDisk(volume.getUuid()); + File diskFile = new File(disk.getPath()); + long time1 = diskFile.lastModified(); + try { + Thread.sleep(30 * 1000); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + long time2 = diskFile.lastModified(); + if (time1 != time2) { + logger.info("VM root disk [{}] has been modified in the last 30 seconds. It seems the VM is running, even though we were unable to find it. If the VM is " + + "running on this host, you can try again later. If the VM was somehow migrated, you should update the database directly."); + return false; + } + logger.warn("VM [{}] was not found by Libvirt and the root disk has not had any writes on the last 30 seconds. Assuming that it is stopped.", command.getVmName()); + return true; + } + + private void cleanupBackupDeltasOnSecondary(CleanupKbossBackupErrorCommand command, KVMStoragePoolManager storagePoolManager, List kbossTOS) { + KVMStoragePool storagePool = null; + try { + storagePool = storagePoolManager.getStoragePoolByURI(command.getImageStoreUrl()); + for (KbossTO kbossTO : kbossTOS) { + String deltaPath = storagePool.getLocalPathFor(kbossTO.getDeltaPathOnSecondary()); + logger.debug("Cleaning up file at [{}] if it exists.", deltaPath); + try { + Files.deleteIfExists(Path.of(deltaPath)); + } catch (IOException e) { + logger.error("Unable to delete leftover backup delta at [{}].", deltaPath); + } + } + } finally { + if (storagePool != null) { + storagePoolManager.deleteStoragePool(storagePool.getType(), storagePool.getUuid()); + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCompressBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCompressBackupCommandWrapper.java new file mode 100644 index 000000000000..2cc08311cbce --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCompressBackupCommandWrapper.java @@ -0,0 +1,150 @@ +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.CompressBackupCommand; +import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.utils.qemu.QemuImageOptions; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +@ResourceWrapper(handles = CompressBackupCommand.class) +public class LibvirtCompressBackupCommandWrapper extends CommandWrapper { + public static final String COMPRESSION_TYPE = "compression_type"; + private static final int MIN_QCOW_2_VERSION_FOR_ZSTD = 3; + + @Override + public Answer execute(CompressBackupCommand command, LibvirtComputingResource serverResource) { + List secondaryStorages = new ArrayList<>(); + List deltas = command.getBackupDeltasToCompress(); + KVMStoragePoolManager storagePoolManager = serverResource.getStoragePoolMgr(); + + logger.info("Starting compression for backup deltas [{}].", deltas); + try { + QemuImg qemuImg = new QemuImg(command.getWait() * 1000); + Integer rateLimit = validateAndGetRateLimit(command, qemuImg); + + KVMStoragePool mainSecStorage = storagePoolManager.getStoragePoolByURI(deltas.stream().findFirst().orElseThrow().getChild().getDataStore().getUrl()); + secondaryStorages.add(mainSecStorage); + secondaryStorages.addAll(command.getBackupChainImageStoreUrls().stream().map(storagePoolManager::getStoragePoolByURI).collect(Collectors.toList())); + + if (!checkAvailableStorage(command, mainSecStorage, storagePoolManager)) { + return new Answer(command, false, "Not enough available space on secondary."); + } + + for (DeltaMergeTreeTO delta : deltas) { + DataTO child = delta.getChild(); + + QemuImgFile backingFile = null; + DataTO parent = delta.getParent(); + if (parent != null) { + KVMStoragePool parentSecondaryStorage = storagePoolManager.getStoragePoolByURI(parent.getDataStore().getUrl()); + secondaryStorages.add(parentSecondaryStorage); + backingFile = new QemuImgFile(parentSecondaryStorage.getLocalPathFor(parent.getPath()), QemuImg.PhysicalDiskFormat.QCOW2); + } + + String fullDeltaPath = mainSecStorage.getLocalPathFor(child.getPath()); + String compressedPath = fullDeltaPath + ".comp"; + QemuImgFile originalBackup = new QemuImgFile(fullDeltaPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile compressedBackup = new QemuImgFile(compressedPath, QemuImg.PhysicalDiskFormat.QCOW2); + + HashMap options = new HashMap<>(); + Backup.CompressionLibrary compressionLib = getCompressionLibrary(command, fullDeltaPath); + setCompressionTypeOptionIfAvailable(qemuImg, options, compressionLib); + int coroutines = command.getCoroutines(); + logger.info("Starting compression for backup delta [{}] with parent [{}] using [{}] coroutines.", child, parent, coroutines); + qemuImg.convert(originalBackup, compressedBackup, backingFile, options, null, new QemuImageOptions(originalBackup.getFormat(), originalBackup.getFileName(), + null), null, false, false, true, true, coroutines, rateLimit); + } + } catch (LibvirtException | QemuImgException e) { + return new Answer(command, e); + } finally { + for (KVMStoragePool secondaryStorage : secondaryStorages) { + storagePoolManager.deleteStoragePool(secondaryStorage.getType(), secondaryStorage.getUuid()); + } + } + + return new Answer(command); + } + + private Integer validateAndGetRateLimit(CompressBackupCommand command, QemuImg qemuImg) { + if (command.getRateLimit() < 1) { + return null; + } + + if (qemuImg.getVersion() < QemuImg.QEMU_5_2) { + throw new CloudRuntimeException("Qemu version is lower than 5.2.0, unable to set the rate limit."); + } + + return command.getRateLimit(); + } + + /** + * Sets the compression type option if qemu-img is at least in version 5.1. Otherwise, will not set it and qemu will use zlib. + * */ + private void setCompressionTypeOptionIfAvailable(QemuImg qemuImg, HashMap options, Backup.CompressionLibrary compressionLib) { + if (qemuImg.getVersion() >= QemuImg.QEMU_5_1) { + options.put(COMPRESSION_TYPE, compressionLib.name()); + return; + } + logger.warn("Qemu is at a lower version than 5.1, we will not be able to use zstd to compress backups. Only zlib is supported for this version. Current version is [{}].", + qemuImg.getVersion()); + } + + private Backup.CompressionLibrary getCompressionLibrary(CompressBackupCommand command, String fullDeltaPath) { + Backup.CompressionLibrary compressionLib = command.getCompressionLib(); + if (compressionLib == Backup.CompressionLibrary.zlib || !Qcow2Inspector.validateQcow2Version(fullDeltaPath, MIN_QCOW_2_VERSION_FOR_ZSTD)) { + logger.debug("Compression for delta [{}] will use zlib as the compression library.", fullDeltaPath); + return Backup.CompressionLibrary.zlib; + } + + logger.debug("Compression for delta [{}] will try to use zstd as the compression library.", fullDeltaPath); + return Backup.CompressionLibrary.zstd; + } + + /** + * Validates available storage. Forces Libvirt to refresh storage info so that we have the most up to date data. + * */ + private boolean checkAvailableStorage(CompressBackupCommand command, KVMStoragePool mainSecStorage, KVMStoragePoolManager storagePoolManager) { + logger.debug("Checking available storage [{}].", mainSecStorage); + mainSecStorage = storagePoolManager.getStoragePool(mainSecStorage.getType(), mainSecStorage.getUuid(), true, false); + if (mainSecStorage.getAvailable() < command.getMinFreeStorage()) { + logger.warn("There is not enough available space for compression of backup! Available size is [{}], needed [{}]. Aborting compression.", + mainSecStorage.getAvailable(), command.getMinFreeStorage()); + return false; + } + return true; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConsolidateVolumesCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConsolidateVolumesCommandWrapper.java new file mode 100644 index 000000000000..30c8849df5fa --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConsolidateVolumesCommandWrapper.java @@ -0,0 +1,59 @@ +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import org.apache.cloudstack.backup.ConsolidateVolumesAnswer; +import org.apache.cloudstack.backup.ConsolidateVolumesCommand; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.List; + +@ResourceWrapper(handles = ConsolidateVolumesCommand.class) +public class LibvirtConsolidateVolumesCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(ConsolidateVolumesCommand command, LibvirtComputingResource serverResource) { + List volumeObjectTOs = command.getVolumesToConsolidate(); + String vmName = command.getVmName(); + + List successfulConsolidations = new ArrayList<>(); + try { + for (VolumeObjectTO volumeObjectTO : volumeObjectTOs) { + if (!serverResource.pullVolumeBackingFile(volumeObjectTO, vmName)) { + return new ConsolidateVolumesAnswer(command, false, "Failed to consolidate all volumes.", successfulConsolidations); + } + successfulConsolidations.add(volumeObjectTO); + } + } catch (LibvirtException ex) { + return new ConsolidateVolumesAnswer(command, false, ex.getMessage(), successfulConsolidations); + } + + KVMStoragePoolManager kvmStoragePoolManager = serverResource.getStoragePoolMgr(); + for (String secStorageUuid : command.getSecondaryStorageUuids()) { + kvmStoragePoolManager.deleteStoragePool(Storage.StoragePoolType.NetworkFilesystem, secStorageUuid); + } + return new ConsolidateVolumesAnswer(command, true, "Success", successfulConsolidations); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java index 98e4bddbb7e9..26265f82467b 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java @@ -19,193 +19,29 @@ package com.cloud.hypervisor.kvm.resource.wrapper; import com.cloud.agent.api.Answer; -import com.cloud.agent.api.VMSnapshotTO; import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer; import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotCommand; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; -import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; -import com.cloud.hypervisor.kvm.storage.KVMStoragePool; -import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; -import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; import com.cloud.vm.VirtualMachine; -import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; -import org.apache.cloudstack.storage.to.VolumeObjectTO; -import org.apache.cloudstack.utils.qemu.QemuImg; -import org.apache.cloudstack.utils.qemu.QemuImgException; -import org.apache.cloudstack.utils.qemu.QemuImgFile; -import org.libvirt.Connect; -import org.libvirt.Domain; -import org.libvirt.LibvirtException; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; @ResourceWrapper(handles = CreateDiskOnlyVmSnapshotCommand.class) public class LibvirtCreateDiskOnlyVMSnapshotCommandWrapper extends CommandWrapper { - private static final String SNAPSHOT_XML = "\n" + - "%s\n" + - "\n" + - " \n" + - "%s" + - " \n" + - ""; - - private static final String TAG_DISK_SNAPSHOT = "\n" + - "\n" + - "\n"; - @Override public Answer execute(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) { VirtualMachine.State state = cmd.getVmState(); - if (VirtualMachine.State.Running.equals(state)) { - return takeDiskOnlyVmSnapshotOfRunningVm(cmd, resource); - } - - return takeDiskOnlyVmSnapshotOfStoppedVm(cmd, resource); - } - - protected Answer takeDiskOnlyVmSnapshotOfRunningVm(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) { - String vmName = cmd.getVmName(); - logger.info("Taking disk-only VM snapshot of running VM [{}].", vmName); - - Domain dm = null; try { - LibvirtUtilitiesHelper libvirtUtilitiesHelper = resource.getLibvirtUtilitiesHelper(); - Connect conn = libvirtUtilitiesHelper.getConnection(); - List volumeObjectTOS = cmd.getVolumeTOs(); - List disks = resource.getDisks(conn, vmName); - - dm = resource.getDomain(conn, vmName); - - if (dm == null) { - return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, String.format("Creation of disk-only VM Snapshot failed as we could not find the VM [%s].", vmName), null); - } - - VMSnapshotTO target = cmd.getTarget(); - Pair>> snapshotXmlAndVolumeToNewPathMap = createSnapshotXmlAndNewVolumePathMap(volumeObjectTOS, disks, target, resource); - - dm.snapshotCreateXML(snapshotXmlAndVolumeToNewPathMap.first(), getFlagsToUseForRunningVmSnapshotCreation(target)); - - return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, snapshotXmlAndVolumeToNewPathMap.second()); - } catch (LibvirtException e) { - String errorMsg = String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage()); - logger.error(errorMsg, e); - if (e.getMessage().contains("QEMU guest agent is not connected")) { - errorMsg = "QEMU guest agent is not connected. If the VM has been recently started, it might connect soon. Otherwise the VM does not have the" + - " guest agent installed; thus the QuiesceVM parameter is not supported."; - return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, errorMsg, null); - } - return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, e.getMessage(), null); - } catch (Exception e) { - String errorMsg = String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage()); - logger.error(errorMsg, e); - return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, errorMsg, null); - } finally { - if (dm != null) { - try { - dm.free(); - } catch (LibvirtException l) { - logger.trace("Ignoring libvirt error.", l); - } + if (VirtualMachine.State.Running.equals(state)) { + return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, resource.createDiskOnlyVmSnapshotForRunningVm(cmd.getVolumeTosAndNewPaths(), cmd.getVmName(), + cmd.getTarget().getSnapshotName(), cmd.getTarget().getQuiescevm())); } + return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, resource.createDiskOnlyVMSnapshotOfStoppedVm(cmd.getVolumeTosAndNewPaths(), cmd.getVmName())); + } catch (BackupException ex) { + return new Answer(cmd, ex); } } - - protected Answer takeDiskOnlyVmSnapshotOfStoppedVm(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) { - String vmName = cmd.getVmName(); - logger.info("Taking disk-only VM snapshot of stopped VM [{}].", vmName); - - Map> mapVolumeToSnapshotSizeAndNewVolumePath = new HashMap<>(); - - List volumeObjectTos = cmd.getVolumeTOs(); - KVMStoragePoolManager storagePoolMgr = resource.getStoragePoolMgr(); - try { - for (VolumeObjectTO volumeObjectTO : volumeObjectTos) { - PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); - KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); - - String snapshotPath = UUID.randomUUID().toString(); - String snapshotFullPath = kvmStoragePool.getLocalPathFor(snapshotPath); - QemuImgFile newDelta = new QemuImgFile(snapshotFullPath, QemuImg.PhysicalDiskFormat.QCOW2); - - String currentDeltaFullPath = kvmStoragePool.getLocalPathFor(volumeObjectTO.getPath()); - QemuImgFile currentDelta = new QemuImgFile(currentDeltaFullPath, QemuImg.PhysicalDiskFormat.QCOW2); - - QemuImg qemuImg = new QemuImg(0); - - logger.debug("Creating new delta for volume [{}] as part of the disk-only VM snapshot process for VM [{}].", volumeObjectTO.getUuid(), vmName); - qemuImg.create(newDelta, currentDelta); - - mapVolumeToSnapshotSizeAndNewVolumePath.put(volumeObjectTO.getUuid(), new Pair<>(getFileSize(currentDeltaFullPath), snapshotPath)); - } - } catch (LibvirtException | QemuImgException e) { - logger.error("Exception while creating disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e); - cleanupLeftoverDeltas(volumeObjectTos, mapVolumeToSnapshotSizeAndNewVolumePath, storagePoolMgr); - return new Answer(cmd, e); - } catch (Exception e) { - logger.error("Unexpected exception while creating disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e); - cleanupLeftoverDeltas(volumeObjectTos, mapVolumeToSnapshotSizeAndNewVolumePath, storagePoolMgr); - return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, - String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage()), null); - } - - return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, mapVolumeToSnapshotSizeAndNewVolumePath); - } - - protected int getFlagsToUseForRunningVmSnapshotCreation(VMSnapshotTO target) { - int flags = target.getQuiescevm() ? Domain.SnapshotCreateFlags.QUIESCE : 0; - flags += Domain.SnapshotCreateFlags.DISK_ONLY + - Domain.SnapshotCreateFlags.ATOMIC + - Domain.SnapshotCreateFlags.NO_METADATA; - return flags; - } - - protected Pair>> createSnapshotXmlAndNewVolumePathMap(List volumeObjectTOS, List disks, VMSnapshotTO target, LibvirtComputingResource resource) { - StringBuilder stringBuilder = new StringBuilder(); - Map> volumeObjectToNewPathMap = new HashMap<>(); - - for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) { - LibvirtVMDef.DiskDef diskdef = resource.getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO); - String newPath = UUID.randomUUID().toString(); - stringBuilder.append(String.format(TAG_DISK_SNAPSHOT, diskdef.getDiskLabel(), resource.getSnapshotTemporaryPath(diskdef.getDiskPath(), newPath))); - - long snapSize = getFileSize(diskdef.getDiskPath()); - - volumeObjectToNewPathMap.put(volumeObjectTO.getUuid(), new Pair<>(snapSize, newPath)); - } - - String snapshotXml = String.format(SNAPSHOT_XML, target.getSnapshotName(), stringBuilder); - return new Pair<>(snapshotXml, volumeObjectToNewPathMap); - } - - protected void cleanupLeftoverDeltas(List volumeObjectTos, Map> mapVolumeToSnapshotSizeAndNewVolumePath, KVMStoragePoolManager storagePoolMgr) { - for (VolumeObjectTO volumeObjectTO : volumeObjectTos) { - Pair volSizeAndNewPath = mapVolumeToSnapshotSizeAndNewVolumePath.get(volumeObjectTO.getUuid()); - PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); - KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); - - if (volSizeAndNewPath == null) { - continue; - } - try { - Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volSizeAndNewPath.second()))); - } catch (IOException ex) { - logger.warn("Tried to delete leftover snapshot at [{}] failed.", volSizeAndNewPath.second(), ex); - } - } - } - - protected long getFileSize(String path) { - return new File(path).length(); - } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeBackupCompressionCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeBackupCompressionCommandWrapper.java new file mode 100644 index 000000000000..4b02c20c6d26 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeBackupCompressionCommandWrapper.java @@ -0,0 +1,73 @@ +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +import org.apache.cloudstack.backup.FinalizeBackupCompressionCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +@ResourceWrapper(handles = FinalizeBackupCompressionCommand.class) +public class LibvirtFinalizeBackupCompressionCommandWrapper extends CommandWrapper { + @Override + public Answer execute(FinalizeBackupCompressionCommand command, LibvirtComputingResource serverResource) { + KVMStoragePool storagePool = null; + KVMStoragePoolManager storagePoolManager = serverResource.getStoragePoolMgr(); + long totalPhysicalSize = 0; + + if (command.isCleanup()) { + logger.info("Cleaning up compressed backup deltas [{}].", command.getBackupDeltaTOList()); + } else { + logger.info("Finalizing backup compression for deltas [{}].", command.getBackupDeltaTOList()); + } + try { + storagePool = storagePoolManager.getStoragePoolByURI(command.getBackupDeltaTOList().get(0).getDataStore().getUrl()); + for (BackupDeltaTO delta : command.getBackupDeltaTOList()) { + Path deltaPath = Path.of(storagePool.getLocalPathFor(delta.getPath())); + Path compressedDeltaPath = Path.of(deltaPath + ".comp"); + + if (command.isCleanup()) { + logger.debug("Cleaning up backup delta at [{}].", compressedDeltaPath); + Files.deleteIfExists(compressedDeltaPath); + continue; + } + + logger.debug("Moving compressed backup delta at [{}] to [{}].", compressedDeltaPath, deltaPath); + Files.move(compressedDeltaPath, deltaPath, StandardCopyOption.REPLACE_EXISTING); + totalPhysicalSize += Files.size(deltaPath); + } + } catch (IOException e) { + return new Answer(command, e); + } finally { + if (storagePool != null) { + storagePoolManager.deleteStoragePool(storagePool.getType(), storagePool.getUuid()); + } + } + return new Answer(command, true, String.valueOf(totalPhysicalSize)); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetStorageStatsCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetStorageStatsCommandWrapper.java index 419b54492583..424622dfc2ce 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetStorageStatsCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetStorageStatsCommandWrapper.java @@ -36,7 +36,7 @@ public final class LibvirtGetStorageStatsCommandWrapper extends CommandWrapper { @Override public Answer execute(MergeDiskOnlyVmSnapshotCommand command, LibvirtComputingResource serverResource) { - VirtualMachine.State vmState = command.getVmState(); + boolean isVmRunning = command.isVmRunning(); try { - if (VirtualMachine.State.Running.equals(vmState)) { + if (isVmRunning) { return mergeDiskOnlySnapshotsForRunningVM(command, serverResource); } return mergeDiskOnlySnapshotsForStoppedVM(command, serverResource); @@ -66,83 +50,28 @@ public Answer execute(MergeDiskOnlyVmSnapshotCommand command, LibvirtComputingRe } protected Answer mergeDiskOnlySnapshotsForStoppedVM(MergeDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws QemuImgException, LibvirtException { - QemuImg qemuImg = new QemuImg(resource.getCmdsTimeout()); - KVMStoragePoolManager storageManager = resource.getStoragePoolMgr(); + List deltaMergeTreeTOList = cmd.getDeltaMergeTreeToList(); - List snapshotMergeTreeTOList = cmd.getSnapshotMergeTreeToList(); + logger.debug("Merging deltas for stopped VM [{}] using the following Delta Merge Trees [{}].", cmd.getVmName(), deltaMergeTreeTOList); - logger.debug("Merging disk-only snapshots for stopped VM [{}] using the following Snapshot Merge Trees [{}].", cmd.getVmName(), snapshotMergeTreeTOList); - - for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeTOList) { - DataTO parentTo = snapshotMergeTreeTO.getParent(); - PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) parentTo.getDataStore(); - KVMStoragePool storagePool = storageManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); - String childLocalPath = storagePool.getLocalPathFor(snapshotMergeTreeTO.getChild().getPath()); - - QemuImgFile parent = new QemuImgFile(storagePool.getLocalPathFor(parentTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2); - QemuImgFile child = new QemuImgFile(childLocalPath, QemuImg.PhysicalDiskFormat.QCOW2); - - logger.debug("Committing child delta [{}] into parent snapshot [{}].", parentTo, snapshotMergeTreeTO.getChild()); - qemuImg.commit(child, parent, true); - - List grandChildren = snapshotMergeTreeTO.getGrandChildren().stream() - .map(snapshotTo -> new QemuImgFile(storagePool.getLocalPathFor(snapshotTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2)) - .collect(Collectors.toList()); - - logger.debug("Rebasing grandChildren [{}] into parent at [{}].", grandChildren, parent.getFileName()); - for (QemuImgFile grandChild : grandChildren) { - qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false); - } - - logger.debug("Deleting child at [{}] as it is useless.", childLocalPath); + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOList) { try { - Files.deleteIfExists(Path.of(childLocalPath)); - } catch (IOException e) { - return new Answer(cmd, e); + resource.mergeDeltaForStoppedVm(deltaMergeTreeTO); + } catch (IOException ex) { + return new Answer(cmd, ex); } } return new Answer(cmd, true, null); } - protected Answer mergeDiskOnlySnapshotsForRunningVM(MergeDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws LibvirtException, QemuImgException { + protected Answer mergeDiskOnlySnapshotsForRunningVM(MergeDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws QemuImgException, LibvirtException { String vmName = cmd.getVmName(); - List snapshotMergeTreeTOList = cmd.getSnapshotMergeTreeToList(); - - LibvirtUtilitiesHelper libvirtUtilitiesHelper = resource.getLibvirtUtilitiesHelper(); - Connect conn = libvirtUtilitiesHelper.getConnection(); - Domain domain = resource.getDomain(conn, vmName); - List disks = resource.getDisks(conn, vmName); - KVMStoragePoolManager storageManager = resource.getStoragePoolMgr(); - QemuImg qemuImg = new QemuImg(resource.getCmdsTimeout()); + List deltaMergeTreeTOs = cmd.getDeltaMergeTreeToList(); - logger.debug("Merging disk-only snapshots for running VM [{}] using the following Snapshot Merge Trees [{}].", vmName, snapshotMergeTreeTOList); + logger.debug("Merging deltas for running VM [{}] using the following Delta Merge Trees [{}].", vmName, deltaMergeTreeTOs); - for (SnapshotMergeTreeTO mergeTreeTO : snapshotMergeTreeTOList) { - DataTO childTO = mergeTreeTO.getChild(); - SnapshotObjectTO parentSnapshotTO = (SnapshotObjectTO) mergeTreeTO.getParent(); - VolumeObjectTO volumeObjectTO = parentSnapshotTO.getVolume(); - KVMStoragePool storagePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(volumeObjectTO, storageManager); - - boolean active = DataObjectType.VOLUME.equals(childTO.getObjectType()); - String label = resource.getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO).getDiskLabel(); - String parentSnapshotLocalPath = storagePool.getLocalPathFor(parentSnapshotTO.getPath()); - String childDeltaPath = storagePool.getLocalPathFor(childTO.getPath()); - - logger.debug("Found label [{}] for [{}]. Will merge delta at [{}] into delta at [{}].", label, volumeObjectTO, parentSnapshotLocalPath, childDeltaPath); - - resource.mergeSnapshotIntoBaseFile(domain, label, parentSnapshotLocalPath, childDeltaPath, active, childTO.getPath(), - volumeObjectTO, conn); - - QemuImgFile parent = new QemuImgFile(parentSnapshotLocalPath, QemuImg.PhysicalDiskFormat.QCOW2); - - List grandChildren = mergeTreeTO.getGrandChildren().stream() - .map(snapshotTo -> new QemuImgFile(storagePool.getLocalPathFor(snapshotTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2)) - .collect(Collectors.toList()); - - logger.debug("Rebasing grandChildren [{}] into parent at [{}].", grandChildren, parentSnapshotLocalPath); - for (QemuImgFile grandChild : grandChildren) { - qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false); - } + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOs) { + resource.mergeDeltaForRunningVm(deltaMergeTreeTO, vmName, deltaMergeTreeTO.getVolumeObjectTO()); } return new Answer(cmd, true, null); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateBackupsBetweenSecondaryStoragesCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateBackupsBetweenSecondaryStoragesCommandWrapper.java new file mode 100644 index 000000000000..f786a0821d05 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateBackupsBetweenSecondaryStoragesCommandWrapper.java @@ -0,0 +1,132 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.MigrateBackupsBetweenSecondaryStoragesCommand; +import com.cloud.agent.api.MigrateBetweenSecondaryStoragesCommandAnswer; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtMigrateResourceBetweenSecondaryStorages; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.commons.lang3.BooleanUtils; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@ResourceWrapper(handles = MigrateBackupsBetweenSecondaryStoragesCommand.class) +public class LibvirtMigrateBackupsBetweenSecondaryStoragesCommandWrapper extends LibvirtMigrateResourceBetweenSecondaryStorages { + + @Override + public Answer execute(MigrateBackupsBetweenSecondaryStoragesCommand command, LibvirtComputingResource serverResource) { + resourceType = BACKUP; + filesToRemove = new HashSet<>(); + resourcesToUpdate = new ArrayList<>(); + wait = command.getWait() * 1000; + + DataStoreTO srcDataStore = command.getSrcDataStore(); + DataStoreTO destDataStore = command.getDestDataStore(); + KVMStoragePoolManager storagePoolManager = serverResource.getStoragePoolMgr(); + + Set imagePools = new HashSet<>(); + KVMStoragePool destImagePool = storagePoolManager.getStoragePoolByURI(destDataStore.getUrl()); + imagePools.add(destImagePool); + + String imagePoolUrl; + KVMStoragePool imagePool = null; + + List> backupChains = command.getBackupChain(); + + try { + Map parentBackupPathMap = new HashMap<>(); + Map parentBackupMigrationMap = new HashMap<>(); + + Map backupPathMap = new HashMap<>(); + Map backupMigrationMap = new HashMap<>(); + + for (List chain : backupChains) { + long lastBackupId = 0; + boolean backupWasMigrated = false; + + backupPathMap.clear(); + backupMigrationMap.clear(); + + for (DataTO backup : chain) { + lastBackupId = backup.getId(); + + imagePoolUrl = backup.getDataStore().getUrl(); + imagePool = storagePoolManager.getStoragePoolByURI(imagePoolUrl); + imagePools.add(imagePool); + + String volumeId = backup.getPath().split("/")[2]; + String resourceCurrentPath = imagePool.getLocalPathFor(backup.getPath()); + String resourceParentPath = parentBackupPathMap.get(volumeId); + + if (imagePoolUrl.equals(srcDataStore.getUrl())) { + backupPathMap.put(volumeId, copyResourceToDestDataStore(backup, resourceCurrentPath, destImagePool, resourceParentPath)); + backupMigrationMap.put(volumeId, true); + backupWasMigrated = true; + } else { + if (BooleanUtils.isTrue(parentBackupMigrationMap.get(volumeId))) { + backupPathMap.put(volumeId, rebaseResourceToNewParentPath(resourceCurrentPath, resourceParentPath)); + } else { + backupPathMap.put(volumeId, resourceCurrentPath); + } + backupMigrationMap.put(volumeId, false); + } + } + + parentBackupPathMap.clear(); + parentBackupPathMap.putAll(backupPathMap); + + parentBackupMigrationMap.clear(); + parentBackupMigrationMap.putAll(backupMigrationMap); + + if (backupWasMigrated) { + resourcesToUpdate.add(new Pair<>(lastBackupId, null)); + } + } + } catch (LibvirtException | QemuImgException e) { + logger.error("Exception while migrating backups [{}] to secondary storage [{}] due to: [{}].", + command.getBackupChain(), imagePool, e.getMessage(), e); + return new MigrateBetweenSecondaryStoragesCommandAnswer(command, false, "Migration of backups between secondary storages failed", resourcesToUpdate); + } finally { + for (String file : filesToRemove) { + removeResourceFromSourceDataStore(file); + } + + for (KVMStoragePool storagePool : imagePools) { + storagePoolManager.deleteStoragePool(storagePool.getType(), storagePool.getUuid()); + } + } + + return new MigrateBetweenSecondaryStoragesCommandAnswer(command, true, "success", resourcesToUpdate); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareValidationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareValidationCommandWrapper.java new file mode 100644 index 000000000000..1d9ed5631fa1 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareValidationCommandWrapper.java @@ -0,0 +1,91 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.backup.PrepareValidationCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.List; + +@ResourceWrapper(handles = PrepareValidationCommand.class) +public class LibvirtPrepareValidationCommandWrapper extends CommandWrapper { + @Override + public Answer execute(PrepareValidationCommand command, LibvirtComputingResource resource) { + List> backingFileAndVolumeList = command.getBackupToVolumeList(); + + List secondaryReferences = new ArrayList<>(); + KVMStoragePoolManager storagePoolMgr = resource.getStoragePoolMgr(); + try { + for (String imageStoreUri : command.getImageStoreSet()) { + secondaryReferences.add(storagePoolMgr.getStoragePoolByURI(imageStoreUri)); + } + + for (Pair backingFileAndVolume : backingFileAndVolumeList) { + logger.debug("Preparing volume [{}] for validation.", backingFileAndVolume.second()); + BackupDeltaTO backupDelta = backingFileAndVolume.first(); + DataStoreTO dataStoreTO = backupDelta.getDataStore(); + KVMStoragePool imageStore = storagePoolMgr.getStoragePoolByURI(dataStoreTO.getUrl()); + secondaryReferences.add(imageStore); + + createVolume(command, backingFileAndVolume, imageStore, backupDelta, storagePoolMgr); + } + } catch (LibvirtException | QemuImgException e) { + logger.error("Failed to prepare VM [{}] for validation due to:", backingFileAndVolumeList.get(0).second().getVmName(), e); + throw new CloudRuntimeException(e); + } finally { + for (KVMStoragePool secondary : secondaryReferences) { + storagePoolMgr.deleteStoragePool(secondary.getType(), secondary.getUuid()); + } + } + return new Answer(command); + } + + private void createVolume(PrepareValidationCommand command, Pair volumeAndBackingFile, KVMStoragePool imageStore, BackupDeltaTO backupDelta, + KVMStoragePoolManager storagePoolMgr) throws LibvirtException, QemuImgException { + String fullBackupPath = imageStore.getLocalPathFor(backupDelta.getPath()); + + VolumeObjectTO volumeObjectTO = volumeAndBackingFile.second(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); + KVMStoragePool primaryStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + String fullVolumePath = primaryStoragePool.getLocalPathFor(volumeObjectTO.getPath()); + + QemuImgFile backup = new QemuImgFile(fullBackupPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile volume = new QemuImgFile(fullVolumePath, QemuImg.PhysicalDiskFormat.QCOW2); + + QemuImg qemuImg = new QemuImg(command.getWait() * 1000); + + qemuImg.create(volume, backup); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapper.java new file mode 100644 index 000000000000..a9010b46c1d4 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapper.java @@ -0,0 +1,123 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import com.cloud.utils.Pair; +import org.apache.cloudstack.backup.RestoreKbossBackupAnswer; +import org.apache.cloudstack.backup.RestoreKbossBackupCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.libvirt.LibvirtException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; + +@ResourceWrapper(handles = RestoreKbossBackupCommand.class) +public class LibvirtRestoreKbossBackupCommandWrapper extends CommandWrapper { + @Override + public Answer execute(RestoreKbossBackupCommand cmd, LibvirtComputingResource resource) { + Set> backupToAndVolumeObjectPairs = cmd.getBackupAndVolumePairs(); + Set deltasToRemove = cmd.getDeltasToRemove(); + Set secondaryStorageUrls = cmd.getSecondaryStorageUrls(); + + KVMStoragePoolManager storagePoolManager = resource.getStoragePoolMgr(); + + Set secondaryStorageUuids = new HashSet<>(); + try { + KVMStoragePool secondaryStorage = mountSecondaryStorages(secondaryStorageUrls, backupToAndVolumeObjectPairs.stream().findFirst().get().first().getDataStore().getUrl(), + storagePoolManager, secondaryStorageUuids); + + restoreVolumes(backupToAndVolumeObjectPairs, secondaryStorage, storagePoolManager, cmd.isQuickRestore(), cmd.getWait() * 1000); + + deleteDeltas(deltasToRemove, storagePoolManager); + } catch (LibvirtException | QemuImgException | IOException e) { + return new RestoreKbossBackupAnswer(cmd, e, secondaryStorageUuids); + } finally { + if (!cmd.isQuickRestore()) { + for (String uuid : secondaryStorageUuids) { + storagePoolManager.deleteStoragePool(Storage.StoragePoolType.NetworkFilesystem, uuid); + } + } + } + return new RestoreKbossBackupAnswer(cmd, secondaryStorageUuids); + } + + protected void restoreVolumes(Set> backupToAndVolumeObjectPairs, KVMStoragePool secondaryStorage, KVMStoragePoolManager storagePoolManager, + boolean quickRestore, int timeoutInMillis) throws LibvirtException, QemuImgException { + for (Pair backupToVolumeToPair : backupToAndVolumeObjectPairs) { + String fullBackupPath = secondaryStorage.getLocalPathFor(backupToVolumeToPair.first().getPath()); + + VolumeObjectTO volumeObjectTO = backupToVolumeToPair.second(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); + KVMStoragePool primaryStoragePool = storagePoolManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + String fullVolumePath = primaryStoragePool.getLocalPathFor(volumeObjectTO.getPath()); + + QemuImgFile backup = new QemuImgFile(fullBackupPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile volume = new QemuImgFile(fullVolumePath, QemuImg.PhysicalDiskFormat.QCOW2); + + QemuImg qemuImg = getQemuImg(timeoutInMillis); + + if (quickRestore) { + logger.info("Creating delta over old volume [{}] at [{}] with backing store stored at [{}].", volumeObjectTO.getUuid(), fullVolumePath, fullBackupPath); + qemuImg.create(volume, backup); + } else { + logger.info("Restoring volume [{}] at [{}] with backup stored at [{}].", volumeObjectTO.getUuid(), fullVolumePath, fullBackupPath); + qemuImg.convert(backup, volume); + } + } + } + + protected QemuImg getQemuImg(int timeoutInMillis) throws LibvirtException, QemuImgException { + return new QemuImg(timeoutInMillis); + } + + protected void deleteDeltas(Set deltasToRemove, KVMStoragePoolManager storagePoolManager) throws IOException { + for (BackupDeltaTO deltaToRemove : deltasToRemove) { + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) deltaToRemove.getDataStore(); + KVMStoragePool primaryStoragePool = storagePoolManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + String fullDeltaPath = primaryStoragePool.getLocalPathFor(deltaToRemove.getPath()); + logger.debug("Deleting leftover delta [{}].", fullDeltaPath); + Files.deleteIfExists(Path.of(fullDeltaPath)); + } + } + + protected KVMStoragePool mountSecondaryStorages(Set parentSecondaryStorageUrls, String secondaryStorageUrl, KVMStoragePoolManager storagePoolManager, Set secondaryStorageUuids) { + for (String url : parentSecondaryStorageUrls) { + KVMStoragePool pool = storagePoolManager.getStoragePoolByURI(url); + secondaryStorageUuids.add(pool.getUuid()); + } + KVMStoragePool pool = storagePoolManager.getStoragePoolByURI(secondaryStorageUrl); + secondaryStorageUuids.add(pool.getUuid()); + return pool; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java index 16c1a5a2fac1..865d2bfb1e50 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java @@ -28,6 +28,7 @@ import com.cloud.agent.properties.AgentProperties; import com.cloud.agent.properties.AgentPropertiesFileHandler; +import com.cloud.storage.Storage; import org.apache.cloudstack.storage.command.RevertSnapshotCommand; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.SnapshotObjectTO; @@ -56,8 +57,11 @@ import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.commons.collections4.CollectionUtils; import org.libvirt.LibvirtException; +import static com.cloud.hypervisor.kvm.storage.KVMStorageProcessor.poolTypesToDeleteChainInfo; + @ResourceWrapper(handles = RevertSnapshotCommand.class) public class LibvirtRevertSnapshotCommandWrapper extends CommandWrapper { @@ -128,7 +132,7 @@ public Answer execute(final RevertSnapshotCommand command, final LibvirtComputin return new Answer(command, false, result); } } else { - revertVolumeToSnapshot(secondaryStoragePool, snapshotOnPrimaryStorage, snapshot, primaryPool, libvirtComputingResource); + revertVolumeToSnapshot(secondaryStoragePool, snapshotOnPrimaryStorage, snapshot, primaryPool, libvirtComputingResource, command.isDeleteChain()); } } @@ -161,7 +165,7 @@ protected String getFullPathAccordingToStorage(KVMStoragePool kvmStoragePool, St * Reverts the volume to the snapshot. */ protected void revertVolumeToSnapshot(KVMStoragePool kvmStoragePoolSecondary, SnapshotObjectTO snapshotOnPrimaryStorage, SnapshotObjectTO snapshotOnSecondaryStorage, - KVMStoragePool kvmStoragePoolPrimary, LibvirtComputingResource resource) { + KVMStoragePool kvmStoragePoolPrimary, LibvirtComputingResource resource, boolean deleteChain) { VolumeObjectTO volumeObjectTo = snapshotOnSecondaryStorage.getVolume(); String volumePath = getFullPathAccordingToStorage(kvmStoragePoolPrimary, volumeObjectTo.getPath()); @@ -178,6 +182,13 @@ protected void revertVolumeToSnapshot(KVMStoragePool kvmStoragePoolSecondary, Sn try { replaceVolumeWithSnapshot(volumePath, snapshotPath); + if (CollectionUtils.isNotEmpty(volumeObjectTo.getDeltasToRemove()) && poolTypesToDeleteChainInfo.contains(kvmStoragePoolPrimary.getType()) && + volumeObjectTo.getFormat() == Storage.ImageFormat.QCOW2 && deleteChain) { + for (String deltaPath : volumeObjectTo.getDeltasToRemove()) { + logger.debug("Deleting leftover backup delta at [{}].", deltaPath); + kvmStoragePoolPrimary.deletePhysicalDisk(deltaPath, volumeObjectTo.getFormat()); + } + } logger.debug(String.format("Successfully reverted volume [%s] to snapshot [%s].", volumeObjectTo, snapshotToPrint)); } catch (LibvirtException | QemuImgException ex) { throw new CloudRuntimeException(String.format("Unable to revert volume [%s] to snapshot [%s] due to [%s].", volumeObjectTo, snapshotToPrint, ex.getMessage()), ex); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java index 567986465906..486989661909 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java @@ -21,13 +21,16 @@ import java.io.File; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import com.cloud.agent.resource.virtualnetwork.VRScripts; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; import com.cloud.utils.FileUtil; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.collections4.CollectionUtils; import org.libvirt.Connect; import org.libvirt.DomainInfo.DomainState; import org.libvirt.LibvirtException; @@ -64,7 +67,9 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); Connect conn = null; + List secondaryStorages = new ArrayList<>(); try { + mountSecondaryStoragesIfNeeded(command, libvirtComputingResource, secondaryStorages); vm = libvirtComputingResource.createVMFromSpec(vmSpec); conn = libvirtUtilitiesHelper.getConnectionByType(vm.getHvsType()); @@ -167,6 +172,17 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource } finally { if (state != DomainState.VIR_DOMAIN_RUNNING) { storagePoolMgr.disconnectPhysicalDisksViaVmSpec(vmSpec); + for (KVMStoragePool secondaryStorage : secondaryStorages) { + libvirtComputingResource.getStoragePoolMgr().deleteStoragePool(secondaryStorage.getType(), secondaryStorage.getUuid()); + } + } + } + } + + private void mountSecondaryStoragesIfNeeded(StartCommand command, LibvirtComputingResource libvirtComputingResource, List secondaryStorages) { + if (CollectionUtils.isNotEmpty(command.getSecondaryStorages())) { + for (String secondaryStorageUrl : command.getSecondaryStorages()) { + secondaryStorages.add(libvirtComputingResource.getStoragePoolMgr().getStoragePoolByURI(secondaryStorageUrl)); } } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupHashCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupHashCommandWrapper.java new file mode 100644 index 000000000000..64669b3b4b5c --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupHashCommandWrapper.java @@ -0,0 +1,72 @@ +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +import com.cloud.utils.exception.CloudRuntimeException; +import com.dynatrace.hash4j.hashing.HashStream128; +import com.dynatrace.hash4j.hashing.HashValue128; +import com.dynatrace.hash4j.hashing.Hashing; +import org.apache.cloudstack.backup.TakeBackupHashCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +@ResourceWrapper(handles = TakeBackupHashCommand.class) +public class LibvirtTakeBackupHashCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(TakeBackupHashCommand command, LibvirtComputingResource resource) { + String backupUuid = command.getBackupUuid(); + logger.info("Taking hash of backup [{}].", backupUuid); + + KVMStoragePoolManager storagePoolManager = resource.getStoragePoolMgr(); + KVMStoragePool imagePool = null; + try { + imagePool = storagePoolManager.getStoragePoolByURI(command.getBackupDeltaTOList().get(0).getDataStore().getUrl()); + HashStream128 hashStream128 = Hashing.xxh3_128().hashStream(); + for (BackupDeltaTO backupDelta : command.getBackupDeltaTOList()) { + try (InputStream is = new BufferedInputStream(new FileInputStream(imagePool.getLocalPathFor(backupDelta.getPath())))) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + hashStream128.putBytes(buffer, 0, bytesRead); + } + } catch (IOException e) { + throw new CloudRuntimeException(e); + } + } + HashValue128 hash = hashStream128.get(); + String hashString = hash.toString(); + logger.info("The xxHash128 of backup [{}] is [{}].", backupUuid, hashString); + return new Answer(command, true, hashString); + } finally { + if (imagePool != null) { + storagePoolManager.deleteStoragePool(imagePool.getType(), imagePool.getUuid()); + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeKbossBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeKbossBackupCommandWrapper.java new file mode 100644 index 000000000000..5ff9bbaaad0f --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeKbossBackupCommandWrapper.java @@ -0,0 +1,394 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; +import org.apache.cloudstack.backup.TakeKbossBackupAnswer; +import org.apache.cloudstack.backup.TakeKbossBackupCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImageOptions; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.libvirt.LibvirtException; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +@ResourceWrapper(handles = TakeKbossBackupCommand.class) +public class LibvirtTakeKbossBackupCommandWrapper extends CommandWrapper { + @Override + public Answer execute(TakeKbossBackupCommand command, LibvirtComputingResource resource) { + String vmName = command.getVmName(); + logger.info("Starting backup process for VM [{}].", vmName); + List kbossTOS = command.getKbossTOs(); + List> volumeTosAndNewPaths = + kbossTOS.stream().map(kbossTO -> new Pair<>(kbossTO.getVolumeObjectTO(), kbossTO.getDeltaPathOnPrimary())).collect(Collectors.toList()); + + Map> mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize = new HashMap<>(); + Map mapVolumeUuidToNewVolumePath = new HashMap<>(); + + KVMStoragePoolManager storagePoolManager = resource.getStoragePoolMgr(); + boolean runningVM = command.isRunningVM(); + + try { + if (runningVM) { + resource.createDiskOnlyVmSnapshotForRunningVm(volumeTosAndNewPaths, vmName, UUID.randomUUID().toString(), command.isQuiesceVm()); + } else { + resource.createDiskOnlyVMSnapshotOfStoppedVm(volumeTosAndNewPaths, vmName); + } + + backupVolumes(command, resource, storagePoolManager, kbossTOS, volumeTosAndNewPaths, vmName, runningVM, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize); + + cleanupVm(command, resource, kbossTOS, vmName, runningVM, mapVolumeUuidToNewVolumePath); + } catch (BackupException ex) { + return new TakeKbossBackupAnswer(command, ex); + } + + return new TakeKbossBackupAnswer(command, true, mapVolumeUuidToNewVolumePath, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize); + } + + /** + * Backup (copy) volumes to secondary storage. Will also populate the mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize argument. + * The timeout for this method is guided by the wait time for the given command, if the wait time is bigger than 24 days, there will be an overflow on the timeout. + *
+ * If an exception is caught while copying the volumes, will try to recover the VM to the previous state so that it is consistent. + * */ + protected void backupVolumes(TakeKbossBackupCommand command, LibvirtComputingResource resource, KVMStoragePoolManager storagePoolManager, List kbossTOS, + List> volumeTosAndNewPaths, String vmName, boolean runningVM, + Map> mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize) { + try { + int maxWaitInMillis = command.getWait() * 1000; + for (KbossTO kbossTO : kbossTOS) { + long startTimeMillis = System.currentTimeMillis(); + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + String volumeUuid = volumeObjectTO.getUuid(); + + logger.debug("Backing up volume [{}].", volumeUuid); + Pair deltaPathOnSecondaryAndSize = copyBackupDeltaToSecondary(storagePoolManager, kbossTO, command.getBackupChainImageStoreUrls(), + command.getImageStoreUrl(), maxWaitInMillis); + + mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize.put(volumeUuid, deltaPathOnSecondaryAndSize); + maxWaitInMillis = calculateRemainingTime(maxWaitInMillis, startTimeMillis); + } + } catch (Exception ex) { + logger.error("There has been an exception during the backup creation process. We will try to revert the VM [{}] to its previous state. The exception is: {}", vmName, + ex.getMessage(), ex); + recoverPreviousVmStateAndDeletePartialBackup(resource, volumeTosAndNewPaths, vmName, runningVM, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize, storagePoolManager, + command.getImageStoreUrl()); + + throw new BackupException(String.format("There was an exception during the backup process for VM [%s], but the VM has been successfully normalized.", vmName), ex, + true); + } + } + + protected int calculateRemainingTime(int maxWaitInMillis, long startTimeMillis) throws TimeoutException { + maxWaitInMillis -= (int)(System.currentTimeMillis() - startTimeMillis); + if (maxWaitInMillis < 0) { + throw new TimeoutException("Timeout while converting backups to secondary storage."); + } + return maxWaitInMillis; + } + + /** + * For each KbossTO, will merge its DeltaMergeTreeTO (if it exists). Also, if this is the end of the chain, will also end the chain for the volume. + * Will populate the mapVolumeUuidToNewVolumePath argument. + * */ + protected void cleanupVm(TakeKbossBackupCommand command, LibvirtComputingResource resource, List kbossTOS, String vmName, boolean runningVM, + Map mapVolumeUuidToNewVolumePath) { + for (KbossTO kbossTO : kbossTOS) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + String currentVolumePath = volumeObjectTO.getPath(); + String volumeUuid = volumeObjectTO.getUuid(); + DeltaMergeTreeTO deltaMergeTreeTO = kbossTO.getDeltaMergeTreeTO(); + volumeObjectTO.setPath(kbossTO.getDeltaPathOnPrimary()); + + if (deltaMergeTreeTO != null) { + List snapshotDataStoreVos = kbossTO.getDeltaPaths(); + mergeBackupDelta(resource, deltaMergeTreeTO, volumeObjectTO, vmName, runningVM, volumeUuid, CollectionUtils.isEmpty(snapshotDataStoreVos)); + } + + if (command.isEndChain() || command.isIsolated()) { + String baseVolumePath = currentVolumePath; + if (deltaMergeTreeTO != null && deltaMergeTreeTO.getChild().getPath().equals(baseVolumePath)) { + baseVolumePath = deltaMergeTreeTO.getParent().getPath(); + } + endChainForVolume(resource, volumeObjectTO, vmName, runningVM, volumeUuid, baseVolumePath); + mapVolumeUuidToNewVolumePath.put(volumeUuid, baseVolumePath); + } else { + mapVolumeUuidToNewVolumePath.put(volumeUuid, kbossTO.getDeltaPathOnPrimary()); + } + } + } + + /** + * Copy the backup delta to the secondary storage. Since we created a snapshot on top of the volume, the volume is now the backup delta. + * If there were snapshots created after the last backup, they'll be copied alongside and merged in the secondary storage. + * */ + protected Pair copyBackupDeltaToSecondary(KVMStoragePoolManager storagePoolManager, KbossTO kbossTO, List chainImageStoreUrls, String imageStoreUrl, + int waitInMillis) { + VolumeObjectTO delta = kbossTO.getVolumeObjectTO(); + String parentDeltaPathOnSecondary = kbossTO.getPathBackupParentOnSecondary(); + List deltaPathsToCopy = ObjectUtils.defaultIfNull(kbossTO.getDeltaPaths(), new ArrayList<>()); + deltaPathsToCopy.add(delta.getPath()); + + KVMStoragePool parentImagePool = null; + List chainImagePools = null; + KVMStoragePool imagePool = null; + long backupSize; + final String backupOnSecondary = kbossTO.getDeltaPathOnSecondary(); + ArrayList temporaryDeltasToRemove = new ArrayList<>(); + boolean result = false; + try { + imagePool = storagePoolManager.getStoragePoolByURI(imageStoreUrl); + if (chainImageStoreUrls != null) { + parentImagePool = storagePoolManager.getStoragePoolByURI(chainImageStoreUrls.get(0)); + chainImagePools = chainImageStoreUrls.subList(1, chainImageStoreUrls.size()).stream().map(storagePoolManager::getStoragePoolByURI).collect(Collectors.toList()); + } + + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) delta.getDataStore(); + KVMStoragePool primaryPool = storagePoolManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + + String topDelta = backupOnSecondary; + while (!deltaPathsToCopy.isEmpty()) { + String backupDeltaFullPathOnSecondary = imagePool.getLocalPathFor(topDelta); + temporaryDeltasToRemove.add(backupDeltaFullPathOnSecondary); + String parentBackupFullPath = null; + + if (parentDeltaPathOnSecondary != null) { + parentBackupFullPath = parentImagePool.getLocalPathFor(parentDeltaPathOnSecondary); + } + + String backupDeltaFullPathOnPrimary = primaryPool.getLocalPathFor(deltaPathsToCopy.remove(0)); + convertDeltaToSecondary(backupDeltaFullPathOnPrimary, backupDeltaFullPathOnSecondary, parentBackupFullPath, delta.getUuid(), waitInMillis); + + if (!deltaPathsToCopy.isEmpty()) { + parentDeltaPathOnSecondary = topDelta; + topDelta = getRelativePathOnSecondaryForBackup(delta.getAccountId(), delta.getVolumeId(), UUID.randomUUID().toString()); + parentImagePool = imagePool; + } + } + + String backupOnSecondaryFullPath = imagePool.getLocalPathFor(backupOnSecondary); + + commitTopDeltaOnBaseBackupOnSecondaryIfNeeded(topDelta, backupOnSecondary, imagePool, backupOnSecondaryFullPath, waitInMillis); + + backupSize = Files.size(Path.of(backupOnSecondaryFullPath)); + result = true; + } catch (LibvirtException | QemuImgException | IOException e) { + logger.error("Exception while converting backup [{}] to secondary storage [{}] due to: [{}].", delta.getPath(), imagePool, e.getMessage(), e); + throw new BackupException("Exception while converting backup to secondary storage.", e, true); + } finally { + removeTemporaryDeltas(temporaryDeltasToRemove, result); + + if (parentImagePool != null) { + storagePoolManager.deleteStoragePool(parentImagePool.getType(), parentImagePool.getUuid()); + } + if (chainImagePools != null) { + chainImagePools.forEach(pool -> storagePoolManager.deleteStoragePool(pool.getType(), pool.getUuid())); + } + if (imagePool != null) { + storagePoolManager.deleteStoragePool(imagePool.getType(), imagePool.getUuid()); + } + } + return new Pair<>(backupOnSecondary, backupSize); + } + + /** + * If there were VM snapshots created after the last backup, we will have copied them alongside the backup delta. If this is the case, we will commit all of them into a single + * base file so that we are left with one file per volume per backup. + * */ + protected void commitTopDeltaOnBaseBackupOnSecondaryIfNeeded(String topDelta, String backupOnSecondary, KVMStoragePool imagePool, String backupOnSecondaryFullPath, + int waitInMillis) throws LibvirtException, QemuImgException { + if (topDelta.equals(backupOnSecondary)) { + return; + } + + QemuImg qemuImg = new QemuImg(waitInMillis); + QemuImgFile topDeltaImg = new QemuImgFile(imagePool.getLocalPathFor(topDelta), QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile baseDeltaImg = new QemuImgFile(backupOnSecondaryFullPath, QemuImg.PhysicalDiskFormat.QCOW2); + + logger.debug("Committing top delta [{}] on base delta [{}].", topDeltaImg, baseDeltaImg); + qemuImg.commit(topDeltaImg, baseDeltaImg, true); + } + + /** + * Will remove any temporary deltas created on secondary storage. If result is true, this means that the backup was a success and the first "temporary delta" is our backup, so + * it will not be removed. + *
+ * There are two uses for this method:
+ * - If we fail to backup we have to clean up the secondary storage.
+ * - If we had VM snapshots created after the last backup, we copied multiple files to secondary storage, and thus we have to clean them up after merging them. + * */ + protected void removeTemporaryDeltas(List temporaryDeltasToRemove, boolean result) { + if (result) { + temporaryDeltasToRemove.remove(0); + } + logger.debug("Removing temporary deltas {}.", temporaryDeltasToRemove); + for (String delta : temporaryDeltasToRemove) { + try { + Files.deleteIfExists(Path.of(delta)); + } catch (IOException ex) { + logger.error("Failed to remove temporary delta [{}]. Will not stop the backup process, but this should be investigated.", delta, ex); + } + } + } + + /** + * Converts a delta from primary storage to secondary storage, if a parent was given, will set it as the backing file for the delta being copied. + * + * @param pathDeltaOnPrimary absolute path of the delta to be copied. + * @param pathDeltaOnSecondary absolute path of the destination of the delta to be copied. + * @param pathParentOnSecondary absolute path of the parent delta, if it exists. + * @param volumeUuid volume uuid, used for logging. + * @param waitInMillis timeout in milliseconds. + * */ + protected void convertDeltaToSecondary(String pathDeltaOnPrimary, String pathDeltaOnSecondary, String pathParentOnSecondary, String volumeUuid, int waitInMillis) + throws QemuImgException, LibvirtException { + QemuImgFile backupDestination = new QemuImgFile(pathDeltaOnSecondary, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile backupOrigin = new QemuImgFile(pathDeltaOnPrimary, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile parentBackup = null; + + if (pathParentOnSecondary != null) { + parentBackup = new QemuImgFile(pathParentOnSecondary, QemuImg.PhysicalDiskFormat.QCOW2); + } + + logger.debug("Converting delta [{}] to [{}] with {}", backupOrigin, backupDestination, parentBackup == null ? "no parent." : String.format("parent [%s].", parentBackup)); + + createDirsIfNeeded(pathDeltaOnSecondary, volumeUuid); + + QemuImg qemuImg = new QemuImg(waitInMillis); + qemuImg.convert(backupOrigin, backupDestination, parentBackup, null, null, new QemuImageOptions(backupOrigin.getFormat(), backupOrigin.getFileName(), null), null, + true, false, false, false, null, null); + } + + + protected void endChainForVolume(LibvirtComputingResource resource, VolumeObjectTO volumeObjectTO, String vmName, boolean isVmRunning, String volumeUuid, String baseVolumePath) + throws BackupException { + + BackupDeltaTO baseVolume = new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, baseVolumePath); + DeltaMergeTreeTO deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, baseVolume, volumeObjectTO, new ArrayList<>()); + + logger.debug("Ending backup chain for volume [{}], the next backup will be a full backup.", volumeObjectTO.getUuid()); + + mergeBackupDelta(resource, deltaMergeTreeTO, volumeObjectTO, vmName, isVmRunning, volumeUuid, false); + } + + /** + * Tries to recover the previous state of the VM. Should only be called if an exception in the backup creation process happened.
+ * For each volume, will:
+ * - Merge back any backup deltas created; + * - Remove the data backed up to the secondary storage; + * */ + protected void recoverPreviousVmStateAndDeletePartialBackup(LibvirtComputingResource resource, List> volumeTosAndNewPaths, String vmName, + boolean runningVm, Map> mapVolumeUuidToDeltaPathOnSecondaryAndSize, KVMStoragePoolManager storagePoolManager, String imageStoreUrl) { + for (Pair volumeObjectTOAndNewPath : volumeTosAndNewPaths) { + VolumeObjectTO volumeObjectTO = volumeObjectTOAndNewPath.first(); + String volumeUuid = volumeObjectTO.getUuid(); + + BackupDeltaTO oldDelta = new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, volumeObjectTO.getPath()); + volumeObjectTO.setPath(volumeObjectTOAndNewPath.second()); + DeltaMergeTreeTO deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, oldDelta, volumeObjectTO, new ArrayList<>()); + + mergeBackupDelta(resource, deltaMergeTreeTO, volumeObjectTO, vmName, runningVm, volumeUuid, false); + + Pair deltaPathOnSecondaryAndSize = mapVolumeUuidToDeltaPathOnSecondaryAndSize.get(volumeUuid); + if (deltaPathOnSecondaryAndSize == null) { + continue; + } + + cleanupDeltaOnSecondary(storagePoolManager, imageStoreUrl, deltaPathOnSecondaryAndSize.first()); + } + } + + protected void cleanupDeltaOnSecondary(KVMStoragePoolManager storagePoolManager, String imageStoreUrl, String deltaPath) { + KVMStoragePool imagePool = null; + + try { + imagePool = storagePoolManager.getStoragePoolByURI(imageStoreUrl); + String fullDeltaPath = imagePool.getLocalPathFor(deltaPath); + + logger.debug("Cleaning up delta at [{}] as part of the post backup error normalization effort.", fullDeltaPath); + + Files.deleteIfExists(Path.of(fullDeltaPath)); + } catch (IOException e) { + logger.error("Exception while trying to cleanup delta at [{}].", deltaPath, e); + } finally { + if (imagePool != null) { + storagePoolManager.deleteStoragePool(imagePool.getType(), imagePool.getUuid()); + } + } + } + + + protected void mergeBackupDelta(LibvirtComputingResource resource, DeltaMergeTreeTO deltaMergeTreeTO, VolumeObjectTO volumeObjectTO, String vmName, boolean isVmRunning, + String volumeUuid, boolean countNewestDeltaAsGrandchild) throws BackupException { + try { + if (isVmRunning) { + resource.mergeDeltaForRunningVm(deltaMergeTreeTO, vmName, volumeObjectTO); + } else { + if (countNewestDeltaAsGrandchild) { + deltaMergeTreeTO.addGrandChild(volumeObjectTO); + } + resource.mergeDeltaForStoppedVm(deltaMergeTreeTO); + } + } catch (LibvirtException | QemuImgException | IOException e) { + logger.error("Exception while merging the last backup delta using delta merge tree [{}] for VM [{}] and volume [{}].", deltaMergeTreeTO, vmName, volumeUuid, e); + throw new BackupException(String.format("Exception during backup wrap-up phase for VM [%s].", vmName), e, false); + } + } + + protected String getRelativePathOnSecondaryForBackup(long accountId, long volumeId, String backupPath) { + return String.format("%s%s%s%s%s%s%s", "backups", File.separator, accountId, File.separator, volumeId, File.separator, backupPath); + } + + protected void createDirsIfNeeded(String deltaFullPath, String volumeUuid) { + String dirs = deltaFullPath.substring(0, deltaFullPath.lastIndexOf(File.separator)); + try { + Files.createDirectories(Path.of(dirs)); + } catch (IOException e) { + throw new BackupException(String.format("Error while creating directories for backup of volume [%s].", volumeUuid), e, true); + } + } + +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtValidateKbossVmCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtValidateKbossVmCommandWrapper.java new file mode 100644 index 000000000000..eec68cd464e3 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtValidateKbossVmCommandWrapper.java @@ -0,0 +1,225 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.DateUtil; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.Script; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.json.JsonSanitizer; +import org.apache.cloudstack.backup.ValidateKbossVmAnswer; +import org.apache.cloudstack.backup.ValidateKbossVmCommand; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; + +@ResourceWrapper(handles = ValidateKbossVmCommand.class) +public class LibvirtValidateKbossVmCommandWrapper extends CommandWrapper { + + private static final String SCREENSHOT_COMMAND = "virsh screenshot --domain %s --file %s"; + private static final String GUEST_SYNC_COMMAND = "{\"execute\": \"guest-sync\", \"arguments\":{\"id\":%s}}"; + private static final String GUEST_EXEC_COMMAND = "{\"execute\": \"guest-exec\", \"arguments\":{\"path\":\"%s\",\"arg\":%s,\"capture-output\":true}}"; + private static final String GUEST_EXEC_STATUS_COMMAND = "{\"execute\": \"guest-exec-status\", \"arguments\":{\"pid\":%s}}"; + + @Override + public Answer execute(ValidateKbossVmCommand command, LibvirtComputingResource serverResource) { + VirtualMachineTO vmTo = command.getVm(); + KVMStoragePool secondaryStorage = null; + KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + try { + Domain vm = serverResource.getDomain(serverResource.getLibvirtUtilitiesHelper().getConnection(), vmTo.getName()); + secondaryStorage = storagePoolMgr.getStoragePoolByURI(command.getBackupDeltaTO().getDataStore().getUrl()); + logger.info("Validating VM [{}].", vm.getName()); + boolean bootValidated = waitForBoot(command, vm); + String screenshotPath = takeScreenshot(command, vm, secondaryStorage); + String scriptResult = runScript(command, vm); + return new ValidateKbossVmAnswer(command, bootValidated, screenshotPath, scriptResult); + } catch (LibvirtException e) { + logger.error("Received Libvirt exception while trying to validate VM [{}].", vmTo.getName(), e); + return new Answer(command, e); + } finally { + if (secondaryStorage != null) { + storagePoolMgr.deleteStoragePool(secondaryStorage.getType(), secondaryStorage.getUuid()); + } + } + } + + private boolean waitForBoot(ValidateKbossVmCommand cmd, Domain vm) throws LibvirtException { + if (!cmd.isWaitForBoot()) { + return false; + } + Random random = new Random(); + Integer bootTimeout = cmd.getBootTimeout(); + logger.debug("Waiting for validation VM [{}] to boot. We will wait for [{}] seconds at most.", vm.getName(), bootTimeout); + while (bootTimeout > 0) { + bootTimeout -= 5; + int randomInt = random.nextInt(); + try { + String result = vm.qemuAgentCommand(String.format(GUEST_SYNC_COMMAND, randomInt), 1, 0); + if (result.contains(String.valueOf(randomInt))) { + logger.info("Validation VM [{}] has booted.", vm.getName()); + return true; + } + } catch (LibvirtException ex) { + if (!ex.getMessage().contains(LibvirtComputingResource.AGENT_IS_NOT_CONNECTED)) { + logger.error("Got an unexpected Libvirt Exception, giving up on validating VM [{}].", vm.getName(), ex); + throw ex; + } + } + try { + Thread.sleep(5 * 1000L); + } catch (InterruptedException e) { + logger.debug("Got interrupted while waiting for VM [{}] to boot. Ignoring.", vm.getName()); + } + } + logger.debug("Boot wait timed out for VM [{}].", vm.getName()); + return false; + } + + private String takeScreenshot(ValidateKbossVmCommand command, Domain vm, KVMStoragePool secondaryStorage) throws LibvirtException { + if (!command.isTakeScreenshot()) { + return null; + } + String vmName = vm.getName(); + try { + logger.info("Waiting [{}] seconds to take screenshot of validation VM [{}].", command.getScreenshotWait(), vm.getName()); + Thread.sleep(command.getScreenshotWait() * 1000L); + } catch (InterruptedException e) { + logger.debug("Got interrupted while waiting to take screenshot of validation VM [{}].", vm.getName()); + } + logger.info("Taking screenshot of VM [{}].", vmName); + String ssPath = secondaryStorage.getLocalPathFor(command.getBackupDeltaTO().getPath()) + String.format("-screenshot-%s", DateUtil.getDateInSystemTimeZone()); + if (Script.runSimpleBashScript(String.format(SCREENSHOT_COMMAND, vmName, ssPath)) == null) { + throw new CloudRuntimeException(String.format("Got an unexpected error while trying to execute the screenshot validation step for VM [%s].", vmName)); + } + try { + return tryToConvertFileToPng(ssPath); + } catch (IOException ex) { + throw new CloudRuntimeException(ex); + } + } + + private String tryToConvertFileToPng(String ssPath) throws IOException { + File inputFile = new File(ssPath); + String pngPath = ssPath + ".png"; + File outputFile = new File(pngPath); + BufferedImage image = ImageIO.read(inputFile); + + String warnMessage = String.format("Unable to convert screenshot at [%s] to PNG. Leaving it as is.", ssPath); + if (image == null) { + logger.warn(warnMessage); + return ssPath.substring(ssPath.indexOf("backups")); + } + boolean result = ImageIO.write(image, "png", outputFile); + + if (result) { + logger.debug("Successfully converted image at [{}] to PNG at [{}].", ssPath, pngPath); + Files.deleteIfExists(Path.of(ssPath)); + return pngPath.substring(ssPath.indexOf("backups")); + } else { + logger.warn(warnMessage); + return ssPath.substring(ssPath.indexOf("backups")); + } + } + + private String runScript(ValidateKbossVmCommand command, Domain vm) throws LibvirtException { + if (!command.isExecuteScript()) { + return null; + } + String script = command.getScriptToExecute(); + if (script == null) { + logger.warn("This command is malformed, we should execute an script for VM [{}], but no script was configured. Please review the original VM configurations.", vm.getName()); + return null; + } + String arguments = command.getScriptArguments(); + if (arguments == null) { + arguments = "[]"; + } else { + arguments = "[\"" + arguments.replace(",", "\",\"") + "\"]"; + } + logger.debug("Running validation script [{}] with arguments [{}] on dummy validation VM [{}].", script, arguments, vm.getName()); + String guestCommand = String.format(GUEST_EXEC_COMMAND, script, arguments); + String sanitizedGuestCommand = JsonSanitizer.sanitize(guestCommand); + String execResult; + try { + execResult = vm.qemuAgentCommand(sanitizedGuestCommand, command.getScriptTimeout(), 0); + } catch (LibvirtException ex) { + return ex.getMessage(); + } + JsonObject root = new JsonParser().parse(execResult).getAsJsonObject(); + JsonObject ret = root.getAsJsonObject("return"); + String pid = ret.get("pid").getAsString(); + + return waitForCommandResult(command, vm, pid, script, arguments); + } + + private String waitForCommandResult(ValidateKbossVmCommand command, Domain vm, String pid, String script, String arguments) throws LibvirtException { + JsonObject root; + JsonObject ret; + String expectedResult = command.getExpectedResult(); + Integer timeout = command.getScriptTimeout(); + while (timeout > 0) { + timeout -= 5; + try { + String statusResult = vm.qemuAgentCommand(String.format(GUEST_EXEC_STATUS_COMMAND, pid), 1, 0); + root = new JsonParser().parse(statusResult).getAsJsonObject(); + ret = root.getAsJsonObject("return"); + + boolean exited = ret.get("exited").getAsBoolean(); + if (exited) { + int exitCode = ret.get("exitcode").getAsInt(); + String outData64Coded = ret.get("out-data").getAsString(); + logger.debug("Script [{}] with arguments [{}] that ran on validation VM [{}] exited with code [{}] and had output [{}].", script, arguments, vm.getName(), + exitCode, outData64Coded); + if (expectedResult.equals("0") && exitCode == 0) { + return null; + } + if (outData64Coded.equals(expectedResult)) { + return null; + } + return outData64Coded; + } + } catch (LibvirtException ex) { + logger.error("Caught unexpected Libvirt exception while waiting for validation script result of VM [{}]. Will try again later.", vm.getName(), ex); + } + try { + Thread.sleep(5 * 1000L); + } catch (InterruptedException e) { + logger.debug("Got interrupted while waiting for execution of validation script for VM [{}]. Ignoring.", vm.getName()); + } + } + logger.error("Script [{}] that was executed on validation VM [{}] has timed out, giving up.", script, vm.getName()); + return "Timeout"; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java index 996398a286ff..d99847fd921a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java @@ -275,10 +275,10 @@ public boolean disconnectPhysicalDisksViaVmSpec(VirtualMachineTO vmSpec) { } public KVMStoragePool getStoragePool(StoragePoolType type, String uuid) { - return this.getStoragePool(type, uuid, false); + return this.getStoragePool(type, uuid, false, true); } - public KVMStoragePool getStoragePool(StoragePoolType type, String uuid, boolean refreshInfo) { + public synchronized KVMStoragePool getStoragePool(StoragePoolType type, String uuid, boolean refreshInfo, boolean addDetails) { StorageAdaptor adaptor = getStorageAdaptor(type); KVMStoragePool pool = null; @@ -293,10 +293,9 @@ public KVMStoragePool getStoragePool(StoragePoolType type, String uuid, boolean } } - if (pool instanceof LibvirtStoragePool) { + if (pool instanceof LibvirtStoragePool && addDetails) { LibvirtStoragePool libvirtPool = (LibvirtStoragePool) pool; addPoolDetails(uuid, libvirtPool); - ((LibvirtStoragePool) pool).setType(type); updatePoolTypeIfApplicable(libvirtPool, pool, type, uuid); } @@ -444,7 +443,7 @@ public boolean disconnectPhysicalDisk(StoragePoolType type, String poolUuid, Str return adaptor.disconnectPhysicalDisk(volPath, pool); } - public boolean deleteStoragePool(StoragePoolType type, String uuid) { + public synchronized boolean deleteStoragePool(StoragePoolType type, String uuid) { StorageAdaptor adaptor = getStorageAdaptor(type); if (type == StoragePoolType.NetworkFilesystem) { _haMonitor.removeStoragePool(uuid); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index d10049d0129d..b1d43286b725 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -84,6 +84,7 @@ import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand; import org.apache.cloudstack.storage.command.SyncVolumePathCommand; import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector; +import org.apache.cloudstack.storage.to.BackupDeltaTO; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; @@ -98,6 +99,7 @@ import org.apache.cloudstack.utils.qemu.QemuObject.EncryptFormat; import org.apache.cloudstack.utils.security.ParserUtils; import org.apache.commons.collections.MapUtils; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.BooleanUtils; @@ -248,6 +250,7 @@ public class KVMStorageProcessor implements StorageProcessor { " \n" + ""; + public static final List poolTypesToDeleteChainInfo = Arrays.asList(StoragePoolType.Filesystem, StoragePoolType.NetworkFilesystem, StoragePoolType.SharedMountPoint); public KVMStorageProcessor(final KVMStoragePoolManager storagePoolMgr, final LibvirtComputingResource resource) { this.storagePoolMgr = storagePoolMgr; @@ -2493,7 +2496,7 @@ private SnapshotObjectTO takeFullVolumeSnapshotOfRunningVm(CreateObjectCommand c String convertResult = convertBaseFileToSnapshotFileInStorageDir(ObjectUtils.defaultIfNull(secondaryPool, primaryPool), disk, snapshotPath, directoryPath, volume, cmd.getWait()); - resource.mergeSnapshotIntoBaseFile(vm, diskLabel, diskPath, null, true, snapshotName, volume, conn); + resource.mergeDeltaIntoBaseFile(vm, diskLabel, diskPath, null, true, snapshotName, volume, conn); validateConvertResult(convertResult, snapshotPath); } catch (LibvirtException e) { @@ -2904,6 +2907,12 @@ public Answer deleteVolume(final DeleteCommand cmd) { } } pool.deletePhysicalDisk(vol.getPath(), vol.getFormat()); + if (CollectionUtils.isNotEmpty(vol.getDeltasToRemove()) && poolTypesToDeleteChainInfo.contains(pool.getType()) && vol.getFormat() == ImageFormat.QCOW2 && cmd.isDeleteChain()) { + for (String deltaPath : vol.getDeltasToRemove()) { + logger.debug("Deleting leftover backup delta at [{}].", deltaPath); + pool.deletePhysicalDisk(deltaPath, vol.getFormat()); + } + } return new Answer(null); } catch (final CloudRuntimeException e) { logger.debug("Failed to delete volume: ", e); @@ -3474,6 +3483,20 @@ public Answer syncVolumePath(SyncVolumePathCommand cmd) { return new Answer(cmd, false, "Not currently applicable for KVMStorageProcessor"); } + @Override + public Answer deleteBackup(DeleteCommand cmd) { + BackupDeltaTO delta = (BackupDeltaTO)cmd.getData(); + logger.debug("Deleting backup delta [{}].", delta); + PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO)delta.getDataStore(); + KVMStoragePool pool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid()); + try { + pool.deletePhysicalDisk(delta.getPath(), delta.getFormat()); + } catch (CloudRuntimeException e) { + return new Answer(cmd, e); + } + return new Answer(cmd); + } + /** * Determine if migration is using host-local source pool. If so, return this host's storage as the template source, * rather than remote host's diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java index db37a7e948c4..4bfac31b68f9 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java @@ -1605,8 +1605,9 @@ to support snapshots(backuped) as qcow2 files. */ if (destPool.getType() == StoragePoolType.CLVM) { keepBitmaps = false; } - qemu.convert(srcFile, destFile, null, null, new QemuImageOptions(srcFile.getFormat(), srcFile.getFileName(), null), - null, false, keepBitmaps); + qemu.convert(srcFile, destFile, null, null, null, new QemuImageOptions(srcFile.getFormat(), srcFile.getFileName(), null), + null, false, keepBitmaps, false, + false, null, null); Map destInfo = qemu.info(destFile); Long virtualSize = Long.parseLong(destInfo.get(QemuImg.VIRTUAL_SIZE)); newDisk.setVirtualSize(virtualSize); diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java index e51c80e521c7..cae6832999eb 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java @@ -54,7 +54,8 @@ public class QemuImg { public static final String TARGET_ZERO_FLAG = "--target-is-zero"; public static final String PREALLOCATION = "preallocation"; public static final long QEMU_2_10 = 2010000; - public static final long QEMU_5_10 = 5010000; + public static final long QEMU_5_1 = 5001000; + public static final long QEMU_5_2 = 5002000; public static final int MIN_BITMAP_VERSION = 3; @@ -186,6 +187,12 @@ public QemuImg(final String qemuImgPath) throws LibvirtException { _qemuImgPath = qemuImgPath; } + /** + * Created for testing purposes + * */ + protected QemuImg() { + } + /* These are all methods supported by the qemu-img tool. */ /** @@ -392,7 +399,7 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, */ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, final Map options, final List qemuObjects, final QemuImageOptions srcImageOpts, final String snapshotName, final boolean forceSourceFormat) throws QemuImgException { - convert(srcFile, destFile, options, qemuObjects, srcImageOpts, snapshotName, forceSourceFormat, false); + convert(srcFile, destFile, null, options, qemuObjects, srcImageOpts, snapshotName, forceSourceFormat, false, false, false, null, null); } protected Map getResizeOptionsFromConvertOptions(final Map options) { @@ -408,31 +415,41 @@ protected Map getResizeOptionsFromConvertOptions(final Map * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format. * * @param srcFile - * The source file. + * The source file. * @param destFile - * The destination file. + * The destination file. + * @param backingFile + * The destination's backing file. * @param options - * Options for the conversion. Takes a Map with key value - * pairs which are passed on to qemu-img without validation. + * Options for the conversion. Takes a Map with key value + * pairs which are passed on to qemu-img without validation. * @param qemuObjects - * Pass qemu Objects to create - see objects in the qemu main page. + * Pass qemu Objects to create - see objects in the qemu main page. * @param srcImageOpts - * pass qemu --image-opts to convert. + * pass qemu --image-opts to convert. * @param snapshotName - * If it is provided, conversion uses it as parameter. + * If it is provided, conversion uses it as parameter. * @param forceSourceFormat - * If true, specifies the source format in the conversion command. + * If true, specifies the source format in the conversion command. * @param keepBitmaps - * If true, copies the bitmaps to the destination image. + * If true, copies the bitmaps to the destination image. + * @param outOfOrderWrites + * If true, inform -W to convert + * @param compress + * If true, inform -c to convert + * @param coroutines + * If not null, inform -m and number of coroutines. By default, qemu uses 8 coroutines. + * @param rateLimit + * If not null, inform -r and rate limit in MB/s. By default, qemu does not limit the convert rate. * @return void */ - public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, - final Map options, final List qemuObjects, final QemuImageOptions srcImageOpts, final String snapshotName, final boolean forceSourceFormat, - boolean keepBitmaps) throws QemuImgException { + public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, QemuImgFile backingFile, final Map options, final List qemuObjects, + final QemuImageOptions srcImageOpts, final String snapshotName, final boolean forceSourceFormat, boolean keepBitmaps, boolean outOfOrderWrites, boolean compress, + Integer coroutines, Integer rateLimit) throws QemuImgException { Script script = new Script(_qemuImgPath, timeout); if (StringUtils.isNotBlank(snapshotName)) { String qemuPath = Script.runSimpleBashScript(getQemuImgPathScript); @@ -455,9 +472,28 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, script.add("-O"); script.add(destFile.getFormat().toString()); + addBackingFileToConvertCommand(script, backingFile); addScriptOptionsFromMap(options, script); addSnapshotToConvertCommand(srcFile.getFormat().toString(), snapshotName, forceSourceFormat, script, version); + if (outOfOrderWrites) { + script.add("-W"); + } + + if (rateLimit != null) { + script.add("-r"); + script.add(rateLimit + "M"); + } + + if (coroutines != null) { + script.add("-m"); + script.add(String.valueOf(coroutines)); + } + + if (compress) { + script.add("-c"); + } + if (noCache) { script.add("-t"); script.add("none"); @@ -484,7 +520,7 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, script.add(srcFile.getFileName()); } - if (this.version >= QEMU_5_10 && keepBitmaps && Qcow2Inspector.validateQcow2Version(srcFile.getFileName(), MIN_BITMAP_VERSION)) { + if (this.version >= QEMU_5_1 && keepBitmaps && Qcow2Inspector.validateQcow2Version(srcFile.getFileName(), MIN_BITMAP_VERSION)) { script.add("--bitmaps"); } @@ -500,6 +536,23 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, } } + + protected void addBackingFileToConvertCommand(Script script, QemuImgFile backingFile) { + if (backingFile == null) { + return; + } + + script.add("-o"); + + String opts; + if (backingFile.getFormat() == null) { + opts = String.format("backing_file=%s", backingFile.getFileName()); + } else { + opts = String.format("backing_file=%s,backing_fmt=%s", backingFile.getFileName(), backingFile.getFormat().toString()); + } + script.add(opts); + } + /** * Qemu version 2.0.0 added (via commit ef80654d0dc1edf2dd2a51feff8cc3e1102a6583) the * flag "-l" to inform the snapshot name or ID @@ -871,11 +924,8 @@ public void commit(QemuImgFile file, QemuImgFile base, boolean skipEmptyingFiles throw new QemuImgException("File should not be null"); } - final Script s = new Script(_qemuImgPath, timeout); + final Script s = createScript(_qemuImgPath, timeout); s.add("commit"); - if (skipEmptyingFiles) { - s.add("-d"); - } if (file.getFormat() != null) { s.add("-f"); @@ -885,6 +935,8 @@ public void commit(QemuImgFile file, QemuImgFile base, boolean skipEmptyingFiles if (base != null) { s.add("-b"); s.add(base.getFileName()); + } else if (skipEmptyingFiles) { + s.add("-d"); } s.add(file.getFileName()); @@ -894,6 +946,13 @@ public void commit(QemuImgFile file, QemuImgFile base, boolean skipEmptyingFiles } } + /** + * This was created to facilitate testing + * */ + protected Script createScript(String path, long timeout) { + return new Script(path, timeout); + } + /** * Does qemu-img support --target-is-zero * @return boolean @@ -1009,4 +1068,9 @@ private void removeBitmap(QemuImgFile srcFile, String bitmapName) throws QemuImg throw new QemuImgException(String.format("Exception while removing bitmap [%s] from file [%s]. Result is [%s].", srcFile.getFileName(), bitmapName, result)); } } + + public long getVersion() { + return this.version; + } + } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index 0b72f2232ed0..639f1dc2894c 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -52,7 +52,6 @@ import java.util.Random; import java.util.UUID; import java.util.Vector; -import java.util.concurrent.Semaphore; import javax.naming.ConfigurationException; import javax.xml.parsers.DocumentBuilderFactory; @@ -2407,7 +2406,7 @@ public void testGetStorageStatsCommand() { final KVMStoragePool secondaryPool = Mockito.mock(KVMStoragePool.class); when(libvirtComputingResourceMock.getStoragePoolMgr()).thenReturn(storagePoolMgr); - when(storagePoolMgr.getStoragePool(command.getPooltype(), command.getStorageId(), true)).thenReturn(secondaryPool); + when(storagePoolMgr.getStoragePool(command.getPooltype(), command.getStorageId(), true, true)).thenReturn(secondaryPool); final LibvirtRequestWrapper wrapper = LibvirtRequestWrapper.getInstance(); assertNotNull(wrapper); @@ -2416,7 +2415,7 @@ public void testGetStorageStatsCommand() { assertTrue(answer.getResult()); verify(libvirtComputingResourceMock, times(1)).getStoragePoolMgr(); - verify(storagePoolMgr, times(1)).getStoragePool(command.getPooltype(), command.getStorageId(), true); + verify(storagePoolMgr, times(1)).getStoragePool(command.getPooltype(), command.getStorageId(), true, true); } @SuppressWarnings("unchecked") @@ -6688,10 +6687,12 @@ public void mergeSnapshotIntoBaseFileTestActiveAndDeleteFlags() throws Exception libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10; try (MockedStatic libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); - MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class); + MockedStatic agentPropertiesFileHandlerMockedStatic = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + + agentPropertiesFileHandlerMockedStatic.when(() -> AgentPropertiesFileHandler.getPropertyValue(Mockito.any())).thenAnswer(invocation -> true); libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenAnswer(invocation -> true); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); @@ -6703,7 +6704,7 @@ public void mergeSnapshotIntoBaseFileTestActiveAndDeleteFlags() throws Exception String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock); Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, Domain.BlockCommitFlags.ACTIVE | Domain.BlockCommitFlags.DELETE); Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(true, "/" + snapshotName); @@ -6713,10 +6714,12 @@ public void mergeSnapshotIntoBaseFileTestActiveAndDeleteFlags() throws Exception @Test public void mergeSnapshotIntoBaseFileTestActiveFlag() throws Exception { try (MockedStatic libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); - MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class); + MockedStatic agentPropertiesFileHandlerMockedStatic = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + + agentPropertiesFileHandlerMockedStatic.when(() -> AgentPropertiesFileHandler.getPropertyValue(Mockito.any())).thenAnswer(invocation -> true); libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenAnswer(invocation -> false); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); @@ -6728,7 +6731,7 @@ public void mergeSnapshotIntoBaseFileTestActiveFlag() throws Exception { String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock); Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, Domain.BlockCommitFlags.ACTIVE); Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(false, "/" + snapshotName); @@ -6738,10 +6741,12 @@ public void mergeSnapshotIntoBaseFileTestActiveFlag() throws Exception { @Test public void mergeSnapshotIntoBaseFileTestDeleteFlag() throws Exception { try (MockedStatic libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); - MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class); + MockedStatic agentPropertiesFileHandlerMockedStatic = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + + agentPropertiesFileHandlerMockedStatic.when(() -> AgentPropertiesFileHandler.getPropertyValue(Mockito.any())).thenAnswer(invocation -> true); libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10; libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenReturn(true); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any()); Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt()); @@ -6752,7 +6757,7 @@ public void mergeSnapshotIntoBaseFileTestDeleteFlag() throws Exception { String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, Domain.BlockCommitFlags.DELETE); Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(true, "/" + snapshotName); @@ -6762,10 +6767,12 @@ public void mergeSnapshotIntoBaseFileTestDeleteFlag() throws Exception { @Test public void mergeSnapshotIntoBaseFileTestNoFlags() throws Exception { try (MockedStatic libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); - MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class); + MockedStatic agentPropertiesFileHandlerMockedStatic = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + + agentPropertiesFileHandlerMockedStatic.when(() -> AgentPropertiesFileHandler.getPropertyValue(Mockito.any())).thenAnswer(invocation -> true); libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10; libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenReturn(false); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any()); Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt()); @@ -6776,7 +6783,7 @@ public void mergeSnapshotIntoBaseFileTestNoFlags() throws Exception { String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, 0); Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(false, "/" + snapshotName); @@ -6789,20 +6796,20 @@ public void mergeSnapshotIntoBaseFileTestMergeFailsThrowException() throws Excep MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10; libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenReturn(false); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); + Mockito.doReturn(Boolean.TRUE).when(libvirtComputingResourceSpy).isLibvirtEventsEnabled(); Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any()); Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt()); Mockito.doNothing().when(domainMock).removeBlockJobListener(Mockito.any()); - Mockito.doReturn(blockCommitListenerMock).when(libvirtComputingResourceSpy).getBlockCommitListener(Mockito.any(), Mockito.any()); + Mockito.doReturn(blockCommitListenerMock).when(libvirtComputingResourceSpy).getBlockCommitListener(Mockito.any()); Mockito.doReturn("Failed").when(blockCommitListenerMock).getResult(); String diskLabel = "vda"; String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirTakeKbossBackupCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirTakeKbossBackupCommandWrapperTest.java new file mode 100644 index 000000000000..b726b92c7424 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirTakeKbossBackupCommandWrapperTest.java @@ -0,0 +1,377 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.backup.TakeKbossBackupAnswer; +import org.apache.cloudstack.backup.TakeKbossBackupCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.libvirt.LibvirtException; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; + +@RunWith(MockitoJUnitRunner.class) +public class LibvirTakeKbossBackupCommandWrapperTest { + + @Mock + private TakeKbossBackupCommand takeKbossBackupCommandMock; + + @Mock + private LibvirtComputingResource libvirtComputingResourceMock; + + @Mock + private KVMStoragePoolManager kvmStoragePoolManagerMock; + + @Mock + private KVMStoragePool kvmStoragePool1; + + @Mock + private KVMStoragePool kvmStoragePool2; + + @Mock + private KVMStoragePool kvmStoragePool3; + + @Mock + private KbossTO kbossTO1; + + @Mock + private KbossTO kbossTO2; + + @Mock + private VolumeObjectTO volumeObjectToMock1; + + @Mock + private VolumeObjectTO volumeObjectToMock2; + + @Mock + private DeltaMergeTreeTO deltaMergeTreeToMock; + + @Mock + private BackupDeltaTO backupDeltaTOMock; + + @Mock + private PrimaryDataStoreTO primaryDataStoreToMock; + + @Spy + @InjectMocks + private LibvirtTakeKbossBackupCommandWrapper libvirtTakeKbossBackupCommandWrapperSpy; + + private String volUuid1 = "uuid1"; + + private String volUuid2 = "uuid2"; + + private String deltaPath1 = "deltapath1"; + + private String deltaPath2 = "deltapath2"; + + private String secondaryUrl = "nfs://1.1.1.2:/mnt"; + + private String secondaryUrl2 = "nfs://2.2.2.2:/mnt2"; + + @Test + public void executeTestBackupException() { + doReturn(List.of()).when(takeKbossBackupCommandMock).getKbossTOs(); + doThrow(new BackupException("tst", false)).when(libvirtComputingResourceMock).createDiskOnlyVMSnapshotOfStoppedVm(any(), any()); + + TakeKbossBackupAnswer answer = (TakeKbossBackupAnswer)libvirtTakeKbossBackupCommandWrapperSpy.execute(takeKbossBackupCommandMock, libvirtComputingResourceMock); + + assertFalse(answer.getResult()); + assertFalse(answer.isVmConsistent()); + } + + @Test + public void executeTestSuccessStoppedVm() { + doReturn(List.of()).when(takeKbossBackupCommandMock).getKbossTOs(); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).backupVolumes(any(), any(), any(), any(), any(), any(), anyBoolean(), any()); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).cleanupVm(any(), any(), any(), any(), anyBoolean(), any()); + + TakeKbossBackupAnswer answer = (TakeKbossBackupAnswer)libvirtTakeKbossBackupCommandWrapperSpy.execute(takeKbossBackupCommandMock, libvirtComputingResourceMock); + + verify(libvirtComputingResourceMock).createDiskOnlyVMSnapshotOfStoppedVm(any(), any()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).backupVolumes(any(), any(), any(), any(), any(), any(), anyBoolean(), any()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).cleanupVm(any(), any(), any(), any(), anyBoolean(), any()); + assertTrue(answer.getResult()); + } + + @Test + public void executeTestSuccessRunningVm() { + doReturn(List.of()).when(takeKbossBackupCommandMock).getKbossTOs(); + doReturn(true).when(takeKbossBackupCommandMock).isRunningVM(); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).backupVolumes(any(), any(), any(), any(), any(), any(), anyBoolean(), any()); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).cleanupVm(any(), any(), any(), any(), anyBoolean(), any()); + + TakeKbossBackupAnswer answer = (TakeKbossBackupAnswer)libvirtTakeKbossBackupCommandWrapperSpy.execute(takeKbossBackupCommandMock, libvirtComputingResourceMock); + + verify(libvirtComputingResourceMock).createDiskOnlyVmSnapshotForRunningVm(any(), any(), any(), anyBoolean()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).backupVolumes(any(), any(), any(), any(), any(), any(), anyBoolean(), any()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).cleanupVm(any(), any(), any(), any(), anyBoolean(), any()); + assertTrue(answer.getResult()); + } + + @Test (expected = BackupException.class) + public void backupVolumesTestRecoverIfExceptionThrown() { + List> volumeTosAndNewPaths = new ArrayList<>(); + Map> mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize = new HashMap<>(); + String secondaryUrl = "nfs://1.1.1.2:/mnt"; + + doReturn(secondaryUrl).when(takeKbossBackupCommandMock).getImageStoreUrl(); + doThrow(new RuntimeException("odasij")).when(kbossTO1).getVolumeObjectTO(); + + libvirtTakeKbossBackupCommandWrapperSpy.backupVolumes(takeKbossBackupCommandMock, libvirtComputingResourceMock, kvmStoragePoolManagerMock, List.of(kbossTO1), + volumeTosAndNewPaths, "tst", false, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize); + + + verify(libvirtTakeKbossBackupCommandWrapperSpy).recoverPreviousVmStateAndDeletePartialBackup(libvirtComputingResourceMock, volumeTosAndNewPaths, "tst", false, + mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize, kvmStoragePoolManagerMock, secondaryUrl); + } + + @Test + public void backupVolumesTestHappyPath() { + setupKbossTos(); + List> volumeTosAndNewPaths = new ArrayList<>(); + Map> mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize = new HashMap<>(); + + doReturn(100).when(takeKbossBackupCommandMock).getWait(); + doReturn(secondaryUrl).when(takeKbossBackupCommandMock).getImageStoreUrl(); + Pair pair1 = new Pair<>("p1", 10L); + doReturn(pair1).when(libvirtTakeKbossBackupCommandWrapperSpy).copyBackupDeltaToSecondary(eq(kvmStoragePoolManagerMock), eq(kbossTO1), anyList(), + eq(secondaryUrl), anyInt()); + Pair pair2 = new Pair<>("p2", 13L); + doReturn(pair2).when(libvirtTakeKbossBackupCommandWrapperSpy).copyBackupDeltaToSecondary(eq(kvmStoragePoolManagerMock), eq(kbossTO2), anyList(), + eq(secondaryUrl), anyInt()); + + libvirtTakeKbossBackupCommandWrapperSpy.backupVolumes(takeKbossBackupCommandMock, libvirtComputingResourceMock, kvmStoragePoolManagerMock, List.of(kbossTO1, kbossTO2), + volumeTosAndNewPaths, "tst", false, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize); + + verify(libvirtTakeKbossBackupCommandWrapperSpy, never()).recoverPreviousVmStateAndDeletePartialBackup(libvirtComputingResourceMock, volumeTosAndNewPaths, "tst", false, + mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize, kvmStoragePoolManagerMock, secondaryUrl); + assertEquals(pair1, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize.get(volUuid1)); + assertEquals(pair2, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize.get(volUuid2)); + } + + @Test + public void cleanupVmTestEndOfChain() { + setupKbossTos(); + Map mapVolumeUUidToNewVolumePath = new HashMap<>(); + String path1 = "path1"; + String path2 = "path2"; + String path3 = "path3"; + String vmName = "ttt"; + doReturn(path1).when(volumeObjectToMock1).getPath(); + doReturn(path2).when(volumeObjectToMock2).getPath(); + doReturn(deltaPath1).when(kbossTO1).getDeltaPathOnPrimary(); + doReturn(deltaPath2).when(kbossTO2).getDeltaPathOnPrimary(); + doReturn(deltaMergeTreeToMock).when(kbossTO1).getDeltaMergeTreeTO(); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).mergeBackupDelta(any(), any(), any(), any(), anyBoolean(), any(), anyBoolean()); + doReturn(true).when(takeKbossBackupCommandMock).isEndChain(); + doReturn(volumeObjectToMock1).when(deltaMergeTreeToMock).getChild(); + doReturn(backupDeltaTOMock).when(deltaMergeTreeToMock).getParent(); + doReturn(path3).when(backupDeltaTOMock).getPath(); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).endChainForVolume(libvirtComputingResourceMock, volumeObjectToMock1, vmName, true, volUuid1, path3); + + libvirtTakeKbossBackupCommandWrapperSpy.cleanupVm(takeKbossBackupCommandMock, libvirtComputingResourceMock, List.of(kbossTO1, kbossTO2), vmName, true, + mapVolumeUUidToNewVolumePath); + + verify(volumeObjectToMock1).setPath(deltaPath1); + verify(volumeObjectToMock2).setPath(deltaPath2); + verify(libvirtTakeKbossBackupCommandWrapperSpy).mergeBackupDelta(eq(libvirtComputingResourceMock), eq(deltaMergeTreeToMock), eq(volumeObjectToMock1), eq(vmName), eq(true), + eq(volUuid1), anyBoolean()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).endChainForVolume(libvirtComputingResourceMock, volumeObjectToMock1, vmName, true, volUuid1, path3); + assertEquals(path3, mapVolumeUUidToNewVolumePath.get(volUuid1)); + assertEquals(path2, mapVolumeUUidToNewVolumePath.get(volUuid2)); + } + + @Test + public void cleanupVmTestNotEndOfChainAndNotIsolated() { + setupKbossTos(); + Map mapVolumeUUidToNewVolumePath = new HashMap<>(); + String vmName = "ttt"; + doReturn(deltaPath1).when(kbossTO1).getDeltaPathOnPrimary(); + doReturn(deltaPath2).when(kbossTO2).getDeltaPathOnPrimary(); + + libvirtTakeKbossBackupCommandWrapperSpy.cleanupVm(takeKbossBackupCommandMock, libvirtComputingResourceMock, List.of(kbossTO1, kbossTO2), vmName, true, + mapVolumeUUidToNewVolumePath); + + verify(volumeObjectToMock1).setPath(deltaPath1); + verify(volumeObjectToMock2).setPath(deltaPath2); + assertEquals(deltaPath1, mapVolumeUUidToNewVolumePath.get(volUuid1)); + assertEquals(deltaPath2, mapVolumeUUidToNewVolumePath.get(volUuid2)); + } + + @Test + public void copyBackupDeltaToSecondaryTest() throws LibvirtException, QemuImgException { + String parentPath = "parentPath"; + String volumePath = "volPath"; + String parentBackupFullPath = "parentBackupFullPath"; + String backupDeltaFullPathOnPrimary1 = "backupDeltaFullPathOnPrimary1"; + String backupDeltaFullPathOnSecondary1 = "backupDeltaFullPathOnSecondary1"; + String randomPath1 = "random"; + String backupDeltaFullPathOnPrimary2 = "backupDeltaFullPathOnPrimary2"; + + doReturn(volumeObjectToMock1).when(kbossTO1).getVolumeObjectTO(); + doReturn(volumePath).when(volumeObjectToMock1).getPath(); + doReturn(volUuid1).when(volumeObjectToMock1).getUuid(); + doReturn(parentPath).when(kbossTO1).getPathBackupParentOnSecondary(); + doReturn(new ArrayList<>(List.of(deltaPath2))).when(kbossTO1).getDeltaPaths(); + doReturn(deltaPath1).when(kbossTO1).getDeltaPathOnSecondary(); + doReturn(kvmStoragePool1).when(kvmStoragePoolManagerMock).getStoragePoolByURI(secondaryUrl); + doReturn(kvmStoragePool2).when(kvmStoragePoolManagerMock).getStoragePoolByURI(secondaryUrl2); + doReturn(primaryDataStoreToMock).when(volumeObjectToMock1).getDataStore(); + doReturn(kvmStoragePool3).when(kvmStoragePoolManagerMock).getStoragePool(any(), any()); + + doReturn(parentBackupFullPath).when(kvmStoragePool2).getLocalPathFor(parentPath); + doReturn(backupDeltaFullPathOnPrimary1).when(kvmStoragePool3).getLocalPathFor(deltaPath2); + doReturn(backupDeltaFullPathOnSecondary1).when(kvmStoragePool1).getLocalPathFor(deltaPath1); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).convertDeltaToSecondary(backupDeltaFullPathOnPrimary1, backupDeltaFullPathOnSecondary1, parentBackupFullPath, volUuid1, 100000); + + doReturn(randomPath1).when(libvirtTakeKbossBackupCommandWrapperSpy).getRelativePathOnSecondaryForBackup(anyLong(), anyLong(), any()); + doReturn("random2").when(kvmStoragePool1).getLocalPathFor(randomPath1); + doReturn(backupDeltaFullPathOnPrimary2).when(kvmStoragePool3).getLocalPathFor(volumePath); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).convertDeltaToSecondary(eq(backupDeltaFullPathOnPrimary2), eq("random2"), eq(backupDeltaFullPathOnSecondary1), + any(), anyInt()); + + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).commitTopDeltaOnBaseBackupOnSecondaryIfNeeded(randomPath1, deltaPath1, kvmStoragePool1, + backupDeltaFullPathOnSecondary1, 100000); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).removeTemporaryDeltas(any(), anyBoolean()); + + try(MockedStatic filesMockedStatic = Mockito.mockStatic(Files.class)) { + filesMockedStatic.when(() -> Files.size(any())).thenReturn(1000L); + Pair result = libvirtTakeKbossBackupCommandWrapperSpy.copyBackupDeltaToSecondary(kvmStoragePoolManagerMock, kbossTO1, List.of(secondaryUrl2), + secondaryUrl, 100000); + + assertEquals(deltaPath1, result.first()); + assertEquals(Long.valueOf(1000L), result.second()); + } + + verify(libvirtTakeKbossBackupCommandWrapperSpy).convertDeltaToSecondary(backupDeltaFullPathOnPrimary1, backupDeltaFullPathOnSecondary1, parentBackupFullPath, volUuid1, 100000); + verify(libvirtTakeKbossBackupCommandWrapperSpy).convertDeltaToSecondary(eq(backupDeltaFullPathOnPrimary2), eq("random2"), eq(backupDeltaFullPathOnSecondary1), + any(), anyInt()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).commitTopDeltaOnBaseBackupOnSecondaryIfNeeded(randomPath1, deltaPath1, kvmStoragePool1, + backupDeltaFullPathOnSecondary1, 100000); + verify(libvirtTakeKbossBackupCommandWrapperSpy).removeTemporaryDeltas(any(), anyBoolean()); + } + + @Test + public void removeTemporaryDeltasTestResultTrue() { + ArrayList input = new ArrayList<>(List.of("a", "b")); + + try(MockedStatic filesMockedStatic = Mockito.mockStatic(Files.class)) { + libvirtTakeKbossBackupCommandWrapperSpy.removeTemporaryDeltas(input, true); + + filesMockedStatic.verify(() -> Files.deleteIfExists(any()), Mockito.times(1)); + } + } + + @Test + public void removeTemporaryDeltasTestResultFalse() { + ArrayList input = new ArrayList<>(List.of("a", "b")); + + try(MockedStatic filesMockedStatic = Mockito.mockStatic(Files.class)) { + libvirtTakeKbossBackupCommandWrapperSpy.removeTemporaryDeltas(input, false); + + filesMockedStatic.verify(() -> Files.deleteIfExists(any()), Mockito.times(2)); + } + } + + @Test + public void removeTemporaryDeltasTestExceptionIsIgnored() { + ArrayList input = new ArrayList<>(List.of("a", "b")); + + try(MockedStatic filesMockedStatic = Mockito.mockStatic(Files.class)) { + filesMockedStatic.when(() -> Files.deleteIfExists(any())).thenThrow(new IOException("das")); + libvirtTakeKbossBackupCommandWrapperSpy.removeTemporaryDeltas(input, false); + + filesMockedStatic.verify(() -> Files.deleteIfExists(any()), Mockito.times(2)); + } + } + + @Test (expected = BackupException.class) + public void mergeBackupDeltaTestThrowsException() throws LibvirtException, QemuImgException { + doThrow(new QemuImgException("a")).when(libvirtComputingResourceMock).mergeDeltaForRunningVm(any(), any(), any()); + + libvirtTakeKbossBackupCommandWrapperSpy.mergeBackupDelta(libvirtComputingResourceMock, deltaMergeTreeToMock, volumeObjectToMock1, "ttt", true, volUuid1, false); + } + + @Test + public void mergeBackupDeltaTestRunningVm() throws LibvirtException, QemuImgException { + libvirtTakeKbossBackupCommandWrapperSpy.mergeBackupDelta(libvirtComputingResourceMock, deltaMergeTreeToMock, volumeObjectToMock1, "ttt", true, volUuid1, false); + + verify(libvirtComputingResourceMock).mergeDeltaForRunningVm(deltaMergeTreeToMock, "ttt", volumeObjectToMock1); + } + + @Test + public void mergeBackupDeltaTestStoppedVm() throws LibvirtException, QemuImgException, IOException { + libvirtTakeKbossBackupCommandWrapperSpy.mergeBackupDelta(libvirtComputingResourceMock, deltaMergeTreeToMock, volumeObjectToMock1, "ttt", false, volUuid1, false); + + verify(libvirtComputingResourceMock).mergeDeltaForStoppedVm(deltaMergeTreeToMock); + } + + @Test + public void mergeBackupDeltaTestStoppedVmCountNewestDeltaAsGrandChild() throws LibvirtException, QemuImgException, IOException { + libvirtTakeKbossBackupCommandWrapperSpy.mergeBackupDelta(libvirtComputingResourceMock, deltaMergeTreeToMock, volumeObjectToMock1, "ttt", false, volUuid1, true); + + verify(deltaMergeTreeToMock).addGrandChild(volumeObjectToMock1); + verify(libvirtComputingResourceMock).mergeDeltaForStoppedVm(deltaMergeTreeToMock); + } + + private void setupKbossTos() { + doReturn(volumeObjectToMock1).when(kbossTO1).getVolumeObjectTO(); + doReturn(volUuid1).when(volumeObjectToMock1).getUuid(); + doReturn(volumeObjectToMock2).when(kbossTO2).getVolumeObjectTO(); + doReturn(volUuid2).when(volumeObjectToMock2).getUuid(); + } +} diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapperTest.java index 4e039f318928..f4c85aa611f4 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapperTest.java @@ -88,7 +88,7 @@ public void setUp() { Mockito.when(pool.getUuid()).thenReturn(poolUuid); Mockito.when(pool.getType()).thenReturn(poolType); Mockito.when(libvirtComputingResource.getStoragePoolMgr()).thenReturn(storagePoolMgr); - Mockito.when(storagePoolMgr.getStoragePool(poolType, poolUuid, true)).thenReturn(storagePool); + Mockito.when(storagePoolMgr.getStoragePool(poolType, poolUuid, true, true)).thenReturn(storagePool); qemuImg = Mockito.mockConstruction(QemuImg.class, (mock, context) -> { Mockito.when(mock.info(Mockito.any(QemuImgFile.class), Mockito.eq(true))).thenReturn(qemuImgInfo); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapperTest.java new file mode 100644 index 000000000000..75668ad15f3b --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapperTest.java @@ -0,0 +1,181 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.storage.Storage; +import com.cloud.utils.Pair; +import org.apache.cloudstack.backup.RestoreKbossBackupAnswer; +import org.apache.cloudstack.backup.RestoreKbossBackupCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.libvirt.LibvirtException; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; + +import java.io.IOException; +import java.util.Set; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class LibvirtRestoreKbossBackupCommandWrapperTest { + + @Mock + private LibvirtComputingResource libvirtComputingResourceMock; + + @Mock + private KVMStoragePoolManager kvmStoragePoolManagerMock; + + @Mock + private KVMStoragePool kvmStoragePool1; + + @Mock + private KVMStoragePool kvmStoragePool2; + + @Mock + private KVMStoragePool kvmStoragePool3; + + @Mock + private KbossTO kbossTO1; + + @Mock + private KbossTO kbossTO2; + + @Mock + private VolumeObjectTO volumeObjectToMock1; + + @Mock + private VolumeObjectTO volumeObjectToMock2; + + @Mock + private DeltaMergeTreeTO deltaMergeTreeToMock; + + @Mock + private BackupDeltaTO backupDeltaTOMock; + + @Mock + private PrimaryDataStoreTO primaryDataStoreToMock; + + @Mock + private RestoreKbossBackupCommand cmdMock; + + @Mock + private QemuImg qemuImgMock; + + @Spy + @InjectMocks + private LibvirtRestoreKbossBackupCommandWrapper libvirtRestoreKbossBackupCommandWrapperSpy; + + @Test + public void executeTestException() throws LibvirtException, QemuImgException { + doReturn(primaryDataStoreToMock).when(backupDeltaTOMock).getDataStore(); + doReturn(Set.of(new Pair<>(backupDeltaTOMock, volumeObjectToMock1))).when(cmdMock).getBackupAndVolumePairs(); + doReturn(null).when(libvirtRestoreKbossBackupCommandWrapperSpy).mountSecondaryStorages(any(), any(), any(), any()); + doThrow(new QemuImgException("asd")).when(libvirtRestoreKbossBackupCommandWrapperSpy).restoreVolumes(any(), any(), any(), anyBoolean(), anyInt()); + + RestoreKbossBackupAnswer answer = (RestoreKbossBackupAnswer)libvirtRestoreKbossBackupCommandWrapperSpy.execute(cmdMock, libvirtComputingResourceMock); + assertFalse(answer.getResult()); + } + + + @Test + public void executeTestLibvirtNotQuickRestore() throws LibvirtException, QemuImgException, IOException { + doReturn(primaryDataStoreToMock).when(backupDeltaTOMock).getDataStore(); + doReturn(Set.of(new Pair<>(backupDeltaTOMock, volumeObjectToMock1))).when(cmdMock).getBackupAndVolumePairs(); + doReturn(kvmStoragePoolManagerMock).when(libvirtComputingResourceMock).getStoragePoolMgr(); + doReturn(kvmStoragePool1).when(kvmStoragePoolManagerMock).getStoragePoolByURI(any()); + doReturn("uuid").when(kvmStoragePool1).getUuid(); + doNothing().when(libvirtRestoreKbossBackupCommandWrapperSpy).restoreVolumes(any(), any(), any(), anyBoolean(), anyInt()); + doNothing().when(libvirtRestoreKbossBackupCommandWrapperSpy).deleteDeltas(any(), any()); + + doReturn(false).when(cmdMock).isQuickRestore(); + + RestoreKbossBackupAnswer answer = (RestoreKbossBackupAnswer)libvirtRestoreKbossBackupCommandWrapperSpy.execute(cmdMock, libvirtComputingResourceMock); + assertTrue(answer.getResult()); + verify(kvmStoragePoolManagerMock).deleteStoragePool(Storage.StoragePoolType.NetworkFilesystem, "uuid"); + } + + + @Test + public void executeTestLibvirtQuickRestore() throws LibvirtException, QemuImgException, IOException { + doReturn(primaryDataStoreToMock).when(backupDeltaTOMock).getDataStore(); + doReturn(Set.of(new Pair<>(backupDeltaTOMock, volumeObjectToMock1))).when(cmdMock).getBackupAndVolumePairs(); + doReturn(kvmStoragePoolManagerMock).when(libvirtComputingResourceMock).getStoragePoolMgr(); + doReturn(kvmStoragePool1).when(kvmStoragePoolManagerMock).getStoragePoolByURI(any()); + doReturn("uuid").when(kvmStoragePool1).getUuid(); + doNothing().when(libvirtRestoreKbossBackupCommandWrapperSpy).restoreVolumes(any(), any(), any(), anyBoolean(), anyInt()); + doNothing().when(libvirtRestoreKbossBackupCommandWrapperSpy).deleteDeltas(any(), any()); + + doReturn(true).when(cmdMock).isQuickRestore(); + + RestoreKbossBackupAnswer answer = (RestoreKbossBackupAnswer)libvirtRestoreKbossBackupCommandWrapperSpy.execute(cmdMock, libvirtComputingResourceMock); + assertTrue(answer.getResult()); + verify(kvmStoragePoolManagerMock, never()).deleteStoragePool(Storage.StoragePoolType.NetworkFilesystem, "uuid"); + } + + @Test + public void restoreVolumesTestQuickRestore() throws LibvirtException, QemuImgException { + doReturn(primaryDataStoreToMock).when(volumeObjectToMock1).getDataStore(); + doReturn(kvmStoragePool2).when(kvmStoragePoolManagerMock).getStoragePool(any(), any()); + doReturn("p2").when(kvmStoragePool2).getLocalPathFor(any()); + doReturn(qemuImgMock).when(libvirtRestoreKbossBackupCommandWrapperSpy).getQemuImg(anyInt()); + + libvirtRestoreKbossBackupCommandWrapperSpy.restoreVolumes(Set.of(new Pair<>(backupDeltaTOMock, volumeObjectToMock1)), kvmStoragePool1, kvmStoragePoolManagerMock, + true, 1000); + + verify(qemuImgMock).create(any(), (QemuImgFile)any()); + } + + @Test + public void restoreVolumesTestNormalRestore() throws LibvirtException, QemuImgException { + doReturn(primaryDataStoreToMock).when(volumeObjectToMock1).getDataStore(); + doReturn(kvmStoragePool2).when(kvmStoragePoolManagerMock).getStoragePool(any(), any()); + doReturn("p2").when(kvmStoragePool2).getLocalPathFor(any()); + doReturn(qemuImgMock).when(libvirtRestoreKbossBackupCommandWrapperSpy).getQemuImg(anyInt()); + + libvirtRestoreKbossBackupCommandWrapperSpy.restoreVolumes(Set.of(new Pair<>(backupDeltaTOMock, volumeObjectToMock1)), kvmStoragePool1, kvmStoragePoolManagerMock, + false, 1000); + + verify(qemuImgMock).convert(any(), any()); + } + +} diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapperTest.java index cfcb2a2f972d..368f963a9a8b 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapperTest.java @@ -148,8 +148,8 @@ public void validateRevertVolumeToSnapshotReplaceSuccessfully() throws LibvirtEx Mockito.doReturn(volumeObjectToMock).when(snapshotObjectToSecondaryMock).getVolume(); Mockito.doReturn(pairStringSnapshotObjectToMock).when(libvirtRevertSnapshotCommandWrapperSpy).getSnapshot(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.doNothing().when(libvirtRevertSnapshotCommandWrapperSpy).replaceVolumeWithSnapshot(Mockito.any(), Mockito.any()); - libvirtRevertSnapshotCommandWrapperSpy.revertVolumeToSnapshot(kvmStoragePoolSecondaryMock, snapshotObjectToPrimaryMock, snapshotObjectToSecondaryMock, kvmStoragePoolPrimaryMock, resourceMock - ); + libvirtRevertSnapshotCommandWrapperSpy.revertVolumeToSnapshot(kvmStoragePoolSecondaryMock, snapshotObjectToPrimaryMock, snapshotObjectToSecondaryMock, kvmStoragePoolPrimaryMock, resourceMock, + false); } @Test (expected = CloudRuntimeException.class) @@ -157,8 +157,8 @@ public void validateRevertVolumeToSnapshotReplaceVolumeThrowsQemuImgException() Mockito.doReturn(volumeObjectToMock).when(snapshotObjectToSecondaryMock).getVolume(); Mockito.doReturn(pairStringSnapshotObjectToMock).when(libvirtRevertSnapshotCommandWrapperSpy).getSnapshot(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.doThrow(QemuImgException.class).when(libvirtRevertSnapshotCommandWrapperSpy).replaceVolumeWithSnapshot(Mockito.any(), Mockito.any()); - libvirtRevertSnapshotCommandWrapperSpy.revertVolumeToSnapshot(kvmStoragePoolSecondaryMock, snapshotObjectToPrimaryMock, snapshotObjectToSecondaryMock, kvmStoragePoolPrimaryMock, resourceMock - ); + libvirtRevertSnapshotCommandWrapperSpy.revertVolumeToSnapshot(kvmStoragePoolSecondaryMock, snapshotObjectToPrimaryMock, snapshotObjectToSecondaryMock, kvmStoragePoolPrimaryMock, resourceMock, + false); } @Test (expected = CloudRuntimeException.class) @@ -167,6 +167,6 @@ public void validateRevertVolumeToSnapshotReplaceVolumeThrowsLibvirtException() Mockito.doReturn(pairStringSnapshotObjectToMock).when(libvirtRevertSnapshotCommandWrapperSpy).getSnapshot(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.doThrow(LibvirtException.class).when(libvirtRevertSnapshotCommandWrapperSpy).replaceVolumeWithSnapshot(Mockito.any(), Mockito.any()); libvirtRevertSnapshotCommandWrapperSpy.revertVolumeToSnapshot(kvmStoragePoolSecondaryMock, snapshotObjectToPrimaryMock, snapshotObjectToSecondaryMock, kvmStoragePoolPrimaryMock, resourceMock - ); + , false); } } diff --git a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java index 502474e7c6b0..5b089ad37031 100644 --- a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java +++ b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java @@ -20,6 +20,11 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import java.io.File; import java.nio.file.Path; @@ -41,7 +46,9 @@ import org.junit.runner.RunWith; import org.libvirt.Connect; import org.libvirt.LibvirtException; +import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import com.cloud.utils.script.Script; @@ -432,8 +439,8 @@ public void addScriptOptionsFromMapAddsValidOptions() throws LibvirtException, Q QemuImg qemu = new QemuImg(0); qemu.addScriptOptionsFromMap(options, script); - Mockito.verify(script, Mockito.times(1)).add("-o"); - Mockito.verify(script, Mockito.times(1)).add("key1=value1,key2=value2"); + verify(script, Mockito.times(1)).add("-o"); + verify(script, Mockito.times(1)).add("key1=value1,key2=value2"); } @Test @@ -444,7 +451,7 @@ public void addScriptOptionsFromMapHandlesEmptyOptions() throws LibvirtException QemuImg qemu = new QemuImg(0); qemu.addScriptOptionsFromMap(options, script); - Mockito.verify(script, Mockito.never()).add(Mockito.anyString()); + verify(script, never()).add(Mockito.anyString()); } @Test @@ -454,7 +461,7 @@ public void addScriptOptionsFromMapHandlesNullOptions() throws LibvirtException, QemuImg qemu = new QemuImg(0); qemu.addScriptOptionsFromMap(null, script); - Mockito.verify(script, Mockito.never()).add(Mockito.anyString()); + verify(script, never()).add(Mockito.anyString()); } @Test @@ -466,8 +473,8 @@ public void addScriptOptionsFromMapHandlesTrailingComma() throws LibvirtExceptio QemuImg qemu = new QemuImg(0); qemu.addScriptOptionsFromMap(options, script); - Mockito.verify(script, Mockito.times(1)).add("-o"); - Mockito.verify(script, Mockito.times(1)).add("key1=value1"); + verify(script, Mockito.times(1)).add("-o"); + verify(script, Mockito.times(1)).add("key1=value1"); } @Test @@ -536,8 +543,8 @@ public void addScriptResizeOptionsFromMapAddsPreallocationOption() throws Libvir QemuImg qemuImg = new QemuImg(0); qemuImg.addScriptResizeOptionsFromMap(options, script); - Mockito.verify(script, Mockito.times(1)).add("--preallocation=metadata"); - Mockito.verify(script, Mockito.never()).add("-o"); + verify(script, Mockito.times(1)).add("--preallocation=metadata"); + verify(script, never()).add("-o"); assertTrue(options.isEmpty()); } @@ -549,7 +556,7 @@ public void addScriptResizeOptionsFromMapHandlesEmptyOptions() throws LibvirtExc QemuImg qemuImg = new QemuImg(0); qemuImg.addScriptResizeOptionsFromMap(options, script); - Mockito.verify(script, Mockito.never()).add(Mockito.anyString()); + verify(script, never()).add(Mockito.anyString()); } @Test @@ -559,7 +566,7 @@ public void addScriptResizeOptionsFromMapHandlesNullOptions() throws LibvirtExce QemuImg qemuImg = new QemuImg(0); qemuImg.addScriptResizeOptionsFromMap(null, script); - Mockito.verify(script, Mockito.never()).add(Mockito.anyString()); + verify(script, never()).add(Mockito.anyString()); } @Test @@ -572,9 +579,92 @@ public void addScriptResizeOptionsFromMapHandlesMixedOptions() throws LibvirtExc QemuImg qemuImg = new QemuImg(0); qemuImg.addScriptResizeOptionsFromMap(options, script); - Mockito.verify(script, Mockito.times(1)).add("--preallocation=full"); - Mockito.verify(script, Mockito.times(1)).add("-o"); - Mockito.verify(script, Mockito.times(1)).add("key=value"); + verify(script, Mockito.times(1)).add("--preallocation=full"); + verify(script, Mockito.times(1)).add("-o"); + verify(script, Mockito.times(1)).add("key=value"); assertFalse(options.containsKey(QemuImg.PREALLOCATION)); } + + @Spy + private QemuImg qemuImgSpy; + + @Mock + private Script scriptMock; + + @Mock + private QemuImgFile qemuImgFileMock1; + + @Mock + private QemuImgFile qemuImgFileMock2; + + @Test(expected = QemuImgException.class) + public void commitTestNullFileThrows() throws Exception { + qemuImgSpy.commit(null, null, false); + } + + @Test + public void commitTestBasicCommand() throws Exception { + doReturn(scriptMock).when(qemuImgSpy).createScript(any(), anyLong()); + doReturn(null).when(scriptMock).execute(); + doReturn(null).when(qemuImgFileMock1).getFormat(); + doReturn("file.qcow2").when(qemuImgFileMock1).getFileName(); + + qemuImgSpy.commit(qemuImgFileMock1, null, false); + + verify(scriptMock).add("commit"); + verify(scriptMock).add("file.qcow2"); + } + + @Test + public void commitTestWithFormat() throws Exception { + doReturn(scriptMock).when(qemuImgSpy).createScript(any(), anyLong()); + doReturn(null).when(scriptMock).execute(); + doReturn(PhysicalDiskFormat.QCOW2).when(qemuImgFileMock1).getFormat(); + doReturn("file.qcow2").when(qemuImgFileMock1).getFileName(); + + qemuImgSpy.commit(qemuImgFileMock1, null, false); + + verify(scriptMock).add("-f"); + verify(scriptMock).add("qcow2"); + } + + @Test + public void commitTestWithBase() throws Exception { + doReturn(scriptMock).when(qemuImgSpy).createScript(any(), anyLong()); + doReturn(null).when(scriptMock).execute(); + + doReturn(null).when(qemuImgFileMock1).getFormat(); + doReturn("file.qcow2").when(qemuImgFileMock1).getFileName(); + doReturn("base.qcow2").when(qemuImgFileMock2).getFileName(); + + qemuImgSpy.commit(qemuImgFileMock1, qemuImgFileMock2, true); + + verify(scriptMock).add("-b"); + verify(scriptMock).add("base.qcow2"); + verify(scriptMock, never()).add("-d"); + } + + @Test + public void commitTestSkipEmptyingFiles() throws Exception { + doReturn(scriptMock).when(qemuImgSpy).createScript(any(), anyLong()); + doReturn(null).when(scriptMock).execute(); + + doReturn(null).when(qemuImgFileMock1).getFormat(); + doReturn("file.qcow2").when(qemuImgFileMock1).getFileName(); + + qemuImgSpy.commit(qemuImgFileMock1, null, true); + + verify(scriptMock).add("-d"); + } + + @Test(expected = QemuImgException.class) + public void commitTestExecutionFails() throws Exception { + doReturn(scriptMock).when(qemuImgSpy).createScript(any(), anyLong()); + doReturn("error").when(scriptMock).execute(); + + doReturn(null).when(qemuImgFileMock1).getFormat(); + doReturn("file.qcow2").when(qemuImgFileMock1).getFileName(); + + qemuImgSpy.commit(qemuImgFileMock1, null, false); + } } diff --git a/plugins/hypervisors/simulator/src/main/java/com/cloud/simulator/SimulatorGuru.java b/plugins/hypervisors/simulator/src/main/java/com/cloud/simulator/SimulatorGuru.java index aae28c428b8d..a361018b0d8a 100644 --- a/plugins/hypervisors/simulator/src/main/java/com/cloud/simulator/SimulatorGuru.java +++ b/plugins/hypervisors/simulator/src/main/java/com/cloud/simulator/SimulatorGuru.java @@ -38,6 +38,7 @@ import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.dao.NicDao; +import org.apache.cloudstack.backup.BackupProvider; public class SimulatorGuru extends HypervisorGuruBase implements HypervisorGuru { @Inject @@ -74,7 +75,7 @@ public VirtualMachineTO implement(VirtualMachineProfile vm) { @Override public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, - String vmInternalName, Backup backup) { + String vmInternalName, Backup backup, BackupProvider backupProvider) { VMInstanceVO vm = instanceDao.findVMByInstanceNameIncludingRemoved(vmInternalName); if (vm.getRemoved() != null) { vm.setState(VirtualMachine.State.Stopped); @@ -92,7 +93,7 @@ public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, } @Override - public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, VirtualMachine vm, long poolId, Backup backup) { + public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, VirtualMachine vm, long poolId, Backup backup, BackupProvider backupProvider) { VMInstanceVO targetVM = instanceDao.findVMByInstanceNameIncludingRemoved(vm.getName()); List vmVolumes = volumeDao.findByInstance(targetVM.getId()); diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java index 287601d47d6b..f41bf3e76f84 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java @@ -41,6 +41,7 @@ import com.vmware.vim25.VirtualMachinePowerState; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupProvider; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; @@ -1162,7 +1163,8 @@ private ManagedObjectReference getDestStoreMor(VirtualMachineMO vmMo) throws Exc } @Override - public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, String vmInternalName, Backup backup) throws Exception { + public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, String vmInternalName, Backup backup, + BackupProvider backupProvider) throws Exception { logger.debug(String.format("Trying to import VM [vmInternalName: %s] from Backup [%s].", vmInternalName, ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "type"))); DatacenterMO dcMo = getDatacenterMO(zoneId); @@ -1191,7 +1193,8 @@ public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, } @Override - public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, VirtualMachine vm, long poolId, Backup backup) + public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, VirtualMachine vm, long poolId, Backup backup, + BackupProvider backupProvider) throws Exception { DatacenterMO dcMo = getDatacenterMO(zoneId); VirtualMachineMO vmRestored = findVM(dcMo, location); 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 109fe71a8d75..8297c74864c3 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 @@ -1610,7 +1610,7 @@ public DiskAttachment attachInstanceDisk(final String vmUuid, final DiskAttachme context.setEventDetails("Volume Id: " + volumeVO.getUuid() + " VmId: " + vmVo.getUuid()); context.setEventResourceType(ApiCommandResourceType.Volume); context.setEventResourceId(volumeVO.getId()); - Volume volume = volumeApiService.attachVolumeToVM(vmVo.getId(), volumeVO.getId(), deviceId, true); + Volume volume = volumeApiService.attachVolumeToVM(vmVo.getId(), volumeVO.getId(), deviceId, true, false); processInstanceRestoreConfigIfNeeded(vmVo, volume); VolumeJoinVO attachedVolumeVO = volumeJoinDao.findById(volume.getId()); return VolumeJoinVOToDiskConverter.toDiskAttachment(attachedVolumeVO, this::getVolumePhysicalSize); @@ -1696,7 +1696,7 @@ public void deleteDisk(String uuid) { context.setEventDetails("Volume Id: " + vo.getUuid()); context.setEventResourceType(ApiCommandResourceType.Volume); context.setEventResourceId(vo.getId()); - volumeApiService.destroyVolume(vo.getId(), CallContext.current().getCallingAccount(), true, false); + volumeApiService.destroyVolume(vo.getId(), CallContext.current().getCallingAccount(), true, false, null); } @ApiAccess(command = UpdateVolumeCmd.class) diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java index f3f1f45584f4..b105e7306e5f 100644 --- a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java @@ -970,7 +970,7 @@ public void testDeleteDisk_Found_DeletesVolume() { serverAdapter.deleteDisk("vol-uuid"); - verify(volumeApiService).destroyVolume(10L, account, true, false); + verify(volumeApiService).destroyVolume(10L, account, true, false, null); } diff --git a/plugins/pom.xml b/plugins/pom.xml index e1e1ba08889b..bcd1ca59f03b 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -63,6 +63,7 @@ backup/dummy backup/networker backup/nas + backup/kboss ca/root-ca diff --git a/plugins/storage/image/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackImageStoreDriverImpl.java b/plugins/storage/image/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackImageStoreDriverImpl.java index 179d68efea7f..f4c310be40f4 100644 --- a/plugins/storage/image/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackImageStoreDriverImpl.java +++ b/plugins/storage/image/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackImageStoreDriverImpl.java @@ -66,15 +66,15 @@ public DataStoreTO getStoreTO(DataStore store) { } private String createObjectNameForExtractUrl(String installPath, ImageFormat format, DataObject dataObject) { - String objectNameInUrl = dataObject.getName(); + String objectNameInUrl; try { - objectNameInUrl = cleanObjectName(objectNameInUrl); + objectNameInUrl = cleanObjectName(dataObject.getName()); } catch (Exception e) { objectNameInUrl = UUID.randomUUID().toString(); } if (format != null) { - if (dataObject.getTO() != null + if (dataObject != null && dataObject.getTO() != null && DataObjectType.VOLUME.equals(dataObject.getTO().getObjectType()) && HypervisorType.KVM.equals(dataObject.getTO().getHypervisorType())) { // Fix: The format of KVM volumes on image store is qcow2 diff --git a/plugins/storage/sharedfs/storagevm/src/main/java/org/apache/cloudstack/storage/sharedfs/lifecycle/StorageVmSharedFSLifeCycle.java b/plugins/storage/sharedfs/storagevm/src/main/java/org/apache/cloudstack/storage/sharedfs/lifecycle/StorageVmSharedFSLifeCycle.java index f47a35ced44a..ed799e9030be 100644 --- a/plugins/storage/sharedfs/storagevm/src/main/java/org/apache/cloudstack/storage/sharedfs/lifecycle/StorageVmSharedFSLifeCycle.java +++ b/plugins/storage/sharedfs/storagevm/src/main/java/org/apache/cloudstack/storage/sharedfs/lifecycle/StorageVmSharedFSLifeCycle.java @@ -298,7 +298,7 @@ public boolean deleteSharedFS(SharedFS sharedFS) { expunge = true; forceExpunge = true; } - volumeApiService.destroyVolume(volume.getId(), CallContext.current().getCallingAccount(), expunge, forceExpunge); + volumeApiService.destroyVolume(volume.getId(), CallContext.current().getCallingAccount(), expunge, forceExpunge, null); return true; } diff --git a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java index a12836cdb965..d002cd1caa4f 100644 --- a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java @@ -29,6 +29,7 @@ import com.cloud.agent.api.to.DiskTO; import com.cloud.ha.HighAvailabilityManager; import com.cloud.storage.VolumeVO; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; @@ -146,6 +147,9 @@ public Map getCapabilities() { @Inject private AgentManager agentMgr; + @Inject + private InternalBackupService internalBackupService; + @Override public DataTO getTO(DataObject data) { return null; @@ -253,12 +257,14 @@ private boolean commandCanBypassHostMaintenance(DataObject data) { @Override public void deleteAsync(DataStore dataStore, DataObject data, AsyncCompletionCallback callback) { - DeleteCommand cmd = new DeleteCommand(data.getTO()); + DataTO dataTO = data.getTO(); + DeleteCommand cmd = new DeleteCommand(dataTO); cmd.setBypassHostMaintenance(commandCanBypassHostMaintenance(data)); CommandResult result = new CommandResult(); try { EndPoint ep; if (data.getType() == DataObjectType.VOLUME) { + internalBackupService.configureChainInfo(dataTO, cmd); ep = epSelector.select(data, StorageAction.DELETEVOLUME); } else if (data.getType() == DataObjectType.SNAPSHOT) { ep = epSelector.select(data, StorageAction.DELETESNAPSHOT); @@ -431,7 +437,11 @@ public void revertSnapshot(SnapshotInfo snapshot, SnapshotInfo snapshotOnPrimary if (snapshotOnPrimaryStore != null) { dataOnPrimaryStorage = (SnapshotObjectTO)snapshotOnPrimaryStore.getTO(); } - RevertSnapshotCommand cmd = new RevertSnapshotCommand((SnapshotObjectTO)snapshot.getTO(), dataOnPrimaryStorage); + + SnapshotObjectTO snapshotObjectTO = (SnapshotObjectTO)snapshot.getTO(); + + RevertSnapshotCommand cmd = new RevertSnapshotCommand(snapshotObjectTO, dataOnPrimaryStorage); + internalBackupService.configureChainInfo(snapshotObjectTO.getVolume(), cmd); CommandResult result = new CommandResult(); try { diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index ab5f572021cd..2510dc0a88b4 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -5162,6 +5162,7 @@ public BackupScheduleResponse createBackupScheduleResponse(BackupSchedule schedu response.setSchedule(schedule.getSchedule()); response.setTimezone(schedule.getTimezone()); response.setMaxBackups(schedule.getMaxBackups()); + response.setIsolated(schedule.isIsolated()); if (schedule.getQuiesceVM() != null) { response.setQuiesceVM(schedule.getQuiesceVM()); diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 616e75bfc361..d11913262818 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -43,8 +43,9 @@ import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.acl.dao.RoleDao; -import com.cloud.dc.Pod; +import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.Pod; import com.cloud.dc.dao.HostPodDao; import com.cloud.org.Cluster; import com.cloud.server.ManagementService; @@ -96,6 +97,7 @@ import org.apache.cloudstack.api.command.user.account.ListProjectAccountsCmd; import org.apache.cloudstack.api.command.user.address.ListQuarantinedIpsCmd; import org.apache.cloudstack.api.command.user.affinitygroup.ListAffinityGroupsCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupServiceJobsCmd; import org.apache.cloudstack.api.command.user.bucket.ListBucketsCmd; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.iso.ListIsosCmd; @@ -118,6 +120,7 @@ import org.apache.cloudstack.api.command.user.zone.ListZonesCmd; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.AsyncJobResponse; +import org.apache.cloudstack.api.response.BackupServiceJobResponse; import org.apache.cloudstack.api.response.BucketResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.DetailOptionsResponse; @@ -155,7 +158,12 @@ import org.apache.cloudstack.api.response.VirtualMachineResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.InternalBackupServiceJobType; +import org.apache.cloudstack.backup.InternalBackupServiceJobVO; import org.apache.cloudstack.backup.BackupOfferingVO; +import org.apache.cloudstack.backup.BackupVO; +import org.apache.cloudstack.backup.dao.InternalBackupServiceJobDao; +import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -657,6 +665,12 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject RoleDao roleDao; + @Inject + private InternalBackupServiceJobDao internalBackupServiceJobDao; + + @Inject + private BackupDao backupDao; + /* * (non-Javadoc) * @@ -5500,6 +5514,13 @@ private void fillVMOrTemplateDetailOptions(final Map> optio options.put(VmDetailConstants.GUEST_CPU_MODEL, Collections.emptyList()); options.put(VmDetailConstants.KVM_GUEST_OS_MACHINE_TYPE, Collections.emptyList()); options.put(VmDetailConstants.KVM_SKIP_FORCE_DISK_CONTROLLER, Arrays.asList("true", "false")); + options.put(VmDetailConstants.VALIDATION_COMMAND, Collections.emptyList()); + options.put(VmDetailConstants.VALIDATION_COMMAND_ARGUMENTS, Collections.emptyList()); + options.put(VmDetailConstants.VALIDATION_COMMAND_EXPECTED_RESULT, Collections.emptyList()); + options.put(VmDetailConstants.VALIDATION_COMMAND_TIMEOUT, Collections.emptyList()); + options.put(VmDetailConstants.VALIDATION_BOOT_TIMEOUT, Collections.emptyList()); + options.put(VmDetailConstants.VALIDATION_SCREENSHOT_WAIT, Collections.emptyList()); + } if (HypervisorType.VMware.equals(hypervisorType)) { @@ -6354,6 +6375,34 @@ private List searchForBucketsInternal(ListBucketsCmd cmd) { return bucketDao.searchByIds(bktIds); } + @Override + public ListResponse listBackupServiceJobs(ListBackupServiceJobsCmd cmd) { + ListResponse responses = new ListResponse<>(); + Pair, Integer> result = listBackupServiceJobsInternal(cmd); + List compressionJobResponses = new ArrayList<>(); + + for (InternalBackupServiceJobVO jobVO : result.first()) { + BackupVO backup = backupDao.findByIdIncludingRemoved(jobVO.getBackupId()); + DataCenterVO zone = dataCenterDao.findByIdIncludingRemoved(jobVO.getZoneId()); + + BackupServiceJobResponse response = new BackupServiceJobResponse(jobVO.getId(), backup.getUuid(), zone.getUuid(), jobVO.getAttempts(), + jobVO.getType().toString(), jobVO.getStartTime(), jobVO.getScheduledStartTime(), jobVO.getRemoved()); + + if (jobVO.getHostId() != null) { + response.setHostId(hostDao.findByIdIncludingRemoved(jobVO.getHostId()).getUuid()); + } + compressionJobResponses.add(response); + } + + responses.setResponses(compressionJobResponses, result.second()); + return responses; + } + + private Pair, Integer> listBackupServiceJobsInternal(ListBackupServiceJobsCmd cmd) { + return internalBackupServiceJobDao.searchAndCountForListApi(cmd.getId(), cmd.getBackupId(), cmd.getHostId(), cmd.getZoneId(), + InternalBackupServiceJobType.valueOf(cmd.getType()), cmd.getExecuting(), cmd.getScheduled(), cmd.getStartIndex(), cmd.getPageSizeVal()); + } + @Override public String getConfigComponentName() { return QueryService.class.getSimpleName(); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 3f7f459c2a1b..ebc2326a72d5 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -46,6 +46,7 @@ import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VnfNicResponse; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.extension.ExtensionHelper; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -146,12 +147,15 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation VmDetailSearch; private final SearchBuilder activeVmByIsoSearch; private final SearchBuilder leaseExpiredInstanceSearch; private final SearchBuilder remainingLeaseInDaysSearch; + protected UserVmJoinDaoImpl() { VmDetailSearch = createSearchBuilder(); @@ -294,6 +298,9 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us if (details.contains(VMDetails.all) || details.contains(VMDetails.backoff)) { userVmResponse.setBackupOfferingId(userVm.getBackupOfferingUuid()); userVmResponse.setBackupOfferingName(userVm.getBackupOfferingName()); + if (userVm.getBackupOfferingUuid() != null) { + userVmResponse.setBackupProvider(backupOfferingDao.findByUuidIncludingRemoved(userVm.getBackupOfferingUuid()).getProvider()); + } } if (details.contains(VMDetails.all) || details.contains(VMDetails.servoff) || details.contains(VMDetails.stats)) { userVmResponse.setCpuNumber(userVm.getCpu()); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index d9e2bd623810..d07641eacc8a 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -21,7 +21,6 @@ import java.util.Arrays; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -252,7 +251,7 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy protected Gson jsonParser = new GsonBuilder().setVersion(1.3).create(); - protected Set availableVmStateOnAssignProxy = new HashSet<>(Arrays.asList(State.Starting, State.Running, State.Stopping, State.Migrating)); + protected Set availableVmStateOnAssignProxy = Set.of(State.Starting, State.Running, State.Stopping, State.Migrating, State.BackingUp, State.BackupError); @Inject private KeystoreDao _ksDao; @@ -353,9 +352,7 @@ public ConsoleProxyVO doAssignProxy(long dataCenterId, VMInstanceVO vm) { ConsoleProxyVO proxy = null; if (!availableVmStateOnAssignProxy.contains(vm.getState())) { - if (logger.isInfoEnabled()) { - logger.info(String.format("Detected that %s is not currently in \"Starting\", \"Running\", \"Stopping\" or \"Migrating\" state, it will fail the proxy assignment.", vm.toString())); - } + logger.info("Detected that {} is not currently in any of the following states: {}. Therefore, it is not possible to assign a console to the VM.", vm, availableVmStateOnAssignProxy); return null; } diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java index c7de3d472e3f..943d1e73c4b5 100644 --- a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java +++ b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java @@ -54,6 +54,7 @@ import com.cloud.vm.dao.UserVmDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupProvider; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -449,13 +450,13 @@ public Map getClusterSettings(long vmId) { @Override public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, - String vmInternalName, Backup backup) throws Exception { + String vmInternalName, Backup backup, BackupProvider backupProvider) throws Exception { return null; } @Override public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, - VirtualMachine vm, long poolId, Backup backup) throws Exception { + VirtualMachine vm, long poolId, Backup backup, BackupProvider backupProvider) throws Exception { return false; } diff --git a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java index 6154522af4ae..4aa67c5835f0 100644 --- a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java +++ b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java @@ -32,6 +32,7 @@ import com.cloud.storage.GuestOSHypervisorVO; import com.cloud.storage.GuestOSVO; import com.cloud.storage.Volume; +import com.cloud.storage.VolumeApiService; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.GuestOSHypervisorDao; @@ -44,6 +45,8 @@ import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManagerImpl; +import org.apache.cloudstack.backup.BackupProvider; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.storage.command.CopyCommand; import org.apache.cloudstack.storage.command.StorageSubSystemCommand; @@ -71,6 +74,9 @@ public class KVMGuru extends HypervisorGuruBase implements HypervisorGuru { @Inject HypervisorCapabilitiesDao _hypervisorCapabilitiesDao; + @Inject + private VolumeApiService volumeApiService; + @Override public HypervisorType getHypervisorType() { @@ -349,7 +355,8 @@ public Map getClusterSettings(long vmId) { } @Override - public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, String vmInternalName, Backup backup) { + public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, String vmInternalName, Backup backup, + BackupProvider backupProvider) { logger.debug(String.format("Trying to import VM [vmInternalName: %s] from Backup [%s].", vmInternalName, ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "type"))); @@ -357,6 +364,9 @@ public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, if (vm == null) { throw new CloudRuntimeException("Cannot find VM: " + vmInternalName); } + if (backupProvider.getName().equals(BackupManagerImpl.KBOSS_BACKUP_PROVIDER)) { + return vm; + } try { if (vm.getRemoved() == null) { vm.setState(VirtualMachine.State.Stopped); @@ -384,11 +394,16 @@ public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, return vm; } - @Override public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, VirtualMachine vm, long poolId, Backup backup) { + @Override + public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, VirtualMachine vm, long poolId, Backup backup, + BackupProvider backupProvider) { VMInstanceVO targetVM = _instanceDao.findVMByInstanceNameIncludingRemoved(vm.getName()); List vmVolumes = _volumeDao.findByInstance(targetVM.getId()); VolumeVO restoredVolume = _volumeDao.findByUuid(location); + if (backupProvider.getName().equals(BackupManagerImpl.KBOSS_BACKUP_PROVIDER)) { + return true; + } if (restoredVolume != null) { try { _volumeDao.attachVolume(restoredVolume.getId(), vm.getId(), getNextAvailableDeviceId(vmVolumes)); @@ -405,6 +420,6 @@ public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, throw new RuntimeException("Unable to attach volume " + restoredVolume.getName() + " to VM" + vm.getName() + " due to : " + e.getMessage()); } } - return false; + return false; } } diff --git a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java index f860bfc90280..1bd642ff7edc 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java @@ -2026,7 +2026,7 @@ public void checkAutoScaleVmGroupName(String groupName) { private UserVmVO startNewVM(long vmId) { try { CallContext.current().setEventDetails("Instance ID: " + vmId); - return userVmMgr.startVirtualMachine(vmId, null, new HashMap<>(), null).first(); + return userVmMgr.startVirtualMachine(vmId, null, new HashMap<>(), null, false).first(); } catch (final ResourceUnavailableException ex) { logger.warn("Exception: ", ex); throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, ex.getMessage()); diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 27facb304eba..bc3abd30d880 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -1742,8 +1742,8 @@ public List getResourceLimitStorageTags(DiskOffering diskOffering) { } @Override - public List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering) { - if (Boolean.FALSE.equals(display)) { + public List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering, Boolean enforceResourceLimitOnDisplayFalse) { + if (Boolean.FALSE.equals(display) && Boolean.FALSE.equals(enforceResourceLimitOnDisplayFalse)) { return new ArrayList<>(); } List tags = getResourceLimitStorageTags(diskOffering); @@ -1757,7 +1757,7 @@ public List getResourceLimitStorageTagsForResourceCountOperation(Boolean @Override public void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException { - List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering); + List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -1773,7 +1773,7 @@ public void checkVolumeResourceLimit(Account owner, Boolean display, Long size, @Override public void checkPrimaryStorageResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException { - List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering); + List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -1790,8 +1790,8 @@ public void checkVolumeResourceLimitForDiskOfferingChange(Account owner, Boolean return; } - List currentTags = getResourceLimitStorageTagsForResourceCountOperation(true, currentOffering); - List tagsAfterUpdate = getResourceLimitStorageTagsForResourceCountOperation(true, newOffering); + List currentTags = getResourceLimitStorageTagsForResourceCountOperation(true, currentOffering, null); + List tagsAfterUpdate = getResourceLimitStorageTagsForResourceCountOperation(true, newOffering, null); if (currentTags.isEmpty() && tagsAfterUpdate.isEmpty()) { return; } @@ -1811,7 +1811,7 @@ public void incrementVolumeResourceCount(long accountId, Boolean display, Long s Transaction.execute(new TransactionCallbackNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { - List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering); + List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -1827,11 +1827,11 @@ public void doInTransactionWithoutResult(TransactionStatus status) { @DB @Override - public void decrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering) { + public void decrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering, Boolean enforceResourceLimitOnDisplayFalse) { Transaction.execute(new TransactionCallbackNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { - List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering); + List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering, enforceResourceLimitOnDisplayFalse); if (CollectionUtils.isEmpty(tags)) { return; } @@ -1865,11 +1865,11 @@ private Ternary, Set, Set> getResourceLimitHostTagsF Boolean display, ServiceOffering currentOffering, ServiceOffering newOffering, VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate ) { - Set currentOfferingTags = new HashSet<>(getResourceLimitHostTagsForResourceCountOperation(display, currentOffering, currentTemplate)); + Set currentOfferingTags = new HashSet<>(getResourceLimitHostTagsForResourceCountOperation(display, currentOffering, currentTemplate, null)); if (currentOffering.getId() == newOffering.getId() && currentTemplate.getId() == newTemplate.getId()) { return new Ternary<>(currentOfferingTags, new HashSet<>(), new HashSet<>()); } - Set newOfferingTags = new HashSet<>(getResourceLimitHostTagsForResourceCountOperation(display, newOffering, newTemplate)); + Set newOfferingTags = new HashSet<>(getResourceLimitHostTagsForResourceCountOperation(display, newOffering, newTemplate, null)); if (currentOfferingTags.isEmpty() && newOfferingTags.isEmpty()) { return null; @@ -1941,11 +1941,11 @@ private void adjustResourceCount(Long newValue, Long currentValue, Resource.Reso private Ternary, Set, Set> getResourceLimitStorageTagsForDiskOfferingChange( Boolean display, DiskOffering currentOffering, DiskOffering newOffering ) { - Set currentOfferingTags = new HashSet<>(getResourceLimitStorageTagsForResourceCountOperation(display, currentOffering)); + Set currentOfferingTags = new HashSet<>(getResourceLimitStorageTagsForResourceCountOperation(display, currentOffering, null)); if (newOffering == null || currentOffering.getId() == newOffering.getId()) { return new Ternary<>(currentOfferingTags, new HashSet<>(), new HashSet<>()); } - Set newOfferingTags = new HashSet<>(getResourceLimitStorageTagsForResourceCountOperation(display, newOffering)); + Set newOfferingTags = new HashSet<>(getResourceLimitStorageTagsForResourceCountOperation(display, newOffering, null)); if (currentOfferingTags.isEmpty() && newOfferingTags.isEmpty()) { return null; } @@ -1992,7 +1992,7 @@ public void incrementVolumePrimaryStorageResourceCount(long accountId, Boolean d if (size == null) { return; } - List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering); + List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2006,7 +2006,7 @@ public void decrementVolumePrimaryStorageResourceCount(long accountId, Boolean d if (size == null) { return; } - List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering); + List tags = getResourceLimitStorageTagsForResourceCountOperation(display, diskOffering, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2015,8 +2015,9 @@ public void decrementVolumePrimaryStorageResourceCount(long accountId, Boolean d } } - protected List getResourceLimitHostTagsForResourceCountOperation(Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template) { - if (Boolean.FALSE.equals(display)) { + protected List getResourceLimitHostTagsForResourceCountOperation(Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, + Boolean enforceResourceLimitOnDisplayFalse) { + if (Boolean.FALSE.equals(display) && Boolean.FALSE.equals(enforceResourceLimitOnDisplayFalse)) { return new ArrayList<>(); } List tags = getResourceLimitHostTags(serviceOffering, template); @@ -2030,7 +2031,7 @@ protected List getResourceLimitHostTagsForResourceCountOperation(Boolean @Override public void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); + List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2053,11 +2054,12 @@ public void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering } @Override - public void incrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template) { + public void incrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, + Boolean countDisplayFalseInResourceLimit) { Transaction.execute(new TransactionCallbackNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); + List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template, countDisplayFalseInResourceLimit); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2076,11 +2078,11 @@ public void doInTransactionWithoutResult(TransactionStatus status) { @Override public void decrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, - VirtualMachineTemplate template) { + VirtualMachineTemplate template, Boolean enforceResourceLimitOnDisplayFalse) { Transaction.execute(new TransactionCallbackNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); + List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template, enforceResourceLimitOnDisplayFalse); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2117,8 +2119,8 @@ private void checkVmResourceLimitsForServiceOfferingAndTemplateChange(Account ow Long currentMemory, Long newMemory, ServiceOffering currentOffering, ServiceOffering newOffering, VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate, List reservations ) throws ResourceAllocationException { - List currentTags = getResourceLimitHostTagsForResourceCountOperation(true, currentOffering, currentTemplate); - List tagsAfterUpdate = getResourceLimitHostTagsForResourceCountOperation(true, newOffering, newTemplate); + List currentTags = getResourceLimitHostTagsForResourceCountOperation(true, currentOffering, currentTemplate, null); + List tagsAfterUpdate = getResourceLimitHostTagsForResourceCountOperation(true, newOffering, newTemplate, null); if (currentTags.isEmpty() && tagsAfterUpdate.isEmpty()) { return; } @@ -2157,7 +2159,7 @@ private void checkVmResourceLimitsForServiceOfferingAndTemplateChange(Account ow @Override public void incrementVmCpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu) { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); + List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2171,7 +2173,7 @@ public void incrementVmCpuResourceCount(long accountId, Boolean display, Service @Override public void decrementVmCpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu) { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); + List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2185,7 +2187,7 @@ public void decrementVmCpuResourceCount(long accountId, Boolean display, Service @Override public void incrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory) { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); + List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2199,7 +2201,7 @@ public void incrementVmMemoryResourceCount(long accountId, Boolean display, Serv @Override public void decrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory) { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); + List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2213,7 +2215,7 @@ public void decrementVmMemoryResourceCount(long accountId, Boolean display, Serv @Override public void incrementVmGpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long gpu) { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); + List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template, null); if (CollectionUtils.isEmpty(tags)) { return; } @@ -2227,7 +2229,7 @@ public void incrementVmGpuResourceCount(long accountId, Boolean display, Service @Override public void decrementVmGpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long gpu) { - List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template); + List tags = getResourceLimitHostTagsForResourceCountOperation(display, serviceOffering, template, null); if (CollectionUtils.isEmpty(tags)) { return; } diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index 9cb5155753c6..d9b9bc629a6b 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -2190,7 +2190,7 @@ public void cleanupStorage(boolean recurring) { volService.destroyVolume(volume.getId()); // decrement volume resource count _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), volume.isDisplayVolume(), - null, _diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId())); + null, _diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId()), null); // expunge volume from secondary if volume is on image store VolumeInfo volOnSecondary = volFactory.getVolume(volume.getId(), DataStoreRole.Image); if (volOnSecondary != null) { @@ -4627,7 +4627,8 @@ public ConfigKey[] getConfigKeys() { AllowVolumeReSizeBeyondAllocation, StoragePoolHostConnectWorkers, ObjectStorageCapacityThreshold, - COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES + COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES, + AgentMaxDataMigrationWaitTime }; } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 39686b4d3843..1535218790f0 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -54,6 +54,8 @@ import org.apache.cloudstack.api.response.GetUploadParamsResponse; import org.apache.cloudstack.backup.Backup; import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupManagerImpl; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.direct.download.DirectDownloadHelper; @@ -379,6 +381,11 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic private VMSnapshotDetailsDao vmSnapshotDetailsDao; @Inject private KMSManager kmsManager; + @Inject + private InternalBackupService internalBackupService; + + @Inject + private BackupManager backupManager; public static final String KVM_FILE_BASED_STORAGE_SNAPSHOT = "kvmFileBasedStorageSnapshot"; @@ -974,7 +981,7 @@ public VolumeVO allocVolume(long ownerId, Long zoneId, Long diskOfferingId, Long Storage.ProvisioningType provisioningType = diskOffering.getProvisioningType(); - List tags = _resourceLimitMgr.getResourceLimitStorageTagsForResourceCountOperation(displayVolume, diskOffering); + List tags = _resourceLimitMgr.getResourceLimitStorageTagsForResourceCountOperation(displayVolume, diskOffering, null); if (tags.size() == 1 && tags.get(0) == null) { tags = new ArrayList<>(); } @@ -1154,7 +1161,7 @@ public VolumeVO createVolume(long volumeId, Long vmId, Long snapshotId, Long sto // if VM Id is provided, attach the volume to the VM if (vmId != null) { try { - attachVolumeToVM(vmId, volume.getId(), volume.getDeviceId(), false); + attachVolumeToVM(vmId, volume.getId(), volume.getDeviceId(), false, false); } catch (Exception ex) { StringBuilder message = new StringBuilder("Volume: "); message.append(volume.getUuid()); @@ -1182,7 +1189,7 @@ public VolumeVO createVolume(long volumeId, Long vmId, Long snapshotId, Long sto 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(), display, - volume.getSize(), _diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId())); + volume.getSize(), _diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId()), null); } } } @@ -1737,7 +1744,7 @@ private VolumeVO orchestrateResizeVolume(long volumeId, long currentSize, long n * Otherwise, after the removal in the database, we will try to remove the volume from both primary and secondary storage. */ public boolean deleteVolume(long volumeId, Account caller) throws ConcurrentOperationException { - Volume volume = destroyVolume(volumeId, caller, true, true); + Volume volume = destroyVolume(volumeId, caller, true, true, null); return (volume != null); } @@ -1915,7 +1922,7 @@ public void validateDestroyVolume(Volume volume, Account caller, boolean expunge @Override @ActionEvent(eventType = EventTypes.EVENT_VOLUME_DESTROY, eventDescription = "destroying a volume") - public Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge) { + public Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge, Boolean enforceResourceLimitOnDisplayFalse) { VolumeVO volume = retrieveAndValidateVolume(volumeId, caller); validateDestroyVolume(volume, caller, expunge, forceExpunge); @@ -1934,7 +1941,7 @@ public Volume destroyVolume(long volumeId, Account caller, boolean expunge, bool return null; } _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), volume.isDisplay(), - volume.getSize(), _diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId())); + volume.getSize(), _diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId()), enforceResourceLimitOnDisplayFalse); return volume; } if (!deleteVolumeFromStorage(volume, caller)) { @@ -2647,7 +2654,7 @@ private void validateVolumeResizeWithSize(VolumeVO volume, long currentSize, Lon @Override @ActionEvent(eventType = EventTypes.EVENT_VOLUME_ATTACH, eventDescription = "attaching volume", async = true) public Volume attachVolumeToVM(AttachVolumeCmd command) { - return attachVolumeToVM(command.getVirtualMachineId(), command.getId(), command.getDeviceId(), false); + return attachVolumeToVM(command.getVirtualMachineId(), command.getId(), command.getDeviceId(), false, false); } protected VolumeVO getVmExistingVolumeForVolumeAttach(UserVmVO vm, VolumeInfo volumeToAttach) { @@ -2736,7 +2743,7 @@ protected VolumeInfo createVolumeOnPrimaryForAttachIfNeeded(final VolumeInfo vol throw new InvalidParameterValueException("Cannot attach uploaded volume, this operation is unsupported on storage pool type " + destPrimaryStorage.getPoolType()); } newVolumeOnPrimaryStorage = _volumeMgr.createVolumeOnPrimaryStorage(vm, volumeToAttach, - vm.getHypervisorType(), destPrimaryStorage); + vm.getHypervisorType(), destPrimaryStorage, null, null); } catch (NoTransitionException e) { logger.debug("Failed to create volume on primary storage", e); throw new CloudRuntimeException("Failed to create volume on primary storage", e); @@ -2992,12 +2999,12 @@ private VolumeInfo performLightweightLockMigration(VolumeInfo volume, UserVmVO v @Override @ActionEvent(eventType = EventTypes.EVENT_VOLUME_ATTACH, eventDescription = "attaching volume", async = true) - public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean allowAttachForSharedFS) { + public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean allowAttachForSharedFS, boolean allowAttachOnRestoring) { Account caller = CallContext.current().getCallingAccount(); VolumeInfo volumeToAttach = getAndCheckVolumeInfo(volumeId); - UserVmVO vm = getAndCheckUserVmVO(vmId, volumeToAttach); + UserVmVO vm = getAndCheckUserVmVO(vmId, volumeToAttach, allowAttachOnRestoring); if (!allowAttachForSharedFS && UserVmManager.SHAREDFSVM.equals(vm.getUserVmType())) { throw new InvalidParameterValueException("Can't attach a volume to a Shared FileSystem Instance"); @@ -3018,7 +3025,7 @@ public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean checkForVMSnapshots(vmId, vm); - checkForBackups(vm, true); + validateIfVmHasBackups(vm, true); _accountMgr.checkAccess(caller, null, true, volumeToAttach, vm); @@ -3045,7 +3052,7 @@ public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean } Account owner = _accountDao.findById(volumeToAttach.getAccountId()); - List resourceLimitStorageTags = _resourceLimitMgr.getResourceLimitStorageTagsForResourceCountOperation(true, diskOffering); + List resourceLimitStorageTags = _resourceLimitMgr.getResourceLimitStorageTagsForResourceCountOperation(true, diskOffering, null); Long requiredPrimaryStorageSpace = getRequiredPrimaryStorageSizeForVolumeAttach(resourceLimitStorageTags, volumeToAttach); try (CheckedReservation primaryStorageReservation = new CheckedReservation(owner, ResourceType.primary_storage, resourceLimitStorageTags, requiredPrimaryStorageSpace, reservationDao, _resourceLimitMgr)) { @@ -3178,15 +3185,16 @@ private void checkDeviceId(Long deviceId, VolumeInfo volumeToAttach, UserVmVO vm * * @return the user vm vo object correcponding to the vmId to attach to */ - @NotNull private UserVmVO getAndCheckUserVmVO(Long vmId, VolumeInfo volumeToAttach) { + @NotNull private UserVmVO getAndCheckUserVmVO(Long vmId, VolumeInfo volumeToAttach, boolean allowAttachOnRestoring) { UserVmVO vm = _userVmDao.findById(vmId); if (vm == null || vm.getType() != VirtualMachine.Type.User) { throw new InvalidParameterValueException("Please specify a valid User VM."); } - // Check that the VM is in the correct state - if (vm.getState() != State.Running && vm.getState() != State.Stopped) { - throw new InvalidParameterValueException("Please specify a VM that is either running or stopped."); + if (allowAttachOnRestoring) { + validateVmState(vm, State.Running, State.Stopped, State.Restoring); + } else { + validateVmState(vm, State.Running, State.Stopped); } // Check that the VM and the volume are in the same zone @@ -3196,6 +3204,13 @@ private void checkDeviceId(Long deviceId, VolumeInfo volumeToAttach, UserVmVO vm return vm; } + private void validateVmState(UserVmVO vm, State... states) { + List allowedStates = Arrays.asList(states); + if (!allowedStates.contains(vm.getState())) { + throw new InvalidParameterValueException(String.format("Please specify a VM that is on of the following states: %s.", allowedStates)); + } + } + /** * Check that the volume ID is valid * Check that the volume is a data volume @@ -3225,9 +3240,11 @@ private void checkDeviceId(Long deviceId, VolumeInfo volumeToAttach, UserVmVO vm return volumeToAttach; } - protected void checkForBackups(UserVmVO vm, boolean attach) { - if ((vm.getBackupOfferingId() == null || CollectionUtils.isEmpty(vm.getBackupVolumeList())) || BooleanUtils.isTrue(BackupManager.BackupEnableAttachDetachVolumes.value())) { - return; + protected boolean validateIfVmHasBackups(UserVmVO vm, boolean attach) { + if (vm.getBackupOfferingId() == null || CollectionUtils.isEmpty(backupDao.listByVmId(vm.getDataCenterId(), vm.getId()))) { + return false; + } else if (BooleanUtils.isTrue(BackupManager.BackupEnableAttachDetachVolumes.value())) { + return true; } String errorMsg = String.format("Unable to detach volume, cannot detach volume from a VM that has backups. First remove the VM from the backup offering or " + "set the global configuration '%s' to true.", BackupManager.BackupEnableAttachDetachVolumes.key()); @@ -3354,7 +3371,7 @@ private void updateResourceCount(Volume volume, Boolean displayVolume) { // Update only when the flag has changed. if (displayVolume != null && displayVolume != volume.isDisplayVolume()) { if (Boolean.FALSE.equals(displayVolume)) { - _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), true, volume.getSize(), _diskOfferingDao.findById(volume.getDiskOfferingId())); + _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), true, volume.getSize(), _diskOfferingDao.findById(volume.getDiskOfferingId()), null); } else { _resourceLimitMgr.incrementVolumeResourceCount(volume.getAccountId(), true, volume.getSize(), _diskOfferingDao.findById(volume.getDiskOfferingId())); } @@ -3450,7 +3467,10 @@ public Volume detachVolumeFromVM(DetachVolumeCmd cmmd) { throw new InvalidParameterValueException("Unable to detach volume, please specify an Instance that does not have Instance Snapshots"); } - checkForBackups(vm, false); + boolean hasBackup = validateIfVmHasBackups(vm, false); + if (hasBackup) { + internalBackupService.prepareVolumeForDetach(volume, vm); + } AsyncJobExecutionContext asyncExecutionContext = AsyncJobExecutionContext.getCurrentExecutionContext(); if (asyncExecutionContext != null) { @@ -4450,6 +4470,12 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, } } + VirtualMachine attachedVm = volume.getAttachedVM(); + if (attachedVm != null && HypervisorType.KVM.equals(attachedVm.getHypervisorType()) && SnapshotManager.kvmIncrementalSnapshot.valueIn(_hostDao.findClusterIdByVolumeInfo(volume)) && + backupManager.getBackupProvider(attachedVm.getDataCenterId()).getName().equals(BackupManagerImpl.KBOSS_BACKUP_PROVIDER) && + CollectionUtils.isNotEmpty(backupDao.listByVmId(attachedVm.getDataCenterId(), attachedVm.getId()))) { + throw new CloudRuntimeException(String.format("VM [%s] has KBOSS backups, cannot take incremental snapshots of it.", attachedVm.getUuid())); + } return snapshotMgr.allocSnapshot(volumeId, policyId, snapshotName, locationType, false, zoneIds); } @@ -4709,7 +4735,7 @@ protected void updateVolumeAccount(Account oldAccount, VolumeVO volume, Account Volume.class.getName(), volume.getUuid(), volume.isDisplayVolume()); DiskOfferingVO diskOfferingVO = _diskOfferingDao.findById(volume.getDiskOfferingId()); _resourceLimitMgr.decrementVolumeResourceCount(oldAccount.getAccountId(), true, volume.getSize(), - diskOfferingVO); + diskOfferingVO, null); volume.setAccountId(newAccount.getAccountId()); volume.setDomainId(newAccount.getDomainId()); diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index 543da444706b..f6eb0593be6c 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -115,10 +115,15 @@ public interface UserVmManager extends UserVmService { "true", "Defines whether a VM should be automatically migrated to a suitable host when the current host " + "lacks sufficient compute capacity to live scale the instance. Defaults to true.", true, ConfigKey.Scope.Cluster); + ConfigKey EnforceResourceLimitOnValidationVm = new ConfigKey( + "Advanced", Boolean.class, "enforce.resource.limit.on.backup.validation.vm", "false", "If set to true, validation VMs will be accounted in the resource limit of the " + + "account/domain.", true, ConfigKey.Scope.Account); + static final int MAX_USER_DATA_LENGTH_BYTES = 2048; public static final String CKS_NODE = "cksnode"; public static final String SHAREDFSVM = "sharedfsvm"; + String VALIDATION_VM = "validationvm"; /** * @param hostId get all of the virtual machines that belong to one host. @@ -158,13 +163,15 @@ public interface UserVmManager extends UserVmService { boolean expunge(UserVmVO vm); - Pair> startVirtualMachine(long vmId, Long hostId, Map additionalParams, String deploymentPlannerToUse) + Pair> startVirtualMachine(long vmId, Long hostId, Map additionalParams, String deploymentPlannerToUse, boolean quickRestore) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException; - Pair> startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, Map additionalParams, String deploymentPlannerToUse) + Pair> startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, + Map additionalParams, String deploymentPlannerToUse, boolean quickRestore) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException; - Pair> startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, Map additionalParams, String deploymentPlannerToUse, boolean isExplicitHost) + Pair> startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, + Map additionalParams, String deploymentPlannerToUse, boolean isExplicitHost, boolean quickRestore) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException; boolean upgradeVirtualMachine(Long id, Long serviceOfferingId, Map customParameters) throws ResourceUnavailableException, @@ -210,4 +217,6 @@ static Set getStrictHostTags() { * @return true if the VM is part of a CKS cluster, false otherwise. */ boolean isVMPartOfAnyCKSCluster(VMInstanceVO vm); + + UserVm allocateVMForValidation(long backupId, HypervisorType hypervisor) throws InsufficientCapacityException, ResourceAllocationException, ResourceUnavailableException; } diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index d4e7e0ced2b4..b3bc69835ff5 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -66,6 +66,9 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; +import com.cloud.agent.api.StartCommand; +import com.cloud.network.NetworkService; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -85,6 +88,7 @@ import org.apache.cloudstack.api.command.admin.vm.DeployVMCmdByAdmin; import org.apache.cloudstack.api.command.admin.vm.ExpungeVMCmd; import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; +import org.apache.cloudstack.api.command.user.network.CreateNetworkCmd; import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; import org.apache.cloudstack.api.command.user.vm.BaseDeployVMCmd; import org.apache.cloudstack.api.command.user.vm.CreateVMFromBackupCmd; @@ -112,6 +116,7 @@ import org.apache.cloudstack.backup.BackupManager; import org.apache.cloudstack.backup.BackupScheduleVO; import org.apache.cloudstack.backup.BackupVO; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupScheduleDao; import org.apache.cloudstack.schedule.ResourceScheduleManager; @@ -276,7 +281,6 @@ import com.cloud.kubernetes.cluster.KubernetesServiceHelper; import com.cloud.network.IpAddressManager; import com.cloud.network.Network; -import com.cloud.network.NetworkService; import com.cloud.network.Network.GuestType; import com.cloud.network.Network.IpAddresses; import com.cloud.network.Network.Provider; @@ -437,6 +441,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir private static final int ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION = 3; private static final long GiB_TO_BYTES = 1024 * 1024 * 1024; + private static final String BACKUP_VALIDATION_NETWORK = "BackupValidationNetwork"; + private static final String DEFAULT_SHARED_NETWORK_OFFERING_WITH_NO_SERVICE = "DefaultSharedNetworkOfferingWithNoService"; @Inject @@ -556,6 +562,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private VMSnapshotDao _vmSnapshotDao; @Inject + private VMSnapshotDetailsDao vmSnapshotDetailsDao; + @Inject private VMSnapshotManager _vmSnapshotMgr; @Inject private AffinityGroupVMMapDao _affinityGroupVMMapDao; @@ -618,6 +626,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private BackupManager backupManager; @Inject + private InternalBackupService internalBackupService; + @Inject private AnnotationDao annotationDao; @Inject private VmStatsDao vmStatsDao; @@ -731,15 +741,17 @@ public List getVirtualMachines(long hostId) { return _vmDao.listByHostId(hostId); } - protected void resourceCountIncrement(long accountId, Boolean displayVm, ServiceOffering serviceOffering, VirtualMachineTemplate template) { + protected void resourceCountIncrement(long accountId, Boolean displayVm, ServiceOffering serviceOffering, VirtualMachineTemplate template, + Boolean countDisplayFalseInResourceCount) { if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { - _resourceLimitMgr.incrementVmResourceCount(accountId, displayVm, serviceOffering, template); + _resourceLimitMgr.incrementVmResourceCount(accountId, displayVm, serviceOffering, template, countDisplayFalseInResourceCount); } } - protected void resourceCountDecrement(long accountId, Boolean displayVm, ServiceOffering serviceOffering, VirtualMachineTemplate template) { + protected void resourceCountDecrement(long accountId, Boolean displayVm, ServiceOffering serviceOffering, VirtualMachineTemplate template, + Boolean enforceResourceLimitOnDisplayFalse) { if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { - _resourceLimitMgr.decrementVmResourceCount(accountId, displayVm, serviceOffering, template); + _resourceLimitMgr.decrementVmResourceCount(accountId, displayVm, serviceOffering, template, enforceResourceLimitOnDisplayFalse); } } @@ -1249,7 +1261,7 @@ private UserVm forceRebootVirtualMachine(UserVmVO vm, long hostId, boolean enter if (enterSetup) { params.put(VirtualMachineProfile.Param.BootIntoSetup, Boolean.TRUE); } - return startVirtualMachine(vm.getId(), null, null, hostId, params, null, false).first(); + return startVirtualMachine(vm.getId(), null, null, hostId, params, null, false, false).first(); } } catch (CloudException e) { throw new CloudRuntimeException(String.format("Unable to reboot the VM: %s", vm), e); @@ -2539,7 +2551,7 @@ public UserVm recoverVirtualMachine(RecoverVMCmd cmd) throws ResourceAllocationE } //Update Resource Count for the given account - resourceCountIncrement(account.getId(), vm.isDisplayVm(), serviceOffering, template); + resourceCountIncrement(account.getId(), vm.isDisplayVm(), serviceOffering, template, null); } finally { ReservationHelper.closeAll(reservations); @@ -2863,7 +2875,7 @@ private void updateVmStateForFailedVmCreation(Long vmId, Long hostId) { VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); // Update Resource Count for the given account - resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), offering, template); + resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), offering, template, null); } } } @@ -3194,9 +3206,9 @@ protected void updateDisplayVmFlag(Boolean isDisplayVm, Long id, UserVmVO vmInst ServiceOffering offering = serviceOfferingDao.findByIdIncludingRemoved(vmInstance.getId(), vmInstance.getServiceOfferingId()); VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vmInstance.getTemplateId()); if (isDisplayVm) { - resourceCountIncrement(vmInstance.getAccountId(), true, offering, template); + resourceCountIncrement(vmInstance.getAccountId(), true, offering, template, null); } else { - resourceCountDecrement(vmInstance.getAccountId(), true, offering, template); + resourceCountDecrement(vmInstance.getAccountId(), true, offering, template, null); } // Usage @@ -3558,7 +3570,7 @@ public UserVm startVirtualMachine(StartVMCmd cmd) throws ExecutionException, Con additonalParams.put(VirtualMachineProfile.Param.ConsiderLastHost, cmd.getConsiderLastHost().toString()); } - return startVirtualMachine(cmd.getId(), cmd.getPodId(), cmd.getClusterId(), cmd.getHostId(), additonalParams, cmd.getDeploymentPlanner()).first(); + return startVirtualMachine(cmd.getId(), cmd.getPodId(), cmd.getClusterId(), cmd.getHostId(), additonalParams, cmd.getDeploymentPlanner(), false).first(); } @Override @@ -3662,13 +3674,13 @@ protected void checkPluginsIfVmCanBeDestroyed(UserVm vm) { @Override @ActionEvent(eventType = EventTypes.EVENT_VM_DESTROY, eventDescription = "destroying Vm", async = true) - public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, ConcurrentOperationException { + public UserVm destroyVm(DestroyVMCmd cmd, boolean checkExpunge) throws ResourceUnavailableException, ConcurrentOperationException { CallContext ctx = CallContext.current(); long vmId = cmd.getId(); boolean expunge = cmd.getExpunge(); boolean forced = cmd.isForced(); - if (expunge) { + if (checkExpunge && expunge) { String jobParamsString = ((AsyncJobVO) cmd.getJob()).getCmdInfo(); HashMap jobParams = GsonHelper.getGson().fromJson(jobParamsString, jobParamsType); String apiKey = jobParams.get("apiKey"); @@ -4456,7 +4468,7 @@ private UserVm getCheckedUserVmResource(DataCenter zone, String hostName, String Map userVmOVFPropertiesMap, boolean dynamicScalingEnabled, String vmType, VMTemplateVO template, HypervisorType hypervisorType, long accountId, ServiceOfferingVO offering, boolean isIso, Long rootDiskOfferingId, Long rootDiskKmsKeyId, long volumesSize, Volume volume, Snapshot snapshot) throws ResourceAllocationException { - if (!VirtualMachineManager.ResourceCountRunningVMsonly.value()) { + if (!VirtualMachineManager.ResourceCountRunningVMsonly.value() && !(VALIDATION_VM.equals(vmType) && !EnforceResourceLimitOnValidationVm.valueIn(accountId))) { List resourceLimitHostTags = resourceLimitService.getResourceLimitHostTags(offering, template); try (CheckedReservation vmReservation = new CheckedReservation(owner, ResourceType.user_vm, resourceLimitHostTags, 1l, reservationDao, resourceLimitService); CheckedReservation cpuReservation = new CheckedReservation(owner, ResourceType.cpu, resourceLimitHostTags, Long.valueOf(offering.getCpu()), reservationDao, resourceLimitService); @@ -4482,7 +4494,13 @@ protected List getResourceLimitStorageTags(long diskOfferingId) { return resourceLimitService.getResourceLimitStorageTags(diskOfferingVO); } - private void reserveStorageResourcesForVm(List checkedReservations, Account owner, Long diskOfferingId, Long diskSize, List dataDiskInfoList, Long rootDiskOfferingId, ServiceOfferingVO offering, Long rootDiskSize) throws ResourceAllocationException { + private void reserveStorageResourcesForVm(List checkedReservations, Account owner, Long diskOfferingId, + Long diskSize, List dataDiskInfoList, Long rootDiskOfferingId, + ServiceOfferingVO offering, Long rootDiskSize, String vmType) throws ResourceAllocationException { + if (VALIDATION_VM.equals(vmType) && !EnforceResourceLimitOnValidationVm.valueIn(owner.getAccountId())) { + return; + } + List rootResourceLimitStorageTags = getResourceLimitStorageTags(rootDiskOfferingId != null ? rootDiskOfferingId : offering.getDiskOfferingId()); CheckedReservation rootVolumeReservation = new CheckedReservation(owner, ResourceType.volume, rootResourceLimitStorageTags, 1L, reservationDao, resourceLimitService); checkedReservations.add(rootVolumeReservation); @@ -4526,7 +4544,7 @@ private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, Stri try { - reserveStorageResourcesForVm(checkedReservations, owner, diskOfferingId, diskSize, dataDiskInfoList, rootDiskOfferingId, offering, volumesSize); + reserveStorageResourcesForVm(checkedReservations, owner, diskOfferingId, diskSize, dataDiskInfoList, rootDiskOfferingId, offering, volumesSize, vmType); // verify security group ids if (securityGroupIdList != null) { @@ -4703,19 +4721,7 @@ private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, Stri } profile.setDefaultNic(true); - if (!_networkModel.areServicesSupportedInNetwork(network.getId(), new Service[]{Service.UserData})) { - if ((userData != null) && (!userData.isEmpty())) { - throw new InvalidParameterValueException(String.format("Unable to deploy VM as UserData is provided while deploying the VM, but there is no support for %s service in the default network %s/%s.", Service.UserData.getName(), network.getName(), network.getUuid())); - } - - if ((sshPublicKeys != null) && (!sshPublicKeys.isEmpty())) { - throw new InvalidParameterValueException(String.format("Unable to deploy VM as SSH keypair is provided while deploying the VM, but there is no support for %s service in the default network %s/%s", Service.UserData.getName(), network.getName(), network.getUuid())); - } - - if (template.isEnablePassword()) { - throw new InvalidParameterValueException(String.format("Unable to deploy VM as template %s is password enabled, but there is no support for %s service in the default network %s/%s", template, Service.UserData.getName(), network.getName(), network.getUuid())); - } - } + validateUserdataSupport(userData, vmType, template, network, sshPublicKeys); } if (_networkModel.isSecurityGroupSupportedInNetwork(network)) { @@ -4826,6 +4832,30 @@ private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, Stri } } + /** + * Validates that the network supports the necessary UserData-related features for the VM + *
+ * Validation VMs are not validated, these VMs should be in a no-service network regardless of the original VM's settings. + * */ + private void validateUserdataSupport(String userData, String vmType, VMTemplateVO template, NetworkVO network, String sshPublicKeys) { + if (VALIDATION_VM.equals(vmType)) { + return; + } + if (!_networkModel.areServicesSupportedInNetwork(network.getId(), new Service[]{Service.UserData})) { + if ((userData != null) && (!userData.isEmpty())) { + throw new InvalidParameterValueException(String.format("Unable to deploy VM as UserData is provided while deploying the VM, but there is no support for %s service in the default network %s/%s.", Service.UserData.getName(), network.getName(), network.getUuid())); + } + + if ((sshPublicKeys != null) && (!sshPublicKeys.isEmpty())) { + throw new InvalidParameterValueException(String.format("Unable to deploy VM as SSH keypair is provided while deploying the VM, but there is no support for %s service in the default network %s/%s", Service.UserData.getName(), network.getName(), network.getUuid())); + } + + if (template.isEnablePassword()) { + throw new InvalidParameterValueException(String.format("Unable to deploy VM as template %s is password enabled, but there is no support for %s service in the default network %s/%s", template.getId(), Service.UserData.getName(), network.getName(), network.getUuid())); + } + } + } + private void assignInstanceToGroup(String group, long id) { // Assign instance to the group try { @@ -5145,7 +5175,8 @@ private UserVmVO commitUserVm(final boolean isImport, final DataCenter zone, fin try { //Update Resource Count for the given account - resourceCountIncrement(accountId, isDisplayVm, offering, template); + boolean countDisplayFalseInResourceCount = VALIDATION_VM.equals(vm.getUserVmType()) && EnforceResourceLimitOnValidationVm.valueIn(accountId); + resourceCountIncrement(accountId, isDisplayVm, offering, template, countDisplayFalseInResourceCount); } catch (CloudRuntimeException cre) { ArrayList epoList = cre.getIdProxyList(); if (epoList == null || !epoList.stream().anyMatch( e -> e.getUuid().equals(vm.getUuid()))) { @@ -5528,7 +5559,7 @@ private UserVm startVirtualMachine(long vmId, Long podId, Long clusterId, Long h Pair> vmParamPair = null; try { - vmParamPair = startVirtualMachine(vmId, podId, clusterId, hostId, additonalParams, deploymentPlannerToUse); + vmParamPair = startVirtualMachine(vmId, podId, clusterId, hostId, additonalParams, deploymentPlannerToUse, false); vm = vmParamPair.first(); // At this point VM should be in "Running" state @@ -5721,6 +5752,12 @@ public boolean finalizeDeployment(Commands cmds, VirtualMachineProfile profile, @Override public boolean finalizeCommandsOnStart(Commands cmds, VirtualMachineProfile profile) { UserVmVO vm = _vmDao.findById(profile.getId()); + + if (vm.getHypervisorType() == HypervisorType.KVM && VALIDATION_VM.equals(vm.getUserVmType())) { + StartCommand startCommand = cmds.getCommand(StartCommand.class); + startCommand.setSecondaryStorages(new ArrayList<>(internalBackupService.getSecondaryStorageUrls(vm))); + } + List vmSnapshots = _vmSnapshotDao.findByVm(vm.getId()); RestoreVMSnapshotCommand command = _vmSnapshotMgr.createRestoreCommand(vm, vmSnapshots); if (command != null) { @@ -5815,6 +5852,10 @@ public boolean finalizeStart(VirtualMachineProfile profile, long hostId, Command return false; } + if (UserVmManager.VALIDATION_VM.equals(vm.getUserVmType())) { + return true; + } + Answer answer = cmds.getAnswer("restoreVMSnapshot"); if (answer != null && answer instanceof RestoreVMSnapshotAnswer) { RestoreVMSnapshotAnswer restoreVMSnapshotAnswer = (RestoreVMSnapshotAnswer) answer; @@ -5937,20 +5978,21 @@ public void finalizeStop(VirtualMachineProfile profile, Answer answer) { @Override public Pair> startVirtualMachine(long vmId, Long hostId, @NotNull Map additionalParams, - String deploymentPlannerToUse) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { - return startVirtualMachine(vmId, null, null, hostId, additionalParams, deploymentPlannerToUse); + String deploymentPlannerToUse, boolean quickRestore) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, + ResourceAllocationException { + return startVirtualMachine(vmId, null, null, hostId, additionalParams, deploymentPlannerToUse, quickRestore); } @Override public Pair> startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, - @NotNull Map additionalParams, String deploymentPlannerToUse) + @NotNull Map additionalParams, String deploymentPlannerToUse, boolean quickRestore) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { - return startVirtualMachine(vmId, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, true); + return startVirtualMachine(vmId, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, true, quickRestore); } private Pair> startVirtualMachineUnchecked(UserVmVO vm, VMTemplateVO template, Long podId, Long clusterId, Long hostId, @NotNull Map additionalParams, String deploymentPlannerToUse, - boolean isExplicitHost, boolean isRootAdmin) throws ResourceUnavailableException, InsufficientCapacityException { + boolean isExplicitHost, boolean isRootAdmin, boolean quickRestore) throws ResourceUnavailableException, InsufficientCapacityException { // check if vm is security group enabled if (_securityGroupMgr.isVmSecurityGroupEnabled(vm.getId()) && _securityGroupMgr.getSecurityGroupsForVm(vm.getId()).isEmpty() @@ -5973,7 +6015,7 @@ private Pair> startVirtualMac // Default behaviour is invoked when host, cluster or pod are not specified Pod destinationPod = getDestinationPod(podId, isRootAdmin); Cluster destinationCluster = getDestinationCluster(clusterId, isRootAdmin); - HostVO destinationHost = getDestinationHost(hostId, isRootAdmin, isExplicitHost); + HostVO destinationHost = getDestinationHost(hostId, isRootAdmin, isExplicitHost, quickRestore); DataCenterDeployment plan = null; boolean deployOnGivenHost = false; if (destinationHost != null) { @@ -6073,7 +6115,7 @@ private Pair> startVirtualMac @Override public Pair> startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, - @NotNull Map additionalParams, String deploymentPlannerToUse, boolean isExplicitHost) + @NotNull Map additionalParams, String deploymentPlannerToUse, boolean isExplicitHost, boolean quickRestore) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { // Input validation final Account callerAccount = CallContext.current().getCallingAccount(); @@ -6114,10 +6156,10 @@ public Pair> startVirtualMach CheckedReservation cpuReservation = new CheckedReservation(owner, ResourceType.cpu, resourceLimitHostTags, Long.valueOf(offering.getCpu()), reservationDao, resourceLimitService); CheckedReservation memReservation = new CheckedReservation(owner, ResourceType.memory, resourceLimitHostTags, Long.valueOf(offering.getRamSize()), reservationDao, resourceLimitService); ) { - return startVirtualMachineUnchecked(vm, template, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, isExplicitHost, isRootAdmin); + return startVirtualMachineUnchecked(vm, template, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, isExplicitHost, isRootAdmin, quickRestore); } } else { - return startVirtualMachineUnchecked(vm, template, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, isExplicitHost, isRootAdmin); + return startVirtualMachineUnchecked(vm, template, podId, clusterId, hostId, additionalParams, deploymentPlannerToUse, isExplicitHost, isRootAdmin, quickRestore); } } @@ -6202,10 +6244,10 @@ private Cluster getDestinationCluster(Long clusterId, boolean isRootAdmin) { return destinationCluster; } - private HostVO getDestinationHost(Long hostId, boolean isRootAdmin, boolean isExplicitHost) { + private HostVO getDestinationHost(Long hostId, boolean isRootAdmin, boolean isExplicitHost, boolean quickRestore) { HostVO destinationHost = null; if (hostId != null) { - if (isExplicitHost && !isRootAdmin) { + if (isExplicitHost && !isRootAdmin && !quickRestore) { throw new PermissionDeniedException( "Parameter " + ApiConstants.HOST_ID + " can only be specified by a Root Admin, permission denied"); } @@ -6271,7 +6313,8 @@ public UserVm destroyVm(long vmId, boolean expunge) throws ResourceUnavailableEx // Get serviceOffering and template for Virtual Machine VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); //Update Resource Count for the given account - resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), offering, template); + resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), offering, template, + (VALIDATION_VM.equals(vm.getUserVmType()) && EnforceResourceLimitOnValidationVm.valueIn(owner.getAccountId()))); } return _vmDao.findById(vmId); } else { @@ -6707,12 +6750,14 @@ public UserVm createVirtualMachine(DeployVMCmd cmd) throws InsufficientCapacityE networkIds = new ArrayList<>(userVmNetworkMap.values()); } - return createVirtualMachine(cmd, zone, owner, serviceOffering, template, cmd.getHypervisor(), diskOfferingId, cmd.getSize(), overrideDiskOfferingId, dataDiskInfoList, networkIds, cmd.getIpToNetworkMap(), volume, snapshot); + return createVirtualMachine(cmd, zone, owner, serviceOffering, template, cmd.getHypervisor(), diskOfferingId, cmd.getSize(), overrideDiskOfferingId, dataDiskInfoList, + networkIds, cmd.getIpToNetworkMap(), volume, snapshot); } private UserVm createVirtualMachine(BaseDeployVMCmd cmd, DataCenter zone, Account owner, ServiceOffering serviceOffering, VirtualMachineTemplate template, HypervisorType hypervisor, Long diskOfferingId, Long size, Long overrideDiskOfferingId, List dataDiskInfoList, - List networkIds, Map ipToNetworkMap, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException, ResourceAllocationException { + List networkIds, Map ipToNetworkMap, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, + ResourceUnavailableException, ConcurrentOperationException, ResourceAllocationException { ServiceOfferingJoinVO svcOffering = serviceOfferingJoinDao.findById(serviceOffering.getId()); boolean isLeaseFeatureEnabled = VMLeaseManager.InstanceLeaseEnabled.value(); @@ -6747,7 +6792,7 @@ private UserVm createVirtualMachine(BaseDeployVMCmd cmd, DataCenter zone, Accoun boolean isRootAdmin = _accountService.isRootAdmin(callerId); Long hostId = cmd.getHostId(); - getDestinationHost(hostId, isRootAdmin, true); + getDestinationHost(hostId, isRootAdmin, true, false); String ipAddress = cmd.getIpAddress(); String ip6Address = cmd.getIp6Address(); @@ -8383,12 +8428,12 @@ protected NetworkOfferingVO getOfferingWithRequiredAvailabilityForNetworkCreatio /** * Executes all ownership steps necessary to assign a VM to another user: * generating a destroy VM event ({@link EventTypes}), - * decrementing the old user resource count ({@link #resourceCountDecrement(long, Boolean, ServiceOffering, VirtualMachineTemplate)}), + * decrementing the old user resource count ({@link #resourceCountDecrement(long, Boolean, ServiceOffering, VirtualMachineTemplate, Boolean)}), * removing the VM from its instance group ({@link #removeInstanceFromInstanceGroup(long)}), * updating the VM owner to the new account ({@link #updateVmOwner(Account, UserVmVO, Long, Long)}), * updating the volumes to the new account ({@link #updateVolumesOwner(List, Account, Account, Long)}), * updating the network for the VM ({@link #updateVmNetwork(AssignVMCmd, Account, UserVmVO, Account, VirtualMachineTemplate)}), - * incrementing the new user resource count ({@link #resourceCountIncrement(long, Boolean, ServiceOffering, VirtualMachineTemplate)}), + * incrementing the new user resource count ({@link #resourceCountIncrement(long, Boolean, ServiceOffering, VirtualMachineTemplate, Boolean)}), * and generating a create VM event ({@link EventTypes}). * @param cmd The assignVMCmd. * @param caller The account calling the assignVMCmd. @@ -8408,7 +8453,7 @@ protected void executeStepsToChangeOwnershipOfVm(AssignVMCmd cmd, Account caller vm.getTemplateId(), vm.getHypervisorType().toString(), VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplayVm()); logger.trace("Decrementing old account [{}] resource count.", oldAccount); - resourceCountDecrement(oldAccount.getAccountId(), vm.isDisplayVm(), offering, template); + resourceCountDecrement(oldAccount.getAccountId(), vm.isDisplayVm(), offering, template, null); logger.trace("Removing VM [{}] from its instance group.", vm); removeInstanceFromInstanceGroup(vm.getId()); @@ -8429,7 +8474,7 @@ protected void executeStepsToChangeOwnershipOfVm(AssignVMCmd cmd, Account caller logger.trace(String.format("Incrementing new account [%s] resource count.", newAccount)); if (!isResourceCountRunningVmsOnlyEnabled()) { - resourceCountIncrement(newAccountId, vm.isDisplayVm(), offering, template); + resourceCountIncrement(newAccountId, vm.isDisplayVm(), offering, template, null); } logger.trace(String.format("Generating create event for VM [%s].", vm)); @@ -8456,7 +8501,7 @@ protected void updateVolumesOwner(final List volumes, Account oldAccou logger.trace("Decrementing volume [{}] and primary storage resource count for the old account [{}].", volume, oldAccount); DiskOfferingVO diskOfferingVO = _diskOfferingDao.findById(volume.getDiskOfferingId()); - _resourceLimitMgr.decrementVolumeResourceCount(oldAccount.getAccountId(), volume.isDisplay(), volume.getSize(), diskOfferingVO); + _resourceLimitMgr.decrementVolumeResourceCount(oldAccount.getAccountId(), volume.isDisplay(), volume.getSize(), diskOfferingVO, null); logger.trace("Setting the new account [{}] and domain [{}] for volume [{}].", newAccount, newAccount.getDomainId(), volume); volume.setAccountId(newAccountId); @@ -9205,6 +9250,7 @@ public UserVm restoreVirtualMachine(final Account caller, final long vmId, final if (needRestart) { try { _itMgr.stop(vm.getUuid()); + vm.setState(State.Stopped); } catch (ResourceUnavailableException e) { logger.debug("Stop vm {} failed", vm, e); CloudRuntimeException ex = new CloudRuntimeException("Stop vm failed for specified vmId"); @@ -9280,6 +9326,7 @@ public Pair doInTransaction(final TransactionStatus status) th newVol.getDiskOfferingId(), newVol.getTemplateId(), newVol.getSize(), Volume.class.getName(), newVol.getUuid(), vmId, newVol.isDisplay()); // Detach, destroy and create the usage event for the old root volume. + internalBackupService.prepareVolumeForDetach(root, vm); _volsDao.detachVolume(root.getId()); destroyVolumeInContext(vm, Volume.State.Allocated.equals(root.getState()) || expunge, root); @@ -9668,7 +9715,8 @@ public ConfigKey[] getConfigKeys() { VmIpFetchThreadPoolMax, VmIpFetchTaskWorkers, AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig, DisplayVMOVFProperties, KvmAdditionalConfigAllowList, XenServerAdditionalConfigAllowList, VmwareAdditionalConfigAllowList, DestroyRootVolumeOnVmDestruction, EnforceStrictResourceLimitHostTagCheck, StrictHostTags, AllowUserForceStopVm, VmDistinctHostNameScope, - VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva, AllowDifferentHostTagsOfferingsForVmScale, AutoMigrateVmOnLiveScaleInsufficientCapacity}; + VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva, AllowDifferentHostTagsOfferingsForVmScale, + AutoMigrateVmOnLiveScaleInsufficientCapacity, EnforceResourceLimitOnValidationVm}; } @Override @@ -9785,7 +9833,8 @@ private void destroyVolumeInContext(UserVmVO vm, boolean expunge, VolumeVO volum volumeContext.setEventResourceType(ApiCommandResourceType.Volume); volumeContext.setEventResourceId(volume.getId()); try { - Volume result = _volumeService.destroyVolume(volume.getId(), CallContext.current().getCallingAccount(), expunge, false); + Volume result = _volumeService.destroyVolume(volume.getId(), CallContext.current().getCallingAccount(), expunge, false, + (VALIDATION_VM.equals(vm.getUserVmType()) && EnforceResourceLimitOnValidationVm.valueIn(volume.getAccountId()))); if (result == null) { logger.error("DestroyVM remove volume - failed to delete volume {} from instance {}", volume, vm); @@ -10032,7 +10081,8 @@ public UserVm allocateVMFromBackup(CreateVMFromBackupCmd cmd) throws Insufficien ipToNetworkMap = backupManager.getIpToNetworkMapFromBackup(backup, cmd.getPreserveIp(), networkIds); } - UserVm vm = createVirtualMachine(cmd, targetZone, owner, serviceOffering, template, hypervisorType, diskOfferingId, size, overrideDiskOfferingId, dataDiskInfoList, networkIds, ipToNetworkMap, null, null); + UserVm vm = createVirtualMachine(cmd, targetZone, owner, serviceOffering, template, hypervisorType, diskOfferingId, size, overrideDiskOfferingId, dataDiskInfoList, + networkIds, ipToNetworkMap, null, null); String vmSettingsFromBackup = backup.getDetail(ApiConstants.VM_SETTINGS); if (vm != null && vmSettingsFromBackup != null) { @@ -10063,7 +10113,7 @@ public UserVm restoreVMFromBackup(CreateVMFromBackupCmd cmd) throws ResourceUnav try { Pair> vmParamPair = null; - vmParamPair = startVirtualMachine(vmId, null, null, null, additonalParams, null); + vmParamPair = startVirtualMachine(vmId, null, null, null, additonalParams, null, false); vm = vmParamPair.first(); Long isoId = vm.getIsoId(); @@ -10073,7 +10123,7 @@ public UserVm restoreVMFromBackup(CreateVMFromBackupCmd cmd) throws ResourceUnav _vmDao.update(vm.getId(), vmVO); } - backupManager.restoreBackupToVM(cmd.getBackupId(), vmId); + backupManager.restoreBackupToVM(cmd.getBackupId(), vmId, cmd.getQuickRestore()); } catch (CloudRuntimeException | ResourceUnavailableException | ResourceAllocationException | InsufficientCapacityException e) { UserVmVO vmVO = _vmDao.findById(vmId); @@ -10094,7 +10144,7 @@ public UserVm restoreVMFromBackup(CreateVMFromBackupCmd cmd) throws ResourceUnav vm = resetVMSSHKeyInternal(userVm, owner, sshKeyPairNames); } - if (cmd.getStartVm()) { + if (cmd.getStartVm() && !cmd.getQuickRestore()) { Long podId = null; Long clusterId = null; if (cmd instanceof CreateVMFromBackupCmdByAdmin) { @@ -10158,7 +10208,7 @@ private void postProcessingUnmanageVM(UserVmVO vm) { vm.getId(), vm.getHostName(), vm.getServiceOfferingId(), vm.getTemplateId(), vm.getHypervisorType().toString(), VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplayVm()); - resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), offering, template); + resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), offering, template, null); resourceNotDecremented = false; } // VM destroy usage event @@ -10166,7 +10216,7 @@ private void postProcessingUnmanageVM(UserVmVO vm) { vm.getId(), vm.getHostName(), vm.getServiceOfferingId(), vm.getTemplateId(), vm.getHypervisorType().toString(), VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplayVm()); if (resourceNotDecremented) { - resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), offering, template); + resourceCountDecrement(vm.getAccountId(), vm.isDisplayVm(), offering, template, null); } } @@ -10182,7 +10232,7 @@ private void postProcessingUnmanageVMVolumes(List volumes, UserVmVO vm Volume.class.getName(), volume.getUuid(), volume.isDisplayVolume()); } _resourceLimitMgr.decrementVolumeResourceCount(vm.getAccountId(), volume.isDisplayVolume(), - volume.getSize(), _diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId())); + volume.getSize(), _diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId()), null); } } @@ -10290,4 +10340,111 @@ protected VMTemplateVO getBlankInstanceTemplate() { template = _templateDao.persist(template); return template; } + + @Override + public UserVm allocateVMForValidation(long backupId, HypervisorType hypervisor) throws InsufficientCapacityException, ResourceAllocationException, ResourceUnavailableException { + BackupVO backup = backupDao.findById(backupId); + if (backup == null) { + throw new CloudRuntimeException(String.format("Backup [%s] was not found.", backupId)); + } + + DataCenter zone = _dcDao.findById(backup.getZoneId()); + if (zone == null) { + throw new CloudRuntimeException(String.format("Unable to find zone [%s] of backup [%s].", backup.getZoneId(), backup.getUuid())); + } + + backupManager.validateBackupForZone(backup.getZoneId()); + backupDao.loadDetails(backup); + + UserVmVO backupVm = _vmDao.findByIdIncludingRemoved(backup.getVmId()); + HypervisorType hypervisorType = backupVm.getHypervisorType(); + + String serviceOfferingUuid = backup.getDetail(ApiConstants.SERVICE_OFFERING_ID); + if (serviceOfferingUuid == null) { + throw new CloudRuntimeException(String.format("Backup [%s] doesn't contain service offering UUID. Unable to validate it.", backup.getUuid())); + } + ServiceOffering serviceOffering = serviceOfferingDao.findByUuid(serviceOfferingUuid); + if (serviceOffering == null) { + throw new CloudRuntimeException(String.format("Unable to find service offering with the UUID stored in backup [%s]. Unable to validate the backup.", backup.getUuid())); + } + + String templateUuid = backup.getDetail(ApiConstants.TEMPLATE_ID); + if (templateUuid == null) { + throw new CloudRuntimeException(String.format("Backup [%s] doesn't contain a template UUID. Unable to validate it.", backup.getUuid())); + } + VirtualMachineTemplate template = _templateDao.findByUuidIncludingRemoved(templateUuid); + if (template == null) { + throw new CloudRuntimeException(String.format("Unable to find template associated with the backup [%s]. Unable to validate it.", backup.getUuid())); + } + + Map details = new HashMap<>(); + + VmDiskInfo rootVmDiskInfoFromBackup = backupManager.getRootDiskInfoFromBackup(backup); + updateDetailsWithRootDiskAttributes(details, rootVmDiskInfoFromBackup); + Long size = rootVmDiskInfoFromBackup.getSize(); + List dataDiskInfoList = backupManager.getDataDiskInfoListFromBackup(backup); + + List networkIds = new ArrayList(); + Network network = getValidationNetwork(zone.getId()); + networkIds.add(network.getId()); + + Account owner = _accountService.getActiveAccountById(backup.getAccountId()); + CreateVMFromBackupCmd cmd = new CreateVMFromBackupCmdByAdmin(hypervisor.name(), VALIDATION_VM); + UserVm vm = createVirtualMachine(cmd, zone, owner, serviceOffering, template, hypervisorType, null, size, null, dataDiskInfoList, networkIds, null, null, null); + + String vmSettingsFromBackup = backup.getDetail(ApiConstants.VM_SETTINGS); + UserVmVO vmVO = null; + if (vm != null) { + vmVO = _vmDao.findById(vm.getId()); + Map vmDetails = new HashMap<>(); + vmDetails.put(ApiConstants.BACKUP_ID, backup.getUuid()); + vmVO.setDetails(vmDetails); + } + if (vmVO != null && vmSettingsFromBackup != null) { + Map detailsFromBackup = vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId()); + vmVO.getDetails().putAll(detailsFromBackup); + + Type type = new com.google.common.reflect.TypeToken>(){}.getType(); + Map vmDetailsFromBackup = new Gson().fromJson(vmSettingsFromBackup, type); + for (Entry entry : vmDetailsFromBackup.entrySet()) { + if (!detailsFromBackup.containsKey(entry.getKey())) { + vmVO.setDetail(entry.getKey(), entry.getValue()); + } + } + } + if (vmVO != null) { + _vmDao.saveDetails(vmVO); + } + + return vm; + } + + private Network getValidationNetwork(long zoneId) { + NetworkVO networkVo = _networkDao.findByZoneIdAndAccountIdAndGuestTypeAndName(zoneId, Account.ACCOUNT_ID_SYSTEM, GuestType.Shared, BACKUP_VALIDATION_NETWORK); + AccountVO accountVO = _accountDao.findById(Account.ACCOUNT_ID_SYSTEM); + + if (networkVo != null) { + return networkVo; + } + + NetworkOfferingVO offeringVo = _networkOfferingDao.findByUniqueName(DEFAULT_SHARED_NETWORK_OFFERING_WITH_NO_SERVICE); + + if (offeringVo == null) { + offeringVo = new NetworkOfferingVO(DEFAULT_SHARED_NETWORK_OFFERING_WITH_NO_SERVICE, + "Default shared offering with no services.", TrafficType.Guest, false, false, null, null, true, Availability.Optional, null, GuestType.Shared, false, true, + false, false, false, false); + offeringVo.setState(NetworkOffering.State.Enabled); + offeringVo = _networkOfferingDao.persistDefaultNetworkOffering(offeringVo); + } + + try { + CreateNetworkCmd cmd = new CreateNetworkCmd(offeringVo.getId(), BACKUP_VALIDATION_NETWORK, "System network for validating backups", "192.168.0.1", "255.255.0.0", + "192.168.0.2", "192.168.255.255", accountVO.getDomainId(), accountVO.getAccountName(), zoneId, ACLType.Domain.name(), true, false); + ComponentContext.inject(cmd); + return networkService.createGuestNetwork(cmd); + } catch (InsufficientCapacityException | ResourceAllocationException ex) { + logger.error("Failed to create network for backup validation.", ex); + throw new CloudRuntimeException(ex); + } + } } diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupCompressionServiceJobController.java b/server/src/main/java/org/apache/cloudstack/backup/BackupCompressionServiceJobController.java new file mode 100644 index 000000000000..ba17241aa3a6 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupCompressionServiceJobController.java @@ -0,0 +1,241 @@ +//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 +//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.backup; + +import com.cloud.dc.ClusterVO; +import com.cloud.dc.DataCenterVO; +import com.cloud.host.HostVO; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.UuidUtils; +import com.cloud.utils.concurrency.NamedThreadFactory; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.logging.log4j.ThreadContext; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class BackupCompressionServiceJobController extends InternalBackupServiceJobController implements Configurable { + + private static final String LOCK = "compression_lock"; + protected ConfigKey backupCompressionMaxConcurrentOperationsPerHost = new ConfigKey<>("Advanced", Integer.class, + "backup.compression.max.concurrent.operations.per.host", "5", "Determines the maximum number of concurrent backup compressions per host. Values lower than 0 remove" + + " the limit, meaning that as many compressions as possible will be done at the same time.", true, ConfigKey.Scope.Cluster); + + protected ConfigKey backupCompressionMaxConcurrentOperations = new ConfigKey<>("Advanced", Integer.class, + "backup.compression.max.concurrent.operations", "10", "Determines the maximum number of concurrent backup compressions in the zone. Values lower than 1 remove" + + " the limit, meaning that as many compressions as possible will be done at the same time.", true, ConfigKey.Scope.Zone); + + protected ConfigKey backupCompressionMaxJobRetries = new ConfigKey<>("Advanced", Integer.class, + "backup.compression.max.job.retries", "2", "Determines the maximum number of retries for backup compression jobs. This includes both start compression jobs and " + + "finalize compression jobs.", true, ConfigKey.Scope.Cluster); + + protected ConfigKey backupCompressionRetryInterval = new ConfigKey<>("Advanced", Integer.class, + "backup.compression.retry.interval", "60", "Determines the minimum amount of time (in minutes) to retry a backup compression job. This includes both start " + + "compression jobs and finalize compression jobs.", true, ConfigKey.Scope.Cluster); + + protected ConfigKey backupCompressionTaskEnabled = new ConfigKey<>("Advanced", Boolean.class, "backup.compression.task.enabled", "true", "Whether the backup " + + "compression task should be running or not. Please set this to false and wait for any compression jobs to finish before restarting the Management Server.", true, + ConfigKey.Scope.Account); + + @Inject + private InternalBackupService internalBackupService; + + private ExecutorService executor; + + private ScheduledExecutorService scheduledExecutor; + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + + executor = Executors.newCachedThreadPool(new NamedThreadFactory("BackupCompressionTask")); + scheduledExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("BackupCompressionScheduler")); + scheduledExecutor.scheduleAtFixedRate(this::run, 60, 60, TimeUnit.SECONDS); + this.controllerType = "compression"; + return true; + } + + /** + * For each zone, get the jobs that should be started and distribute them through the hosts. + * Will lock the backup compression table so that only one management server executes this task at a time. + * Catches all exceptions and only sends them to the log. If we throw an exception, the task will stop running until the server is restarted. + * */ + @Override + protected void searchAndDispatchJobs() { + try { + List zones = dataCenterDao.listEnabledZones(); + if (!internalBackupServiceJobDao.lockInLockTable(LOCK, 300)) { + logger.warn("Unable to get lock for compression jobs."); + return; + } + + rescheduleLostJobs(); + + if (!backupCompressionTaskEnabled.value()) { + logger.debug("Backup compression task is disabled. Not running."); + return; + } + + for (DataCenterVO zone : zones) { + if (!isFrameworkEnabledForZone(zone)) { + logger.debug("Backup framework is not enabled for zone [{}], will not run the backup compression task for this zone.", zone.getUuid()); + continue; + } + List jobsToStart = internalBackupServiceJobDao.listWaitingJobsAndScheduledToBeforeNow(zone.getId(), + InternalBackupServiceJobType.StartCompression, InternalBackupServiceJobType.FinalizeCompression); + jobsToStart = filterJobsOfDomainsAndAccountsWithDisabledCompressionTask(jobsToStart); + if (jobsToStart.isEmpty()) { + continue; + } + logger.debug("Found [{}] compression jobs to submit.", jobsToStart.size()); + Pair, Integer> hostToNumberOfExecutingJobsAndTotalExecutingJobs = getHostToNumberOfExecutingJobsAndTotalExecutingJobs(zone, InternalBackupServiceJobType.StartCompression); + List> hostAndNumberOfJobsPairList = filterHostsWithTooManyJobs(hostToNumberOfExecutingJobsAndTotalExecutingJobs.first(), + backupCompressionMaxConcurrentOperationsPerHost); + HashSet busyInstances = submitFinalizeJobsForExecution(jobsToStart, hostAndNumberOfJobsPairList, zone.getId()); + busyInstances.addAll(internalBackupServiceJobDao.listExecutingJobsByZoneIdAndJobType(zone.getId(), InternalBackupServiceJobType.StartCompression).stream(). + map(InternalBackupServiceJobVO::getInstanceId).collect(Collectors.toSet())); + + jobsToStart = thinJobsToStartList(zone, jobsToStart, hostToNumberOfExecutingJobsAndTotalExecutingJobs.second(), backupCompressionMaxConcurrentOperations); + submitQueuedJobsForExecution(jobsToStart, hostAndNumberOfJobsPairList, busyInstances, backupCompressionMaxConcurrentOperationsPerHost, zone.getId()); + } + + ThreadContext.pop(); + } catch (Exception e) { + logger.error("Caught exception [{}] while trying to search and dispatch backup compression jobs.", e.getMessage(), e); + } finally { + internalBackupServiceJobDao.unlockFromLockTable(LOCK); + } + } + + @Override + protected List getLostJobs(ClusterVO clusterVO, Calendar date, List hostVOS) { + date.add(Calendar.SECOND, (int)Math.round(InternalBackupProvider.backupCompressionTimeout.valueIn(clusterVO.getId()) * -RESCHEDULE_TO_TIMEOUT_RATIO)); + List result = internalBackupServiceJobDao.listExecutingJobsByHostsAndStartTimeBeforeAndTypeIn(hostVOS.stream().map(HostVO::getId).toArray(), + date.getTime(), InternalBackupServiceJobType.StartCompression, InternalBackupServiceJobType.FinalizeCompression); + logger.info("Got [{}] lost backup compression jobs.", result.size()); + if (result.isEmpty()) { + return result; + } + logger.debug("Lost backups compression jobs found: {}", result); + return result; + } + + @Override + protected void submitQueuedJob(InternalBackupServiceJobVO job, long zoneId, String logId) { + executor.submit(() -> startBackupCompression(job, zoneId, logId)); + } + + /** + * Submit FinalizeCompression jobs, this should be called before submitStartJobsForExecution. + * */ + protected HashSet submitFinalizeJobsForExecution(List jobsToExecute, List> hostAndNumberOfJobsPairList, long zoneId) { + List submittedJobs = new ArrayList<>(); + HashSet setOfInstancesWithExecutingCompressionJobs = new HashSet<>(); + for (InternalBackupServiceJobVO job : jobsToExecute) { + if (job.getType() != InternalBackupServiceJobType.FinalizeCompression) { + continue; + } + submittedJobs.add(job); + String logId = UuidUtils.first(UUID.randomUUID().toString()); + logger.debug("Dispatching backup compression job [{}{}] with logid:{} for backup [{}].", BACKUP_JOB, job.getId(), logId, job.getBackupId()); + + Pair hostAndNumberOfJobs = hostAndNumberOfJobsPairList.get((int) (Math.random()*hostAndNumberOfJobsPairList.size())); + job.setHostId(hostAndNumberOfJobs.first().getId()); + job.setStartTime(DateUtil.now()); + internalBackupServiceJobDao.update(job); + + setOfInstancesWithExecutingCompressionJobs.add(job.getInstanceId()); + executor.submit(() -> finalizeBackupCompression(job, zoneId, logId)); + } + jobsToExecute.removeAll(submittedJobs); + return setOfInstancesWithExecutingCompressionJobs; + } + + private void startBackupCompression(InternalBackupServiceJobVO job, long zoneId, String logId) { + boolean result = false; + try { + ThreadContext.push(BACKUP_JOB + job.getId()); + ThreadContext.put(LOGCONTEXTID, logId); + result = internalBackupService.startBackupCompression(job.getBackupId(), job.getHostId(), zoneId); + } catch (Exception e) { + logger.error("Caught exception [{}] while trying to compress backup [{}].", e.getMessage(), job.getBackupId(), e); + } finally { + processJobResult(job, result); + ThreadContext.clearAll(); + } + } + + private void finalizeBackupCompression(InternalBackupServiceJobVO job, long zoneId, String logId) { + boolean result = false; + try { + ThreadContext.push(BACKUP_JOB + job.getId()); + ThreadContext.put(LOGCONTEXTID, logId); + result = internalBackupService.finalizeBackupCompression(job.getBackupId(), job.getHostId(), zoneId); + } catch (Exception e) { + logger.error("Caught exception [{}] while trying to finalize backup compression [{}].", e.getMessage(), job.getBackupId(), e); + } finally { + processJobResult(job, result); + ThreadContext.clearAll(); + } + } + + protected List filterJobsOfDomainsAndAccountsWithDisabledCompressionTask(List jobsToFilter) { + ArrayList filteredJobs = new ArrayList<>(); + for (InternalBackupServiceJobVO job : jobsToFilter) { + if (backupCompressionTaskEnabled.valueIn(job.getAccountId())) { + filteredJobs.add(job); + } + } + return filteredJobs; + } + + @Override + protected int getMaxAttempts(InternalBackupServiceJobVO jobVo) { + HostVO hostVO = hostDao.findById(jobVo.getHostId()); + return backupCompressionMaxJobRetries.valueIn(hostVO.getClusterId()); + } + + @Override + protected int getRetryInterval(InternalBackupServiceJobVO jobVo) { + HostVO hostVO = hostDao.findById(jobVo.getHostId()); + return backupCompressionRetryInterval.valueIn(hostVO.getClusterId()); + } + + @Override + public String getConfigComponentName() { + return BackupCompressionServiceJobController.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {backupCompressionMaxConcurrentOperationsPerHost, backupCompressionMaxJobRetries, backupCompressionRetryInterval, backupCompressionTaskEnabled, backupCompressionMaxConcurrentOperations}; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 3c9855ae87a2..9be4c7ea083c 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -35,9 +35,17 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.cloud.host.Host; +import com.cloud.storage.VolumeApiService; +import com.cloud.utils.exception.BackupProviderException; +import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.VirtualMachineManager; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.vm.VmDiskInfo; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; import com.cloud.utils.DomainHelper; import com.cloud.utils.EnumUtils; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -55,6 +63,9 @@ import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.DeleteBackupCmd; import org.apache.cloudstack.api.command.user.backup.DeleteBackupScheduleCmd; +import org.apache.cloudstack.api.command.user.backup.DownloadValidationScreenshotCmd; +import org.apache.cloudstack.api.command.user.backup.FinishBackupChainCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupServiceJobsCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; @@ -62,6 +73,7 @@ import org.apache.cloudstack.api.command.user.backup.RestoreBackupCmd; import org.apache.cloudstack.api.command.user.backup.RestoreVolumeFromBackupAndAttachToVMCmd; import org.apache.cloudstack.api.command.user.backup.UpdateBackupScheduleCmd; +import org.apache.cloudstack.api.command.user.backup.CreateBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.repository.AddBackupRepositoryCmd; import org.apache.cloudstack.api.command.user.backup.repository.DeleteBackupRepositoryCmd; import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd; @@ -134,7 +146,6 @@ import com.cloud.storage.ScopeType; import com.cloud.storage.Storage; import com.cloud.storage.Volume; -import com.cloud.storage.VolumeApiService; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.GuestOSDao; @@ -166,18 +177,13 @@ import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.utils.fsm.NoTransitionException; import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; -import com.cloud.vm.VirtualMachineManager; -import com.cloud.vm.VmDiskInfo; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.dao.VMInstanceDetailsDao; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; public class BackupManagerImpl extends ManagerBase implements BackupManager { @@ -259,6 +265,12 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { private static final List INVALID_BACKUP_STATUS = List.of(Backup.Status.Expunged, Backup.Status.Removed); + public static final String KBOSS_BACKUP_PROVIDER = "kboss"; + + private static List quiesceSupported = List.of("nas", KBOSS_BACKUP_PROVIDER); + + private static List providersThatIgnoreHostAndDatastore = List.of("nas", KBOSS_BACKUP_PROVIDER); + public AsyncJobDispatcher getAsyncJobDispatcher() { return asyncJobDispatcher; } @@ -341,6 +353,76 @@ public List getBackupOfferingDomains(Long offeringId) { return backupOfferingDetailsDao.findDomainIds(offeringId); } + @Override + public BackupOffering createBackupOffering(CreateBackupOfferingCmd cmd) { + validateBackupForZone(cmd.getZoneId()); + if (backupOfferingDao.findByName(cmd.getName(), cmd.getZoneId()) != null) { + throw new CloudRuntimeException("A backup offering with the same name already exists in this zone"); + } + + if (CollectionUtils.isNotEmpty(cmd.getDomainIds())) { + for (final Long domainId: cmd.getDomainIds()) { + if (domainDao.findById(domainId) == null) { + throw new InvalidParameterValueException("Please specify a valid domain ID"); + } + } + } + + List filteredDomainIds = cmd.getDomainIds() == null ? new ArrayList<>() : new ArrayList<>(cmd.getDomainIds()); + if (filteredDomainIds.size() > 1) { + filteredDomainIds = domainHelper.filterChildSubDomains(filteredDomainIds); + } + + final BackupProvider provider = getBackupProvider(cmd.getZoneId()); + if (!KBOSS_BACKUP_PROVIDER.equals(provider.getName())) { + throw new InvalidParameterValueException("Only KBOSS supports this API currently."); + } + + if (!provider.isValidProviderOffering(cmd.getZoneId(), null)) { + throw new CloudRuntimeException(String.format("Backup offering is not valid for provider [%s] in zone [%s]", provider, cmd.getZoneId())); + } + + final BackupOfferingVO offering = new BackupOfferingVO(cmd.getZoneId(), provider.getName(), cmd.getName(), cmd.getDescription(), cmd.getUserDrivenBackups()); + + final BackupOfferingVO savedOffering = backupOfferingDao.persist(offering); + if (savedOffering == null) { + throw new CloudRuntimeException("Unable to create backup offering: " + cmd.getName()); + } + List detailsVOList = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(filteredDomainIds)) { + for (Long domainId : filteredDomainIds) { + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false)); + } + } + if (cmd.isCompress()) { + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.COMPRESS, "true", true)); + } + if (cmd.isValidate()) { + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.VALIDATE, "true", true)); + } + if (cmd.isAllowExtractFile()) { + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.ALLOW_EXTRACT_FILE, "true", true)); + } + if (cmd.isAllowQuickRestore()) { + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.ALLOW_QUICK_RESTORE, "true", true)); + } + if (cmd.getBackupChainSize() != null) { + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.BACKUP_CHAIN_SIZE, cmd.getBackupChainSize().toString(), true)); + } + if (cmd.getCompressionLibrary() != null) { + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.COMPRESSION_LIBRARY, cmd.getCompressionLibrary().name(), true)); + } + if (cmd.getValidationSteps() != null) { + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.VALIDATION_STEPS, cmd.getValidationSteps(), true)); + } + + if (!detailsVOList.isEmpty()) { + backupOfferingDetailsDao.saveDetails(detailsVOList); + } + logger.debug("Successfully created backup offering [{}] mapped to backup provider offering [{}].",cmd.getName(), savedOffering.getUuid()); + return savedOffering; + } + @Override @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_OFFERING_CLONE, eventDescription = "cloning backup offering") public BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd) { @@ -388,28 +470,34 @@ public BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd) { List filteredDomainIds = cmd.getDomainIds() == null ? new ArrayList<>() : new ArrayList<>(cmd.getDomainIds()); Collections.sort(filteredDomainIds); - updateBackupOfferingDomainDetail(savedOffering, filteredDomainIds); + updateBackupOfferingDetails(savedOffering, sourceOffering, filteredDomainIds); logger.debug("Successfully cloned backup offering '" + sourceOffering.getName() + "' (ID: " + cmd.getSourceOfferingId() + ") to '" + cmd.getName() + "' (ID: " + savedOffering.getId() + ")"); return savedOffering; } - private void updateBackupOfferingDomainDetail(BackupOfferingVO savedOffering, List filteredDomainIds) { + private void updateBackupOfferingDetails(BackupOfferingVO savedOffering, BackupOfferingVO sourceOffering, List filteredDomainIds) { if (filteredDomainIds.size() > 1) { filteredDomainIds = domainHelper.filterChildSubDomains(filteredDomainIds); } + List detailsVOList = new ArrayList<>(); if (CollectionUtils.isNotEmpty(filteredDomainIds)) { - List detailsVOList = new ArrayList<>(); for (Long domainId : filteredDomainIds) { if (domainDao.findById(domainId) == null) { throw new InvalidParameterValueException("Please specify a valid domain id"); } detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false)); } - if (!detailsVOList.isEmpty()) { - backupOfferingDetailsDao.saveDetails(detailsVOList); - } + } + + List details = backupOfferingDetailsDao.listDetails(sourceOffering.getId()); + details.removeIf(backupOfferingDetailsVO -> ApiConstants.DOMAIN_ID.equals(backupOfferingDetailsVO.getName())); + details.forEach(detail -> detail.setResourceId(savedOffering.getId())); + detailsVOList.addAll(details); + + if (!detailsVOList.isEmpty()) { + backupOfferingDetailsDao.saveDetails(detailsVOList); } } @@ -718,6 +806,7 @@ public BackupSchedule configureBackupSchedule(CreateBackupScheduleCmd cmd) { final DateUtil.IntervalType intervalType = cmd.getIntervalType(); final String scheduleString = cmd.getSchedule(); final TimeZone timeZone = TimeZone.getTimeZone(cmd.getTimezone()); + boolean isolated = cmd.isIsolated(); if (intervalType == null) { throw new CloudRuntimeException("Invalid interval type provided"); @@ -738,8 +827,12 @@ public BackupSchedule configureBackupSchedule(CreateBackupScheduleCmd cmd) { final int maxBackups = validateAndGetDefaultBackupRetentionIfRequired(cmd.getMaxBackups(), offering, vm); - if (!"nas".equals(offering.getProvider()) && cmd.getQuiesceVM() != null) { - throw new InvalidParameterValueException("Quiesce VM option is supported only for NAS backup provider"); + if (isolated && !KBOSS_BACKUP_PROVIDER.equals(offering.getProvider())) { + throw new InvalidParameterValueException("Isolated backups are only supported by KBOSS backup provider."); + } + + if (!quiesceSupported.contains(offering.getProvider()) && cmd.getQuiesceVM() != null) { + throw new InvalidParameterValueException("Quiesce VM option is supported only by NAS and KBOSS backup providers."); } final String timezoneId = timeZone.getID(); @@ -756,7 +849,8 @@ public BackupSchedule configureBackupSchedule(CreateBackupScheduleCmd cmd) { final BackupScheduleVO schedule = backupScheduleDao.findByVMAndIntervalType(vmId, intervalType); if (schedule == null) { - return backupScheduleDao.persist(new BackupScheduleVO(vmId, intervalType, scheduleString, timezoneId, nextDateTime, maxBackups, cmd.getQuiesceVM(), vm.getAccountId(), vm.getDomainId())); + return backupScheduleDao.persist(new BackupScheduleVO(vmId, intervalType, scheduleString, timezoneId, nextDateTime, maxBackups, cmd.getQuiesceVM(), vm.getAccountId(), + vm.getDomainId(), isolated)); } schedule.setScheduleType((short) intervalType.ordinal()); @@ -765,6 +859,7 @@ public BackupSchedule configureBackupSchedule(CreateBackupScheduleCmd cmd) { schedule.setScheduledTimestamp(nextDateTime); schedule.setMaxBackups(maxBackups); schedule.setQuiesceVM(cmd.getQuiesceVM()); + schedule.setIsolated(isolated); backupScheduleDao.update(schedule.getId(), schedule); return backupScheduleDao.findById(schedule.getId()); } @@ -876,6 +971,7 @@ public boolean deleteBackupSchedule(DeleteBackupScheduleCmd cmd) { throw new InvalidParameterValueException("Could not find the requested backup schedule."); } checkCallerAccessToBackupScheduleVm(schedule.getVmId()); + finalizeBackupScheduleIfNeeded(schedule); return backupScheduleDao.remove(schedule.getId()); } @@ -883,6 +979,33 @@ public boolean deleteBackupSchedule(DeleteBackupScheduleCmd cmd) { return deleteAllVmBackupSchedules(vmId); } + /** + * Terminates the backup schedule if necessary. + * + * @param backupSchedule the backup schedule to be processed for termination. + * @throws CloudRuntimeException if the backup offering associated with the + * virtual machine was not found or if the backup provider could not finalize + * the backup schedule. + */ + protected void finalizeBackupScheduleIfNeeded(BackupSchedule backupSchedule) { + VMInstanceVO vm = findVmById(backupSchedule.getVmId()); + + if (vm.getBackupOfferingId() == null) { + logger.debug("The virtual machine {} backup offering has already been removed; therefore, it is not necessary to finalize the backup schedule.", vm.getUuid()); + return; + } + + BackupOfferingVO backupOffering = backupOfferingDao.findById(vm.getBackupOfferingId()); + if (backupOffering == null) { + throw new CloudRuntimeException("Could not find the backup offering of the backup schedule virtual machine."); + } + + BackupProvider backupProvider = getBackupProvider(backupOffering.getProvider()); + if (!backupProvider.removeVMBackupSchedule(vm, backupSchedule)) { + throw new CloudRuntimeException(String.format("Failed to finalize VM backup schedule with ID [%s].", backupSchedule.getUuid())); + } + } + /** * Checks if the backup framework is enabled for the zone in which the VM with specified ID is allocated and * if the caller has access to the VM. @@ -907,6 +1030,7 @@ protected boolean deleteAllVmBackupSchedules(long vmId) { List vmBackupSchedules = backupScheduleDao.listByVM(vmId); boolean success = true; for (BackupScheduleVO vmBackupSchedule : vmBackupSchedules) { + finalizeBackupScheduleIfNeeded(vmBackupSchedule); success = success && backupScheduleDao.remove(vmBackupSchedule.getId()); } return success; @@ -917,7 +1041,6 @@ protected boolean deleteAllVmBackupSchedules(long vmId) { public boolean createBackup(CreateBackupCmd cmd, Object job) throws ResourceAllocationException { Long vmId = cmd.getVmId(); Account caller = CallContext.current().getCallingAccount(); - final VMInstanceVO vm = findVmById(vmId); validateBackupForZone(vm.getDataCenterId()); accountManager.checkAccess(caller, null, true, vm); @@ -940,8 +1063,8 @@ public boolean createBackup(CreateBackupCmd cmd, Object job) throws ResourceAllo throw new CloudRuntimeException("The assigned backup offering does not allow ad-hoc user backup"); } - if (!"nas".equals(offering.getProvider()) && cmd.getQuiesceVM() != null) { - throw new InvalidParameterValueException("Quiesce VM option is supported only for NAS backup provider"); + if (!quiesceSupported.contains(offering.getProvider()) && cmd.getQuiesceVM() != null) { + throw new InvalidParameterValueException("Quiesce VM option is supported only by NAS and KBOSS backup providers"); } Long backupScheduleId = getBackupScheduleId(job); @@ -973,7 +1096,7 @@ private void createCheckedBackup(CreateBackupCmd cmd, Account owner, boolean isS CheckedReservation backupStorageReservation = new CheckedReservation(owner, Resource.ResourceType.backup_storage, backupSize, reservationDao, resourceLimitMgr)) { - Pair result = backupProvider.takeBackup(vm, cmd.getQuiesceVM()); + Pair result = backupProvider.takeBackup(vm, cmd.getQuiesceVM(), cmd.isIsolated(), backupScheduleId); if (!result.first()) { throw new CloudRuntimeException("Failed to create Instance Backup"); } @@ -1187,11 +1310,11 @@ public Pair, Integer> listBackups(final ListBackupsCmd cmd) { } public boolean importRestoredVM(long zoneId, long domainId, long accountId, long userId, - String vmInternalName, Hypervisor.HypervisorType hypervisorType, Backup backup) { + String vmInternalName, Hypervisor.HypervisorType hypervisorType, Backup backup, BackupOffering offering) { VirtualMachine vm = null; HypervisorGuru guru = hypervisorGuruManager.getGuru(hypervisorType); try { - vm = guru.importVirtualMachineFromBackup(zoneId, domainId, accountId, userId, vmInternalName, backup); + vm = guru.importVirtualMachineFromBackup(zoneId, domainId, accountId, userId, vmInternalName, backup, getBackupProvider(offering.getProvider())); } catch (final Exception e) { logger.error(String.format("Failed to import VM [vmInternalName: %s] from backup restoration [%s] with hypervisor [type: %s] due to: [%s].", vmInternalName, ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "type"), hypervisorType, e.getMessage()), e); @@ -1216,7 +1339,7 @@ public boolean importRestoredVM(long zoneId, long domainId, long accountId, long @Override @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_RESTORE, eventDescription = "restoring Instance from Backup", async = true) - public boolean restoreBackup(final Long backupId) { + public boolean restoreBackup(final Long backupId, boolean quickRestore, Long hostId) { final BackupVO backup = backupDao.findById(backupId); if (backup == null) { throw new CloudRuntimeException("Backup " + backupId + " does not exist"); @@ -1230,38 +1353,62 @@ public boolean restoreBackup(final Long backupId) { if (vm == null || VirtualMachine.State.Expunging.equals(vm.getState())) { throw new CloudRuntimeException("The Instance from which the backup was taken could not be found."); } - accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + Account callerAccount = CallContext.current().getCallingAccount(); + accountManager.checkAccess(callerAccount, null, true, vm); + validateHostIdParameter(hostId, callerAccount); if (vm.getRemoved() == null && !vm.getState().equals(VirtualMachine.State.Stopped) && !vm.getState().equals(VirtualMachine.State.Destroyed)) { throw new CloudRuntimeException("Existing Instance should be stopped before being restored from Backup"); } - // This is done to handle historic backups if any with Veeam / Networker plugins - List backupVolumes = CollectionUtils.isEmpty(backup.getBackedUpVolumes()) ? - vm.getBackupVolumeList() : backup.getBackedUpVolumes(); - List vmVolumes = volumeDao.findByInstance(vm.getId()); - if (vmVolumes.size() != backupVolumes.size()) { - throw new CloudRuntimeException("Unable to restore Instance with the current Backup as the Backup has different number of disks to the Instance"); - } - - BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(vm.getBackupOfferingId()); - String errorMessage = "Failed to find backup offering of the VM backup."; - if (offering == null) { - logger.warn(errorMessage); - } logger.debug("Attempting to get backup offering from VM backup"); - offering = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); if (offering == null) { - throw new CloudRuntimeException(errorMessage); + throw new CloudRuntimeException("Failed to find backup offering of the VM backup."); } + validateBackupVolumes(backup, vm, offering); String backupDetailsInMessage = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "uuid", "externalId", "vmId", "name"); - tryRestoreVM(backup, vm, offering, backupDetailsInMessage); + tryRestoreVM(backup, vm, offering, backupDetailsInMessage, quickRestore, hostId); + + updateStates(vm, getBackupProvider(offering.getProvider()), quickRestore); + + return importRestoredVM(vm.getDataCenterId(), vm.getDomainId(), vm.getAccountId(), vm.getUserId(), + vm.getInstanceName(), vm.getHypervisorType(), backup, offering); + } + + private void validateHostIdParameter(Long hostId, Account callerAccount) { + if (hostId != null && !accountService.isRootAdmin(callerAccount.getId())) { + throw new PermissionDeniedException(String.format("Parameter %s can only be specified by a Root Admin", ApiConstants.HOST_ID)); + } + } + + /** + * Updates the VM and volume states. + * If using quick restore, the states should already be set (the VM should be running). + * Only KBOSS supports this parameter for now; will do nothing if the backup provider is KBOSS and quickRestore is true. + * */ + private void updateStates(VMInstanceVO vm, BackupProvider backupProvider, boolean quickRestore) { + if (KBOSS_BACKUP_PROVIDER.equals(backupProvider.getName()) && quickRestore) { + return; + } updateVolumeState(vm, Volume.Event.RestoreSucceeded, Volume.State.Ready); updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped); + } - return importRestoredVM(vm.getDataCenterId(), vm.getDomainId(), vm.getAccountId(), vm.getUserId(), - vm.getInstanceName(), vm.getHypervisorType(), backup); + protected void validateBackupVolumes(BackupVO backup, VMInstanceVO vm, BackupOffering offering) { + BackupProvider backupProvider = getBackupProvider(offering.getProvider()); + if (KBOSS_BACKUP_PROVIDER.equals(backupProvider.getName())) { + return; + } + // This is done to handle historic backups if any with Veeam / Networker plugins + List backupVolumes = CollectionUtils.isEmpty(backup.getBackedUpVolumes()) ? + vm.getBackupVolumeList() : backup.getBackedUpVolumes(); + List vmVolumes = volumeDao.findByInstance(vm.getId()); + if (vmVolumes.size() != backupVolumes.size()) { + throw new CloudRuntimeException("Unable to restore Instance with the current Backup as the Backup has different number of disks to the Instance"); + } } /** @@ -1271,7 +1418,7 @@ public boolean restoreBackup(final Long backupId) { * * If restore fails, then update the VM state to {@link VirtualMachine.Event#RestoringFailed}, and its volumes to {@link Volume.Event#RestoreFailed} and throw an {@link CloudRuntimeException}. */ - protected void tryRestoreVM(BackupVO backup, VMInstanceVO vm, BackupOffering offering, String backupDetailsInMessage) { + protected void tryRestoreVM(BackupVO backup, VMInstanceVO vm, BackupOffering offering, String backupDetailsInMessage, boolean quickRestore, Long hostId) { try { updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring); updateVolumeState(vm, Volume.Event.RestoreRequested, Volume.State.Restoring); @@ -1281,7 +1428,7 @@ protected void tryRestoreVM(BackupVO backup, VMInstanceVO vm, BackupOffering off true, 0); final BackupProvider backupProvider = getBackupProvider(offering.getProvider()); - if (!backupProvider.restoreVMFromBackup(vm, backup)) { + if (!backupProvider.restoreVMFromBackup(vm, backup, quickRestore, hostId)) { ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_BACKUP_RESTORE, String.format("Failed to restore Instance %s from Backup %s", vm.getInstanceName(), backup.getUuid()), vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),0); @@ -1293,6 +1440,9 @@ protected void tryRestoreVM(BackupVO backup, VMInstanceVO vm, BackupOffering off logger.error(String.format("Failed to restore backup [%s] due to: [%s].", backupDetailsInMessage, e.getMessage()), e); updateVolumeState(vm, Volume.Event.RestoreFailed, Volume.State.Ready); updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped); + if (e instanceof BackupProviderException) { + throw e; + } throw new CloudRuntimeException(String.format("Error restoring Instance from Backup [%s].", backupDetailsInMessage)); } } @@ -1503,7 +1653,7 @@ public Boolean canCreateInstanceFromBackupAcrossZones(final Long backupId) { } @Override - public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws CloudRuntimeException { + public boolean restoreBackupToVM(final Long backupId, final Long vmId, boolean quickRestore) throws CloudRuntimeException { final BackupVO backup = backupDao.findById(backupId); if (backup == null) { throw new CloudRuntimeException("Backup " + backupId + " does not exist"); @@ -1545,6 +1695,10 @@ public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws Cl throw new CloudRuntimeException("Create instance from backup is not supported by the " + offering.getProvider() + " provider."); } + if (quickRestore && !backupProvider.getName().equals(KBOSS_BACKUP_PROVIDER)) { + throw new CloudRuntimeException("Quick restore is only supported by KBOSS."); + } + String backupDetailsInMessage = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "uuid", "externalId", "name"); Pair result = null; Long eventId = null; @@ -1558,12 +1712,12 @@ public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws Cl String host = null; String dataStore = null; - if (!"nas".equals(offering.getProvider())) { + if (!providersThatIgnoreHostAndDatastore.contains(offering.getProvider())) { Pair restoreInfo = getRestoreVolumeHostAndDatastore(vm); host = restoreInfo.first().getPrivateIpAddress(); dataStore = restoreInfo.second().getUuid(); } - result = backupProvider.restoreBackupToVM(vm, backup, host, dataStore); + result = backupProvider.restoreBackupToVM(vm, backup, host, dataStore, quickRestore); } catch (Exception e) { logger.error(String.format("Failed to create Instance [%s] from backup [%s] due to: [%s]", vm.getInstanceName(), backupDetailsInMessage, e.getMessage()), e); @@ -1578,8 +1732,7 @@ public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws Cl throw new CloudRuntimeException(error_msg); } - updateVolumeState(vm, Volume.Event.RestoreSucceeded, Volume.State.Ready); - updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped); + updateStates(vm, backupProvider, quickRestore); ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_VM_CREATE_FROM_BACKUP, String.format("Successfully created Instance %s from backup %s", vm.getInstanceName(), backup.getUuid()), vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),eventId); @@ -1588,7 +1741,8 @@ public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws Cl @Override @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_RESTORE_VOLUME_TO_VM, eventDescription = "restoring Volume from Backup to Instance", async = true) - public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, final Long backupId, final Long vmId) throws Exception { + public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, final Long backupId, final Long vmId, boolean isQuickRestore, + Long hostId) throws Exception { if (StringUtils.isEmpty(backedUpVolumeUuid)) { throw new CloudRuntimeException("Invalid volume ID passed"); } @@ -1602,7 +1756,9 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, validateBackupForZone(backup.getZoneId()); final VMInstanceVO vm = findVmById(vmId); - accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + Account callerAccount = CallContext.current().getCallingAccount(); + accountManager.checkAccess(callerAccount, null, true, vm); + validateHostIdParameter(hostId, callerAccount); if (vm.getBackupOfferingId() != null && !BackupEnableAttachDetachVolumes.value()) { throw new CloudRuntimeException("The selected Instance is attached to a backup offering and, thus, it is not possible to restore and attach Volumes from backups to the Instance."); @@ -1613,8 +1769,8 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, } List volumeInfoList = backup.getBackedUpVolumes(); + final VMInstanceVO vmFromBackup = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); if (volumeInfoList == null) { - final VMInstanceVO vmFromBackup = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); if (vmFromBackup == null) { throw new CloudRuntimeException("Instance reference for the provided Instance backup not found"); } else if (vmFromBackup == null || vmFromBackup.getBackupVolumeList() == null) { @@ -1627,19 +1783,26 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, throw new CloudRuntimeException("Failed to find volume with Id " + backedUpVolumeUuid + " in the backed-up volumes metadata"); } - accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vmFromBackup); final BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); if (offering == null) { throw new CloudRuntimeException("Failed to find Instance Backup Offering"); } + if (!StringUtils.equals(KBOSS_BACKUP_PROVIDER, offering.getProvider()) && !VirtualMachine.PowerState.PowerOff.equals(vm.getPowerState())) { + throw new CloudRuntimeException(String.format("VM [%s] needs to be powered off to restore the volume [%s].", vm.getUuid(), backedUpVolumeUuid)); + } + BackupProvider backupProvider = getBackupProvider(offering.getProvider()); - VolumeVO backedUpVolume = volumeDao.findByUuid(backedUpVolumeUuid); + VolumeVO backedUpVolume = volumeDao.findByUuidIncludingRemoved(backedUpVolumeUuid); Pair restoreInfo; - if (!"nas".equals(offering.getProvider()) || (backedUpVolume == null)) { - restoreInfo = getRestoreVolumeHostAndDatastore(vm); - } else { + + if ("nas".equals(offering.getProvider()) && backedUpVolume != null) { restoreInfo = getRestoreVolumeHostAndDatastoreForNas(vm, backedUpVolume); + } else if (KBOSS_BACKUP_PROVIDER.equals(offering.getProvider())) { + restoreInfo = getRestoreVolumeHostAndDatastoreForKboss(vm, backedUpVolume, isQuickRestore, hostId); + } else { + restoreInfo = getRestoreVolumeHostAndDatastore(vm); } HostVO host = restoreInfo.first(); @@ -1653,21 +1816,21 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, String[] hostPossibleValues = {host.getPrivateIpAddress(), host.getName()}; String[] datastoresPossibleValues = {datastore.getUuid(), datastore.getName()}; - Pair result = restoreBackedUpVolume(backupVolumeInfo, backup, backupProvider, hostPossibleValues, datastoresPossibleValues, vm); + Pair result = restoreBackedUpVolume(backupVolumeInfo, backup, backupProvider, hostPossibleValues, datastoresPossibleValues, vm, isQuickRestore); if (BooleanUtils.isFalse(result.first())) { throw new CloudRuntimeException(String.format("Error restoring Volume [%s] of Instance [%s] to host [%s] using backup provider [%s] due to: [%s].", backedUpVolumeUuid, vm.getUuid(), host.getUuid(), backupProvider.getName(), result.second())); } if (!attachVolumeToVM(vm.getDataCenterId(), result.second(), backupVolumeInfo, - backedUpVolumeUuid, vm, datastore.getUuid(), backup)) { + backedUpVolumeUuid, vm, datastore.getUuid(), backup, backupProvider)) { throw new CloudRuntimeException(String.format("Error attaching Volume [%s] to Instance [%s].", backedUpVolumeUuid, vm.getUuid())); } return true; } protected Pair restoreBackedUpVolume(final Backup.VolumeInfo backupVolumeInfo, final BackupVO backup, - BackupProvider backupProvider, String[] hostPossibleValues, String[] datastoresPossibleValues, VMInstanceVO vm) { + BackupProvider backupProvider, String[] hostPossibleValues, String[] datastoresPossibleValues, VMInstanceVO vm, boolean quickRestore) { Pair result = new Pair<>(false, ""); for (String hostData : hostPossibleValues) { for (String datastoreData : datastoresPossibleValues) { @@ -1675,7 +1838,7 @@ protected Pair restoreBackedUpVolume(final Backup.VolumeInfo ba backupVolumeInfo.getUuid(), hostData, datastoreData)); try { - result = backupProvider.restoreBackedUpVolume(backup, backupVolumeInfo, hostData, datastoreData, new Pair<>(vm.getName(), vm.getState())); + result = backupProvider.restoreBackedUpVolume(backup, backupVolumeInfo, hostData, datastoreData, new Pair<>(vm.getName(), vm.getState()), vm, quickRestore); if (BooleanUtils.isTrue(result.first())) { return result; @@ -1683,6 +1846,12 @@ protected Pair restoreBackedUpVolume(final Backup.VolumeInfo ba } catch (Exception e) { logger.debug(String.format("Failed to restore volume [UUID: %s], using host [%s] and datastore [%s] due to: [%s].", backupVolumeInfo.getUuid(), hostData, datastoreData, e.getMessage()), e); + if (e instanceof BackupProviderException) { + throw e; + } + if (KBOSS_BACKUP_PROVIDER.equals(backupProvider.getName())) { + return result; + } } } } @@ -1782,6 +1951,30 @@ private Pair getRestoreVolumeHostAndDatastoreForNas(VMIns return new Pair<>(hostVO, storagePoolVO); } + private Pair getRestoreVolumeHostAndDatastoreForKboss(VMInstanceVO vm, VolumeVO backedVolume, boolean quickRestore, Long hostId) { + StoragePoolVO storagePool = primaryDataStoreDao.findById(backedVolume.getPoolId()); + if (vm.getHostId() != null) { + hostId = vm.getHostId(); + } else if (hostId == null || !quickRestore) { + if (vm.getLastHostId() != null) { + hostId = vm.getLastHostId(); + } else { + if (storagePool == null) { + throw new InvalidParameterValueException(String.format("Storage pool of volume [%s] was not found.", backedVolume.getUuid())); + } + List listHost = + hostDao.listAllUpAndEnabledNonHAHosts(Host.Type.Routing, storagePool.getClusterId(), storagePool.getPodId(), storagePool.getDataCenterId(), null); + return new Pair<>(listHost.stream().findFirst().orElseThrow(() -> new CloudRuntimeException(String.format("Unable to find a host to restore backup for VM " + + "[%s].", vm.getUuid()))), null); + } + } + if (hostId == null) { + throw new InvalidParameterValueException(String.format("No host found to quick restore VM [%s]. Please check the logs.", vm.getUuid())); + } + + return new Pair<>(hostDao.findById(hostId), storagePool); + } + /** * Find a host from storage pool access */ @@ -1801,14 +1994,14 @@ private HostVO getFirstHostFromStoragePool(StoragePoolVO storagePoolVO) { * Attach volume to VM */ private boolean attachVolumeToVM(Long zoneId, String restoredVolumeLocation, Backup.VolumeInfo backupVolumeInfo, - String volumeUuid, VMInstanceVO vm, String datastoreUuid, Backup backup) throws Exception { + String volumeUuid, VMInstanceVO vm, String datastoreUuid, Backup backup, BackupProvider backupProvider) throws Exception { HypervisorGuru guru = hypervisorGuruManager.getGuru(vm.getHypervisorType()); backupVolumeInfo.setType(Volume.Type.DATADISK); logger.info("Attaching the restored Volume {} to Instance {}.", () -> ReflectionToStringBuilder.toString(backupVolumeInfo, ToStringStyle.JSON_STYLE), () -> vm); StoragePoolVO pool = primaryDataStoreDao.findByUuid(datastoreUuid); try { - return guru.attachRestoredVolumeToVirtualMachine(zoneId, restoredVolumeLocation, backupVolumeInfo, vm, pool.getId(), backup); + return guru.attachRestoredVolumeToVirtualMachine(zoneId, restoredVolumeLocation, backupVolumeInfo, vm, pool.getId(), backup, backupProvider); } catch (Exception e) { throw new CloudRuntimeException("Error attach restored Volume to Instance " + vm.getUuid() + " due to: " + e.getMessage()); } @@ -1855,6 +2048,7 @@ public BackupProvider getBackupProvider(final Long zoneId) { return getBackupProvider(name); } + @Override public BackupProvider getBackupProvider(final String name) { if (StringUtils.isEmpty(name)) { throw new CloudRuntimeException("Invalid backup provider name provided"); @@ -1900,6 +2094,10 @@ public List> getCommands() { cmdList.add(ListBackupRepositoriesCmd.class); cmdList.add(CreateVMFromBackupCmd.class); cmdList.add(CreateVMFromBackupCmdByAdmin.class); + cmdList.add(CreateBackupOfferingCmd.class); + cmdList.add(DownloadValidationScreenshotCmd.class); + cmdList.add(ListBackupServiceJobsCmd.class); + cmdList.add(FinishBackupChainCmd.class); return cmdList; } @@ -2059,6 +2257,7 @@ public void scheduleBackups() { if (quiesceVm != null) { params.put(ApiConstants.QUIESCE_VM, "" + quiesceVm.toString()); } + params.put(ApiConstants.ISOLATED, String.valueOf(backupSchedule.isIsolated())); params.put("ctxUserId", "1"); params.put("ctxAccountId", "" + vm.getAccountId()); params.put("ctxStartEventId", String.valueOf(eventId)); @@ -2454,6 +2653,11 @@ public BackupResponse createBackupResponse(Backup backup, Boolean listVmDetails) response.setProtectedSize(backup.getProtectedSize()); response.setStatus(backup.getStatus()); response.setIntervalType("MANUAL"); + response.setCompressionStatus(backup.getCompressionStatus()); + if (backup.getUncompressedSize() != null && backup.getUncompressedSize() > 0) { + response.setUncompressedSize(backup.getUncompressedSize()); + } + response.setValidationStatus(backup.getValidationStatus()); if (backup.getBackupScheduleId() != null) { BackupScheduleVO scheduleVO = backupScheduleDao.findById(backup.getBackupScheduleId()); if (scheduleVO != null) { diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupValidationServiceJobController.java b/server/src/main/java/org/apache/cloudstack/backup/BackupValidationServiceJobController.java new file mode 100644 index 000000000000..c3594327a6d1 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupValidationServiceJobController.java @@ -0,0 +1,217 @@ +//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 +//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.backup; + +import com.cloud.dc.ClusterVO; +import com.cloud.dc.DataCenterVO; +import com.cloud.host.HostVO; +import com.cloud.utils.Pair; +import com.cloud.utils.concurrency.NamedThreadFactory; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.logging.log4j.ThreadContext; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class BackupValidationServiceJobController extends InternalBackupServiceJobController implements Configurable { + + private static final String LOCK = "validation_lock"; + + protected ConfigKey backupValidationMaxConcurrentOperationsPerHost = new ConfigKey<>("Advanced", Integer.class, + "backup.validation.max.concurrent.operations.per.host", "1", "Determines the maximum number of concurrent backup validations per host. Values lower than 0 remove" + + " the limit, meaning that as many validations as possible will be done at the same time.", true, ConfigKey.Scope.Cluster); + + protected ConfigKey backupValidationMaxConcurrentOperations = new ConfigKey<>("Advanced", Integer.class, + "backup.validation.max.concurrent.operations", "10", "Determines the maximum number of concurrent backup validations in the zone. Values lower than 1 remove" + + " the limit, meaning that as many validations as possible will be done at the same time.", true, ConfigKey.Scope.Zone); + + protected ConfigKey backupValidationInterval = new ConfigKey<>("Advanced", Integer.class, + "backup.validation.interval", "24", "Determines the period (in hours) between two backup validations for the same backup.", true, ConfigKey.Scope.Account); + + protected ConfigKey backupValidationMaxJobRetries = new ConfigKey<>("Advanced", Integer.class, + "backup.validation.max.job.retries", "2", "Determines the maximum number of retries for backup validation jobs. This includes both start validation jobs and " + + "finalize validation jobs.", true, ConfigKey.Scope.Account); + + protected ConfigKey backupValidationRetryInterval = new ConfigKey<>("Advanced", Integer.class, + "backup.validation.retry.interval", "60", "Determines the minimum amount of time (in minutes) to retry a backup validation job. This includes both start " + + "validation jobs and finalize validation jobs.", true, ConfigKey.Scope.Account); + + protected ConfigKey backupValidationTaskEnabled = new ConfigKey<>("Advanced", Boolean.class, "backup.validation.task.enabled", "true", "Whether the backup " + + "validation task should be running or not. Please set this to false and wait for any validation jobs to finish before restarting the Management Server.", true, + ConfigKey.Scope.Account); + + public static ConfigKey backupValidationBootDefaultTimeout = new ConfigKey<>("Advanced", Integer.class, "backup.validation.boot.default.timeout", "240", + "Default timeout, in seconds, to wait for the validation VM to boot.", true, ConfigKey.Scope.Account); + + public static ConfigKey backupValidationScriptDefaultTimeout = new ConfigKey<>("Advanced", Integer.class, "backup.validation.script.default.timeout", "60", + "Default timeout, in seconds, to wait for the validation script to finish.", true, ConfigKey.Scope.Account); + + public static ConfigKey backupValidationScreenshotDefaultWait = new ConfigKey<>("Advanced", Integer.class, "backup.validation.screenshot.default.wait", "60", + "Default period to wait, in seconds, to wait before taking a screenshot of the validating VM.", true, ConfigKey.Scope.Account); + + public static ConfigKey backupValidationEndChainOnFail = new ConfigKey<>("Advanced", Boolean.class, "backup.validation.end.chain.on.fail", "true", + "Whether to end the backup chain when the validation fails.", true, ConfigKey.Scope.Account); + + private ExecutorService executor; + + private ScheduledExecutorService scheduledExecutor; + + @Inject + private InternalBackupService internalBackupService; + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + + executor = Executors.newCachedThreadPool(new NamedThreadFactory("BackupValidationTask")); + scheduledExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("BackupValidationScheduler")); + scheduledExecutor.scheduleAtFixedRate(this::run, 60, 60, TimeUnit.SECONDS); + this.controllerType = "validation"; + return true; + } + + @Override + protected void searchAndDispatchJobs() { + try { + List zones = dataCenterDao.listEnabledZones(); + if (!internalBackupServiceJobDao.lockInLockTable(LOCK, 300)) { + logger.warn("Unable to get lock for validation jobs."); + return; + } + + rescheduleLostJobs(); + + if (!backupValidationTaskEnabled.value()) { + logger.debug("Backup validation task is disabled. Not running."); + return; + } + + for (DataCenterVO zone : zones) { + if (!isFrameworkEnabledForZone(zone)) { + logger.debug("Backup framework is not enabled for zone [{}], will not run the backup validation task for this zone.", zone.getUuid()); + continue; + } + List jobsToStart = internalBackupServiceJobDao.listWaitingJobsAndScheduledToBeforeNow(zone.getId(), InternalBackupServiceJobType.BackupValidation); + jobsToStart = filterJobsOfDomainsAndAccountsWithDisabledValidationTask(jobsToStart); + if (jobsToStart.isEmpty()) { + continue; + } + logger.debug("Found [{}] validation jobs to submit.", jobsToStart.size()); + Pair, Integer> hostToNumberOfExecutingJobsAndTotalExecutingJobs = getHostToNumberOfExecutingJobsAndTotalExecutingJobs(zone, InternalBackupServiceJobType.BackupValidation); + jobsToStart = thinJobsToStartList(zone, jobsToStart, hostToNumberOfExecutingJobsAndTotalExecutingJobs.second(), backupValidationMaxConcurrentOperations); + + List> hostAndNumberOfJobsPairList = filterHostsWithTooManyJobs(hostToNumberOfExecutingJobsAndTotalExecutingJobs.first(), backupValidationMaxConcurrentOperationsPerHost); + Set busyInstances = internalBackupServiceJobDao.listExecutingJobsByZoneIdAndJobType(zone.getId(), InternalBackupServiceJobType.BackupValidation) + .stream().map(InternalBackupServiceJobVO::getInstanceId).collect(Collectors.toSet()); + + submitQueuedJobsForExecution(jobsToStart, hostAndNumberOfJobsPairList, busyInstances, backupValidationMaxConcurrentOperationsPerHost, zone.getId()); + } + + ThreadContext.pop(); + } catch (Exception e) { + logger.error("Caught exception [{}] while trying to search and dispatch backup validation jobs.", e.getMessage(), e); + } finally { + internalBackupServiceJobDao.unlockFromLockTable(LOCK); + } + } + + @Override + protected void submitQueuedJob(InternalBackupServiceJobVO job, long zoneId, String logId) { + executor.submit(() -> startBackupValidation(job, zoneId, logId)); + } + + @Override + protected List getLostJobs(ClusterVO clusterVO, Calendar date, List hostVOS) { + date.add(Calendar.SECOND, (int)Math.round(InternalBackupProvider.backupValidationTimeout.valueIn(clusterVO.getId()) * -RESCHEDULE_TO_TIMEOUT_RATIO)); + List result = internalBackupServiceJobDao.listExecutingJobsByHostsAndStartTimeBeforeAndTypeIn(hostVOS.stream().map(HostVO::getId).toArray(), + date.getTime(), InternalBackupServiceJobType.BackupValidation); + logger.info("Got [{}] lost backup validation jobs.", result.size()); + if (result.isEmpty()) { + return result; + } + logger.debug("Lost backups validation jobs found: {}", result); + return result; + } + + private void startBackupValidation(InternalBackupServiceJobVO job, long zoneId, String logId) { + boolean result = false; + try { + ThreadContext.push(BACKUP_JOB + job.getId()); + ThreadContext.put(LOGCONTEXTID, logId); + result = internalBackupService.validateBackup(job.getBackupId(), job.getHostId(), zoneId); + if (result) { + scheduleNextValidation(job); + } + } catch (Exception e) { + logger.error("Caught exception [{}] while trying to validate backup [{}].", e.getMessage(), job.getBackupId(), e); + } finally { + processJobResult(job, result); + ThreadContext.clearAll(); + } + } + + protected List filterJobsOfDomainsAndAccountsWithDisabledValidationTask(List jobsToFilter) { + ArrayList filteredJobs = new ArrayList<>(); + for (InternalBackupServiceJobVO job : jobsToFilter) { + if (backupValidationTaskEnabled.valueIn(job.getAccountId())) { + filteredJobs.add(job); + } + } + return filteredJobs; + } + + private void scheduleNextValidation(InternalBackupServiceJobVO job) { + Calendar nextValidation = Calendar.getInstance(); + nextValidation.add(Calendar.HOUR, backupValidationInterval.valueIn(job.getAccountId())); + internalBackupServiceJobDao.persist(new InternalBackupServiceJobVO(job.getBackupId(), job.getZoneId(), job.getInstanceId(), job.getAccountId(), + InternalBackupServiceJobType.BackupValidation, nextValidation.getTime())); + } + + @Override + public String getConfigComponentName() { + return BackupValidationServiceJobController.class.getSimpleName(); + } + + @Override + protected int getMaxAttempts(InternalBackupServiceJobVO jobVo) { + return backupValidationMaxJobRetries.valueIn(jobVo.getAccountId()); + } + + @Override + protected int getRetryInterval(InternalBackupServiceJobVO jobVo) { + return backupValidationRetryInterval.valueIn(jobVo.getAccountId()); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {backupValidationInterval, backupValidationTaskEnabled, backupValidationMaxConcurrentOperationsPerHost, backupValidationBootDefaultTimeout, + backupValidationScriptDefaultTimeout, backupValidationScreenshotDefaultWait, backupValidationEndChainOnFail, backupValidationMaxConcurrentOperations, + backupValidationMaxJobRetries, backupValidationRetryInterval}; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceImpl.java b/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceImpl.java new file mode 100644 index 000000000000..5c30188a8c41 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceImpl.java @@ -0,0 +1,370 @@ +// +// 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.backup; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.DataTO; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Storage; +import com.cloud.storage.Upload; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; +import com.cloud.utils.ReflectionUse; +import com.cloud.utils.component.ComponentLifecycleBase; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VmWork; +import com.cloud.vm.VmWorkDeleteBackup; +import com.cloud.vm.VmWorkJobHandler; +import com.cloud.vm.VmWorkJobHandlerProxy; +import com.cloud.vm.VmWorkRestoreBackup; +import com.cloud.vm.VmWorkRestoreVolumeBackupAndAttach; +import com.cloud.vm.VmWorkTakeBackup; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.snapshot.VMSnapshot; +import org.apache.cloudstack.api.response.ExtractResponse; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.backup.dao.InternalBackupStoragePoolDao; +import org.apache.cloudstack.backup.to.BackupScreenshotObject; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.jobs.JobInfo; +import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.command.RevertSnapshotCommand; +import org.apache.cloudstack.storage.datastore.db.ImageStoreObjectDownloadDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreObjectDownloadVO; +import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; +import org.apache.cloudstack.backup.to.BackupScreenshotTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.inject.Inject; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class InternalBackupServiceImpl extends ComponentLifecycleBase implements InternalBackupService, VmWorkJobHandler { + protected Logger logger = LogManager.getLogger(getClass()); + + @Inject + private InternalBackupStoragePoolDao internalBackupStoragePoolDao; + + @Inject + private BackupManager backupManager; + @Inject + private BackupDao backupDao; + @Inject + private AsyncJobManager jobManager; + @Inject + private UserVmDao userVmDao; + @Inject + private VirtualMachineManager virtualMachineManager; + @Inject + private VolumeDao volumeDao; + @Inject + private InternalBackupJoinDao internalBackupJoinDao; + @Inject + private BackupDetailsDao backupDetailDao; + + @Inject + private ImageStoreObjectDownloadDao imageStoreObjectDownloadDao; + + @Inject + private DataStoreManager dataStoreMgr; + + private VmWorkJobHandlerProxy jobHandlerProxy = new VmWorkJobHandlerProxy(this); + private HashMap internalBackupProviderMap = new HashMap<>(); + private List internalBackupProviders; + + public void setInternalBackupProviders(final List internalBackupProviders) { + this.internalBackupProviders = internalBackupProviders; + } + + @Override + public boolean start() { + super.start(); + + if (internalBackupProviders != null) { + for (InternalBackupProvider internalBackupProvider : internalBackupProviders) { + internalBackupProviderMap.put(internalBackupProvider.getName().toLowerCase(), internalBackupProvider); + } + } + return true; + } + + @Override + public void configureChainInfo(DataTO volumeTo, Command cmd) { + if (!(volumeTo instanceof VolumeObjectTO)) { + return; + } + VolumeObjectTO volumeObjectTO = (VolumeObjectTO) volumeTo; + List backupDeltas = internalBackupStoragePoolDao.listByVolumeId(volumeObjectTO.getVolumeId()); + if (backupDeltas.isEmpty()) { + return; + } + volumeObjectTO.setDeltasToRemove(backupDeltas.stream().map(InternalBackupStoragePoolVO::getBackupDeltaParentPath).collect(Collectors.toSet())); + if (cmd instanceof DeleteCommand) { + ((DeleteCommand) cmd).setDeleteChain(true); + } else if (cmd instanceof RevertSnapshotCommand) { + ((RevertSnapshotCommand) cmd).setDeleteChain(true); + } else { + return; + } + logger.debug("Configured chain info for volume [{}]. Set it as [{}].", volumeObjectTO.getUuid(), volumeObjectTO.getChainInfo()); + } + + @Override + public void cleanupBackupMetadata(long volumeId) { + logger.debug("Cleaning up backup metadata for volume [{}].", volumeId); + List currents = internalBackupJoinDao.listCurrentsByVolumeIdDesc(volumeId); + if (currents.isEmpty()) { + return; + } + internalBackupStoragePoolDao.expungeByVolumeId(volumeId); + for (InternalBackupJoinVO current : currents) { + if (CollectionUtils.isNotEmpty(internalBackupStoragePoolDao.listByBackupId(current.getId()))) { + continue; + } + + logger.debug("Volume [{}] was the last volume with deltas in backup [{}]. Setting the backup as END_OF_CHAIN and not current.", volumeId, current.getUuid()); + backupDetailDao.removeDetail(current.getId(), BackupDetailsDao.CURRENT); + if (!current.getEndOfChain()) { + backupDetailDao.persist(new BackupDetailVO(current.getId(), BackupDetailsDao.END_OF_CHAIN, Boolean.TRUE.toString(), true)); + } + } + + } + + + @Override + public void prepareVolumeForDetach(Volume volume, VirtualMachine virtualMachine) { + if (isBackupFrameworkDisabled(virtualMachine)) { + return; + } + + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(virtualMachine.getDataCenterId()); + if (internalBackupProvider == null) { + return; + } + internalBackupProvider.prepareVolumeForDetach(volume, virtualMachine); + } + + @Override + public void prepareVolumeForMigration(Volume volume) { + if (volume.getInstanceId() == null) { + return; + } + VirtualMachine virtualMachine = virtualMachineManager.findById(volume.getInstanceId()); + if (isBackupFrameworkDisabled(virtualMachine)) { + return; + } + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(volume.getDataCenterId()); + if (internalBackupProvider == null) { + return; + } + internalBackupProvider.prepareVolumeForMigration(volume, virtualMachine); + } + + @Override + public void updateVolumeId(long oldVolumeId, long newVolumeId) { + VolumeVO volumeVO = volumeDao.findById(newVolumeId); + if (volumeVO.getInstanceId() == null) { + return; + } + VirtualMachine virtualMachine = virtualMachineManager.findById(volumeVO.getInstanceId()); + if (isBackupFrameworkDisabled(virtualMachine)) { + return; + } + + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(virtualMachine.getDataCenterId()); + if (internalBackupProvider == null) { + return; + } + internalBackupProvider.updateVolumeId(virtualMachine, oldVolumeId, newVolumeId); + } + + @Override + public void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot) { + VirtualMachine virtualMachine = virtualMachineManager.findById(vmSnapshot.getVmId()); + if (isBackupFrameworkDisabled(virtualMachine)) { + return; + } + + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(virtualMachine.getDataCenterId()); + if (internalBackupProvider == null) { + return; + } + internalBackupProvider.prepareVmForSnapshotRevert(vmSnapshot, virtualMachine); + } + + /** + * Ask the backup provider to get the necessary secondary storages that must be mounted at VM start. + *
+ * Note: This is currently only used for Backup Validation VMs. As they are created with backing files that are on secondary storage. + * */ + @Override + public Set getSecondaryStorageUrls(UserVm userVm) { + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(userVm.getDataCenterId()); + if (internalBackupProvider == null) { + return Set.of(); + } + return internalBackupProvider.getSecondaryStorageUrls(userVm); + } + + @Override + public boolean startBackupCompression(long backupId, long hostId, long zoneId) { + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(zoneId); + if (internalBackupProvider == null) { + return false; + } + return internalBackupProvider.startBackupCompression(backupId, hostId); + } + + @Override + public boolean finalizeBackupCompression(long backupId, long hostId, long zoneId) { + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(zoneId); + if (internalBackupProvider == null) { + return false; + } + return internalBackupProvider.finalizeBackupCompression(backupId, hostId); + } + + @Override + public boolean validateBackup(long backupId, long hostId, long zoneId) { + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(zoneId); + if (internalBackupProvider == null) { + return false; + } + return internalBackupProvider.validateBackup(backupId, hostId); + } + + @Override + public ExtractResponse downloadScreenshot(long backupId) { + BackupDetailVO screenshotPathDetail = backupDetailDao.findDetail(backupId, BackupDetailsDao.SCREENSHOT_PATH); + ExtractResponse response = new ExtractResponse(); + if (screenshotPathDetail == null) { + response.setState(Upload.Status.DOWNLOAD_URL_NOT_CREATED.toString()); + return response; + } + BackupDetailVO imageStoreId = backupDetailDao.findDetail(backupId, BackupDetailsDao.IMAGE_STORE_ID); + ImageStoreEntity imageStore = (ImageStoreEntity) dataStoreMgr.getDataStore(Long.parseLong(imageStoreId.getValue()), DataStoreRole.Image); + String screenshotPath = screenshotPathDetail.getValue(); + ImageStoreObjectDownloadVO imageStoreObj = imageStoreObjectDownloadDao.findByStoreIdAndPath(Long.parseLong(imageStoreId.getValue()), screenshotPath); + + if (imageStoreObj == null) { + BackupScreenshotTO dataTo = new BackupScreenshotTO(imageStore.getTO(), Hypervisor.HypervisorType.KVM, screenshotPath); + BackupScreenshotObject objectTo = new BackupScreenshotObject(dataTo, imageStore); + String downloadUrl = imageStore.createEntityExtractUrl(screenshotPath, Storage.ImageFormat.PNG, objectTo); + imageStoreObj = imageStoreObjectDownloadDao.persist(new ImageStoreObjectDownloadVO(imageStore.getId(), screenshotPath, downloadUrl)); + } + + if (imageStoreObj != null) { + response.setUrl(imageStoreObj.getDownloadUrl()); + response.setName(screenshotPath.substring(screenshotPath.lastIndexOf("/") + 1)); + response.setState(Upload.Status.DOWNLOAD_URL_CREATED.toString()); + } else { + response.setState(Upload.Status.DOWNLOAD_URL_NOT_CREATED.toString()); + } + return response; + } + + @Override + public boolean finishBackupChain(long vmId) { + VirtualMachine vm = virtualMachineManager.findById(vmId); + if (vm == null) { + throw new InvalidParameterValueException(String.format("Unable to find VM with ID [%s].", vmId)); + } + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(vm.getDataCenterId()); + if (internalBackupProvider == null) { + return false; + } + return internalBackupProvider.finishBackupChains(vm); + } + + @Override + public Pair handleVmWorkJob(VmWork work) throws Exception { + return jobHandlerProxy.handleVmWorkJob(work); + } + + @ReflectionUse + public Pair orchestrateTakeBackup(VmWorkTakeBackup work) { + BackupVO backupVO = backupDao.findById(work.getBackupId()); + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(backupVO.getZoneId()); + if (internalBackupProvider == null) { + return new Pair<>(JobInfo.Status.FAILED, jobManager.marshallResultObject(Boolean.FALSE)); + } + return new Pair<>(JobInfo.Status.SUCCEEDED, jobManager.marshallResultObject(internalBackupProvider.orchestrateTakeBackup(backupVO, work.isQuiesceVm(), work.isIsolated()))); + } + + @ReflectionUse + public Pair orchestrateDeleteBackup(VmWorkDeleteBackup work) { + BackupVO backupVO = backupDao.findById(work.getBackupId()); + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(backupVO.getZoneId()); + if (internalBackupProvider == null) { + return new Pair<>(JobInfo.Status.FAILED, jobManager.marshallResultObject(Boolean.FALSE)); + } + return new Pair<>(JobInfo.Status.SUCCEEDED, jobManager.marshallResultObject(internalBackupProvider.orchestrateDeleteBackup(backupVO, work.isForced()))); + } + + @ReflectionUse + public Pair orchestrateRestoreVMFromBackup(VmWorkRestoreBackup work) { + BackupVO backupVO = backupDao.findById(work.getBackupId()); + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(backupVO.getZoneId()); + if (internalBackupProvider == null) { + return new Pair<>(JobInfo.Status.FAILED, jobManager.marshallResultObject(Boolean.FALSE)); + } + return new Pair<>(JobInfo.Status.SUCCEEDED, jobManager.marshallResultObject(internalBackupProvider.orchestrateRestoreVMFromBackup(backupVO, + userVmDao.findById(work.getVmId()), work.isQuickRestore(), work.getHostId(), true))); + } + + @ReflectionUse + public Pair orchestrateRestoreBackupVolumeAndAttachToVM(VmWorkRestoreVolumeBackupAndAttach work) { + BackupVO backupVO = backupDao.findById(work.getBackupId()); + InternalBackupProvider internalBackupProvider = getInternalBackupProviderForZone(backupVO.getZoneId()); + if (internalBackupProvider == null) { + return new Pair<>(JobInfo.Status.FAILED, jobManager.marshallResultObject(Boolean.FALSE)); + } + return new Pair<>(JobInfo.Status.SUCCEEDED, jobManager.marshallResultObject(internalBackupProvider.orchestrateRestoreBackedUpVolume(backupVO, userVmDao.findById(work.getVmId()), + work.getBackupVolumeInfo(), work.getHostIp(), work.isQuickRestore()))); + } + + protected InternalBackupProvider getInternalBackupProviderForZone(long zoneId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback)status -> { + BackupProvider backupProvider = backupManager.getBackupProvider(zoneId); + return internalBackupProviderMap.get(backupProvider.getName()); + }); + } + + protected boolean isBackupFrameworkDisabled(VirtualMachine virtualMachine) { + return !BackupManager.BackupFrameworkEnabled.valueIn(virtualMachine.getDataCenterId()); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobController.java b/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobController.java new file mode 100644 index 000000000000..6c55c078f572 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobController.java @@ -0,0 +1,307 @@ +//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 +//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.backup; + +import com.cloud.dc.ClusterVO; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.resource.ResourceState; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.UuidUtils; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackNoReturn; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.db.TransactionStatus; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.InternalBackupServiceJobDao; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.logging.log4j.ThreadContext; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/** + * Abstract class that implements most of the native backup services logic. Classes that implement this one should only implement service specific logic. + * */ +public abstract class InternalBackupServiceJobController extends ManagerBase { + + protected static final String LOGCONTEXTID = "logcontextid"; + + @Inject + protected AsyncJobManager asyncJobManager; + + @Inject + protected ClusterDao clusterDao; + + @Inject + protected DataCenterDao dataCenterDao; + + @Inject + protected HostDao hostDao; + + @Inject + protected BackupDao backupDao; + + @Inject + private VMInstanceDao instanceDao; + + @Inject + protected InternalBackupServiceJobDao internalBackupServiceJobDao; + + protected String controllerType = "abstract"; + + protected static final String BACKUP_JOB = "backup-service-job-"; + + protected static final double RESCHEDULE_TO_TIMEOUT_RATIO = 2.5; + + protected void run() { + ThreadContext.put(LOGCONTEXTID, UuidUtils.first(UUID.randomUUID().toString())); + logger.debug("Searching for {} jobs to dispatch.", controllerType); + + if (!asyncJobManager.isAsyncJobsEnabled()) { + logger.debug("A management shutdown has been triggered. Not running {} task.", controllerType); + return; + } + + Transaction.execute(TransactionLegacy.CLOUD_DB, new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + searchAndDispatchJobs(); + } + }); + } + + /** + * Reschedule jobs that seem to be stuck. This should run even if the native backup service task is disabled, so that stuck jobs get cleaned if necessary. + * */ + protected void rescheduleLostJobs() { + for (DataCenterVO dataCenterVO : dataCenterDao.listAllZones()) { + logger.debug("Searching lost {} jobs to reschedule in zone [{}].", controllerType, dataCenterVO.getUuid()); + for (ClusterVO clusterVO : clusterDao.listByDcHyType(dataCenterVO.getId(), Hypervisor.HypervisorType.KVM.toString())) { + List hostVOS = hostDao.findRoutingByClusterId(clusterVO.getId()); + if (hostVOS.isEmpty()) { + logger.debug("No hosts found in cluster [{}]. Cannot reschedule jobs for it.", clusterVO.getUuid()); + continue; + } + Calendar date = Calendar.getInstance(); + List lostJobs = getLostJobs(clusterVO, date, hostVOS); + if (lostJobs.isEmpty()) { + logger.debug("Found no {} jobs to reschedule for cluster [{}].", controllerType, clusterVO.getUuid()); + continue; + } + logger.debug("Found [{}] {} jobs to reschedule for cluster [{}]. Processing them as failures and rescheduling them.", lostJobs.size(), controllerType, + clusterVO.getUuid()); + lostJobs.forEach(job -> processJobResult(job, false)); + } + } + } + + protected void processJobResult(InternalBackupServiceJobVO job, boolean result) { + job.setAttempts(job.getAttempts() + 1); + if (result) { + logger.debug("{} job [{}] finished with success. Removing it from queue.", controllerType, job); + job.setRemoved(DateUtil.now()); + internalBackupServiceJobDao.update(job); + return; + } + + BackupVO backupVO = backupDao.findByIdIncludingRemoved(job.getBackupId()); + if (backupVO.getRemoved() != null) { + logger.debug("Backup [{}] is marked as removed. Will not reschedule the {} job for it.", backupVO, controllerType); + job.setRemoved(DateUtil.now()); + internalBackupServiceJobDao.update(job); + return; + } + + int maxAttempts = getMaxAttempts(job); + if (job.getAttempts() >= maxAttempts) { + logger.debug("{} job [{}] reached the maximum amount of attempts [{}]. Removing it from queue.", controllerType, job, maxAttempts); + job.setRemoved(DateUtil.now()); + internalBackupServiceJobDao.update(job); + return; + } + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + calendar.add(Calendar.MINUTE, getRetryInterval(job)); + job.setScheduledStartTime(calendar.getTime()); + job.setStartTime(null); + job.setHostId(null); + logger.debug("{} job [{}] failed. Scheduling it to retry at [{}].", controllerType, job, job.getScheduledStartTime()); + internalBackupServiceJobDao.update(job); + } + + /** + * Goes through all the executing jobs for the zone and returns a map from up and enabled KVM hosts to the number of executing jobs. + */ + protected Pair, Integer> getHostToNumberOfExecutingJobsAndTotalExecutingJobs(DataCenterVO zone, InternalBackupServiceJobType... jobTypes) { + List allKvmHostsForZone = hostDao.listAllRoutingHostsByZoneAndHypervisorType(zone.getId(), Hypervisor.HypervisorType.KVM); + HashMap hostToNumberOfExecutingJobs = new HashMap<>(); + + for (HostVO host : allKvmHostsForZone) { + if (host.getStatus() == Status.Up && host.getResourceState() == ResourceState.Enabled) { + hostToNumberOfExecutingJobs.put(host, 0L); + } + } + + List executingJobs = internalBackupServiceJobDao.listExecutingJobsByZoneIdAndJobType(zone.getId(), jobTypes); + for (InternalBackupServiceJobVO executingJob : executingJobs) { + HostVO host = allKvmHostsForZone.stream().filter(hostVO -> hostVO.getId() == executingJob.getHostId()).findFirst().orElse(null); + if (host == null) { + logger.error("{} job [{}] is running in an unknown host. This job will be rescheduled in the future.", controllerType, executingJob); + continue; + } else if (host.getStatus() != Status.Up || host.getResourceState() != ResourceState.Enabled) { + logger.warn("{} job [{}] is running in host [{}], which is not up or not enabled. If possible, wait for the job to finish before restarting the Agent.", + controllerType, executingJob, host); + continue; + } + + hostToNumberOfExecutingJobs.computeIfPresent(host, (hostVO, numberOfJobs) -> numberOfJobs + 1); + } + + return new Pair<>(hostToNumberOfExecutingJobs, executingJobs.size()); + } + + protected List thinJobsToStartList(DataCenterVO zone, List jobsToStart, Integer totalExecutingJobs, + ConfigKey jobsPerZoneConfiguration) { + Integer maxConcurrentJobsInTheZone = jobsPerZoneConfiguration.valueIn(zone.getId()); + if (maxConcurrentJobsInTheZone < 1) { + return jobsToStart; + } + logger.debug("Since [{}] is set to [{}]. We will only execute up to [{}] at the same time. We already have [{}] executing jobs.", + jobsPerZoneConfiguration.toString(), maxConcurrentJobsInTheZone, maxConcurrentJobsInTheZone, totalExecutingJobs); + + if (maxConcurrentJobsInTheZone <= totalExecutingJobs) { + logger.debug("We are already executing the maximum amount of jobs in this zone, we will not execute any new jobs."); + return List.of(); + } + + int maxAllowedJobsToStart = maxConcurrentJobsInTheZone - totalExecutingJobs; + if (jobsToStart.size() > maxAllowedJobsToStart) { + return jobsToStart.subList(0, maxAllowedJobsToStart); + } + return jobsToStart; + } + + protected List> filterHostsWithTooManyJobs(HashMap hostToNumberOfExecutingJobs, ConfigKey jobsPerHostConfiguration) { + List> hostAndNumberOfJobsPairList = new ArrayList<>(); + for (HostVO host : hostToNumberOfExecutingJobs.keySet()) { + Long numberOfJobs = hostToNumberOfExecutingJobs.get(host); + hostDao.loadDetails(host); + Integer maxConcurrentJobsPerHost = getMaxConcurrentJobsPerHost(jobsPerHostConfiguration, host); + if (maxConcurrentJobsPerHost > 0 && numberOfJobs >= maxConcurrentJobsPerHost) { + logger.debug("Host [{}] is already executing the maximum number of concurrent {} jobs set in [{}]. Current number of jobs being executed is " + + "[{}], the value for the configuration is [{}].", host, controllerType, jobsPerHostConfiguration.toString(), numberOfJobs, maxConcurrentJobsPerHost); + continue; + } + hostAndNumberOfJobsPairList.add(new Pair<>(host, numberOfJobs)); + } + return hostAndNumberOfJobsPairList; + } + + protected Integer getMaxConcurrentJobsPerHost(ConfigKey jobsPerHostConfiguration, HostVO host) { + if (host.getDetail(jobsPerHostConfiguration.key()) != null) { + return Integer.valueOf(host.getDetail(jobsPerHostConfiguration.key())); + } + return jobsPerHostConfiguration.valueIn(host.getClusterId()); + } + + /** + * Submit StartCompression jobs, this should be called after submitFinalizeJobsForExecution. + * */ + protected void submitQueuedJobsForExecution(List jobsToExecute, List> hostAndNumberOfJobsPairList, + Set busyInstances, ConfigKey maxJobPerHostConfig, long zoneId) { + for (InternalBackupServiceJobVO job : jobsToExecute) { + if (hostAndNumberOfJobsPairList.isEmpty()) { + logger.debug("There are no more available hosts to send [{}] jobs. Will try to submit them later.", job.getType()); + return; + } + + if (busyInstances.contains(job.getInstanceId())) { + VirtualMachine vm = instanceDao.findByIdIncludingRemoved(job.getInstanceId()); + logger.debug("Instance [{}] has another backup service job running, will not schedule a {} job for it now.", vm.getUuid(), controllerType); + continue; + } + + String logId = UuidUtils.first(UUID.randomUUID().toString()); + logger.debug("Dispatching backup {} job [{}{}] with logid:{} for backup [{}].", controllerType, BACKUP_JOB, job.getId(), logId, job.getBackupId()); + + Pair hostAndNumberOfJobs; + hostAndNumberOfJobs = hostAndNumberOfJobsPairList.remove(0); + hostAndNumberOfJobs.second(hostAndNumberOfJobs.second()+1); + job.setHostId(hostAndNumberOfJobs.first().getId()); + job.setStartTime(DateUtil.now()); + internalBackupServiceJobDao.update(job); + + submitQueuedJob(job, zoneId, logId); + + Integer maxJobsPerHost = getMaxConcurrentJobsPerHost(maxJobPerHostConfig, hostAndNumberOfJobs.first()); + if (hostAndNumberOfJobs.second() < maxJobsPerHost || maxJobsPerHost < 0) { + hostAndNumberOfJobsPairList.add(hostAndNumberOfJobs); + hostAndNumberOfJobsPairList.sort(Comparator.comparing(Pair::second)); + } + busyInstances.add(job.getInstanceId()); + } + } + + + protected Boolean isFrameworkEnabledForZone(DataCenterVO zone) { + return BackupManager.BackupFrameworkEnabled.valueIn(zone.getId()); + } + + protected abstract void submitQueuedJob(InternalBackupServiceJobVO job, long zoneId, String logId); + + /** + * Implementing classes should override this if they want jobs to be tried more than once. + * */ + protected int getMaxAttempts(InternalBackupServiceJobVO jobVo) { + return 1; + } + + /** + * Implementing classes should override this if the jobs execute more than once. Otherwise, you can leave it as is. + * */ + protected int getRetryInterval(InternalBackupServiceJobVO jobVo) { + return -1; + } + + /** + * Implementing classes should override this so their lost jobs are caught and rescheduled, otherwise lost jobs will be left as is. + * */ + protected List getLostJobs(ClusterVO clusterVO, Calendar date, List hostVOS) { + return List.of(); + } + + protected abstract void searchAndDispatchJobs(); +} diff --git a/server/src/main/java/org/apache/cloudstack/backup/to/BackupScreenshotObject.java b/server/src/main/java/org/apache/cloudstack/backup/to/BackupScreenshotObject.java new file mode 100644 index 000000000000..c09685ec7877 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/backup/to/BackupScreenshotObject.java @@ -0,0 +1,113 @@ +// 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 +// 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.backup.to; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataTO; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import java.util.UUID; + +public class BackupScreenshotObject implements DataObject { + + private DataTO dataTO; + private DataStore dataStore; + + public BackupScreenshotObject(DataTO dataTO, DataStore dataStore) { + this.dataTO = dataTO; + this.dataStore = dataStore; + } + + @Override + public String toString() { + return String.format("BackupScreenshotObject %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "dataTO", "dataStore")); + } + + @Override + public long getId() { + return 0; + } + + @Override + public String getUri() { + return null; + } + + @Override + public DataTO getTO() { + return dataTO; + } + + @Override + public DataStore getDataStore() { + return dataStore; + } + + @Override + public Long getSize() { + return null; + } + + @Override + public long getPhysicalSize() { + return 0; + } + + @Override + public DataObjectType getType() { + return dataTO.getObjectType(); + } + + @Override + public String getUuid() { + return null; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public void processEvent(ObjectInDataStoreStateMachine.Event event) { + } + + @Override + public void processEvent(ObjectInDataStoreStateMachine.Event event, Answer answer) { + } + + @Override + public void incRefCount() { + } + + @Override + public void decRefCount() { + } + + @Override + public Long getRefCount() { + return null; + } + + @Override + public String getName() { + return UUID.randomUUID().toString(); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/backup/to/BackupScreenshotTO.java b/server/src/main/java/org/apache/cloudstack/backup/to/BackupScreenshotTO.java new file mode 100644 index 000000000000..7af4dbf7c246 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/backup/to/BackupScreenshotTO.java @@ -0,0 +1,59 @@ +// 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 +// 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.backup.to; + +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.Hypervisor; + +public class BackupScreenshotTO implements DataTO { + private DataStoreTO dataStoreTO; + private Hypervisor.HypervisorType hypervisor; + private String path; + + public BackupScreenshotTO(DataStoreTO dataStoreTO, Hypervisor.HypervisorType hypervisor, String path) { + this.dataStoreTO = dataStoreTO; + this.hypervisor = hypervisor; + this.path = path; + } + + @Override + public DataObjectType getObjectType() { + return DataObjectType.ARCHIVE; + } + + @Override + public DataStoreTO getDataStore() { + return dataStoreTO; + } + + @Override + public Hypervisor.HypervisorType getHypervisorType() { + return hypervisor; + } + + @Override + public String getPath() { + return path; + } + + @Override + public long getId() { + return 0; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/command/ReconcileCommandServiceImpl.java b/server/src/main/java/org/apache/cloudstack/command/ReconcileCommandServiceImpl.java index 5edf05d1a5c0..535459dc110f 100644 --- a/server/src/main/java/org/apache/cloudstack/command/ReconcileCommandServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/command/ReconcileCommandServiceImpl.java @@ -1083,7 +1083,7 @@ private void updateVolumeAndDestroyOldVolume(VolumeVO sourceVolume, DataTO srcDa volumeDao.update(newVolume.getId(), newVolume); logger.debug(String.format("Deleting the dummy volume %s on pool %s", newVolume, destDataStore.getId())); - volumeApiService.destroyVolume(newVolume.getId(), accountManager.getAccount(Account.ACCOUNT_ID_SYSTEM), true, true); + volumeApiService.destroyVolume(newVolume.getId(), accountManager.getAccount(Account.ACCOUNT_ID_SYSTEM), true, true, null); } } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java index 97c13f714474..1ae8c84eebdd 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java @@ -27,6 +27,8 @@ import com.cloud.user.AccountVO; import com.cloud.user.dao.AccountDao; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.backup.BackupVO; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; @@ -36,6 +38,7 @@ import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.heuristics.presetvariables.Account; +import org.apache.cloudstack.storage.heuristics.presetvariables.Backup; import org.apache.cloudstack.storage.heuristics.presetvariables.Domain; import org.apache.cloudstack.storage.heuristics.presetvariables.PresetVariables; import org.apache.cloudstack.storage.heuristics.presetvariables.SecondaryStorage; @@ -78,6 +81,9 @@ public class HeuristicRuleHelper { @Inject private DataCenterDao zoneDao; + @Inject + private BackupOfferingDao backupOfferingDao; + /** * Returns the {@link DataStore} object if the zone, specified by the ID, has an active heuristic rule for the given {@link HeuristicType}. * It returns null otherwise. @@ -120,6 +126,10 @@ protected void buildPresetVariables(JsInterpreter jsInterpreter, HeuristicType h presetVariables.setVolume(setVolumePresetVariable((com.cloud.storage.Volume) obj)); accountId = ((com.cloud.storage.Volume) obj).getAccountId(); break; + case BACKUP: + presetVariables.setBackup(setBackupPresetVariable((BackupVO) obj)); + accountId = ((BackupVO) obj).getAccountId(); + break; } presetVariables.setAccount(setAccountPresetVariable(accountId)); presetVariables.setSecondaryStorages(setSecondaryStoragesVariable(zoneId)); @@ -154,6 +164,10 @@ protected void injectPresetVariables(JsInterpreter jsInterpreter, PresetVariable jsInterpreter.injectVariable("volume", presetVariables.getVolume()); } + if (presetVariables.getBackup() != null) { + jsInterpreter.injectVariable("backup", presetVariables.getBackup()); + } + if (presetVariables.getAccount() != null) { jsInterpreter.injectVariable("account", presetVariables.getAccount()); } @@ -211,6 +225,16 @@ protected Snapshot setSnapshotPresetVariable(SnapshotInfo snapshotInfo) { return snapshot; } + protected Backup setBackupPresetVariable(BackupVO backupVO) { + Backup backup = new Backup(); + + backup.setName(backupVO.getName()); + backup.setVirtualSize(backupVO.getProtectedSize()); + backup.setOfferingUuid(backupOfferingDao.findById(backupVO.getBackupOfferingId()).getUuid()); + + return backup; + } + protected Account setAccountPresetVariable(Long accountId) { if (accountId == null) { return null; diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Backup.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Backup.java new file mode 100644 index 000000000000..fd8909e3e786 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Backup.java @@ -0,0 +1,40 @@ +// 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.storage.heuristics.presetvariables; + +public class Backup extends GenericHeuristicPresetVariable { + + private Long virtualSize; + + private String offeringUuid; + + public Long getVirtualSize() { + return virtualSize; + } + + public void setVirtualSize(Long virtualSize) { + this.virtualSize = virtualSize; + } + + public String getOfferingUuid() { + return offeringUuid; + } + + public void setOfferingUuid(String offeringUuid) { + this.offeringUuid = offeringUuid; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/PresetVariables.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/PresetVariables.java index d04874953272..099c76b7ccb5 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/PresetVariables.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/PresetVariables.java @@ -30,6 +30,8 @@ public class PresetVariables { private Volume volume; + private Backup backup; + public List getSecondaryStorages() { return secondaryStorages; } @@ -62,6 +64,14 @@ public void setVolume(Volume volume) { this.volume = volume; } + public Backup getBackup() { + return backup; + } + + public void setBackup(Backup backup) { + this.backup = backup; + } + public Account getAccount() { return account; } diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 4d9db4a55b1d..840a1de45962 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -1027,7 +1027,7 @@ private void publishVMUsageUpdateResourceCount(final UserVm userVm, ServiceOffer cleanupFailedImportVM(userVm); throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed for unmanaged vm %s during publishing usage records", userVm.getInstanceName())); } - resourceLimitService.incrementVmResourceCount(userVm.getAccountId(), userVm.isDisplayVm(), serviceOfferingVO, templateVO); + resourceLimitService.incrementVmResourceCount(userVm.getAccountId(), userVm.isDisplayVm(), serviceOfferingVO, templateVO, null); // Save usage event and update resource count for user vm volumes List volumes = volumeDao.findByInstance(userVm.getId()); for (VolumeVO volume : volumes) { @@ -2938,7 +2938,7 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, } DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); - List resourceLimitStorageTags = resourceLimitService.getResourceLimitStorageTagsForResourceCountOperation(true, diskOffering); + List resourceLimitStorageTags = resourceLimitService.getResourceLimitStorageTagsForResourceCountOperation(true, diskOffering, null); CheckedReservation volumeReservation = new CheckedReservation(owner, Resource.ResourceType.volume, resourceLimitStorageTags, CollectionUtils.isNotEmpty(resourceLimitStorageTags) ? 1L : 0L, reservationDao, resourceLimitService); reservations.add(volumeReservation); diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index eef304226af2..c0bcba44c642 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -414,6 +414,11 @@ + + + + + diff --git a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java index 7a6b492464ae..a492d0bec21b 100644 --- a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java @@ -1516,13 +1516,14 @@ public void testDoScaleUp() throws ResourceUnavailableException, InsufficientCap when(loadBalancerVMMapMock.getInstanceId()).thenReturn(virtualMachineId + 1); when(loadBalancingRulesService.assignToLoadBalancer(anyLong(), any(), any(), any(), eq(true))).thenReturn(true); - Mockito.doReturn(new Pair>(userVmMock, null)).when(userVmMgr).startVirtualMachine(virtualMachineId, null, new HashMap<>(), null); + Mockito.doReturn(new Pair>(userVmMock, null)).when(userVmMgr).startVirtualMachine(virtualMachineId, null, + new HashMap<>(), null, false); autoScaleManagerImplSpy.doScaleUp(vmGroupId, 1); Mockito.verify(autoScaleManagerImplSpy).createNewVM(asVmGroupMock); Mockito.verify(loadBalancingRulesService).assignToLoadBalancer(anyLong(), any(), any(), any(), eq(true)); - Mockito.verify(userVmMgr).startVirtualMachine(virtualMachineId, null, new HashMap<>(), null); + Mockito.verify(userVmMgr).startVirtualMachine(virtualMachineId, null, new HashMap<>(), null, false); } } diff --git a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java index 18fa16e04a21..a4e0c953c0ee 100644 --- a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java +++ b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java @@ -952,7 +952,7 @@ public void testCheckVolumeResourceCount() throws ResourceAllocationException { String tag = "tag"; long delta = 10L; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); resourceLimitManager.incrementVolumeResourceCount(1L, false, delta, Mockito.mock(DiskOffering.class)); Mockito.verify(resourceLimitManager, Mockito.never()).checkResourceLimitWithTag(Mockito.any(Account.class), Mockito.eq(Resource.ResourceType.volume), Mockito.anyString()); @@ -960,7 +960,7 @@ public void testCheckVolumeResourceCount() throws ResourceAllocationException { Mockito.eq(Resource.ResourceType.primary_storage), Mockito.anyString(), Mockito.anyLong()); Mockito.doReturn(List.of(tag)).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { resourceLimitManager.checkVolumeResourceLimit(account, false, delta, Mockito.mock(DiskOffering.class), reservations); @@ -974,7 +974,7 @@ public void testIncrementVolumeResourceCount() { String tag = "tag"; long delta = 10L; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); resourceLimitManager.incrementVolumeResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class)); Mockito.verify(resourceLimitManager, Mockito.never()).incrementResourceCountWithTag(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.volume), Mockito.anyString()); @@ -982,7 +982,7 @@ public void testIncrementVolumeResourceCount() { Mockito.eq(Resource.ResourceType.primary_storage), Mockito.anyString(), Mockito.anyLong()); Mockito.doReturn(List.of(tag)).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); mockIncrementResourceCountWithTag(); resourceLimitManager.incrementVolumeResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class)); Mockito.verify(resourceLimitManager, Mockito.times(1)).incrementResourceCountWithTag( @@ -997,17 +997,17 @@ public void testDecrementVolumeResourceCount() { String tag = "tag"; long delta = 10L; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); - resourceLimitManager.decrementVolumeResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); + resourceLimitManager.decrementVolumeResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class), null); Mockito.verify(resourceLimitManager, Mockito.never()).decrementResourceCountWithTag(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.volume), Mockito.anyString()); Mockito.verify(resourceLimitManager, Mockito.never()).decrementResourceCountWithTag(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.primary_storage), Mockito.anyString(), Mockito.anyLong()); Mockito.doReturn(List.of(tag)).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); mockDecrementResourceCountWithTag(); - resourceLimitManager.decrementVolumeResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class)); + resourceLimitManager.decrementVolumeResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class), null); Mockito.verify(resourceLimitManager, Mockito.times(1)).decrementResourceCountWithTag( 1L, Resource.ResourceType.volume, tag); Mockito.verify(resourceLimitManager, Mockito.times(1)) @@ -1020,13 +1020,13 @@ public void testIncrementVolumePrimaryStorageResourceCount() { String tag = "tag"; long delta = 10L; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); resourceLimitManager.incrementVolumePrimaryStorageResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class)); Mockito.verify(resourceLimitManager, Mockito.never()).incrementResourceCountWithTag(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.primary_storage), Mockito.anyString(), Mockito.anyLong()); Mockito.doReturn(List.of(tag)).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); mockIncrementResourceCountWithTag(); resourceLimitManager.incrementVolumePrimaryStorageResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class)); Mockito.verify(resourceLimitManager, Mockito.times(1)) @@ -1039,13 +1039,13 @@ public void testDecrementVolumePrimaryStorageResourceCount() { String tag = "tag"; long delta = 10L; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); resourceLimitManager.decrementVolumePrimaryStorageResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class)); Mockito.verify(resourceLimitManager, Mockito.never()).decrementResourceCountWithTag(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.primary_storage), Mockito.anyString(), Mockito.anyLong()); Mockito.doReturn(List.of(tag)).when(resourceLimitManager) - .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class)); + .getResourceLimitStorageTagsForResourceCountOperation(Mockito.anyBoolean(), Mockito.any(DiskOffering.class), Mockito.any()); mockDecrementResourceCountWithTag(); resourceLimitManager.decrementVolumePrimaryStorageResourceCount(accountId, false, delta, Mockito.mock(DiskOffering.class)); Mockito.verify(resourceLimitManager, Mockito.times(1)) @@ -1058,9 +1058,9 @@ public void testIncrementVmResourceCount() { String tag = "tag"; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); resourceLimitManager.incrementVmResourceCount(accountId, false, - Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class)); + Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.never()).incrementResourceCountWithTag(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.user_vm), Mockito.anyString()); Mockito.verify(resourceLimitManager, Mockito.never()).incrementResourceCountWithTag(Mockito.anyLong(), @@ -1070,7 +1070,7 @@ public void testIncrementVmResourceCount() { Mockito.doReturn(List.of(tag)).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); mockIncrementResourceCountWithTag(); ServiceOffering offering = Mockito.mock(ServiceOffering.class); int cpu = 1; @@ -1078,7 +1078,7 @@ public void testIncrementVmResourceCount() { int memory = 1024; Mockito.when(offering.getRamSize()).thenReturn(memory); resourceLimitManager.incrementVmResourceCount(accountId, false, - offering, Mockito.mock(VirtualMachineTemplate.class)); + offering, Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.times(1)).incrementResourceCountWithTag( 1L, Resource.ResourceType.user_vm, tag); Mockito.verify(resourceLimitManager, Mockito.times(1)) @@ -1093,9 +1093,9 @@ public void testDecrementVmResourceCount() { String tag = "tag"; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); resourceLimitManager.decrementVmResourceCount(accountId, false, - Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class)); + Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.never()).decrementResourceCountWithTag(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.user_vm), Mockito.anyString()); Mockito.verify(resourceLimitManager, Mockito.never()).decrementResourceCountWithTag(Mockito.anyLong(), @@ -1105,7 +1105,7 @@ public void testDecrementVmResourceCount() { Mockito.doReturn(List.of(tag)).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); mockDecrementResourceCountWithTag(); ServiceOffering offering = Mockito.mock(ServiceOffering.class); int cpu = 1; @@ -1113,7 +1113,7 @@ public void testDecrementVmResourceCount() { int memory = 1024; Mockito.when(offering.getRamSize()).thenReturn(memory); resourceLimitManager.decrementVmResourceCount(accountId, false, - offering, Mockito.mock(VirtualMachineTemplate.class)); + offering, Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.times(1)).decrementResourceCountWithTag( 1L, Resource.ResourceType.user_vm, tag); Mockito.verify(resourceLimitManager, Mockito.times(1)) @@ -1128,7 +1128,7 @@ public void testIncrementVmCpuResourceCount() { String tag = "tag"; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); resourceLimitManager.incrementVmCpuResourceCount(accountId, false, Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.never()).incrementResourceCountWithTag(Mockito.anyLong(), @@ -1136,7 +1136,7 @@ public void testIncrementVmCpuResourceCount() { Mockito.doReturn(List.of(tag)).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); mockIncrementResourceCountWithTag(); ServiceOffering offering = Mockito.mock(ServiceOffering.class); Long cpu = 2L; @@ -1152,7 +1152,7 @@ public void testDecrementVmCpuResourceCount() { String tag = "tag"; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); resourceLimitManager.decrementVmCpuResourceCount(accountId, false, Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.never()).decrementResourceCountWithTag(Mockito.anyLong(), @@ -1160,7 +1160,7 @@ public void testDecrementVmCpuResourceCount() { Mockito.doReturn(List.of(tag)).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); mockDecrementResourceCountWithTag(); ServiceOffering offering = Mockito.mock(ServiceOffering.class); int cpu = 1; @@ -1177,7 +1177,7 @@ public void testIncrementVmMemoryResourceCount() { String tag = "tag"; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); resourceLimitManager.incrementVmMemoryResourceCount(accountId, false, Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.never()).incrementResourceCountWithTag(Mockito.anyLong(), @@ -1185,7 +1185,7 @@ public void testIncrementVmMemoryResourceCount() { Mockito.doReturn(List.of(tag)).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); mockIncrementResourceCountWithTag(); ServiceOffering offering = Mockito.mock(ServiceOffering.class); long memory = 1024L; @@ -1201,7 +1201,7 @@ public void testDecrementVmMemoryResourceCount() { String tag = "tag"; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); resourceLimitManager.decrementVmMemoryResourceCount(accountId, false, Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.never()).decrementResourceCountWithTag(Mockito.anyLong(), @@ -1209,7 +1209,7 @@ public void testDecrementVmMemoryResourceCount() { Mockito.doReturn(List.of(tag)).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); mockDecrementResourceCountWithTag(); ServiceOffering offering = Mockito.mock(ServiceOffering.class); int memory = 1024; @@ -1226,7 +1226,7 @@ public void testIncrementVmGpuResourceCount() { String tag = "tag"; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); resourceLimitManager.incrementVmGpuResourceCount(accountId, false, Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.never()).incrementResourceCountWithTag(Mockito.anyLong(), @@ -1234,7 +1234,7 @@ public void testIncrementVmGpuResourceCount() { Mockito.doReturn(List.of(tag)).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); mockIncrementResourceCountWithTag(); ServiceOffering offering = Mockito.mock(ServiceOffering.class); Long gpuCount = 2L; @@ -1251,7 +1251,7 @@ public void testDecrementVmGpuResourceCount() { String tag = "tag"; Mockito.doReturn(new ArrayList<>()).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); resourceLimitManager.decrementVmGpuResourceCount(accountId, false, Mockito.mock(ServiceOffering.class), Mockito.mock(VirtualMachineTemplate.class), null); Mockito.verify(resourceLimitManager, Mockito.never()).decrementResourceCountWithTag(Mockito.anyLong(), @@ -1259,7 +1259,7 @@ public void testDecrementVmGpuResourceCount() { Mockito.doReturn(List.of(tag)).when(resourceLimitManager) .getResourceLimitHostTagsForResourceCountOperation(Mockito.anyBoolean(), - Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class)); + Mockito.any(ServiceOffering.class), Mockito.any(VirtualMachineTemplate.class), Mockito.any()); mockDecrementResourceCountWithTag(); ServiceOffering offering = Mockito.mock(ServiceOffering.class); int gpuCount = 1; diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index e2c89dae3cce..12258e2cc160 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -626,37 +626,37 @@ public void testDetachVolumeFromStoppedXenVm() throws NoSuchFieldException, Ille // Negative test - try to attach non-root non-datadisk volume @Test(expected = InvalidParameterValueException.class) public void attachIncorrectDiskType() throws NoSuchFieldException, IllegalAccessException { - volumeApiServiceImpl.attachVolumeToVM(1L, 5L, 0L, false); + volumeApiServiceImpl.attachVolumeToVM(1L, 5L, 0L, false, false); } // Negative test - attach root volume to running vm @Test(expected = InvalidParameterValueException.class) public void attachRootDiskToRunningVm() throws NoSuchFieldException, IllegalAccessException { - volumeApiServiceImpl.attachVolumeToVM(1L, 6L, 0L, false); + volumeApiServiceImpl.attachVolumeToVM(1L, 6L, 0L, false, false); } // Negative test - attach root volume to non-xen vm @Test(expected = InvalidParameterValueException.class) public void attachRootDiskToHyperVm() throws NoSuchFieldException, IllegalAccessException { - volumeApiServiceImpl.attachVolumeToVM(3L, 6L, 0L, false); + volumeApiServiceImpl.attachVolumeToVM(3L, 6L, 0L, false, false); } // Negative test - attach root volume from the managed data store @Test(expected = InvalidParameterValueException.class) public void attachRootDiskOfManagedDataStore() throws NoSuchFieldException, IllegalAccessException { - volumeApiServiceImpl.attachVolumeToVM(2L, 7L, 0L, false); + volumeApiServiceImpl.attachVolumeToVM(2L, 7L, 0L, false, false); } // Negative test - root volume can't be attached to the vm already having a root volume attached @Test(expected = InvalidParameterValueException.class) public void attachRootDiskToVmHavingRootDisk() throws NoSuchFieldException, IllegalAccessException { - volumeApiServiceImpl.attachVolumeToVM(4L, 6L, 0L, false); + volumeApiServiceImpl.attachVolumeToVM(4L, 6L, 0L, false, false); } // Negative test - root volume in uploaded state can't be attached @Test(expected = InvalidParameterValueException.class) public void attachRootInUploadedState() throws NoSuchFieldException, IllegalAccessException { - volumeApiServiceImpl.attachVolumeToVM(2L, 8L, 0L, false); + volumeApiServiceImpl.attachVolumeToVM(2L, 8L, 0L, false, false); } // Positive test - attach ROOT volume in correct state, to the vm not having root volume attached @@ -664,7 +664,7 @@ public void attachRootInUploadedState() throws NoSuchFieldException, IllegalAcce public void attachRootVolumePositive() throws NoSuchFieldException, IllegalAccessException { thrown.expect(NullPointerException.class); try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { - volumeApiServiceImpl.attachVolumeToVM(2L, 6L, 0L, false); + volumeApiServiceImpl.attachVolumeToVM(2L, 6L, 0L, false, false); } } @@ -674,7 +674,7 @@ public void attachDiskWithEncryptEnabledOfferingonNonKVM() throws NoSuchFieldExc DiskOfferingVO diskOffering = Mockito.mock(DiskOfferingVO.class); when(diskOffering.getEncrypt()).thenReturn(true); when(_diskOfferingDao.findById(anyLong())).thenReturn(diskOffering); - volumeApiServiceImpl.attachVolumeToVM(2L, 10L, 1L, false); + volumeApiServiceImpl.attachVolumeToVM(2L, 10L, 1L, false, false); } // Positive test - attach data volume, to the vm on kvm hypervisor @@ -685,7 +685,7 @@ public void attachDiskWithEncryptEnabledOfferingOnKVM() throws NoSuchFieldExcept when(diskOffering.getEncrypt()).thenReturn(true); when(_diskOfferingDao.findById(anyLong())).thenReturn(diskOffering); try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { - volumeApiServiceImpl.attachVolumeToVM(4L, 10L, 1L, false); + volumeApiServiceImpl.attachVolumeToVM(4L, 10L, 1L, false, false); } } @@ -790,7 +790,7 @@ public void testAttachVolumeToVMPerformsResourceReservation() throws NoSuchField when(zoneWithDisabledLocalStorage.isLocalStorageEnabled()).thenReturn(true); doReturn(volumeVoMock).when(volumeApiServiceImpl).getVolumeAttachJobResult(Mockito.any(), Mockito.any(), Mockito.any()); try (MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { - volumeApiServiceImpl.attachVolumeToVM(2L, 9L, null, false); + volumeApiServiceImpl.attachVolumeToVM(2L, 9L, null, false, false); Assert.assertEquals(1, mockCheckedReservation.constructed().size()); } } @@ -1044,7 +1044,7 @@ public void destroyVolumeIfPossibleTestVolumeStateReady() { private void verifyMocksForTestDestroyVolumeWhenVolumeIsNotInRightState() { Mockito.verify(volumeServiceMock, Mockito.times(0)).destroyVolume(volumeMockId); - Mockito.verify(resourceLimitServiceMock, Mockito.times(0)).decrementVolumeResourceCount(accountMockId, true, volumeSizeMock, newDiskOfferingMock); + Mockito.verify(resourceLimitServiceMock, Mockito.times(0)).decrementVolumeResourceCount(accountMockId, true, volumeSizeMock, newDiskOfferingMock, null); } private void configureMocksForTestDestroyVolumeWhenVolume() { @@ -1052,7 +1052,7 @@ private void configureMocksForTestDestroyVolumeWhenVolume() { Mockito.lenient().doReturn(true).when(volumeVoMock).isDisplayVolume(); Mockito.lenient().doNothing().when(volumeServiceMock).destroyVolume(volumeMockId); - Mockito.lenient().doNothing().when(resourceLimitServiceMock).decrementVolumeResourceCount(accountMockId, true, volumeSizeMock, newDiskOfferingMock); + Mockito.lenient().doNothing().when(resourceLimitServiceMock).decrementVolumeResourceCount(accountMockId, true, volumeSizeMock, newDiskOfferingMock, null); } @Test @@ -1420,7 +1420,7 @@ public void validateIfVmHaveBackupsTestExceptionWhenTryToDetachVolumeFromVMWhich try { UserVmVO vm = Mockito.mock(UserVmVO.class); when(vm.getBackupOfferingId()).thenReturn(1l); - volumeApiServiceImpl.checkForBackups(vm, false); + volumeApiServiceImpl.validateIfVmHasBackups(vm, false); } catch (Exception e) { Assert.assertEquals("Unable to detach volume, cannot detach volume from a VM that has backups. First remove the VM from the backup offering or set the global configuration 'backup.enable.attach.detach.of.volumes' to true.", e.getMessage()); } @@ -1431,7 +1431,7 @@ public void validateIfVmHaveBackupsTestExceptionWhenTryToAttachVolumeFromVMWhich try { UserVmVO vm = Mockito.mock(UserVmVO.class); when(vm.getBackupOfferingId()).thenReturn(1l); - volumeApiServiceImpl.checkForBackups(vm, true); + volumeApiServiceImpl.validateIfVmHasBackups(vm, true); } catch (Exception e) { Assert.assertEquals("Unable to attach volume, please specify a VM that does not have any backups or set the global configuration 'backup.enable.attach.detach.of.volumes' to true.", e.getMessage()); } @@ -1441,7 +1441,7 @@ public void validateIfVmHaveBackupsTestExceptionWhenTryToAttachVolumeFromVMWhich public void validateIfVmHaveBackupsTestSuccessWhenVMDontHaveBackupOffering() { UserVmVO vm = Mockito.mock(UserVmVO.class); when(vm.getBackupOfferingId()).thenReturn(null); - volumeApiServiceImpl.checkForBackups(vm, true); + volumeApiServiceImpl.validateIfVmHasBackups(vm, true); } @Test @@ -1600,7 +1600,7 @@ public void updateVolumeAccountTest() { usageEventUtilsMocked.verify(() -> UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_DELETE, volumeVoMock.getAccountId(), volumeVoMock.getDataCenterId(), volumeVoMock.getId(), volumeVoMock.getName(), Volume.class.getName(), volumeVoMock.getUuid(), volumeVoMock.isDisplayVolume())); - Mockito.verify(resourceLimitServiceMock).decrementVolumeResourceCount(accountMock.getAccountId(), true, volumeVoMock.getSize(), newDiskOfferingMock); + Mockito.verify(resourceLimitServiceMock).decrementVolumeResourceCount(accountMock.getAccountId(), true, volumeVoMock.getSize(), newDiskOfferingMock, null); Mockito.verify(volumeVoMock).setAccountId(newAccountMock.getAccountId()); Mockito.verify(volumeVoMock).setDomainId(newAccountMock.getDomainId()); @@ -2223,7 +2223,7 @@ public void testCreateVolumeOnSecondaryForAttachIfNeeded_ExistingVolumeDetermine Mockito.when(primaryDataStoreDaoMock.findById(1L)).thenReturn(destPrimaryStorage); VolumeInfo newVolumeOnPrimaryStorage = Mockito.mock(VolumeInfo.class); try { - Mockito.when(volumeOrchestrationService.createVolumeOnPrimaryStorage(vm, volumeToAttach, vm.getHypervisorType(), destPrimaryStorage)) + Mockito.when(volumeOrchestrationService.createVolumeOnPrimaryStorage(vm, volumeToAttach, vm.getHypervisorType(), destPrimaryStorage, null, null)) .thenReturn(newVolumeOnPrimaryStorage); } catch (NoTransitionException nte) { Assert.fail(nte.getMessage()); @@ -2244,7 +2244,7 @@ public void testCreateVolumeOnPrimaryForAttachIfNeeded_UsesGetPoolForAttach() { VolumeInfo newVolumeOnPrimaryStorage = Mockito.mock(VolumeInfo.class); try { Mockito.when(volumeOrchestrationService.createVolumeOnPrimaryStorage( - vm, volumeToAttach, vm.getHypervisorType(), destPrimaryStorage)) + vm, volumeToAttach, vm.getHypervisorType(), destPrimaryStorage, null, null)) .thenReturn(newVolumeOnPrimaryStorage); } catch (NoTransitionException nte) { Assert.fail(nte.getMessage()); @@ -2276,7 +2276,7 @@ public void testCreateVolumeOnSecondaryForAttachIfNeeded_CreateVolumeFails_Throw Mockito.doReturn(destPrimaryStorage).when(volumeApiServiceImpl) .getSuitablePoolForAllocatedOrUploadedVolumeForAttach(volumeToAttach, vm); try { - Mockito.when(volumeOrchestrationService.createVolumeOnPrimaryStorage(vm, volumeToAttach, vm.getHypervisorType(), destPrimaryStorage)) + Mockito.when(volumeOrchestrationService.createVolumeOnPrimaryStorage(vm, volumeToAttach, vm.getHypervisorType(), destPrimaryStorage, null, null)) .thenThrow(new NoTransitionException("Mocked exception")); } catch (NoTransitionException nte) { Assert.fail(nte.getMessage()); @@ -2312,7 +2312,7 @@ public void testCreateVolumeOnSecondaryForAttachIfNeeded_NoSuitablePool_ReturnsS Assert.assertSame(volumeToAttach, result); try { Mockito.verify(volumeOrchestrationService, Mockito.never()).createVolumeOnPrimaryStorage(Mockito.any(), - Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); } catch (NoTransitionException e) { Assert.fail(); } diff --git a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java index 47099c371dce..6d8003914d36 100755 --- a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java +++ b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java @@ -90,6 +90,7 @@ import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -1233,6 +1234,10 @@ public SnapshotJoinDao snapshotJoinDao() { return Mockito.mock(SnapshotJoinDao.class); } + @Bean + public BackupOfferingDao backupOfferingDao() { + return Mockito.mock(BackupOfferingDao.class); + } public static class Library implements TypeFilter { @Override diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index f55124f0ad69..f70a3abd5871 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -65,10 +65,8 @@ import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.BaseCmd.HTTPMethod; import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; -import org.apache.cloudstack.api.command.admin.vm.ExpungeVMCmd; import org.apache.cloudstack.api.command.user.vm.CreateVMFromBackupCmd; import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; @@ -80,6 +78,7 @@ import org.apache.cloudstack.api.command.user.vm.UpdateVmNicCmd; import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupProvider; import org.apache.cloudstack.backup.BackupVO; import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupScheduleDao; @@ -427,6 +426,9 @@ public class UserVmManagerImplTest { @Mock SSHKeyPairDao sshKeyPairDao; + @Mock + private BackupProvider backupProviderMock; + @Mock private VMInstanceVO vmInstanceMock; @@ -737,7 +739,7 @@ private void configureDoNothingForMethodsThatWeDoNotWantToTest() throws Resource Mockito.doNothing().when(userVmManagerImpl).updateVolumesOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.doNothing().when(userVmManagerImpl).updateVmNetwork(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - Mockito.doNothing().when(userVmManagerImpl).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doNothing().when(userVmManagerImpl).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); } @Test @@ -3148,7 +3150,7 @@ public void executeStepsToChangeOwnershipOfVmTestUpdateVmNetworkThrowsInsufficie Assert.assertThrows(CloudRuntimeException.class, () -> userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock, userVmVoMock, serviceOfferingVoMock, volumes, virtualMachineTemplateMock, 1L)); - Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); Mockito.verify(userVmManagerImpl).updateVolumesOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong()); } @@ -3171,7 +3173,7 @@ public void executeStepsToChangeOwnershipOfVmTestUpdateVmNetworkThrowsResourceAl Assert.assertThrows(CloudRuntimeException.class, () -> userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock, userVmVoMock, serviceOfferingVoMock, volumes, virtualMachineTemplateMock, 1L)); - Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); Mockito.verify(userVmManagerImpl).updateVolumesOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong()); } @@ -3193,11 +3195,11 @@ public void executeStepsToChangeOwnershipOfVmTestResourceCountRunningVmsOnlyEnab userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock, userVmVoMock, serviceOfferingVoMock, volumes, virtualMachineTemplateMock, 1L); - Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); Mockito.verify(userVmManagerImpl).updateVolumesOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong()); Mockito.verify(userVmManagerImpl).updateVmNetwork(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - Mockito.verify(userVmManagerImpl).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.verify(userVmManagerImpl).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); } } @@ -3216,11 +3218,11 @@ public void executeStepsToChangeOwnershipOfVmTestResourceCountRunningVmsOnlyEnab userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock, userVmVoMock, serviceOfferingVoMock, volumes, virtualMachineTemplateMock, 1L); - Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); Mockito.verify(userVmManagerImpl).updateVolumesOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong()); Mockito.verify(userVmManagerImpl).updateVmNetwork(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - Mockito.verify(userVmManagerImpl, Mockito.never()).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.verify(userVmManagerImpl, Mockito.never()).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); } } @@ -3626,12 +3628,12 @@ public void testRestoreVMFromBackup() throws ResourceUnavailableException, Insuf when(vm.getState()).thenReturn(VirtualMachine.State.Running); when(vm.getTemplateId()).thenReturn(templateId); - when(backupManager.restoreBackupToVM(backupId, vmId)).thenReturn(true); + when(backupManager.restoreBackupToVM(backupId, vmId, false)).thenReturn(true); Map params = new HashMap<>(); Pair> vmPair = new Pair<>(vm, params); - doReturn(vmPair).when(userVmManagerImpl).startVirtualMachine(anyLong(), isNull(), isNull(), isNull(), anyMap(), isNull()); - doReturn(vmPair).when(userVmManagerImpl).startVirtualMachine(anyLong(), isNull(), isNull(), anyLong(), anyMap(), isNull()); + doReturn(vmPair).when(userVmManagerImpl).startVirtualMachine(anyLong(), isNull(), isNull(), isNull(), anyMap(), isNull(), anyBoolean()); + doReturn(vmPair).when(userVmManagerImpl).startVirtualMachine(anyLong(), isNull(), isNull(), anyLong(), anyMap(), isNull(), anyBoolean()); when(userVmDao.findById(vmId)).thenReturn(vm); when(templateDao.findByIdIncludingRemoved(templateId)).thenReturn(mock(VMTemplateVO.class)); @@ -3639,7 +3641,7 @@ public void testRestoreVMFromBackup() throws ResourceUnavailableException, Insuf assertNotNull(result); assertEquals(vm, result); - Mockito.verify(backupManager).restoreBackupToVM(backupId, vmId); + Mockito.verify(backupManager).restoreBackupToVM(backupId, vmId, false); } @Test @@ -3652,10 +3654,7 @@ public void testDestroyVm() throws ResourceUnavailableException { ReflectionTestUtils.setField(userVmManagerImpl, "_uuidMgr", uuidMgr); CallContext callContext = mock(CallContext.class); Account callingAccount = mock(Account.class); - when(callingAccount.getId()).thenReturn(accountId); when(callContext.getCallingAccount()).thenReturn(callingAccount); - when(accountManager.isAdmin(callingAccount.getId())).thenReturn(true); - doNothing().when(accountManager).checkApiAccess(callingAccount, BaseCmd.getCommandNameByClass(ExpungeVMCmd.class), null); try (MockedStatic mockedCallContext = mockStatic(CallContext.class)) { mockedCallContext.when(CallContext::current).thenReturn(callContext); mockedCallContext.when(() -> CallContext.register(callContext, ApiCommandResourceType.Volume)).thenReturn(callContext); @@ -3666,8 +3665,6 @@ public void testDestroyVm() throws ResourceUnavailableException { List volumeIds = List.of(volumeId); when(cmd.getVolumeIds()).thenReturn(volumeIds); AsyncJobVO asyncJobMock = mock(AsyncJobVO.class); - when(cmd.getJob()).thenReturn(asyncJobMock); - when(asyncJobMock.getCmdInfo()).thenReturn("{}"); UserVmVO vm = mock(UserVmVO.class); when(vm.getId()).thenReturn(vmId); @@ -3685,15 +3682,13 @@ public void testDestroyVm() throws ResourceUnavailableException { List dataVolumes = new ArrayList<>(); when(volumeDaoMock.findByInstanceAndType(vmId, Volume.Type.DATADISK)).thenReturn(dataVolumes); - when(volumeApiService.destroyVolume(volumeId, CallContext.current().getCallingAccount(), expunge, false)).thenReturn(vol); - doReturn(vm).when(userVmManagerImpl).stopVirtualMachine(anyLong(), anyBoolean()); doReturn(vm).when(userVmManagerImpl).destroyVm(vmId, expunge); doReturn(true).when(userVmManagerImpl).expunge(vm); try (MockedStatic mockedUsageEventUtils = mockStatic(UsageEventUtils.class)) { - UserVm result = userVmManagerImpl.destroyVm(cmd); + UserVm result = userVmManagerImpl.destroyVm(cmd, false); assertNotNull(result); assertEquals(vm, result); diff --git a/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java index 9e0997df08cb..3aec187c29d4 100644 --- a/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockResourceLimitManagerImpl.java @@ -279,7 +279,7 @@ public void checkVolumeResourceLimit(Account owner, Boolean display, Long size, } @Override - public List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering) { + public List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering, Boolean enforceResourceLimitOnDisplayFalse) { return null; } @@ -301,7 +301,7 @@ public void incrementVolumeResourceCount(long accountId, Boolean display, Long s } @Override - public void decrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering) { + public void decrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering, Boolean countDisplayFalseInResourceCount) { } @@ -340,12 +340,14 @@ public void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering } @Override - public void incrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template) { + public void incrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, + Boolean countDisplayFalseInResourceLimit) { } @Override - public void decrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template) { + public void decrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, + Boolean countDisplayFalseInResourceCount) { } diff --git a/server/src/test/java/com/cloud/vpc/dao/MockNetworkDaoImpl.java b/server/src/test/java/com/cloud/vpc/dao/MockNetworkDaoImpl.java index ea822e7eec8b..0cf4a9951ba4 100644 --- a/server/src/test/java/com/cloud/vpc/dao/MockNetworkDaoImpl.java +++ b/server/src/test/java/com/cloud/vpc/dao/MockNetworkDaoImpl.java @@ -292,4 +292,9 @@ public List listByNetworkDomainsAndAccountIds(Set uniqueNtwkD public List listByNetworkDomainsAndDomainIds(Set uniqueNtwkDomains, Set domainIds) { return List.of(); } + + @Override + public NetworkVO findByZoneIdAndAccountIdAndGuestTypeAndName(long zoneId, long accountId, GuestType guestType, String name) { + return null; + } } diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupCompressionServiceJobControllerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupCompressionServiceJobControllerTest.java new file mode 100644 index 000000000000..9227a79904e6 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupCompressionServiceJobControllerTest.java @@ -0,0 +1,528 @@ +// 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.backup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.InternalBackupServiceJobDao; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.dc.ClusterVO; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.resource.ResourceState; +import com.cloud.utils.Pair; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; + +@RunWith(MockitoJUnitRunner.class) +public class BackupCompressionServiceJobControllerTest { + + @Mock + private DataCenterDao dataCenterDaoMock; + + @Mock + private DataCenterVO dataCenterVoMock; + + @Mock + private ClusterDao clusterDaoMock; + + @Mock + private ClusterVO clusterVoMock; + + @Mock + private HostDao hostDaoMock; + + @Mock + private HostVO hostVO; + + @Mock + private InternalBackupServiceJobDao internalBackupServiceJobDaoMock; + + @Mock + private InternalBackupServiceJobVO internalBackupServiceJobVoMock; + + @Mock + private BackupDao backupDaoMock; + + @Mock + private BackupVO backupVoMock; + + @Mock + private VMInstanceDao vmInstanceDaoMock; + + @Mock + private VMInstanceVO vmInstanceVoMock; + + @Mock + private ConfigKey maxConcurrentJobsConfigKey; + + @Mock + private ConfigKey backupCompressionTaskEnabledMock; + + @Spy + @InjectMocks + private BackupCompressionServiceJobController backupCompressionServiceJobControllerSpy; + + long datacenterId = 1L; + long clusterId = 2L; + long hostId = 3L; + long jobId = 32L; + long backupId = 46L; + long instanceId = 100L; + + @Test + public void rescheduleLostJobsTestNoHostsInCluster() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listAllZones(); + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(List.of(clusterVoMock)).when(clusterDaoMock).listByDcHyType(datacenterId, Hypervisor.HypervisorType.KVM.toString()); + doReturn(clusterId).when(clusterVoMock).getId(); + doReturn(List.of()).when(hostDaoMock).findRoutingByClusterId(clusterId); + + backupCompressionServiceJobControllerSpy.rescheduleLostJobs(); + + verify(backupCompressionServiceJobControllerSpy, never()).getLostJobs(any(), any(), any()); + } + + @Test + public void rescheduleLostJobsTestNoLostJobs() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listAllZones(); + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(List.of(clusterVoMock)).when(clusterDaoMock).listByDcHyType(datacenterId, Hypervisor.HypervisorType.KVM.toString()); + doReturn(clusterId).when(clusterVoMock).getId(); + doReturn(List.of(hostVO)).when(hostDaoMock).findRoutingByClusterId(clusterId); + doReturn(List.of()).when(backupCompressionServiceJobControllerSpy).getLostJobs(any(), any(), any()); + + backupCompressionServiceJobControllerSpy.rescheduleLostJobs(); + + verify(backupCompressionServiceJobControllerSpy, never()).processJobResult(any(), anyBoolean()); + } + + @Test + public void rescheduleLostJobsTestProcessJob() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listAllZones(); + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(List.of(clusterVoMock)).when(clusterDaoMock).listByDcHyType(datacenterId, Hypervisor.HypervisorType.KVM.toString()); + doReturn(clusterId).when(clusterVoMock).getId(); + doReturn(List.of(hostVO)).when(hostDaoMock).findRoutingByClusterId(clusterId); + doReturn(List.of(internalBackupServiceJobVoMock)).when(backupCompressionServiceJobControllerSpy).getLostJobs(eq(clusterVoMock), any(), any()); + doNothing().when(backupCompressionServiceJobControllerSpy).processJobResult(any(), anyBoolean()); + + backupCompressionServiceJobControllerSpy.rescheduleLostJobs(); + + verify(backupCompressionServiceJobControllerSpy).processJobResult(any(), anyBoolean()); + } + + @Test + public void processJobResultTestSuccessfulJob() { + backupCompressionServiceJobControllerSpy.processJobResult(internalBackupServiceJobVoMock, true); + + verify(internalBackupServiceJobDaoMock).update(internalBackupServiceJobVoMock); + verify(backupDaoMock, never()).findByIdIncludingRemoved(any()); + } + + @Test + public void processJobResultTestFailedJobAndRemovedBackup() { + doReturn(backupId).when(internalBackupServiceJobVoMock).getBackupId(); + doReturn(backupVoMock).when(backupDaoMock).findByIdIncludingRemoved(backupId); + doReturn(new Date()).when(backupVoMock).getRemoved(); + + backupCompressionServiceJobControllerSpy.processJobResult(internalBackupServiceJobVoMock, false); + + verify(backupDaoMock).findByIdIncludingRemoved(any()); + verify(internalBackupServiceJobVoMock).setRemoved(any()); + verify(internalBackupServiceJobDaoMock).update(internalBackupServiceJobVoMock); + } + + @Test + public void processJobResultTestFailedJobAndReachedMaxAttempts() { + doReturn(backupId).when(internalBackupServiceJobVoMock).getBackupId(); + doReturn(backupVoMock).when(backupDaoMock).findByIdIncludingRemoved(backupId); + doReturn(1).when(backupCompressionServiceJobControllerSpy).getMaxAttempts(internalBackupServiceJobVoMock); + doReturn(1).when(internalBackupServiceJobVoMock).getAttempts(); + + backupCompressionServiceJobControllerSpy.processJobResult(internalBackupServiceJobVoMock, false); + + verify(backupDaoMock).findByIdIncludingRemoved(any()); + verify(backupCompressionServiceJobControllerSpy).getMaxAttempts(internalBackupServiceJobVoMock); + verify(internalBackupServiceJobVoMock).setRemoved(any()); + verify(internalBackupServiceJobDaoMock).update(internalBackupServiceJobVoMock); + } + + @Test + public void processJobResultTestFailedJob() { + doReturn(backupId).when(internalBackupServiceJobVoMock).getBackupId(); + doReturn(backupVoMock).when(backupDaoMock).findByIdIncludingRemoved(backupId); + doReturn(2).when(backupCompressionServiceJobControllerSpy).getMaxAttempts(internalBackupServiceJobVoMock); + doReturn(1).when(internalBackupServiceJobVoMock).getAttempts(); + doReturn(60).when(backupCompressionServiceJobControllerSpy).getRetryInterval(internalBackupServiceJobVoMock); + + backupCompressionServiceJobControllerSpy.processJobResult(internalBackupServiceJobVoMock, false); + + verify(backupDaoMock).findByIdIncludingRemoved(any()); + verify(backupCompressionServiceJobControllerSpy).getMaxAttempts(internalBackupServiceJobVoMock); + verify(internalBackupServiceJobVoMock, never()).setRemoved(any()); + verify(internalBackupServiceJobVoMock).setScheduledStartTime(any()); + verify(internalBackupServiceJobVoMock).setStartTime(null); + verify(internalBackupServiceJobVoMock).setHostId(null); + verify(internalBackupServiceJobDaoMock).update(internalBackupServiceJobVoMock); + } + + @Test + public void getHostToNumberOfExecutingJobsAndTotalExecutingJobsTestNoExecutingJobsAndNoEligibleHosts() { + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(List.of(hostVO)).when(hostDaoMock).listAllRoutingHostsByZoneAndHypervisorType(datacenterId, Hypervisor.HypervisorType.KVM); + doReturn(Status.Down).when(hostVO).getStatus(); + doReturn(List.of()).when(internalBackupServiceJobDaoMock).listExecutingJobsByZoneIdAndJobType(datacenterId); + + Pair, Integer> result = + backupCompressionServiceJobControllerSpy.getHostToNumberOfExecutingJobsAndTotalExecutingJobs(dataCenterVoMock); + + assertTrue(result.first().isEmpty()); + assertEquals(Integer.valueOf(0), result.second()); + verify(hostDaoMock).listAllRoutingHostsByZoneAndHypervisorType(datacenterId, Hypervisor.HypervisorType.KVM); + verify(internalBackupServiceJobDaoMock).listExecutingJobsByZoneIdAndJobType(datacenterId); + } + + @Test + public void getHostToNumberOfExecutingJobsAndTotalExecutingJobsTestOneEligibleHostAndOneExecutingJob() { + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(hostId).when(hostVO).getId(); + doReturn(Status.Up).when(hostVO).getStatus(); + doReturn(ResourceState.Enabled).when(hostVO).getResourceState(); + doReturn(List.of(hostVO)).when(hostDaoMock).listAllRoutingHostsByZoneAndHypervisorType(datacenterId, Hypervisor.HypervisorType.KVM); + doReturn(List.of(internalBackupServiceJobVoMock)).when(internalBackupServiceJobDaoMock).listExecutingJobsByZoneIdAndJobType(datacenterId); + doReturn(hostId).when(internalBackupServiceJobVoMock).getHostId(); + + Pair, Integer> result = + backupCompressionServiceJobControllerSpy.getHostToNumberOfExecutingJobsAndTotalExecutingJobs(dataCenterVoMock); + + assertEquals(Integer.valueOf(1), result.second()); + assertEquals(1, result.first().size()); + assertTrue(result.first().containsKey(hostVO)); + assertEquals(Long.valueOf(1L), result.first().get(hostVO)); + } + + @Test + public void getHostToNumberOfExecutingJobsAndTotalExecutingJobsTestEligibleHostButExecutingJobOnDisabledHostIsIgnored() { + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(hostId).when(hostVO).getId(); + doReturn(Status.Down).when(hostVO).getStatus(); + doReturn(List.of(hostVO)).when(hostDaoMock).listAllRoutingHostsByZoneAndHypervisorType(datacenterId, Hypervisor.HypervisorType.KVM); + doReturn(List.of(internalBackupServiceJobVoMock)).when(internalBackupServiceJobDaoMock).listExecutingJobsByZoneIdAndJobType(datacenterId); + doReturn(hostId).when(internalBackupServiceJobVoMock).getHostId(); + + Pair, Integer> result = + backupCompressionServiceJobControllerSpy.getHostToNumberOfExecutingJobsAndTotalExecutingJobs(dataCenterVoMock); + + assertEquals(Integer.valueOf(1), result.second()); + assertTrue(result.first().isEmpty()); + assertFalse(result.first().containsKey(hostVO)); + } + + @Test + public void getHostToNumberOfExecutingJobsAndTotalExecutingJobsTestJobExecutingInUnknownHost() { + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(List.of()).when(hostDaoMock).listAllRoutingHostsByZoneAndHypervisorType(datacenterId, Hypervisor.HypervisorType.KVM); + doReturn(List.of(internalBackupServiceJobVoMock)).when(internalBackupServiceJobDaoMock).listExecutingJobsByZoneIdAndJobType(datacenterId); + + Pair, Integer> result = + backupCompressionServiceJobControllerSpy.getHostToNumberOfExecutingJobsAndTotalExecutingJobs(dataCenterVoMock); + + assertTrue(result.first().isEmpty()); + assertEquals(Integer.valueOf(1), result.second()); + verify(hostDaoMock).listAllRoutingHostsByZoneAndHypervisorType(datacenterId, Hypervisor.HypervisorType.KVM); + verify(internalBackupServiceJobDaoMock).listExecutingJobsByZoneIdAndJobType(datacenterId); + } + + @Test + public void thinJobsToStartListTestUnlimitedConcurrentJobs() { + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(0).when(maxConcurrentJobsConfigKey).valueIn(datacenterId); + ArrayList originalJobs = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + originalJobs.add(Mockito.mock(InternalBackupServiceJobVO.class)); + } + + List result = backupCompressionServiceJobControllerSpy.thinJobsToStartList(dataCenterVoMock, new ArrayList<>(originalJobs), 0, + maxConcurrentJobsConfigKey); + + assertEquals(originalJobs, result); + } + + @Test + public void thinJobsToStartListTestTotalExecutingJobsBiggerThanMax() { + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(3).when(maxConcurrentJobsConfigKey).valueIn(datacenterId); + ArrayList originalJobs = new ArrayList<>(); + for (int i = 0; i < 7; i++) { + originalJobs.add(Mockito.mock(InternalBackupServiceJobVO.class)); + } + + List result = backupCompressionServiceJobControllerSpy.thinJobsToStartList(dataCenterVoMock, new ArrayList<>(originalJobs), 3, + maxConcurrentJobsConfigKey); + + assertEquals(List.of(), result); + } + + + @Test + public void thinJobsToStartListTestTotalExecutingJobsLowerThanMax() { + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(6).when(maxConcurrentJobsConfigKey).valueIn(datacenterId); + ArrayList originalJobs = new ArrayList<>(); + for (int i = 0; i < 7; i++) { + originalJobs.add(Mockito.mock(InternalBackupServiceJobVO.class)); + } + + List result = backupCompressionServiceJobControllerSpy.thinJobsToStartList(dataCenterVoMock, new ArrayList<>(originalJobs), 3, + maxConcurrentJobsConfigKey); + + assertEquals(originalJobs.subList(0, 3), result); + } + + @Test + public void thinJobsToStartListTestNoCurrentExecutingJobsAndNewJobsLowerThanMax() { + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(6).when(maxConcurrentJobsConfigKey).valueIn(datacenterId); + ArrayList originalJobs = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + originalJobs.add(Mockito.mock(InternalBackupServiceJobVO.class)); + } + + List result = backupCompressionServiceJobControllerSpy.thinJobsToStartList(dataCenterVoMock, new ArrayList<>(originalJobs), 0, + maxConcurrentJobsConfigKey); + + assertEquals(originalJobs, result); + } + + @Test + public void filterHostsWithTooManyJobsTestUnlimitedCompressionPerHost() { + doReturn(0).when(backupCompressionServiceJobControllerSpy).getMaxConcurrentJobsPerHost(maxConcurrentJobsConfigKey, hostVO); + + HashMap hostToNumberOfExecutingJobs = new HashMap<>(); + hostToNumberOfExecutingJobs.put(hostVO, 10L); + + List> result = backupCompressionServiceJobControllerSpy.filterHostsWithTooManyJobs(hostToNumberOfExecutingJobs, maxConcurrentJobsConfigKey); + + assertEquals(List.of(new Pair<>(hostVO, 10L)), result); + } + + @Test + public void filterHostsWithTooManyJobsTestLimitedCompressionPerHost() { + doReturn(4).when(backupCompressionServiceJobControllerSpy).getMaxConcurrentJobsPerHost(any(), any()); + + + HashMap hostToNumberOfExecutingJobs = new HashMap<>(); + hostToNumberOfExecutingJobs.put(hostVO, 10L); + HostVO hostVO2 = Mockito.mock(HostVO.class); + hostToNumberOfExecutingJobs.put(hostVO2, 2L); + + List> result = backupCompressionServiceJobControllerSpy.filterHostsWithTooManyJobs(hostToNumberOfExecutingJobs, maxConcurrentJobsConfigKey); + + assertEquals(List.of(new Pair<>(hostVO2, 2L)), result); + } + + @Test + public void submitQueuedJobsForExecutionTestNoAvailableHostsReturnsEarly() { + List jobsToExecute = List.of(internalBackupServiceJobVoMock); + + backupCompressionServiceJobControllerSpy.submitQueuedJobsForExecution(jobsToExecute, List.of(), new HashSet<>(), maxConcurrentJobsConfigKey, datacenterId); + + verify(internalBackupServiceJobVoMock, never()).setHostId(any()); + verify(internalBackupServiceJobVoMock, never()).setStartTime(any()); + verify(internalBackupServiceJobDaoMock, never()).update(any()); + } + + @Test + public void submitQueuedJobsForExecutionTestBusyInstanceIsSkipped() { + doReturn(instanceId).when(internalBackupServiceJobVoMock).getInstanceId(); + doReturn(vmInstanceVoMock).when(vmInstanceDaoMock).findByIdIncludingRemoved(instanceId); + + List> hostAndNumberOfJobsPairList = List.of(new Pair<>(hostVO, 0L)); + Set busyInstances = Set.of(instanceId); + + backupCompressionServiceJobControllerSpy.submitQueuedJobsForExecution(List.of(internalBackupServiceJobVoMock), hostAndNumberOfJobsPairList, busyInstances, + maxConcurrentJobsConfigKey, datacenterId); + + verify(internalBackupServiceJobVoMock, never()).setHostId(any()); + verify(internalBackupServiceJobVoMock, never()).setStartTime(any()); + verify(internalBackupServiceJobDaoMock, never()).update(any()); + verify(backupCompressionServiceJobControllerSpy, never()).submitQueuedJob(any(), anyLong(), any()); + } + + @Test + public void submitQueuedJobsForExecutionTestSchedulesJobAndRequeuesHostWhenBelowLimit() { + doReturn(instanceId).when(internalBackupServiceJobVoMock).getInstanceId(); + doReturn(jobId).when(internalBackupServiceJobVoMock).getId(); + doReturn(backupId).when(internalBackupServiceJobVoMock).getBackupId(); + doReturn(hostId).when(hostVO).getId(); + doReturn(5).when(backupCompressionServiceJobControllerSpy).getMaxConcurrentJobsPerHost(maxConcurrentJobsConfigKey, hostVO); + doNothing().when(backupCompressionServiceJobControllerSpy).submitQueuedJob(any(), eq(datacenterId), any()); + + List> hostAndNumberOfJobsPairList = new java.util.ArrayList<>(); + hostAndNumberOfJobsPairList.add(new Pair<>(hostVO, 0L)); + Set busyInstances = new HashSet<>(); + + backupCompressionServiceJobControllerSpy.submitQueuedJobsForExecution(List.of(internalBackupServiceJobVoMock), hostAndNumberOfJobsPairList, busyInstances, + maxConcurrentJobsConfigKey, datacenterId); + + verify(internalBackupServiceJobVoMock).setHostId(hostId); + verify(internalBackupServiceJobVoMock).setStartTime(any()); + verify(internalBackupServiceJobDaoMock).update(internalBackupServiceJobVoMock); + verify(backupCompressionServiceJobControllerSpy).submitQueuedJob(eq(internalBackupServiceJobVoMock), eq(datacenterId), any()); + assertEquals(1, hostAndNumberOfJobsPairList.size()); + assertSame(hostVO, hostAndNumberOfJobsPairList.get(0).first()); + assertEquals(Long.valueOf(1L), hostAndNumberOfJobsPairList.get(0).second()); + assertTrue(busyInstances.contains(instanceId)); + } + + @Test + public void submitQueuedJobsForExecutionTestDoesNotRequeueHostWhenLimitReached() { + doReturn(instanceId).when(internalBackupServiceJobVoMock).getInstanceId(); + doReturn(jobId).when(internalBackupServiceJobVoMock).getId(); + doReturn(backupId).when(internalBackupServiceJobVoMock).getBackupId(); + doReturn(hostId).when(hostVO).getId(); + doReturn(1).when(backupCompressionServiceJobControllerSpy).getMaxConcurrentJobsPerHost(maxConcurrentJobsConfigKey, hostVO); + doNothing().when(backupCompressionServiceJobControllerSpy).submitQueuedJob(any(), eq(datacenterId), any()); + + List> hostAndNumberOfJobsPairList = new java.util.ArrayList<>(); + hostAndNumberOfJobsPairList.add(new Pair<>(hostVO, 0L)); + Set busyInstances = new HashSet<>(); + + backupCompressionServiceJobControllerSpy.submitQueuedJobsForExecution(List.of(internalBackupServiceJobVoMock), hostAndNumberOfJobsPairList, busyInstances, + maxConcurrentJobsConfigKey, datacenterId); + + assertTrue(hostAndNumberOfJobsPairList.isEmpty()); + assertTrue(busyInstances.contains(instanceId)); + } + + @Test + public void searchAndDispatchJobsTestLockFailureReturnsEarly() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(false).when(internalBackupServiceJobDaoMock).lockInLockTable("compression_lock", 300); + + backupCompressionServiceJobControllerSpy.searchAndDispatchJobs(); + + verify(internalBackupServiceJobDaoMock).unlockFromLockTable(any()); + verify(backupCompressionServiceJobControllerSpy, never()).rescheduleLostJobs(); + verify(internalBackupServiceJobDaoMock, never()).listWaitingJobsAndScheduledToBeforeNow(anyLong(), any()); + } + + @Test + public void searchAndDispatchJobsTestTaskDisabledReturnsAfterReschedule() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(true).when(internalBackupServiceJobDaoMock).lockInLockTable("compression_lock", 300); + doNothing().when(backupCompressionServiceJobControllerSpy).rescheduleLostJobs(); + doReturn(false).when(backupCompressionTaskEnabledMock).value(); + + backupCompressionServiceJobControllerSpy.searchAndDispatchJobs(); + + verify(backupCompressionServiceJobControllerSpy).rescheduleLostJobs(); + verify(internalBackupServiceJobDaoMock, never()).listWaitingJobsAndScheduledToBeforeNow(anyLong(), any()); + verify(internalBackupServiceJobDaoMock).unlockFromLockTable("compression_lock"); + } + + @Test + public void searchAndDispatchJobsTestZoneWithoutBackupFrameworkIsSkipped() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(true).when(internalBackupServiceJobDaoMock).lockInLockTable("compression_lock", 300); + doNothing().when(backupCompressionServiceJobControllerSpy).rescheduleLostJobs(); + doReturn(true).when(backupCompressionTaskEnabledMock).value(); + doReturn(false).when(backupCompressionServiceJobControllerSpy).isFrameworkEnabledForZone(dataCenterVoMock); + + backupCompressionServiceJobControllerSpy.searchAndDispatchJobs(); + + verify(internalBackupServiceJobDaoMock, never()).listWaitingJobsAndScheduledToBeforeNow(anyLong(), any()); + verify(internalBackupServiceJobDaoMock).unlockFromLockTable("compression_lock"); + } + + @Test + public void searchAndDispatchJobsTestEmptyWaitingJobsSkipsDispatch() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(true).when(internalBackupServiceJobDaoMock).lockInLockTable("compression_lock", 300); + doNothing().when(backupCompressionServiceJobControllerSpy).rescheduleLostJobs(); + doReturn(true).when(backupCompressionTaskEnabledMock).value(); + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(true).when(backupCompressionServiceJobControllerSpy).isFrameworkEnabledForZone(dataCenterVoMock); + + backupCompressionServiceJobControllerSpy.searchAndDispatchJobs(); + + verify(backupCompressionServiceJobControllerSpy, never()).getHostToNumberOfExecutingJobsAndTotalExecutingJobs(any(), any()); + verify(backupCompressionServiceJobControllerSpy, never()).submitQueuedJobsForExecution(any(), any(), any(), any(), anyLong()); + verify(internalBackupServiceJobDaoMock).unlockFromLockTable("compression_lock"); + } + + @Test + public void searchAndDispatchJobsTestHappyPathDispatchesJobs() { + Pair, Integer> executingJobsPair = new Pair<>(new HashMap<>(), 0); + + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(true).when(internalBackupServiceJobDaoMock).lockInLockTable("compression_lock", 300); + doNothing().when(backupCompressionServiceJobControllerSpy).rescheduleLostJobs(); + doReturn(true).when(backupCompressionTaskEnabledMock).value(); + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(true).when(backupCompressionServiceJobControllerSpy).isFrameworkEnabledForZone(dataCenterVoMock); + doReturn(List.of(internalBackupServiceJobVoMock)).when(internalBackupServiceJobDaoMock).listWaitingJobsAndScheduledToBeforeNow(eq(datacenterId), + eq(InternalBackupServiceJobType.StartCompression), eq(InternalBackupServiceJobType.FinalizeCompression)); + doReturn(List.of(internalBackupServiceJobVoMock)).when(backupCompressionServiceJobControllerSpy).filterJobsOfDomainsAndAccountsWithDisabledCompressionTask(any()); + doReturn(executingJobsPair).when(backupCompressionServiceJobControllerSpy).getHostToNumberOfExecutingJobsAndTotalExecutingJobs(eq(dataCenterVoMock), any()); + doReturn(List.of(new Pair<>(hostVO, 0L))).when(backupCompressionServiceJobControllerSpy).filterHostsWithTooManyJobs(any(), any()); + doReturn(new HashSet()).when(backupCompressionServiceJobControllerSpy).submitFinalizeJobsForExecution(any(), any(), eq(datacenterId)); + doReturn(List.of(internalBackupServiceJobVoMock)).when(backupCompressionServiceJobControllerSpy).thinJobsToStartList(eq(dataCenterVoMock), any(), anyInt(), any()); + doNothing().when(backupCompressionServiceJobControllerSpy).submitQueuedJobsForExecution(any(), any(), any(), any(), eq(datacenterId)); + + backupCompressionServiceJobControllerSpy.searchAndDispatchJobs(); + + + verify(backupCompressionServiceJobControllerSpy).submitQueuedJobsForExecution(any(), any(), any(), any(), eq(datacenterId)); + verify(internalBackupServiceJobDaoMock).unlockFromLockTable("compression_lock"); + verify(internalBackupServiceJobDaoMock).listExecutingJobsByZoneIdAndJobType(eq(datacenterId), eq(InternalBackupServiceJobType.StartCompression)); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java index d1817047499e..927b2831c6a7 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java @@ -48,6 +48,7 @@ import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd; +import org.apache.cloudstack.api.command.user.backup.CreateBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.DeleteBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; @@ -67,12 +68,14 @@ import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.commons.collections4.CollectionUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -121,6 +124,7 @@ import com.cloud.storage.dao.VolumeDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; +import com.cloud.user.AccountService; import com.cloud.user.AccountVO; import com.cloud.user.DomainManager; import com.cloud.user.ResourceLimitService; @@ -267,6 +271,12 @@ public class BackupManagerTest { @Mock AsyncJobManager asyncJobManager; + @Mock + private AccountService accountServiceMock; + + @Mock + private DomainVO domainVOMock; + private Gson gson; private String[] hostPossibleValues = {"127.0.0.1", "hostname"}; @@ -433,15 +443,16 @@ public void restoreBackedUpVolumeTestHostIpAndDatastoreUuid() { doReturn(new Pair(Boolean.TRUE, "Success")) .when(backupProvider).restoreBackedUpVolume(any(Backup.class), any(Backup.VolumeInfo.class), - any(String.class), any(String.class), any(Pair.class)); + any(String.class), any(String.class), any(Pair.class), any(), any(Boolean.class)); - Pair restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeInfo, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, vm); + Pair restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeInfo, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, vm, + false); assertEquals(Boolean.TRUE, restoreBackedUpVolume.first()); assertEquals("Success", restoreBackedUpVolume.second()); verify(backupProvider, atLeastOnce()).restoreBackedUpVolume(any(Backup.class), any(Backup.VolumeInfo.class), - any(String.class), any(String.class), any(Pair.class)); + any(String.class), any(String.class), any(Pair.class), any(), Mockito.anyBoolean()); } @Test @@ -460,15 +471,16 @@ public void restoreBackedUpVolumeTestHostIpAndDatastoreName() { doReturn(new Pair(Boolean.TRUE, "Success2")) .when(backupProvider).restoreBackedUpVolume(any(Backup.class), any(Backup.VolumeInfo.class), - any(String.class), any(String.class), any(Pair.class)); + any(String.class), any(String.class), any(Pair.class), any(), any(Boolean.class)); - Pair restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeInfo, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, vm); + Pair restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeInfo, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, vm, + false); assertEquals(Boolean.TRUE, restoreBackedUpVolume.first()); assertEquals("Success2", restoreBackedUpVolume.second()); verify(backupProvider, atLeastOnce()).restoreBackedUpVolume(any(Backup.class), any(Backup.VolumeInfo.class), - any(String.class), any(String.class), any(Pair.class)); + any(String.class), any(String.class), any(Pair.class), any(), Mockito.anyBoolean()); } @Test @@ -487,15 +499,16 @@ public void restoreBackedUpVolumeTestHostNameAndDatastoreUuid() { doReturn(new Pair(Boolean.TRUE, "Success3")) .when(backupProvider).restoreBackedUpVolume(any(Backup.class), any(Backup.VolumeInfo.class), - any(String.class), any(String.class), any(Pair.class)); + any(String.class), any(String.class), any(Pair.class), any(), any(Boolean.class)); - Pair restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeInfo, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, vm); + Pair restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeInfo, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, vm, + false); assertEquals(Boolean.TRUE, restoreBackedUpVolume.first()); assertEquals("Success3", restoreBackedUpVolume.second()); verify(backupProvider, atLeastOnce()).restoreBackedUpVolume(any(Backup.class), any(Backup.VolumeInfo.class), - any(String.class), any(String.class), any(Pair.class)); + any(String.class), any(String.class), any(Pair.class), any(), Mockito.anyBoolean()); } @Test @@ -514,15 +527,16 @@ public void restoreBackedUpVolumeTestHostAndDatastoreName() { doReturn(new Pair(Boolean.TRUE, "Success4")) .when(backupProvider).restoreBackedUpVolume(any(Backup.class), any(Backup.VolumeInfo.class), - any(String.class), any(String.class), any(Pair.class)); + any(String.class), any(String.class), any(Pair.class), any(), any(Boolean.class)); - Pair restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeInfo, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, vm); + Pair restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeInfo, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, vm, + false); assertEquals(Boolean.TRUE, restoreBackedUpVolume.first()); assertEquals("Success4", restoreBackedUpVolume.second()); verify(backupProvider, atLeastOnce()).restoreBackedUpVolume(any(Backup.class), any(Backup.VolumeInfo.class), - any(String.class), any(String.class), any(Pair.class)); + any(String.class), any(String.class), any(Pair.class), any(), any(Boolean.class)); } @Test @@ -547,9 +561,9 @@ public void tryRestoreVMTestRestoreSucceeded() throws NoTransitionException { Mockito.when(vm.getId()).thenReturn(1L); Mockito.when(offering.getProvider()).thenReturn("veeam"); Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider("veeam"); - Mockito.when(backupProvider.restoreVMFromBackup(vm, backup)).thenReturn(true); + Mockito.when(backupProvider.restoreVMFromBackup(vm, backup, false, null)).thenReturn(true); - backupManager.tryRestoreVM(backup, vm, offering, "Nothing to write here."); + backupManager.tryRestoreVM(backup, vm, offering, "Nothing to write here.", false, null); } } @@ -577,9 +591,9 @@ public void tryRestoreVMTestRestoreFails() throws NoTransitionException { Mockito.when(vm.getId()).thenReturn(1L); Mockito.when(offering.getProvider()).thenReturn("veeam"); Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider("veeam"); - Mockito.when(backupProvider.restoreVMFromBackup(vm, backup)).thenReturn(false); + Mockito.when(backupProvider.restoreVMFromBackup(vm, backup, false, null)).thenReturn(false); try { - backupManager.tryRestoreVM(backup, vm, offering, "Checking message error."); + backupManager.tryRestoreVM(backup, vm, offering, "Checking message error.", false, null); fail("An exception is needed."); } catch (CloudRuntimeException e) { assertEquals("Error restoring Instance from Backup [Checking message error.].", e.getMessage()); @@ -711,7 +725,7 @@ public void createBackupTestCreateScheduledBackup() throws ResourceAllocationExc when(backup.getId()).thenReturn(backupId); when(backup.getSize()).thenReturn(newBackupSize); when(backupProvider.getName()).thenReturn("testbackupprovider"); - when(backupProvider.takeBackup(vmInstanceVOMock, null)).thenReturn(new Pair<>(true, backup)); + when(backupProvider.takeBackup(vmInstanceVOMock, null, false, scheduleId)).thenReturn(new Pair<>(true, backup)); Map backupProvidersMap = new HashMap<>(); backupProvidersMap.put(backupProvider.getName().toLowerCase(), backupProvider); ReflectionTestUtils.setField(backupManager, "backupProvidersMap", backupProvidersMap); @@ -941,6 +955,7 @@ public void deleteAllVmBackupSchedulesTestReturnSuccessWhenAllSchedulesAreDelete Mockito.when(backupSchedules.get(0).getId()).thenReturn(2L); Mockito.when(backupSchedules.get(1).getId()).thenReturn(3L); Mockito.when(backupScheduleDao.remove(Mockito.anyLong())).thenReturn(true); + Mockito.doNothing().when(backupManager).finalizeBackupScheduleIfNeeded(Mockito.any()); boolean success = backupManager.deleteAllVmBackupSchedules(vmId); assertTrue(success); @@ -956,6 +971,7 @@ public void deleteAllVmBackupSchedulesTestReturnFalseWhenAnyDeletionFails() { Mockito.when(backupSchedules.get(1).getId()).thenReturn(3L); Mockito.when(backupScheduleDao.remove(2L)).thenReturn(true); Mockito.when(backupScheduleDao.remove(3L)).thenReturn(false); + Mockito.doNothing().when(backupManager).finalizeBackupScheduleIfNeeded(Mockito.any()); boolean success = backupManager.deleteAllVmBackupSchedules(vmId); assertFalse(success); @@ -1001,6 +1017,7 @@ public void deleteBackupScheduleTestDeleteSpecificScheduleWhenItsIdIsSpecified() Mockito.doNothing().when(backupManager).checkCallerAccessToBackupScheduleVm(vmId); when(backupScheduleVOMock.getId()).thenReturn(id); when(backupScheduleDao.remove(id)).thenReturn(true); + Mockito.doNothing().when(backupManager).finalizeBackupScheduleIfNeeded(Mockito.any()); boolean success = backupManager.deleteBackupSchedule(deleteBackupScheduleCmdMock); assertTrue(success); @@ -1315,6 +1332,7 @@ public void testDeleteBackupScheduleByVmId() { when(schedule.getId()).thenReturn(scheduleId); when(backupScheduleDao.listByVM(vmId)).thenReturn(List.of(schedule)); when(backupScheduleDao.remove(scheduleId)).thenReturn(true); + doNothing().when(backupManager).finalizeBackupScheduleIfNeeded(any()); boolean result = backupManager.deleteBackupSchedule(cmd); assertTrue(result); @@ -1362,13 +1380,13 @@ public void testRestoreBackupToVM() throws NoTransitionException { when(rootVolume.getPoolId()).thenReturn(poolId); when(volumeDao.findIncludingRemovedByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(List.of(rootVolume)); when(primaryDataStoreDao.findById(poolId)).thenReturn(pool); - when(backupProvider.restoreBackupToVM(vm, backup, null, null)).thenReturn(new Pair<>(true, null)); + when(backupProvider.restoreBackupToVM(vm, backup, null, null, false)).thenReturn(new Pair<>(true, null)); try (MockedStatic utils = Mockito.mockStatic(ActionEventUtils.class)) { - boolean result = backupManager.restoreBackupToVM(backupId, vmId); + boolean result = backupManager.restoreBackupToVM(backupId, vmId, false); assertTrue(result); - verify(backupProvider, times(1)).restoreBackupToVM(vm, backup, null, null); + verify(backupProvider, times(1)).restoreBackupToVM(vm, backup, null, null, false); verify(virtualMachineManager, times(1)).stateTransitTo(vm, VirtualMachine.Event.RestoringRequested, hostId); verify(virtualMachineManager, times(1)).stateTransitTo(vm, VirtualMachine.Event.RestoringSuccess, hostId); } catch (CloudRuntimeException e) { @@ -1418,13 +1436,13 @@ public void testRestoreBackupToVMException() throws NoTransitionException { when(rootVolume.getPoolId()).thenReturn(poolId); when(volumeDao.findIncludingRemovedByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(List.of(rootVolume)); when(primaryDataStoreDao.findById(poolId)).thenReturn(pool); - when(backupProvider.restoreBackupToVM(vm, backup, null, null)).thenReturn(new Pair<>(false, null)); + when(backupProvider.restoreBackupToVM(vm, backup, null, null, false)).thenReturn(new Pair<>(false, null)); try (MockedStatic utils = Mockito.mockStatic(ActionEventUtils.class)) { CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class, - () -> backupManager.restoreBackupToVM(backupId, vmId)); + () -> backupManager.restoreBackupToVM(backupId, vmId, false)); - verify(backupProvider, times(1)).restoreBackupToVM(vm, backup, null, null); + verify(backupProvider, times(1)).restoreBackupToVM(vm, backup, null, null, false); verify(virtualMachineManager, times(1)).stateTransitTo(vm, VirtualMachine.Event.RestoringRequested, hostId); verify(virtualMachineManager, times(1)).stateTransitTo(vm, VirtualMachine.Event.RestoringFailed, hostId); } @@ -2105,7 +2123,6 @@ public void testRestoreBackupSuccess() throws NoTransitionException { when(vm.getHypervisorType()).thenReturn(hypervisorType); when(vm.getState()).thenReturn(VirtualMachine.State.Stopped); when(vm.getRemoved()).thenReturn(null); - when(vm.getBackupOfferingId()).thenReturn(offeringId); BackupOfferingVO offering = mock(BackupOfferingVO.class); when(offering.getProvider()).thenReturn("testbackupprovider"); @@ -2114,13 +2131,13 @@ public void testRestoreBackupSuccess() throws NoTransitionException { when(volumeDao.findByInstance(vmId)).thenReturn(Collections.singletonList(volume)); BackupProvider backupProvider = mock(BackupProvider.class); - when(backupProvider.restoreVMFromBackup(vm, backup)).thenReturn(true); + when(backupProvider.restoreVMFromBackup(vm, backup, false, null)).thenReturn(true); when(backupDao.findById(backupId)).thenReturn(backup); when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); when(backupOfferingDao.findByIdIncludingRemoved(offeringId)).thenReturn(offering); when(backupManager.getBackupProvider("testbackupprovider")).thenReturn(backupProvider); - doReturn(true).when(backupManager).importRestoredVM(zoneId, domainId, accountId, userId, vmInstanceName, hypervisorType, backup); + doReturn(true).when(backupManager).importRestoredVM(zoneId, domainId, accountId, userId, vmInstanceName, hypervisorType, backup, offering); doNothing().when(backupManager).validateBackupForZone(any()); when(virtualMachineManager.stateTransitTo(any(), any(), any())).thenReturn(true); @@ -2129,14 +2146,14 @@ public void testRestoreBackupSuccess() throws NoTransitionException { Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.eq(true), Mockito.eq(0))).thenReturn(1L); - boolean result = backupManager.restoreBackup(backupId); + boolean result = backupManager.restoreBackup(backupId, false, null); assertTrue(result); verify(backupDao, times(1)).findById(backupId); verify(vmInstanceDao, times(1)).findByIdIncludingRemoved(vmId); - verify(backupOfferingDao, times(2)).findByIdIncludingRemoved(offeringId); - verify(backupProvider, times(1)).restoreVMFromBackup(vm, backup); - verify(backupManager, times(1)).importRestoredVM(zoneId, domainId, accountId, userId, vmInstanceName, hypervisorType, backup); + verify(backupOfferingDao, times(1)).findByIdIncludingRemoved(offeringId); + verify(backupProvider, times(1)).restoreVMFromBackup(vm, backup, false, null); + verify(backupManager, times(1)).importRestoredVM(zoneId, domainId, accountId, userId, vmInstanceName, hypervisorType, backup, offering); } } @@ -2147,7 +2164,7 @@ public void testRestoreBackupBackupNotFound() { when(backupDao.findById(backupId)).thenReturn(null); CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class, - () -> backupManager.restoreBackup(backupId)); + () -> backupManager.restoreBackup(backupId, false, null)); assertEquals("Backup " + backupId + " does not exist", exception.getMessage()); verify(backupDao, times(1)).findById(backupId); @@ -2164,7 +2181,7 @@ public void testRestoreBackupBackupNotBackedUp() { when(backupDao.findById(backupId)).thenReturn(backup); CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class, - () -> backupManager.restoreBackup(backupId)); + () -> backupManager.restoreBackup(backupId, false, null)); assertEquals("Backup should be in BackedUp state", exception.getMessage()); verify(backupDao, times(1)).findById(backupId); @@ -2190,7 +2207,7 @@ public void testRestoreBackupVmExpunging() { doNothing().when(backupManager).validateBackupForZone(any()); CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class, - () -> backupManager.restoreBackup(backupId)); + () -> backupManager.restoreBackup(backupId, false, null)); assertEquals("The Instance from which the backup was taken could not be found.", exception.getMessage()); verify(backupDao, times(1)).findById(backupId); @@ -2217,7 +2234,7 @@ public void testRestoreBackupVmNotStopped() { doNothing().when(backupManager).validateBackupForZone(any()); CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class, - () -> backupManager.restoreBackup(backupId)); + () -> backupManager.restoreBackup(backupId, false, null)); assertEquals("Existing Instance should be stopped before being restored from Backup", exception.getMessage()); verify(backupDao, times(1)).findById(backupId); @@ -2248,13 +2265,15 @@ public void testRestoreBackupVolumeMismatch() { when(backupDao.findById(backupId)).thenReturn(backup); when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); doNothing().when(backupManager).validateBackupForZone(any()); + doReturn(new BackupOfferingVO()).when(backupOfferingDao).findByIdIncludingRemoved(Mockito.anyLong()); + doReturn(backupProvider).when(backupManager).getBackupProvider(Mockito.nullable(String.class)); try (MockedStatic utils = Mockito.mockStatic(ActionEventUtils.class)) { Mockito.when(ActionEventUtils.onStartedActionEvent(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.eq(true), Mockito.eq(0))).thenReturn(1L); CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class, - () -> backupManager.restoreBackup(backupId)); + () -> backupManager.restoreBackup(backupId, false, null)); assertEquals("Unable to restore Instance with the current Backup as the Backup has different number of disks to the Instance", exception.getMessage()); } @@ -2715,4 +2734,189 @@ public void testCloneBackupOfferingInheritsDomainIdsFromSource() { assertTrue(values.contains(String.valueOf(21L))); assertTrue(values.contains(String.valueOf(22L))); } + + @Test (expected = CloudRuntimeException.class) + public void createBackupOfferingTestOfferingAlreadyExists() { + long zoneId = 1L; + String offeringName = "name"; + + doNothing().when(backupManager).validateBackupForZone(zoneId); + doReturn(backupOfferingVOMock).when(backupOfferingDao).findByName(offeringName, zoneId); + CreateBackupOfferingCmd cmd = Mockito.mock(CreateBackupOfferingCmd.class); + doReturn(zoneId).when(cmd).getZoneId(); + doReturn(offeringName).when(cmd).getName(); + + backupManager.createBackupOffering(cmd); + + verify(backupManager).validateBackupForZone(zoneId); + } + + @Test (expected = InvalidParameterValueException.class) + public void createBackupOfferingTestInvalidDomainId() { + long zoneId = 1L; + String offeringName = "name"; + long domainId = 3L; + + doNothing().when(backupManager).validateBackupForZone(zoneId); + doReturn(null).when(backupOfferingDao).findByName(offeringName, zoneId); + CreateBackupOfferingCmd cmd = Mockito.mock(CreateBackupOfferingCmd.class); + doReturn(zoneId).when(cmd).getZoneId(); + doReturn(offeringName).when(cmd).getName(); + doReturn(List.of(domainId)).when(cmd).getDomainIds(); + doReturn(null).when(domainDao).findById(domainId); + + backupManager.createBackupOffering(cmd); + + verify(backupManager).validateBackupForZone(zoneId); + } + + @Test (expected = InvalidParameterValueException.class) + public void createBackupOfferingTestInvalidBackupProvider() { + long zoneId = 1L; + String offeringName = "name"; + + doNothing().when(backupManager).validateBackupForZone(zoneId); + doReturn(null).when(backupOfferingDao).findByName(offeringName, zoneId); + CreateBackupOfferingCmd cmd = Mockito.mock(CreateBackupOfferingCmd.class); + doReturn(zoneId).when(cmd).getZoneId(); + doReturn(offeringName).when(cmd).getName(); + doReturn(backupProvider).when(backupManager).getBackupProvider(zoneId); + doReturn("dummy").when(backupProvider).getName(); + + backupManager.createBackupOffering(cmd); + + verify(backupManager).validateBackupForZone(zoneId); + } + + @Test (expected = CloudRuntimeException.class) + public void createBackupOfferingTestInvalidProviderOffering() { + long zoneId = 1L; + String offeringName = "name"; + + doNothing().when(backupManager).validateBackupForZone(zoneId); + doReturn(null).when(backupOfferingDao).findByName(offeringName, zoneId); + CreateBackupOfferingCmd cmd = Mockito.mock(CreateBackupOfferingCmd.class); + doReturn(zoneId).when(cmd).getZoneId(); + doReturn(offeringName).when(cmd).getName(); + doReturn(backupProvider).when(backupManager).getBackupProvider(zoneId); + doReturn("kboss").when(backupProvider).getName(); + doReturn(false).when(backupProvider).isValidProviderOffering(zoneId, null); + + backupManager.createBackupOffering(cmd); + + verify(backupManager).validateBackupForZone(zoneId); + } + + @Test + public void createBackupOfferingTestAddsDetails() { + long zoneId = 1L; + String offeringName = "name"; + long domainId = 3L; + + doNothing().when(backupManager).validateBackupForZone(zoneId); + doReturn(null).when(backupOfferingDao).findByName(offeringName, zoneId); + CreateBackupOfferingCmd cmd = Mockito.mock(CreateBackupOfferingCmd.class); + doReturn(zoneId).when(cmd).getZoneId(); + doReturn(List.of(domainId)).when(cmd).getDomainIds(); + doReturn(domainVOMock).when(domainDao).findById(domainId); + doReturn(offeringName).when(cmd).getName(); + doReturn(backupProvider).when(backupManager).getBackupProvider(zoneId); + doReturn(backupOfferingVOMock).when(backupOfferingDao).persist(any()); + doReturn("kboss").when(backupProvider).getName(); + doReturn(true).when(backupProvider).isValidProviderOffering(zoneId, null); + doReturn(true).when(cmd).isCompress(); + doReturn(true).when(cmd).isValidate(); + doReturn(true).when(cmd).isAllowExtractFile(); + doReturn(true).when(cmd).isAllowQuickRestore(); + doReturn(3).when(cmd).getBackupChainSize(); + doReturn(Backup.CompressionLibrary.zlib).when(cmd).getCompressionLibrary(); + doReturn("execute_command").when(cmd).getValidationSteps(); + + backupManager.createBackupOffering(cmd); + + verify(backupManager).validateBackupForZone(zoneId); + verify(backupOfferingDao).persist(any()); + ArrayList detailsToBeSaved = new ArrayList<>(List.of(ApiConstants.DOMAIN_ID, ApiConstants.COMPRESS, ApiConstants.VALIDATE, ApiConstants.ALLOW_EXTRACT_FILE, + ApiConstants.ALLOW_QUICK_RESTORE, ApiConstants.BACKUP_CHAIN_SIZE, ApiConstants.COMPRESSION_LIBRARY, ApiConstants.VALIDATION_STEPS)); + verify(backupOfferingDetailsDao).saveDetails(ArgumentMatchers.argThat( detailList -> { + if (CollectionUtils.isEmpty(detailList) || detailList.size() < 8) { + return false; + } + for (BackupOfferingDetailsVO detailsVO : detailList) { + detailsToBeSaved.removeIf(detailName -> detailsVO.getName().equals(detailName)); + } + + return detailsToBeSaved.isEmpty(); + })); + } + + @Test + public void createBackupOfferingTestAddsNoDetails() { + long zoneId = 1L; + String offeringName = "name"; + + doNothing().when(backupManager).validateBackupForZone(zoneId); + doReturn(null).when(backupOfferingDao).findByName(offeringName, zoneId); + CreateBackupOfferingCmd cmd = Mockito.mock(CreateBackupOfferingCmd.class); + doReturn(zoneId).when(cmd).getZoneId(); + doReturn(offeringName).when(cmd).getName(); + doReturn(backupProvider).when(backupManager).getBackupProvider(zoneId); + doReturn(backupOfferingVOMock).when(backupOfferingDao).persist(any()); + doReturn("kboss").when(backupProvider).getName(); + doReturn(true).when(backupProvider).isValidProviderOffering(zoneId, null); + doReturn(false).when(cmd).isCompress(); + doReturn(false).when(cmd).isValidate(); + doReturn(false).when(cmd).isAllowExtractFile(); + doReturn(false).when(cmd).isAllowQuickRestore(); + doReturn(null).when(cmd).getBackupChainSize(); + doReturn(null).when(cmd).getCompressionLibrary(); + doReturn(null).when(cmd).getValidationSteps(); + + backupManager.createBackupOffering(cmd); + + verify(backupManager).validateBackupForZone(zoneId); + verify(backupOfferingDao).persist(any()); + verify(backupOfferingDetailsDao, never()).saveDetails(any()); + } + + @Test(expected = CloudRuntimeException.class) + public void endScheduleBackupChainIfNeededTestInvalidVirtualMachineThrowCloudRuntimeException() { + Mockito.doReturn(1L).when(backupScheduleVOMock).getVmId(); + + backupManager.finalizeBackupScheduleIfNeeded(backupScheduleVOMock); + } + + @Test(expected = CloudRuntimeException.class) + public void endScheduleBackupChainIfNeededTestInvalidBackupOfferingThrowCloudRuntimeException() { + Mockito.doReturn(1L).when(backupScheduleVOMock).getVmId(); + Mockito.doReturn(vmInstanceVOMock).when(vmInstanceDao).findById(1L); + + backupManager.finalizeBackupScheduleIfNeeded(backupScheduleVOMock); + } + + @Test + public void endScheduleBackupChainIfNeededTestBackupProviderSuccessDoesNotThrowException() { + Mockito.doReturn(1L).when(backupScheduleVOMock).getVmId(); + Mockito.doReturn(vmInstanceVOMock).when(vmInstanceDao).findById(1L); + Mockito.doReturn(2L).when(vmInstanceVOMock).getBackupOfferingId(); + Mockito.doReturn(backupOfferingVOMock).when(backupOfferingDao).findById(2L); + Mockito.doReturn(BackupManagerImpl.KBOSS_BACKUP_PROVIDER).when(backupOfferingVOMock).getProvider(); + Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider(BackupManagerImpl.KBOSS_BACKUP_PROVIDER); + Mockito.doReturn(true).when(backupProvider).removeVMBackupSchedule(vmInstanceVOMock, backupScheduleVOMock); + + backupManager.finalizeBackupScheduleIfNeeded(backupScheduleVOMock); + } + + @Test(expected = CloudRuntimeException.class) + public void endScheduleBackupChainIfNeededTestBackupProviderFailThrowCloudRuntimeException() { + Mockito.doReturn(1L).when(backupScheduleVOMock).getVmId(); + Mockito.doReturn(vmInstanceVOMock).when(vmInstanceDao).findById(1L); + Mockito.doReturn(2L).when(vmInstanceVOMock).getBackupOfferingId(); + Mockito.doReturn(backupOfferingVOMock).when(backupOfferingDao).findById(2L); + Mockito.doReturn(BackupManagerImpl.KBOSS_BACKUP_PROVIDER).when(backupOfferingVOMock).getProvider(); + Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider(BackupManagerImpl.KBOSS_BACKUP_PROVIDER); + Mockito.doReturn(false).when(backupProvider).removeVMBackupSchedule(vmInstanceVOMock, backupScheduleVOMock); + + backupManager.finalizeBackupScheduleIfNeeded(backupScheduleVOMock); + } } diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupValidationServiceJobControllerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupValidationServiceJobControllerTest.java new file mode 100644 index 000000000000..cd3fe9a80953 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupValidationServiceJobControllerTest.java @@ -0,0 +1,150 @@ +// 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.backup; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.util.HashMap; +import java.util.List; + +import org.apache.cloudstack.backup.dao.InternalBackupServiceJobDao; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.host.HostVO; +import com.cloud.utils.Pair; + +@RunWith(MockitoJUnitRunner.class) +public class BackupValidationServiceJobControllerTest { + + @Mock + private DataCenterDao dataCenterDaoMock; + + @Mock + private DataCenterVO dataCenterVoMock; + + @Mock + private HostVO hostVO; + + @Mock + private InternalBackupServiceJobDao internalBackupServiceJobDaoMock; + + @Mock + private InternalBackupServiceJobVO internalBackupServiceJobVoMock; + + @Mock + private ConfigKey backupValidationTaskEnabledMock; + + @Spy + @InjectMocks + private BackupValidationServiceJobController backupValidationServiceJobControllerSpy; + + long datacenterId = 1L; + + @Test + public void searchAndDispatchJobsTestLockFailureReturnsEarly() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(false).when(internalBackupServiceJobDaoMock).lockInLockTable("validation_lock", 300); + + backupValidationServiceJobControllerSpy.searchAndDispatchJobs(); + + verify(internalBackupServiceJobDaoMock).unlockFromLockTable(any()); + verify(backupValidationServiceJobControllerSpy, never()).rescheduleLostJobs(); + verify(internalBackupServiceJobDaoMock, never()).listWaitingJobsAndScheduledToBeforeNow(anyLong(), any()); + } + + @Test + public void searchAndDispatchJobsTestTaskDisabledReturnsAfterReschedule() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(true).when(internalBackupServiceJobDaoMock).lockInLockTable("validation_lock", 300); + doNothing().when(backupValidationServiceJobControllerSpy).rescheduleLostJobs(); + doReturn(false).when(backupValidationTaskEnabledMock).value(); + + backupValidationServiceJobControllerSpy.searchAndDispatchJobs(); + + verify(backupValidationServiceJobControllerSpy).rescheduleLostJobs(); + verify(internalBackupServiceJobDaoMock, never()).listWaitingJobsAndScheduledToBeforeNow(anyLong(), any()); + verify(internalBackupServiceJobDaoMock).unlockFromLockTable("validation_lock"); + } + + @Test + public void searchAndDispatchJobsTestZoneWithoutBackupFrameworkIsSkipped() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(true).when(internalBackupServiceJobDaoMock).lockInLockTable("validation_lock", 300); + doNothing().when(backupValidationServiceJobControllerSpy).rescheduleLostJobs(); + doReturn(true).when(backupValidationTaskEnabledMock).value(); + doReturn(false).when(backupValidationServiceJobControllerSpy).isFrameworkEnabledForZone(dataCenterVoMock); + + backupValidationServiceJobControllerSpy.searchAndDispatchJobs(); + + verify(internalBackupServiceJobDaoMock, never()).listWaitingJobsAndScheduledToBeforeNow(anyLong(), any()); + verify(internalBackupServiceJobDaoMock).unlockFromLockTable("validation_lock"); + } + + @Test + public void searchAndDispatchJobsTestEmptyWaitingJobsSkipsDispatch() { + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(true).when(internalBackupServiceJobDaoMock).lockInLockTable("validation_lock", 300); + doNothing().when(backupValidationServiceJobControllerSpy).rescheduleLostJobs(); + doReturn(true).when(backupValidationTaskEnabledMock).value(); + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(true).when(backupValidationServiceJobControllerSpy).isFrameworkEnabledForZone(dataCenterVoMock); + + backupValidationServiceJobControllerSpy.searchAndDispatchJobs(); + + verify(backupValidationServiceJobControllerSpy, never()).getHostToNumberOfExecutingJobsAndTotalExecutingJobs(any(), any()); + verify(backupValidationServiceJobControllerSpy, never()).submitQueuedJobsForExecution(any(), any(), any(), any(), anyLong()); + verify(internalBackupServiceJobDaoMock).unlockFromLockTable("validation_lock"); + } + + @Test + public void searchAndDispatchJobsTestHappyPathDispatchesJobs() { + Pair, Integer> executingJobsPair = new Pair<>(new HashMap<>(), 0); + + doReturn(List.of(dataCenterVoMock)).when(dataCenterDaoMock).listEnabledZones(); + doReturn(true).when(internalBackupServiceJobDaoMock).lockInLockTable("validation_lock", 300); + doNothing().when(backupValidationServiceJobControllerSpy).rescheduleLostJobs(); + doReturn(true).when(backupValidationTaskEnabledMock).value(); + doReturn(datacenterId).when(dataCenterVoMock).getId(); + doReturn(true).when(backupValidationServiceJobControllerSpy).isFrameworkEnabledForZone(dataCenterVoMock); + doReturn(List.of(internalBackupServiceJobVoMock)).when(backupValidationServiceJobControllerSpy).filterJobsOfDomainsAndAccountsWithDisabledValidationTask(any()); + doReturn(executingJobsPair).when(backupValidationServiceJobControllerSpy).getHostToNumberOfExecutingJobsAndTotalExecutingJobs(eq(dataCenterVoMock), any()); + doReturn(List.of(new Pair<>(hostVO, 0L))).when(backupValidationServiceJobControllerSpy).filterHostsWithTooManyJobs(any(), any()); + doReturn(List.of(internalBackupServiceJobVoMock)).when(backupValidationServiceJobControllerSpy).thinJobsToStartList(eq(dataCenterVoMock), any(), anyInt(), any()); + doNothing().when(backupValidationServiceJobControllerSpy).submitQueuedJobsForExecution(any(), any(), any(), any(), eq(datacenterId)); + + backupValidationServiceJobControllerSpy.searchAndDispatchJobs(); + + + verify(backupValidationServiceJobControllerSpy).submitQueuedJobsForExecution(any(), any(), any(), any(), eq(datacenterId)); + verify(internalBackupServiceJobDaoMock).unlockFromLockTable("validation_lock"); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/backup/InternalBackupServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/backup/InternalBackupServiceImplTest.java new file mode 100644 index 000000000000..5ad0aabaf825 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/backup/InternalBackupServiceImplTest.java @@ -0,0 +1,482 @@ +// 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.backup; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.util.List; + +import org.apache.cloudstack.api.response.ExtractResponse; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.backup.dao.InternalBackupStoragePoolDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.storage.datastore.db.ImageStoreObjectDownloadDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreObjectDownloadVO; +import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.DataTO; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Storage; +import com.cloud.storage.Upload; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.snapshot.VMSnapshot; + +@RunWith(MockitoJUnitRunner.class) +public class InternalBackupServiceImplTest { + + @Mock + private InternalBackupStoragePoolDao internalBackupStoragePoolDaoMock; + + @Mock + private VolumeObjectTO volumeObjectToMock; + + @Mock + private SnapshotObjectTO snapshotObjectToMock; + + @Mock + private InternalBackupStoragePoolVO internalBackupStoragePoolVoMock; + + @Mock + private InternalBackupJoinDao internalBackupJoinDaoMock; + + @Mock + private BackupDetailsDao backupDetailDaoMock; + + @Mock + private BackupDetailVO backupDetailVoMock; + + @Mock + private BackupDetailVO backupDetailVoMock2; + + @Mock + private InternalBackupJoinVO internalBackupJoinVoMock; + + @Mock + private Volume volumeMock; + + @Mock + private VolumeVO volumeVoMock; + + @Mock + private VirtualMachine virtualMachineMock; + + @Mock + private InternalBackupProvider internalBackupProviderMock; + + @Mock + private VirtualMachineManager virtualMachineManagerMock; + + @Mock + private VolumeDao volumeDaoMock; + + @Mock + private ImageStoreObjectDownloadDao imageStoreObjectDownloadDaoMock; + + @Mock + private DataStoreManager dataStoreMgrMock; + + @Mock + private ImageStoreEntity imageStoreEntityMock; + + @Mock + private ImageStoreObjectDownloadVO imageStoreObjectDownloadVoMock; + + @Mock + private VMSnapshot vmSnapshotMock; + + @Spy + @InjectMocks + private InternalBackupServiceImpl internalBackupServiceImplSpy; + + private static final long IMAGE_STORE_ID = 7L; + private static final String SCREENSHOT_PATH = "/tmp/screenshot.png"; + private static final long VOLUME_ID = 42L; + private static final long BACKUP_ID = 100L; + private static final long ZONE_ID = 1L; + private static final long OLD_VOLUME_ID = 5L; + private static final long NEW_VOLUME_ID = 6L; + private static final long INSTANCE_ID = 10L; + + @Test + public void configureChainInfoTestNonVolumeObjectReturnsImmediately() { + DataTO dataToMock = mock(DataTO.class); + Command cmdMock = mock(Command.class); + + internalBackupServiceImplSpy.configureChainInfo(dataToMock, cmdMock); + + verify(internalBackupStoragePoolDaoMock, never()).listByVolumeId(anyLong()); + } + + @Test + public void cleanupBackupMetadataTestNoDeltaReturnsImmediately() { + internalBackupServiceImplSpy.cleanupBackupMetadata(VOLUME_ID); + + verify(internalBackupStoragePoolDaoMock, never()).expungeByVolumeId(VOLUME_ID); + verify(internalBackupJoinDaoMock, never()).findById(anyLong()); + } + + @Test + public void cleanupBackupMetadataTestDeltaExistsButOtherDeltasRemainReturnsImmediately() { + doReturn(BACKUP_ID).when(internalBackupJoinVoMock).getId(); + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(VOLUME_ID); + doReturn(List.of(internalBackupStoragePoolVoMock, mock(InternalBackupStoragePoolVO.class))) + .when(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + + internalBackupServiceImplSpy.cleanupBackupMetadata(VOLUME_ID); + + verify(internalBackupStoragePoolDaoMock).expungeByVolumeId(VOLUME_ID); + verify(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + verify(internalBackupJoinDaoMock, never()).findById(anyLong()); + } + + @Test + public void cleanupBackupMetadataTestLastDeltaAndEndOfChainTrue() { + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(VOLUME_ID); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + doReturn(BACKUP_ID).when(internalBackupJoinVoMock).getId(); + doReturn(true).when(internalBackupJoinVoMock).getEndOfChain(); + + internalBackupServiceImplSpy.cleanupBackupMetadata(VOLUME_ID); + + verify(internalBackupStoragePoolDaoMock).expungeByVolumeId(VOLUME_ID); + verify(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + verify(backupDetailDaoMock).removeDetail(BACKUP_ID, BackupDetailsDao.CURRENT); + verify(backupDetailDaoMock, never()).persist(any()); + } + + @Test + public void cleanupBackupMetadataTestLastDeltaAndEndOfChainFalse() { + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(VOLUME_ID); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + doReturn(BACKUP_ID).when(internalBackupJoinVoMock).getId(); + doReturn(false).when(internalBackupJoinVoMock).getEndOfChain(); + + internalBackupServiceImplSpy.cleanupBackupMetadata(VOLUME_ID); + + verify(internalBackupStoragePoolDaoMock).expungeByVolumeId(VOLUME_ID); + verify(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + verify(backupDetailDaoMock).removeDetail(BACKUP_ID, BackupDetailsDao.CURRENT); + verify(backupDetailDaoMock).persist(any(BackupDetailVO.class)); + } + + @Test + public void prepareVolumeForDetachTestBackupFrameworkDisabledReturnsImmediately() { + doReturn(true).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + + internalBackupServiceImplSpy.prepareVolumeForDetach(volumeMock, virtualMachineMock); + + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy, never()).getInternalBackupProviderForZone(anyLong()); + } + + @Test + public void prepareVolumeForDetachTestProviderIsNullReturnsImmediately() { + doReturn(false).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + doReturn(ZONE_ID).when(virtualMachineMock).getDataCenterId(); + doReturn(null).when(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + + internalBackupServiceImplSpy.prepareVolumeForDetach(volumeMock, virtualMachineMock); + + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + verify(internalBackupProviderMock, never()).prepareVolumeForDetach(any(), any()); + } + + @Test + public void prepareVolumeForDetachTestProviderCallsPrepareVolumeForDetach() { + doReturn(false).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + doReturn(ZONE_ID).when(virtualMachineMock).getDataCenterId(); + doReturn(internalBackupProviderMock).when(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + doNothing().when(internalBackupProviderMock).prepareVolumeForDetach(volumeMock, virtualMachineMock); + + internalBackupServiceImplSpy.prepareVolumeForDetach(volumeMock, virtualMachineMock); + + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + verify(internalBackupProviderMock).prepareVolumeForDetach(volumeMock, virtualMachineMock); + } + + @Test + public void prepareVolumeForMigrationTestVolumeInstanceIdIsNullReturnsImmediately() { + doReturn(null).when(volumeMock).getInstanceId(); + + internalBackupServiceImplSpy.prepareVolumeForMigration(volumeMock); + + verify(volumeMock).getInstanceId(); + verify(virtualMachineManagerMock, never()).findById(anyLong()); + } + + @Test + public void prepareVolumeForMigrationTestBackupFrameworkDisabledReturnsImmediately() { + doReturn(INSTANCE_ID).when(volumeMock).getInstanceId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(INSTANCE_ID); + doReturn(true).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + + internalBackupServiceImplSpy.prepareVolumeForMigration(volumeMock); + + verify(virtualMachineManagerMock).findById(INSTANCE_ID); + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy, never()).getInternalBackupProviderForZone(anyLong()); + } + + @Test + public void prepareVolumeForMigrationTestProviderIsNullReturnsImmediately() { + doReturn(INSTANCE_ID).when(volumeMock).getInstanceId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(INSTANCE_ID); + doReturn(false).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + doReturn(ZONE_ID).when(volumeMock).getDataCenterId(); + doReturn(null).when(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + + internalBackupServiceImplSpy.prepareVolumeForMigration(volumeMock); + + verify(virtualMachineManagerMock).findById(INSTANCE_ID); + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + verify(internalBackupProviderMock, never()).prepareVolumeForMigration(any(), any()); + } + + @Test + public void prepareVolumeForMigrationTestProviderCallsPrepareVolumeForMigration() { + doReturn(INSTANCE_ID).when(volumeMock).getInstanceId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(INSTANCE_ID); + doReturn(false).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + doReturn(ZONE_ID).when(volumeMock).getDataCenterId(); + doReturn(internalBackupProviderMock).when(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + doNothing().when(internalBackupProviderMock).prepareVolumeForMigration(volumeMock, virtualMachineMock); + + internalBackupServiceImplSpy.prepareVolumeForMigration(volumeMock); + + verify(virtualMachineManagerMock).findById(INSTANCE_ID); + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + verify(internalBackupProviderMock).prepareVolumeForMigration(volumeMock, virtualMachineMock); + } + + @Test + public void updateVolumeIdTestVolumeInstanceIdIsNullReturnsImmediately() { + doReturn(volumeVoMock).when(volumeDaoMock).findById(NEW_VOLUME_ID); + doReturn(null).when(volumeVoMock).getInstanceId(); + + internalBackupServiceImplSpy.updateVolumeId(OLD_VOLUME_ID, NEW_VOLUME_ID); + + verify(volumeDaoMock).findById(NEW_VOLUME_ID); + verify(volumeVoMock).getInstanceId(); + verify(virtualMachineManagerMock, never()).findById(anyLong()); + } + + @Test + public void updateVolumeIdTestBackupFrameworkDisabledReturnsImmediately() { + doReturn(volumeVoMock).when(volumeDaoMock).findById(NEW_VOLUME_ID); + doReturn(INSTANCE_ID).when(volumeVoMock).getInstanceId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(INSTANCE_ID); + doReturn(true).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + + internalBackupServiceImplSpy.updateVolumeId(OLD_VOLUME_ID, NEW_VOLUME_ID); + + verify(volumeDaoMock).findById(NEW_VOLUME_ID); + verify(virtualMachineManagerMock).findById(INSTANCE_ID); + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy, never()).getInternalBackupProviderForZone(anyLong()); + } + + @Test + public void updateVolumeIdTestProviderIsNullReturnsImmediately() { + doReturn(volumeVoMock).when(volumeDaoMock).findById(NEW_VOLUME_ID); + doReturn(INSTANCE_ID).when(volumeVoMock).getInstanceId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(INSTANCE_ID); + doReturn(false).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + doReturn(ZONE_ID).when(virtualMachineMock).getDataCenterId(); + doReturn(null).when(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + + internalBackupServiceImplSpy.updateVolumeId(OLD_VOLUME_ID, NEW_VOLUME_ID); + + verify(volumeDaoMock).findById(NEW_VOLUME_ID); + verify(virtualMachineManagerMock).findById(INSTANCE_ID); + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + verify(internalBackupProviderMock, never()).updateVolumeId(any(), anyLong(), anyLong()); + } + + @Test + public void updateVolumeIdTestProviderCallsUpdateVolumeId() { + doReturn(volumeVoMock).when(volumeDaoMock).findById(NEW_VOLUME_ID); + doReturn(INSTANCE_ID).when(volumeVoMock).getInstanceId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(INSTANCE_ID); + doReturn(false).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + doReturn(ZONE_ID).when(virtualMachineMock).getDataCenterId(); + doReturn(internalBackupProviderMock).when(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + doNothing().when(internalBackupProviderMock).updateVolumeId(virtualMachineMock, OLD_VOLUME_ID, NEW_VOLUME_ID); + + internalBackupServiceImplSpy.updateVolumeId(OLD_VOLUME_ID, NEW_VOLUME_ID); + + verify(volumeDaoMock).findById(NEW_VOLUME_ID); + verify(virtualMachineManagerMock).findById(INSTANCE_ID); + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + verify(internalBackupProviderMock).updateVolumeId(virtualMachineMock, OLD_VOLUME_ID, NEW_VOLUME_ID); + } + + @Test + public void downloadScreenshotTestScreenshotPathDetailMissingReturnsNotCreated() { + doReturn(null).when(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.SCREENSHOT_PATH); + + ExtractResponse result = internalBackupServiceImplSpy.downloadScreenshot(BACKUP_ID); + + verify(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.SCREENSHOT_PATH); + assertEquals(Upload.Status.DOWNLOAD_URL_NOT_CREATED.toString(), result.getState()); + } + + @Test + public void downloadScreenshotTestImageStoreObjectExistsReturnsCreatedResponse() { + doReturn(backupDetailVoMock).when(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.SCREENSHOT_PATH); + doReturn(backupDetailVoMock2).when(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.IMAGE_STORE_ID); + doReturn(String.valueOf(IMAGE_STORE_ID)).when(backupDetailVoMock2).getValue(); + doReturn(SCREENSHOT_PATH).when(backupDetailVoMock).getValue(); + doReturn(imageStoreEntityMock).when(dataStoreMgrMock).getDataStore(IMAGE_STORE_ID, DataStoreRole.Image); + doReturn(imageStoreObjectDownloadVoMock).when(imageStoreObjectDownloadDaoMock) + .findByStoreIdAndPath(IMAGE_STORE_ID, SCREENSHOT_PATH); + doReturn("http://download/url").when(imageStoreObjectDownloadVoMock).getDownloadUrl(); + + ExtractResponse result = internalBackupServiceImplSpy.downloadScreenshot(BACKUP_ID); + + verify(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.SCREENSHOT_PATH); + verify(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.IMAGE_STORE_ID); + verify(imageStoreObjectDownloadDaoMock).findByStoreIdAndPath(IMAGE_STORE_ID, SCREENSHOT_PATH); + verify(imageStoreObjectDownloadDaoMock, never()).persist(any()); + assertEquals("http://download/url", result.getUrl()); + assertEquals("screenshot.png", result.getName()); + assertEquals(Upload.Status.DOWNLOAD_URL_CREATED.toString(), result.getState()); + } + + @Test + public void downloadScreenshotTestImageStoreObjectMissingButPersistSucceedsReturnsCreatedResponse() { + doReturn(backupDetailVoMock).when(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.SCREENSHOT_PATH); + doReturn(backupDetailVoMock2).when(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.IMAGE_STORE_ID); + doReturn(String.valueOf(IMAGE_STORE_ID)).when(backupDetailVoMock2).getValue(); + doReturn(SCREENSHOT_PATH).when(backupDetailVoMock).getValue(); + doReturn(imageStoreEntityMock).when(dataStoreMgrMock).getDataStore(IMAGE_STORE_ID, DataStoreRole.Image); + doReturn(null).when(imageStoreObjectDownloadDaoMock).findByStoreIdAndPath(IMAGE_STORE_ID, SCREENSHOT_PATH); + doReturn(123L).when(imageStoreEntityMock).getId(); + doReturn(imageStoreObjectDownloadVoMock).when(imageStoreObjectDownloadDaoMock).persist(any(ImageStoreObjectDownloadVO.class)); + doReturn("http://download/url").when(imageStoreObjectDownloadVoMock).getDownloadUrl(); + + ExtractResponse result = internalBackupServiceImplSpy.downloadScreenshot(BACKUP_ID); + + verify(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.SCREENSHOT_PATH); + verify(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.IMAGE_STORE_ID); + verify(imageStoreObjectDownloadDaoMock).findByStoreIdAndPath(IMAGE_STORE_ID, SCREENSHOT_PATH); + verify(imageStoreEntityMock).createEntityExtractUrl(eq(SCREENSHOT_PATH), eq(Storage.ImageFormat.PNG), any()); + verify(imageStoreObjectDownloadDaoMock).persist(any(ImageStoreObjectDownloadVO.class)); + assertEquals("http://download/url", result.getUrl()); + assertEquals("screenshot.png", result.getName()); + assertEquals(Upload.Status.DOWNLOAD_URL_CREATED.toString(), result.getState()); + } + + @Test + public void downloadScreenshotTestImageStoreObjectMissingAndPersistReturnsNullReturnsNotCreated() { + doReturn(backupDetailVoMock).when(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.SCREENSHOT_PATH); + doReturn(backupDetailVoMock2).when(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.IMAGE_STORE_ID); + doReturn(String.valueOf(IMAGE_STORE_ID)).when(backupDetailVoMock2).getValue(); + doReturn(SCREENSHOT_PATH).when(backupDetailVoMock).getValue(); + doReturn(imageStoreEntityMock).when(dataStoreMgrMock).getDataStore(IMAGE_STORE_ID, DataStoreRole.Image); + doReturn(null).when(imageStoreObjectDownloadDaoMock).findByStoreIdAndPath(IMAGE_STORE_ID, SCREENSHOT_PATH); + doReturn(123L).when(imageStoreEntityMock).getId(); + doReturn(null).when(imageStoreObjectDownloadDaoMock).persist(any(ImageStoreObjectDownloadVO.class)); + + ExtractResponse result = internalBackupServiceImplSpy.downloadScreenshot(BACKUP_ID); + + verify(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.SCREENSHOT_PATH); + verify(backupDetailDaoMock).findDetail(BACKUP_ID, BackupDetailsDao.IMAGE_STORE_ID); + verify(imageStoreObjectDownloadDaoMock).findByStoreIdAndPath(IMAGE_STORE_ID, SCREENSHOT_PATH); + verify(imageStoreEntityMock).createEntityExtractUrl(eq(SCREENSHOT_PATH), eq(Storage.ImageFormat.PNG), any()); + verify(imageStoreObjectDownloadDaoMock).persist(any(ImageStoreObjectDownloadVO.class)); + org.junit.Assert.assertNull(result.getUrl()); + org.junit.Assert.assertNull(result.getName()); + org.junit.Assert.assertEquals(Upload.Status.DOWNLOAD_URL_NOT_CREATED.toString(), result.getState()); + } + + @Test + public void prepareVmForSnapshotRevertTestBackupFrameworkDisabledReturnsImmediately() { + doReturn(INSTANCE_ID).when(vmSnapshotMock).getVmId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(INSTANCE_ID); + doReturn(true).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + + internalBackupServiceImplSpy.prepareVmForSnapshotRevert(vmSnapshotMock); + + verify(vmSnapshotMock).getVmId(); + verify(virtualMachineManagerMock).findById(INSTANCE_ID); + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy, never()).getInternalBackupProviderForZone(anyLong()); + } + + @Test + public void prepareVmForSnapshotRevertTestProviderIsNullReturnsImmediately() { + doReturn(INSTANCE_ID).when(vmSnapshotMock).getVmId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(INSTANCE_ID); + doReturn(false).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + doReturn(ZONE_ID).when(virtualMachineMock).getDataCenterId(); + doReturn(null).when(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + + internalBackupServiceImplSpy.prepareVmForSnapshotRevert(vmSnapshotMock); + + verify(vmSnapshotMock).getVmId(); + verify(virtualMachineManagerMock).findById(INSTANCE_ID); + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + verify(internalBackupProviderMock, never()).prepareVmForSnapshotRevert(any(), any()); + } + + @Test + public void prepareVmForSnapshotRevertTestProviderCallsPrepareVmForSnapshotRevert() { + doReturn(INSTANCE_ID).when(vmSnapshotMock).getVmId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(INSTANCE_ID); + doReturn(false).when(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + doReturn(ZONE_ID).when(virtualMachineMock).getDataCenterId(); + doReturn(internalBackupProviderMock).when(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + doNothing().when(internalBackupProviderMock).prepareVmForSnapshotRevert(vmSnapshotMock, virtualMachineMock); + + internalBackupServiceImplSpy.prepareVmForSnapshotRevert(vmSnapshotMock); + + verify(vmSnapshotMock).getVmId(); + verify(virtualMachineManagerMock).findById(INSTANCE_ID); + verify(internalBackupServiceImplSpy).isBackupFrameworkDisabled(virtualMachineMock); + verify(internalBackupServiceImplSpy).getInternalBackupProviderForZone(ZONE_ID); + verify(internalBackupProviderMock).prepareVmForSnapshotRevert(vmSnapshotMock, virtualMachineMock); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelperTest.java b/server/src/test/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelperTest.java index 032e947fdce7..ece7e69f3f76 100644 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelperTest.java +++ b/server/src/test/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelperTest.java @@ -21,6 +21,7 @@ import com.cloud.storage.VMTemplateVO; import com.cloud.storage.VolumeVO; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.backup.BackupVO; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; @@ -58,6 +59,9 @@ public class HeuristicRuleHelperTest { @Mock VolumeVO volumeVOMock; + @Mock + BackupVO backupVOMock; + @Mock DataStoreManager dataStoreManagerMock; @@ -165,6 +169,21 @@ public void buildPresetVariablesTestWithSnapshotHeuristicTypeShouldSetVolumeAndS Mockito.verify(heuristicRuleHelperSpy, Mockito.times(1)).injectPresetVariables(Mockito.isNull(), Mockito.any(PresetVariables.class)); } + @Test + public void buildPresetVariablesTestWithBackupHeuristicTypeShouldSetBackupAndSecondaryStorageAndAccountPresetVariables() { + Mockito.doNothing().when(heuristicRuleHelperSpy).injectPresetVariables(Mockito.isNull(), Mockito.any(PresetVariables.class)); + Mockito.doReturn(null).when(heuristicRuleHelperSpy).setBackupPresetVariable(Mockito.any(BackupVO.class)); + Mockito.doReturn(null).when(heuristicRuleHelperSpy).setSecondaryStoragesVariable(Mockito.anyLong()); + Mockito.doReturn(null).when(heuristicRuleHelperSpy).setAccountPresetVariable(Mockito.anyLong()); + + heuristicRuleHelperSpy.buildPresetVariables(null, HeuristicType.BACKUP, 1L, backupVOMock); + + Mockito.verify(heuristicRuleHelperSpy, Mockito.times(1)).setBackupPresetVariable(Mockito.any(BackupVO.class)); + Mockito.verify(heuristicRuleHelperSpy, Mockito.times(1)).setSecondaryStoragesVariable(Mockito.anyLong()); + Mockito.verify(heuristicRuleHelperSpy, Mockito.times(1)).setAccountPresetVariable(Mockito.anyLong()); + Mockito.verify(heuristicRuleHelperSpy, Mockito.times(1)).injectPresetVariables(Mockito.isNull(), Mockito.any(PresetVariables.class)); + } + @Test public void interpretHeuristicRuleTestHeuristicRuleDoesNotReturnAStringShouldThrowCloudRuntimeException() { String heuristicRule = "1"; diff --git a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java index 3c3f1f5747b8..4345ff24e3c5 100644 --- a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java +++ b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java @@ -57,6 +57,7 @@ import org.apache.cloudstack.framework.security.keystore.KeystoreManager; import org.apache.cloudstack.storage.NfsMountManagerImpl.PathParser; +import org.apache.cloudstack.storage.command.BackupDeleteAnswer; import org.apache.cloudstack.storage.command.CopyCmdAnswer; import org.apache.cloudstack.storage.command.CopyCommand; import org.apache.cloudstack.storage.command.DeleteCommand; @@ -78,6 +79,7 @@ import org.apache.cloudstack.storage.template.UploadEntity; import org.apache.cloudstack.storage.template.UploadManager; import org.apache.cloudstack.storage.template.UploadManagerImpl; +import org.apache.cloudstack.storage.to.BackupDeltaTO; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -2142,6 +2144,74 @@ protected Answer deleteSnapshot(final DeleteCommand cmd) { } + protected Answer deleteBackup(DeleteCommand cmd) { + BackupDeltaTO deltaTo = (BackupDeltaTO) cmd.getData(); + NfsTO nfs = (NfsTO)deltaTo.getDataStore(); + String parent = getRootDir(nfs.getUrl(), _nfsVersion); + if (!parent.endsWith(File.separator)) { + parent += File.separator; + } + String backupRelativePath = deltaTo.getPath(); + if (backupRelativePath.startsWith(File.separator)) { + backupRelativePath = backupRelativePath.substring(1); + } + + String fullDeltaPath = parent + backupRelativePath; + File deltaFile = new File(fullDeltaPath); + logger.debug("Deleting backup at [{}].", fullDeltaPath); + String deltaDeleteResult = deleteLocalFile(fullDeltaPath); + + String details; + if (deltaDeleteResult != null) { + details = String.format("Failed to delete backup delta [%s] with result [%s]. ", fullDeltaPath, deltaDeleteResult); + logger.warn(details); + return new BackupDeleteAnswer(cmd, false, details); + } + + String screenshotRelativePath = deltaTo.getScreenshotPath(); + BackupDeleteAnswer answer = deleteScreenshot(cmd, screenshotRelativePath, parent); + if (answer != null) { + return answer; + } + + File deltaDir = deltaFile.getParentFile(); + if (!deleteEmptyDirectory(deltaDir)) { + details = String.format("Unable to delete directory [%s] at path [%s].", deltaDir.getName(), deltaDir.getPath()); + logger.debug(details); + return new BackupDeleteAnswer(cmd, false, details); + } + + return new Answer(cmd, true, null); + } + + protected BackupDeleteAnswer deleteScreenshot(DeleteCommand cmd, String screenshotRelativePath, String parent) { + if (screenshotRelativePath == null) { + return null; + } + if (screenshotRelativePath.startsWith(File.separator)) { + screenshotRelativePath = screenshotRelativePath.substring(1); + } + String fullScreenshotPath = parent + screenshotRelativePath; + logger.debug("Deleting screenshot at [{}].", fullScreenshotPath); + String screenshotDeleteResult = deleteLocalFile(fullScreenshotPath); + if (screenshotDeleteResult != null) { + String details = String.format("Failed to delete backup validation screenshot [%s] with result [%s]. ", fullScreenshotPath, screenshotDeleteResult); + logger.warn(details); + return new BackupDeleteAnswer(cmd, false, details); + } + return null; + } + + protected boolean deleteEmptyDirectory(File dir) { + if (dir == null || !dir.isDirectory()) { + return true; + } + if (dir.list().length > 0) { + return true; + } + return dir.delete(); + } + private String deleteCheckpointIfExists(DataTO obj, String parent) { SnapshotObjectTO snapshotObjectTO = (SnapshotObjectTO) obj; String checkpointPath = snapshotObjectTO.getCheckpointPath(); @@ -2333,7 +2403,7 @@ private Answer execute(SecStorageVMSetupCommand cmd) { } - private String deleteLocalFile(String fullPath) { + protected String deleteLocalFile(String fullPath) { Script command = new Script("/bin/bash", logger); command.add("-c"); command.add("rm -rf " + fullPath); @@ -2468,6 +2538,8 @@ protected Answer execute(final DeleteCommand cmd) { return deleteVolume(cmd); case SNAPSHOT: return deleteSnapshot(cmd); + case BACKUP: + return deleteBackup(cmd); } return Answer.createUnsupportedCommandAnswer(cmd); } diff --git a/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResourceTest.java b/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResourceTest.java index 5917e1287639..4ed6001d3b7e 100644 --- a/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResourceTest.java +++ b/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResourceTest.java @@ -18,9 +18,15 @@ */ package org.apache.cloudstack.storage.resource; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.io.File; import java.nio.file.Files; @@ -30,9 +36,13 @@ import java.util.Map; import java.util.stream.Stream; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.NfsTO; +import org.apache.cloudstack.storage.command.BackupDeleteAnswer; import org.apache.cloudstack.storage.command.DeleteCommand; import org.apache.cloudstack.storage.command.QuerySnapshotZoneCopyAnswer; import org.apache.cloudstack.storage.command.QuerySnapshotZoneCopyCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.logging.log4j.Logger; @@ -76,6 +86,15 @@ public class NfsSecondaryStorageResourceTest { @Mock private Logger loggerMock; + @Mock + private DeleteCommand deleteCommandMock; + + @Mock + private BackupDeltaTO backupDeltaTOMock; + + @Mock + private NfsTO nfsMock; + @Test public void testSwiftWriteMetadataFile() throws Exception { String metaFileName = "test_metadata_file"; @@ -87,7 +106,7 @@ public void testSwiftWriteMetadataFile() throws Exception { File metaFile = resource.swiftWriteMetadataFile(metaFileName, uniqueName, filename, size, virtualSize); - Assert.assertTrue(metaFile.exists()); + assertTrue(metaFile.exists()); Assert.assertEquals(metaFileName, metaFile.getName()); String expectedContent = "uniquename=" + uniqueName + "\n" + @@ -114,7 +133,7 @@ public void testCleanupStagingNfs() throws Exception{ spyResource.cleanupStagingNfs(mockTemplate); - Mockito.verify(loggerMock, times(1)).debug("Failed to clean up staging area:", exception); + verify(loggerMock, times(1)).debug("Failed to clean up staging area:", exception); } @@ -164,8 +183,8 @@ public void testExecuteQuerySnapshotZoneCopyCommand() { } private void prepareForValidatePostUploadRequestSignatureTests(MockedStatic encryptionUtilMock) { - Mockito.doReturn(PROTOCOL).when(resource).getUploadProtocol(); - Mockito.doReturn(PSK).when(resource).getPostUploadPSK(); + doReturn(PROTOCOL).when(resource).getUploadProtocol(); + doReturn(PSK).when(resource).getPostUploadPSK(); encryptionUtilMock.when(() -> EncryptionUtil.generateSignature(Mockito.anyString(), Mockito.anyString())).thenReturn(COMPUTED_SIGNATURE); String fullUrl = String.format("%s://%s/upload/%s", PROTOCOL, HOSTNAME, UUID); String data = String.format("%s%s%s", METADATA, fullUrl, TIMEOUT); @@ -176,7 +195,7 @@ private void prepareForValidatePostUploadRequestSignatureTests(MockedStatic encryptionUtilMock = Mockito.mockStatic(EncryptionUtil.class)) { prepareForValidatePostUploadRequestSignatureTests(encryptionUtilMock); - Mockito.doReturn(NetUtils.HTTPS_PROTO).when(resource).getUploadProtocol(); + doReturn(NetUtils.HTTPS_PROTO).when(resource).getUploadProtocol(); resource.validatePostUploadRequestSignature(EXPECTED_SIGNATURE, HOSTNAME, UUID, METADATA, TIMEOUT); } @@ -229,7 +248,7 @@ public void validatePostUploadRequestSignatureTestSuccessWhenDataIsTheSame() { @Test public void getUploadProtocolTestReturnHttpsWhenUseHttpsToUploadIsTrue() { - Mockito.doReturn(true).when(resource).useHttpsToUpload(); + doReturn(true).when(resource).useHttpsToUpload(); String result = resource.getUploadProtocol(); @@ -238,7 +257,7 @@ public void getUploadProtocolTestReturnHttpsWhenUseHttpsToUploadIsTrue() { @Test public void getUploadProtocolTestReturnHttpWhenUseHttpsToUploadIsFalse() { - Mockito.doReturn(false).when(resource).useHttpsToUpload(); + doReturn(false).when(resource).useHttpsToUpload(); String result = resource.getUploadProtocol(); @@ -281,8 +300,111 @@ public void configureStorageNetworkDoesNotSetStorageNetworkWhenNotInSystemVMAndS Map params = new HashMap<>(); resource._inSystemVM = false; resource.configureStorageNetwork(params); - Assert.assertNull(ReflectionTestUtils.getField(resource, "_storageIp")); - Assert.assertNull(ReflectionTestUtils.getField(resource, "_storageNetmask")); - Assert.assertNull(ReflectionTestUtils.getField(resource, "_storageGateway")); + assertNull(ReflectionTestUtils.getField(resource, "_storageIp")); + assertNull(ReflectionTestUtils.getField(resource, "_storageNetmask")); + assertNull(ReflectionTestUtils.getField(resource, "_storageGateway")); + } + + + @Test + public void deleteBackupTestSuccess() { + doReturn(backupDeltaTOMock).when(deleteCommandMock).getData(); + doReturn(nfsMock).when(backupDeltaTOMock).getDataStore(); + doReturn("asd").when(nfsMock).getUrl(); + + doReturn("fds").when(resource).getRootDir(any(), any()); + doReturn("path/to/delta").when(backupDeltaTOMock).getPath(); + doReturn(null).when(resource).deleteLocalFile(any()); + doReturn(null).when(resource).deleteScreenshot(any(), any(), any()); + + Answer answer = resource.deleteBackup(deleteCommandMock); + + assertTrue(answer.getResult()); + } + + @Test + public void deleteBackupTestDeleteDeltaFails() { + doReturn(backupDeltaTOMock).when(deleteCommandMock).getData(); + doReturn(nfsMock).when(backupDeltaTOMock).getDataStore(); + doReturn("asd").when(nfsMock).getUrl(); + + doReturn("fds").when(resource).getRootDir(any(), any()); + doReturn("path/to/delta").when(backupDeltaTOMock).getPath(); + doReturn("error").when(resource).deleteLocalFile(any()); + + Answer answer = resource.deleteBackup(deleteCommandMock); + + assertFalse(answer.getResult()); + } + + @Test + public void deleteBackupTestScreenshotFailure() { + doReturn(backupDeltaTOMock).when(deleteCommandMock).getData(); + doReturn(nfsMock).when(backupDeltaTOMock).getDataStore(); + doReturn("asd").when(nfsMock).getUrl(); + + doReturn("fds").when(resource).getRootDir(any(), any()); + doReturn("path/to/delta").when(backupDeltaTOMock).getPath(); + doReturn(null).when(resource).deleteLocalFile(any()); + + BackupDeleteAnswer failureAnswer = new BackupDeleteAnswer(deleteCommandMock, false, "fail"); + doReturn(failureAnswer).when(resource).deleteScreenshot(any(), any(), any()); + + Answer answer = resource.deleteBackup(deleteCommandMock); + + assertFalse(answer.getResult()); + } + + @Test + public void deleteBackupTestDirectoryDeletionFails() { + doReturn(backupDeltaTOMock).when(deleteCommandMock).getData(); + doReturn(nfsMock).when(backupDeltaTOMock).getDataStore(); + doReturn("asd").when(nfsMock).getUrl(); + + doReturn("fds").when(resource).getRootDir(any(), any()); + doReturn("path/to/delta").when(backupDeltaTOMock).getPath(); + doReturn(null).when(resource).deleteLocalFile(any()); + doReturn(null).when(resource).deleteScreenshot(any(), any(), any()); + doReturn(false).when(resource).deleteEmptyDirectory(any()); + + Answer answer = resource.deleteBackup(deleteCommandMock); + + assertFalse(answer.getResult()); + } + + @Test + public void deleteScreenshotTestNullPath() { + BackupDeleteAnswer answer = resource.deleteScreenshot(deleteCommandMock, null, "/root/"); + + assertNull(answer); + } + + @Test + public void deleteScreenshotTestSuccess() { + doReturn(null).when(resource).deleteLocalFile(any()); + + BackupDeleteAnswer answer = resource.deleteScreenshot(deleteCommandMock, "path/to/file", "/root/"); + + assertNull(answer); + } + + @Test + public void deleteScreenshotTestFailure() { + doReturn("error").when(resource).deleteLocalFile(any()); + doReturn(backupDeltaTOMock).when(deleteCommandMock).getData(); + + BackupDeleteAnswer answer = resource.deleteScreenshot(deleteCommandMock, "path/to/file", "/root/"); + + assertNotNull(answer); + assertFalse(answer.getResult()); + } + + @Test + public void deleteScreenshotTestPathStartsWithSeparator() { + doReturn(null).when(resource).deleteLocalFile(any()); + + resource.deleteScreenshot(deleteCommandMock, File.separator + "path/to/file", "/root/"); + + verify(resource).deleteLocalFile("/root/" + "path/to/file"); } } diff --git a/test/integration/smoke/test_backup_recovery_kboss.py b/test/integration/smoke/test_backup_recovery_kboss.py new file mode 100644 index 000000000000..10d823da195c --- /dev/null +++ b/test/integration/smoke/test_backup_recovery_kboss.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python +# 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. + +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib import utils +from marvin.lib.base import (Account, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering, + Backup, Configurations, Volume, StoragePool) +from marvin.lib.common import (get_domain, get_zone, get_template) +from nose.plugins.attrib import attr +from marvin.codes import FAILED +import time +import tempfile +import urllib.parse +import urllib.request +import os + +class TestKBOSSBackupAndRecovery(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + # Setup + + cls.testClient = super(TestKBOSSBackupAndRecovery, cls).getClsTestClient() + cls.api_client = cls.testClient.getApiClient() + print(cls.api_client) + cls.services = cls.testClient.getParsedTestDataConfig() + cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) + cls.services["mode"] = cls.zone.networktype + cls.hypervisor = cls.testClient.getHypervisorInfo() + cls.domain = get_domain(cls.api_client) + cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + if cls.template == FAILED: + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["small"]["template"] = cls.template.id + cls._cleanup = [] + + if cls.hypervisor.lower() != 'kvm': + cls.skipTest(cls, reason="Test can be run only on KVM hypervisor") + + cls.storage_pool = StoragePool.list(cls.api_client)[0] + if cls.storage_pool.type.lower() != 'networkfilesystem': + cls.skipTest(cls, reason="Test can be run only if the primary storage is of type NFS") + + # Check backup configuration values, set them to enable the kboss provider + backup_enabled_cfg = Configurations.list(cls.api_client, name='backup.framework.enabled') + backup_provider_cfg = Configurations.list(cls.api_client, name='backup.framework.provider.plugin') + cls.backup_enabled = backup_enabled_cfg[0].value + cls.backup_provider = backup_provider_cfg[0].value + + if cls.backup_enabled == "false": + cls.skipTest(cls, reason="Test can be run only if the config backup.framework.enabled is true") + if cls.backup_provider != "kboss": + Configurations.update(cls.api_client, 'backup.framework.provider.plugin', value='kboss') + + cls.account = Account.create(cls.api_client, cls.services["account"], domainid=cls.domain.id) + + cls._cleanup = [cls.account] + + cls.basic_backup_offering = BackupOffering.createOffering(cls.api_client, utils.random_gen(), utils.random_gen(), cls.zone.id) + cls._cleanup.append(cls.basic_backup_offering) + cls.compress_backup_offering = BackupOffering.createOffering(cls.api_client, utils.random_gen(), utils.random_gen(), cls.zone.id, compress=True) + cls._cleanup.append(cls.compress_backup_offering) + cls.validate_backup_offering = BackupOffering.createOffering(cls.api_client, utils.random_gen(), utils.random_gen(), cls.zone.id, validate=True, + validationsteps="screenshot") + cls._cleanup.append(cls.validate_backup_offering) + + cls.offering = ServiceOffering.create(cls.api_client,cls.services["service_offerings"]["small"]) + cls.diskoffering = DiskOffering.create(cls.api_client, cls.services["disk_offering"]) + cls._cleanup.extend([cls.offering, cls.diskoffering]) + cls.vm = VirtualMachine.create(cls.api_client, cls.services["small"], accountid=cls.account.name, + domainid=cls.account.domainid, serviceofferingid=cls.offering.id, + diskofferingid=cls.diskoffering.id, mode=cls.services["mode"]) + + + @classmethod + def tearDownClass(cls): + try: + # Cleanup resources used + utils.cleanup_resources(cls.api_client, cls._cleanup) + + if cls.backup_provider != "kboss": + Configurations.update(cls.api_client, 'backup.framework.provider.plugin', value=cls.backup_provider) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + def setUp(self): + if self.hypervisor.lower() != 'kvm': + raise self.skipTest("Skipping test cases which must only run for Simulator") + self.cleanup = [] + + def tearDown(self): + try: + utils.cleanup_resources(self.api_client, self.cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + def waitForCompression(self, vm): + def checkBackupCompression(): + backups = Backup.list(self.api_client, vm.id) + if isinstance(backups, list) and len(backups) != 0 and backups[0].compressionstatus == "Compressed": + return True, None + return False, None + + res, _ = utils.wait_until(10, 60, checkBackupCompression) + if not res: + self.fail("Failed to wait for backup compression of VM %s" % vm.id) + + def waitForValidation(self, vm): + def checkBackupValidation(): + backups = Backup.list(self.api_client, vm.id) + if isinstance(backups, list) and len(backups) != 0 and backups[0].validationstatus == "Valid": + return True, None + return False, None + + res, _ = utils.wait_until(10, 60, checkBackupValidation) + if not res: + self.fail("Failed to wait for backup compression of VM %s" % vm.id) + + def download_screenshot(self, backupid): + extract_ss = Backup.downloadValidationScreenshot(self.api_client, backupid) + + try: + formatted_url = urllib.parse.unquote_plus(extract_ss.url) + self.debug("Attempting to download screenshot at url %s" % formatted_url) + response = urllib.request.urlopen(formatted_url) + self.debug("response from screenshot url %s" % response.getcode()) + fd, path = tempfile.mkstemp() + self.debug("Saving screenshot %s to path %s" % (backupid, path)) + os.close(fd) + with open(path, 'wb') as fd: + fd.write(response.read()) + self.debug("Saved screenshot successfully") + except Exception: + self.fail( + "Extract screenshot of backup Failed with invalid URL %s (backup id: %s)" \ + % (extract_ss.url, backupid) + ) + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_vm_backup_lifecycle(self): + """ + Test VM backup lifecycle + """ + + # Verify there are no backups for the VM + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Assign VM to offering and create ad-hoc backup + self.basic_backup_offering.assignOffering(self.api_client, self.vm.id) + Backup.create(self.api_client, self.vm.id) + + # Verify backup is created for the VM + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(len(backups), 1, "There should exist only one backup for the VM") + backup = backups[0] + + # Delete backup + Backup.delete(self.api_client, backup.id) + + # Verify backup is deleted + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Remove VM from offering + self.basic_backup_offering.removeOffering(self.api_client, self.vm.id) + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_vm_backup_lifecycle_with_compression(self): + """ + Test VM backup lifecycle with compression + """ + + compression_enabled = Configurations.list(self.api_client, name='backup.compression.task.enabled') + if compression_enabled[0].value == "false": + self.skipTest("Skipping test due to backup compression task is disabled.") + + # Verify there are no backups for the VM + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Assign VM to offering and create ad-hoc backup + self.compress_backup_offering.assignOffering(self.api_client, self.vm.id) + Backup.create(self.api_client, self.vm.id) + + # Verify backup is created for the VM + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(len(backups), 1, "There should exist only one backup for the VM") + backup = backups[0] + + self.waitForCompression(self.vm) + + # Delete backup + Backup.delete(self.api_client, backup.id) + + # Verify backup is deleted + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Remove VM from offering + self.compress_backup_offering.removeOffering(self.api_client, self.vm.id) + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_vm_backup_lifecycle_with_validation(self): + """ + Test VM backup lifecycle with validation + """ + + validation_enabled = Configurations.list(self.api_client, name='backup.validation.task.enabled') + if validation_enabled[0].value == "false": + self.skipTest("Skipping test due to backup compression task is disabled.") + + # Verify there are no backups for the VM + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Assign VM to offering and create ad-hoc backup + self.validate_backup_offering.assignOffering(self.api_client, self.vm.id) + Backup.create(self.api_client, self.vm.id) + + # Verify backup is created for the VM + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(len(backups), 1, "There should exist only one backup for the VM") + backup = backups[0] + + # Verify validation is performed + self.waitForValidation(self.vm) + self.download_screenshot(backup.id) + + # Delete backup + Backup.delete(self.api_client, backup.id) + + # Verify backup is deleted + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Remove VM from offering + self.validate_backup_offering.removeOffering(self.api_client, self.vm.id) + + @attr(tags=["advanced", "backup"], required_hardware="true") + def test_vm_backup_create_vm_from_backup(self): + """ + Test creating a new VM from a backup + """ + self.basic_backup_offering.assignOffering(self.api_client, self.vm.id) + + # Create a file and take backup + try: + ssh_client_vm = self.vm.get_ssh_client(reconnect=True) + ssh_client_vm.execute("touch test_backup_and_recovery.txt") + except Exception as err: + self.fail("SSH failed for Virtual machine: %s due to %s" % (self.vm.ipaddress, err)) + + time.sleep(5) + + Backup.create(self.api_client, self.vm.id, "backup1") + Backup.create(self.api_client, self.vm.id, "backup2") + + # Verify backup is created for the VM + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(len(backups), 2, "There should exist two backups for the VM") + + # Remove VM from offering + self.basic_backup_offering.removeOffering(self.api_client, self.vm.id) + + # Verify no. of backups after removing the backup offering + backups = Backup.list(self.api_client, self.vm.id) + self.assertEqual(len(backups), 2, "There should exist two backups for the VM") + + # Create a new VM from first backup + new_vm_name = "vm-from-backup1-" + str(int(time.time())) + new_vm = Backup.createVMFromBackup( + self.api_client, + self.services["small"], + mode=self.services["mode"], + backupid=backups[0].id, + vmname=new_vm_name, + accountname=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id, + networkids=None, + templateid=None + ) + self.cleanup.append(new_vm) + + # Verify the new VM was created successfully + self.assertIsNotNone(new_vm, "Failed to create VM from backup") + self.assertEqual(new_vm.name, new_vm_name, "VM name does not match the requested name") + + # Verify the new VM is running + self.assertEqual(new_vm.state, "Running", "New VM should be in Running state") + + # Verify the new VM has the correct service offering + self.assertEqual(new_vm.serviceofferingid, self.offering.id, + "New VM should have the correct service offering") + + # Verify the new VM has the correct zone + self.assertEqual(new_vm.zoneid, self.zone.id, "New VM should be in the correct zone") + + # Verify the new VM has the correct number of volumes (ROOT + DATADISK) + volumes = Volume.list( + self.api_client, + virtualmachineid=new_vm.id, + listall=True + ) + self.assertTrue(isinstance(volumes, list), "List volumes should return a valid list") + self.assertEqual(2, len(volumes), "The new VM should have 2 volumes (ROOT + DATADISK)") + + # Verify that the file is present in the Instance created from backup + try: + ssh_client_new_vm = new_vm.get_ssh_client(reconnect=True) + result = ssh_client_new_vm.execute("ls test_backup_and_recovery.txt") + self.assertEqual(result[0], "test_backup_and_recovery.txt", + "Instance created from Backup should have the same file as the backup.") + except Exception as err: + self.fail("SSH failed for Virtual machine: %s due to %s" % (self.vm.ipaddress, err)) + + # Delete backups + Backup.delete(self.api_client, backups[0].id) + Backup.delete(self.api_client, backups[1].id) diff --git a/test/integration/smoke/test_public_ip_range.py b/test/integration/smoke/test_public_ip_range.py index 997716caaaf4..fb9fe2494d57 100644 --- a/test/integration/smoke/test_public_ip_range.py +++ b/test/integration/smoke/test_public_ip_range.py @@ -145,10 +145,10 @@ def test_dedicate_public_ip_range_for_system_vms(self): # 7. Delete the Public IP range services = { - "gateway":"192.168.99.1", + "gateway":"10.1.99.1", "netmask":"255.255.255.0", - "startip":"192.168.99.2", - "endip":"192.168.99.200", + "startip":"10.1.99.2", + "endip":"10.1.99.200", "forvirtualnetwork":self.services["forvirtualnetwork"], "zoneid":self.services["zoneid"], "vlan":self.services["vlan"] @@ -344,10 +344,10 @@ def test_dedicate_public_ip_range_for_system_vms_01_ssvm(self): self.skipTest("An existing IP range defined for system vms, aborting test") services = { - "gateway":"192.168.100.1", + "gateway":"10.1.100.1", "netmask":"255.255.255.0", - "startip":"192.168.100.2", - "endip":"192.168.100.200", + "startip":"10.1.100.2", + "endip":"10.1.100.200", "forvirtualnetwork":self.services["forvirtualnetwork"], "zoneid":self.services["zoneid"], "vlan":self.services["vlan"] @@ -372,10 +372,10 @@ def test_dedicate_public_ip_range_for_system_vms_02_cpvm(self): self.skipTest("An existing IP range defined for system vms, aborting test") services = { - "gateway":"192.168.200.1", + "gateway":"10.1.200.1", "netmask":"255.255.255.0", - "startip":"192.168.200.2", - "endip":"192.168.200.200", + "startip":"10.1.200.2", + "endip":"10.1.200.200", "forvirtualnetwork":self.services["forvirtualnetwork"], "zoneid":self.services["zoneid"], "vlan":self.services["vlan"] diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index d703769eb1c1..c99328fff9ff 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -282,7 +282,8 @@ 'CustomAction' : 'Extension', 'CustomActions' : 'Extension', 'ImportVmTask': 'Import VM Task', - 'Dns': 'DNS' + 'Dns': 'DNS', + 'downloadValidationScreenshot': 'Backup and Recovery' } diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 82155fe27e76..e7fa2f763db5 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -6219,6 +6219,29 @@ def removeOffering(self, apiclient, vmid, forced=True): cmd.forced = forced return (apiclient.removeVirtualMachineFromBackupOffering(cmd)) + @classmethod + def createOffering(cls, api_client, description, name, zoneid, allowquickrestore=False, compress=False, validate=False, validationsteps=None, allowuserdrivenbackups=True, + compressionlibrary='zlib'): + """Create a backup offering""" + + cmd = createBackupOffering.createBackupOfferingCmd() + cmd.description = description + cmd.name = name + cmd.zoneid = zoneid + cmd.allowuserdrivenbackups = allowuserdrivenbackups + + if allowquickrestore: + cmd.allowquickrestore = allowquickrestore + if compress: + cmd.compress = compress + cmd.compressionlibrary = compressionlibrary + if validate: + cmd.validate = validate + if validationsteps: + cmd.validationsteps = validationsteps + + return BackupOffering(api_client.createBackupOffering(cmd).__dict__) + class Backup: @@ -6293,6 +6316,14 @@ def createVMFromBackup(cls, apiclient, services, mode, backupid, accountname, do VirtualMachine.program_ssh_access(apiclient, services, mode, cmd.networkids, virtual_machine) return virtual_machine + @classmethod + def downloadValidationScreenshot(self, apiclient, backupid): + """Download Validation Screenshot""" + + cmd = downloadValidationScreenshot.downloadValidationScreenshotCmd() + cmd.backupid = backupid + return (apiclient.downloadValidationScreenshot(cmd)) + class BackupSchedule: def __init__(self, items): diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 007ef04a6cac..775de26103a0 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -401,6 +401,8 @@ "label.allocatedonly": "Allocated", "label.allocationstate": "Allocation state", "label.allow": "Allow", +"label.allowextractfile": "Allow extract file", +"label.allowquickrestore": "Allow quick restore", "label.allowedroletypes": "Allowed Role Types", "label.allow.duplicate.macaddresses": "Allow duplicate MAC addresses", "label.allowuserdrivenbackups": "Allow User driven backups", @@ -474,8 +476,11 @@ "label.backups": "Backups", "label.backedup": "BackedUp", "label.backingup": "BackingUp", +"label.backup.isolated": "Isolated", "label.backup.attach.restore": "Restore and attach backup volume", "label.backup.configure.schedule": "Configure Backup Schedule", +"label.backup.chain.finish": "Finish backup chain", +"label.backupchainsize": "Backup chain size", "label.backup.offering.assign": "Assign Instance to backup offering", "label.backup.offering.remove": "Remove Instance from backup offering", "label.backup.offerings": "Backup Offerings", @@ -623,6 +628,9 @@ "label.community": "Community", "label.complete": "Complete", "label.completed": "Completed", +"label.compress": "Compress", +"label.compressionstatus": "Compression status", +"label.compressionlibrary": "Compression library", "label.compute": "Compute", "label.compute.offerings": "Compute Offerings", "label.compute.offering.for.sharedfs.instance": "Compute Offering for Instance", @@ -712,6 +720,7 @@ "label.create.account": "Create Account", "label.create.asnrange": "Create AS Range", "label.create.backup": "Start Backup", +"label.create.backup.offering": "Create Backup Offering", "label.create.extension": "Create Extension", "label.create.sharedfs": "Create Shared FileSystem", "label.create.network": "Create new Network", @@ -2102,6 +2111,7 @@ "label.purpose": "Purpose", "label.qostype": "QoS type", "label.queued": "Queued", +"label.quickrestore": "Quick Restore", "label.quickview": "Quick view", "label.quiescevm": "Quiesce Instance", "label.quiettime": "Quiet time (in sec)", @@ -2839,7 +2849,10 @@ "label.utilization": "Utilization", "label.uuid": "ID", "label.value": "Value", +"label.validate": "Validate", "label.validationformat": "Validation Format", +"label.validationstatus": "Validation status", +"label.validationsteps": "Validation steps", "label.valueoptions": "Values Options", "label.vcenter": "VMware datacenter vCenter", "label.vcenter.datacenter": "vCenter datacenter", diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index fbcf61489b90..b3eae6eb11ce 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -436,6 +436,7 @@ "label.back": "Voltar", "label.backup": "Backup", "label.backups": "Backups", +"label.backup.isolated": "Isolado", "label.back.login": "Voltar à página de login", "label.backedup": "Salvo", "label.backingup": "Salvando", @@ -444,6 +445,8 @@ "label.backup.storage": "Armazenamento de backup", "label.backupstoragelimit": "Limite de armazenamento de backup (GiB)", "label.backup.configure.schedule": "Configurar Agendamento de Backup", +"label.backup.chain.finish": "Finalizar cadeia de backup", +"label.backupchainsize": "Tamanho da cadeia de backup", "label.backup.offering.assign": "Atribuir VM a oferta de backup", "label.backup.offering.remove": "Remover VM de oferta de backup", "label.backup.offerings": "Ofertas de backup", @@ -559,7 +562,9 @@ "label.communities": "Comunidades", "label.community": "Comunidade", "label.complete": "Complete", +"label.compress": "Comprimir", "label.compressionstatus": "Estado de compress\u00e3o", +"label.compressionlibrary": "Biblioteca de compress\u00e3o", "label.compute": "Computa\u00e7\u00e3o", "label.compute.offerings": "Oferta de computa\u00e7\u00e3o", "label.compute.offering.for.sharedfs.instance": "Oferta de computa\u00e7\u00e3o para Inst\u00e2ncia", @@ -639,6 +644,7 @@ "label.create.account": "Criar conta", "label.create.asnrange": "Criar faixa de número AS", "label.create.backup": "Iniciar backup", +"label.create.backup.offering": "Criar oferta de backup", "label.create.bucket": "Criar Bucket", "label.create.instance": "Criar inst\u00e2ncia na nuvem", "label.create.instance.from.backup": "Criar nova inst\u00e2ncia a partir de backup", @@ -1835,6 +1841,7 @@ "label.purpose": "Prop\u00f3sito", "label.qostype": "Tipo de QoS", "label.queued": "Enfileirado", +"label.quickrestore": "Restaura\u00E7\u00e3o R\u00e1pida", "label.quickview": "Visualiza\u00e7\u00e3o r\u00e1pida", "label.quiescevm": "Quiesce VM", "label.quiettime": "Tempo em espera (em seg)", @@ -2511,6 +2518,9 @@ "label.utilization": "Utiliza\u00e7\u00e3o", "label.uuid": "ID", "label.value": "Valor", +"label.validate": "Validar", +"label.validationstatus": "Estado de valida\u00E7\u00E3o", +"label.validationsteps": "Passos de valida\u00E7\u00E3o", "label.vcenter": "vcenter", "label.vcenter.datacenter": "Datacenter vCenter", "label.vcenter.datastore": "Datastore vCenter", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index ff3810c13a86..3119e4d57545 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -232,6 +232,19 @@
{{ dataResource[item] }}
+
+ +
+ {{ $t('label.' + String(key).toLowerCase()) }} +
+
+ {{ value }} +
+
+
+
@@ -300,7 +313,7 @@ export default { }, computed: { customDisplayItems () { - var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider', 'details', 'parameters', 'secretkey'] + var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider', 'details', 'parameters', 'secretkey', 'backupofferingdetails'] if (this.$route.meta.name === 'webhookdeliveries') { items.push('startdate') items.push('enddate') diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index d57100429b86..c5346390f178 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -483,6 +483,12 @@ displayText /> + + + + + +