| // 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 org.apache.cloudstack.consoleproxy; |
| |
| import java.util.Arrays; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.UUID; |
| |
| import javax.crypto.Mac; |
| import javax.crypto.spec.SecretKeySpec; |
| import javax.inject.Inject; |
| import javax.naming.ConfigurationException; |
| |
| import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint; |
| import org.apache.cloudstack.context.CallContext; |
| import org.apache.cloudstack.framework.security.keys.KeysManager; |
| import org.apache.commons.codec.binary.Base64; |
| import org.apache.commons.lang3.ArrayUtils; |
| import org.apache.commons.lang3.ObjectUtils; |
| import org.apache.commons.lang3.StringUtils; |
| |
| 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.consoleproxy.ConsoleProxyManager; |
| import com.cloud.dc.DataCenter; |
| import com.cloud.dc.dao.DataCenterDao; |
| import com.cloud.exception.AgentUnavailableException; |
| import com.cloud.exception.OperationTimedoutException; |
| import com.cloud.exception.PermissionDeniedException; |
| import com.cloud.host.HostVO; |
| import com.cloud.hypervisor.Hypervisor; |
| import com.cloud.resource.ResourceState; |
| import com.cloud.server.ManagementServer; |
| import com.cloud.servlet.ConsoleProxyClientParam; |
| import com.cloud.servlet.ConsoleProxyPasswordBasedEncryptor; |
| import com.cloud.storage.GuestOSVO; |
| import com.cloud.user.Account; |
| import com.cloud.user.AccountManager; |
| import com.cloud.uservm.UserVm; |
| import com.cloud.utils.Pair; |
| import com.cloud.utils.Ternary; |
| import com.cloud.utils.component.ManagerBase; |
| import com.cloud.utils.concurrency.NamedThreadFactory; |
| import com.cloud.utils.db.EntityManager; |
| import com.cloud.utils.db.GlobalLock; |
| import com.cloud.utils.exception.CloudRuntimeException; |
| import com.cloud.vm.ConsoleSessionVO; |
| import com.cloud.vm.UserVmDetailVO; |
| import com.cloud.vm.VirtualMachine; |
| import com.cloud.vm.VirtualMachineManager; |
| import com.cloud.vm.VmDetailConstants; |
| import com.cloud.vm.dao.ConsoleSessionDao; |
| import com.cloud.vm.dao.UserVmDetailsDao; |
| import com.google.gson.Gson; |
| import com.google.gson.GsonBuilder; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.cloudstack.framework.config.ConfigKey; |
| import org.apache.cloudstack.managed.context.ManagedContextRunnable; |
| import org.joda.time.DateTime; |
| |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.ScheduledExecutorService; |
| import java.util.concurrent.TimeUnit; |
| |
| public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAccessManager { |
| |
| @Inject |
| private AccountManager accountManager; |
| @Inject |
| private VirtualMachineManager virtualMachineManager; |
| @Inject |
| private ManagementServer managementServer; |
| @Inject |
| private EntityManager entityManager; |
| @Inject |
| private UserVmDetailsDao userVmDetailsDao; |
| @Inject |
| private KeysManager keysManager; |
| @Inject |
| private AgentManager agentManager; |
| @Inject |
| private ConsoleProxyManager consoleProxyManager; |
| @Inject |
| DataCenterDao dataCenterDao; |
| @Inject |
| private ConsoleSessionDao consoleSessionDao; |
| |
| private ScheduledExecutorService executorService = null; |
| |
| private static KeysManager secretKeysManager; |
| private final Gson gson = new GsonBuilder().create(); |
| |
| protected Logger logger = LogManager.getLogger(ConsoleAccessManagerImpl.class); |
| |
| private static final List<VirtualMachine.State> unsupportedConsoleVMState = Arrays.asList( |
| VirtualMachine.State.Stopped, VirtualMachine.State.Error, VirtualMachine.State.Destroyed |
| ); |
| |
| @Override |
| public boolean configure(String name, Map<String, Object> params) throws ConfigurationException { |
| ConsoleAccessManagerImpl.secretKeysManager = keysManager; |
| executorService = Executors.newScheduledThreadPool(1, new NamedThreadFactory("ConsoleSession-Scavenger")); |
| return super.configure(name, params); |
| } |
| |
| @Override |
| public boolean start() { |
| int consoleCleanupInterval = ConsoleAccessManager.ConsoleSessionCleanupInterval.value(); |
| if (consoleCleanupInterval > 0) { |
| logger.info(String.format("The ConsoleSessionCleanupTask will run every %s hours", consoleCleanupInterval)); |
| executorService.scheduleWithFixedDelay(new ConsoleSessionCleanupTask(), consoleCleanupInterval, consoleCleanupInterval, TimeUnit.HOURS); |
| } |
| return true; |
| } |
| |
| @Override |
| public String getConfigComponentName() { |
| return ConsoleAccessManager.class.getName(); |
| } |
| |
| @Override |
| public ConfigKey<?>[] getConfigKeys() { |
| return new ConfigKey[] { |
| ConsoleAccessManager.ConsoleSessionCleanupInterval, |
| ConsoleAccessManager.ConsoleSessionCleanupRetentionHours |
| }; |
| } |
| |
| public class ConsoleSessionCleanupTask extends ManagedContextRunnable { |
| @Override |
| protected void runInContext() { |
| final GlobalLock gcLock = GlobalLock.getInternLock("ConsoleSession.Cleanup.Lock"); |
| try { |
| if (gcLock.lock(3)) { |
| try { |
| reallyRun(); |
| } finally { |
| gcLock.unlock(); |
| } |
| } |
| } finally { |
| gcLock.releaseRef(); |
| } |
| } |
| |
| private void reallyRun() { |
| if (logger.isDebugEnabled()) { |
| logger.debug("Starting ConsoleSessionCleanupTask..."); |
| } |
| Integer retentionHours = ConsoleAccessManager.ConsoleSessionCleanupRetentionHours.value(); |
| Date dateBefore = DateTime.now().minusHours(retentionHours).toDate(); |
| if (logger.isDebugEnabled()) { |
| logger.debug(String.format("Retention hours: %s, checking for removed console session " + |
| "records to expunge older than: %s", retentionHours, dateBefore)); |
| } |
| int sessionsExpunged = consoleSessionDao.expungeSessionsOlderThanDate(dateBefore); |
| if (logger.isDebugEnabled()) { |
| logger.debug(sessionsExpunged > 0 ? |
| String.format("Expunged %s removed console session records", sessionsExpunged) : |
| "No removed console session records expunged on this cleanup task run"); |
| } |
| } |
| } |
| |
| @Override |
| public ConsoleEndpoint generateConsoleEndpoint(Long vmId, String extraSecurityToken, String clientAddress) { |
| try { |
| if (ObjectUtils.anyNull(accountManager, virtualMachineManager, managementServer)) { |
| return new ConsoleEndpoint(false, null, "Console service is not ready"); |
| } |
| |
| if (keysManager.getHashKey() == null) { |
| String msg = "Console access denied. Ticket service is not ready yet"; |
| logger.debug(msg); |
| return new ConsoleEndpoint(false, null, msg); |
| } |
| |
| Account account = CallContext.current().getCallingAccount(); |
| |
| // Do a sanity check here to make sure the user hasn't already been deleted |
| if (account == null) { |
| logger.debug("Invalid user/account, reject console access"); |
| return new ConsoleEndpoint(false, null,"Access denied. Invalid or inconsistent account is found"); |
| } |
| |
| VirtualMachine vm = entityManager.findById(VirtualMachine.class, vmId); |
| if (vm == null) { |
| logger.info("Invalid console servlet command parameter: " + vmId); |
| return new ConsoleEndpoint(false, null, "Cannot find VM with ID " + vmId); |
| } |
| |
| if (!checkSessionPermission(vm, account)) { |
| return new ConsoleEndpoint(false, null, "Permission denied"); |
| } |
| |
| DataCenter zone = dataCenterDao.findById(vm.getDataCenterId()); |
| if (zone != null && DataCenter.Type.Edge.equals(zone.getType())) { |
| String errorMsg = "Console access is not supported for Edge zones"; |
| logger.error(errorMsg); |
| return new ConsoleEndpoint(false, null, errorMsg); |
| } |
| |
| String sessionUuid = UUID.randomUUID().toString(); |
| return generateAccessEndpoint(vmId, sessionUuid, extraSecurityToken, clientAddress); |
| } catch (Exception e) { |
| String errorMsg = String.format("Unexepected exception in ConsoleAccessManager - vmId: %s, clientAddress: %s", |
| vmId, clientAddress); |
| logger.error(errorMsg, e); |
| return new ConsoleEndpoint(false, null, "Server Internal Error: " + e.getMessage()); |
| } |
| } |
| |
| @Override |
| public boolean isSessionAllowed(String sessionUuid) { |
| return consoleSessionDao.isSessionAllowed(sessionUuid); |
| } |
| |
| @Override |
| public void removeSessions(String[] sessionUuids) { |
| if (ArrayUtils.isNotEmpty(sessionUuids)) { |
| for (String sessionUuid : sessionUuids) { |
| removeSession(sessionUuid); |
| } |
| } |
| } |
| |
| protected void removeSession(String sessionUuid) { |
| consoleSessionDao.removeSession(sessionUuid); |
| } |
| |
| @Override |
| public void acquireSession(String sessionUuid) { |
| consoleSessionDao.acquireSession(sessionUuid); |
| } |
| |
| protected boolean checkSessionPermission(VirtualMachine vm, Account account) { |
| if (accountManager.isRootAdmin(account.getId())) { |
| return true; |
| } |
| |
| switch (vm.getType()) { |
| case User: |
| try { |
| accountManager.checkAccess(account, null, true, vm); |
| } catch (PermissionDeniedException ex) { |
| if (accountManager.isNormalUser(account.getId())) { |
| if (logger.isDebugEnabled()) { |
| logger.debug("VM access is denied for VM ID " + vm.getUuid() + ". VM owner account " + |
| vm.getAccountId() + " does not match the account id in session " + |
| account.getId() + " and caller is a normal user"); |
| } |
| } else if ((accountManager.isDomainAdmin(account.getId()) |
| || account.getType() == Account.Type.READ_ONLY_ADMIN) && logger.isDebugEnabled()) { |
| logger.debug("VM access is denied for VM ID " + vm.getUuid() + ". VM owner account " + |
| vm.getAccountId() + " does not match the account id in session " + |
| account.getId() + " and the domain-admin caller does not manage the target domain"); |
| } |
| return false; |
| } |
| break; |
| |
| case DomainRouter: |
| case ConsoleProxy: |
| case SecondaryStorageVm: |
| return false; |
| |
| default: |
| logger.warn("Unrecoginized virtual machine type, deny access by default. type: " + vm.getType()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private ConsoleEndpoint generateAccessEndpoint(Long vmId, String sessionUuid, String extraSecurityToken, String clientAddress) { |
| VirtualMachine vm = virtualMachineManager.findById(vmId); |
| String msg; |
| if (vm == null) { |
| msg = "VM " + vmId + " does not exist, sending blank response for console access request"; |
| logger.warn(msg); |
| throw new CloudRuntimeException(msg); |
| } |
| |
| String vmUuid = vm.getUuid(); |
| if (unsupportedConsoleVMState.contains(vm.getState())) { |
| msg = "VM " + vmUuid + " must be running to connect console, sending blank response for console access request"; |
| logger.warn(msg); |
| throw new CloudRuntimeException(msg); |
| } |
| |
| Long hostId = vm.getState() != VirtualMachine.State.Migrating ? vm.getHostId() : vm.getLastHostId(); |
| if (hostId == null) { |
| msg = "VM " + vmUuid + " lost host info, sending blank response for console access request"; |
| logger.warn(msg); |
| throw new CloudRuntimeException(msg); |
| } |
| |
| HostVO host = managementServer.getHostBy(hostId); |
| if (host == null) { |
| msg = "VM " + vmUuid + "'s host does not exist, sending blank response for console access request"; |
| logger.warn(msg); |
| throw new CloudRuntimeException(msg); |
| } |
| |
| if (Hypervisor.HypervisorType.LXC.equals(vm.getHypervisorType())) { |
| throw new CloudRuntimeException("Console access is not supported for LXC"); |
| } |
| |
| String rootUrl = managementServer.getConsoleAccessUrlRoot(vmId); |
| if (rootUrl == null) { |
| throw new CloudRuntimeException("Console access will be ready in a few minutes. Please try it again later."); |
| } |
| |
| ConsoleEndpoint consoleEndpoint = composeConsoleAccessEndpoint(rootUrl, vm, host, clientAddress, sessionUuid, extraSecurityToken); |
| logger.debug("The console URL is: " + consoleEndpoint.getUrl()); |
| return consoleEndpoint; |
| } |
| |
| private ConsoleEndpoint composeConsoleAccessEndpoint(String rootUrl, VirtualMachine vm, HostVO hostVo, String addr, |
| String sessionUuid, String extraSecurityToken) { |
| String host = hostVo.getPrivateIpAddress(); |
| |
| Pair<String, Integer> portInfo = null; |
| if (hostVo.getHypervisorType() == Hypervisor.HypervisorType.KVM && |
| (hostVo.getResourceState().equals(ResourceState.ErrorInMaintenance) || |
| hostVo.getResourceState().equals(ResourceState.ErrorInPrepareForMaintenance))) { |
| UserVmDetailVO detailAddress = userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_ADDRESS); |
| UserVmDetailVO detailPort = userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_PORT); |
| if (detailAddress != null && detailPort != null) { |
| portInfo = new Pair<>(detailAddress.getValue(), Integer.valueOf(detailPort.getValue())); |
| } else { |
| logger.warn("KVM Host in ErrorInMaintenance/ErrorInPrepareForMaintenance but " + |
| "no VNC Address/Port was available. Falling back to default one from MS."); |
| } |
| } |
| |
| if (portInfo == null) { |
| portInfo = managementServer.getVncPort(vm); |
| } |
| |
| if (logger.isDebugEnabled()) |
| logger.debug("Port info " + portInfo.first()); |
| |
| Ternary<String, String, String> parsedHostInfo = parseHostInfo(portInfo.first()); |
| |
| int port = -1; |
| if (portInfo.second() == -9) { |
| //for hyperv |
| port = Integer.parseInt(managementServer.findDetail(hostVo.getId(), "rdp.server.port").getValue()); |
| } else { |
| port = portInfo.second(); |
| } |
| |
| String sid = vm.getVncPassword(); |
| UserVmDetailVO details = userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KEYBOARD); |
| |
| String tag = vm.getUuid(); |
| String displayName = vm.getHostName(); |
| if (vm instanceof UserVm) { |
| displayName = ((UserVm) vm).getDisplayName(); |
| } |
| |
| String ticket = genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag, sessionUuid); |
| ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword()); |
| ConsoleProxyClientParam param = generateConsoleProxyClientParam(parsedHostInfo, port, sid, tag, ticket, |
| sessionUuid, addr, extraSecurityToken, vm, hostVo, details, portInfo, host, displayName); |
| String token = encryptor.encryptObject(ConsoleProxyClientParam.class, param); |
| int vncPort = consoleProxyManager.getVncPort(); |
| |
| String url = generateConsoleAccessUrl(rootUrl, param, token, vncPort, vm, hostVo, details); |
| |
| logger.debug("Adding allowed session: " + sessionUuid); |
| persistConsoleSession(sessionUuid, vm.getId(), hostVo.getId()); |
| managementServer.setConsoleAccessForVm(vm.getId(), sessionUuid); |
| |
| ConsoleEndpoint consoleEndpoint = new ConsoleEndpoint(true, url); |
| consoleEndpoint.setWebsocketHost(managementServer.getConsoleAccessAddress(vm.getId())); |
| consoleEndpoint.setWebsocketPort(String.valueOf(vncPort)); |
| consoleEndpoint.setWebsocketPath("websockify"); |
| consoleEndpoint.setWebsocketToken(token); |
| if (StringUtils.isNotBlank(param.getExtraSecurityToken())) { |
| consoleEndpoint.setWebsocketExtra(param.getExtraSecurityToken()); |
| } |
| return consoleEndpoint; |
| } |
| |
| protected void persistConsoleSession(String sessionUuid, long instanceId, long hostId) { |
| ConsoleSessionVO consoleSessionVo = new ConsoleSessionVO(); |
| consoleSessionVo.setUuid(sessionUuid); |
| consoleSessionVo.setAccountId(CallContext.current().getCallingAccountId()); |
| consoleSessionVo.setUserId(CallContext.current().getCallingUserId()); |
| consoleSessionVo.setInstanceId(instanceId); |
| consoleSessionVo.setHostId(hostId); |
| consoleSessionDao.persist(consoleSessionVo); |
| } |
| |
| private String generateConsoleAccessUrl(String rootUrl, ConsoleProxyClientParam param, String token, int vncPort, |
| VirtualMachine vm, HostVO hostVo, UserVmDetailVO details) { |
| StringBuilder sb = new StringBuilder(rootUrl); |
| if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) { |
| sb.append("/ajax?token=" + token); |
| } else { |
| sb.append("/resource/noVNC/vnc.html") |
| .append("?autoconnect=true") |
| .append("&port=" + vncPort) |
| .append("&token=" + token); |
| if (requiresVncOverWebSocketConnection(vm, hostVo) && details != null && details.getValue() != null) { |
| sb.append("&language=" + details.getValue()); |
| } |
| } |
| |
| if (StringUtils.isNotBlank(param.getExtraSecurityToken())) { |
| sb.append("&extra=" + param.getExtraSecurityToken()); |
| } |
| |
| // for console access, we need guest OS type to help implement keyboard |
| long guestOs = vm.getGuestOSId(); |
| GuestOSVO guestOsVo = managementServer.getGuestOs(guestOs); |
| if (guestOsVo.getCategoryId() == 6) |
| sb.append("&guest=windows"); |
| |
| if (logger.isDebugEnabled()) { |
| logger.debug("Compose console url: " + sb); |
| } |
| return sb.toString().startsWith("https") ? sb.toString() : "http:" + sb; |
| } |
| |
| private ConsoleProxyClientParam generateConsoleProxyClientParam(Ternary<String, String, String> parsedHostInfo, |
| int port, String sid, String tag, String ticket, |
| String sessionUuid, String addr, |
| String extraSecurityToken, VirtualMachine vm, |
| HostVO hostVo, UserVmDetailVO details, |
| Pair<String, Integer> portInfo, String host, |
| String displayName) { |
| ConsoleProxyClientParam param = new ConsoleProxyClientParam(); |
| param.setClientHostAddress(parsedHostInfo.first()); |
| param.setClientHostPort(port); |
| param.setClientHostPassword(sid); |
| param.setClientTag(tag); |
| param.setClientDisplayName(displayName); |
| param.setTicket(ticket); |
| param.setSessionUuid(sessionUuid); |
| param.setSourceIP(addr); |
| |
| if (StringUtils.isNotBlank(extraSecurityToken)) { |
| param.setExtraSecurityToken(extraSecurityToken); |
| logger.debug("Added security token for client validation"); |
| } |
| |
| if (requiresVncOverWebSocketConnection(vm, hostVo)) { |
| setWebsocketUrl(vm, param); |
| } |
| |
| if (details != null) { |
| param.setLocale(details.getValue()); |
| } |
| |
| if (portInfo.second() == -9) { |
| //For Hyperv Clinet Host Address will send Instance id |
| param.setHypervHost(host); |
| param.setUsername(managementServer.findDetail(hostVo.getId(), "username").getValue()); |
| param.setPassword(managementServer.findDetail(hostVo.getId(), "password").getValue()); |
| } |
| if (parsedHostInfo.second() != null && parsedHostInfo.third() != null) { |
| param.setClientTunnelUrl(parsedHostInfo.second()); |
| param.setClientTunnelSession(parsedHostInfo.third()); |
| } |
| return param; |
| } |
| |
| public Ternary<String, String, String> parseHostInfo(String hostInfo) { |
| String host = null; |
| String tunnelUrl = null; |
| String tunnelSession = null; |
| |
| logger.info("Parse host info returned from executing GetVNCPortCommand. host info: " + hostInfo); |
| |
| if (hostInfo != null) { |
| if (hostInfo.startsWith("consoleurl")) { |
| String[] tokens = hostInfo.split("&"); |
| |
| if (hostInfo.length() > 19 && hostInfo.indexOf('/', 19) > 19) { |
| host = hostInfo.substring(19, hostInfo.indexOf('/', 19)).trim(); |
| tunnelUrl = tokens[0].substring("consoleurl=".length()); |
| tunnelSession = tokens[1].split("=")[1]; |
| } else { |
| host = ""; |
| } |
| } else if (hostInfo.startsWith("instanceId")) { |
| host = hostInfo.substring(hostInfo.indexOf('=') + 1); |
| } else { |
| host = hostInfo; |
| } |
| } else { |
| host = hostInfo; |
| } |
| |
| return new Ternary<>(host, tunnelUrl, tunnelSession); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| @Override |
| public String genAccessTicket(String host, String port, String sid, String tag, String sessionUuid) { |
| return genAccessTicket(host, port, sid, tag, new Date(), sessionUuid); |
| } |
| |
| @Override |
| public String genAccessTicket(String host, String port, String sid, String tag, Date normalizedHashTime, String sessionUuid) { |
| String params = "host=" + host + "&port=" + port + "&sid=" + sid + "&tag=" + tag + "&session=" + sessionUuid; |
| |
| try { |
| Mac mac = Mac.getInstance("HmacSHA512"); |
| |
| long ts = normalizedHashTime.getTime(); |
| ts = ts / 60000; // round up to 1 minute |
| String secretKey = secretKeysManager.getHashKey(); |
| |
| SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), mac.getAlgorithm()); |
| mac.init(keySpec); |
| mac.update(params.getBytes()); |
| mac.update(String.valueOf(ts).getBytes()); |
| |
| byte[] encryptedBytes = mac.doFinal(); |
| |
| return Base64.encodeBase64String(encryptedBytes); |
| } catch (Exception e) { |
| logger.error("Unexpected exception ", e); |
| } |
| return ""; |
| } |
| |
| private String getEncryptorPassword() { |
| String key = keysManager.getEncryptionKey(); |
| String iv = keysManager.getEncryptionIV(); |
| |
| ConsoleProxyPasswordBasedEncryptor.KeyIVPair keyIvPair = new ConsoleProxyPasswordBasedEncryptor.KeyIVPair(key, iv); |
| return gson.toJson(keyIvPair); |
| } |
| |
| /** |
| * 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)) { |
| 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 { |
| 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()) { |
| logger.info("VNC ticket could not be acquired correctly: " + ans.getDetails()); |
| } |
| return ans.getTicket(); |
| } catch (AgentUnavailableException | OperationTimedoutException e) { |
| logger.error("Error acquiring ticket", e); |
| return null; |
| } |
| } |
| |
| } |