extension/proxmox: add console access for instances (#11601)

This PR introduces console access support for instances deployed using Orchestrator Extensions, available via either VNC or a direct URL.

- CloudStack queries the extension using the getconsole action.
- For VNC-based access, the extension must return host/port/ticket details. CloudStack then forwards these to the Console Proxy VM (CPVM) in the instance’s zone. It is assumed that the CPVM can reach the specified host and port.
- For direct URL access, the extension returns a console URL with the protocol set to `direct`. The URL is then provided directly to the user.
- The built-in Proxmox Orchestrator Extension now supports console access via VNC. The extension calls the Proxmox API to fetch console details and returns them in the required format.

Also, adds changes to send caller details to the extension payload.
```
# cat /var/lib/cloudstack/management/extensions/Proxmox/02b650f6-bb98-49cb-8cac-82b7a78f43a2.json | jq
{
  "caller": {
    "roleid": "6b86674b-7e61-11f0-ba77-1e00c8000158",
    "rolename": "Root Admin",
    "name": "admin",
    "roletype": "Admin",
    "id": "93567ed9-7e61-11f0-ba77-1e00c8000158",
    "type": "ADMIN"
  },
  "virtualmachineid": "126f4562-1f0f-4313-875e-6150cabeb72f",
  ...
```

Documentation PR: https://github.com/apache/cloudstack-documentation/pull/560

---------

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2025-09-27 08:54:27 +05:30 committed by GitHub
parent ec533cd24d
commit 928972f767
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1714 additions and 234 deletions

View File

@ -80,6 +80,7 @@ public class ApiConstants {
public static final String BYTES_WRITE_RATE_MAX = "byteswriteratemax";
public static final String BYTES_WRITE_RATE_MAX_LENGTH = "byteswriteratemaxlength";
public static final String BYPASS_VLAN_OVERLAP_CHECK = "bypassvlanoverlapcheck";
public static final String CALLER = "caller";
public static final String CAPACITY = "capacity";
public static final String CATEGORY = "category";
public static final String CAN_REVERT = "canrevert";

View File

@ -35,6 +35,7 @@ import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ObjectUtils;
import javax.inject.Inject;
import java.util.Map;
@ -86,6 +87,10 @@ public class CreateConsoleEndpointCmd extends BaseCmd {
}
private ConsoleEndpointWebsocketResponse createWebsocketResponse(ConsoleEndpoint endpoint) {
if (ObjectUtils.allNull(endpoint.getWebsocketHost(), endpoint.getWebsocketPort(), endpoint.getWebsocketPath(),
endpoint.getWebsocketToken(), endpoint.getWebsocketExtra())) {
return null;
}
ConsoleEndpointWebsocketResponse wsResponse = new ConsoleEndpointWebsocketResponse();
wsResponse.setHost(endpoint.getWebsocketHost());
wsResponse.setPort(endpoint.getWebsocketPort());

View File

@ -0,0 +1,68 @@
// 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 GetExternalConsoleAnswer extends Answer {
private String url;
private String host;
private Integer port;
@LogLevel(LogLevel.Log4jLevel.Off)
private String password;
private String protocol;
private boolean passwordOneTimeUseOnly;
public GetExternalConsoleAnswer(Command command, String details) {
super(command, false, details);
}
public GetExternalConsoleAnswer(Command command, String url, String host, Integer port, String password,
boolean passwordOneTimeUseOnly, String protocol) {
super(command, true, "");
this.url = url;
this.host = host;
this.port = port;
this.password = password;
this.passwordOneTimeUseOnly = passwordOneTimeUseOnly;
this.protocol = protocol;
}
public String getUrl() {
return url;
}
public String getHost() {
return host;
}
public Integer getPort() {
return port;
}
public String getPassword() {
return password;
}
public String getProtocol() {
return protocol;
}
public boolean isPasswordOneTimeUseOnly() {
return passwordOneTimeUseOnly;
}
}

View File

@ -0,0 +1,53 @@
// 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;
import com.cloud.agent.api.to.VirtualMachineTO;
public class GetExternalConsoleCommand extends Command {
String vmName;
VirtualMachineTO vm;
protected boolean executeInSequence;
public GetExternalConsoleCommand(String vmName, VirtualMachineTO vm) {
this.vmName = vmName;
this.vm = vm;
this.executeInSequence = false;
}
public String getVmName() {
return this.vmName;
}
public void setVirtualMachine(VirtualMachineTO vm) {
this.vm = vm;
}
public VirtualMachineTO getVirtualMachine() {
return vm;
}
@Override
public boolean executeInSequence() {
return executeInSequence;
}
public void setExecuteInSequence(boolean executeInSequence) {
this.executeInSequence = executeInSequence;
}
}

View File

@ -21,10 +21,12 @@ package com.cloud.agent.api;
import java.util.Map;
import com.cloud.agent.api.to.VirtualMachineTO;
public class RunCustomActionCommand extends Command {
String actionName;
Long vmId;
VirtualMachineTO vmTO;
Map<String, Object> parameters;
public RunCustomActionCommand(String actionName) {
@ -36,12 +38,12 @@ public class RunCustomActionCommand extends Command {
return actionName;
}
public Long getVmId() {
return vmId;
public VirtualMachineTO getVmTO() {
return vmTO;
}
public void setVmId(Long vmId) {
this.vmId = vmId;
public void setVmTO(VirtualMachineTO vmTO) {
this.vmTO = vmTO;
}
public Map<String, Object> getParameters() {

View File

@ -18,6 +18,8 @@ package com.cloud.hypervisor;
import java.util.Map;
import com.cloud.agent.api.GetExternalConsoleAnswer;
import com.cloud.agent.api.GetExternalConsoleCommand;
import com.cloud.agent.api.HostVmStateReportEntry;
import com.cloud.agent.api.PrepareExternalProvisioningAnswer;
import com.cloud.agent.api.PrepareExternalProvisioningCommand;
@ -57,5 +59,7 @@ public interface ExternalProvisioner extends Manager {
Map<String, HostVmStateReportEntry> getHostVmStateReport(long hostId, String extensionName, String extensionRelativePath);
GetExternalConsoleAnswer getInstanceConsole(String hostGuid, String extensionName, String extensionRelativePath, GetExternalConsoleCommand cmd);
RunCustomActionAnswer runCustomAction(String hostGuid, String extensionName, String extensionRelativePath, RunCustomActionCommand cmd);
}

View File

@ -25,7 +25,7 @@ import winrm
def fail(message):
print(json.dumps({"error": message}))
print(json.dumps({"status": "error", "error": message}))
sys.exit(1)
@ -220,6 +220,9 @@ class HyperVManager:
fail(str(e))
succeed({"status": "success", "message": "Instance deleted"})
def get_console(self):
fail("Operation not supported")
def suspend(self):
self.run_ps(f'Suspend-VM -Name "{self.data["vmname"]}"')
succeed({"status": "success", "message": "Instance suspended"})
@ -283,6 +286,7 @@ def main():
"reboot": manager.reboot,
"delete": manager.delete,
"status": manager.status,
"getconsole": manager.get_console,
"suspend": manager.suspend,
"resume": manager.resume,
"listsnapshots": manager.list_snapshots,

View File

@ -18,7 +18,7 @@
parse_json() {
local json_string="$1"
echo "$json_string" | jq '.' > /dev/null || { echo '{"error":"Invalid JSON input"}'; exit 1; }
echo "$json_string" | jq '.' > /dev/null || { echo '{"status": "error", "error": "Invalid JSON input"}'; exit 1; }
local -A details
while IFS="=" read -r key value; do
@ -112,9 +112,14 @@ call_proxmox_api() {
curl_opts+=(-d "$data")
fi
#echo curl "${curl_opts[@]}" "https://${url}:8006/api2/json${path}" >&2
response=$(curl "${curl_opts[@]}" "https://${url}:8006/api2/json${path}")
local status=$?
if [[ $status -ne 0 ]]; then
echo "{\"errors\":{\"curl\":\"API call failed with status $status: $(echo "$response" | jq -Rsa . | jq -r .)\"}}"
return $status
fi
echo "$response"
return 0
}
wait_for_proxmox_task() {
@ -129,7 +134,7 @@ wait_for_proxmox_task() {
local now
now=$(date +%s)
if (( now - start_time > timeout )); then
echo '{"error":"Timeout while waiting for async task"}'
echo '{"status": "error", "error":"Timeout while waiting for async task"}'
exit 1
fi
@ -139,7 +144,7 @@ wait_for_proxmox_task() {
if [[ -z "$status_response" || "$status_response" == *'"errors":'* ]]; then
local msg
msg=$(echo "$status_response" | jq -r '.message // "Unknown error"')
echo "{\"error\":\"$msg\"}"
echo "{\"status\": \"error\", \"error\": \"$msg\"}"
exit 1
fi
@ -285,6 +290,86 @@ status() {
echo "{\"status\": \"success\", \"power_state\": \"$powerstate\"}"
}
get_node_host() {
check_required_fields node
local net_json host
if ! net_json="$(call_proxmox_api GET "/nodes/${node}/network")"; then
echo ""
return 1
fi
# Prefer a static non-bridge IP
host="$(echo "$net_json" | jq -r '
.data
| map(select(
(.type // "") != "bridge" and
(.type // "") != "bond" and
(.method // "") == "static" and
((.address // .cidr // "") != "")
))
| map(.address // (.cidr | split("/")[0]))
| .[0] // empty
' 2>/dev/null)"
# Fallback: first interface with a CIDR
if [[ -z "$host" ]]; then
host="$(echo "$net_json" | jq -r '
.data
| map(select((.cidr // "") != ""))
| map(.cidr | split("/")[0])
| .[0] // empty
' 2>/dev/null)"
fi
echo "$host"
}
get_console() {
check_required_fields node vmid
local api_resp port ticket
if ! api_resp="$(call_proxmox_api POST "/nodes/${node}/qemu/${vmid}/vncproxy")"; then
echo "$api_resp" | jq -c '{status:"error", error:(.errors.curl // (.errors|tostring))}'
exit 1
fi
port="$(echo "$api_resp" | jq -re '.data.port // empty' 2>/dev/null || true)"
ticket="$(echo "$api_resp" | jq -re '.data.ticket // empty' 2>/dev/null || true)"
if [[ -z "$port" || -z "$ticket" ]]; then
jq -n --arg raw "$api_resp" \
'{status:"error", error:"Proxmox response missing port/ticket", upstream:$raw}'
exit 1
fi
# Derive host from nodes network info
local host
host="$(get_node_host)"
if [[ -z "$host" ]]; then
jq -n --arg msg "Could not determine host IP for node $node" \
'{status:"error", error:$msg}'
exit 1
fi
jq -n \
--arg host "$host" \
--arg port "$port" \
--arg password "$ticket" \
--argjson passwordonetimeuseonly true \
'{
status: "success",
message: "Console retrieved",
console: {
host: $host,
port: $port,
password: $password,
passwordonetimeuseonly: $passwordonetimeuseonly,
protocol: "vnc"
}
}'
}
list_snapshots() {
snapshot_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/snapshot")
echo "$snapshot_response" | jq '
@ -356,7 +441,12 @@ parameters_file="$2"
wait_time=$3
if [[ -z "$action" || -z "$parameters_file" ]]; then
echo '{"error":"Missing required arguments"}'
echo '{"status": "error", "error": "Missing required arguments"}'
exit 1
fi
if [[ ! -r "$parameters_file" ]]; then
echo '{"status": "error", "error": "File not found or unreadable"}'
exit 1
fi
@ -396,6 +486,9 @@ case $action in
status)
status
;;
getconsole)
get_console
;;
ListSnapshots)
list_snapshots
;;
@ -409,7 +502,7 @@ case $action in
delete_snapshot
;;
*)
echo '{"error":"Invalid action"}'
echo '{"status": "error", "error": "Invalid action"}'
exit 1
;;
esac

View File

@ -44,10 +44,12 @@ import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd;
import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd;
import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand;
import com.cloud.agent.api.Answer;
import com.cloud.host.Host;
import com.cloud.org.Cluster;
import com.cloud.utils.Pair;
import com.cloud.utils.component.Manager;
import com.cloud.vm.VirtualMachine;
public interface ExtensionsManager extends Manager {
@ -93,4 +95,6 @@ public interface ExtensionsManager extends Manager {
final ExtensionResourceMap.ResourceType resourceType, final Map<String, String> details);
void updateExtensionResourceMapDetails(final long extensionResourceMapId, final Map<String, String> details);
Answer getInstanceConsole(VirtualMachine vm, Host host);
}

View File

@ -104,8 +104,10 @@ import org.apache.commons.lang3.StringUtils;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.GetExternalConsoleCommand;
import com.cloud.agent.api.RunCustomActionAnswer;
import com.cloud.agent.api.RunCustomActionCommand;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.alert.AlertManager;
import com.cloud.cluster.ClusterManager;
import com.cloud.cluster.ManagementServerHostVO;
@ -141,6 +143,8 @@ import com.cloud.utils.db.TransactionCallbackWithException;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VirtualMachineProfileImpl;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.VMInstanceDao;
@ -472,6 +476,29 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
executorService.shutdown();
}
protected Map<String, String> getCallerDetails() {
Account caller = CallContext.current().getCallingAccount();
if (caller == null) {
return null;
}
Map<String, String> callerDetails = new HashMap<>();
callerDetails.put(ApiConstants.ID, caller.getUuid());
callerDetails.put(ApiConstants.NAME, caller.getAccountName());
if (caller.getType() != null) {
callerDetails.put(ApiConstants.TYPE, caller.getType().name());
}
Role role = roleService.findRole(caller.getRoleId());
if (role == null) {
return callerDetails;
}
callerDetails.put(ApiConstants.ROLE_ID, role.getUuid());
callerDetails.put(ApiConstants.ROLE_NAME, role.getName());
if (role.getRoleType() != null) {
callerDetails.put(ApiConstants.ROLE_TYPE, role.getRoleType().name());
}
return callerDetails;
}
protected Map<String, Map<String, String>> getExternalAccessDetails(Map<String, String> actionDetails, long hostId,
ExtensionResourceMap resourceMap) {
Map<String, Map<String, String>> externalDetails = new HashMap<>();
@ -493,6 +520,10 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
if (MapUtils.isNotEmpty(extensionDetails)) {
externalDetails.put(ApiConstants.EXTENSION, extensionDetails);
}
Map<String, String> callerDetails = getCallerDetails();
if (MapUtils.isNotEmpty(callerDetails)) {
externalDetails.put(ApiConstants.CALLER, callerDetails);
}
return externalDetails;
}
@ -1323,11 +1354,13 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
clusterId = host.getClusterId();
} else if (entity instanceof VirtualMachine) {
VirtualMachine virtualMachine = (VirtualMachine)entity;
runCustomActionCommand.setVmId(virtualMachine.getId());
if (!Hypervisor.HypervisorType.External.equals(virtualMachine.getHypervisorType())) {
logger.error("Invalid {} specified as VM resource for running {}", entity, customActionVO);
throw new InvalidParameterValueException(error);
}
VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(virtualMachine);
VirtualMachineTO virtualMachineTO = virtualMachineManager.toVmTO(vmProfile);
runCustomActionCommand.setVmTO(virtualMachineTO);
Pair<Long, Long> clusterAndHostId = virtualMachineManager.findClusterAndHostIdForVm(virtualMachine, false);
clusterId = clusterAndHostId.first();
hostId = clusterAndHostId.second();
@ -1369,6 +1402,13 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
actionResourceType, entity));
Map<String, Map<String, String>> externalDetails =
getExternalAccessDetails(allDetails.first(), hostId, extensionResource);
Map<String, String> vmExternalDetails = null;
if (runCustomActionCommand.getVmTO() != null) {
vmExternalDetails = runCustomActionCommand.getVmTO().getExternalDetails();
}
if (MapUtils.isNotEmpty(vmExternalDetails)) {
externalDetails.put(ApiConstants.VIRTUAL_MACHINE, vmExternalDetails);
}
runCustomActionCommand.setParameters(parameters);
runCustomActionCommand.setExternalDetails(externalDetails);
runCustomActionCommand.setWait(customActionVO.getTimeout());
@ -1517,6 +1557,25 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
extensionResourceMapDetailsDao.saveDetails(detailsList);
}
@Override
public Answer getInstanceConsole(VirtualMachine vm, Host host) {
Extension extension = getExtensionForCluster(host.getClusterId());
if (extension == null || !Extension.Type.Orchestrator.equals(extension.getType()) ||
!Extension.State.Enabled.equals(extension.getState())) {
logger.error("No enabled orchestrator {} found for the {} while trying to get console for {}",
extension == null ? "extension" : extension, host, vm);
return new Answer(null, false,
String.format("No enabled orchestrator extension found for the host: %s", host.getName()));
}
VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(vm);
VirtualMachineTO virtualMachineTO = virtualMachineManager.toVmTO(vmProfile);
GetExternalConsoleCommand cmd = new GetExternalConsoleCommand(vm.getInstanceName(), virtualMachineTO);
Map<String, Map<String, String>> externalAccessDetails =
getExternalAccessDetails(host, virtualMachineTO.getExternalDetails());
cmd.setExternalDetails(externalAccessDetails);
return agentMgr.easySend(host.getId(), cmd);
}
@Override
public Long getExtensionIdForCluster(long clusterId) {
ExtensionResourceMapVO map = extensionResourceMapDao.findByResourceIdAndType(clusterId,

View File

@ -92,7 +92,6 @@ import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
@ -102,6 +101,7 @@ import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.RunCustomActionAnswer;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.alert.AlertManager;
import com.cloud.cluster.ClusterManager;
import com.cloud.cluster.ManagementServerHostVO;
@ -111,6 +111,7 @@ import com.cloud.dc.dao.ClusterDao;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.Host;
import com.cloud.host.dao.HostDao;
import com.cloud.host.dao.HostDetailsDao;
import com.cloud.hypervisor.ExternalProvisioner;
@ -120,6 +121,7 @@ import com.cloud.serializer.GsonHelper;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.user.Account;
import com.cloud.utils.Pair;
import com.cloud.utils.UuidUtils;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@ -524,11 +526,15 @@ public class ExtensionsManagerImplTest {
when(hostDetailsDao.findDetails(hostId)).thenReturn(null);
when(extensionResourceMapDetailsDao.listDetailsKeyPairs(2L, true)).thenReturn(Collections.emptyMap());
when(extensionDetailsDao.listDetailsKeyPairs(3L, true)).thenReturn(map);
Map<String, Map<String, String>> result = extensionsManager.getExternalAccessDetails(map, hostId, resourceMap);
assertTrue(result.containsKey(ApiConstants.ACTION));
assertFalse(result.containsKey(ApiConstants.HOST));
assertFalse(result.containsKey(ApiConstants.RESOURCE_MAP));
assertTrue(result.containsKey(ApiConstants.EXTENSION));
try (MockedStatic<CallContext> ignored = mockStatic(CallContext.class)) {
mockCallerRole(RoleType.Admin);
Map<String, Map<String, String>> result = extensionsManager.getExternalAccessDetails(map, hostId, resourceMap);
assertTrue(result.containsKey(ApiConstants.ACTION));
assertFalse(result.containsKey(ApiConstants.HOST));
assertFalse(result.containsKey(ApiConstants.RESOURCE_MAP));
assertTrue(result.containsKey(ApiConstants.EXTENSION));
assertTrue(result.containsKey(ApiConstants.CALLER));
}
}
@Test(expected = CloudRuntimeException.class)
@ -1281,12 +1287,17 @@ public class ExtensionsManagerImplTest {
}
private void mockCallerRole(RoleType roleType) {
CallContext callContextMock = Mockito.mock(CallContext.class);
CallContext callContextMock = mock(CallContext.class);
when(CallContext.current()).thenReturn(callContextMock);
Account accountMock = mock(Account.class);
when(accountMock.getAccountName()).thenReturn("testAccount");
when(accountMock.getUuid()).thenReturn(UUID.randomUUID().toString());
when(accountMock.getType()).thenReturn(RoleType.Admin.equals(roleType) ? Account.Type.ADMIN : Account.Type.NORMAL);
when(accountMock.getRoleId()).thenReturn(1L);
Role role = mock(Role.class);
when(role.getRoleType()).thenReturn(roleType);
when(role.getUuid()).thenReturn("role-uuid-1");
when(role.getName()).thenReturn(roleType.name() + "Role");
when(roleService.findRole(1L)).thenReturn(role);
when(callContextMock.getCallingAccount()).thenReturn(accountMock);
}
@ -1882,4 +1893,147 @@ public class ExtensionsManagerImplTest {
Extension result = extensionsManager.getExtensionForCluster(clusterId);
assertNull(result);
}
@Test
public void getInstanceConsole_whenValid() {
Extension extension = mock(Extension.class);
when(extension.getType()).thenReturn(Extension.Type.Orchestrator);
when(extension.getState()).thenReturn(Extension.State.Enabled);
when(extensionsManager.getExtensionForCluster(anyLong())).thenReturn(extension);
VirtualMachine vm = mock(VirtualMachine.class);
Host host = mock(Host.class);
when(host.getClusterId()).thenReturn(1L);
Answer expectedAnswer = mock(Answer.class);
when(virtualMachineManager.toVmTO(any())).thenReturn(mock(VirtualMachineTO.class));
when(agentMgr.easySend(anyLong(), any())).thenReturn(expectedAnswer);
Answer result = extensionsManager.getInstanceConsole(vm, host);
assertNotNull(result);
assertEquals(expectedAnswer, result);
}
@Test
public void getInstanceConsole_whenNullExtension() {
when(extensionsManager.getExtensionForCluster(anyLong())).thenReturn(null);
VirtualMachine vm = mock(VirtualMachine.class);
Host host = mock(Host.class);
when(host.getClusterId()).thenReturn(1L);
Answer result = extensionsManager.getInstanceConsole(vm, host);
assertNotNull(result);
assertFalse(result.getResult());
}
@Test
public void getInstanceConsole_whenNullExtensionNotOrchestrator() {
Extension extension = mock(Extension.class);
when(extensionsManager.getExtensionForCluster(anyLong())).thenReturn(extension);
VirtualMachine vm = mock(VirtualMachine.class);
Host host = mock(Host.class);
when(host.getClusterId()).thenReturn(1L);
Answer result = extensionsManager.getInstanceConsole(vm, host);
assertNotNull(result);
assertFalse(result.getResult());
}
@Test
public void getInstanceConsole_whenNullExtensionNotEnabled() {
Extension extension = mock(Extension.class);
when(extension.getType()).thenReturn(Extension.Type.Orchestrator);
when(extension.getState()).thenReturn(Extension.State.Disabled);
when(extensionsManager.getExtensionForCluster(anyLong())).thenReturn(extension);
VirtualMachine vm = mock(VirtualMachine.class);
Host host = mock(Host.class);
when(host.getClusterId()).thenReturn(1L);
Answer result = extensionsManager.getInstanceConsole(vm, host);
assertNotNull(result);
assertFalse(result.getResult());
}
@Test
public void getInstanceConsole_whenAgentManagerFails() {
Extension extension = mock(Extension.class);
when(extension.getType()).thenReturn(Extension.Type.Orchestrator);
when(extension.getState()).thenReturn(Extension.State.Enabled);
when(extensionsManager.getExtensionForCluster(anyLong())).thenReturn(extension);
VirtualMachine vm = mock(VirtualMachine.class);
Host host = mock(Host.class);
when(host.getClusterId()).thenReturn(1L);
when(virtualMachineManager.toVmTO(any())).thenReturn(mock(VirtualMachineTO.class));
when(agentMgr.easySend(anyLong(), any())).thenReturn(null);
Answer result = extensionsManager.getInstanceConsole(vm, host);
assertNull(result);
}
@Test
public void getExternalAccessDetailsReturnsExpectedDetails() {
Host host = mock(Host.class);
when(host.getId()).thenReturn(100L);
when(host.getClusterId()).thenReturn(1L);
Map<String, String> vmDetails = Map.of("key1", "value1", "key2", "value2");
ExtensionResourceMapVO resourceMapVO = mock(ExtensionResourceMapVO.class);
when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster))
.thenReturn(resourceMapVO);
doReturn(new HashMap<>()).when(extensionsManager).getExternalAccessDetails(null, 100L, resourceMapVO);
Map<String, Map<String, String>> result = extensionsManager.getExternalAccessDetails(host, vmDetails);
assertNotNull(result);
assertNotNull(result.get(ApiConstants.VIRTUAL_MACHINE));
assertEquals(vmDetails, result.get(ApiConstants.VIRTUAL_MACHINE));
}
@Test
public void getExternalAccessDetailsReturnsExpectedNullDetails() {
Host host = mock(Host.class);
when(host.getId()).thenReturn(101L);
when(host.getClusterId()).thenReturn(1L);
Map<String, String> vmDetails = null;
ExtensionResourceMapVO resourceMapVO = mock(ExtensionResourceMapVO.class);
when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster))
.thenReturn(resourceMapVO);
doReturn(new HashMap<>()).when(extensionsManager).getExternalAccessDetails(null, 101L, resourceMapVO);
Map<String, Map<String, String>> result = extensionsManager.getExternalAccessDetails(host, vmDetails);
assertNotNull(result);
assertNull(result.get(ApiConstants.VIRTUAL_MACHINE));
}
@Test
public void getCallerDetailsReturnsExpectedDetailsForValidCaller() {
try (MockedStatic<CallContext> ignored = mockStatic(CallContext.class)) {
mockCallerRole(RoleType.Admin);
Map<String, String> result = extensionsManager.getCallerDetails();
assertNotNull(result);
assertTrue(UuidUtils.isUuid(result.get(ApiConstants.ID)));
assertEquals("testAccount", result.get(ApiConstants.NAME));
assertEquals("ADMIN", result.get(ApiConstants.TYPE));
assertEquals("role-uuid-1", result.get(ApiConstants.ROLE_ID));
assertEquals("AdminRole", result.get(ApiConstants.ROLE_NAME));
assertEquals("Admin", result.get(ApiConstants.ROLE_TYPE));
}
}
@Test
public void getCallerDetailsReturnsNullWhenCallerIsNull() {
CallContext callContext = mock(CallContext.class);
when(callContext.getCallingAccount()).thenReturn(null);
try (MockedStatic<CallContext> mockedCallContext = mockStatic(CallContext.class)) {
mockedCallContext.when(CallContext::current).thenReturn(callContext);
Map<String, String> result = extensionsManager.getCallerDetails();
assertNull(result);
}
}
@Test
public void getCallerDetailsReturnsDetailsWithoutRoleWhenRoleIsNull() {
try (MockedStatic<CallContext> ignored = mockStatic(CallContext.class)) {
mockCallerRole(RoleType.User);
when(roleService.findRole(1L)).thenReturn(null);
Map<String, String> result = extensionsManager.getCallerDetails();
assertNotNull(result);
assertTrue(UuidUtils.isUuid(result.get(ApiConstants.ID)));
assertEquals("testAccount", result.get(ApiConstants.NAME));
assertEquals("NORMAL", result.get(ApiConstants.TYPE));
assertNull(result.get(ApiConstants.ROLE_ID));
assertNull(result.get(ApiConstants.ROLE_NAME));
assertNull(result.get(ApiConstants.ROLE_TYPE));
}
}
}

View File

@ -54,8 +54,11 @@ import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import org.apache.cloudstack.utils.security.DigestHelper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ObjectUtils;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.GetExternalConsoleAnswer;
import com.cloud.agent.api.GetExternalConsoleCommand;
import com.cloud.agent.api.HostVmStateReportEntry;
import com.cloud.agent.api.PrepareExternalProvisioningAnswer;
import com.cloud.agent.api.PrepareExternalProvisioningCommand;
@ -85,13 +88,11 @@ import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.json.JsonMergeUtil;
import com.cloud.utils.script.Script;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VirtualMachineProfileImpl;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@ -114,9 +115,6 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
@Inject
HostDao hostDao;
@Inject
VMInstanceDao vmInstanceDao;
@Inject
HypervisorGuruManager hypervisorGuruManager;
@ -140,6 +138,10 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
protected Map<String, Object> loadAccessDetails(Map<String, Map<String, String>> externalDetails,
VirtualMachineTO virtualMachineTO) {
Map<String, Object> modifiedDetails = new HashMap<>();
if (MapUtils.isNotEmpty(externalDetails) && externalDetails.containsKey(ApiConstants.CALLER)) {
modifiedDetails.put(ApiConstants.CALLER, externalDetails.get(ApiConstants.CALLER));
externalDetails.remove(ApiConstants.CALLER);
}
if (MapUtils.isNotEmpty(externalDetails)) {
modifiedDetails.put(ApiConstants.EXTERNAL_DETAILS, externalDetails);
}
@ -208,6 +210,22 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
logger.info("Extensions data directory path: {}", extensionsDataDirectory);
}
protected VirtualMachineTO getVirtualMachineTO(VirtualMachine vm) {
if (vm == null) {
return null;
}
final HypervisorGuru hvGuru = hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External);
VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm);
return hvGuru.implement(profile);
}
protected String getSanitizedJsonStringForLog(String json) {
if (StringUtils.isBlank(json)) {
return json;
}
return json.replaceAll("(\"password\"\\s*:\\s*\")([^\"]*)(\")", "$1****$3");
}
private String getServerProperty(String name) {
Properties props = propertiesRef.get();
if (props == null) {
@ -289,12 +307,20 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
}
}
protected String getExtensionConfigureError(String extensionName, String hostName) {
StringBuilder sb = new StringBuilder("Extension: ").append(extensionName).append(" not configured");
if (StringUtils.isNotBlank(hostName)) {
sb.append(" for host: ").append(hostName);
}
return sb.toString();
}
@Override
public PrepareExternalProvisioningAnswer prepareExternalProvisioning(String hostGuid,
public PrepareExternalProvisioningAnswer prepareExternalProvisioning(String hostName,
String extensionName, String extensionRelativePath, PrepareExternalProvisioningCommand cmd) {
String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath);
if (StringUtils.isEmpty(extensionPath)) {
return new PrepareExternalProvisioningAnswer(cmd, false, "Extension not configured");
return new PrepareExternalProvisioningAnswer(cmd, false, getExtensionConfigureError(extensionName, hostName));
}
VirtualMachineTO vmTO = cmd.getVirtualMachineTO();
String vmUUID = vmTO.getUuid();
@ -322,11 +348,11 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
}
@Override
public StartAnswer startInstance(String hostGuid, String extensionName, String extensionRelativePath,
public StartAnswer startInstance(String hostName, String extensionName, String extensionRelativePath,
StartCommand cmd) {
String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath);
if (StringUtils.isEmpty(extensionPath)) {
return new StartAnswer(cmd, "Extension not configured");
return new StartAnswer(cmd, getExtensionConfigureError(extensionName, hostName));
}
VirtualMachineTO virtualMachineTO = cmd.getVirtualMachine();
Map<String, Object> accessDetails = loadAccessDetails(cmd.getExternalDetails(), virtualMachineTO);
@ -366,11 +392,11 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
}
@Override
public StopAnswer stopInstance(String hostGuid, String extensionName, String extensionRelativePath,
public StopAnswer stopInstance(String hostName, String extensionName, String extensionRelativePath,
StopCommand cmd) {
String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath);
if (StringUtils.isEmpty(extensionPath)) {
return new StopAnswer(cmd, "Extension not configured", false);
return new StopAnswer(cmd, getExtensionConfigureError(extensionName, hostName), false);
}
logger.debug("Executing stop command on the external provisioner");
VirtualMachineTO virtualMachineTO = cmd.getVirtualMachine();
@ -387,13 +413,12 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
}
@Override
public RebootAnswer rebootInstance(String hostGuid, String extensionName, String extensionRelativePath,
public RebootAnswer rebootInstance(String hostName, String extensionName, String extensionRelativePath,
RebootCommand cmd) {
String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath);
if (StringUtils.isEmpty(extensionPath)) {
return new RebootAnswer(cmd, "Extension not configured", false);
return new RebootAnswer(cmd, getExtensionConfigureError(extensionName, hostName), false);
}
logger.debug("Executing reboot command using IPMI in the external provisioner");
VirtualMachineTO virtualMachineTO = cmd.getVirtualMachine();
String vmUUID = virtualMachineTO.getUuid();
logger.debug("Executing reboot command in the external system for the VM {}", vmUUID);
@ -408,11 +433,11 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
}
@Override
public StopAnswer expungeInstance(String hostGuid, String extensionName, String extensionRelativePath,
public StopAnswer expungeInstance(String hostName, String extensionName, String extensionRelativePath,
StopCommand cmd) {
String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath);
if (StringUtils.isEmpty(extensionPath)) {
return new StopAnswer(cmd, "Extension not configured", false);
return new StopAnswer(cmd, getExtensionConfigureError(extensionName, hostName), false);
}
VirtualMachineTO virtualMachineTO = cmd.getVirtualMachine();
String vmUUID = virtualMachineTO.getUuid();
@ -456,24 +481,65 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
}
@Override
public RunCustomActionAnswer runCustomAction(String hostGuid, String extensionName,
public GetExternalConsoleAnswer getInstanceConsole(String hostName, String extensionName,
String extensionRelativePath, GetExternalConsoleCommand cmd) {
String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath);
if (StringUtils.isEmpty(extensionPath)) {
return new GetExternalConsoleAnswer(cmd, getExtensionConfigureError(extensionName, hostName));
}
VirtualMachineTO virtualMachineTO = cmd.getVirtualMachine();
String vmUUID = virtualMachineTO.getUuid();
logger.debug("Executing getconsole command in the external system for the VM {}", vmUUID);
Map<String, Object> accessDetails = loadAccessDetails(cmd.getExternalDetails(), virtualMachineTO);
Pair<Boolean, String> result = getInstanceConsoleOnExternalSystem(extensionName, extensionPath, vmUUID,
accessDetails, cmd.getWait());
if (result == null) {
return new GetExternalConsoleAnswer(cmd, "No response from external system");
}
String output = result.second();
if (!result.first()) {
return new GetExternalConsoleAnswer(cmd, output);
}
logger.debug("Received console details from the external system: {}",
getSanitizedJsonStringForLog(output));
try {
JsonObject jsonObj = JsonParser.parseString(output).getAsJsonObject();
JsonObject consoleObj = jsonObj.has("console") ? jsonObj.getAsJsonObject("console") : null;
if (consoleObj == null) {
logger.error("Missing console object in external console output: {}",
getSanitizedJsonStringForLog(output));
return new GetExternalConsoleAnswer(cmd, "Missing console object in output");
}
String url = consoleObj.has("url") ? consoleObj.get("url").getAsString() : null;
String host = consoleObj.has("host") ? consoleObj.get("host").getAsString() : null;
Integer port = consoleObj.has("port") ? Integer.valueOf(consoleObj.get("port").getAsString()) : null;
String password = consoleObj.has("password") ? consoleObj.get("password").getAsString() : null;
boolean passwordOneTimeUseOnly = consoleObj.has("passwordonetimeuseonly") &&
consoleObj.get("passwordonetimeuseonly").getAsBoolean();
String protocol = consoleObj.has("protocol") ? consoleObj.get("protocol").getAsString() : null;
if (url == null && ObjectUtils.anyNull(host, port)) {
logger.error("Missing required fields in external console output: {}",
getSanitizedJsonStringForLog(output));
return new GetExternalConsoleAnswer(cmd, "Missing required fields in output");
}
return new GetExternalConsoleAnswer(cmd, url, host, port, password, passwordOneTimeUseOnly, protocol);
} catch (RuntimeException e) {
logger.error("Failed to parse output for getInstanceConsole: {}", e.getMessage(), e);
return new GetExternalConsoleAnswer(cmd, "Failed to parse output");
}
}
@Override
public RunCustomActionAnswer runCustomAction(String hostName, String extensionName,
String extensionRelativePath, RunCustomActionCommand cmd) {
String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath);
if (StringUtils.isEmpty(extensionPath)) {
return new RunCustomActionAnswer(cmd, false, "Extension not configured");
return new RunCustomActionAnswer(cmd, false, getExtensionConfigureError(extensionName, hostName));
}
final String actionName = cmd.getActionName();
final Map<String, Object> parameters = cmd.getParameters();
logger.debug("Executing custom action '{}' in the external provisioner", actionName);
VirtualMachineTO virtualMachineTO = null;
if (cmd.getVmId() != null) {
VMInstanceVO vm = vmInstanceDao.findById(cmd.getVmId());
final HypervisorGuru hvGuru = hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External);
VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm);
virtualMachineTO = hvGuru.implement(profile);
}
logger.debug("Executing custom action '{}' in the external system", actionName);
Map<String, Object> accessDetails = loadAccessDetails(cmd.getExternalDetails(), virtualMachineTO);
Map<String, Object> accessDetails = loadAccessDetails(cmd.getExternalDetails(), cmd.getVmTO());
accessDetails.put(ApiConstants.ACTION, actionName);
if (MapUtils.isNotEmpty(parameters)) {
accessDetails.put(ApiConstants.PARAMETERS, parameters);
@ -659,9 +725,7 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
private VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map<String, Map<String, String>> accessDetails,
String extensionName, String extensionPath) {
final HypervisorGuru hvGuru = hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External);
VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVmVO);
VirtualMachineTO virtualMachineTO = hvGuru.implement(profile);
VirtualMachineTO virtualMachineTO = getVirtualMachineTO(userVmVO);
accessDetails.put(ApiConstants.VIRTUAL_MACHINE, virtualMachineTO.getExternalDetails());
Map<String, Object> modifiedDetails = loadAccessDetails(accessDetails, virtualMachineTO);
String vmUUID = userVmVO.getUuid();
@ -718,6 +782,12 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
String.format("Failed to get the instance power status %s on external system", vmUUID), filename);
}
public Pair<Boolean, String> getInstanceConsoleOnExternalSystem(String extensionName, String filename,
String vmUUID, Map<String, Object> accessDetails, int wait) {
return executeExternalCommand(extensionName, "getconsole", accessDetails, wait,
String.format("Failed to get the instance console %s on external system", vmUUID), filename);
}
public Pair<Boolean, String> executeExternalCommand(String extensionName, String action,
Map<String, Object> accessDetails, int wait, String errorLogPrefix, String file) {
try {

View File

@ -35,6 +35,8 @@ import com.cloud.agent.api.CheckNetworkAnswer;
import com.cloud.agent.api.CheckNetworkCommand;
import com.cloud.agent.api.CleanupNetworkRulesCmd;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.GetExternalConsoleAnswer;
import com.cloud.agent.api.GetExternalConsoleCommand;
import com.cloud.agent.api.GetHostStatsAnswer;
import com.cloud.agent.api.GetHostStatsCommand;
import com.cloud.agent.api.GetVmStatsCommand;
@ -162,6 +164,8 @@ public class ExternalResource implements ServerResource {
return execute((StopCommand) cmd);
} else if (cmd instanceof RebootCommand) {
return execute((RebootCommand) cmd);
} else if (cmd instanceof GetExternalConsoleCommand) {
return execute((GetExternalConsoleCommand) cmd);
} else if (cmd instanceof PrepareExternalProvisioningCommand) {
return execute((PrepareExternalProvisioningCommand) cmd);
} else if (cmd instanceof GetHostStatsCommand) {
@ -273,6 +277,13 @@ public class ExternalResource implements ServerResource {
return externalProvisioner.rebootInstance(guid, extensionName, extensionRelativePath, cmd);
}
public GetExternalConsoleAnswer execute(GetExternalConsoleCommand cmd) {
if (isExtensionDisconnected() || isExtensionNotEnabled() || isExtensionPathNotReady()) {
return new GetExternalConsoleAnswer(cmd, logAndGetExtensionNotConnectedOrDisabledError());
}
return externalProvisioner.getInstanceConsole(guid, extensionName, extensionRelativePath, cmd);
}
public PrepareExternalProvisioningAnswer execute(PrepareExternalProvisioningCommand cmd) {
if (isExtensionDisconnected() || isExtensionNotEnabled() || isExtensionPathNotReady()) {
return new PrepareExternalProvisioningAnswer(cmd, false, logAndGetExtensionNotConnectedOrDisabledError());

View File

@ -65,6 +65,8 @@ import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.agent.api.GetExternalConsoleAnswer;
import com.cloud.agent.api.GetExternalConsoleCommand;
import com.cloud.agent.api.HostVmStateReportEntry;
import com.cloud.agent.api.PrepareExternalProvisioningAnswer;
import com.cloud.agent.api.PrepareExternalProvisioningCommand;
@ -88,11 +90,10 @@ import com.cloud.utils.Pair;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.script.Script;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
@RunWith(MockitoJUnitRunner.class)
public class ExternalPathPayloadProvisionerTest {
@ -107,9 +108,6 @@ public class ExternalPathPayloadProvisionerTest {
@Mock
private HostDao hostDao;
@Mock
private VMInstanceDao vmInstanceDao;
@Mock
private HypervisorGuruManager hypervisorGuruManager;
@ -208,6 +206,20 @@ public class ExternalPathPayloadProvisionerTest {
assertEquals("test-vm", result.get(ApiConstants.VIRTUAL_MACHINE_NAME));
}
@Test
public void testLoadAccessDetails_WithCaller() {
Map<String, Map<String, String>> externalDetails = new HashMap<>();
externalDetails.put(ApiConstants.EXTENSION, Map.of("key1", "value1"));
externalDetails.put(ApiConstants.CALLER, Map.of("key2", "value2"));
Map<String, Object> result = provisioner.loadAccessDetails(externalDetails, null);
assertNotNull(result);
assertNotNull(result.get(ApiConstants.EXTERNAL_DETAILS));
assertNotNull(((Map<String, String>) result.get(ApiConstants.EXTERNAL_DETAILS)).get(ApiConstants.EXTENSION));
assertNotNull(result.get(ApiConstants.CALLER));
assertNull(result.get(VmDetailConstants.CLOUDSTACK_VM_DETAILS));
}
@Test
public void testGetExtensionCheckedPathValidFile() {
String result = provisioner.getExtensionCheckedPath("test-extension", "test-extension.sh");
@ -317,7 +329,7 @@ public class ExternalPathPayloadProvisionerTest {
.executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());
PrepareExternalProvisioningAnswer answer = provisioner.prepareExternalProvisioning(
"host-guid", "test-extension", "test-extension.sh", cmd);
"host-name", "test-extension", "test-extension.sh", cmd);
assertTrue(answer.getResult());
assertEquals("test-net-uuid", answer.getVirtualMachineTO().getNics()[0].getNetworkUuid());
@ -329,11 +341,14 @@ public class ExternalPathPayloadProvisionerTest {
public void testPrepareExternalProvisioning_ExtensionNotConfigured() {
PrepareExternalProvisioningCommand cmd = mock(PrepareExternalProvisioningCommand.class);
String extensionName = "test-extension";
String hostName = "host-name";
PrepareExternalProvisioningAnswer answer = provisioner.prepareExternalProvisioning(
"host-guid", "test-extension", "nonexistent.sh", cmd);
hostName, extensionName, "nonexistent.sh", cmd);
assertFalse(answer.getResult());
assertEquals("Extension not configured", answer.getDetails());
assertNotNull(answer);
assertEquals(String.format("Extension: %s not configured for host: %s", extensionName, hostName), answer.getDetails());
}
@Test
@ -348,7 +363,7 @@ public class ExternalPathPayloadProvisionerTest {
doReturn(new Pair<>(true, "{\"status\": \"success\", \"message\": \"Instance started\"}")).when(provisioner)
.executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());
StartAnswer answer = provisioner.startInstance("host-guid", "test-extension", "test-extension.sh", cmd);
StartAnswer answer = provisioner.startInstance("host-name", "test-extension", "test-extension.sh", cmd);
assertTrue(answer.getResult());
Mockito.verify(logger).debug("Starting VM test-uuid on the external system");
@ -369,7 +384,7 @@ public class ExternalPathPayloadProvisionerTest {
doReturn(new Pair<>(true, "{\"status\": \"success\", \"message\": \"Instance started\"}")).when(provisioner)
.executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());
StartAnswer answer = provisioner.startInstance("host-guid", "test-extension", "test-extension.sh", cmd);
StartAnswer answer = provisioner.startInstance("host-name", "test-extension", "test-extension.sh", cmd);
assertTrue(answer.getResult());
Mockito.verify(logger).debug("Deploying VM test-uuid on the external system");
@ -387,7 +402,7 @@ public class ExternalPathPayloadProvisionerTest {
doReturn(new Pair<>(false, "{\"error\": \"Instance failed to start\"}")).when(provisioner)
.executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());
StartAnswer answer = provisioner.startInstance("host-guid", "test-extension", "test-extension.sh", cmd);
StartAnswer answer = provisioner.startInstance("host-name", "test-extension", "test-extension.sh", cmd);
assertFalse(answer.getResult());
assertEquals("{\"error\": \"Instance failed to start\"}", answer.getDetails());
@ -406,7 +421,7 @@ public class ExternalPathPayloadProvisionerTest {
doReturn(new Pair<>(true, "success")).when(provisioner)
.executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());
StopAnswer answer = provisioner.stopInstance("host-guid", "test-extension", "test-extension.sh", cmd);
StopAnswer answer = provisioner.stopInstance("host-name", "test-extension", "test-extension.sh", cmd);
assertTrue(answer.getResult());
}
@ -423,7 +438,7 @@ public class ExternalPathPayloadProvisionerTest {
doReturn(new Pair<>(true, "success")).when(provisioner)
.executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());
RebootAnswer answer = provisioner.rebootInstance("host-guid", "test-extension", "test-extension.sh", cmd);
RebootAnswer answer = provisioner.rebootInstance("host-name", "test-extension", "test-extension.sh", cmd);
assertTrue(answer.getResult());
}
@ -440,7 +455,7 @@ public class ExternalPathPayloadProvisionerTest {
doReturn(new Pair<>(true, "success")).when(provisioner)
.executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());
StopAnswer answer = provisioner.expungeInstance("host-guid", "test-extension", "test-extension.sh", cmd);
StopAnswer answer = provisioner.expungeInstance("host-name", "test-extension", "test-extension.sh", cmd);
assertTrue(answer.getResult());
}
@ -489,19 +504,11 @@ public class ExternalPathPayloadProvisionerTest {
when(cmd.getParameters()).thenReturn(new HashMap<>());
when(cmd.getExternalDetails()).thenReturn(new HashMap<>());
when(cmd.getWait()).thenReturn(30);
when(cmd.getVmId()).thenReturn(1L);
VMInstanceVO vm = mock(VMInstanceVO.class);
when(vmInstanceDao.findById(anyLong())).thenReturn(vm);
when(hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External)).thenReturn(hypervisorGuru);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
when(hypervisorGuru.implement(any(VirtualMachineProfile.class))).thenReturn(vmTO);
doReturn(new Pair<>(true, "success")).when(provisioner)
.executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());
RunCustomActionAnswer answer = provisioner.runCustomAction("host-guid", "test-extension", "test-extension.sh", cmd);
RunCustomActionAnswer answer = provisioner.runCustomAction("host-name", "test-extension", "test-extension.sh", cmd);
assertTrue(answer.getResult());
Mockito.verify(logger).debug("Executing custom action '{}' in the external system", "test-action");
@ -747,4 +754,236 @@ public class ExternalPathPayloadProvisionerTest {
VirtualMachine.PowerState result = provisioner.parsePowerStateFromResponse(vm, response);
assertEquals(VirtualMachine.PowerState.PowerOn, result);
}
@Test
public void getVirtualMachineTOReturnsNullWhenVmIsNull() {
VirtualMachineTO result = provisioner.getVirtualMachineTO(null);
assertNull(result);
}
@Test
public void getVirtualMachineTOReturnsValidTOWhenVmIsNotNull() {
VirtualMachine vm = mock(VirtualMachine.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
when(hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External)).thenReturn(hypervisorGuru);
when(hypervisorGuru.implement(any(VirtualMachineProfile.class))).thenReturn(vmTO);
VirtualMachineTO result = provisioner.getVirtualMachineTO(vm);
assertNotNull(result);
assertEquals(vmTO, result);
Mockito.verify(hypervisorGuruManager).getGuru(Hypervisor.HypervisorType.External);
Mockito.verify(hypervisorGuru).implement(any(VirtualMachineProfile.class));
}
@Test
public void getInstanceConsoleReturnsAnswerWhenConsoleDetailsAreValid() {
GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
when(cmd.getVirtualMachine()).thenReturn(vmTO);
when(vmTO.getUuid()).thenReturn("test-uuid");
Map<String, Object> accessDetails = new HashMap<>();
when(provisioner.loadAccessDetails(any(), eq(vmTO))).thenReturn(accessDetails);
String validOutput = "{\"console\":{\"host\":\"127.0.0.1\",\"port\":5900,\"password\":\"pass\",\"protocol\":\"vnc\"}}";
doReturn(new Pair<>(true, validOutput)).when(provisioner)
.getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());
GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);
assertNotNull(result);
assertEquals("127.0.0.1", result.getHost());
Integer port = 5900;
assertEquals(port, result.getPort());
assertEquals("pass", result.getPassword());
assertEquals("vnc", result.getProtocol());
}
@Test
public void getInstanceConsoleReturnsErrorWhenExtensionNotConfigured() {
GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
when(provisioner.getExtensionCheckedPath(anyString(), anyString())).thenReturn(null);
String extensionName = "test-extension";
String hostName = "host-name";
GetExternalConsoleAnswer result = provisioner.getInstanceConsole(hostName,
extensionName, "test-extension.sh", cmd);
assertNotNull(result);
assertEquals(String.format("Extension: %s not configured for host: %s", extensionName, hostName), result.getDetails());
}
@Test
public void getInstanceConsoleReturnsErrorWhenExternalSystemFails() {
GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
when(cmd.getVirtualMachine()).thenReturn(vmTO);
when(vmTO.getUuid()).thenReturn("test-uuid");
doReturn(new Pair<>(false, "External system error")).when(provisioner)
.getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());
GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);
assertNotNull(result);
assertEquals("External system error", result.getDetails());
}
@Test
public void getInstanceConsoleReturnsErrorWhenConsoleObjectIsMissing() {
GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
when(cmd.getVirtualMachine()).thenReturn(vmTO);
when(vmTO.getUuid()).thenReturn("test-uuid");
String invalidOutput = "{\"invalid_key\":\"value\"}";
doReturn(new Pair<>(true, invalidOutput)).when(provisioner)
.getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());
GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);
assertNotNull(result);
assertEquals("Missing console object in output", result.getDetails());
}
@Test
public void getInstanceConsoleReturnsErrorWhenRequiredFieldsAreMissing() {
GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
when(cmd.getVirtualMachine()).thenReturn(vmTO);
when(vmTO.getUuid()).thenReturn("test-uuid");
String incompleteOutput = "{\"console\":{\"host\":\"127.0.0.1\"}}";
doReturn(new Pair<>(true, incompleteOutput)).when(provisioner)
.getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());
GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);
assertNotNull(result);
assertEquals("Missing required fields in output", result.getDetails());
}
@Test
public void getInstanceConsoleReturnsErrorWhenOutputParsingFails() {
GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
when(cmd.getVirtualMachine()).thenReturn(vmTO);
when(vmTO.getUuid()).thenReturn("test-uuid");
String malformedOutput = "{console:invalid}";
doReturn(new Pair<>(true, malformedOutput)).when(provisioner)
.getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());
GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);
assertNotNull(result);
assertEquals("Failed to parse output", result.getDetails());
}
@Test
public void getInstanceConsoleOnExternalSystemReturnsSuccessWhenCommandExecutesSuccessfully() {
String extensionName = "test-extension";
String filename = "test-script.sh";
String vmUUID = "test-vm-uuid";
Map<String, Object> accessDetails = new HashMap<>();
int wait = 30;
doReturn(new Pair<>(true, "Console details")).when(provisioner)
.executeExternalCommand(eq(extensionName), eq("getconsole"), eq(accessDetails), eq(wait), anyString(), eq(filename));
Pair<Boolean, String> result = provisioner.getInstanceConsoleOnExternalSystem(extensionName, filename, vmUUID, accessDetails, wait);
assertTrue(result.first());
assertEquals("Console details", result.second());
}
@Test
public void getInstanceConsoleOnExternalSystemReturnsFailureWhenCommandFails() {
String extensionName = "test-extension";
String filename = "test-script.sh";
String vmUUID = "test-vm-uuid";
Map<String, Object> accessDetails = new HashMap<>();
int wait = 30;
doReturn(new Pair<>(false, "Failed to get console")).when(provisioner)
.executeExternalCommand(eq(extensionName), eq("getconsole"), eq(accessDetails), eq(wait), anyString(), eq(filename));
Pair<Boolean, String> result = provisioner.getInstanceConsoleOnExternalSystem(extensionName, filename, vmUUID, accessDetails, wait);
assertFalse(result.first());
assertEquals("Failed to get console", result.second());
}
@Test
public void getInstanceConsoleOnExternalSystemHandlesNullResponseGracefully() {
String extensionName = "test-extension";
String filename = "test-script.sh";
String vmUUID = "test-vm-uuid";
Map<String, Object> accessDetails = new HashMap<>();
int wait = 30;
doReturn(null).when(provisioner)
.executeExternalCommand(eq(extensionName), eq("getconsole"), eq(accessDetails), eq(wait), anyString(), eq(filename));
Pair<Boolean, String> result = provisioner.getInstanceConsoleOnExternalSystem(extensionName, filename, vmUUID, accessDetails, wait);
assertNull(result);
}
@Test
public void getSanitizedJsonStringForLogReturnsNullWhenInputIsNull() {
String result = provisioner.getSanitizedJsonStringForLog(null);
assertNull(result);
}
@Test
public void getSanitizedJsonStringForLogReturnsEmptyWhenInputIsEmpty() {
String result = provisioner.getSanitizedJsonStringForLog("");
assertEquals("", result);
}
@Test
public void getSanitizedJsonStringForLogReturnsSameStringWhenNoPasswordField() {
String json = "{\"key\":\"value\"}";
String result = provisioner.getSanitizedJsonStringForLog(json);
assertEquals(json, result);
}
@Test
public void getSanitizedJsonStringForLogMasksPasswordField() {
String json = "{\"password\":\"secret\"}";
String result = provisioner.getSanitizedJsonStringForLog(json);
assertEquals("{\"password\":\"****\"}", result);
}
@Test
public void getSanitizedJsonStringForLogHandlesMultiplePasswordFields() {
String json = "{\"password\":\"secret\",\"nested\":{\"password\":\"anotherSecret\"}}";
String result = provisioner.getSanitizedJsonStringForLog(json);
assertEquals("{\"password\":\"****\",\"nested\":{\"password\":\"****\"}}", result);
}
@Test
public void getSanitizedJsonStringForLogHandlesMalformedJsonGracefully() {
String json = "{password:\"secret\"";
String result = provisioner.getSanitizedJsonStringForLog(json);
assertEquals("{password:\"secret\"", result);
}
@Test
public void getExtensionConfigureErrorReturnsMessageWhenHostNameIsNotBlank() {
String result = provisioner.getExtensionConfigureError("test-extension", "test-host");
assertEquals("Extension: test-extension not configured for host: test-host", result);
}
@Test
public void getExtensionConfigureErrorReturnsMessageWhenHostNameIsBlank() {
String result = provisioner.getExtensionConfigureError("test-extension", "");
assertEquals("Extension: test-extension not configured", result);
}
@Test
public void getExtensionConfigureErrorReturnsMessageWhenHostNameIsNull() {
String result = provisioner.getExtensionConfigureError("test-extension", null);
assertEquals("Extension: test-extension not configured", result);
}
}

View File

@ -18,7 +18,7 @@
parse_json() {
local json_string=$1
echo "$json_string" | jq '.' > /dev/null || { echo '{"error":"Invalid JSON input"}'; exit 1; }
echo "$json_string" | jq '.' > /dev/null || { echo '{"status": "error", "error": "Invalid JSON input"}'; exit 1; }
}
generate_random_mac() {
@ -99,17 +99,24 @@ status() {
echo '{"status": "success", "power_state": "poweron"}'
}
get_console() {
parse_json "$1" || exit 1
local response
jq -n '{status:"error", error: "Operation not supported"}'
exit 1
}
action=$1
parameters_file="$2"
wait_time="$3"
if [[ -z "$action" || -z "$parameters_file" ]]; then
echo '{"error":"Missing required arguments"}'
echo '{"status": "error", "error": "Missing required arguments"}'
exit 1
fi
if [[ ! -r "$parameters_file" ]]; then
echo '{"error":"File not found or unreadable"}'
echo '{"status": "error", "error": "File not found or unreadable"}'
exit 1
fi
@ -138,8 +145,11 @@ case $action in
status)
status "$parameters"
;;
getconsole)
get_console "$parameters"
;;
*)
echo '{"error":"Invalid action"}'
echo '{"status": "error", "error": "Invalid action"}'
exit 1
;;
esac

View File

@ -48,6 +48,7 @@ import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.servlet.ConsoleProxyPasswordBasedEncryptor;
import com.cloud.servlet.ConsoleProxyServlet;
import com.cloud.utils.Ternary;
@ -161,8 +162,12 @@ public abstract class AgentHookBase implements AgentHook {
String sid = cmd.getSid();
if (sid == null || !sid.equals(vm.getVncPassword())) {
logger.warn("sid " + sid + " in url does not match stored sid.");
return new ConsoleAccessAuthenticationAnswer(cmd, false);
if (Hypervisor.HypervisorType.External.equals(vm.getHypervisorType())) {
logger.debug("{} is on External hypervisor, skip checking sid", vm.getHypervisorType());
} else {
logger.warn("sid {} in url does not match stored sid.", sid);
return new ConsoleAccessAuthenticationAnswer(cmd, false);
}
}
if (cmd.isReauthenticating()) {

View File

@ -16,7 +16,9 @@
// under the License.
package com.cloud.server;
import com.cloud.agent.api.Answer;
import com.cloud.host.DetailVO;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.storage.GuestOSHypervisorVO;
import com.cloud.storage.GuestOSVO;
@ -74,4 +76,6 @@ public interface ManagementServer extends ManagementService, PluggableService {
Pair<Boolean, String> updateSystemVM(VMInstanceVO systemVM, boolean forced);
Answer getExternalVmConsole(VirtualMachine vm, Host host);
}

View File

@ -44,18 +44,6 @@ import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.cpu.CPU;
import com.cloud.dc.VlanDetailsVO;
import com.cloud.dc.dao.VlanDetailsDao;
import com.cloud.network.dao.NetrisProviderDao;
import com.cloud.network.dao.NsxProviderDao;
import com.cloud.utils.security.CertificateHelper;
import com.cloud.api.query.dao.ManagementServerJoinDao;
import com.cloud.api.query.vo.ManagementServerJoinVO;
import com.cloud.gpu.VgpuProfileVO;
import com.cloud.gpu.dao.VgpuProfileDao;
import com.cloud.offering.ServiceOffering;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.affinity.AffinityGroupProcessor;
@ -695,7 +683,9 @@ import com.cloud.alert.AlertManager;
import com.cloud.alert.AlertVO;
import com.cloud.alert.dao.AlertDao;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.query.dao.ManagementServerJoinDao;
import com.cloud.api.query.dao.StoragePoolJoinDao;
import com.cloud.api.query.vo.ManagementServerJoinVO;
import com.cloud.api.query.vo.StoragePoolJoinVO;
import com.cloud.capacity.Capacity;
import com.cloud.capacity.CapacityVO;
@ -706,6 +696,7 @@ import com.cloud.configuration.Config;
import com.cloud.configuration.ConfigurationManagerImpl;
import com.cloud.consoleproxy.ConsoleProxyManagementState;
import com.cloud.consoleproxy.ConsoleProxyManager;
import com.cloud.cpu.CPU;
import com.cloud.dc.AccountVlanMapVO;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.DataCenterVO;
@ -715,6 +706,7 @@ import com.cloud.dc.Pod;
import com.cloud.dc.PodVlanMapVO;
import com.cloud.dc.Vlan;
import com.cloud.dc.Vlan.VlanType;
import com.cloud.dc.VlanDetailsVO;
import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.AccountVlanMapDao;
import com.cloud.dc.dao.ClusterDao;
@ -723,6 +715,7 @@ import com.cloud.dc.dao.DomainVlanMapDao;
import com.cloud.dc.dao.HostPodDao;
import com.cloud.dc.dao.PodVlanMapDao;
import com.cloud.dc.dao.VlanDao;
import com.cloud.dc.dao.VlanDetailsDao;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeploymentPlanner;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
@ -744,6 +737,8 @@ import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.exception.VirtualMachineMigrationException;
import com.cloud.gpu.GPU;
import com.cloud.gpu.VgpuProfileVO;
import com.cloud.gpu.dao.VgpuProfileDao;
import com.cloud.ha.HighAvailabilityManager;
import com.cloud.host.DetailVO;
import com.cloud.host.Host;
@ -770,13 +765,16 @@ import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.LoadBalancerDao;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.NetrisProviderDao;
import com.cloud.network.dao.NetworkAccountDao;
import com.cloud.network.dao.NetworkAccountVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkDomainDao;
import com.cloud.network.dao.NetworkDomainVO;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.NsxProviderDao;
import com.cloud.network.vpc.dao.VpcDao;
import com.cloud.offering.ServiceOffering;
import com.cloud.org.Cluster;
import com.cloud.org.Grouping.AllocationState;
import com.cloud.projects.Project;
@ -850,6 +848,7 @@ import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.fsm.StateMachine2;
import com.cloud.utils.net.MacAddress;
import com.cloud.utils.net.NetUtils;
import com.cloud.utils.security.CertificateHelper;
import com.cloud.utils.ssh.SSHKeysHelper;
import com.cloud.vm.ConsoleProxyVO;
import com.cloud.vm.DiskProfile;
@ -5813,4 +5812,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
}
@Override
public Answer getExternalVmConsole(VirtualMachine vm, Host host) {
return extensionsManager.getInstanceConsole(vm, host);
}
}

View File

@ -34,6 +34,8 @@ public class ConsoleProxyClientParam {
private String username;
private String password;
private boolean sessionRequiresNewViewer = false;
/**
* IP that has generated the console endpoint
*/
@ -218,4 +220,8 @@ public class ConsoleProxyClientParam {
public void setClientIp(String clientIp) {
this.clientIp = clientIp;
}
public void setSessionRequiresNewViewer(boolean sessionRequiresNewViewer) {
this.sessionRequiresNewViewer = sessionRequiresNewViewer;
}
}

View File

@ -43,6 +43,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import com.cloud.hypervisor.Hypervisor;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -277,15 +278,19 @@ public class ConsoleProxyServlet extends HttpServlet {
String sid = req.getParameter("sid");
if (sid == null || !sid.equals(vm.getVncPassword())) {
if(sid != null) {
sid = sid.replaceAll(SANITIZATION_REGEX, "_");
LOGGER.warn(String.format("sid [%s] in url does not match stored sid.", sid));
if (Hypervisor.HypervisorType.External.equals(vm.getHypervisorType())) {
LOGGER.debug("{} is on External hypervisor, skip checking sid", vm.getHypervisorType());
} else {
LOGGER.warn("Null sid in URL.");
}
if (sid != null) {
sid = sid.replaceAll(SANITIZATION_REGEX, "_");
LOGGER.warn(String.format("sid [%s] in url does not match stored sid.", sid));
} else {
LOGGER.warn("Null sid in URL.");
}
sendResponse(resp, "failed");
return;
sendResponse(resp, "failed");
return;
}
}
sendResponse(resp, "success");

View File

@ -22,15 +22,15 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.domain.Domain;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.InvalidParameterValueException;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint;
@ -38,20 +38,29 @@ import org.apache.cloudstack.api.command.user.consoleproxy.ListConsoleSessionsCm
import org.apache.cloudstack.api.response.ConsoleSessionResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.security.keys.KeysManager;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joda.time.DateTime;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetExternalConsoleAnswer;
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.domain.Domain;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.host.HostVO;
@ -72,7 +81,6 @@ 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.VMInstanceDetailVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VmDetailConstants;
@ -80,15 +88,6 @@ import com.cloud.vm.dao.ConsoleSessionDao;
import com.cloud.vm.dao.VMInstanceDetailsDao;
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 {
@ -128,6 +127,11 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
VirtualMachine.State.Stopped, VirtualMachine.State.Restoring, VirtualMachine.State.Error, VirtualMachine.State.Destroyed
);
protected static final List<ResourceState> MAINTENANCE_RESOURCE_STATES = new ArrayList<>(Arrays.asList(
ResourceState.ErrorInMaintenance, ResourceState.ErrorInPrepareForMaintenance
));
protected static final String WEB_SOCKET_PATH= "websockify";
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
ConsoleAccessManagerImpl.secretKeysManager = keysManager;
@ -293,12 +297,6 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
return new ConsoleEndpoint(false, null, "Cannot find VM with ID " + vmId);
}
if (Hypervisor.HypervisorType.External.equals(vm.getHypervisorType())) {
logger.error("Console access for {} cannot be provided it is {} hypervisor instance", vm,
Hypervisor.HypervisorType.External);
return new ConsoleEndpoint(false, null, "Console access to this instance cannot be provided");
}
if (!checkSessionPermission(vm, account)) {
return new ConsoleEndpoint(false, null, "Permission denied");
}
@ -427,67 +425,119 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
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))) {
VMInstanceDetailVO detailAddress = vmInstanceDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_ADDRESS);
VMInstanceDetailVO detailPort = vmInstanceDetailsDao.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.");
}
protected ConsoleConnectionDetails getConsoleConnectionDetailsForExternalVm(ConsoleConnectionDetails details,
VirtualMachine vm, HostVO host) {
Answer answer = managementServer.getExternalVmConsole(vm, host);
if (answer == null) {
logger.error("Unable to get console access details for external {} on {}: answer is null.", vm, host);
return null;
}
if (portInfo == null) {
portInfo = managementServer.getVncPort(vm);
if (!answer.getResult()) {
logger.error("Unable to get console access details for external {} on {}: answer result is false. Reason: {}", vm, host, answer.getDetails());
return null;
}
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();
if (!(answer instanceof GetExternalConsoleAnswer)) {
logger.error("Unable to get console access details for external {} on {}: answer is not of type GetExternalConsoleAnswer.", vm, host);
return null;
}
GetExternalConsoleAnswer getExternalConsoleAnswer = (GetExternalConsoleAnswer) answer;
details.setModeFromExternalProtocol(getExternalConsoleAnswer.getProtocol());
details.setDirectUrl(getExternalConsoleAnswer.getUrl());
details.setHost(getExternalConsoleAnswer.getHost());
if (getExternalConsoleAnswer.getPort() != null) {
details.setPort(getExternalConsoleAnswer.getPort());
}
if (StringUtils.isNotBlank(getExternalConsoleAnswer.getPassword())) {
details.setSid(getExternalConsoleAnswer.getPassword());
details.setSessionRequiresNewViewer(getExternalConsoleAnswer.isPasswordOneTimeUseOnly());
}
return details;
}
String sid = vm.getVncPassword();
VMInstanceDetailVO details = vmInstanceDetailsDao.findDetail(vm.getId(), VmDetailConstants.KEYBOARD);
protected Pair<String, Integer> getHostAndPortForKVMMaintenanceHostIfNeeded(HostVO host,
Map<String, String> vmDetails) {
if (!Hypervisor.HypervisorType.KVM.equals(host.getHypervisorType())) {
return null;
}
if(!MAINTENANCE_RESOURCE_STATES.contains(host.getResourceState())) {
return null;
}
String address = vmDetails.get(VmDetailConstants.KVM_VNC_ADDRESS);
String port = vmDetails.get(VmDetailConstants.KVM_VNC_PORT);
if (ObjectUtils.allNotNull(address, port)) {
return new Pair<>(address, Integer.valueOf(port));
}
logger.warn("KVM Host in ErrorInMaintenance/ErrorInPrepareForMaintenance but " +
"no VNC Address/Port was available. Falling back to default one from MS.");
return null;
}
protected ConsoleConnectionDetails getConsoleConnectionDetails(VirtualMachine vm, HostVO host) {
String locale = null;
String tag = vm.getUuid();
String displayName = vm.getHostName();
if (vm instanceof UserVm) {
displayName = ((UserVm) vm).getDisplayName();
}
Map<String, String> vmDetails = vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId(),
List.of(VmDetailConstants.KEYBOARD, VmDetailConstants.KVM_VNC_ADDRESS, VmDetailConstants.KVM_VNC_PORT));
if (vmDetails.get(VmDetailConstants.KEYBOARD) != null) {
locale = vmDetails.get(VmDetailConstants.KEYBOARD);
}
ConsoleConnectionDetails details = new ConsoleConnectionDetails(vm.getVncPassword(), locale, tag, displayName);
if (Hypervisor.HypervisorType.External.equals(host.getHypervisorType())) {
return getConsoleConnectionDetailsForExternalVm(details, vm, host);
}
Pair<String, Integer> hostPortInfo = getHostAndPortForKVMMaintenanceHostIfNeeded(host, vmDetails);
if (hostPortInfo == null) {
hostPortInfo = managementServer.getVncPort(vm);
}
logger.debug("Retrieved VNC host and port info :[{}, {}] for {} on {}", hostPortInfo.first(),
hostPortInfo.second(), vm, host);
Ternary<String, String, String> parsedHostInfo = parseHostInfo(hostPortInfo.first());
details.setHost(parsedHostInfo.first());
details.setTunnelUrl(parsedHostInfo.second());
details.setTunnelSession(parsedHostInfo.third());
details.setPort(hostPortInfo.second());
if (hostPortInfo.second() == -9) {
details.setUsingRDP(true);
details.setPort(Integer.parseInt(managementServer.findDetail(host.getId(), "rdp.server.port")
.getValue()));
logger.debug("HyperV RDP port for {} on {} is: {}", vm, host, details.getPort());
}
return details;
}
String ticket = genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag, sessionUuid);
protected ConsoleEndpoint composeConsoleAccessEndpoint(String rootUrl, VirtualMachine vm, HostVO hostVo, String addr,
String sessionUuid, String extraSecurityToken) {
ConsoleConnectionDetails result = getConsoleConnectionDetails(vm, hostVo);
if (result == null) {
return new ConsoleEndpoint(false, null, "Console access to this instance cannot be provided");
}
if (ConsoleConnectionDetails.Mode.Direct.equals(result.getMode())) {
persistConsoleSession(sessionUuid, vm.getId(), hostVo.getId(), addr);
return new ConsoleEndpoint(true, result.getDirectUrl());
}
String ticket = genAccessTicket(result.getHost(), String.valueOf(result.getPort()), result.getSid(),
result.getTag(), sessionUuid);
ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword());
ConsoleProxyClientParam param = generateConsoleProxyClientParam(parsedHostInfo, port, sid, tag, ticket,
sessionUuid, addr, extraSecurityToken, vm, hostVo, details, portInfo, host, displayName);
ConsoleProxyClientParam param = generateConsoleProxyClientParam(result, ticket, sessionUuid, addr,
extraSecurityToken, vm, hostVo);
String token = encryptor.encryptObject(ConsoleProxyClientParam.class, param);
int vncPort = consoleProxyManager.getVncPort(vm.getDataCenterId());
String url = generateConsoleAccessUrl(rootUrl, param, token, vncPort, vm, hostVo, details);
String url = generateConsoleAccessUrl(rootUrl, param, token, vncPort, vm, hostVo, result.getLocale());
logger.debug("Adding allowed session: " + sessionUuid);
logger.debug("Adding allowed session: {}", sessionUuid);
persistConsoleSession(sessionUuid, vm.getId(), hostVo.getId(), addr);
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.setWebsocketPath(WEB_SOCKET_PATH);
consoleEndpoint.setWebsocketToken(token);
if (StringUtils.isNotBlank(param.getExtraSecurityToken())) {
consoleEndpoint.setWebsocketExtra(param.getExtraSecurityToken());
@ -509,23 +559,23 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
consoleSessionDao.persist(consoleSessionVo);
}
private String generateConsoleAccessUrl(String rootUrl, ConsoleProxyClientParam param, String token, int vncPort,
VirtualMachine vm, HostVO hostVo, VMInstanceDetailVO details) {
protected String generateConsoleAccessUrl(String rootUrl, ConsoleProxyClientParam param, String token, int vncPort,
VirtualMachine vm, HostVO hostVo, String locale) {
StringBuilder sb = new StringBuilder(rootUrl);
if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) {
sb.append("/ajax?token=" + token);
sb.append("/ajax?token=").append(token);
} else {
sb.append("/resource/noVNC/vnc.html")
.append("?autoconnect=true&show_dot=true")
.append("&port=" + vncPort)
.append("&token=" + token);
if (requiresVncOverWebSocketConnection(vm, hostVo) && details != null && details.getValue() != null) {
sb.append("&language=" + details.getValue());
.append("&port=").append(vncPort)
.append("&token=").append(token);
if (requiresVncOverWebSocketConnection(vm, hostVo) && StringUtils.isNotBlank(locale)) {
sb.append("&language=").append(locale);
}
}
if (StringUtils.isNotBlank(param.getExtraSecurityToken())) {
sb.append("&extra=" + param.getExtraSecurityToken());
sb.append("&extra=").append(param.getExtraSecurityToken());
}
// for console access, we need guest OS type to help implement keyboard
@ -535,50 +585,46 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
sb.append("&guest=windows");
if (logger.isDebugEnabled()) {
logger.debug("Compose console url: " + sb);
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, VMInstanceDetailVO details,
Pair<String, Integer> portInfo, String host,
String displayName) {
protected ConsoleProxyClientParam generateConsoleProxyClientParam(ConsoleConnectionDetails details, String ticket,
String sessionUuid, String addr, String extraSecurityToken, VirtualMachine vm, HostVO host) {
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
param.setClientHostAddress(parsedHostInfo.first());
param.setClientHostPort(port);
param.setClientHostPassword(sid);
param.setClientTag(tag);
param.setClientDisplayName(displayName);
param.setClientHostAddress(details.getHost());
param.setClientHostPort(details.getPort());
param.setClientHostPassword(details.getSid());
param.setClientTag(details.getTag());
param.setClientDisplayName(details.getDisplayName());
param.setTicket(ticket);
param.setSessionUuid(sessionUuid);
param.setSourceIP(addr);
param.setSessionRequiresNewViewer(details.isSessionRequiresNewViewer());
if (StringUtils.isNotBlank(extraSecurityToken)) {
param.setExtraSecurityToken(extraSecurityToken);
logger.debug("Added security token for client validation");
}
if (requiresVncOverWebSocketConnection(vm, hostVo)) {
if (requiresVncOverWebSocketConnection(vm, host)) {
setWebsocketUrl(vm, param);
}
if (details != null) {
param.setLocale(details.getValue());
if (details.getLocale() != null) {
param.setLocale(details.getLocale());
}
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 (details.isUsingRDP()) {
//For Hyperv Client Host Address will send Instance id
param.setHypervHost(host.getPrivateIpAddress());
param.setUsername(managementServer.findDetail(host.getId(), "username").getValue());
param.setPassword(managementServer.findDetail(host.getId(), "password").getValue());
}
if (parsedHostInfo.second() != null && parsedHostInfo.third() != null) {
param.setClientTunnelUrl(parsedHostInfo.second());
param.setClientTunnelSession(parsedHostInfo.third());
if (ObjectUtils.allNotNull(details.getTunnelUrl(), details.getTunnelSession())) {
param.setClientTunnelUrl(details.getTunnelUrl());
param.setClientTunnelSession(details.getTunnelSession());
}
return param;
}
@ -651,7 +697,7 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
return "";
}
private String getEncryptorPassword() {
protected String getEncryptorPassword() {
String key = keysManager.getEncryptionKey();
String iv = keysManager.getEncryptionIV();
@ -700,4 +746,120 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
}
}
protected static class ConsoleConnectionDetails {
public enum Mode {
ConsoleProxy,
Direct
}
private Mode mode = Mode.ConsoleProxy;
private String host;
private int port = -1;
private String sid;
private String locale;
private String tag;
private String displayName;
private String tunnelUrl = null;
private String tunnelSession = null;
private boolean usingRDP;
private String directUrl;
private boolean sessionRequiresNewViewer = false;
ConsoleConnectionDetails(String sid, String locale, String tag, String displayName) {
this.sid = sid;
this.locale = locale;
this.tag = tag;
this.displayName = displayName;
}
public Mode getMode() {
return mode;
}
public void setModeFromExternalProtocol(String protocol) {
this.mode = Mode.ConsoleProxy;
if (StringUtils.isBlank(protocol)) {
return;
}
if (Mode.Direct.name().toLowerCase().equalsIgnoreCase(protocol)) {
this.mode = Mode.Direct;
}
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getSid() {
return sid;
}
public void setSid(String sid) {
this.sid = sid;
}
public String getLocale() {
return locale;
}
public String getTag() {
return tag;
}
public String getDisplayName() {
return displayName;
}
public String getTunnelUrl() {
return tunnelUrl;
}
public void setTunnelUrl(String tunnelUrl) {
this.tunnelUrl = tunnelUrl;
}
public String getTunnelSession() {
return tunnelSession;
}
public void setTunnelSession(String tunnelSession) {
this.tunnelSession = tunnelSession;
}
public boolean isUsingRDP() {
return usingRDP;
}
public void setUsingRDP(boolean usingRDP) {
this.usingRDP = usingRDP;
}
public String getDirectUrl() {
return directUrl;
}
public void setDirectUrl(String directUrl) {
this.directUrl = directUrl;
}
public boolean isSessionRequiresNewViewer() {
return sessionRequiresNewViewer;
}
public void setSessionRequiresNewViewer(boolean sessionRequiresNewViewer) {
this.sessionRequiresNewViewer = sessionRequiresNewViewer;
}
}
}

View File

@ -49,6 +49,7 @@ import org.apache.cloudstack.framework.config.ConfigDepot;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.config.impl.ConfigurationVO;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import org.apache.cloudstack.userdata.UserDataManager;
import org.junit.After;
import org.junit.Assert;
@ -99,6 +100,7 @@ import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VMInstanceDetailVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDetailsDao;
@ -165,6 +167,9 @@ public class ManagementServerImplTest {
@Mock
GuestOSDao _guestOSDao;
@Mock
ExtensionsManager extensionManager;
@Spy
@InjectMocks
ManagementServerImpl spy = new ManagementServerImpl();
@ -1019,4 +1024,13 @@ public class ManagementServerImplTest {
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).searchAndCount(Mockito.eq(searchCriteria), Mockito.any());
}
@Test
public void testGetExternalVmConsole() {
VirtualMachine virtualMachine = Mockito.mock(VirtualMachine.class);
Host host = Mockito.mock(Host.class);
Mockito.when(extensionManager.getInstanceConsole(virtualMachine, host)).thenReturn(Mockito.mock(com.cloud.agent.api.Answer.class));
Assert.assertNotNull(spy.getExternalVmConsole(virtualMachine, host));
Mockito.verify(extensionManager).getInstanceConsole(virtualMachine, host);
}
}

View File

@ -16,25 +16,18 @@
// under the License.
package org.apache.cloudstack.consoleproxy;
import com.cloud.agent.AgentManager;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.server.ManagementServer;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.utils.Pair;
import com.cloud.utils.db.EntityManager;
import com.cloud.vm.ConsoleSessionVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.dao.ConsoleSessionDao;
import com.cloud.vm.dao.VMInstanceDetailsDao;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint;
import org.apache.cloudstack.api.command.user.consoleproxy.ListConsoleSessionsCmd;
import org.apache.cloudstack.api.response.ConsoleSessionResponse;
import org.apache.cloudstack.consoleproxy.ConsoleAccessManagerImpl.ConsoleConnectionDetails;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.security.keys.KeysManager;
import org.junit.Assert;
@ -47,8 +40,32 @@ import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.List;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetExternalConsoleAnswer;
import com.cloud.consoleproxy.ConsoleProxyManager;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.host.DetailVO;
import com.cloud.host.HostVO;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.resource.ResourceState;
import com.cloud.serializer.GsonHelper;
import com.cloud.server.ManagementServer;
import com.cloud.servlet.ConsoleProxyClientParam;
import com.cloud.servlet.ConsoleProxyPasswordBasedEncryptor;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.db.EntityManager;
import com.cloud.vm.ConsoleSessionVO;
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.VMInstanceDetailsDao;
@RunWith(MockitoJUnitRunner.class)
public class ConsoleAccessManagerImplTest {
@ -67,6 +84,8 @@ public class ConsoleAccessManagerImplTest {
private KeysManager keysManager;
@Mock
private AgentManager agentManager;
@Mock
ConsoleProxyManager consoleProxyManager;
@Spy
@InjectMocks
@ -311,4 +330,461 @@ public class ConsoleAccessManagerImplTest {
consoleAccessManager.listConsoleSessionById(1L);
Mockito.verify(consoleSessionDaoMock).findByIdIncludingRemoved(1L);
}
@Test
public void getConsoleConnectionDetailsForExternalVmReturnsNullWhenAnswerIsNull() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails("sid", "en", "tag", "displayName");
Mockito.when(managementServer.getExternalVmConsole(vm, host)).thenReturn(null);
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetailsForExternalVm(details, vm, host);
Assert.assertNull(result);
}
@Test
public void getConsoleConnectionDetailsForExternalVmReturnsNullWhenAnswerResultIsFalse() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails("sid", "en", "tag", "displayName");
Answer answer = Mockito.mock(Answer.class);
Mockito.when(answer.getResult()).thenReturn(false);
Mockito.when(answer.getDetails()).thenReturn("Error details");
Mockito.when(managementServer.getExternalVmConsole(vm, host)).thenReturn(answer);
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetailsForExternalVm(details, vm, host);
Assert.assertNull(result);
}
@Test
public void getConsoleConnectionDetailsForExternalVmReturnsNullWhenAnswerIsNotOfTypeGetExternalConsoleAnswer() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails("sid", "en", "tag", "displayName");
Answer answer = Mockito.mock(Answer.class);
Mockito.when(answer.getResult()).thenReturn(true);
Mockito.when(managementServer.getExternalVmConsole(vm, host)).thenReturn(answer);
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetailsForExternalVm(details, vm, host);
Assert.assertNull(result);
}
@Test
public void getConsoleConnectionDetailsForExternalVmSetsDetailsWhenAnswerIsValid() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails("sid", "en", "tag", "displayName");
GetExternalConsoleAnswer answer = Mockito.mock(GetExternalConsoleAnswer.class);
String expectedHost = "10.0.0.1";
int expectedPort = 5900;
String expectedPassword = "password";
Mockito.when(answer.getResult()).thenReturn(true);
Mockito.when(answer.getHost()).thenReturn(expectedHost);
Mockito.when(answer.getPort()).thenReturn(expectedPort);
Mockito.when(answer.getPassword()).thenReturn(expectedPassword);
Mockito.when(managementServer.getExternalVmConsole(vm, host)).thenReturn(answer);
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetailsForExternalVm(details, vm, host);
Assert.assertNotNull(result);
Assert.assertEquals(ConsoleConnectionDetails.Mode.ConsoleProxy, result.getMode());
Assert.assertEquals(expectedHost, result.getHost());
Assert.assertEquals(expectedPort, result.getPort());
Assert.assertEquals(expectedPassword, result.getSid());
Assert.assertNull(result.getDirectUrl());
}
@Test
public void getConsoleConnectionDetailsForExternalVmSetsDetailsWhenAnswerIsValidDirect() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails("sid", "en", "tag", "displayName");
GetExternalConsoleAnswer answer = Mockito.mock(GetExternalConsoleAnswer.class);
String url = "url";
Mockito.when(answer.getResult()).thenReturn(true);
Mockito.when(answer.getUrl()).thenReturn(url);
Mockito.when(answer.getProtocol()).thenReturn(ConsoleConnectionDetails.Mode.Direct.name());
Mockito.when(managementServer.getExternalVmConsole(vm, host)).thenReturn(answer);
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetailsForExternalVm(details, vm, host);
Assert.assertNotNull(result);
Assert.assertEquals(ConsoleConnectionDetails.Mode.Direct, result.getMode());
Assert.assertEquals(url, result.getDirectUrl());
}
@Test
public void getConsoleConnectionDetailsForExternalVmDoesNotSetSidWhenPasswordIsBlank() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails("sid", "en", "tag", "displayName");
GetExternalConsoleAnswer answer = Mockito.mock(GetExternalConsoleAnswer.class);
Mockito.when(answer.getResult()).thenReturn(true);
Mockito.when(answer.getHost()).thenReturn("10.0.0.1");
Mockito.when(answer.getPort()).thenReturn(5900);
Mockito.when(answer.getPassword()).thenReturn("");
Mockito.when(managementServer.getExternalVmConsole(vm, host)).thenReturn(answer);
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetailsForExternalVm(details, vm, host);
Assert.assertNotNull(result);
Assert.assertEquals("10.0.0.1", result.getHost());
Assert.assertEquals(5900, result.getPort());
Assert.assertEquals("sid", result.getSid());
}
@Test
public void getHostAndPortForKVMMaintenanceHostIfNeededReturnsNullForNonKVMHypervisor() {
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.XenServer);
Pair<String, Integer> result = consoleAccessManager.getHostAndPortForKVMMaintenanceHostIfNeeded(host, Map.of());
Assert.assertNull(result);
}
@Test
public void getHostAndPortForKVMMaintenanceHostIfNeededReturnsNullForNonMaintenanceResourceState() {
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(host.getResourceState()).thenReturn(ResourceState.Enabled);
Pair<String, Integer> result = consoleAccessManager.getHostAndPortForKVMMaintenanceHostIfNeeded(host, Map.of());
Assert.assertNull(result);
}
@Test
public void getHostAndPortForKVMMaintenanceHostIfNeededReturnsHostAndPortForValidKVMInMaintenance() {
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(host.getResourceState()).thenReturn(ResourceState.ErrorInMaintenance);
String address = "192.168.1.100";
int port = 5901;
Map<String, String> vmDetails = Map.of(
VmDetailConstants.KVM_VNC_ADDRESS, address,
VmDetailConstants.KVM_VNC_PORT, String.valueOf(port)
);
Pair<String, Integer> result = consoleAccessManager.getHostAndPortForKVMMaintenanceHostIfNeeded(host, vmDetails);
Assert.assertNotNull(result);
Assert.assertEquals(address, result.first());
Assert.assertEquals(port, (int) result.second());
}
@Test
public void getHostAndPortForKVMMaintenanceHostIfNeededReturnsNullWhenVncAddressOrPortIsMissing() {
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(host.getResourceState()).thenReturn(ResourceState.ErrorInMaintenance);
Map<String, String> vmDetails = Map.of(VmDetailConstants.KVM_VNC_ADDRESS, "192.168.1.100");
Pair<String, Integer> result = consoleAccessManager.getHostAndPortForKVMMaintenanceHostIfNeeded(host, vmDetails);
Assert.assertNull(result);
}
@Test
public void getConsoleConnectionDetailsReturnsDetailsForExternalHypervisor() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
ConsoleConnectionDetails details = Mockito.mock(ConsoleConnectionDetails.class);
Mockito.when(vm.getUuid()).thenReturn("vm-uuid");
Mockito.when(vm.getHostName()).thenReturn("vm-hostname");
Mockito.when(vm.getVncPassword()).thenReturn("vnc-password");
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External);
Mockito.when(vmInstanceDetailsDao.listDetailsKeyPairs(Mockito.anyLong(), Mockito.anyList())).thenReturn(Map.of());
Mockito.doReturn(details).when(consoleAccessManager).getConsoleConnectionDetailsForExternalVm(Mockito.any(), Mockito.eq(vm), Mockito.eq(host));
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetails(vm, host);
Assert.assertNotNull(result);
Mockito.verify(consoleAccessManager).getConsoleConnectionDetailsForExternalVm(Mockito.any(), Mockito.eq(vm), Mockito.eq(host));
}
@Test
public void getConsoleConnectionDetailsReturnsDetailsForKVMHypervisor() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
String hostAddress = "192.168.1.100";
int port = 5900;
String vmUuid = "vm-uuid";
String vmHostName = "vm-hostname";
String vncPassword = "vnc-password";
Pair<String, Integer> hostPortInfo = new Pair<>(hostAddress, port);
Mockito.when(vm.getUuid()).thenReturn(vmUuid);
Mockito.when(vm.getHostName()).thenReturn(vmHostName);
Mockito.when(vm.getVncPassword()).thenReturn(vncPassword);
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(vmInstanceDetailsDao.listDetailsKeyPairs(Mockito.anyLong(), Mockito.anyList())).thenReturn(Map.of());
Mockito.when(managementServer.getVncPort(vm)).thenReturn(hostPortInfo);
Mockito.doReturn(new Ternary<>(hostAddress, null, null)).when(consoleAccessManager).parseHostInfo(Mockito.anyString());
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetails(vm, host);
Assert.assertNotNull(result);
Assert.assertEquals(hostAddress, result.getHost());
Assert.assertEquals(port, result.getPort());
}
@Test
public void getConsoleConnectionDetailsReturnsDetailsWithRDPForHyperV() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
String hostAddress = "192.168.1.100";
Pair<String, Integer> hostPortInfo = new Pair<>(hostAddress, -9);
Mockito.when(vm.getUuid()).thenReturn("vm-uuid");
Mockito.when(vm.getHostName()).thenReturn("vm-hostname");
Mockito.when(vm.getVncPassword()).thenReturn("vnc-password");
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.Hyperv);
Mockito.when(vmInstanceDetailsDao.listDetailsKeyPairs(Mockito.anyLong(), Mockito.anyList())).thenReturn(Map.of());
Mockito.when(managementServer.getVncPort(vm)).thenReturn(hostPortInfo);
int port = 3389;
DetailVO detailVO = Mockito.mock(DetailVO.class);
Mockito.when(detailVO.getValue()).thenReturn(String.valueOf(port));
Mockito.when(managementServer.findDetail(Mockito.anyLong(), Mockito.eq("rdp.server.port"))).thenReturn(detailVO);
Mockito.doReturn(new Ternary<>(hostAddress, null, null)).when(consoleAccessManager).parseHostInfo(Mockito.anyString());
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetails(vm, host);
Assert.assertNotNull(result);
Assert.assertTrue(result.isUsingRDP());
Assert.assertEquals(port, result.getPort());
}
@Test
public void getConsoleConnectionDetailsReturnsNullHostInvalidPortWhenVncPortInfoIsMissing() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(vm.getUuid()).thenReturn("vm-uuid");
Mockito.when(vm.getHostName()).thenReturn("vm-hostname");
Mockito.when(vm.getVncPassword()).thenReturn("vnc-password");
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(vmInstanceDetailsDao.listDetailsKeyPairs(Mockito.anyLong(), Mockito.anyList())).thenReturn(Map.of());
Mockito.when(managementServer.getVncPort(vm)).thenReturn(new Pair<>(null, -1));
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetails(vm, host);
Assert.assertNull(result.getHost());
Assert.assertEquals(-1, result.getPort());
}
@Test
public void getConsoleConnectionDetailsSetsLocaleWhenKeyboardDetailIsPresent() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
String hostAddress = "192.168.1.100";
Pair<String, Integer> hostPortInfo = new Pair<>(hostAddress, 5900);
Mockito.when(vm.getUuid()).thenReturn("vm-uuid");
Mockito.when(vm.getHostName()).thenReturn("vm-hostname");
Mockito.when(vm.getVncPassword()).thenReturn("vnc-password");
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(vmInstanceDetailsDao.listDetailsKeyPairs(Mockito.anyLong(), Mockito.anyList())).thenReturn(Map.of(VmDetailConstants.KEYBOARD, "en-us"));
Mockito.when(managementServer.getVncPort(vm)).thenReturn(hostPortInfo);
Mockito.doReturn(new Ternary<>(hostAddress, null, null)).when(consoleAccessManager).parseHostInfo(Mockito.anyString());
ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetails(vm, host);
Assert.assertNotNull(result);
Assert.assertEquals("en-us", result.getLocale());
}
@Test
public void generateConsoleProxyClientParamSetsBasicDetailsCorrectly() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
String hostAddress = "192.168.1.100";
int port = 5902;
String sid = "sid";
String tag = "tag";
String displayName = "displayName";
String ticket = "ticket";
String sessionUuid = "sessionUuid";
String sourceIp = "127.0.0.1";
ConsoleConnectionDetails details = new ConsoleConnectionDetails(sid, null, tag, displayName);
details.setHost(hostAddress);
details.setPort(port);
ConsoleProxyClientParam param = consoleAccessManager.generateConsoleProxyClientParam(details, ticket, sessionUuid, sourceIp, null, vm, host);
Assert.assertEquals(hostAddress, param.getClientHostAddress());
Assert.assertEquals(port, param.getClientHostPort());
Assert.assertEquals(sid, param.getClientHostPassword());
Assert.assertEquals(tag, param.getClientTag());
Assert.assertEquals(displayName, param.getClientDisplayName());
Assert.assertEquals(ticket, param.getTicket());
Assert.assertEquals(sessionUuid, param.getSessionUuid());
Assert.assertEquals(sourceIp, param.getSourceIP());
Assert.assertNull(param.getLocale());
Assert.assertNull(param.getExtraSecurityToken());
}
@Test
public void generateConsoleProxyClientParamSetsExtraSecurityTokenWhenProvided() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails("password", null, null, null);
ConsoleProxyClientParam param = consoleAccessManager.generateConsoleProxyClientParam(details, "ticket", "sessionUuid", "127.0.0.1", "extraToken", vm, host);
Assert.assertEquals("extraToken", param.getExtraSecurityToken());
}
@Test
public void generateConsoleProxyClientParamSetsLocaleWhenProvided() {
HostVO host = Mockito.mock(HostVO.class);
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails(null, "fr-fr", null, null);
ConsoleProxyClientParam param = consoleAccessManager.generateConsoleProxyClientParam(details, "ticket", "sessionUuid", "127.0.0.1", null, vm, host);
Assert.assertEquals("fr-fr", param.getLocale());
}
@Test
public void generateConsoleProxyClientParamSetsRdpDetailsForHyperV() {
long hostId = 1L;
String username = "admin";
String password = "adminPass";
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getId()).thenReturn(hostId);
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails(null, null, null, null);
details.setUsingRDP(true);
String ip = "10.0.0.1";
Mockito.when(host.getPrivateIpAddress()).thenReturn(ip);
Mockito.when(managementServer.findDetail(host.getId(), "username")).thenReturn(new DetailVO(hostId, "username", username));
Mockito.when(managementServer.findDetail(host.getId(), "password")).thenReturn(new DetailVO(hostId, "password", password));
ConsoleProxyClientParam param = consoleAccessManager.generateConsoleProxyClientParam(details, "ticket", "sessionUuid", "127.0.0.1", null, vm, host);
Assert.assertEquals(ip, param.getHypervHost());
Assert.assertEquals(username, param.getUsername());
Assert.assertEquals(password, param.getPassword());
}
@Test
public void generateConsoleProxyClientParamSetsTunnelDetailsWhenProvided() {
HostVO host = Mockito.mock(HostVO.class);
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
ConsoleConnectionDetails details = new ConsoleConnectionDetails(null, null, null, null);
details.setTunnelUrl("tunnelUrl");
details.setTunnelSession("tunnelSession");
ConsoleProxyClientParam param = consoleAccessManager.generateConsoleProxyClientParam(details, "ticket", "sessionUuid", "127.0.0.1", null, vm, host);
Assert.assertEquals("tunnelUrl", param.getClientTunnelUrl());
Assert.assertEquals("tunnelSession", param.getClientTunnelSession());
}
@Test
public void returnsNullWhenConsoleConnectionDetailsAreNull() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
HostVO host = Mockito.mock(HostVO.class);
Mockito.doReturn(null).when(consoleAccessManager).getConsoleConnectionDetails(vm, host);
ConsoleEndpoint result = consoleAccessManager.composeConsoleAccessEndpoint("rootUrl", vm, host, "addr", "sessionUuid", "extraToken");
Assert.assertNotNull(result);
Assert.assertFalse(result.isResult());
Assert.assertNull(result.getUrl());
Assert.assertEquals("Console access to this instance cannot be provided", result.getDetails());
}
@Test
public void composeConsoleAccessEndpointReturnsConsoleEndpointWhenConsoleConnectionDetailsAreValid() {
String locale = "en";
String hostStr = "192.168.1.100";
int port = 5900;
String sid = "SID";
String sessionUuid = UUID.randomUUID().toString();
String ticket = UUID.randomUUID().toString();
String addr = "addr";
String extraToken = "extraToken";
String rootUrl = "rootUrl";
int vncPort = 443;
long vmId = 100L;
long hostId = 1L;
String url = "url";
String consoleAddress = "127.0.0.1";
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Mockito.when(vm.getId()).thenReturn(vmId);
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getId()).thenReturn(hostId);
String tag = UUID.randomUUID().toString();
ConsoleConnectionDetails details = new ConsoleConnectionDetails("password", locale, tag, null);
details.setHost(hostStr);
details.setPort(port);
details.setSid(sid);
Mockito.doReturn(details).when(consoleAccessManager).getConsoleConnectionDetails(vm, host);
Mockito.when(consoleProxyManager.getVncPort(Mockito.anyLong())).thenReturn(vncPort);
ConsoleProxyPasswordBasedEncryptor.KeyIVPair keyIvPair = new ConsoleProxyPasswordBasedEncryptor.KeyIVPair("key", "iv");
Mockito.doReturn(GsonHelper.getGson().toJson(keyIvPair)).when(consoleAccessManager).getEncryptorPassword();
Mockito.doReturn(ticket).when(consoleAccessManager).genAccessTicket(hostStr, String.valueOf(port), sid, tag, sessionUuid);
ConsoleProxyClientParam param = Mockito.mock(ConsoleProxyClientParam.class);
Mockito.when(param.getExtraSecurityToken()).thenReturn(extraToken);
Mockito.doReturn(param).when(consoleAccessManager).generateConsoleProxyClientParam(details, ticket, sessionUuid, addr, extraToken, vm, host);
Mockito.doReturn(url).when(consoleAccessManager).generateConsoleAccessUrl(Mockito.eq(rootUrl),
Mockito.eq(param), Mockito.anyString(), Mockito.eq(vncPort), Mockito.eq(vm), Mockito.eq(host),
Mockito.eq(locale));
Mockito.doNothing().when(consoleAccessManager).persistConsoleSession(sessionUuid, vmId, hostId, addr);
Mockito.when(managementServer.getConsoleAccessAddress(vmId)).thenReturn(consoleAddress);
ConsoleEndpoint endpoint = consoleAccessManager.composeConsoleAccessEndpoint(rootUrl, vm, host, addr, sessionUuid, extraToken);
Mockito.verify(consoleAccessManager).persistConsoleSession(sessionUuid, vmId, hostId, addr);
Mockito.verify(managementServer).setConsoleAccessForVm(vmId, sessionUuid);
Assert.assertEquals(url, endpoint.getUrl());
Assert.assertEquals(ConsoleAccessManagerImpl.WEB_SOCKET_PATH, endpoint.getWebsocketPath());
Assert.assertEquals(extraToken, endpoint.getWebsocketExtra());
Assert.assertEquals(consoleAddress, endpoint.getWebsocketHost());
}
@Test
public void composeConsoleAccessEndpointReturnsWithoutPersistWhenConsoleConnectionDetailsAreValidDirect() {
String url = "url";
long vmId = 100L;
long hostId = 1L;
String sessionUuid = UUID.randomUUID().toString();
String addr = "addr";
ConsoleConnectionDetails details = new ConsoleConnectionDetails("password", "en", "tag", null);
details.setDirectUrl(url);
details.setModeFromExternalProtocol("direct");
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Mockito.when(vm.getId()).thenReturn(vmId);
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getId()).thenReturn(hostId);
Mockito.doReturn(details).when(consoleAccessManager).getConsoleConnectionDetails(vm, host);
Mockito.doNothing().when(consoleAccessManager).persistConsoleSession(sessionUuid, vmId, hostId, addr);
ConsoleEndpoint endpoint = consoleAccessManager.composeConsoleAccessEndpoint("rootUrl", vm, host, addr, sessionUuid, "");
Mockito.verify(consoleAccessManager).persistConsoleSession(sessionUuid, vmId, hostId, addr);
Mockito.verify(managementServer, Mockito.never()).setConsoleAccessForVm(Mockito.anyLong(), Mockito.anyString());
Assert.assertEquals(url, endpoint.getUrl());
Assert.assertNull(endpoint.getWebsocketPath());
Assert.assertNull(endpoint.getWebsocketExtra());
Assert.assertNull(endpoint.getWebsocketHost());
}
}

View File

@ -36,6 +36,8 @@ import java.util.concurrent.Executor;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;
import org.eclipse.jetty.websocket.api.Session;
@ -43,9 +45,6 @@ import com.cloud.utils.PropertiesUtil;
import com.google.gson.Gson;
import com.sun.net.httpserver.HttpServer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
*
* ConsoleProxy, singleton class that manages overall activities in console proxy process. To make legacy code work, we still
@ -598,6 +597,8 @@ public class ConsoleProxy {
Session session) throws AuthenticationException {
boolean reportLoadChange = false;
String clientKey = param.getClientMapKey();
LOGGER.debug("Getting NoVNC viewer for {}. Session requires new viewer: {}, client tag: {}. session UUID: {}",
clientKey, param.isSessionRequiresNewViewer(), param.getClientTag(), param.getSessionUuid());
synchronized (connectionMap) {
ConsoleProxyClient viewer = connectionMap.get(clientKey);
if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) {

View File

@ -16,6 +16,8 @@
// under the License.
package com.cloud.consoleproxy;
import org.apache.commons.lang3.StringUtils;
/**
*
* Data object to store parameter info needed by client to connect to its host
@ -39,6 +41,8 @@ public class ConsoleProxyClientParam {
private String password;
private String websocketUrl;
private boolean sessionRequiresNewViewer;
/**
* IP that has generated the console endpoint
*/
@ -143,8 +147,12 @@ public class ConsoleProxyClientParam {
}
public String getClientMapKey() {
if (clientTag != null && !clientTag.isEmpty())
if (sessionRequiresNewViewer && StringUtils.isNotBlank(sessionUuid)) {
return sessionUuid;
}
if (StringUtils.isNotBlank(clientTag)) {
return clientTag;
}
return clientHostAddress + ":" + clientHostPort;
}
@ -220,4 +228,12 @@ public class ConsoleProxyClientParam {
public void setClientIp(String clientIp) {
this.clientIp = clientIp;
}
public boolean isSessionRequiresNewViewer() {
return sessionRequiresNewViewer;
}
public void setSessionRequiresNewViewer(boolean sessionRequiresNewViewer) {
this.sessionRequiresNewViewer = sessionRequiresNewViewer;
}
}

View File

@ -110,6 +110,9 @@ public class ConsoleProxyHttpHandlerHelper {
if (param.getExtraSecurityToken() != null) {
map.put("extraSecurityToken", param.getExtraSecurityToken());
}
if (param.isSessionRequiresNewViewer()) {
map.put("sessionRequiresNewViewer", Boolean.TRUE.toString());
}
} else {
LOGGER.error("Unable to decode token");
}

View File

@ -93,6 +93,7 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
String websocketUrl = queryMap.get("websocketUrl");
String sessionUuid = queryMap.get("sessionUuid");
String clientIp = session.getRemoteAddress().getAddress().getHostAddress();
boolean sessionRequiresNewViewer = Boolean.parseBoolean(queryMap.get("sessionRequiresNewViewer"));
if (tag == null)
tag = "";
@ -141,6 +142,7 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
param.setSessionUuid(sessionUuid);
param.setSourceIP(sourceIP);
param.setClientIp(clientIp);
param.setSessionRequiresNewViewer(sessionRequiresNewViewer);
if (queryMap.containsKey("extraSecurityToken")) {
param.setExtraSecurityToken(queryMap.get("extraSecurityToken"));

View File

@ -109,7 +109,12 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
String tunnelSession = param.getClientTunnelSession();
String websocketUrl = param.getWebsocketUrl();
connectClientToVNCServer(tunnelUrl, tunnelSession, websocketUrl);
if (!connectClientToVNCServer(tunnelUrl, tunnelSession, websocketUrl)) {
logger.error("Failed to connect to VNC server, will close connection with client [{}] [IP: {}].", clientId, clientSourceIp);
connectionAlive = false;
session.close();
return;
}
authenticateToVNCServer(clientSourceIp);
// Track consecutive iterations with no data and sleep accordingly. Only used for NIO socket connections.
@ -313,7 +318,7 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
* - 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) {
private boolean connectClientToVNCServer(String tunnelUrl, String tunnelSession, String websocketUrl) {
try {
if (StringUtils.isNotBlank(websocketUrl)) {
logger.info(String.format("Connect to VNC over websocket URL: %s", websocketUrl));
@ -337,7 +342,9 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
logger.info("Connection to VNC server has been established successfully.");
} catch (Throwable e) {
logger.error("Unexpected exception while connecting to VNC server.", e);
return false;
}
return true;
}
private void setClientParam(ConsoleProxyClientParam param) {

View File

@ -88,15 +88,16 @@ public class NoVncClient {
setTunnelSocketStreams();
}
public void connectTo(String host, int port) {
public void connectTo(String host, int port) throws IOException {
// Connect to server
logger.info("Connecting to VNC server {}:{} ...", host, port);
try {
NioSocket nioSocket = new NioSocket(host, port);
this.nioSocketConnection = new NioSocketHandlerImpl(nioSocket);
} catch (Exception e) {
} catch (IOException e) {
logger.error(String.format("Cannot create socket to host: %s and port %s: %s", host, port,
e.getMessage()), e);
throw e;
}
}

View File

@ -36,7 +36,7 @@ public class NioSocket {
private static final int CONNECTION_TIMEOUT_MILLIS = 3000;
protected Logger logger = LogManager.getLogger(getClass());
private void initializeSocket() {
private void initializeSocket() throws IOException {
try {
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
@ -49,30 +49,27 @@ public class NioSocket {
socketChannel.register(readSelector, SelectionKey.OP_READ);
} catch (IOException e) {
logger.error("Could not initialize NioSocket: " + e.getMessage(), e);
throw e;
}
}
private void waitForSocketSelectorConnected(Selector selector) {
try {
while (selector.select(CONNECTION_TIMEOUT_MILLIS) <= 0) {
logger.debug("Waiting for ready operations to connect to the socket");
}
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey selectionKey: keys) {
if (selectionKey.isConnectable()) {
if (socketChannel.isConnectionPending()) {
socketChannel.finishConnect();
}
logger.debug("Connected to the socket");
break;
private void waitForSocketSelectorConnected(Selector selector) throws IOException {
while (selector.select(CONNECTION_TIMEOUT_MILLIS) <= 0) {
logger.debug("Waiting for ready operations to connect to the socket");
}
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey selectionKey: keys) {
if (selectionKey.isConnectable()) {
if (socketChannel.isConnectionPending()) {
socketChannel.finishConnect();
}
logger.debug("Connected to the socket");
break;
}
} catch (IOException e) {
logger.error(String.format("Error waiting for socket selector ready: %s", e.getMessage()), e);
}
}
private void connectSocket(String host, int port) {
private void connectSocket(String host, int port) throws IOException {
try {
socketChannel.connect(new InetSocketAddress(host, port));
Selector selector = Selector.open();
@ -80,11 +77,12 @@ public class NioSocket {
waitForSocketSelectorConnected(selector);
} catch (IOException e) {
logger.error(String.format("Error creating NioSocket to %s:%s: %s", host, port, e.getMessage()), e);
logger.error("Error connecting NioSocket to {}:{}: {}", host, port, e.getMessage(), e);
throw e;
}
}
public NioSocket(String host, int port) {
public NioSocket(String host, int port) throws IOException {
initializeSocket();
connectSocket(host, port);
}

View File

@ -17,7 +17,7 @@
<template>
<span class="row-action-button">
<a-tooltip arrowPointAtCenter placement="bottomRight" v-if="resource && resource.id && dataView && resource.hypervisor !== 'External'">
<a-tooltip arrowPointAtCenter placement="bottomRight" v-if="resource && resource.id && dataView">
<template #title>
{{ $t('label.view.console') }}
</template>
@ -29,7 +29,7 @@
icon="code"
/>
</a-tooltip>
<a-tooltip arrowPointAtCenter placement="bottomRight" v-if="resource && resource.id && dataView && resource.hypervisor !== 'External'">
<a-tooltip arrowPointAtCenter placement="bottomRight" v-if="resource && resource.id && dataView">
<template #title>
{{ $t('label.copy.consoleurl') }}
</template>