Skip to content

Commit c1c1b0e

Browse files
authored
extension: improve host vm power reporting (#11619)
* extension/proxmox: improve host vm power reporting Add `statuses` action in extensions to report VM power states This PR introduces support for retrieving the power state of all VMs on a host directly from an extension using the new `statuses` action. When available, this provides a single aggregated response, reducing the need for multiple calls. If the extension does not implement `statuses`, the server will gracefully fall back to querying individual VMs using the existing `status` action. This helps with updating the host in CloudStack after out-of-band migrations for the VM. Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * address review Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> --------- Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent 81f16b6 commit c1c1b0e

File tree

5 files changed

+345
-71
lines changed

5 files changed

+345
-71
lines changed

extensions/HyperV/hyperv.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,29 @@ def status(self):
210210
power_state = "poweroff"
211211
succeed({"status": "success", "power_state": power_state})
212212

213+
def statuses(self):
214+
command = 'Get-VM | Select-Object Name, State | ConvertTo-Json'
215+
output = self.run_ps(command)
216+
if not output or output.strip() in ("", "null"):
217+
vms = []
218+
else:
219+
try:
220+
vms = json.loads(output)
221+
except json.JSONDecodeError:
222+
fail("Failed to parse VM status output: " + output)
223+
power_state = {}
224+
if isinstance(vms, dict):
225+
vms = [vms]
226+
for vm in vms:
227+
state = vm["State"].strip().lower()
228+
if state == "running":
229+
power_state[vm["Name"]] = "poweron"
230+
elif state == "off":
231+
power_state[vm["Name"]] = "poweroff"
232+
else:
233+
power_state[vm["Name"]] = "unknown"
234+
succeed({"status": "success", "power_state": power_state})
235+
213236
def delete(self):
214237
try:
215238
self.run_ps_int(f'Remove-VM -Name "{self.data["vmname"]}" -Force')
@@ -286,6 +309,7 @@ def main():
286309
"reboot": manager.reboot,
287310
"delete": manager.delete,
288311
"status": manager.status,
312+
"statuses": manager.statuses,
289313
"getconsole": manager.get_console,
290314
"suspend": manager.suspend,
291315
"resume": manager.resume,

extensions/Proxmox/proxmox.sh

Lines changed: 103 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ parse_json() {
6464
token="${host_token:-$extension_token}"
6565
secret="${host_secret:-$extension_secret}"
6666

67-
check_required_fields vm_internal_name url user token secret node
67+
check_required_fields url user token secret node
6868
}
6969

7070
urlencode() {
@@ -206,6 +206,10 @@ prepare() {
206206

207207
create() {
208208
if [[ -z "$vm_name" ]]; then
209+
if [[ -z "$vm_internal_name" ]]; then
210+
echo '{"error":"Missing required fields: vm_internal_name"}'
211+
exit 1
212+
fi
209213
vm_name="$vm_internal_name"
210214
fi
211215
validate_name "VM" "$vm_name"
@@ -331,71 +335,102 @@ get_node_host() {
331335
echo "$host"
332336
}
333337

334-
get_console() {
335-
check_required_fields node vmid
336-
337-
local api_resp port ticket
338-
if ! api_resp="$(call_proxmox_api POST "/nodes/${node}/qemu/${vmid}/vncproxy")"; then
339-
echo "$api_resp" | jq -c '{status:"error", error:(.errors.curl // (.errors|tostring))}'
340-
exit 1
341-
fi
342-
343-
port="$(echo "$api_resp" | jq -re '.data.port // empty' 2>/dev/null || true)"
344-
ticket="$(echo "$api_resp" | jq -re '.data.ticket // empty' 2>/dev/null || true)"
345-
346-
if [[ -z "$port" || -z "$ticket" ]]; then
347-
jq -n --arg raw "$api_resp" \
348-
'{status:"error", error:"Proxmox response missing port/ticket", upstream:$raw}'
349-
exit 1
350-
fi
351-
352-
# Derive host from node’s network info
353-
local host
354-
host="$(get_node_host)"
355-
if [[ -z "$host" ]]; then
356-
jq -n --arg msg "Could not determine host IP for node $node" \
357-
'{status:"error", error:$msg}'
358-
exit 1
359-
fi
360-
361-
jq -n \
362-
--arg host "$host" \
363-
--arg port "$port" \
364-
--arg password "$ticket" \
365-
--argjson passwordonetimeuseonly true \
366-
'{
367-
status: "success",
368-
message: "Console retrieved",
369-
console: {
370-
host: $host,
371-
port: $port,
372-
password: $password,
373-
passwordonetimeuseonly: $passwordonetimeuseonly,
374-
protocol: "vnc"
375-
}
376-
}'
377-
}
338+
get_console() {
339+
check_required_fields node vmid
378340

379-
list_snapshots() {
380-
snapshot_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/snapshot")
381-
echo "$snapshot_response" | jq '
382-
def to_date:
383-
if . == "-" then "-"
384-
elif . == null then "-"
385-
else (. | tonumber | strftime("%Y-%m-%d %H:%M:%S"))
386-
end;
341+
local api_resp port ticket
342+
if ! api_resp="$(call_proxmox_api POST "/nodes/${node}/qemu/${vmid}/vncproxy")"; then
343+
echo "$api_resp" | jq -c '{status:"error", error:(.errors.curl // (.errors|tostring))}'
344+
exit 1
345+
fi
346+
347+
port="$(echo "$api_resp" | jq -re '.data.port // empty' 2>/dev/null || true)"
348+
ticket="$(echo "$api_resp" | jq -re '.data.ticket // empty' 2>/dev/null || true)"
349+
350+
if [[ -z "$port" || -z "$ticket" ]]; then
351+
jq -n --arg raw "$api_resp" \
352+
'{status:"error", error:"Proxmox response missing port/ticket", upstream:$raw}'
353+
exit 1
354+
fi
355+
356+
# Derive host from node’s network info
357+
local host
358+
host="$(get_node_host)"
359+
if [[ -z "$host" ]]; then
360+
jq -n --arg msg "Could not determine host IP for node $node" \
361+
'{status:"error", error:$msg}'
362+
exit 1
363+
fi
364+
365+
jq -n \
366+
--arg host "$host" \
367+
--arg port "$port" \
368+
--arg password "$ticket" \
369+
--argjson passwordonetimeuseonly true \
370+
'{
371+
status: "success",
372+
message: "Console retrieved",
373+
console: {
374+
host: $host,
375+
port: $port,
376+
password: $password,
377+
passwordonetimeuseonly: $passwordonetimeuseonly,
378+
protocol: "vnc"
379+
}
380+
}'
381+
}
382+
383+
statuses() {
384+
local response
385+
response=$(call_proxmox_api GET "/nodes/${node}/qemu")
386+
387+
if [[ -z "$response" ]]; then
388+
echo '{"status":"error","message":"empty response from Proxmox API"}'
389+
return 1
390+
fi
391+
392+
if ! echo "$response" | jq empty >/dev/null 2>&1; then
393+
echo '{"status":"error","message":"invalid JSON response from Proxmox API"}'
394+
return 1
395+
fi
396+
397+
echo "$response" | jq -c '
398+
def map_state(s):
399+
if s=="running" then "poweron"
400+
elif s=="stopped" then "poweroff"
401+
else "unknown" end;
387402
388403
{
389404
status: "success",
390-
printmessage: "true",
391-
message: [.data[] | {
392-
name: .name,
393-
snaptime: ((.snaptime // "-") | to_date),
394-
description: .description,
395-
parent: (.parent // "-"),
396-
vmstate: (.vmstate // "-")
397-
}]
398-
}
405+
power_state: (
406+
.data
407+
| map(select(.template != 1))
408+
| map({ ( (.name // (.vmid|tostring)) ): map_state(.status) })
409+
| add // {}
410+
)
411+
}'
412+
}
413+
414+
list_snapshots() {
415+
snapshot_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/snapshot")
416+
echo "$snapshot_response" | jq '
417+
def to_date:
418+
if . == "-" then "-"
419+
elif . == null then "-"
420+
else (. | tonumber | strftime("%Y-%m-%d %H:%M:%S"))
421+
end;
422+
423+
{
424+
status: "success",
425+
printmessage: "true",
426+
message: [.data[] | {
427+
name: .name,
428+
snaptime: ((.snaptime // "-") | to_date),
429+
description: .description,
430+
parent: (.parent // "-"),
431+
vmstate: (.vmstate // "-")
432+
}]
433+
}
399434
'
400435
}
401436

@@ -463,9 +498,9 @@ parse_json "$parameters" || exit 1
463498

464499
cleanup_vm=0
465500
cleanup() {
466-
if (( cleanup_vm == 1 )); then
467-
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}"
468-
fi
501+
if (( cleanup_vm == 1 )); then
502+
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}"
503+
fi
469504
}
470505

471506
trap cleanup EXIT
@@ -492,6 +527,9 @@ case $action in
492527
status)
493528
status
494529
;;
530+
statuses)
531+
statuses
532+
;;
495533
getconsole)
496534
get_console
497535
;;

plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import com.cloud.agent.api.StopAnswer;
7272
import com.cloud.agent.api.StopCommand;
7373
import com.cloud.agent.api.to.VirtualMachineTO;
74+
import com.cloud.host.Host;
7475
import com.cloud.host.HostVO;
7576
import com.cloud.host.dao.HostDao;
7677
import com.cloud.hypervisor.ExternalProvisioner;
@@ -128,7 +129,7 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter
128129
private ExecutorService payloadCleanupExecutor;
129130
private ScheduledExecutorService payloadCleanupScheduler;
130131
private static final List<String> TRIVIAL_ACTIONS = Arrays.asList(
131-
"status"
132+
"status", "statuses"
132133
);
133134

134135
@Override
@@ -456,7 +457,7 @@ public StopAnswer expungeInstance(String hostName, String extensionName, String
456457
@Override
457458
public Map<String, HostVmStateReportEntry> getHostVmStateReport(long hostId, String extensionName,
458459
String extensionRelativePath) {
459-
final Map<String, HostVmStateReportEntry> vmStates = new HashMap<>();
460+
Map<String, HostVmStateReportEntry> vmStates = new HashMap<>();
460461
String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath);
461462
if (StringUtils.isEmpty(extensionPath)) {
462463
return vmStates;
@@ -466,14 +467,20 @@ public Map<String, HostVmStateReportEntry> getHostVmStateReport(long hostId, Str
466467
logger.error("Host with ID: {} not found", hostId);
467468
return vmStates;
468469
}
470+
Map<String, Map<String, String>> accessDetails =
471+
extensionsManager.getExternalAccessDetails(host, null);
472+
vmStates = getVmPowerStates(host, accessDetails, extensionName, extensionPath);
473+
if (vmStates != null) {
474+
logger.debug("Found {} VMs on the host {}", vmStates.size(), host);
475+
return vmStates;
476+
}
477+
vmStates = new HashMap<>();
469478
List<UserVmVO> allVms = _uservmDao.listByHostId(hostId);
470479
allVms.addAll(_uservmDao.listByLastHostId(hostId));
471480
if (CollectionUtils.isEmpty(allVms)) {
472481
logger.debug("No VMs found for the {}", host);
473482
return vmStates;
474483
}
475-
Map<String, Map<String, String>> accessDetails =
476-
extensionsManager.getExternalAccessDetails(host, null);
477484
for (UserVmVO vm: allVms) {
478485
VirtualMachine.PowerState powerState = getVmPowerState(vm, accessDetails, extensionName, extensionPath);
479486
vmStates.put(vm.getInstanceName(), new HostVmStateReportEntry(powerState, "host-" + hostId));
@@ -714,7 +721,7 @@ protected VirtualMachine.PowerState parsePowerStateFromResponse(UserVmVO userVmV
714721
return getPowerStateFromString(response);
715722
}
716723
try {
717-
JsonObject jsonObj = new JsonParser().parse(response).getAsJsonObject();
724+
JsonObject jsonObj = JsonParser.parseString(response).getAsJsonObject();
718725
String powerState = jsonObj.has("power_state") ? jsonObj.get("power_state").getAsString() : null;
719726
return getPowerStateFromString(powerState);
720727
} catch (Exception e) {
@@ -724,7 +731,7 @@ protected VirtualMachine.PowerState parsePowerStateFromResponse(UserVmVO userVmV
724731
}
725732
}
726733

727-
private VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map<String, Map<String, String>> accessDetails,
734+
protected VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map<String, Map<String, String>> accessDetails,
728735
String extensionName, String extensionPath) {
729736
VirtualMachineTO virtualMachineTO = getVirtualMachineTO(userVmVO);
730737
accessDetails.put(ApiConstants.VIRTUAL_MACHINE, virtualMachineTO.getExternalDetails());
@@ -740,6 +747,46 @@ private VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map<String,
740747
}
741748
return parsePowerStateFromResponse(userVmVO, result.second());
742749
}
750+
751+
protected Map<String, HostVmStateReportEntry> getVmPowerStates(Host host,
752+
Map<String, Map<String, String>> accessDetails, String extensionName, String extensionPath) {
753+
Map<String, Object> modifiedDetails = loadAccessDetails(accessDetails, null);
754+
logger.debug("Trying to get VM power statuses from the external system for {}", host);
755+
Pair<Boolean, String> result = getInstanceStatusesOnExternalSystem(extensionName, extensionPath,
756+
host.getName(), modifiedDetails, AgentManager.Wait.value());
757+
if (!result.first()) {
758+
logger.warn("Failure response received while trying to fetch the power statuses for {} : {}",
759+
host, result.second());
760+
return null;
761+
}
762+
if (StringUtils.isBlank(result.second())) {
763+
logger.warn("Empty response while trying to fetch VM power statuses for host: {}", host);
764+
return null;
765+
}
766+
try {
767+
JsonObject jsonObj = JsonParser.parseString(result.second()).getAsJsonObject();
768+
if (!jsonObj.has("status") || !"success".equalsIgnoreCase(jsonObj.get("status").getAsString())) {
769+
logger.warn("Invalid status in response while trying to fetch VM power statuses for host: {}: {}",
770+
host, result.second());
771+
return null;
772+
}
773+
if (!jsonObj.has("power_state") || !jsonObj.get("power_state").isJsonObject()) {
774+
logger.warn("Missing or invalid power_state in response for host: {}: {}", host, result.second());
775+
return null;
776+
}
777+
JsonObject powerStates = jsonObj.getAsJsonObject("power_state");
778+
Map<String, HostVmStateReportEntry> states = new HashMap<>();
779+
for (Map.Entry<String, com.google.gson.JsonElement> entry : powerStates.entrySet()) {
780+
VirtualMachine.PowerState powerState = getPowerStateFromString(entry.getValue().getAsString());
781+
states.put(entry.getKey(), new HostVmStateReportEntry(powerState, "host-" + host.getId()));
782+
}
783+
return states;
784+
} catch (Exception e) {
785+
logger.warn("Failed to parse VM power statuses response for host: {}: {}", host, e.getMessage());
786+
return null;
787+
}
788+
}
789+
743790
public Pair<Boolean, String> prepareExternalProvisioningInternal(String extensionName, String filename,
744791
String vmUUID, Map<String, Object> accessDetails, int wait) {
745792
return executeExternalCommand(extensionName, "prepare", accessDetails, wait,
@@ -783,6 +830,12 @@ public Pair<Boolean, String> getInstanceStatusOnExternalSystem(String extensionN
783830
String.format("Failed to get the instance power status %s on external system", vmUUID), filename);
784831
}
785832

833+
public Pair<Boolean, String> getInstanceStatusesOnExternalSystem(String extensionName, String filename,
834+
String hostName, Map<String, Object> accessDetails, int wait) {
835+
return executeExternalCommand(extensionName, "statuses", accessDetails, wait,
836+
String.format("Failed to get the %s instances power status on external system", hostName), filename);
837+
}
838+
786839
public Pair<Boolean, String> getInstanceConsoleOnExternalSystem(String extensionName, String filename,
787840
String vmUUID, Map<String, Object> accessDetails, int wait) {
788841
return executeExternalCommand(extensionName, "getconsole", accessDetails, wait,

0 commit comments

Comments
 (0)