From b869913529ec8e230abcb5d6b0924a36a7cdc7e3 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 2 Feb 2026 10:46:54 +0100 Subject: [PATCH 1/5] noVNC: support Spanish Latin American keyboard on VMware (#12484) * noVNC: support Spanish Latin American keyboard * Update server/src/main/java/com/cloud/vm/UserVmManagerImpl.java --- .../api/command/user/vm/BaseDeployVMCmd.java | 2 +- .../api/response/UserVmResponse.java | 12 ++ .../com/cloud/api/query/QueryManagerImpl.java | 2 +- .../api/query/dao/UserVmJoinDaoImpl.java | 8 ++ .../main/java/com/cloud/vm/UserVmManager.java | 9 ++ .../java/com/cloud/vm/UserVmManagerImpl.java | 29 +++- .../api/query/dao/UserVmJoinDaoImplTest.java | 4 + systemvm/agent/noVNC/core/rfb.js | 124 +++++++++++++---- .../keymaps/generate-language-keymaps.py | 7 +- .../noVNC/keymaps/keymap-es-latam-atset1.js | 131 ++++++++++++++++++ ui/public/config.json | 3 +- ui/public/locales/en.json | 2 + ui/src/components/view/DetailSettings.vue | 21 ++- 13 files changed, 312 insertions(+), 42 deletions(-) create mode 100644 systemvm/agent/noVNC/keymaps/keymap-es-latam-atset1.js 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 ecbde47692f..e71b4feea03 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 745c0ba4683..a7f6dff96f8 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 Instance 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 class UserVmResponse extends BaseResponseWithTagInformation implements Co this.readOnlyDetails = readOnlyDetails; } + public void setAllowedDetails(String allowedDetails) { + this.allowedDetails = allowedDetails; + } + public void setOsTypeId(String osTypeId) { this.osTypeId = osTypeId; } @@ -1115,6 +1123,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co 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 d42dbaec6de..ac9f8ee1433 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -5379,7 +5379,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q 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 a2f9544de39..93dca8cc07a 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.service.ServiceOfferingDetailsVO; 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 class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation 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 eccea944fe6..f36c851e5bb 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -2886,11 +2886,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir 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"); @@ -2920,6 +2916,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir .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())) { @@ -2948,6 +2947,23 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir 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) { @@ -9342,7 +9358,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir 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 14074add021..34e5e48cc32 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 static org.mockito.MockitoAnnotations.openMocks; 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 8faa993d425..59218b136b9 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 4a88a05ef0d..604018c69a2 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 00000000000..91ef42642c4 --- /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 de3a3c39952..a067803ba7a 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 0a9539abff3..300b8b3f9f0 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).", @@ -3401,6 +3402,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 987a8ac4213..fc3b4257311 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 From ce42ce54c2d966bf5814afb7e0e4a1b973686ffa Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Tue, 3 Feb 2026 14:21:43 +0530 Subject: [PATCH 2/5] Fix template details deletion while updating template from UI (#12559) * Fix template details deletion while updating template from UI * update the latest template details before submit --- .../api/BaseUpdateTemplateOrIsoCmd.java | 4 +- .../cloud/storage/dao/VMTemplateDaoImpl.java | 4 +- .../cloud/template/TemplateManagerImpl.java | 5 +- ui/src/views/image/UpdateTemplate.vue | 47 ++++++++++++++----- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java index 696a500860e..54a398d756f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java @@ -145,8 +145,8 @@ public abstract class BaseUpdateTemplateOrIsoCmd extends BaseCmd { return (Map) (paramsCollection.toArray())[0]; } - public boolean isCleanupDetails(){ - return cleanupDetails == null ? false : cleanupDetails.booleanValue(); + public boolean isCleanupDetails() { + return cleanupDetails != null && cleanupDetails; } public CPU.CPUArch getCPUArch() { diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java index 98859789f82..45dffea7621 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java @@ -459,7 +459,7 @@ public class VMTemplateDaoImpl extends GenericDaoBase implem if (detailsStr == null) { return; } - List details = new ArrayList(); + List details = new ArrayList<>(); for (String key : detailsStr.keySet()) { VMTemplateDetailVO detail = new VMTemplateDetailVO(tmpl.getId(), key, detailsStr.get(key), true); details.add(detail); @@ -481,7 +481,7 @@ public class VMTemplateDaoImpl extends GenericDaoBase implem } if (tmplt.getDetails() != null) { - List details = new ArrayList(); + List details = new ArrayList<>(); for (String key : tmplt.getDetails().keySet()) { details.add(new VMTemplateDetailVO(tmplt.getId(), key, tmplt.getDetails().get(key), true)); } diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 78265021c0a..194d97349f0 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -2210,7 +2210,7 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, templateType == null && templateTag == null && arch == null && - (! cleanupDetails && details == null) //update details in every case except this one + (!cleanupDetails && details == null) //update details in every case except this one ); if (!updateNeeded) { return template; @@ -2308,8 +2308,7 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, if (cleanupDetails) { template.setDetails(null); _tmpltDetailsDao.removeDetails(id); - } - else if (details != null && !details.isEmpty()) { + } else if (details != null && !details.isEmpty()) { template.setDetails(details); _tmpltDao.saveDetails(template); } diff --git a/ui/src/views/image/UpdateTemplate.vue b/ui/src/views/image/UpdateTemplate.vue index 7db402cdd5b..590e8c233c8 100644 --- a/ui/src/views/image/UpdateTemplate.vue +++ b/ui/src/views/image/UpdateTemplate.vue @@ -245,7 +245,9 @@ export default { userdataid: null, userdatapolicy: null, userdatapolicylist: {}, - architectureTypes: {} + architectureTypes: {}, + detailsFields: [], + details: {} } }, beforeCreate () { @@ -295,17 +297,10 @@ export default { } } } - const resourceDetailsFields = [] if (this.resource.hypervisor === 'KVM') { - resourceDetailsFields.push('rootDiskController') + this.detailsFields.push('rootDiskController') } else if (this.resource.hypervisor === 'VMware' && !this.resource.deployasis) { - resourceDetailsFields.push(...['rootDiskController', 'nicAdapter', 'keyboard']) - } - for (var detailsField of resourceDetailsFields) { - var detailValue = this.resource?.details?.[detailsField] || null - if (detailValue) { - this.form[detailValue] = fieldValue - } + this.detailsFields.push(...['rootDiskController', 'nicAdapter', 'keyboard']) } }, fetchData () { @@ -316,6 +311,7 @@ export default { this.fetchKeyboardTypes() this.fetchUserdata() this.fetchUserdataPolicy() + this.fetchDetails() }, isValidValueForKey (obj, key) { if (this.emptyAllowedFields.includes(key) && obj[key] === '') { @@ -360,6 +356,10 @@ export default { id: 'virtio', description: 'virtio' }) + controller.push({ + id: 'virtio-blk', + description: 'virtio-blk' + }) } else if (hyperVisor === 'VMware') { controller.push({ id: '', @@ -486,6 +486,25 @@ export default { this.userdata.loading = false }) }, + fetchDetails () { + const params = {} + params.id = this.resource.id + params.templatefilter = 'all' + + api('listTemplates', params).then(response => { + if (response?.listtemplatesresponse?.template?.length > 0) { + this.details = response.listtemplatesresponse.template[0].details + if (this.details) { + for (var detailsField of this.detailsFields) { + var detailValue = this.details?.[detailsField] || null + if (detailValue) { + this.form[detailsField] = detailValue + } + } + } + } + }) + }, handleSubmit (e) { e.preventDefault() if (this.loading) return @@ -495,10 +514,14 @@ export default { const params = { id: this.resource.id } - const detailsField = ['rootDiskController', 'nicAdapter', 'keyboard'] + if (this.details) { + Object.keys(this.details).forEach((detail, index) => { + params['details[0].' + detail] = this.details[detail] + }) + } for (const key in values) { if (!this.isValidValueForKey(values, key)) continue - if (detailsField.includes(key)) { + if (this.detailsFields.includes(key)) { params['details[0].' + key] = values[key] continue } From 9ae696d1c82efb6ade3a365600f909bba2660197 Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:45:09 +0530 Subject: [PATCH 3/5] Preserve VM settings on Instance Snapshot revert for Custom Service Offering (#12555) --- .../vm/snapshot/VMSnapshotManagerImpl.java | 23 ++++++---- .../vm/snapshot/VMSnapshotManagerTest.java | 44 +++++++++++++------ 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java index 43270e6a029..cdbb7119e9e 100644 --- a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java @@ -23,6 +23,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import javax.naming.ConfigurationException; @@ -182,6 +183,11 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme Long.class, "vm.job.check.interval", "3000", "Interval in milliseconds to check if the job is complete", false); + private static final Set VM_SNAPSHOT_CUSTOM_SERVICE_OFFERING_DETAILS = Set.of( + VmDetailConstants.CPU_NUMBER.toLowerCase(), + VmDetailConstants.CPU_SPEED.toLowerCase(), + VmDetailConstants.MEMORY.toLowerCase()); + @Override public boolean configure(String name, Map params) throws ConfigurationException { _name = name; @@ -473,7 +479,8 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme } /** - * Add entries on vm_snapshot_details if service offering is dynamic. This will allow setting details when revert to vm snapshot + * Add entries about cpu, cpu_speed and memory in vm_snapshot_details if service offering is dynamic. + * This will allow setting details when revert to vm snapshot. * @param vmId vm id * @param serviceOfferingId service offering id * @param vmSnapshotId vm snapshot id @@ -484,7 +491,7 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme List vmDetails = _userVmDetailsDao.listDetails(vmId); List vmSnapshotDetails = new ArrayList(); for (UserVmDetailVO detail : vmDetails) { - if(detail.getName().equalsIgnoreCase(VmDetailConstants.CPU_NUMBER) || detail.getName().equalsIgnoreCase(VmDetailConstants.CPU_SPEED) || detail.getName().equalsIgnoreCase(VmDetailConstants.MEMORY)) { + if (VM_SNAPSHOT_CUSTOM_SERVICE_OFFERING_DETAILS.contains(detail.getName().toLowerCase())) { vmSnapshotDetails.add(new VMSnapshotDetailsVO(vmSnapshotId, detail.getName(), detail.getValue(), detail.isDisplay())); } } @@ -931,7 +938,7 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) throws CloudRuntimeException { - revertUserVmDetailsFromVmSnapshot(userVm, vmSnapshotVo); + revertCustomServiceOfferingDetailsFromVmSnapshot(userVm, vmSnapshotVo); updateUserVmServiceOffering(userVm, vmSnapshotVo); } }); @@ -943,19 +950,19 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme } /** - * Update or add user vm details from vm snapshot for vms with custom service offerings + * Update or add user vm details (cpu, cpu_speed and memory) from vm snapshot for vms with custom service offerings * @param userVm user vm * @param vmSnapshotVo vm snapshot */ - protected void revertUserVmDetailsFromVmSnapshot(UserVmVO userVm, VMSnapshotVO vmSnapshotVo) { + protected void revertCustomServiceOfferingDetailsFromVmSnapshot(UserVmVO userVm, VMSnapshotVO vmSnapshotVo) { ServiceOfferingVO serviceOfferingVO = _serviceOfferingDao.findById(vmSnapshotVo.getServiceOfferingId()); if (serviceOfferingVO.isDynamic()) { List vmSnapshotDetails = _vmSnapshotDetailsDao.listDetails(vmSnapshotVo.getId()); - List userVmDetails = new ArrayList(); for (VMSnapshotDetailsVO detail : vmSnapshotDetails) { - userVmDetails.add(new UserVmDetailVO(userVm.getId(), detail.getName(), detail.getValue(), detail.isDisplay())); + if (VM_SNAPSHOT_CUSTOM_SERVICE_OFFERING_DETAILS.contains(detail.getName().toLowerCase())) { + _userVmDetailsDao.addDetail(userVm.getId(), detail.getName(), detail.getValue(), detail.isDisplay()); + } } - _userVmDetailsDao.saveDetails(userVmDetails); } } diff --git a/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java b/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java index 06c917a1244..8d48fc4dac5 100644 --- a/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java +++ b/server/src/test/java/com/cloud/vm/snapshot/VMSnapshotManagerTest.java @@ -51,6 +51,7 @@ import com.cloud.vm.UserVmManager; import com.cloud.vm.UserVmVO; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; @@ -67,7 +68,6 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; @@ -79,13 +79,18 @@ import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -225,13 +230,13 @@ public class VMSnapshotManagerTest { when(_serviceOfferingDao.findById(SERVICE_OFFERING_ID)).thenReturn(serviceOffering); for (ResourceDetail detail : Arrays.asList(userVmDetailCpuNumber, vmSnapshotDetailCpuNumber)) { - when(detail.getName()).thenReturn("cpuNumber"); + when(detail.getName()).thenReturn(VmDetailConstants.CPU_NUMBER); when(detail.getValue()).thenReturn("2"); when(detail.isDisplay()).thenReturn(true); } for (ResourceDetail detail : Arrays.asList(userVmDetailMemory, vmSnapshotDetailMemory)) { - when(detail.getName()).thenReturn("memory"); + when(detail.getName()).thenReturn(VmDetailConstants.MEMORY); when(detail.getValue()).thenReturn("2048"); when(detail.isDisplay()).thenReturn(true); } @@ -348,12 +353,12 @@ public class VMSnapshotManagerTest { @Test public void testUpdateUserVmServiceOfferingDifferentServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { when(userVm.getServiceOfferingId()).thenReturn(SERVICE_OFFERING_DIFFERENT_ID); - when(_userVmManager.upgradeVirtualMachine(ArgumentMatchers.eq(TEST_VM_ID), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); + when(_userVmManager.upgradeVirtualMachine(eq(TEST_VM_ID), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); _vmSnapshotMgr.updateUserVmServiceOffering(userVm, vmSnapshotVO); verify(_vmSnapshotMgr).changeUserVmServiceOffering(userVm, vmSnapshotVO); verify(_vmSnapshotMgr).getVmMapDetails(userVm); - verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(ArgumentMatchers.eq(userVm), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); + verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(eq(userVm), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); } @Test @@ -368,18 +373,18 @@ public class VMSnapshotManagerTest { @Test public void testChangeUserVmServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { - when(_userVmManager.upgradeVirtualMachine(ArgumentMatchers.eq(TEST_VM_ID), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); + when(_userVmManager.upgradeVirtualMachine(eq(TEST_VM_ID), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); _vmSnapshotMgr.changeUserVmServiceOffering(userVm, vmSnapshotVO); verify(_vmSnapshotMgr).getVmMapDetails(userVm); - verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(ArgumentMatchers.eq(userVm), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); + verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(eq(userVm), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); } @Test(expected=CloudRuntimeException.class) public void testChangeUserVmServiceOfferingFailOnUpgradeVMServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { - when(_userVmManager.upgradeVirtualMachine(ArgumentMatchers.eq(TEST_VM_ID), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(false); + when(_userVmManager.upgradeVirtualMachine(eq(TEST_VM_ID), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(false); _vmSnapshotMgr.changeUserVmServiceOffering(userVm, vmSnapshotVO); verify(_vmSnapshotMgr).getVmMapDetails(userVm); - verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(ArgumentMatchers.eq(userVm), ArgumentMatchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); + verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(eq(userVm), eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); } @Test @@ -396,16 +401,27 @@ public class VMSnapshotManagerTest { @Test public void testRevertUserVmDetailsFromVmSnapshotNotDynamicServiceOffering() { - _vmSnapshotMgr.revertUserVmDetailsFromVmSnapshot(vmMock, vmSnapshotVO); + _vmSnapshotMgr.revertCustomServiceOfferingDetailsFromVmSnapshot(vmMock, vmSnapshotVO); verify(_vmSnapshotDetailsDao, never()).listDetails(anyLong()); } @Test public void testRevertUserVmDetailsFromVmSnapshotDynamicServiceOffering() { when(serviceOffering.isDynamic()).thenReturn(true); - _vmSnapshotMgr.revertUserVmDetailsFromVmSnapshot(vmMock, vmSnapshotVO); - verify(_vmSnapshotDetailsDao).listDetails(VM_SNAPSHOT_ID); - verify(_userVmDetailsDao).saveDetails(listUserVmDetailsCaptor.capture()); - } + VMSnapshotDetailsVO uefiSnapshotDetail = new VMSnapshotDetailsVO(VM_SNAPSHOT_ID, "UEFI", "SECURE", true); + List snapshotDetailsWithUefi = Arrays.asList( + vmSnapshotDetailCpuNumber, vmSnapshotDetailMemory, uefiSnapshotDetail); + when(_vmSnapshotDetailsDao.listDetails(VM_SNAPSHOT_ID)).thenReturn(snapshotDetailsWithUefi); + _vmSnapshotMgr.revertCustomServiceOfferingDetailsFromVmSnapshot(vmMock, vmSnapshotVO); + + verify(_vmSnapshotDetailsDao).listDetails(VM_SNAPSHOT_ID); + verify(_userVmDetailsDao, never()).saveDetails(any()); + ArgumentCaptor detailNameCaptor = ArgumentCaptor.forClass(String.class); + verify(_userVmDetailsDao, times(2)).addDetail(eq(TEST_VM_ID), detailNameCaptor.capture(), anyString(), anyBoolean()); + List appliedNames = detailNameCaptor.getAllValues(); + assertTrue(appliedNames.contains(VmDetailConstants.CPU_NUMBER)); + assertTrue(appliedNames.contains(VmDetailConstants.MEMORY)); + assertFalse("UEFI must not be applied from snapshot so that existing UEFI setting is preserved", appliedNames.contains("UEFI")); + } } From 349eea501abef9f0bced6e1f2023b77f56363a1d Mon Sep 17 00:00:00 2001 From: dahn Date: Thu, 5 Feb 2026 09:41:41 +0100 Subject: [PATCH 4/5] Mvn updates (#12575) --- pom.xml | 8 +-- .../src/main/resources/cloud-pmd.xml | 53 +++++++++---------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/pom.xml b/pom.xml index 6985108302d..e57d6dfc46a 100644 --- a/pom.xml +++ b/pom.xml @@ -63,20 +63,20 @@ 1.8 3.0.0 - 3.1.0 + 3.6.0 0.8.11 3.8.1 - 3.1.1 + 3.9.0 2.22.2 3.1.12 3.1.12.2 3.2.0 - 3.12.0 + 3.28.0 3.0.0 7.4.4 2.5.3 3.1.0 - 3.8.2 + 3.21.0 2.22.2 4.4.1 3.2.0 diff --git a/tools/checkstyle/src/main/resources/cloud-pmd.xml b/tools/checkstyle/src/main/resources/cloud-pmd.xml index 66a4ec08294..78d394ea15e 100644 --- a/tools/checkstyle/src/main/resources/cloud-pmd.xml +++ b/tools/checkstyle/src/main/resources/cloud-pmd.xml @@ -19,11 +19,8 @@ under the License. --> - + Ruleset that brings all the rulesets we want from the pmd jar, because @@ -31,16 +28,16 @@ to add our own future rulesets, if any. - - - - - + + + + + - + @@ -50,35 +47,35 @@ - + - + - - - - - - - + + + + + + + - + - - - - - - - + + + + + + + From 3d7d412d5bedf77c8994553562a1ba2c0c65cfbe Mon Sep 17 00:00:00 2001 From: Imvedansh Date: Thu, 5 Feb 2026 16:22:11 +0530 Subject: [PATCH 5/5] UI: Add comprehensive domain deletion confirmation dialog (Feature Request #11497) (#12380) --- ui/public/locales/en.json | 4 + .../components/view/DomainDeleteConfirm.vue | 155 ++++++++++++++++++ ui/src/views/iam/DomainView.vue | 56 ++++++- 3 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 ui/src/components/view/DomainDeleteConfirm.vue diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 99873820d53..673f6da0ad1 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -935,6 +935,7 @@ "label.endpoint": "Endpoint", "label.endport": "End port", "label.enter.account.name": "Enter the account name", +"label.enter.domain.name": "Enter the domain name", "label.enter.code": "Enter 2FA code to verify", "label.enter.static.pin": "Enter static PIN to verify", "label.enter.token": "Enter token", @@ -2973,6 +2974,9 @@ "message.delete.account.processing": "Deleting account", "message.delete.account.success": "Successfully deleted account", "message.delete.account.warning": "Deleting this account will delete all of the instances, volumes and snapshots associated with the account.", +"message.delete.domain.confirm": "Please confirm that you want to delete this domain by entering the name of the domain below.", +"message.delete.domain.warning": "All associated accounts, users, VMs, and sub-domains will be permanently deleted. This action cannot be undone.", +"message.delete.domain.failed": "Delete domain failed", "message.delete.acl.processing": "Removing ACL rule...", "message.delete.acl.rule": "Remove ACL rule", "message.delete.acl.rule.failed": "Failed to remove ACL rule.", diff --git a/ui/src/components/view/DomainDeleteConfirm.vue b/ui/src/components/view/DomainDeleteConfirm.vue new file mode 100644 index 00000000000..4fdb9b658ad --- /dev/null +++ b/ui/src/components/view/DomainDeleteConfirm.vue @@ -0,0 +1,155 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + + + + diff --git a/ui/src/views/iam/DomainView.vue b/ui/src/views/iam/DomainView.vue index ed875151a13..2b75d836b13 100644 --- a/ui/src/views/iam/DomainView.vue +++ b/ui/src/views/iam/DomainView.vue @@ -74,6 +74,11 @@ :resource="resource" :action="action"/> + @@ -87,6 +92,7 @@ import ActionButton from '@/components/view/ActionButton' import TreeView from '@/components/view/TreeView' import DomainActionForm from '@/views/iam/DomainActionForm' import ResourceView from '@/components/view/ResourceView' +import DomainDeleteConfirm from '@/components/view/DomainDeleteConfirm' import eventBus from '@/config/eventBus' export default { @@ -96,7 +102,8 @@ export default { ActionButton, TreeView, DomainActionForm, - ResourceView + ResourceView, + DomainDeleteConfirm }, mixins: [mixinDevice], data () { @@ -111,7 +118,9 @@ export default { action: {}, dataView: false, domainStore: {}, - treeDeletedKey: null + treeDeletedKey: null, + showDeleteConfirm: false, + deleteDomainResource: null } }, computed: { @@ -205,7 +214,12 @@ export default { }) }, execAction (action) { - this.treeDeletedKey = action.api === 'deleteDomain' ? this.resource.key : null + if (action.api === 'deleteDomain') { + this.deleteDomainResource = this.resource + this.showDeleteConfirm = true + return + } + this.treeDeletedKey = null this.actionData = [] this.action = action this.action.params = store.getters.apis[this.action.api].params @@ -319,6 +333,42 @@ export default { closeAction () { this.showAction = false }, + confirmDeleteDomain () { + const domain = this.deleteDomainResource + const params = { id: domain.id, cleanup: true } + + api('deleteDomain', params).then(json => { + const jobId = json.deletedomainresponse.jobid + + this.$pollJob({ + jobId, + title: this.$t('label.action.delete.domain'), + description: domain.name, + loadingMessage: `${this.$t('label.action.delete.domain')} ${domain.name}`, + successMessage: `${this.$t('label.action.delete.domain')} ${domain.name}`, + catchMessage: this.$t('error.fetching.async.job.result'), + successMethod: () => { + this.$router.replace({ path: '/domain' }) + this.resource = {} + this.treeSelected = {} + this.treeDeletedKey = null + this.treeViewKey += 1 + this.$nextTick(() => { + this.fetchData() + }) + } + }) + }).catch(error => { + this.$notification.error({ + message: this.$t('message.request.failed'), + description: error.response?.headers['x-description'] || this.$t('message.request.failed') + }) + }).finally(() => { + this.showDeleteConfirm = false + this.deleteDomainResource = null + this.treeDeletedKey = null + }) + }, forceRerender () { this.treeViewKey += 1 }