vmware: Add support for VMware 7 (#4300)

This commit is contained in:
Rohit Yadav 2021-04-15 16:10:14 +05:30 committed by GitHub
parent 8fec222af0
commit 0302750aac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 506 additions and 39 deletions

View File

@ -0,0 +1,34 @@
//
// 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.
//
package com.cloud.agent.api;
public class GetVmVncTicketAnswer extends Answer {
private String ticket;
public GetVmVncTicketAnswer(String ticket, boolean result, String details) {
this.ticket = ticket;
this.result = result;
this.details = details;
}
public String getTicket() {
return ticket;
}
}

View File

@ -0,0 +1,37 @@
//
// 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.
//
package com.cloud.agent.api;
public class GetVmVncTicketCommand extends Command {
private String vmInternalName;
public GetVmVncTicketCommand(String vmInternalName) {
this.vmInternalName = vmInternalName;
}
public String getVmInternalName() {
return this.vmInternalName;
}
@Override
public boolean executeInSequence() {
return false;
}
}

View File

@ -18,7 +18,6 @@
--;
-- Schema upgrade from 4.15.0.0 to 4.15.1.0
--;
-- Correct guest OS names
UPDATE `cloud`.`guest_os` SET display_name='Fedora Linux (32 bit)' WHERE id=320;
UPDATE `cloud`.`guest_os` SET display_name='Mandriva Linux (32 bit)' WHERE id=323;
@ -56,3 +55,80 @@ INSERT IGNORE INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervis
-- Add support for Ubuntu Focal Fossa 20.04 for Xenserver 8.2.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (335, UUID(), 10, 'Ubuntu 20.04 LTS', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'Xenserver', '8.2.0', 'Ubuntu Focal Fossa 20.04', 330, now(), 0);
-------------------------------------------------------------------------------------------------------------
-- Add support for VMware 7.0
INSERT IGNORE INTO `cloud`.`hypervisor_capabilities` (uuid, hypervisor_type, hypervisor_version, max_guests_limit, security_group_enabled, max_data_volumes_limit, max_hosts_per_cluster, storage_motion_supported, vm_snapshot_enabled) values (UUID(), 'VMware', '7.0', 1024, 0, 59, 64, 1, 1);
INSERT IGNORE INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) SELECT UUID(),'VMware', '7.0', guest_os_name, guest_os_id, utc_timestamp(), 0 FROM `cloud`.`guest_os_hypervisor` WHERE hypervisor_type='VMware' AND hypervisor_version='6.7';
-- Add support for darwin19_64Guest from VMware 7.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (336, UUID(), 7, 'macOS 10.15 (64 bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0', 'darwin19_64Guest', 336, now(), 0);
-- Add support for debian11_64Guest from VMware 7.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (337, UUID(), 2, 'Debian GNU/Linux 11 (64-bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0', 'debian11_64Guest', 337, now(), 0);
-- Add support for debian11Guest from VMware 7.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (338, UUID(), 2, 'Debian GNU/Linux 11 (32-bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0', 'debian11Guest', 338, now(), 0);
-- Add support for windows2019srv_64Guest from VMware 7.0
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0', 'windows2019srv_64Guest', 276, now(), 0);
-- Add support for VMware 7.0.1.0
INSERT IGNORE INTO `cloud`.`hypervisor_capabilities` (uuid, hypervisor_type, hypervisor_version, max_guests_limit, security_group_enabled, max_data_volumes_limit, max_hosts_per_cluster, storage_motion_supported, vm_snapshot_enabled) values (UUID(), 'VMware', '7.0.1.0', 1024, 0, 59, 64, 1, 1);
INSERT IGNORE INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) SELECT UUID(),'VMware', '7.0.1.0', guest_os_name, guest_os_id, utc_timestamp(), 0 FROM `cloud`.`guest_os_hypervisor` WHERE hypervisor_type='VMware' AND hypervisor_version='7.0';
-- Add support for amazonlinux3_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (339, UUID(), 7, 'Amazon Linux 3 (64 bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'amazonlinux3_64Guest', 339, now(), 0);
-- Add support for asianux9_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (340, UUID(), 7, 'Asianux Server 9 (64 bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'asianux9_64Guest', 340, now(), 0);
-- Add support for centos9_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (341, UUID(), 1, 'CentOS 9', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'centos9_64Guest', 341, now(), 0);
-- Add support for darwin20_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (342, UUID(), 7, 'macOS 11 (64 bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'darwin20_64Guest', 342, now(), 0);
-- Add support for darwin21_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'darwin21_64Guest', 342, now(), 0);
-- Add support for freebsd13_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (343, UUID(), 9, 'FreeBSD 13 (64-bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'freebsd13_64Guest', 343, now(), 0);
-- Add support for freebsd13Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (344, UUID(), 9, 'FreeBSD 13 (32-bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'freebsd13Guest', 344, now(), 0);
-- Add support for oracleLinux9_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (345, UUID(), 3, 'Oracle Linux 9', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'oracleLinux9_64Guest', 345, now(), 0);
-- Add support for other5xLinux64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (346, UUID(), 2, 'Linux 5.x Kernel (64-bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'other5xLinux64Guest', 346, now(), 0);
-- Add support for other5xLinuxGuest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (347, UUID(), 2, 'Linux 5.x Kernel (32-bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'other5xLinuxGuest', 347, now(), 0);
-- Add support for rhel9_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (348, UUID(), 4, 'Red Hat Enterprise Linux 9.0', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'rhel9_64Guest', 348, now(), 0);
-- Add support for sles16_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os` (id, uuid, category_id, display_name, created) VALUES (349, UUID(), 5, 'SUSE Linux Enterprise Server 16 (64-bit)', now());
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'sles16_64Guest', 349, now(), 0);
-- Add support for windows2019srvNext_64Guest from VMware 7.0.1.0
INSERT INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) VALUES (UUID(),'VMware', '7.0.1.0', 'windows2019srvNext_64Guest', 276, now(), 0);

View File

@ -101,6 +101,8 @@ import com.cloud.agent.api.GetUnmanagedInstancesCommand;
import com.cloud.agent.api.GetVmDiskStatsAnswer;
import com.cloud.agent.api.GetVmDiskStatsCommand;
import com.cloud.agent.api.GetVmIpAddressCommand;
import com.cloud.agent.api.GetVmVncTicketCommand;
import com.cloud.agent.api.GetVmVncTicketAnswer;
import com.cloud.agent.api.GetVmNetworkStatsAnswer;
import com.cloud.agent.api.GetVmNetworkStatsCommand;
import com.cloud.agent.api.GetVmStatsAnswer;
@ -578,6 +580,8 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa
answer = execute((PrepareUnmanageVMInstanceCommand) cmd);
} else if (clz == ValidateVcenterDetailsCommand.class) {
answer = execute((ValidateVcenterDetailsCommand) cmd);
} else if (clz == GetVmVncTicketCommand.class) {
answer = execute((GetVmVncTicketCommand) cmd);
} else {
answer = Answer.createUnsupportedCommandAnswer(cmd);
}
@ -7562,4 +7566,25 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa
return new Answer(cmd, false, "Provided vCenter server address is invalid");
}
}
public String acquireVirtualMachineVncTicket(String vmInternalCSName) throws Exception {
VmwareContext context = getServiceContext();
VmwareHypervisorHost hyperHost = getHyperHost(context);
DatacenterMO dcMo = new DatacenterMO(hyperHost.getContext(), hyperHost.getHyperHostDatacenter());
VirtualMachineMO vmMo = dcMo.findVm(vmInternalCSName);
return vmMo.acquireVncTicket();
}
private GetVmVncTicketAnswer execute(GetVmVncTicketCommand cmd) {
String vmInternalName = cmd.getVmInternalName();
s_logger.info("Getting VNC ticket for VM " + vmInternalName);
try {
String ticket = acquireVirtualMachineVncTicket(vmInternalName);
boolean result = StringUtils.isNotBlank(ticket);
return new GetVmVncTicketAnswer(ticket, result, result ? "" : "Empty ticket obtained");
} catch (Exception e) {
s_logger.error("Error getting VNC ticket for VM " + vmInternalName, e);
return new GetVmVncTicketAnswer(null, false, e.getLocalizedMessage());
}
}
}

View File

@ -166,7 +166,7 @@
<cs.servlet.version>4.0.1</cs.servlet.version>
<cs.tomcat-embed-core.version>8.5.61</cs.tomcat-embed-core.version>
<cs.trilead.version>build-217-jenkins-27</cs.trilead.version>
<cs.vmware.api.version>6.7</cs.vmware.api.version>
<cs.vmware.api.version>7.0</cs.vmware.api.version>
<cs.winrm4j.version>0.5.0</cs.winrm4j.version>
<cs.xapi.version>6.2.0-3.1</cs.xapi.version>
<cs.xmlrpc.version>3.1.3</cs.xmlrpc.version>

View File

@ -34,6 +34,7 @@ public class ConsoleProxyClientParam {
private String password;
private String sourceIP;
private String websocketUrl;
public ConsoleProxyClientParam() {
clientHostPort = 0;
@ -150,4 +151,12 @@ public class ConsoleProxyClientParam {
public void setSourceIP(String sourceIP) {
this.sourceIP = sourceIP;
}
public String getWebsocketUrl() {
return websocketUrl;
}
public void setWebsocketUrl(String websocketUrl) {
this.websocketUrl = websocketUrl;
}
}

View File

@ -37,6 +37,13 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetVmVncTicketAnswer;
import com.cloud.agent.api.GetVmVncTicketCommand;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.utils.StringUtils;
import org.apache.cloudstack.framework.security.keys.KeysManager;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
@ -94,6 +101,8 @@ public class ConsoleProxyServlet extends HttpServlet {
UserVmDetailsDao _userVmDetailsDao;
@Inject
KeysManager _keysMgr;
@Inject
AgentManager agentManager;
static KeysManager s_keysMgr;
@ -427,6 +436,47 @@ public class ConsoleProxyServlet extends HttpServlet {
return sb.toString();
}
/**
* Sets the URL to establish a VNC over websocket connection
*/
private void setWebsocketUrl(VirtualMachine vm, ConsoleProxyClientParam param) {
String ticket = acquireVncTicketForVmwareVm(vm);
if (StringUtils.isBlank(ticket)) {
s_logger.error("Could not obtain VNC ticket for VM " + vm.getInstanceName());
return;
}
String wsUrl = composeWebsocketUrlForVmwareVm(ticket, param);
param.setWebsocketUrl(wsUrl);
}
/**
* Format expected: wss://<ESXi_HOST_IP>:443/ticket/<TICKET_ID>
*/
private String composeWebsocketUrlForVmwareVm(String ticket, ConsoleProxyClientParam param) {
param.setClientHostPort(443);
return String.format("wss://%s:%s/ticket/%s", param.getClientHostAddress(), param.getClientHostPort(), ticket);
}
/**
* Acquires a ticket to be used for console proxy as described in 'Removal of VNC Server from ESXi' on:
* https://docs.vmware.com/en/VMware-vSphere/7.0/rn/vsphere-esxi-vcenter-server-70-release-notes.html
*/
private String acquireVncTicketForVmwareVm(VirtualMachine vm) {
try {
s_logger.info("Acquiring VNC ticket for VM = " + vm.getHostName());
GetVmVncTicketCommand cmd = new GetVmVncTicketCommand(vm.getInstanceName());
Answer answer = agentManager.send(vm.getHostId(), cmd);
GetVmVncTicketAnswer ans = (GetVmVncTicketAnswer) answer;
if (!ans.getResult()) {
s_logger.info("VNC ticket could not be acquired correctly: " + ans.getDetails());
}
return ans.getTicket();
} catch (AgentUnavailableException | OperationTimedoutException e) {
s_logger.error("Error acquiring ticket", e);
return null;
}
}
private String composeConsoleAccessUrl(String rootUrl, VirtualMachine vm, HostVO hostVo, InetAddress addr) {
StringBuffer sb = new StringBuffer(rootUrl);
String host = hostVo.getPrivateIpAddress();
@ -477,6 +527,10 @@ public class ConsoleProxyServlet extends HttpServlet {
param.setTicket(ticket);
param.setSourceIP(addr != null ? addr.getHostAddress(): null);
if (requiresVncOverWebSocketConnection(vm, hostVo)) {
setWebsocketUrl(vm, param);
}
if (details != null) {
param.setLocale(details.getValue());
}
@ -513,6 +567,14 @@ public class ConsoleProxyServlet extends HttpServlet {
return sb.toString();
}
/**
* Since VMware 7.0 VNC servers are deprecated, it uses a ticket to create a VNC over websocket connection
* Check: https://docs.vmware.com/en/VMware-vSphere/7.0/rn/vsphere-esxi-vcenter-server-70-release-notes.html
*/
private boolean requiresVncOverWebSocketConnection(VirtualMachine vm, HostVO hostVo) {
return vm.getHypervisorType() == Hypervisor.HypervisorType.VMware && hostVo.getHypervisorVersion().compareTo("7.0") >= 0;
}
public static String genAccessTicket(String host, String port, String sid, String tag) {
return genAccessTicket(host, port, sid, tag, new Date());
}

View File

@ -65,6 +65,11 @@
<artifactId>websocket-server</artifactId>
<version>${cs.jetty.version}</version>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.1</version>
</dependency>
</dependencies>
<build>
<resources>

View File

@ -31,6 +31,7 @@ import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import com.cloud.utils.StringUtils;
import org.apache.log4j.xml.DOMConfigurator;
import org.eclipse.jetty.websocket.api.Session;
@ -172,6 +173,11 @@ public class ConsoleProxy {
authResult.setHost(param.getClientHostAddress());
authResult.setPort(param.getClientHostPort());
String websocketUrl = param.getWebsocketUrl();
if (StringUtils.isNotBlank(websocketUrl)) {
return authResult;
}
if (standaloneStart) {
return authResult;
}

View File

@ -36,6 +36,7 @@ public class ConsoleProxyClientParam {
private String hypervHost;
private String username;
private String password;
private String websocketUrl;
private String sourceIP;
@ -153,4 +154,12 @@ public class ConsoleProxyClientParam {
public void setSourceIP(String sourceIP) {
this.sourceIP = sourceIP;
}
public String getWebsocketUrl() {
return websocketUrl;
}
public void setWebsocketUrl(String websocketUrl) {
this.websocketUrl = websocketUrl;
}
}

View File

@ -93,6 +93,9 @@ public class ConsoleProxyHttpHandlerHelper {
map.put("password", param.getPassword());
if (param.getSourceIP() != null)
map.put("sourceIP", param.getSourceIP());
if (param.getWebsocketUrl() != null) {
map.put("websocketUrl", param.getWebsocketUrl());
}
} else {
s_logger.error("Unable to decode token");
}
@ -116,5 +119,6 @@ public class ConsoleProxyHttpHandlerHelper {
map.remove("hypervHost");
map.remove("username");
map.remove("password");
map.remove("websocketUrl");
}
}

View File

@ -88,6 +88,7 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
String username = queryMap.get("username");
String password = queryMap.get("password");
String sourceIP = queryMap.get("sourceIP");
String websocketUrl = queryMap.get("websocketUrl");
if (tag == null)
tag = "";
@ -131,6 +132,7 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
param.setHypervHost(hypervHost);
param.setUsername(username);
param.setPassword(password);
param.setWebsocketUrl(websocketUrl);
viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr, session);
} catch (Exception e) {
s_logger.warn("Failed to create viewer due to " + e.getMessage(), e);

View File

@ -16,6 +16,7 @@
// under the License.
package com.cloud.consoleproxy;
import com.cloud.utils.StringUtils;
import org.apache.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.extensions.Frame;
@ -96,47 +97,30 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
String tunnelUrl = param.getClientTunnelUrl();
String tunnelSession = param.getClientTunnelSession();
String websocketUrl = param.getWebsocketUrl();
try {
if (tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null
&& !tunnelSession.isEmpty()) {
URI uri = new URI(tunnelUrl);
s_logger.info("Connect to VNC server via tunnel. url: " + tunnelUrl + ", session: "
+ tunnelSession);
connectClientToVNCServer(tunnelUrl, tunnelSession, websocketUrl);
ConsoleProxy.ensureRoute(uri.getHost());
client.connectTo(uri.getHost(), uri.getPort(), uri.getPath() + "?" + uri.getQuery(),
tunnelSession, "https".equalsIgnoreCase(uri.getScheme()));
} else {
s_logger.info("Connect to VNC server directly. host: " + getClientHostAddress() + ", port: "
+ getClientHostPort());
ConsoleProxy.ensureRoute(getClientHostAddress());
client.connectTo(getClientHostAddress(), getClientHostPort());
}
} catch (UnknownHostException e) {
s_logger.error("Unexpected exception", e);
} catch (IOException e) {
s_logger.error("Unexpected exception", e);
} catch (Throwable e) {
s_logger.error("Unexpected exception", e);
}
String ver = client.handshake();
session.getRemote().sendBytes(ByteBuffer.wrap(ver.getBytes(), 0, ver.length()));
byte[] b = client.authenticate(getClientHostPassword());
session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, 4));
authenticateToVNCServer();
int readBytes;
byte[] b;
while (connectionAlive) {
b = new byte[100];
readBytes = client.read(b);
if (readBytes == -1) {
break;
}
if (readBytes > 0) {
session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, readBytes));
updateFrontEndActivityTime();
if (client.isVncOverWebSocketConnection()) {
if (client.isVncOverWebSocketConnectionOpen()) {
updateFrontEndActivityTime();
}
connectionAlive = client.isVncOverWebSocketConnectionAlive();
} else {
b = new byte[100];
readBytes = client.read(b);
if (readBytes == -1) {
break;
}
if (readBytes > 0) {
session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, readBytes));
updateFrontEndActivityTime();
}
}
}
connectionAlive = false;
@ -149,6 +133,55 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
worker.start();
}
/**
* Authenticate to VNC server when not using websockets
* @throws IOException
*/
private void authenticateToVNCServer() throws IOException {
if (!client.isVncOverWebSocketConnection()) {
String ver = client.handshake();
session.getRemote().sendBytes(ByteBuffer.wrap(ver.getBytes(), 0, ver.length()));
byte[] b = client.authenticate(getClientHostPassword());
session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, 4));
}
}
/**
* Connect to a VNC server in one of three possible ways:
* - When tunnelUrl and tunnelSession are not empty -> via tunnel
* - When websocketUrl is not empty -> connect to websocket
* - Otherwise -> connect to TCP port on host directly
*/
private void connectClientToVNCServer(String tunnelUrl, String tunnelSession, String websocketUrl) {
try {
if (StringUtils.isNotBlank(websocketUrl)) {
s_logger.info("Connect to VNC over websocket URL: " + websocketUrl);
client.connectToWebSocket(websocketUrl, session);
} else if (tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null
&& !tunnelSession.isEmpty()) {
URI uri = new URI(tunnelUrl);
s_logger.info("Connect to VNC server via tunnel. url: " + tunnelUrl + ", session: "
+ tunnelSession);
ConsoleProxy.ensureRoute(uri.getHost());
client.connectTo(uri.getHost(), uri.getPort(), uri.getPath() + "?" + uri.getQuery(),
tunnelSession, "https".equalsIgnoreCase(uri.getScheme()));
} else {
s_logger.info("Connect to VNC server directly. host: " + getClientHostAddress() + ", port: "
+ getClientHostPort());
ConsoleProxy.ensureRoute(getClientHostAddress());
client.connectTo(getClientHostAddress(), getClientHostPort());
}
} catch (UnknownHostException e) {
s_logger.error("Unexpected exception", e);
} catch (IOException e) {
s_logger.error("Unexpected exception", e);
} catch (Throwable e) {
s_logger.error("Unexpected exception", e);
}
}
private void setClientParam(ConsoleProxyClientParam param) {
this.clientParam = param;
}

View File

@ -20,7 +20,10 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.spec.KeySpec;
@ -31,6 +34,8 @@ import javax.crypto.spec.DESKeySpec;
import com.cloud.consoleproxy.util.Logger;
import com.cloud.consoleproxy.util.RawHTTP;
import com.cloud.consoleproxy.websocket.WebSocketReverseProxy;
import org.eclipse.jetty.websocket.api.Session;
public class NoVncClient {
private static final Logger s_logger = Logger.getLogger(NoVncClient.class);
@ -39,6 +44,8 @@ public class NoVncClient {
private DataInputStream is;
private DataOutputStream os;
private WebSocketReverseProxy webSocketReverseProxy;
public NoVncClient() {
}
@ -62,6 +69,30 @@ public class NoVncClient {
setStreams();
}
// VNC over WebSocket connection helpers
public void connectToWebSocket(String websocketUrl, Session session) throws URISyntaxException {
webSocketReverseProxy = new WebSocketReverseProxy(new URI(websocketUrl), session);
webSocketReverseProxy.connect();
}
public boolean isVncOverWebSocketConnection() {
return webSocketReverseProxy != null;
}
public boolean isVncOverWebSocketConnectionOpen() {
return isVncOverWebSocketConnection() && webSocketReverseProxy.isOpen();
}
public boolean isVncOverWebSocketConnectionAlive() {
return isVncOverWebSocketConnection() && !webSocketReverseProxy.isClosing() && !webSocketReverseProxy.isClosed();
}
public void proxyMsgOverWebSocketConnection(ByteBuffer msg) {
if (isVncOverWebSocketConnection()) {
webSocketReverseProxy.proxyMsgFromRemoteSessionToEndpoint(msg);
}
}
private void setStreams() throws IOException {
this.is = new DataInputStream(this.socket.getInputStream());
this.os = new DataOutputStream(this.socket.getOutputStream());
@ -213,7 +244,11 @@ public class NoVncClient {
}
public void write(byte[] b) throws IOException {
os.write(b);
if (isVncOverWebSocketConnection()) {
proxyMsgOverWebSocketConnection(ByteBuffer.wrap(b));
} else {
os.write(b);
}
}
}

View File

@ -0,0 +1,118 @@
// 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.
package com.cloud.consoleproxy.websocket;
import com.cloud.consoleproxy.util.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.extensions.DefaultExtension;
import org.java_websocket.handshake.ServerHandshake;
import org.java_websocket.protocols.Protocol;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
/**
* Acts as a websocket reverse proxy between the remoteSession and the connected endpoint
* - Connects to a websocket endpoint and sends the received data to the remoteSession endpoint
* - Receives data from the remoteSession through the receiveProxiedMsg() method and forwards it to the connected endpoint
*
* remoteSession WebSocketReverseProxy websocket endpoint
* data -----------------> receiveProxiedMsg() -----------> data
* data <----------------- onMessage() <------------------- data
*/
public class WebSocketReverseProxy extends WebSocketClient {
private static final Protocol protocol = new Protocol("binary");
private static final DefaultExtension defaultExtension = new DefaultExtension();
private static final Draft_6455 draft = new Draft_6455(Collections.singletonList(defaultExtension), Collections.singletonList(protocol));
private static final Logger logger = Logger.getLogger(WebSocketReverseProxy.class);
private Session remoteSession;
private void acceptAllCerts() {
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[]{};
}
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
}};
SSLContext sc;
try {
sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
SSLSocketFactory factory = sc.getSocketFactory();
this.setSocketFactory(factory);
} catch (Exception e) {
e.printStackTrace();
}
}
public WebSocketReverseProxy(URI wsUrl, Session session) {
super(wsUrl, draft);
this.remoteSession = session;
acceptAllCerts();
setConnectionLostTimeout(0);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
}
@Override
public void onMessage(String message) {
}
@Override
public void onClose(int code, String reason, boolean remote) {
logger.info("Closing connection to websocket: reason=" + reason + " code=" + code + " remote=" + remote);
}
@Override
public void onError(Exception ex) {
logger.error("Error on connection to websocket: " + ex.getLocalizedMessage());
ex.printStackTrace();
}
@Override
public void onMessage(ByteBuffer bytes) {
try {
this.remoteSession.getRemote().sendBytes(bytes);
} catch (IOException e) {
logger.error("Error proxing msg from websocket to client side: " + e.getLocalizedMessage());
e.printStackTrace();
}
}
public void proxyMsgFromRemoteSessionToEndpoint(ByteBuffer msg) {
this.getConnection().send(msg);
}
}

View File

@ -37,6 +37,9 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import com.cloud.utils.exception.CloudRuntimeException;
import com.vmware.vim25.InvalidStateFaultMsg;
import com.vmware.vim25.RuntimeFaultFaultMsg;
import com.vmware.vim25.VirtualMachineTicket;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
@ -3534,4 +3537,13 @@ public class VirtualMachineMO extends BaseMO {
return false;
}
}
/**
* Acquire VNC ticket for console proxy.
* Since VMware version 7
*/
public String acquireVncTicket() throws InvalidStateFaultMsg, RuntimeFaultFaultMsg {
VirtualMachineTicket ticket = _context.getService().acquireTicket(_mor, "webmks");
return ticket.getTicket();
}
}