Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -1115,6 +1123,10 @@ public String getReadOnlyDetails() {
return readOnlyDetails;
}

public String getAllowedDetails() {
return allowedDetails;
}

public Boolean getDynamicallyScalable() {
return isDynamicallyScalable;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5371,7 +5371,7 @@ private void fillVMOrTemplateDetailOptions(final Map<String, List<String>> 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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -124,6 +126,8 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
private ServiceOfferingDao serviceOfferingDao;
@Inject
private VgpuProfileDao vgpuProfileDao;
@Inject
VMTemplateDao vmTemplateDao;

private final SearchBuilder<UserVmJoinVO> VmDetailSearch;
private final SearchBuilder<UserVmJoinVO> activeVmByIsoSearch;
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions server/src/main/java/com/cloud/vm/UserVmManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ public interface UserVmManager extends UserVmService {
ConfigKey.Scope.Account);


ConfigKey<Boolean> VmwareAdditionalDetailsFromOvaEnabled = new ConfigKey<Boolean>("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<String> 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";
Expand Down
29 changes: 23 additions & 6 deletions server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -2918,6 +2914,9 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx
.collect(Collectors.toList());
List<VMInstanceDetailVO> 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())) {
Expand Down Expand Up @@ -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<String> 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) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down
124 changes: 99 additions & 25 deletions systemvm/agent/noVNC/core/rfb.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {};
Expand Down Expand Up @@ -531,38 +535,21 @@ 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;

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;
Expand All @@ -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);
}
Expand Down
7 changes: 5 additions & 2 deletions systemvm/agent/noVNC/keymaps/generate-language-keymaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading