vmware: Add support for VMware 7 (#4300)

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 0000000..9320098
--- /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 0000000..bc11979
--- /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 21d9dcb..859bbd0 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 @@
 -- 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 9963b75..97a10e5 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.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 @@
                 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 @@
             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 f497032..3fe3329 100644
--- a/pom.xml
+++ b/pom.xml
@@ -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>
diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java
index 3d587c2..8f9363d 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 @@
     private String password;
 
     private String sourceIP;
+    private String websocketUrl;
 
     public ConsoleProxyClientParam() {
         clientHostPort = 0;
@@ -150,4 +151,12 @@
     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 622a4c8..b755a84 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.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 @@
     UserVmDetailsDao _userVmDetailsDao;
     @Inject
     KeysManager _keysMgr;
+    @Inject
+    AgentManager agentManager;
 
     static KeysManager s_keysMgr;
 
@@ -427,6 +436,47 @@
         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 @@
         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 @@
         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 342bb8a..09431d6 100644
--- a/services/console-proxy/server/pom.xml
+++ b/services/console-proxy/server/pom.xml
@@ -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>
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 3c9d272..702e9a8 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.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 @@
         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 ad2fc25..c071f55 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 @@
     private String hypervHost;
     private String username;
     private String password;
+    private String websocketUrl;
 
     private String sourceIP;
 
@@ -153,4 +154,12 @@
     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 4bed150..b7f969a 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 @@
                     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 @@
         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 1c3b47e..91d8e19 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 @@
         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 @@
             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 353c32d..cf0a05d 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 @@
 
                     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 @@
         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 9a43725..7be6421 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.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 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 @@
     private DataInputStream is;
     private DataOutputStream os;
 
+    private WebSocketReverseProxy webSocketReverseProxy;
+
     public NoVncClient() {
     }
 
@@ -62,6 +69,30 @@
         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 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 0000000..e2f62d6
--- /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 364526d..e1ba6b0 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.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 @@
             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();
+    }
 }