Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -35,6 +35,7 @@
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;
Expand Down Expand Up @@ -86,6 +87,10 @@ private CreateConsoleEndpointResponse createResponse(ConsoleEndpoint endpoint) {
}

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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
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);
}
6 changes: 5 additions & 1 deletion extensions/HyperV/hyperv.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@


def fail(message):
print(json.dumps({"error": message}))
print(json.dumps({"status": "error", "error": message}))
sys.exit(1)


Expand Down Expand Up @@ -220,6 +220,9 @@ def delete(self):
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"})
Expand Down Expand Up @@ -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,
Expand Down
105 changes: 99 additions & 6 deletions extensions/Proxmox/proxmox.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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 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", 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 '
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -396,6 +486,9 @@ case $action in
status)
status
;;
getconsole)
get_console
;;
ListSnapshots)
list_snapshots
;;
Expand All @@ -409,7 +502,7 @@ case $action in
delete_snapshot
;;
*)
echo '{"error":"Invalid action"}'
echo '{"status": "error", "error": "Invalid action"}'
exit 1
;;
esac
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);
}
Loading
Loading