Skip to content
Open
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
24 changes: 24 additions & 0 deletions extensions/HyperV/hyperv.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,29 @@ def status(self):
power_state = "poweroff"
succeed({"status": "success", "power_state": power_state})

def statuses(self):
command = 'Get-VM | Select-Object Name, State | ConvertTo-Json'
output = self.run_ps(command)
if not output or output.strip() in ("", "null"):
vms = []
else:
try:
vms = json.loads(output)
except json.JSONDecodeError:
fail("Failed to parse VM status output: " + output)
power_state = {}
if isinstance(vms, dict):
vms = [vms]
for vm in vms:
state = vm["State"].strip().lower()
if state == "running":
power_state[vm["Name"]] = "poweron"
elif state == "off":
power_state[vm["Name"]] = "poweroff"
else:
power_state[vm["Name"]] = "unknown"
succeed({"status": "success", "power_state": power_state})

def delete(self):
try:
self.run_ps_int(f'Remove-VM -Name "{self.data["vmname"]}" -Force')
Expand Down Expand Up @@ -286,6 +309,7 @@ def main():
"reboot": manager.reboot,
"delete": manager.delete,
"status": manager.status,
"statuses": manager.statuses,
"getconsole": manager.get_console,
"suspend": manager.suspend,
"resume": manager.resume,
Expand Down
168 changes: 103 additions & 65 deletions extensions/Proxmox/proxmox.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ parse_json() {
token="${host_token:-$extension_token}"
secret="${host_secret:-$extension_secret}"

check_required_fields vm_internal_name url user token secret node
check_required_fields url user token secret node
}

urlencode() {
Expand Down Expand Up @@ -202,6 +202,10 @@ prepare() {

create() {
if [[ -z "$vm_name" ]]; then
if [[ -z "$vm_internal_name" ]]; then
echo '{"error":"Missing required fields: vm_internal_name"}'
exit 1
fi
vm_name="$vm_internal_name"
fi
validate_name "VM" "$vm_name"
Expand Down Expand Up @@ -325,71 +329,102 @@ get_node_host() {
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"
}
}'
}
get_console() {
check_required_fields node vmid

list_snapshots() {
snapshot_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/snapshot")
echo "$snapshot_response" | jq '
def to_date:
if . == "-" then "-"
elif . == null then "-"
else (. | tonumber | strftime("%Y-%m-%d %H:%M:%S"))
end;
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"
}
}'
}

statuses() {
local response
response=$(call_proxmox_api GET "/nodes/${node}/qemu")

if [[ -z "$response" ]]; then
echo '{"status":"error","message":"empty response from Proxmox API"}'
return 1
fi

if ! echo "$response" | jq empty >/dev/null 2>&1; then
echo '{"status":"error","message":"invalid JSON response from Proxmox API"}'
return 1
fi

echo "$response" | jq -c '
def map_state(s):
if s=="running" then "poweron"
elif s=="stopped" then "poweroff"
else "unknown" end;

{
status: "success",
printmessage: "true",
message: [.data[] | {
name: .name,
snaptime: ((.snaptime // "-") | to_date),
description: .description,
parent: (.parent // "-"),
vmstate: (.vmstate // "-")
}]
}
power_state: (
.data
| map(select(.template != 1))
| map({ ( (.name // (.vmid|tostring)) ): map_state(.status) })
| add // {}
)
}'
}

list_snapshots() {
snapshot_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/snapshot")
echo "$snapshot_response" | jq '
def to_date:
if . == "-" then "-"
elif . == null then "-"
else (. | tonumber | strftime("%Y-%m-%d %H:%M:%S"))
end;

{
status: "success",
printmessage: "true",
message: [.data[] | {
name: .name,
snaptime: ((.snaptime // "-") | to_date),
description: .description,
parent: (.parent // "-"),
vmstate: (.vmstate // "-")
}]
}
'
}

Expand Down Expand Up @@ -457,9 +492,9 @@ parse_json "$parameters" || exit 1

cleanup_vm=0
cleanup() {
if (( cleanup_vm == 1 )); then
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}"
fi
if (( cleanup_vm == 1 )); then
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}"
fi
}

trap cleanup EXIT
Expand All @@ -486,6 +521,9 @@ case $action in
status)
status
;;
statuses)
statuses
;;
getconsole)
get_console
;;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import com.cloud.agent.api.StopAnswer;
import com.cloud.agent.api.StopCommand;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.ExternalProvisioner;
Expand Down Expand Up @@ -128,7 +129,7 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
private ExecutorService payloadCleanupExecutor;
private ScheduledExecutorService payloadCleanupScheduler;
private static final List<String> TRIVIAL_ACTIONS = Arrays.asList(
"status"
"status", "statuses"
);

@Override
Expand Down Expand Up @@ -456,7 +457,7 @@ public StopAnswer expungeInstance(String hostName, String extensionName, String
@Override
public Map<String, HostVmStateReportEntry> getHostVmStateReport(long hostId, String extensionName,
String extensionRelativePath) {
final Map<String, HostVmStateReportEntry> vmStates = new HashMap<>();
Map<String, HostVmStateReportEntry> vmStates = new HashMap<>();
String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath);
if (StringUtils.isEmpty(extensionPath)) {
return vmStates;
Expand All @@ -466,14 +467,20 @@ public Map<String, HostVmStateReportEntry> getHostVmStateReport(long hostId, Str
logger.error("Host with ID: {} not found", hostId);
return vmStates;
}
Map<String, Map<String, String>> accessDetails =
extensionsManager.getExternalAccessDetails(host, null);
vmStates = getVmPowerStates(host, accessDetails, extensionName, extensionPath);
if (vmStates != null) {
logger.debug("Found {} VMs on the host {}", vmStates.size(), host);
return vmStates;
}
vmStates = new HashMap<>();
List<UserVmVO> allVms = _uservmDao.listByHostId(hostId);
allVms.addAll(_uservmDao.listByLastHostId(hostId));
if (CollectionUtils.isEmpty(allVms)) {
logger.debug("No VMs found for the {}", host);
return vmStates;
}
Map<String, Map<String, String>> accessDetails =
extensionsManager.getExternalAccessDetails(host, null);
for (UserVmVO vm: allVms) {
VirtualMachine.PowerState powerState = getVmPowerState(vm, accessDetails, extensionName, extensionPath);
vmStates.put(vm.getInstanceName(), new HostVmStateReportEntry(powerState, "host-" + hostId));
Expand Down Expand Up @@ -714,7 +721,7 @@ protected VirtualMachine.PowerState parsePowerStateFromResponse(UserVmVO userVmV
return getPowerStateFromString(response);
}
try {
JsonObject jsonObj = new JsonParser().parse(response).getAsJsonObject();
JsonObject jsonObj = JsonParser.parseString(response).getAsJsonObject();
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deprecated new JsonParser().parse() method has been replaced with JsonParser.parseString(), but this change should be consistent throughout the file. The same pattern should be applied to line 702 where JsonParser.parseString() is used in the new code.

Copilot uses AI. Check for mistakes.
String powerState = jsonObj.has("power_state") ? jsonObj.get("power_state").getAsString() : null;
return getPowerStateFromString(powerState);
} catch (Exception e) {
Expand All @@ -724,7 +731,7 @@ protected VirtualMachine.PowerState parsePowerStateFromResponse(UserVmVO userVmV
}
}

private VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map<String, Map<String, String>> accessDetails,
protected VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map<String, Map<String, String>> accessDetails,
String extensionName, String extensionPath) {
VirtualMachineTO virtualMachineTO = getVirtualMachineTO(userVmVO);
accessDetails.put(ApiConstants.VIRTUAL_MACHINE, virtualMachineTO.getExternalDetails());
Expand All @@ -740,6 +747,46 @@ private VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map<String,
}
return parsePowerStateFromResponse(userVmVO, result.second());
}

protected Map<String, HostVmStateReportEntry> getVmPowerStates(Host host,
Map<String, Map<String, String>> accessDetails, String extensionName, String extensionPath) {
Map<String, Object> modifiedDetails = loadAccessDetails(accessDetails, null);
logger.debug("Trying to get VM power statuses from the external system for {}", host);
Pair<Boolean, String> result = getInstanceStatusesOnExternalSystem(extensionName, extensionPath,
host.getName(), modifiedDetails, AgentManager.Wait.value());
if (!result.first()) {
logger.warn("Failure response received while trying to fetch the power statuses for {} : {}",
host, result.second());
return null;
}
if (StringUtils.isBlank(result.second())) {
logger.warn("Empty response while trying to fetch VM power statuses for host: {}", host);
return null;
}
try {
JsonObject jsonObj = JsonParser.parseString(result.second()).getAsJsonObject();
if (!jsonObj.has("status") || !"success".equalsIgnoreCase(jsonObj.get("status").getAsString())) {
logger.warn("Invalid status in response while trying to fetch VM power statuses for host: {}: {}",
host, result.second());
return null;
}
if (!jsonObj.has("power_state") || !jsonObj.get("power_state").isJsonObject()) {
logger.warn("Missing or invalid power_state in response for host: {}: {}", host, result.second());
return null;
}
JsonObject powerStates = jsonObj.getAsJsonObject("power_state");
Map<String, HostVmStateReportEntry> states = new HashMap<>();
for (Map.Entry<String, com.google.gson.JsonElement> entry : powerStates.entrySet()) {
VirtualMachine.PowerState powerState = getPowerStateFromString(entry.getValue().getAsString());
states.put(entry.getKey(), new HostVmStateReportEntry(powerState, "host-" + host.getId()));
}
return states;
} catch (Exception e) {
logger.warn("Failed to parse VM power statuses response for host: {}: {}", host, e.getMessage());
return null;
}
}

public Pair<Boolean, String> prepareExternalProvisioningInternal(String extensionName, String filename,
String vmUUID, Map<String, Object> accessDetails, int wait) {
return executeExternalCommand(extensionName, "prepare", accessDetails, wait,
Expand Down Expand Up @@ -783,6 +830,12 @@ public Pair<Boolean, String> getInstanceStatusOnExternalSystem(String extensionN
String.format("Failed to get the instance power status %s on external system", vmUUID), filename);
}

public Pair<Boolean, String> getInstanceStatusesOnExternalSystem(String extensionName, String filename,
String hostName, Map<String, Object> accessDetails, int wait) {
return executeExternalCommand(extensionName, "statuses", accessDetails, wait,
String.format("Failed to get the %s instances power status on external system", hostName), filename);
}

public Pair<Boolean, String> getInstanceConsoleOnExternalSystem(String extensionName, String filename,
String vmUUID, Map<String, Object> accessDetails, int wait) {
return executeExternalCommand(extensionName, "getconsole", accessDetails, wait,
Expand Down
Loading
Loading