Skip to content

Commit ee9acb7

Browse files
davidjumanirohityadavcloud
authored andcommitted
noVNC console integration (apache#3967)
* Adding noVNC repo * Adding support for noVNC * Adding Ctl+Esc * Removing device name from novnc header (cherry picked from commit 1756b0f) Signed-off-by: Rohit Yadav <[email protected]>
1 parent 8a1dff8 commit ee9acb7

File tree

197 files changed

+34009
-7
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

197 files changed

+34009
-7
lines changed

server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import com.cloud.utils.component.Manager;
2020
import com.cloud.vm.ConsoleProxyVO;
2121

22+
import org.apache.cloudstack.framework.config.ConfigKey;
23+
2224
public interface ConsoleProxyManager extends Manager, ConsoleProxyService {
2325

2426
public static final int DEFAULT_PROXY_CAPACITY = 50;
@@ -31,9 +33,14 @@ public interface ConsoleProxyManager extends Manager, ConsoleProxyService {
3133
public static final int DEFAULT_PROXY_URL_PORT = 80;
3234
public static final int DEFAULT_PROXY_SESSION_TIMEOUT = 300000; // 5 minutes
3335

36+
public static final int DEFAULT_NOVNC_PORT = 8080;
37+
3438
public static final String ALERT_SUBJECT = "proxy-alert";
3539
public static final String CERTIFICATE_NAME = "CPVMCertificate";
3640

41+
public static final ConfigKey<Boolean> NoVncConsoleDefault = new ConfigKey<Boolean>("Advanced", Boolean.class, "novnc.console.default", "true",
42+
"If true, noVNC console will be default console for virtual machines", true);
43+
3744
public void setManagementState(ConsoleProxyManagementState state);
3845

3946
public ConsoleProxyManagementState getManagementState();

server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import org.apache.cloudstack.agent.lb.IndirectAgentLB;
3333
import org.apache.cloudstack.context.CallContext;
3434
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
35+
import org.apache.cloudstack.framework.config.ConfigKey;
36+
import org.apache.cloudstack.framework.config.Configurable;
3537
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
3638
import org.apache.cloudstack.framework.security.keys.KeysManager;
3739
import org.apache.cloudstack.framework.security.keystore.KeystoreDao;
@@ -154,7 +156,8 @@
154156
// Starting, HA, Migrating, Running state are all counted as "Open" for available capacity calculation
155157
// because sooner or later, it will be driven into Running state
156158
//
157-
public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxyManager, VirtualMachineGuru, SystemVmLoadScanHandler<Long>, ResourceStateAdapter {
159+
public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxyManager, VirtualMachineGuru, SystemVmLoadScanHandler<Long>, ResourceStateAdapter, Configurable {
160+
158161
private static final Logger s_logger = Logger.getLogger(ConsoleProxyManagerImpl.class);
159162

160163
private static final int DEFAULT_CAPACITY_SCAN_INTERVAL = 30000; // 30 seconds
@@ -1741,4 +1744,14 @@ public void setConsoleProxyAllocators(List<ConsoleProxyAllocator> consoleProxyAl
17411744
_consoleProxyAllocators = consoleProxyAllocators;
17421745
}
17431746

1747+
@Override
1748+
public String getConfigComponentName() {
1749+
return ConsoleProxyManager.class.getSimpleName();
1750+
}
1751+
1752+
@Override
1753+
public ConfigKey<?>[] getConfigKeys() {
1754+
return new ConfigKey<?>[] { NoVncConsoleDefault };
1755+
}
1756+
17441757
}

server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
import org.springframework.stereotype.Component;
4242
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
4343

44+
45+
import com.cloud.vm.VmDetailConstants;
46+
import com.google.gson.Gson;
47+
import com.google.gson.GsonBuilder;
48+
49+
import com.cloud.consoleproxy.ConsoleProxyManager;
4450
import com.cloud.exception.PermissionDeniedException;
4551
import com.cloud.host.HostVO;
4652
import com.cloud.hypervisor.Hypervisor;
@@ -59,10 +65,7 @@
5965
import com.cloud.vm.UserVmDetailVO;
6066
import com.cloud.vm.VirtualMachine;
6167
import com.cloud.vm.VirtualMachineManager;
62-
import com.cloud.vm.VmDetailConstants;
6368
import com.cloud.vm.dao.UserVmDetailsDao;
64-
import com.google.gson.Gson;
65-
import com.google.gson.GsonBuilder;
6669

6770
/**
6871
* Thumbnail access : /console?cmd=thumbnail&vm=xxx&w=xxx&h=xxx
@@ -478,7 +481,12 @@ private String composeConsoleAccessUrl(String rootUrl, VirtualMachine vm, HostVO
478481
param.setClientTunnelSession(parsedHostInfo.third());
479482
}
480483

481-
sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param));
484+
if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) {
485+
sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param));
486+
} else {
487+
sb.append("/resource/noVNC/vnc_lite.html?port=" + ConsoleProxyManager.DEFAULT_NOVNC_PORT + "&token="
488+
+ encryptor.encryptObject(ConsoleProxyClientParam.class, param));
489+
}
482490

483491
// for console access, we need guest OS type to help implement keyboard
484492
long guestOs = vm.getGuestOSId();

services/console-proxy/server/pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@
5050
<artifactId>cloudstack-service-console-proxy-rdpclient</artifactId>
5151
<version>${project.version}</version>
5252
</dependency>
53+
<dependency>
54+
<groupId>javax.websocket</groupId>
55+
<artifactId>javax.websocket-api</artifactId>
56+
<version>1.0</version>
57+
</dependency>
58+
<dependency>
59+
<groupId>org.eclipse.jetty</groupId>
60+
<artifactId>jetty-server</artifactId>
61+
<version>${cs.jetty.version}</version>
62+
</dependency>
63+
<dependency>
64+
<groupId>org.eclipse.jetty.websocket</groupId>
65+
<artifactId>websocket-server</artifactId>
66+
<version>${cs.jetty.version}</version>
67+
</dependency>
5368
</dependencies>
5469
<build>
5570
<resources>

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.concurrent.Executor;
3333

3434
import org.apache.log4j.xml.DOMConfigurator;
35+
import org.eclipse.jetty.websocket.api.Session;
3536

3637
import com.cloud.consoleproxy.util.Logger;
3738
import com.cloud.utils.PropertiesUtil;
@@ -344,12 +345,22 @@ private static void startupHttpMain() {
344345
server.createContext("/ajaximg", new ConsoleProxyAjaxImageHandler());
345346
server.setExecutor(new ThreadExecutor()); // creates a default executor
346347
server.start();
348+
349+
ConsoleProxyNoVNCServer noVNCServer = getNoVNCServer();
350+
noVNCServer.start();
351+
347352
} catch (Exception e) {
348353
s_logger.error(e.getMessage(), e);
349354
System.exit(1);
350355
}
351356
}
352357

358+
private static ConsoleProxyNoVNCServer getNoVNCServer() {
359+
if (httpListenPort == 443)
360+
return new ConsoleProxyNoVNCServer(ksBits, ksPassword);
361+
return new ConsoleProxyNoVNCServer();
362+
}
363+
353364
private static void startupHttpCmdPort() {
354365
try {
355366
s_logger.info("Listening for HTTP CMDs on port " + httpCmdListenPort);
@@ -395,7 +406,7 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr
395406
String clientKey = param.getClientMapKey();
396407
synchronized (connectionMap) {
397408
viewer = connectionMap.get(clientKey);
398-
if (viewer == null) {
409+
if (viewer == null || viewer.getClass() == ConsoleProxyNoVncClient.class) {
399410
viewer = getClient(param);
400411
viewer.initClient(param);
401412
connectionMap.put(clientKey, viewer);
@@ -429,7 +440,7 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param,
429440
String clientKey = param.getClientMapKey();
430441
synchronized (connectionMap) {
431442
ConsoleProxyClient viewer = connectionMap.get(clientKey);
432-
if (viewer == null) {
443+
if (viewer == null || viewer.getClass() == ConsoleProxyNoVncClient.class) {
433444
authenticationExternally(param);
434445
viewer = getClient(param);
435446
viewer.initClient(param);
@@ -521,4 +532,40 @@ public void execute(Runnable r) {
521532
new Thread(r).start();
522533
}
523534
}
535+
536+
public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam param, String ajaxSession,
537+
Session session) throws AuthenticationException {
538+
boolean reportLoadChange = false;
539+
String clientKey = param.getClientMapKey();
540+
synchronized (connectionMap) {
541+
ConsoleProxyClient viewer = connectionMap.get(clientKey);
542+
if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) {
543+
authenticationExternally(param);
544+
viewer = new ConsoleProxyNoVncClient(session);
545+
viewer.initClient(param);
546+
547+
connectionMap.put(clientKey, viewer);
548+
reportLoadChange = true;
549+
} else {
550+
if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() ||
551+
!param.getClientHostPassword().equals(viewer.getClientHostPassword()))
552+
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid");
553+
554+
if (!viewer.isFrontEndAlive()) {
555+
authenticationExternally(param);
556+
viewer.initClient(param);
557+
reportLoadChange = true;
558+
}
559+
}
560+
561+
if (reportLoadChange) {
562+
ConsoleProxyClientStatsCollector statsCollector = getStatsCollector();
563+
String loadInfo = statsCollector.getStatsReport();
564+
reportLoadInfo(loadInfo);
565+
if (s_logger.isDebugEnabled())
566+
s_logger.debug("Report load change : " + loadInfo);
567+
}
568+
return (ConsoleProxyNoVncClient)viewer;
569+
}
570+
}
524571
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package com.cloud.consoleproxy;
18+
19+
import java.io.IOException;
20+
import java.util.Map;
21+
22+
import javax.servlet.ServletException;
23+
import javax.servlet.http.HttpServletRequest;
24+
import javax.servlet.http.HttpServletResponse;
25+
26+
import com.cloud.consoleproxy.util.Logger;
27+
28+
import org.eclipse.jetty.server.Request;
29+
import org.eclipse.jetty.websocket.api.Session;
30+
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
31+
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
32+
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
33+
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
34+
import org.eclipse.jetty.websocket.api.extensions.Frame;
35+
import org.eclipse.jetty.websocket.server.WebSocketHandler;
36+
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
37+
38+
@WebSocket
39+
public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
40+
41+
private ConsoleProxyNoVncClient viewer;
42+
private static final Logger s_logger = Logger.getLogger(ConsoleProxyNoVNCHandler.class);
43+
44+
public ConsoleProxyNoVNCHandler() {
45+
super();
46+
}
47+
48+
@Override
49+
public void configure(WebSocketServletFactory webSocketServletFactory) {
50+
webSocketServletFactory.register(ConsoleProxyNoVNCHandler.class);
51+
}
52+
53+
@Override
54+
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
55+
throws IOException, ServletException {
56+
57+
if (this.getWebSocketFactory().isUpgradeRequest(request, response)) {
58+
response.addHeader("Sec-WebSocket-Protocol", "binary");
59+
if (this.getWebSocketFactory().acceptWebSocket(request, response)) {
60+
baseRequest.setHandled(true);
61+
return;
62+
}
63+
64+
if (response.isCommitted()) {
65+
return;
66+
}
67+
}
68+
69+
super.handle(target, baseRequest, request, response);
70+
}
71+
72+
@OnWebSocketConnect
73+
public void onConnect(final Session session) throws IOException, InterruptedException {
74+
75+
String queries = session.getUpgradeRequest().getQueryString();
76+
Map<String, String> queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(queries);
77+
78+
String host = queryMap.get("host");
79+
String portStr = queryMap.get("port");
80+
String sid = queryMap.get("sid");
81+
String tag = queryMap.get("tag");
82+
String ticket = queryMap.get("ticket");
83+
String ajaxSessionIdStr = queryMap.get("sess");
84+
String console_url = queryMap.get("consoleurl");
85+
String console_host_session = queryMap.get("sessionref");
86+
String vm_locale = queryMap.get("locale");
87+
String hypervHost = queryMap.get("hypervHost");
88+
String username = queryMap.get("username");
89+
String password = queryMap.get("password");
90+
91+
if (tag == null)
92+
tag = "";
93+
94+
long ajaxSessionId = 0;
95+
int port;
96+
97+
if (host == null || portStr == null || sid == null)
98+
throw new IllegalArgumentException();
99+
100+
try {
101+
port = Integer.parseInt(portStr);
102+
} catch (NumberFormatException e) {
103+
s_logger.warn("Invalid number parameter in query string: " + portStr);
104+
throw new IllegalArgumentException(e);
105+
}
106+
107+
if (ajaxSessionIdStr != null) {
108+
try {
109+
ajaxSessionId = Long.parseLong(ajaxSessionIdStr);
110+
} catch (NumberFormatException e) {
111+
s_logger.warn("Invalid number parameter in query string: " + ajaxSessionIdStr);
112+
throw new IllegalArgumentException(e);
113+
}
114+
}
115+
116+
try {
117+
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
118+
param.setClientHostAddress(host);
119+
param.setClientHostPort(port);
120+
param.setClientHostPassword(sid);
121+
param.setClientTag(tag);
122+
param.setTicket(ticket);
123+
param.setClientTunnelUrl(console_url);
124+
param.setClientTunnelSession(console_host_session);
125+
param.setLocale(vm_locale);
126+
param.setHypervHost(hypervHost);
127+
param.setUsername(username);
128+
param.setPassword(password);
129+
viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr, session);
130+
} catch (Exception e) {
131+
s_logger.warn("Failed to create viewer due to " + e.getMessage(), e);
132+
return;
133+
}
134+
}
135+
136+
@OnWebSocketClose
137+
public void onClose(Session session, int statusCode, String reason) throws IOException, InterruptedException {
138+
ConsoleProxy.removeViewer(viewer);
139+
}
140+
141+
@OnWebSocketFrame
142+
public void onFrame(Frame f) throws IOException {
143+
viewer.sendClientFrame(f);
144+
}
145+
}

0 commit comments

Comments
 (0)