Skip to content

Commit ff99081

Browse files
committed
noVNC: support Spanish Latin American keyboard
1 parent 71f47d6 commit ff99081

File tree

13 files changed

+312
-42
lines changed

13 files changed

+312
-42
lines changed

api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme
188188
@Parameter(name = ApiConstants.MAC_ADDRESS, type = CommandType.STRING, description = "the mac address for default vm's network")
189189
private String macAddress;
190190

191-
@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")
191+
@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")
192192
private String keyboard;
193193

194194
@Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "Deploy vm for the project")

api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
340340
@Param(description = "List of read-only Vm details as comma separated string.", since = "4.16.0")
341341
private String readOnlyDetails;
342342

343+
@SerializedName("alloweddetails")
344+
@Param(description = "List of allowed Vm details as comma separated string if VM instance settings are read from OVA.", since = "4.22.1")
345+
private String allowedDetails;
346+
343347
@SerializedName(ApiConstants.SSH_KEYPAIRS)
344348
@Param(description = "ssh key-pairs")
345349
private String keyPairNames;
@@ -1091,6 +1095,10 @@ public void setReadOnlyDetails(String readOnlyDetails) {
10911095
this.readOnlyDetails = readOnlyDetails;
10921096
}
10931097

1098+
public void setAllowedDetails(String allowedDetails) {
1099+
this.allowedDetails = allowedDetails;
1100+
}
1101+
10941102
public void setOsTypeId(String osTypeId) {
10951103
this.osTypeId = osTypeId;
10961104
}
@@ -1115,6 +1123,10 @@ public String getReadOnlyDetails() {
11151123
return readOnlyDetails;
11161124
}
11171125

1126+
public String getAllowedDetails() {
1127+
return allowedDetails;
1128+
}
1129+
11181130
public Boolean getDynamicallyScalable() {
11191131
return isDynamicallyScalable;
11201132
}

server/src/main/java/com/cloud/api/query/QueryManagerImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5371,7 +5371,7 @@ private void fillVMOrTemplateDetailOptions(final Map<String, List<String>> optio
53715371

53725372
options.put(ApiConstants.BootType.UEFI.toString(), Arrays.asList(ApiConstants.BootMode.LEGACY.toString(),
53735373
ApiConstants.BootMode.SECURE.toString()));
5374-
options.put(VmDetailConstants.KEYBOARD, Arrays.asList("uk", "us", "jp", "fr"));
5374+
options.put(VmDetailConstants.KEYBOARD, Arrays.asList("uk", "us", "jp", "fr", "es-latam"));
53755375
options.put(VmDetailConstants.CPU_CORE_PER_SOCKET, Collections.emptyList());
53765376
options.put(VmDetailConstants.ROOT_DISK_SIZE, Collections.emptyList());
53775377

server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,11 @@
6969
import com.cloud.storage.DiskOfferingVO;
7070
import com.cloud.storage.GuestOS;
7171
import com.cloud.storage.Storage.TemplateType;
72+
import com.cloud.storage.VMTemplateVO;
7273
import com.cloud.storage.VnfTemplateDetailVO;
7374
import com.cloud.storage.VnfTemplateNicVO;
7475
import com.cloud.storage.Volume;
76+
import com.cloud.storage.dao.VMTemplateDao;
7577
import com.cloud.storage.dao.VnfTemplateDetailsDao;
7678
import com.cloud.storage.dao.VnfTemplateNicDao;
7779
import com.cloud.user.Account;
@@ -124,6 +126,8 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
124126
private ServiceOfferingDao serviceOfferingDao;
125127
@Inject
126128
private VgpuProfileDao vgpuProfileDao;
129+
@Inject
130+
VMTemplateDao vmTemplateDao;
127131

128132
private final SearchBuilder<UserVmJoinVO> VmDetailSearch;
129133
private final SearchBuilder<UserVmJoinVO> activeVmByIsoSearch;
@@ -465,6 +469,10 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us
465469
if (caller.getType() != Account.Type.ADMIN) {
466470
userVmResponse.setReadOnlyDetails(QueryService.UserVMReadOnlyDetails.value());
467471
}
472+
VMTemplateVO template = vmTemplateDao.findByIdIncludingRemoved(userVm.getTemplateId());
473+
if (template != null && template.isDeployAsIs() && UserVmManager.VmwareAdditionalDetailsFromOvaEnabled.valueIn(userVm.getDataCenterId())) {
474+
userVmResponse.setAllowedDetails(UserVmManager.VmwareAllowedAdditionalDetailsFromOva.valueIn(userVm.getDataCenterId()));
475+
}
468476
}
469477

470478
userVmResponse.setObjectName(objectName);

server/src/main/java/com/cloud/vm/UserVmManager.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ public interface UserVmManager extends UserVmService {
9999
ConfigKey.Scope.Account);
100100

101101

102+
ConfigKey<Boolean> VmwareAdditionalDetailsFromOvaEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class,
103+
"vmware.additional.details.from.ova.enabled", "false",
104+
"If true, allow users to add additional VM settings if VM instance settings are read from OVA.", true, ConfigKey.Scope.Zone);
105+
106+
ConfigKey<String> VmwareAllowedAdditionalDetailsFromOva = new ConfigKey<>(String.class,
107+
"vmware.allowed.additional.details.from.ova", "Advanced", "",
108+
"Comma separated list of allowed additional VM settings if VM instance settings are read from OVA.",
109+
true, ConfigKey.Scope.Zone, null, null, null, null, null, ConfigKey.Kind.CSV, null);
110+
102111
static final int MAX_USER_DATA_LENGTH_BYTES = 2048;
103112

104113
public static final String CKS_NODE = "cksnode";

server/src/main/java/com/cloud/vm/UserVmManagerImpl.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2884,11 +2884,7 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx
28842884

28852885
UserVmVO vmInstance = _vmDao.findById(cmd.getId());
28862886
VMTemplateVO template = _templateDao.findById(vmInstance.getTemplateId());
2887-
if (MapUtils.isNotEmpty(details) || cmd.isCleanupDetails()) {
2888-
if (template != null && template.isDeployAsIs()) {
2889-
throw new CloudRuntimeException("Detail settings are read from OVA, it cannot be changed by API call.");
2890-
}
2891-
}
2887+
28922888
UserVmVO userVm = _vmDao.findById(cmd.getId());
28932889
if (userVm != null && UserVmManager.SHAREDFSVM.equals(userVm.getUserVmType())) {
28942890
throw new InvalidParameterValueException("Operation not supported on Shared FileSystem Instance");
@@ -2918,6 +2914,9 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx
29182914
.collect(Collectors.toList());
29192915
List<VMInstanceDetailVO> existingDetails = vmInstanceDetailsDao.listDetails(id);
29202916
if (cleanupDetails){
2917+
if (template != null && template.isDeployAsIs()) {
2918+
throw new InvalidParameterValueException("Detail settings are read from OVA, it cannot be cleaned up by API call.");
2919+
}
29212920
if (caller != null && caller.getType() == Account.Type.ADMIN) {
29222921
for (final VMInstanceDetailVO detail : existingDetails) {
29232922
if (detail != null && detail.isDisplay() && !isExtraConfig(detail.getName())) {
@@ -2946,6 +2945,23 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx
29462945
throw new InvalidParameterValueException("'extraconfig' should not be included in details as key");
29472946
}
29482947

2948+
if (template != null && template.isDeployAsIs()) {
2949+
final List<String> vmwareAllowedDetailsFromOva = VmwareAdditionalDetailsFromOvaEnabled.valueIn(vmInstance.getDataCenterId()) ?
2950+
Stream.of(VmwareAllowedAdditionalDetailsFromOva.valueIn(vmInstance.getDataCenterId()).split(","))
2951+
.map(String::trim)
2952+
.collect(Collectors.toList()) : List.of();
2953+
for (String detailKey : details.keySet()) {
2954+
if (vmwareAllowedDetailsFromOva.contains(detailKey)) {
2955+
continue;
2956+
}
2957+
UserVmDetailVO detailVO = existingDetails.stream().filter(d -> Objects.equals(d.getName(), detailKey)).findFirst().orElse(null);
2958+
if (detailVO != null && ObjectUtils.allNotNull(detailVO.getValue(), details.get(detailKey)) && detailVO.getValue().equals(details.get(detailKey))) {
2959+
continue;
2960+
}
2961+
throw new InvalidParameterValueException("Detail settings are read from OVA, it cannot be changed by API call.");
2962+
}
2963+
}
2964+
29492965
details.entrySet().removeIf(detail -> isExtraConfig(detail.getKey()));
29502966

29512967
if (caller != null && caller.getType() != Account.Type.ADMIN) {
@@ -9336,7 +9352,8 @@ public ConfigKey<?>[] getConfigKeys() {
93369352
return new ConfigKey<?>[] {EnableDynamicallyScaleVm, AllowDiskOfferingChangeDuringScaleVm, AllowUserExpungeRecoverVm, VmIpFetchWaitInterval, VmIpFetchTrialMax,
93379353
VmIpFetchThreadPoolMax, VmIpFetchTaskWorkers, AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig, DisplayVMOVFProperties,
93389354
KvmAdditionalConfigAllowList, XenServerAdditionalConfigAllowList, VmwareAdditionalConfigAllowList, DestroyRootVolumeOnVmDestruction,
9339-
EnforceStrictResourceLimitHostTagCheck, StrictHostTags, AllowUserForceStopVm, VmDistinctHostNameScope};
9355+
EnforceStrictResourceLimitHostTagCheck, StrictHostTags, AllowUserForceStopVm, VmDistinctHostNameScope,
9356+
VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva};
93409357
}
93419358

93429359
@Override

server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Arrays;
2323
import java.util.EnumSet;
2424

25+
import com.cloud.storage.dao.VMTemplateDao;
2526
import org.apache.cloudstack.annotation.dao.AnnotationDao;
2627
import org.apache.cloudstack.api.ApiConstants;
2728
import org.apache.cloudstack.api.ResponseObject;
@@ -78,6 +79,9 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT
7879
@Mock
7980
private VnfTemplateDetailsDao vnfTemplateDetailsDao;
8081

82+
@Mock
83+
private VMTemplateDao vmTemplateDao;
84+
8185
private UserVmJoinVO userVm = new UserVmJoinVO();
8286
private UserVmResponse userVmResponse = new UserVmResponse();
8387

systemvm/agent/noVNC/core/rfb.js

Lines changed: 99 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import ZRLEDecoder from "./decoders/zrle.js";
3939
import JPEGDecoder from "./decoders/jpeg.js";
4040
import H264Decoder from "./decoders/h264.js";
4141
import SCANCODES_JP from "../keymaps/keymap-ja-atset1.js"
42+
import SCANCODES_ES_LATAM from "../keymaps/keymap-es-latam-atset1.js"
4243

4344
// How many seconds to wait for a disconnect to finish
4445
const DISCONNECT_TIMEOUT = 3;
@@ -127,6 +128,8 @@ export default class RFB extends EventTargetMixin {
127128
this._scancodes = {};
128129
if (this._language === "jp") {
129130
this._scancodes = SCANCODES_JP;
131+
} else if (this._language === "es-latam") {
132+
this._scancodes = SCANCODES_ES_LATAM;
130133
}
131134

132135
// Internal state
@@ -197,6 +200,7 @@ export default class RFB extends EventTargetMixin {
197200
// Keys
198201
this._shiftPressed = false;
199202
this._shiftKey = KeyTable.XK_Shift_L;
203+
this._altgrPressed = false;
200204

201205
// Mouse state
202206
this._mousePos = {};
@@ -531,38 +535,21 @@ export default class RFB extends EventTargetMixin {
531535
this._shiftKey = down ? keysym : KeyTable.XK_Shift_L;
532536
}
533537

538+
if (keysym === KeyTable.XK_Alt_R) {
539+
this._altgrPressed = down;
540+
}
541+
534542
if (this._qemuExtKeyEventSupported && scancode) {
535543
// 0 is NoSymbol
536544
keysym = keysym || 0;
537545

538546
Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
539547

540548
RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
541-
} else if (Object.keys(this._scancodes).length > 0) {
542-
let vscancode = this._scancodes[keysym]
543-
if (vscancode) {
544-
let shifted = vscancode.includes("shift");
545-
let vscancode_int = parseInt(vscancode);
546-
let isLetter = (keysym >= 65 && keysym <=90) || (keysym >=97 && keysym <=122);
547-
if (shifted && ! this._shiftPressed && ! isLetter) {
548-
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
549-
}
550-
if (! shifted && this._shiftPressed && ! isLetter) {
551-
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
552-
}
553-
RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, down, vscancode_int);
554-
if (shifted && ! this._shiftPressed && ! isLetter) {
555-
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
556-
}
557-
if (! shifted && this._shiftPressed && ! isLetter) {
558-
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
559-
}
560-
} else {
561-
if (this._language === "jp" && keysym === 65328) {
562-
keysym = 65509; // Caps lock
563-
}
564-
RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
565-
}
549+
} else if (Object.keys(this._scancodes).length > 0 && this._language === "jp") {
550+
this.sendKeyWithJapaneseKeyboard(keysym, down)
551+
} else if (Object.keys(this._scancodes).length > 0 && this._language === "es-latam") {
552+
this.sendKeyWithSpanishLatamKeyboard(keysym, down)
566553
} else {
567554
if (!keysym) {
568555
return;
@@ -572,6 +559,93 @@ export default class RFB extends EventTargetMixin {
572559
}
573560
}
574561

562+
sendKeyWithJapaneseKeyboard(keysym, down) {
563+
let vscancode = this._scancodes[keysym]
564+
if (vscancode) {
565+
let shifted = vscancode.includes("shift");
566+
let vscancode_int = parseInt(vscancode);
567+
let isLetter = (keysym >= 65 && keysym <= 90) || (keysym >= 97 && keysym <= 122);
568+
if (shifted && !this._shiftPressed && !isLetter) {
569+
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
570+
}
571+
if (!shifted && this._shiftPressed && !isLetter) {
572+
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
573+
}
574+
RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, down, vscancode_int);
575+
if (shifted && !this._shiftPressed && !isLetter) {
576+
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
577+
}
578+
if (!shifted && this._shiftPressed && !isLetter) {
579+
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
580+
}
581+
} else {
582+
if (keysym === 65328) {
583+
keysym = 65509; // Caps lock
584+
}
585+
RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
586+
}
587+
}
588+
589+
sendKeyWithSpanishLatamKeyboard(keysym, down) {
590+
const VSCODE_ACUTE_LATAM = 26; // The ASCII code of acute is 180
591+
let vscancode = this._scancodes[keysym]
592+
if (vscancode) {
593+
let shifted = vscancode.includes("shift");
594+
let altgr = vscancode.includes("altgr");
595+
let acute = vscancode.includes("acute");
596+
let vscancode_int = parseInt(vscancode);
597+
if (acute) {
598+
let shifted_1 = vscancode.includes("shift1"); // Shift with Acute accent
599+
let shifted_2 = vscancode.includes("shift2"); // Shift with a/e/i/o/u
600+
if (down) {
601+
if (shifted_1 && ! this._shiftPressed) {
602+
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
603+
} else if (! shifted_1 && this._shiftPressed) {
604+
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
605+
}
606+
RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, 1, VSCODE_ACUTE_LATAM);
607+
RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, 0, VSCODE_ACUTE_LATAM);
608+
if (shifted_2) {
609+
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
610+
} else {
611+
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
612+
}
613+
} else {
614+
RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, 0, VSCODE_ACUTE_LATAM);
615+
if (shifted_2 && ! this._shiftPressed) {
616+
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
617+
} else if (! shifted_2 && this._shiftPressed) {
618+
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
619+
}
620+
}
621+
RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, down, vscancode_int);
622+
return;
623+
}
624+
let isLetter = (keysym >= 65 && keysym <= 90) || (keysym >= 97 && keysym <= 122);
625+
if (shifted && !this._shiftPressed && !isLetter && down) {
626+
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
627+
}
628+
if (!shifted && this._shiftPressed && !isLetter && down) {
629+
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
630+
}
631+
if (altgr && !this._altgrPressed && down) {
632+
RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_R, 1);
633+
}
634+
RFB.messages.VMwareExtendedKeyEvent(this._sock, keysym, down, vscancode_int);
635+
if (altgr && !this._altgrPressed && !down) {
636+
RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_R, 0);
637+
}
638+
if (shifted && !this._shiftPressed && !isLetter && !down) {
639+
RFB.messages.keyEvent(this._sock, this._shiftKey, 0);
640+
}
641+
if (!shifted && this._shiftPressed && !isLetter && !down) {
642+
RFB.messages.keyEvent(this._sock, this._shiftKey, 1);
643+
}
644+
} else {
645+
RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
646+
}
647+
}
648+
575649
focus(options) {
576650
this._canvas.focus(options);
577651
}

systemvm/agent/noVNC/keymaps/generate-language-keymaps.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# This script
44
# (1) loads keysym name and keycode mappings from noVNC/core/input/keysym.js and
5-
# (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
5+
# (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
66
# (3) generates the mappings of keycode and atset1 code
77
#
88
# Note: please add language specific mappings if needed.
@@ -96,7 +96,10 @@ def generate_js_file(keymap_file):
9696
js_config.append(" */\n")
9797
js_config.append("export default {\n")
9898
for keycode in dict(sorted(list(result_mappings.items()), key=lambda item: int(item[0]))):
99-
js_config.append("%10s : \"%s\",\n" % ("\"" + str(keycode) + "\"", result_mappings[keycode].strip()))
99+
if keycode not in list(keycode_to_x11name.keys()):
100+
js_config.append("%10s : \"%s\",\n" % ("\"" + str(keycode) + "\"", result_mappings[keycode].strip()))
101+
else:
102+
js_config.append("%10s : \"%s\", // %s\n" % ("\"" + str(keycode) + "\"", result_mappings[keycode].strip(), keycode_to_x11name[keycode]))
100103
js_config.append("}\n")
101104
for line in js_config:
102105
handle.write(line)

0 commit comments

Comments
 (0)