diff --git a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java index 5343fb632b5..0232d07b8be 100644 --- a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java +++ b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java @@ -281,4 +281,6 @@ public interface ConfigurationManager { Pair getConfigurationGroupAndSubGroup(String configName); List getConfigurationSubGroups(Long groupId); + + void validateExtraConfigInServiceOfferingDetail(String detailName); } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 3dbab7d5b68..b47215e255f 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -201,6 +201,7 @@ import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostTagsDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.hypervisor.kvm.dpdk.DpdkHelper; import com.cloud.network.IpAddress; import com.cloud.network.IpAddressManager; import com.cloud.network.Ipv6GuestPrefixSubnetNetworkMapVO; @@ -3244,6 +3245,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati } } if (detailEntry.getKey().startsWith(ApiConstants.EXTRA_CONFIG)) { + validateExtraConfigInServiceOfferingDetail(detailEntry.getKey()); try { detailEntryValue = URLDecoder.decode(detailEntry.getValue(), "UTF-8"); } catch (UnsupportedEncodingException | IllegalArgumentException e) { @@ -3309,6 +3311,14 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati } } + @Override + public void validateExtraConfigInServiceOfferingDetail(String detailName) { + if (!detailName.equals(DpdkHelper.DPDK_NUMA) && !detailName.equals(DpdkHelper.DPDK_HUGE_PAGES) + && !detailName.startsWith(DpdkHelper.DPDK_INTERFACE_PREFIX)) { + throw new InvalidParameterValueException("Only extraconfig for DPDK are supported in service offering details"); + } + } + private DiskOfferingVO createDiskOfferingInternal(final long userId, final boolean isSystem, final VirtualMachine.Type vmType, final String name, final Integer cpu, final Integer ramSize, final Integer speed, final String displayText, final ProvisioningType typedProvisioningType, final boolean localStorageRequired, final boolean offerHA, final boolean limitResourceUse, final boolean volatileVm, String tags, final List domainIds, List zoneIds, final String hostTag, diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java index 177fd331dee..74d9130cc04 100644 --- a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java +++ b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java @@ -38,6 +38,7 @@ import com.cloud.agent.api.Command; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.configuration.ConfigurationManager; import com.cloud.gpu.GPU; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; @@ -60,6 +61,7 @@ import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmManager; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; @@ -97,6 +99,10 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis @Inject protected HostDao hostDao; + @Inject + private UserVmManager userVmManager; + @Inject + private ConfigurationManager configurationManager; public static ConfigKey VmMinMemoryEqualsMemoryDividedByMemOverprovisioningFactor = new ConfigKey("Advanced", Boolean.class, "vm.min.memory.equals.memory.divided.by.mem.overprovisioning.factor", "true", "If we set this to 'true', a minimum memory (memory/ mem.overprovisioning.factor) will be set to the VM, independent of using a scalable service offering or not.", true, ConfigKey.Scope.Cluster); @@ -181,10 +187,12 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis /** * Add extra configuration from VM details. Extra configuration is stored as details starting with 'extraconfig' */ - private void addExtraConfig(Map details, VirtualMachineTO to) { + private void addExtraConfig(Map details, VirtualMachineTO to, long accountId, Hypervisor.HypervisorType hypervisorType) { for (String key : details.keySet()) { if (key.startsWith(ApiConstants.EXTRA_CONFIG)) { - to.addExtraConfig(key, details.get(key)); + String extraConfig = details.get(key); + userVmManager.validateExtraConfig(accountId, hypervisorType, extraConfig); + to.addExtraConfig(key, extraConfig); } } } @@ -200,6 +208,7 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis if (CollectionUtils.isNotEmpty(details)) { for (ServiceOfferingDetailsVO detail : details) { if (detail.getName().startsWith(ApiConstants.EXTRA_CONFIG)) { + configurationManager.validateExtraConfigInServiceOfferingDetail(detail.getName()); to.addExtraConfig(detail.getName(), detail.getValue()); } } @@ -263,7 +272,7 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis Map detailsInVm = _userVmDetailsDao.listDetailsKeyPairs(vm.getId()); if (detailsInVm != null) { to.setDetails(detailsInVm); - addExtraConfig(detailsInVm, to); + addExtraConfig(detailsInVm, to, vm.getAccountId(), vm.getHypervisorType()); } addServiceOfferingExtraConfiguration(offering, to); diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index 4f1396913cc..b107a520205 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -30,6 +30,7 @@ import com.cloud.exception.ManagementServerException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.exception.VirtualMachineMigrationException; +import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.offering.ServiceOffering; import com.cloud.service.ServiceOfferingVO; import com.cloud.storage.Storage.StoragePoolType; @@ -96,6 +97,8 @@ public interface UserVmManager extends UserVmService { String validateUserData(String userData, HTTPMethod httpmethod); + void validateExtraConfig(long accountId, HypervisorType hypervisorType, String extraConfig); + boolean isVMUsingLocalStorage(VMInstanceVO vm); boolean expunge(UserVmVO vm); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 0d3f047809a..ae0d66ee482 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -645,7 +645,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir "enable.additional.vm.configuration", "false", "allow additional arbitrary configuration to vm", true, ConfigKey.Scope.Account); private static final ConfigKey KvmAdditionalConfigAllowList = new ConfigKey<>(String.class, - "allow.additional.vm.configuration.list.kvm", "Advanced", "", "Comma separated list of allowed additional configuration options.", true, ConfigKey.Scope.Global, null, null, EnableAdditionalVmConfig.key(), null, null, ConfigKey.Kind.CSV, null); + "allow.additional.vm.configuration.list.kvm", "Advanced", "", "Comma separated list of allowed additional configuration options.", true, ConfigKey.Scope.Account, null, null, EnableAdditionalVmConfig.key(), null, null, ConfigKey.Kind.CSV, null); private static final ConfigKey XenServerAdditionalConfigAllowList = new ConfigKey<>(String.class, "allow.additional.vm.configuration.list.xenserver", "Advanced", "", "Comma separated list of allowed additional configuration options", true, ConfigKey.Scope.Global, null, null, EnableAdditionalVmConfig.key(), null, null, ConfigKey.Kind.CSV, null); @@ -2796,14 +2796,15 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (cleanupDetails){ if (caller != null && caller.getType() == Account.Type.ADMIN) { for (final UserVmDetailVO detail : existingDetails) { - if (detail != null && detail.isDisplay()) { + if (detail != null && detail.isDisplay() && !isExtraConfig(detail.getName())) { userVmDetailsDao.removeDetail(id, detail.getName()); } } } else { for (final UserVmDetailVO detail : existingDetails) { if (detail != null && !userDenyListedSettings.contains(detail.getName()) - && !userReadOnlySettings.contains(detail.getName()) && detail.isDisplay()) { + && !userReadOnlySettings.contains(detail.getName()) && detail.isDisplay() + && !isExtraConfig(detail.getName())) { userVmDetailsDao.removeDetail(id, detail.getName()); } } @@ -2814,6 +2815,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir throw new InvalidParameterValueException("'extraconfig' should not be included in details as key"); } + details.entrySet().removeIf(detail -> isExtraConfig(detail.getKey())); + if (caller != null && caller.getType() != Account.Type.ADMIN) { // Ensure denied or read-only detail is not passed by non-root-admin user for (final String detailName : details.keySet()) { @@ -2837,7 +2840,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir // ensure details marked as non-displayable are maintained, regardless of admin or not for (final UserVmDetailVO existingDetail : existingDetails) { - if (!existingDetail.isDisplay()) { + if (!existingDetail.isDisplay() || isExtraConfig(existingDetail.getName())) { details.put(existingDetail.getName(), existingDetail.getValue()); } } @@ -2859,6 +2862,10 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir cmd.getHttpMethod(), cmd.getCustomId(), hostName, cmd.getInstanceName(), securityGroupIdList, cmd.getDhcpOptionsMap()); } + private boolean isExtraConfig(String detailName) { + return detailName != null && detailName.startsWith(ApiConstants.EXTRA_CONFIG); + } + protected void updateDisplayVmFlag(Boolean isDisplayVm, Long id, UserVmVO vmInstance) { vmInstance.setDisplayVm(isDisplayVm); @@ -6173,7 +6180,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir */ protected void persistExtraConfigKvm(String decodedUrl, UserVm vm) { // validate config against denied cfg commands - validateKvmExtraConfig(decodedUrl); + validateKvmExtraConfig(decodedUrl, vm.getAccountId()); String[] extraConfigs = decodedUrl.split("\n\n"); for (String cfg : extraConfigs) { int i = 1; @@ -6191,6 +6198,18 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir i++; } } + /** + * This method is used to validate if extra config is valid + */ + @Override + public void validateExtraConfig(long accountId, HypervisorType hypervisorType, String extraConfig) { + if (!EnableAdditionalVmConfig.valueIn(accountId)) { + throw new CloudRuntimeException("Additional VM configuration is not enabled for this account"); + } + if (HypervisorType.KVM.equals(hypervisorType)) { + validateKvmExtraConfig(extraConfig, accountId); + } + } /** * This method is called by the persistExtraConfigKvm @@ -6198,8 +6217,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir * controlled by Root admin * @param decodedUrl string containing xml configuration to be validated */ - protected void validateKvmExtraConfig(String decodedUrl) { - String[] allowedConfigOptionList = KvmAdditionalConfigAllowList.value().split(","); + protected void validateKvmExtraConfig(String decodedUrl, long accountId) { + String[] allowedConfigOptionList = KvmAdditionalConfigAllowList.valueIn(accountId).split(","); // Skip allowed keys validation for DPDK if (!decodedUrl.contains(":")) { try { @@ -6219,7 +6238,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } if (!isValidConfig) { - throw new CloudRuntimeException(String.format("Extra config %s is not on the list of allowed keys for KVM hypervisor hosts", currentConfig)); + throw new CloudRuntimeException(String.format("Extra config '%s' is not on the list of allowed keys for KVM hypervisor hosts", currentConfig)); } } } catch (ParserConfigurationException | IOException | SAXException e) { @@ -6321,6 +6340,12 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (details.containsKey("extraconfig")) { throw new InvalidParameterValueException("'extraconfig' should not be included in details as key"); } + + for (String detailName : details.keySet()) { + if (isExtraConfig(detailName)) { + throw new InvalidParameterValueException("detail name should not start with extraconfig"); + } + } } } diff --git a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java index 1d9b65afd1b..8c6e73fcf70 100644 --- a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java @@ -677,4 +677,9 @@ public class MockConfigurationManagerImpl extends ManagerBase implements Configu // TODO Auto-generated method stub return null; } + + @Override + public void validateExtraConfigInServiceOfferingDetail(String detailName) { + // TODO Auto-generated method stub + } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 17a70b423b3..245e9efc23f 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -13,6 +13,7 @@ "error.release.dedicate.host": "Failed to release dedicated host.", "error.release.dedicate.pod": "Failed to release dedicated pod.", "error.release.dedicate.zone": "Failed to release dedicated zone.", +"error.unable.to.add.setting.extraconfig": "It is not allowed to add setting for extraconfig. Please update VirtualMachine with extraconfig parameter.", "error.unable.to.proceed": "Unable to proceed. Please contact your administrator.", "firewall.close": "Firewall", "icmp.code.desc": "Please specify -1 if you want to allow all ICMP codes.", diff --git a/ui/src/components/view/DetailSettings.vue b/ui/src/components/view/DetailSettings.vue index ba96ad6ee8b..ba9f61860e4 100644 --- a/ui/src/components/view/DetailSettings.vue +++ b/ui/src/components/view/DetailSettings.vue @@ -101,7 +101,7 @@ @@ -115,7 +115,12 @@ :cancelText="$t('label.no')" placement="left" > - + @@ -307,6 +312,10 @@ export default { this.error = this.$t('message.error.provide.setting') return } + if (this.newKey.startsWith('extraconfig')) { + this.error = this.$t('error.unable.to.add.setting.extraconfig') + return + } if (!this.allowEditOfDetail(this.newKey)) { this.error = this.$t('error.unable.to.proceed') return