UI support for extraconfig in deploy and update instance (#11719)
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 89c9a19..4abc0d1 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -26,6 +26,7 @@
public static final String ACTIVATION_RULE = "activationrule";
public static final String ACTIVITY = "activity";
public static final String ADAPTER_TYPE = "adaptertype";
+ public static final String ADDITONAL_CONFIG_ENABLED = "additionalconfigenabled";
public static final String ADDRESS = "address";
public static final String ALGORITHM = "algorithm";
public static final String ALIAS = "alias";
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java
index bd3f39a..7553ccf 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java
@@ -73,6 +73,7 @@
response.setSharedFsVmMinCpuCount((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT));
response.setSharedFsVmMinRamSize((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE));
response.setDynamicScalingEnabled((Boolean) capabilities.get(ApiConstants.DYNAMIC_SCALING_ENABLED));
+ response.setAdditionalConfigEnabled((Boolean) capabilities.get(ApiConstants.ADDITONAL_CONFIG_ENABLED));
response.setObjectName("capability");
response.setResponseName(getCommandName());
this.setResponseObject(response);
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java
index ff2e33b..affa130 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java
@@ -140,6 +140,10 @@
@Param(description = "true if dynamically scaling for instances is enabled", since = "4.21.0")
private Boolean dynamicScalingEnabled;
+ @SerializedName(ApiConstants.ADDITONAL_CONFIG_ENABLED)
+ @Param(description = "true if additional configurations or extraconfig can be passed to Instances", since = "4.20.2")
+ private Boolean additionalConfigEnabled;
+
public void setSecurityGroupsEnabled(boolean securityGroupsEnabled) {
this.securityGroupsEnabled = securityGroupsEnabled;
}
@@ -255,4 +259,8 @@
public void setDynamicScalingEnabled(Boolean dynamicScalingEnabled) {
this.dynamicScalingEnabled = dynamicScalingEnabled;
}
+
+ public void setAdditionalConfigEnabled(Boolean additionalConfigEnabled) {
+ this.additionalConfigEnabled = additionalConfigEnabled;
+ }
}
diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
index 271372b..56e8a56 100644
--- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
@@ -4535,6 +4535,8 @@
}
capabilities.put(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT, fsVmMinCpu);
capabilities.put(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE, fsVmMinRam);
+ capabilities.put(ApiConstants.ADDITONAL_CONFIG_ENABLED, UserVmManager.EnableAdditionalVmConfig.valueIn(caller.getId()));
+
return capabilities;
}
diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java
index f2a8a67..21ac6e3 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManager.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManager.java
@@ -83,6 +83,15 @@
"If set to true, tags specified in `resource.limit.host.tags` are also included in vm.strict.host.tags.",
true);
+ ConfigKey<Boolean> EnableAdditionalVmConfig = new ConfigKey<>(
+ "Advanced",
+ Boolean.class,
+ "enable.additional.vm.configuration",
+ "false",
+ "allow additional arbitrary configuration to vm",
+ true,
+ ConfigKey.Scope.Account);
+
static final int MAX_USER_DATA_LENGTH_BYTES = 2048;
public static final String CKS_NODE = "cksnode";
diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
index 5b3284c..a67484b 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
@@ -670,9 +670,6 @@
private static final ConfigKey<Boolean> AllowDeployVmIfGivenHostFails = new ConfigKey<Boolean>("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<Boolean> EnableAdditionalVmConfig = new ConfigKey<>("Advanced", Boolean.class,
- "enable.additional.vm.configuration", "false", "allow additional arbitrary configuration to vm", true, ConfigKey.Scope.Account);
-
private static final ConfigKey<String> KvmAdditionalConfigAllowList = new ConfigKey<>(String.class,
"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);
@@ -6280,7 +6277,7 @@
protected void persistExtraConfigVmware(String decodedUrl, UserVm vm) {
boolean isValidConfig = isValidKeyValuePair(decodedUrl);
if (isValidConfig) {
- String[] extraConfigs = decodedUrl.split("\\r?\\n");
+ String[] extraConfigs = decodedUrl.split("\\r?\\n+");
for (String cfg : extraConfigs) {
// Validate cfg against unsupported operations set by admin here
String[] allowedKeyList = VmwareAdditionalConfigAllowList.value().split(",");
@@ -6308,7 +6305,7 @@
protected void persistExtraConfigXenServer(String decodedUrl, UserVm vm) {
boolean isValidConfig = isValidKeyValuePair(decodedUrl);
if (isValidConfig) {
- String[] extraConfigs = decodedUrl.split("\\r?\\n");
+ String[] extraConfigs = decodedUrl.split("\\r?\\n+");
int i = 1;
String extraConfigKey = ApiConstants.EXTRA_CONFIG + "-";
for (String cfg : extraConfigs) {
@@ -6388,8 +6385,8 @@
// validate config against denied cfg commands
validateKvmExtraConfig(decodedUrl, vm.getAccountId());
String[] extraConfigs = decodedUrl.split("\n\n");
+ int i = 1;
for (String cfg : extraConfigs) {
- int i = 1;
String[] cfgParts = cfg.split("\n");
String extraConfigKey = ApiConstants.EXTRA_CONFIG;
String extraConfigValue;
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 79ea9bb..d18c094 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -975,6 +975,8 @@
"label.externalid": "External Id",
"label.externalloadbalanceripaddress": "External load balancer IP address.",
"label.extra": "Extra arguments",
+"label.extraconfig": "Additional Configuration",
+"label.extraconfig.tooltip": "Additional configuration parameters (extraconfig) to pass to the instance in plain text",
"label.f5": "F5",
"label.f5.ip.loadbalancer": "F5 BIG-IP load balancer.",
"label.failed": "Failed",
diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue
index a604fe6..82a54ed 100644
--- a/ui/src/views/compute/DeployVM.vue
+++ b/ui/src/views/compute/DeployVM.vue
@@ -724,6 +724,12 @@
</div>
</a-card>
</a-form-item>
+ <a-form-item v-if="extraConfigEnabled" name="extraconfig" ref="extraconfig">
+ <template #label>
+ <tooltip-label :title="$t('label.extraconfig')" :tooltip="$t('label.extraconfig.tooltip')"/>
+ </template>
+ <a-textarea v-model:value="form.extraconfig"/>
+ </a-form-item>
<a-form-item :label="$t('label.affinity.groups')">
<affinity-group-selection
:items="options.affinityGroups"
@@ -1418,6 +1424,9 @@
dynamicScalingVmConfigValue () {
return this.$store.getters.features.dynamicscalingenabled
},
+ extraConfigEnabled () {
+ return this.$store.getters.features.additionalconfigenabled
+ },
isCustomizedDiskIOPS () {
return this.diskSelected?.iscustomizediops || false
},
@@ -2054,6 +2063,9 @@
if (isUserdataAllowed && values.userdata && values.userdata.length > 0) {
deployVmData.userdata = this.$toBase64AndURIEncoded(values.userdata)
}
+ if (values.extraconfig && values.extraconfig.length > 0) {
+ deployVmData.extraconfig = encodeURIComponent(values.extraconfig)
+ }
// step 2: select template/iso
if (this.tabKey === 'templateid') {
deployVmData.templateid = values.templateid
diff --git a/ui/src/views/compute/EditVM.vue b/ui/src/views/compute/EditVM.vue
index d5e75fc..9e60175 100644
--- a/ui/src/views/compute/EditVM.vue
+++ b/ui/src/views/compute/EditVM.vue
@@ -91,6 +91,12 @@
<a-textarea v-model:value="form.userdata">
</a-textarea>
</a-form-item>
+ <a-form-item v-if="extraConfigEnabled">
+ <template #label>
+ <tooltip-label :title="$t('label.extraconfig')" :tooltip="$t('label.extraconfig.tooltip')"/>
+ </template>
+ <a-textarea v-model:value="form.extraconfig"/>
+ </a-form-item>
<a-form-item ref="securitygroupids" name="securitygroupids" :label="$t('label.security.groups')" v-if="securityGroupsEnabled">
<a-select
mode="multiple"
@@ -167,6 +173,19 @@
}
}
},
+ computed: {
+ extraConfigEnabled () {
+ return this.$store.getters.features.additionalconfigenabled
+ },
+ combinedExtraConfig () {
+ if (!this.extraConfigEnabled || !this.resource.details) return ''
+ const configs = Object.keys(this.resource.details)
+ .filter(key => key.startsWith('extraconfig-'))
+ .map(key => this.resource.details[key] || '')
+ .filter(val => val.trim())
+ return configs.join('\n\n')
+ }
+ },
beforeCreate () {
this.apiParams = this.$getApiParams('updateVirtualMachine')
},
@@ -185,7 +204,8 @@
deleteprotection: this.resource.deleteprotection,
group: this.resource.group,
userdata: '',
- haenable: this.resource.haenable
+ haenable: this.resource.haenable,
+ extraconfig: this.combinedExtraConfig
})
this.rules = reactive({})
},
@@ -342,6 +362,9 @@
if (values.userdata && values.userdata.length > 0) {
params.userdata = this.$toBase64AndURIEncoded(values.userdata)
}
+ if (values.extraconfig && values.extraconfig.length > 0) {
+ params.extraconfig = encodeURIComponent(values.extraconfig)
+ }
this.loading = true
api('updateVirtualMachine', {}, 'POST', params).then(json => {