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/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index 5dabc5e270a..e7828fabef8 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -262,5 +262,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 b2de40deac2..5fe3de523bb 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -3828,22 +3828,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()) { @@ -3865,6 +3850,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 d2d5fc89819..e478290d486 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; @@ -624,4 +625,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/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 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 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 70875eb2694..f204ead3055 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) * @@ -2962,8 +2966,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) { 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 9cd88b28c7e..b245d45558f 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -1405,24 +1405,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"] @@ -1440,6 +1438,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 049315fb52b..25a2e7e6e3f 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -718,7 +718,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/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/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: { diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 6ffd5091d90..3f97995cd16 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -317,7 +317,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 } }, { 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() } 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" /> + + + + + - + - + + - + @@ -161,6 +161,9 @@ export default { handleOpenShowCreateForm () { this.showCreateForm = true }, + closeAction () { + this.showCreateForm = false + }, changePage (page, pageSize) { this.page = page this.pageSize = pageSize 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') }}
- + { 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 diff --git a/ui/src/views/network/VpcTiersTab.vue b/ui/src/views/network/VpcTiersTab.vue index aae19150934..9c512b74650 100644 --- a/ui/src/views/network/VpcTiersTab.vue +++ b/ui/src/views/network/VpcTiersTab.vue @@ -385,7 +385,13 @@ export default { publicIps: {}, snats: {}, vms: {} - } + }, + lbProviderMap: { + publicLb: { + vpc: ['VpcVirtualRouter', 'Netscaler'] + } + }, + publicLBExists: false } }, created () { @@ -417,6 +423,7 @@ export default { this.fetchLoadBalancers(network.id) this.fetchVMs(network.id) } + this.publicLBNetworkExists() }, fetchNetworkAclList () { this.fetchLoading = true @@ -435,6 +442,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 @@ -445,6 +484,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 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..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 }} @@ -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 }}