From 0302750aacb12e550fe36e154ad53ca964b3001b Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Thu, 15 Apr 2021 16:10:14 +0530 Subject: [PATCH] vmware: Add support for VMware 7 (#4300) --- .../cloud/agent/api/GetVmVncTicketAnswer.java | 34 +++++ .../agent/api/GetVmVncTicketCommand.java | 37 ++++++ .../META-INF/db/schema-41500to41510.sql | 78 +++++++++++- .../vmware/resource/VmwareResource.java | 25 ++++ pom.xml | 2 +- .../servlet/ConsoleProxyClientParam.java | 9 ++ .../cloud/servlet/ConsoleProxyServlet.java | 62 +++++++++ services/console-proxy/server/pom.xml | 5 + .../com/cloud/consoleproxy/ConsoleProxy.java | 6 + .../consoleproxy/ConsoleProxyClientParam.java | 9 ++ .../ConsoleProxyHttpHandlerHelper.java | 4 + .../ConsoleProxyNoVNCHandler.java | 2 + .../consoleproxy/ConsoleProxyNoVncClient.java | 105 ++++++++++------ .../cloud/consoleproxy/vnc/NoVncClient.java | 37 +++++- .../websocket/WebSocketReverseProxy.java | 118 ++++++++++++++++++ .../vmware/mo/VirtualMachineMO.java | 12 ++ 16 files changed, 506 insertions(+), 39 deletions(-) create mode 100644 core/src/main/java/com/cloud/agent/api/GetVmVncTicketAnswer.java create mode 100644 core/src/main/java/com/cloud/agent/api/GetVmVncTicketCommand.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/websocket/WebSocketReverseProxy.java diff --git a/core/src/main/java/com/cloud/agent/api/GetVmVncTicketAnswer.java b/core/src/main/java/com/cloud/agent/api/GetVmVncTicketAnswer.java new file mode 100644 index 00000000000..9320098cd02 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/GetVmVncTicketAnswer.java @@ -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; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/GetVmVncTicketCommand.java b/core/src/main/java/com/cloud/agent/api/GetVmVncTicketCommand.java new file mode 100644 index 00000000000..bc119792762 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/GetVmVncTicketCommand.java @@ -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; + } +} diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41500to41510.sql b/engine/schema/src/main/resources/META-INF/db/schema-41500to41510.sql index 21d9dcba03b..859bbd00776 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41500to41510.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41500to41510.sql @@ -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); + diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java index 9963b7589c2..97a10e51f56 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java @@ -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()); + } + } } diff --git a/pom.xml b/pom.xml index f497032c69f..3fe33297728 100644 --- a/pom.xml +++ b/pom.xml @@ -166,7 +166,7 @@ 4.0.1 8.5.61 build-217-jenkins-27 - 6.7 + 7.0 0.5.0 6.2.0-3.1 3.1.3 diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java index 3d587c247e8..8f9363df5ba 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java @@ -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; + } } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java index 622a4c84761..b755a84887d 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java @@ -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://:443/ticket/ + */ + 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()); } diff --git a/services/console-proxy/server/pom.xml b/services/console-proxy/server/pom.xml index 342bb8a28a1..09431d6e2d8 100644 --- a/services/console-proxy/server/pom.xml +++ b/services/console-proxy/server/pom.xml @@ -65,6 +65,11 @@ websocket-server ${cs.jetty.version} + + org.java-websocket + Java-WebSocket + 1.5.1 + diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java index 3c9d2721ce9..702e9a855d1 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java @@ -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; } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java index ad2fc25026b..c071f551da7 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java @@ -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; + } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java index 4bed1506a28..b7f969a1e57 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java @@ -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"); } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java index 1c3b47e8288..91d8e192fd9 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java @@ -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); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java index 353c32da24b..cf0a05de622 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java @@ -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; } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java index 9a4372544fc..7be6421714e 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java @@ -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); + } } } \ No newline at end of file diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/websocket/WebSocketReverseProxy.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/websocket/WebSocketReverseProxy.java new file mode 100644 index 00000000000..e2f62d6ba16 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/websocket/WebSocketReverseProxy.java @@ -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); + } +} diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java index 364526d7b1c..e1ba6b0b87f 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java @@ -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(); + } }