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();
+ }
}