Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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 host;
private int port;
@LogLevel(LogLevel.Log4jLevel.Off)
private String password;
private String protocol;

public GetExternalConsoleAnswer(Command command, String details) {
super(command, false, details);
}

public GetExternalConsoleAnswer(Command command, String host, int port, String password, String protocol) {
super(command, true, "");
this.host = host;
this.port = port;
this.password = password;
this.protocol = protocol;
}

public String getHost() {
return host;
}

public int getPort() {
return port;
}

public String getPassword() {
return password;
}

public String getProtocol() {
return protocol;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@

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) {
Expand All @@ -36,12 +38,12 @@ public String getActionName() {
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

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;
Expand Down Expand Up @@ -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);
}
85 changes: 85 additions & 0 deletions extensions/Proxmox/proxmox.sh
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,88 @@ 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

# Request VNC proxy from Proxmox
local api_resp port ticket
if ! api_resp="$(call_proxmox_api POST "/nodes/${node}/qemu/${vmid}/vncproxy")"; then
jq -n --arg msg "API call failed for node=$node vmid=$vmid" \
'{status:"error", message:$msg, code:"API_CALL_FAILED"}'
return 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 msg "Proxmox response missing port/ticket" \
--arg raw "$api_resp" \
'{status:"error", message:$msg, code:"BAD_UPSTREAM_RESPONSE", upstream:$raw}'
return 1
fi

# Derive host from node’s 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", message:$msg, code:"HOST_RESOLUTION_ERROR"}'
return 1
fi

# Success JSON to stdout
jq -n \
--arg host "$host" \
--arg port "$port" \
--arg password "$ticket" \
'{
status: "success",
message: "Console retrieved",
console: {
host: $host,
port: $port,
password: $password,
protocol: "vnc"
}
}'
}

list_snapshots() {
snapshot_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/snapshot")
echo "$snapshot_response" | jq '
Expand Down Expand Up @@ -396,6 +478,9 @@ case $action in
status)
status
;;
getconsole)
get_console "$parameters"
;;
ListSnapshots)
list_snapshots
;;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@
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 {

Expand Down Expand Up @@ -93,4 +95,6 @@ Pair<Boolean, ExtensionResourceMap> extensionResourceMapDetailsNeedUpdate(final
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@
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;
Expand Down Expand Up @@ -141,6 +143,8 @@
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;

Expand Down Expand Up @@ -1323,11 +1327,13 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) {
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();
Expand Down Expand Up @@ -1369,6 +1375,13 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) {
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());
Expand Down Expand Up @@ -1517,6 +1530,22 @@ public void updateExtensionResourceMapDetails(long extensionResourceMapId, Map<S
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())) {
return new Answer(null, false, "No enabled orchestrator extension found for the host");
}
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,
Expand Down
Loading
Loading