diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java index ecbde47692f2..e71b4feea032 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java @@ -188,7 +188,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme @Parameter(name = ApiConstants.MAC_ADDRESS, type = CommandType.STRING, description = "the mac address for default vm's network") private String macAddress; - @Parameter(name = ApiConstants.KEYBOARD, type = CommandType.STRING, description = "an optional keyboard device type for the virtual machine. valid value can be one of de,de-ch,es,fi,fr,fr-be,fr-ch,is,it,jp,nl-be,no,pt,uk,us") + @Parameter(name = ApiConstants.KEYBOARD, type = CommandType.STRING, description = "an optional keyboard device type for the virtual machine. valid value can be one of de,de-ch,es,es-latam,fi,fr,fr-be,fr-ch,is,it,jp,nl-be,no,pt,uk,us") private String keyboard; @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "Deploy vm for the project") diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index 29681b5e38fc..b1e048e5025f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -340,6 +340,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "List of read-only Vm details as comma separated string.", since = "4.16.0") private String readOnlyDetails; + @SerializedName("alloweddetails") + @Param(description = "List of allowed Vm details as comma separated string if VM instance settings are read from OVA.", since = "4.22.1") + private String allowedDetails; + @SerializedName(ApiConstants.SSH_KEYPAIRS) @Param(description = "ssh key-pairs") private String keyPairNames; @@ -1091,6 +1095,10 @@ public void setReadOnlyDetails(String readOnlyDetails) { this.readOnlyDetails = readOnlyDetails; } + public void setAllowedDetails(String allowedDetails) { + this.allowedDetails = allowedDetails; + } + public void setOsTypeId(String osTypeId) { this.osTypeId = osTypeId; } @@ -1115,6 +1123,10 @@ public String getReadOnlyDetails() { return readOnlyDetails; } + public String getAllowedDetails() { + return allowedDetails; + } + public Boolean getDynamicallyScalable() { return isDynamicallyScalable; } 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 e1dc73a3225d..e6eb58ccee96 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -5371,7 +5371,7 @@ private void fillVMOrTemplateDetailOptions(final Map> optio options.put(ApiConstants.BootType.UEFI.toString(), Arrays.asList(ApiConstants.BootMode.LEGACY.toString(), ApiConstants.BootMode.SECURE.toString())); - options.put(VmDetailConstants.KEYBOARD, Arrays.asList("uk", "us", "jp", "fr")); + options.put(VmDetailConstants.KEYBOARD, Arrays.asList("uk", "us", "jp", "fr", "es-latam")); options.put(VmDetailConstants.CPU_CORE_PER_SOCKET, Collections.emptyList()); options.put(VmDetailConstants.ROOT_DISK_SIZE, Collections.emptyList()); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index a2f9544de39b..93dca8cc07a1 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -69,9 +69,11 @@ import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOS; import com.cloud.storage.Storage.TemplateType; +import com.cloud.storage.VMTemplateVO; import com.cloud.storage.VnfTemplateDetailVO; import com.cloud.storage.VnfTemplateNicVO; import com.cloud.storage.Volume; +import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VnfTemplateDetailsDao; import com.cloud.storage.dao.VnfTemplateNicDao; import com.cloud.user.Account; @@ -124,6 +126,8 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation VmDetailSearch; private final SearchBuilder activeVmByIsoSearch; @@ -465,6 +469,10 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us if (caller.getType() != Account.Type.ADMIN) { userVmResponse.setReadOnlyDetails(QueryService.UserVMReadOnlyDetails.value()); } + VMTemplateVO template = vmTemplateDao.findByIdIncludingRemoved(userVm.getTemplateId()); + if (template != null && template.isDeployAsIs() && UserVmManager.VmwareAdditionalDetailsFromOvaEnabled.valueIn(userVm.getDataCenterId())) { + userVmResponse.setAllowedDetails(UserVmManager.VmwareAllowedAdditionalDetailsFromOva.valueIn(userVm.getDataCenterId())); + } } userVmResponse.setObjectName(objectName); diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index 972c6cbea89d..c035165a3fa8 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -99,6 +99,15 @@ public interface UserVmManager extends UserVmService { ConfigKey.Scope.Account); + ConfigKey VmwareAdditionalDetailsFromOvaEnabled = new ConfigKey("Advanced", Boolean.class, + "vmware.additional.details.from.ova.enabled", "false", + "If true, allow users to add additional VM settings if VM instance settings are read from OVA.", true, ConfigKey.Scope.Zone); + + ConfigKey VmwareAllowedAdditionalDetailsFromOva = new ConfigKey<>(String.class, + "vmware.allowed.additional.details.from.ova", "Advanced", "", + "Comma separated list of allowed additional VM settings if VM instance settings are read from OVA.", + true, ConfigKey.Scope.Zone, null, null, null, null, null, ConfigKey.Kind.CSV, null); + 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 96c87c5376d8..df8e17fc014e 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -2884,11 +2884,7 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx UserVmVO vmInstance = _vmDao.findById(cmd.getId()); VMTemplateVO template = _templateDao.findById(vmInstance.getTemplateId()); - if (MapUtils.isNotEmpty(details) || cmd.isCleanupDetails()) { - if (template != null && template.isDeployAsIs()) { - throw new CloudRuntimeException("Detail settings are read from OVA, it cannot be changed by API call."); - } - } + UserVmVO userVm = _vmDao.findById(cmd.getId()); if (userVm != null && UserVmManager.SHAREDFSVM.equals(userVm.getUserVmType())) { throw new InvalidParameterValueException("Operation not supported on Shared FileSystem Instance"); @@ -2918,6 +2914,9 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx .collect(Collectors.toList()); List existingDetails = vmInstanceDetailsDao.listDetails(id); if (cleanupDetails){ + if (template != null && template.isDeployAsIs()) { + throw new InvalidParameterValueException("Detail settings are read from OVA, it cannot be cleaned up by API call."); + } if (caller != null && caller.getType() == Account.Type.ADMIN) { for (final VMInstanceDetailVO detail : existingDetails) { if (detail != null && detail.isDisplay() && !isExtraConfig(detail.getName())) { @@ -2946,6 +2945,23 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx throw new InvalidParameterValueException("'extraconfig' should not be included in details as key"); } + if (template != null && template.isDeployAsIs()) { + final List vmwareAllowedDetailsFromOva = VmwareAdditionalDetailsFromOvaEnabled.valueIn(vmInstance.getDataCenterId()) ? + Stream.of(VmwareAllowedAdditionalDetailsFromOva.valueIn(vmInstance.getDataCenterId()).split(",")) + .map(String::trim) + .collect(Collectors.toList()) : List.of(); + for (String detailKey : details.keySet()) { + if (vmwareAllowedDetailsFromOva.contains(detailKey)) { + continue; + } + VMInstanceDetailVO detailVO = existingDetails.stream().filter(d -> Objects.equals(d.getName(), detailKey)).findFirst().orElse(null); + if (detailVO != null && ObjectUtils.allNotNull(detailVO.getValue(), details.get(detailKey)) && detailVO.getValue().equals(details.get(detailKey))) { + continue; + } + throw new InvalidParameterValueException("Detail settings are read from OVA, it cannot be changed by API call."); + } + } + details.entrySet().removeIf(detail -> isExtraConfig(detail.getKey())); if (caller != null && caller.getType() != Account.Type.ADMIN) { @@ -9336,7 +9352,8 @@ public ConfigKey[] getConfigKeys() { return new ConfigKey[] {EnableDynamicallyScaleVm, AllowDiskOfferingChangeDuringScaleVm, AllowUserExpungeRecoverVm, VmIpFetchWaitInterval, VmIpFetchTrialMax, VmIpFetchThreadPoolMax, VmIpFetchTaskWorkers, AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig, DisplayVMOVFProperties, KvmAdditionalConfigAllowList, XenServerAdditionalConfigAllowList, VmwareAdditionalConfigAllowList, DestroyRootVolumeOnVmDestruction, - EnforceStrictResourceLimitHostTagCheck, StrictHostTags, AllowUserForceStopVm, VmDistinctHostNameScope}; + EnforceStrictResourceLimitHostTagCheck, StrictHostTags, AllowUserForceStopVm, VmDistinctHostNameScope, + VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva}; } @Override diff --git a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java index 14074add021d..34e5e48cc32f 100755 --- a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java +++ b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.EnumSet; +import com.cloud.storage.dao.VMTemplateDao; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ResponseObject; @@ -78,6 +79,9 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT @Mock private VnfTemplateDetailsDao vnfTemplateDetailsDao; + @Mock + private VMTemplateDao vmTemplateDao; + private UserVmJoinVO userVm = new UserVmJoinVO(); private UserVmResponse userVmResponse = new UserVmResponse(); diff --git a/systemvm/agent/noVNC/core/rfb.js b/systemvm/agent/noVNC/core/rfb.js index 8faa993d425b..59218b136b94 100644 --- a/systemvm/agent/noVNC/core/rfb.js +++ b/systemvm/agent/noVNC/core/rfb.js @@ -39,6 +39,7 @@ import ZRLEDecoder from "./decoders/zrle.js"; import JPEGDecoder from "./decoders/jpeg.js"; import H264Decoder from "./decoders/h264.js"; import SCANCODES_JP from "../keymaps/keymap-ja-atset1.js" +import SCANCODES_ES_LATAM from "../keymaps/keymap-es-latam-atset1.js" // How many seconds to wait for a disconnect to finish const DISCONNECT_TIMEOUT = 3; @@ -127,6 +128,8 @@ export default class RFB extends EventTargetMixin { this._scancodes = {}; if (this._language === "jp") { this._scancodes = SCANCODES_JP; + } else if (this._language === "es-latam") { + this._scancodes = SCANCODES_ES_LATAM; } // Internal state @@ -197,6 +200,7 @@ export default class RFB extends EventTargetMixin { // Keys this._shiftPressed = false; this._shiftKey = KeyTable.XK_Shift_L; + this._altgrPressed = false; // Mouse state this._mousePos = {}; @@ -531,6 +535,10 @@ export default class RFB extends EventTargetMixin { this._shiftKey = down ? keysym : KeyTable.XK_Shift_L; } + if (keysym === KeyTable.XK_Alt_R) { + this._altgrPressed = down; + } + if (this._qemuExtKeyEventSupported && scancode) { // 0 is NoSymbol keysym = keysym || 0; @@ -538,31 +546,10 @@ export default class RFB extends EventTargetMixin { Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode); RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); - } else if (Object.keys(this._scancodes).length > 0) { - let vscancode = this._scancodes[keysym] - if (vscancode) { - let shifted = vscancode.includes("shift"); - let vscancode_int = parseInt(vscancode); - let isLetter = (keysym >= 65 && keysym <=90) || (keysym >=97 && keysym <=122); - if (shifted && ! this._shiftPressed && ! isLetter) { - RFB.messages.keyEvent(this._sock, this._shiftKey, 1); - } - if (! shifted && this._shiftPressed && ! isLetter) { - RFB.messages.keyEvent(this._sock, this._shiftKey, 0); - } - RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, down, vscancode_int); - if (shifted && ! this._shiftPressed && ! isLetter) { - RFB.messages.keyEvent(this._sock, this._shiftKey, 0); - } - if (! shifted && this._shiftPressed && ! isLetter) { - RFB.messages.keyEvent(this._sock, this._shiftKey, 1); - } - } else { - if (this._language === "jp" && keysym === 65328) { - keysym = 65509; // Caps lock - } - RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); - } + } else if (Object.keys(this._scancodes).length > 0 && this._language === "jp") { + this.sendKeyWithJapaneseKeyboard(keysym, down) + } else if (Object.keys(this._scancodes).length > 0 && this._language === "es-latam") { + this.sendKeyWithSpanishLatamKeyboard(keysym, down) } else { if (!keysym) { return; @@ -572,6 +559,93 @@ export default class RFB extends EventTargetMixin { } } + sendKeyWithJapaneseKeyboard(keysym, down) { + let vscancode = this._scancodes[keysym] + if (vscancode) { + let shifted = vscancode.includes("shift"); + let vscancode_int = parseInt(vscancode); + let isLetter = (keysym >= 65 && keysym <= 90) || (keysym >= 97 && keysym <= 122); + if (shifted && !this._shiftPressed && !isLetter) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 1); + } + if (!shifted && this._shiftPressed && !isLetter) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 0); + } + RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, down, vscancode_int); + if (shifted && !this._shiftPressed && !isLetter) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 0); + } + if (!shifted && this._shiftPressed && !isLetter) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 1); + } + } else { + if (keysym === 65328) { + keysym = 65509; // Caps lock + } + RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); + } + } + + sendKeyWithSpanishLatamKeyboard(keysym, down) { + const VSCODE_ACUTE_LATAM = 26; // The ASCII code of acute is 180 + let vscancode = this._scancodes[keysym] + if (vscancode) { + let shifted = vscancode.includes("shift"); + let altgr = vscancode.includes("altgr"); + let acute = vscancode.includes("acute"); + let vscancode_int = parseInt(vscancode); + if (acute) { + let shifted_1 = vscancode.includes("shift1"); // Shift with Acute accent + let shifted_2 = vscancode.includes("shift2"); // Shift with a/e/i/o/u + if (down) { + if (shifted_1 && ! this._shiftPressed) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 1); + } else if (! shifted_1 && this._shiftPressed) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 0); + } + RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, 1, VSCODE_ACUTE_LATAM); + RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, 0, VSCODE_ACUTE_LATAM); + if (shifted_2) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 1); + } else { + RFB.messages.keyEvent(this._sock, this._shiftKey, 0); + } + } else { + RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, 0, VSCODE_ACUTE_LATAM); + if (shifted_2 && ! this._shiftPressed) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 0); + } else if (! shifted_2 && this._shiftPressed) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 1); + } + } + RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, down, vscancode_int); + return; + } + let isLetter = (keysym >= 65 && keysym <= 90) || (keysym >= 97 && keysym <= 122); + if (shifted && !this._shiftPressed && !isLetter && down) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 1); + } + if (!shifted && this._shiftPressed && !isLetter && down) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 0); + } + if (altgr && !this._altgrPressed && down) { + RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_R, 1); + } + RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, down, vscancode_int); + if (altgr && !this._altgrPressed && !down) { + RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_R, 0); + } + if (shifted && !this._shiftPressed && !isLetter && !down) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 0); + } + if (!shifted && this._shiftPressed && !isLetter && !down) { + RFB.messages.keyEvent(this._sock, this._shiftKey, 1); + } + } else { + RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); + } + } + focus(options) { this._canvas.focus(options); } diff --git a/systemvm/agent/noVNC/keymaps/generate-language-keymaps.py b/systemvm/agent/noVNC/keymaps/generate-language-keymaps.py index 4a88a05ef0d4..604018c69a28 100755 --- a/systemvm/agent/noVNC/keymaps/generate-language-keymaps.py +++ b/systemvm/agent/noVNC/keymaps/generate-language-keymaps.py @@ -2,7 +2,7 @@ # This script # (1) loads keysym name and keycode mappings from noVNC/core/input/keysym.js and -# (2) loads keysyn name to atset1 code mappings from keymap files which can be downloadeded from https://github.com/qemu/qemu/blob/master/pc-bios/keymaps +# (2) loads keysym name to atset1 code mappings from keymap files which can be downloadeded from https://github.com/qemu/qemu/blob/master/pc-bios/keymaps # (3) generates the mappings of keycode and atset1 code # # Note: please add language specific mappings if needed. @@ -96,7 +96,10 @@ def generate_js_file(keymap_file): js_config.append(" */\n") js_config.append("export default {\n") for keycode in dict(sorted(list(result_mappings.items()), key=lambda item: int(item[0]))): - js_config.append("%10s : \"%s\",\n" % ("\"" + str(keycode) + "\"", result_mappings[keycode].strip())) + if keycode not in list(keycode_to_x11name.keys()): + js_config.append("%10s : \"%s\",\n" % ("\"" + str(keycode) + "\"", result_mappings[keycode].strip())) + else: + js_config.append("%10s : \"%s\", // %s\n" % ("\"" + str(keycode) + "\"", result_mappings[keycode].strip(), keycode_to_x11name[keycode])) js_config.append("}\n") for line in js_config: handle.write(line) diff --git a/systemvm/agent/noVNC/keymaps/keymap-es-latam-atset1.js b/systemvm/agent/noVNC/keymaps/keymap-es-latam-atset1.js new file mode 100644 index 000000000000..91ef42642c41 --- /dev/null +++ b/systemvm/agent/noVNC/keymaps/keymap-es-latam-atset1.js @@ -0,0 +1,131 @@ +/* This file is auto-generated by generate-language-keymaps.py + * command : generate-language-keymaps.py keymap-es + * layout : es-latam + */ +export default { + "32" : "57", // XK_space + "33" : "2 shift", // XK_exclam + "34" : "3 shift", // XK_quotedbl + "35" : "4 shift", // XK_numbersign + "36" : "5 shift", // XK_dollar + "37" : "6 shift", // XK_percent + "38" : "7 shift", // XK_ampersand + "39" : "12", // XK_apostrophe + "40" : "9 shift", // XK_parenleft + "41" : "10 shift", // XK_parenright + "42" : "27 shift", // XK_asterisk + "43" : "27", // XK_plus + "44" : "51", // XK_comma + "45" : "53", // XK_minus + "46" : "52", // XK_period + "47" : "8 shift", // XK_slash + "48" : "11", // XK_0 + "49" : "2", // XK_1 + "50" : "3", // XK_2 + "51" : "4", // XK_3 + "52" : "5", // XK_4 + "53" : "6", // XK_5 + "54" : "7", // XK_6 + "55" : "8", // XK_7 + "56" : "9", // XK_8 + "57" : "10", // XK_9 + "58" : "52 shift", // XK_colon + "59" : "51 shift", // XK_semicolon + "60" : "86", // XK_less + "61" : "11 shift", // XK_equal + "62" : "86 shift", // XK_greater + "63" : "12 shift", // XK_question + "64" : "16 altgr", // XK_at + "65" : "30 shift", // XK_A + "66" : "48 shift", // XK_B + "67" : "46 shift", // XK_C + "68" : "32 shift", // XK_D + "69" : "18 shift", // XK_E + "70" : "33 shift", // XK_F + "71" : "34 shift", // XK_G + "72" : "35 shift", // XK_H + "73" : "23 shift", // XK_I + "74" : "36 shift", // XK_J + "75" : "37 shift", // XK_K + "76" : "38 shift", // XK_L + "77" : "50 shift", // XK_M + "78" : "49 shift", // XK_N + "79" : "24 shift", // XK_O + "80" : "25 shift", // XK_P + "81" : "16 shift", // XK_Q + "82" : "19 shift", // XK_R + "83" : "31 shift", // XK_S + "84" : "20 shift", // XK_T + "85" : "22 shift", // XK_U + "86" : "47 shift", // XK_V + "87" : "17 shift", // XK_W + "88" : "45 shift", // XK_X + "89" : "21 shift", // XK_Y + "90" : "44 shift", // XK_Z + "91" : "40 shift", // XK_bracketleft + "92" : "12 altgr", // XK_backslash + "93" : "43 shift", // XK_bracketright + "94": "40 altgr", // ^ + "95" : "53 shift", // XK_underscore + "96": "43 altgr", // ` + "97" : "30", // XK_a + "98" : "48", // XK_b + "99" : "46", // XK_c + "100" : "32", // XK_d + "101" : "18", // XK_e + "102" : "33", // XK_f + "103" : "34", // XK_g + "104" : "35", // XK_h + "105" : "23", // XK_i + "106" : "36", // XK_j + "107" : "37", // XK_k + "108" : "38", // XK_l + "109" : "50", // XK_m + "110" : "49", // XK_n + "111" : "24", // XK_o + "112" : "25", // XK_p + "113" : "16", // XK_q + "114" : "19", // XK_r + "115" : "31", // XK_s + "116" : "20", // XK_t + "117" : "22", // XK_u + "118" : "47", // XK_v + "119" : "17", // XK_w + "120" : "45", // XK_x + "121" : "21", // XK_y + "122" : "44", // XK_z + "123" : "40", // XK_braceleft + "124" : "41", // XK_bar + "125" : "43", // XK_braceright + "126" : "27 altgr", // XK_asciitilde + "161" : "13 shift", // XK_exclamdown + "168" : "26 shift", // ¨ + "171" : "44 altgr", // XK_guillemotleft + "172" : "41 altgr", // XK_notsign + "176" : "41 shift", // XK_degree + "180" : "26", // ´ + "186" : "41", // XK_masculine + "191" : "13", // XK_questiondown + "193" : "30 acute shift2", // Á + "196" : "30 shift1 acute shift2", // Ä + "201" : "18 acute shift2", // É + "203" : "18 shift1 acute shift2", // Ë + "205" : "23 acute shift2", // Í + "207" : "23 shift1 acute shift2", // Ï + "209" : "39 shift", // XK_Ntilde + "211" : "24 acute shift2", // Ó + "214" : "24 shift1 acute shift2", // Ö + "218" : "22 acute shift2", // Ú + "220" : "22 shift1 acute shift2", // Ü + "225" : "30 acute", // á + "228" : "30 shift1 acute", // ä + "233" : "18 acute", // é + "235" : "18 shift1 acute", // ë + "237" : "23 acute", // í + "239" : "23 shift1 acute", // ï + "241" : "39", // XK_ntilde + "243" : "24 acute", // ó + "246" : "24 shift1 acute", // ö + "250" : "22 acute", // ú + "252" : "22 shift1 acute", // ü +} diff --git a/ui/public/config.json b/ui/public/config.json index 1a7beda654e8..edfac010cf50 100644 --- a/ui/public/config.json +++ b/ui/public/config.json @@ -61,7 +61,8 @@ "uk": "label.uk.keyboard", "fr": "label.french.azerty.keyboard", "jp": "label.japanese.keyboard", - "sc": "label.simplified.chinese.keyboard" + "sc": "label.simplified.chinese.keyboard", + "es-latam": "Spanish Latin American Keyboard" }, "userCard": { "enabled": true, diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 4f450e940fc0..375c221a38bd 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -19,6 +19,7 @@ "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.add.setting": "Unable to add or edit setting", "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 (except NSX zones).", @@ -3386,6 +3387,7 @@ "message.error.delete.tungsten.tag": "Removing Tag failed", "message.error.description": "Please enter description.", "message.error.discovering.feature": "Exception caught while discovering features.", +"message.error.setting.deployasistemplate": "Settings are read directly from the template", "message.error.setup.2fa": "2FA setup failed while verifying the code, please retry.", "message.error.verifying.2fa": "Unable to verify 2FA, please retry.", "message.error.display.text": "Please enter display text.", diff --git a/ui/src/components/view/DetailSettings.vue b/ui/src/components/view/DetailSettings.vue index 987a8ac42136..fc3b4257311f 100644 --- a/ui/src/components/view/DetailSettings.vue +++ b/ui/src/components/view/DetailSettings.vue @@ -100,7 +100,7 @@ @@ -115,7 +115,7 @@ > @@ -213,11 +213,16 @@ export default { this.detailOptions = json.listdetailoptionsresponse.detailoptions.details }) this.disableSettings = (this.$route.meta.name === 'vm' && resource.state !== 'Stopped') - getAPI('listTemplates', { templatefilter: 'all', id: resource.templateid }).then(json => { - this.deployasistemplate = json.listtemplatesresponse.template[0].deployasis - }) + if (this.$route.meta.name === 'vm') { + getAPI('listTemplates', { templatefilter: 'all', id: resource.templateid }).then(json => { + this.deployasistemplate = json.listtemplatesresponse.template[0].deployasis + }) + } }, allowEditOfDetail (name) { + if (this.deployasistemplate) { + return this.resource.alloweddetails && this.resource.alloweddetails.split(',').map(item => item.trim()).includes(name) + } if (this.resource.readonlydetails) { if (this.resource.readonlydetails.split(',').map(item => item.trim()).includes(name)) { return false @@ -320,7 +325,11 @@ export default { return } if (!this.allowEditOfDetail(this.newKey)) { - this.error = this.$t('error.unable.to.proceed') + if (this.deployasistemplate) { + this.error = this.$t('error.unable.to.add.setting') + ' : ' + this.newKey + '. ' + this.$t('message.error.setting.deployasistemplate') + } else { + this.error = this.$t('error.unable.to.add.setting') + ' : ' + this.newKey + } return } this.error = false