From 9349d20dd36c490eeb6e140b9215ca68653a85d1 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Fri, 30 Apr 2021 01:17:50 -0300 Subject: [PATCH 01/13] vmware: Make deploy-as-is optional (#4901) * [Vmware] Make deploy-as-is optional * Do not use deployasis on create volume test * Update api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java Co-authored-by: Abhishek Kumar * Update api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java Co-authored-by: Abhishek Kumar * Review comments * Refactor condition to select suitable template Co-authored-by: Abhishek Kumar --- .../user/template/RegisterTemplateCmd.java | 14 +++++--- .../template/RegisterTemplateCmdTest.java | 33 ++++++++++++++++++- .../cloud/template/TemplateAdapterBase.java | 2 +- .../smoke/test_kubernetes_clusters.py | 4 +-- test/integration/smoke/test_storage_policy.py | 1 + test/integration/smoke/test_templates.py | 1 + test/integration/smoke/test_volumes.py | 3 +- tools/marvin/marvin/lib/base.py | 31 +++++++++-------- tools/marvin/marvin/lib/common.py | 11 ++++--- ui/public/locales/en.json | 2 +- .../views/image/RegisterOrUploadTemplate.vue | 22 +++++++++---- 11 files changed, 87 insertions(+), 37 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java index 74053ee809d..bb7f7a441b6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java @@ -72,7 +72,7 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { private String format; @Parameter(name = ApiConstants.HYPERVISOR, type = CommandType.STRING, required = true, description = "the target hypervisor for the template") - private String hypervisor; + protected String hypervisor; @Parameter(name = ApiConstants.IS_FEATURED, type = CommandType.BOOLEAN, description = "true if this template is a featured template, false otherwise") private Boolean featured; @@ -162,6 +162,11 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { description = "true if template should bypass Secondary Storage and be downloaded to Primary Storage on deployment") private Boolean directDownload; + @Parameter(name=ApiConstants.DEPLOY_AS_IS, + type = CommandType.BOOLEAN, + description = "(VMware only) true if VM deployments should preserve all the configurations defined for this template", since = "4.15.1") + protected Boolean deployAsIs; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -274,8 +279,9 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { return directDownload == null ? false : directDownload; } - public Boolean isDeployAsIs() { - return hypervisor != null && hypervisor.equalsIgnoreCase(Hypervisor.HypervisorType.VMware.toString()); + public boolean isDeployAsIs() { + return Hypervisor.HypervisorType.VMware.toString().equalsIgnoreCase(hypervisor) && + Boolean.TRUE.equals(deployAsIs); } ///////////////////////////////////////////////////// @@ -341,7 +347,7 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { "Parameter directdownload is only allowed for KVM templates"); } - if (!getHypervisor().equalsIgnoreCase(Hypervisor.HypervisorType.VMware.toString()) && osTypeId == null) { + if (!isDeployAsIs() && osTypeId == null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please provide a guest OS type"); } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdTest.java index f0cf6a91af2..dfd3b6e2e25 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdTest.java @@ -20,6 +20,7 @@ package org.apache.cloudstack.api.command.user.template; import com.cloud.exception.ResourceAllocationException; +import com.cloud.hypervisor.Hypervisor; import com.cloud.template.TemplateApiService; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ServerApiException; @@ -32,7 +33,7 @@ import org.mockito.runners.MockitoJUnitRunner; import java.util.ArrayList; @RunWith(MockitoJUnitRunner.class) -public class RegisterTemplateCmdTest{ +public class RegisterTemplateCmdTest { @InjectMocks private RegisterTemplateCmd registerTemplateCmd; @@ -108,4 +109,34 @@ public class RegisterTemplateCmdTest{ registerTemplateCmd.zoneId = 1L; Assert.assertEquals((Long)1L,registerTemplateCmd.getZoneIds().get(0)); } + + private void testIsDeployAsIsBase(Hypervisor.HypervisorType hypervisorType, Boolean deployAsIsParameter, boolean expectedResult) { + registerTemplateCmd = new RegisterTemplateCmd(); + registerTemplateCmd.hypervisor = hypervisorType.name(); + registerTemplateCmd.deployAsIs = deployAsIsParameter; + boolean isDeployAsIs = registerTemplateCmd.isDeployAsIs(); + Assert.assertEquals(expectedResult, isDeployAsIs); + } + + @Test + public void testIsDeployAsIsVmwareNullAsIs() { + testIsDeployAsIsBase(Hypervisor.HypervisorType.VMware, null, false); + } + + @Test + public void testIsDeployAsIsVmwareNotAsIs() { + testIsDeployAsIsBase(Hypervisor.HypervisorType.VMware, false, false); + } + + @Test + public void testIsDeployAsIsVmwareAsIs() { + testIsDeployAsIsBase(Hypervisor.HypervisorType.VMware, true, true); + } + + @Test + public void testIsDeployAsIsNonVmware() { + testIsDeployAsIsBase(Hypervisor.HypervisorType.KVM, true, false); + testIsDeployAsIsBase(Hypervisor.HypervisorType.XenServer, true, false); + testIsDeployAsIsBase(Hypervisor.HypervisorType.Any, true, false); + } } diff --git a/server/src/main/java/com/cloud/template/TemplateAdapterBase.java b/server/src/main/java/com/cloud/template/TemplateAdapterBase.java index 2c330f62c69..52628ee2e23 100644 --- a/server/src/main/java/com/cloud/template/TemplateAdapterBase.java +++ b/server/src/main/java/com/cloud/template/TemplateAdapterBase.java @@ -291,7 +291,7 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat } Map details = cmd.getDetails(); - if (hypervisorType == HypervisorType.VMware) { + if (cmd.isDeployAsIs()) { if (MapUtils.isNotEmpty(details)) { if (details.containsKey(VmDetailConstants.ROOT_DISK_CONTROLLER)) { s_logger.info("Ignoring the rootDiskController detail provided, as we honour what is defined in the template"); diff --git a/test/integration/smoke/test_kubernetes_clusters.py b/test/integration/smoke/test_kubernetes_clusters.py index 5ea5d772de0..55394c6f323 100644 --- a/test/integration/smoke/test_kubernetes_clusters.py +++ b/test/integration/smoke/test_kubernetes_clusters.py @@ -260,8 +260,8 @@ class TestKubernetesCluster(cloudstackTestCase): if validateList(templates)[0] != PASS: details = None - if hypervisor in ["vmware"]: - details = [{"keyboard": "us"}] + if hypervisor in ["vmware"] and "details" in cks_template: + details = cks_template["details"] template = Template.register(cls.apiclient, cks_template, zoneid=cls.zone.id, hypervisor=hypervisor.lower(), randomize_name=False, details=details) template.download(cls.apiclient) return template, False diff --git a/test/integration/smoke/test_storage_policy.py b/test/integration/smoke/test_storage_policy.py index a7a5f8d0cba..ea35b4db69b 100644 --- a/test/integration/smoke/test_storage_policy.py +++ b/test/integration/smoke/test_storage_policy.py @@ -59,6 +59,7 @@ class TestVMWareStoragePolicies(cloudstackTestCase): cls.apiclient, cls.zone.id, cls.hypervisor, + deploy_as_is=cls.hypervisor.lower() == "vmware" ) cls._cleanup.append(cls.network_offering) return diff --git a/test/integration/smoke/test_templates.py b/test/integration/smoke/test_templates.py index 0947b4b62eb..2ea8ed24693 100644 --- a/test/integration/smoke/test_templates.py +++ b/test/integration/smoke/test_templates.py @@ -84,6 +84,7 @@ class TestCreateTemplateWithChecksum(cloudstackTestCase): self.test_template.displaytext = 'test sha-1' self.test_template.url = "http://dl.openvm.eu/cloudstack/macchinina/x86_64/macchinina-vmware.ova" self.test_template.format = "OVA" + self.test_template.ostypeid = self.getOsType("Other Linux (64-bit)") self.md5 = "27f3c56a8c7ec7b2f3ff2199f7078006" self.sha256 = "a7b04c1eb507f3f5de844bda352df1ea5e20335b465409493ca6ae07dfd0a158" diff --git a/test/integration/smoke/test_volumes.py b/test/integration/smoke/test_volumes.py index aa4eaf13fa4..9cb324bc7ba 100644 --- a/test/integration/smoke/test_volumes.py +++ b/test/integration/smoke/test_volumes.py @@ -308,7 +308,8 @@ class TestVolumes(cloudstackTestCase): cls.apiclient, cls.zone.id, cls.services["ostype"], - cls.hypervisor + cls.hypervisor, + deploy_as_is=cls.hypervisor.lower() == "vmware" ) if cls.template == FAILED: assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 3de64ef318e..efe7331d0dc 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -1403,24 +1403,22 @@ class Template: elif "hypervisor" in services: cmd.hypervisor = services["hypervisor"] - if cmd.hypervisor.lower() not in ["vmware"]: - # Since version 4.15 VMware templates honour the guest OS defined in the template - if "ostypeid" in services: - cmd.ostypeid = services["ostypeid"] - elif "ostype" in services: - # Find OSTypeId from Os type - sub_cmd = listOsTypes.listOsTypesCmd() - sub_cmd.description = services["ostype"] - ostypes = apiclient.listOsTypes(sub_cmd) + if "ostypeid" in services: + cmd.ostypeid = services["ostypeid"] + elif "ostype" in services: + # Find OSTypeId from Os type + sub_cmd = listOsTypes.listOsTypesCmd() + sub_cmd.description = services["ostype"] + ostypes = apiclient.listOsTypes(sub_cmd) - if not isinstance(ostypes, list): - raise Exception( - "Unable to find Ostype id with desc: %s" % - services["ostype"]) - cmd.ostypeid = ostypes[0].id - else: + if not isinstance(ostypes, list): raise Exception( - "Unable to find Ostype is required for registering template") + "Unable to find Ostype id with desc: %s" % + services["ostype"]) + cmd.ostypeid = ostypes[0].id + else: + raise Exception( + "Unable to find Ostype is required for registering template") cmd.url = services["url"] @@ -1438,6 +1436,7 @@ class Template: cmd.isdynamicallyscalable = services["isdynamicallyscalable"] if "isdynamicallyscalable" in services else False cmd.passwordenabled = services[ "passwordenabled"] if "passwordenabled" in services else False + cmd.deployasis = services["deployasis"] if "deployasis" in services else False if account: cmd.account = account diff --git a/tools/marvin/marvin/lib/common.py b/tools/marvin/marvin/lib/common.py index 18a8a0a1a5e..cd896c909eb 100644 --- a/tools/marvin/marvin/lib/common.py +++ b/tools/marvin/marvin/lib/common.py @@ -348,7 +348,7 @@ def get_template( return list_templatesout[0] -def get_test_template(apiclient, zone_id=None, hypervisor=None, test_templates=None): +def get_test_template(apiclient, zone_id=None, hypervisor=None, test_templates=None, deploy_as_is=False): """ @Name : get_test_template @Desc : Retrieves the test template used to running tests. When the template @@ -373,6 +373,8 @@ def get_test_template(apiclient, zone_id=None, hypervisor=None, test_templates=N return FAILED test_template = test_templates[hypervisor] + if deploy_as_is: + test_template['deployasis'] = True cmd = listTemplates.listTemplatesCmd() cmd.name = test_template['name'] @@ -513,7 +515,7 @@ def get_windows_template( return FAILED -def get_suitable_test_template(apiclient, zoneid, ostypeid, hypervisor): +def get_suitable_test_template(apiclient, zoneid, ostypeid, hypervisor, deploy_as_is=False): ''' @Name : get_suitable_test_template @Desc : Retrieves the test template information based upon inputs provided @@ -525,11 +527,12 @@ def get_suitable_test_template(apiclient, zoneid, ostypeid, hypervisor): template Information matching the inputs ''' template = FAILED - if hypervisor.lower() in ["xenserver"]: + if hypervisor.lower() in ["xenserver"] or (hypervisor.lower() in ["vmware"] and deploy_as_is): template = get_test_template( apiclient, zoneid, - hypervisor) + hypervisor, + deploy_as_is=deploy_as_is) if template == FAILED: template = get_template( apiclient, diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index ba0d2b13d22..cbda4d5094f 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -715,7 +715,7 @@ "label.demote.project.owner": "Demote account to Regular role", "label.demote.project.owner.user": "Demote user to Regular role", "label.deny": "Deny", -"label.deployasis":"Deploy As-Is", +"label.deployasis":"Read VM settings from OVA", "label.deploymentplanner": "Deployment planner", "label.description": "Description", "label.destcidr": "Destination CIDR", diff --git a/ui/src/views/image/RegisterOrUploadTemplate.vue b/ui/src/views/image/RegisterOrUploadTemplate.vue index 13001230758..704cda59ef6 100644 --- a/ui/src/views/image/RegisterOrUploadTemplate.vue +++ b/ui/src/views/image/RegisterOrUploadTemplate.vue @@ -208,8 +208,18 @@ :default-checked="xenServerProvider" /> + + + + + - + - + + Date: Fri, 30 Apr 2021 11:51:28 +0530 Subject: [PATCH 02/13] ui: Fix Settings Tab view (#4964) Co-authored-by: Pearl Dsilva --- ui/src/components/view/SettingsTab.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/src/components/view/SettingsTab.vue b/ui/src/components/view/SettingsTab.vue index ec49d88b34a..10766fd05f5 100644 --- a/ui/src/components/view/SettingsTab.vue +++ b/ui/src/components/view/SettingsTab.vue @@ -96,7 +96,7 @@ export default { filter: '' } }, - beforeMount () { + created () { switch (this.$route.meta.name) { case 'account': this.scopeKey = 'accountid' @@ -119,8 +119,6 @@ export default { default: this.scopeKey = '' } - }, - created () { this.fetchData() }, watch: { From 72f6612971a80f1b87d0a4c9d9d93047cb1af7c1 Mon Sep 17 00:00:00 2001 From: Olivier Lemasle Date: Fri, 30 Apr 2021 08:27:56 +0200 Subject: [PATCH 03/13] server: Increase max length for VMInstanceVO.backupVolumes (#4967) The default length is 255, which caused a truncation of data if the JSON object representing the backup volumes is too big. It caused errors when backups were made on VMs with 3 volumes or more. `vm_instance.backup_volumes` has the type TEXT, which has a maximal length of 65535 characters. Fixes #4965 --- engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java b/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java index 5c81e550e48..84f1d850cfd 100644 --- a/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java @@ -200,7 +200,7 @@ public class VMInstanceVO implements VirtualMachine, FiniteStateObject Date: Fri, 30 Apr 2021 03:29:50 -0300 Subject: [PATCH 04/13] server: Allow to upgrade service offerings from local <> shared storage pools (#4915) This PR addresses the issue raised at #4545 (Fail to change Service offering from local <> shared storage). When upgrading a VM service offering it is validated if the new offering has the same storage scope (local or shared) as the current offering. I think that the validation makes sense in a way of preventing running Root disks with an offering that does not match the current storage pool. However, the validation only compares both offerings and does not consider that it is possible to migrate Volumes between local <> shared storage pools. The idea behind this implementation is that CloudStack should check the scope of the current storage pool which the ROOT volume is allocated; this, it is possible to migrate the volume between storage pools and list/upgrade according to the offerings that are supported for such pool. This PR also fixes an issue where the API command that lists offerings for a VM should follow the same idea and list based on the storage pool that the volume is allocated and not the previous offering. Fixes: #4545 --- .../com/cloud/vm/VirtualMachineManager.java | 6 ++ .../cloud/vm/VirtualMachineManagerImpl.java | 50 +++++++++++------ .../vm/VirtualMachineManagerImplTest.java | 56 +++++++++++++++++++ .../com/cloud/api/query/QueryManagerImpl.java | 10 +++- 4 files changed, 104 insertions(+), 18 deletions(-) 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 40777b38b6d..96704641bc4 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -257,5 +257,11 @@ public interface VirtualMachineManager extends Manager { UserVm restoreVirtualMachine(long vmId, Long newTemplateId) throws ResourceUnavailableException, InsufficientCapacityException; + /** + * Returns true if the VM's Root volume is allocated at a local storage pool + */ + boolean isRootVolumeOnLocalStorage(long vmId); + Pair findClusterAndHostIdForVm(long vmId); + } 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 765bb907b27..6967e74f997 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -3632,22 +3632,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac final ServiceOfferingVO currentServiceOffering = _offeringDao.findByIdIncludingRemoved(vmInstance.getId(), vmInstance.getServiceOfferingId()); - // Check that the service offering being upgraded to has the same Guest IP type as the VM's current service offering - // NOTE: With the new network refactoring in 2.2, we shouldn't need the check for same guest IP type anymore. - /* - * if (!currentServiceOffering.getGuestIpType().equals(newServiceOffering.getGuestIpType())) { String errorMsg = - * "The service offering being upgraded to has a guest IP type: " + newServiceOffering.getGuestIpType(); errorMsg += - * ". Please select a service offering with the same guest IP type as the VM's current service offering (" + - * currentServiceOffering.getGuestIpType() + ")."; throw new InvalidParameterValueException(errorMsg); } - */ - - // Check that the service offering being upgraded to has the same storage pool preference as the VM's current service - // offering - if (currentServiceOffering.isUseLocalStorage() != newServiceOffering.isUseLocalStorage()) { - throw new InvalidParameterValueException("Unable to upgrade virtual machine " + vmInstance.toString() + - ", cannot switch between local storage and shared storage service offerings. Current offering " + "useLocalStorage=" + - currentServiceOffering.isUseLocalStorage() + ", target offering useLocalStorage=" + newServiceOffering.isUseLocalStorage()); - } + checkIfNewOfferingStorageScopeMatchesStoragePool(vmInstance, newServiceOffering); // if vm is a system vm, check if it is a system service offering, if yes return with error as it cannot be used for user vms if (currentServiceOffering.isSystemUse() != newServiceOffering.isSystemUse()) { @@ -3669,6 +3654,39 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } } + /** + * Throws an InvalidParameterValueException in case the new service offerings does not match the storage scope (e.g. local or shared). + */ + protected void checkIfNewOfferingStorageScopeMatchesStoragePool(VirtualMachine vmInstance, ServiceOffering newServiceOffering) { + boolean isRootVolumeOnLocalStorage = isRootVolumeOnLocalStorage(vmInstance.getId()); + + if (newServiceOffering.isUseLocalStorage() && !isRootVolumeOnLocalStorage) { + String message = String .format("Unable to upgrade virtual machine %s, target offering use local storage but the storage pool where " + + "the volume is allocated is a shared storage.", vmInstance.toString()); + throw new InvalidParameterValueException(message); + } + + if (!newServiceOffering.isUseLocalStorage() && isRootVolumeOnLocalStorage) { + String message = String.format("Unable to upgrade virtual machine %s, target offering use shared storage but the storage pool where " + + "the volume is allocated is a local storage.", vmInstance.toString()); + throw new InvalidParameterValueException(message); + } + } + + public boolean isRootVolumeOnLocalStorage(long vmId) { + ScopeType poolScope = ScopeType.ZONE; + List volumes = _volsDao.findByInstanceAndType(vmId, Type.ROOT); + if(CollectionUtils.isNotEmpty(volumes)) { + VolumeVO rootDisk = volumes.get(0); + Long poolId = rootDisk.getPoolId(); + if (poolId != null) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(poolId); + poolScope = storagePoolVO.getScope(); + } + } + return ScopeType.HOST == poolScope; + } + @Override public boolean upgradeVmDb(final long vmId, final ServiceOffering newServiceOffering, ServiceOffering currentServiceOffering) { diff --git a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java index 1725a413145..d8df27d0731 100644 --- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -31,6 +31,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import com.cloud.exception.InvalidParameterValueException; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; @@ -623,4 +624,59 @@ public class VirtualMachineManagerImplTest { assertTrue(VirtualMachineManagerImpl.matches(tags,three)); assertTrue(VirtualMachineManagerImpl.matches(others,three)); } + + @Test + public void isRootVolumeOnLocalStorageTestOnLocal() { + prepareAndTestIsRootVolumeOnLocalStorage(ScopeType.HOST, true); + } + + @Test + public void isRootVolumeOnLocalStorageTestCluster() { + prepareAndTestIsRootVolumeOnLocalStorage(ScopeType.CLUSTER, false); + } + + @Test + public void isRootVolumeOnLocalStorageTestZone() { + prepareAndTestIsRootVolumeOnLocalStorage(ScopeType.ZONE, false); + } + + private void prepareAndTestIsRootVolumeOnLocalStorage(ScopeType scope, boolean expected) { + StoragePoolVO storagePoolVoMock = Mockito.mock(StoragePoolVO.class); + Mockito.doReturn(storagePoolVoMock).when(storagePoolDaoMock).findById(Mockito.anyLong()); + Mockito.doReturn(scope).when(storagePoolVoMock).getScope(); + List mockedVolumes = new ArrayList<>(); + mockedVolumes.add(volumeVoMock); + Mockito.doReturn(mockedVolumes).when(volumeDaoMock).findByInstanceAndType(Mockito.anyLong(), Mockito.any()); + + boolean result = virtualMachineManagerImpl.isRootVolumeOnLocalStorage(0l); + + Assert.assertEquals(expected, result); + } + + @Test + public void checkIfNewOfferingStorageScopeMatchesStoragePoolTestLocalLocal() { + prepareAndRunCheckIfNewOfferingStorageScopeMatchesStoragePool(true, true); + } + + @Test + public void checkIfNewOfferingStorageScopeMatchesStoragePoolTestSharedShared() { + prepareAndRunCheckIfNewOfferingStorageScopeMatchesStoragePool(false, false); + } + + @Test (expected = InvalidParameterValueException.class) + public void checkIfNewOfferingStorageScopeMatchesStoragePoolTestLocalShared() { + prepareAndRunCheckIfNewOfferingStorageScopeMatchesStoragePool(true, false); + } + + @Test (expected = InvalidParameterValueException.class) + public void checkIfNewOfferingStorageScopeMatchesStoragePoolTestSharedLocal() { + prepareAndRunCheckIfNewOfferingStorageScopeMatchesStoragePool(false, true); + } + + private void prepareAndRunCheckIfNewOfferingStorageScopeMatchesStoragePool(boolean isRootOnLocal, boolean isOfferingUsingLocal) { + Mockito.doReturn(isRootOnLocal).when(virtualMachineManagerImpl).isRootVolumeOnLocalStorage(Mockito.anyLong()); + Mockito.doReturn("vmInstanceMockedToString").when(vmInstanceMock).toString(); + Mockito.doReturn(isOfferingUsingLocal).when(serviceOfferingMock).isUseLocalStorage(); + virtualMachineManagerImpl.checkIfNewOfferingStorageScopeMatchesStoragePool(vmInstanceMock, serviceOfferingMock); + } } 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 cf01b8db340..2e6bc8a39cc 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -32,6 +32,7 @@ import java.util.stream.Stream; import javax.inject.Inject; import com.cloud.storage.dao.VMTemplateDetailsDao; +import com.cloud.vm.VirtualMachineManager; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO; import org.apache.cloudstack.affinity.AffinityGroupResponse; @@ -424,6 +425,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject private UserDao userDao; + @Inject + private VirtualMachineManager virtualMachineManager; + /* * (non-Javadoc) * @@ -2959,8 +2963,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sc.addAnd("id", SearchCriteria.Op.NEQ, currentVmOffering.getId()); } - // 1. Only return offerings with the same storage type - sc.addAnd("useLocalStorage", SearchCriteria.Op.EQ, currentVmOffering.isUseLocalStorage()); + boolean isRootVolumeUsingLocalStorage = virtualMachineManager.isRootVolumeOnLocalStorage(vmId); + + // 1. Only return offerings with the same storage type than the storage pool where the VM's root volume is allocated + sc.addAnd("useLocalStorage", SearchCriteria.Op.EQ, isRootVolumeUsingLocalStorage); // 2.In case vm is running return only offerings greater than equal to current offering compute. if (vmInstance.getState() == VirtualMachine.State.Running) { From 2d176db9a474328dd7655c6a7d5bd20338d9bb21 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Fri, 30 Apr 2021 12:15:09 +0530 Subject: [PATCH 05/13] centos: Install libgcrypt v1.8.5 required by libvirt 6.0 on CentOS8 (#4970) Fixes: #4969 This PR upgrades the version of libgcrypt that is required by libevirt 6.0 Co-authored-by: Pearl1594 --- packaging/centos8/cloud.spec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packaging/centos8/cloud.spec b/packaging/centos8/cloud.spec index 60d86f7ca4c..fd05432b1fd 100644 --- a/packaging/centos8/cloud.spec +++ b/packaging/centos8/cloud.spec @@ -80,6 +80,7 @@ Requires: iptables-services Requires: qemu-img Requires: python3-pip Requires: python3-setuptools +Requires: libgcrypt > 1.8.3 Group: System Environment/Libraries %description management The CloudStack management server is the central point of coordination, @@ -109,6 +110,7 @@ Requires: perl Requires: python3-libvirt Requires: qemu-img Requires: qemu-kvm +Requires: libgcrypt > 1.8.3 Provides: cloud-agent Group: System Environment/Libraries %description agent From 603a83066d20de96498f270b471ff5330452032a Mon Sep 17 00:00:00 2001 From: davidjumani Date: Fri, 30 Apr 2021 13:17:14 +0530 Subject: [PATCH 06/13] ui: rename acl reason to description (#4980) --- ui/src/views/network/AclListRulesTab.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/views/network/AclListRulesTab.vue b/ui/src/views/network/AclListRulesTab.vue index 03d4f568633..8831491218e 100644 --- a/ui/src/views/network/AclListRulesTab.vue +++ b/ui/src/views/network/AclListRulesTab.vue @@ -82,7 +82,7 @@
{{ acl.traffictype }}
-
{{ $t('label.reason') }}
+
{{ $t('label.description') }}
{{ acl.reason }}
@@ -184,7 +184,7 @@ {{ $t('label.egress') }}
- + Date: Fri, 30 Apr 2021 16:37:24 +0530 Subject: [PATCH 07/13] ui: show domain paths for offering domain selection (#4979) Signed-off-by: Abhishek Kumar --- ui/src/views/offering/AddComputeOffering.vue | 2 +- ui/src/views/offering/AddDiskOffering.vue | 2 +- ui/src/views/offering/AddNetworkOffering.vue | 2 +- ui/src/views/offering/AddVpcOffering.vue | 2 +- ui/src/views/offering/UpdateOfferingAccess.vue | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/src/views/offering/AddComputeOffering.vue b/ui/src/views/offering/AddComputeOffering.vue index 3c09c6598f5..1536a5b8387 100644 --- a/ui/src/views/offering/AddComputeOffering.vue +++ b/ui/src/views/offering/AddComputeOffering.vue @@ -680,7 +680,7 @@ :loading="domainLoading" :placeholder="this.$t('label.domainid')"> - {{ opt.name || opt.description }} + {{ opt.path || opt.name || opt.description }}
diff --git a/ui/src/views/offering/AddDiskOffering.vue b/ui/src/views/offering/AddDiskOffering.vue index a392e8ebc0a..0b724b69a11 100644 --- a/ui/src/views/offering/AddDiskOffering.vue +++ b/ui/src/views/offering/AddDiskOffering.vue @@ -368,7 +368,7 @@ :loading="domainLoading" :placeholder="this.$t('label.domainid')"> - {{ opt.name || opt.description }} + {{ opt.path || opt.name || opt.description }}
diff --git a/ui/src/views/offering/AddNetworkOffering.vue b/ui/src/views/offering/AddNetworkOffering.vue index 4cafaebb9ea..48b39a3feca 100644 --- a/ui/src/views/offering/AddNetworkOffering.vue +++ b/ui/src/views/offering/AddNetworkOffering.vue @@ -410,7 +410,7 @@ :loading="domainLoading" :placeholder="this.$t('label.domain')"> - {{ opt.name || opt.description }} + {{ opt.path || opt.name || opt.description }} diff --git a/ui/src/views/offering/AddVpcOffering.vue b/ui/src/views/offering/AddVpcOffering.vue index 6921ddae025..cbe2db18cad 100644 --- a/ui/src/views/offering/AddVpcOffering.vue +++ b/ui/src/views/offering/AddVpcOffering.vue @@ -108,7 +108,7 @@ :loading="domainLoading" :placeholder="this.$t('label.domain')"> - {{ opt.name || opt.description }} + {{ opt.path || opt.name || opt.description }} diff --git a/ui/src/views/offering/UpdateOfferingAccess.vue b/ui/src/views/offering/UpdateOfferingAccess.vue index 3fb1cdb4cc1..f0c524cabca 100644 --- a/ui/src/views/offering/UpdateOfferingAccess.vue +++ b/ui/src/views/offering/UpdateOfferingAccess.vue @@ -48,7 +48,7 @@ :loading="domainLoading" :placeholder="this.apiParams.domainid.description"> - {{ opt.name || opt.description }} + {{ opt.path || opt.name || opt.description }} From 155636902c48c50b977011794f939572ad211c73 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 4 May 2021 19:33:00 +0530 Subject: [PATCH 08/13] ui: Close Create network form from Zones -> Physical Network (Guest) -> Traffic Types view (#4993) --- ui/src/views/infra/network/IpRangesTabGuest.vue | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/src/views/infra/network/IpRangesTabGuest.vue b/ui/src/views/infra/network/IpRangesTabGuest.vue index 2cf51d3ce41..58e1e659215 100644 --- a/ui/src/views/infra/network/IpRangesTabGuest.vue +++ b/ui/src/views/infra/network/IpRangesTabGuest.vue @@ -64,10 +64,10 @@ :maskClosable="false" :footer="null" :cancelText="$t('label.cancel')" - @cancel="showCreateForm = false" + @cancel="closeAction" centered width="auto"> - + @@ -161,6 +161,9 @@ export default { handleOpenShowCreateForm () { this.showCreateForm = true }, + closeAction () { + this.showCreateForm = false + }, changePage (page, pageSize) { this.page = page this.pageSize = pageSize From d92022ee5c5498b7c193cd28ef29c5a8f02f1a39 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 4 May 2021 19:33:13 +0530 Subject: [PATCH 09/13] ui: Hide reset password button for a running VM (#4991) --- ui/src/config/section/compute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 9428725302b..77d86337b1f 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -323,7 +323,7 @@ export default { label: 'label.action.reset.password', message: 'message.action.instance.reset.password', dataView: true, - show: (record) => { return ['Running', 'Stopped'].includes(record.state) && record.passwordenabled }, + show: (record) => { return ['Stopped'].includes(record.state) && record.passwordenabled }, response: (result) => { return result.virtualmachine && result.virtualmachine.password ? `The password of VM ${result.virtualmachine.displayname} is ${result.virtualmachine.password}` : null } }, { From 5b6ab3d248bca21d140d6ab6b0bb6b18c0b4f03e Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 4 May 2021 19:33:23 +0530 Subject: [PATCH 10/13] ui: fix for filtering network offering for VPC tiers (#4989) --- ui/src/views/network/VpcTiersTab.vue | 52 +++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/ui/src/views/network/VpcTiersTab.vue b/ui/src/views/network/VpcTiersTab.vue index 87ceec5d46f..8db3954e614 100644 --- a/ui/src/views/network/VpcTiersTab.vue +++ b/ui/src/views/network/VpcTiersTab.vue @@ -370,7 +370,13 @@ export default { publicIps: {}, snats: {}, vms: {} - } + }, + lbProviderMap: { + publicLb: { + vpc: ['VpcVirtualRouter', 'Netscaler'] + } + }, + publicLBExists: false } }, created () { @@ -399,6 +405,7 @@ export default { this.fetchLoadBalancers(network.id) this.fetchVMs(network.id) } + this.publicLBNetworkExists() }, fetchNetworkAclList () { this.fetchLoading = true @@ -417,6 +424,38 @@ export default { this.modalLoading = false }) }, + getNetworkOffering (networkId) { + return new Promise((resolve, reject) => { + api('listNetworkOfferings', { + id: networkId + }).then(json => { + var networkOffering = json.listnetworkofferingsresponse.networkoffering[0] + resolve(networkOffering) + }).catch(e => { + reject(e) + }) + }) + }, + publicLBNetworkExists () { + api('listNetworks', { + vpcid: this.resource.id, + supportedservices: 'LB' + }).then(async json => { + var lbNetworks = json.listnetworksresponse.network || [] + if (lbNetworks.length > 0) { + this.publicLBExists = true + for (var idx = 0; idx < lbNetworks.length; idx++) { + const lbNetworkOffering = await this.getNetworkOffering(lbNetworks[idx].networkofferingid) + const index = lbNetworkOffering.service.map(svc => { return svc.name }).indexOf('Lb') + if (index !== -1 && + this.lbProviderMap.publicLb.vpc.indexOf(lbNetworkOffering.service.map(svc => { return svc.provider[0].name })[index]) !== -1) { + this.publicLBExists = true + break + } + } + } + }) + }, fetchNetworkOfferings () { this.fetchLoading = true this.modalLoading = true @@ -427,6 +466,17 @@ export default { state: 'Enabled' }).then(json => { this.networkOfferings = json.listnetworkofferingsresponse.networkoffering || [] + var filteredOfferings = [] + if (this.publicLBExists) { + for (var index in this.networkOfferings) { + const offering = this.networkOfferings[index] + const idx = offering.service.map(svc => { return svc.name }).indexOf('Lb') + if (idx === -1 || this.lbProviderMap.publicLb.vpc.indexOf(offering.service.map(svc => { return svc.provider[0].name })[idx]) === -1) { + filteredOfferings.push(offering) + } + } + this.networkOfferings = filteredOfferings + } this.$nextTick(function () { this.form.setFieldsValue({ networkOffering: this.networkOfferings[0].id From eb2e5f73d33e2a66d71b2077abb0a72046864f15 Mon Sep 17 00:00:00 2001 From: davidjumani Date: Tue, 4 May 2021 19:33:45 +0530 Subject: [PATCH 11/13] ui: show VR offering when provider is VR (#4988) * ui: show VR offering when provider is VR * send serviceofferingid not index --- ui/src/components/CheckBoxSelectPair.vue | 17 +++++------------ ui/src/views/offering/AddNetworkOffering.vue | 4 ++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/ui/src/components/CheckBoxSelectPair.vue b/ui/src/components/CheckBoxSelectPair.vue index 5c95252d83b..ec6c6fa16c8 100644 --- a/ui/src/components/CheckBoxSelectPair.vue +++ b/ui/src/components/CheckBoxSelectPair.vue @@ -23,7 +23,7 @@ 0 }, getSelectInitialValue () { - if (this.arrayHasItems(this.selectOptions)) { - for (var i = 0; i < this.selectOptions.length; i++) { - if (this.selectOptions[i].enabled !== false) { - return this.selectOptions[i].name - } - } - } - return '' + const provider = this.selectOptions?.filter(x => x.enabled)?.[0]?.name || '' + this.handleSelectChange(provider) + return provider }, handleCheckChange (e) { this.checked = e.target.checked - if (this.checked && this.arrayHasItems(this.selectOptions)) { - this.selectedOption = this.selectOptions[0].name - } this.$emit('handle-checkpair-change', this.resourceKey, this.checked, '') }, handleSelectChange (val) { + this.selectedOption = val this.$emit('handle-checkpair-change', this.resourceKey, this.checked, val) } } diff --git a/ui/src/views/offering/AddNetworkOffering.vue b/ui/src/views/offering/AddNetworkOffering.vue index 48b39a3feca..910bc2bb8ab 100644 --- a/ui/src/views/offering/AddNetworkOffering.vue +++ b/ui/src/views/offering/AddNetworkOffering.vue @@ -228,7 +228,7 @@ message: `${this.$t('message.error.select')}` } ], - initialValue: 0 + initialValue: this.serviceOfferings.length > 0 ? this.serviceOfferings[0].id : '' }]" showSearch optionFilterProp="children" @@ -237,7 +237,7 @@ }" :loading="serviceOfferingLoading" :placeholder="this.$t('label.serviceofferingid')"> - + {{ opt.name || opt.description }} From 1cb8ca69d9e6a670cec98de1652e47c25a5aca5a Mon Sep 17 00:00:00 2001 From: davidjumani Date: Tue, 4 May 2021 19:34:11 +0530 Subject: [PATCH 12/13] ui: Adding success message for DomainActionForm (#4987) --- ui/src/views/iam/DomainActionForm.vue | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ui/src/views/iam/DomainActionForm.vue b/ui/src/views/iam/DomainActionForm.vue index 769da6ce6f9..7e15e32e73c 100644 --- a/ui/src/views/iam/DomainActionForm.vue +++ b/ui/src/views/iam/DomainActionForm.vue @@ -249,6 +249,7 @@ export default { } } + const resourceName = params.displayname || params.displaytext || params.name || this.resource.name let hasJobId = false api(this.action.api, params).then(json => { for (const obj in json) { @@ -270,6 +271,18 @@ export default { } } if (!hasJobId) { + var message = this.action.successMessage ? this.$t(this.action.successMessage) : this.$t(this.action.label) + + (resourceName ? ' - ' + resourceName : '') + var duration = 2 + if (this.action.additionalMessage) { + message = message + ' - ' + this.$t(this.action.successMessage) + duration = 5 + } + this.$message.success({ + content: message, + key: this.action.label + resourceName, + duration: duration + }) this.parentUpdActionData(json) this.parentFetchData() } From 4df8d7ade3de33ccc8aa8dd74fb40ce2473f82c1 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 4 May 2021 19:34:53 +0530 Subject: [PATCH 13/13] ui: Prevent reset of port-forward rules on cancelling a form (#4981) * ui: Prevent reset of port-forward rules on cancelling a form * add check for undefined value --- ui/src/views/network/LoadBalancing.vue | 2 +- ui/src/views/network/PortForwarding.vue | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/src/views/network/LoadBalancing.vue b/ui/src/views/network/LoadBalancing.vue index 6856251c3c9..ddbcf3861e2 100644 --- a/ui/src/views/network/LoadBalancing.vue +++ b/ui/src/views/network/LoadBalancing.vue @@ -554,7 +554,7 @@ export default { vpcid: this.resource.vpcid }).then(json => { this.tiers.data = json.listnetworksresponse.network || [] - this.selectedTier = this.tiers.data && this.tiers.data[0].id ? this.tiers.data[0].id : null + this.selectedTier = this.tiers.data?.[0]?.id ? this.tiers.data[0].id : null this.$forceUpdate() }).catch(error => { this.$notifyError(error) diff --git a/ui/src/views/network/PortForwarding.vue b/ui/src/views/network/PortForwarding.vue index d71672e73b7..29208d96006 100644 --- a/ui/src/views/network/PortForwarding.vue +++ b/ui/src/views/network/PortForwarding.vue @@ -503,7 +503,6 @@ export default { this.addVmModalNicLoading = false this.nics = [] this.resetTagInputs() - this.resetAllRules() }, openTagsModal (id) { this.tagsModalLoading = true