This is an automated email from the ASF dual-hosted git repository.
harikrishna pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.22 by this push:
new c1c1b0e7659 extension: improve host vm power reporting (#11619)
c1c1b0e7659 is described below
commit c1c1b0e7659d82a1abeb450ff40a3013cc0084eb
Author: Abhishek Kumar <[email protected]>
AuthorDate: Fri Jan 30 14:07:22 2026 +0530
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 <[email protected]>
* address review
Signed-off-by: Abhishek Kumar <[email protected]>
---------
Signed-off-by: Abhishek Kumar <[email protected]>
---
extensions/HyperV/hyperv.py | 24 +++
extensions/Proxmox/proxmox.sh | 168 +++++++++++++--------
.../ExternalPathPayloadProvisioner.java | 65 +++++++-
.../ExternalPathPayloadProvisionerTest.java | 148 ++++++++++++++++++
.../hypervisor/external/provisioner/provisioner.sh | 11 ++
5 files changed, 345 insertions(+), 71 deletions(-)
diff --git a/extensions/HyperV/hyperv.py b/extensions/HyperV/hyperv.py
index c9b1d4da77e..8e2858d3cae 100755
--- a/extensions/HyperV/hyperv.py
+++ b/extensions/HyperV/hyperv.py
@@ -210,6 +210,29 @@ class HyperVManager:
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')
@@ -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,
diff --git a/extensions/Proxmox/proxmox.sh b/extensions/Proxmox/proxmox.sh
index fc27f2f3075..459b744f44d 100755
--- a/extensions/Proxmox/proxmox.sh
+++ b/extensions/Proxmox/proxmox.sh
@@ -64,7 +64,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() {
@@ -206,6 +206,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"
@@ -331,71 +335,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 // "-")
+ }]
+ }
'
}
@@ -463,9 +498,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
@@ -492,6 +527,9 @@ case $action in
status)
status
;;
+ statuses)
+ statuses
+ ;;
getconsole)
get_console
;;
diff --git
a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java
b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java
index 92205b13c6f..fa3f4de5026 100644
---
a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java
+++
b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java
@@ -71,6 +71,7 @@ import com.cloud.agent.api.StartCommand;
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;
@@ -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
@@ -456,7 +457,7 @@ public class ExternalPathPayloadProvisioner extends
ManagerBase implements Exter
@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;
@@ -466,14 +467,20 @@ public class ExternalPathPayloadProvisioner extends
ManagerBase implements Exter
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));
@@ -714,7 +721,7 @@ public class ExternalPathPayloadProvisioner extends
ManagerBase implements Exter
return getPowerStateFromString(response);
}
try {
- JsonObject jsonObj = new
JsonParser().parse(response).getAsJsonObject();
+ JsonObject jsonObj =
JsonParser.parseString(response).getAsJsonObject();
String powerState = jsonObj.has("power_state") ?
jsonObj.get("power_state").getAsString() : null;
return getPowerStateFromString(powerState);
} catch (Exception e) {
@@ -724,7 +731,7 @@ public class ExternalPathPayloadProvisioner extends
ManagerBase implements Exter
}
}
- 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());
@@ -740,6 +747,46 @@ public class ExternalPathPayloadProvisioner extends
ManagerBase implements Exter
}
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,
@@ -783,6 +830,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> 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,
diff --git
a/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisionerTest.java
b/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisionerTest.java
index d0a396f7a94..e8ab92c986e 100644
---
a/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisionerTest.java
+++
b/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisionerTest.java
@@ -79,6 +79,7 @@ import com.cloud.agent.api.StartCommand;
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.Hypervisor;
@@ -761,6 +762,37 @@ public class ExternalPathPayloadProvisionerTest {
assertNull(result);
}
+ @Test
+ public void getVmPowerStatesReturnsValidStatesWhenResponseIsSuccessful() {
+ Host host = mock(Host.class);
+ when(host.getId()).thenReturn(1L);
+ when(host.getName()).thenReturn("test-host");
+
+ Map<String, Map<String, String>> accessDetails = new HashMap<>();
+ doReturn(new Pair<>(true,
"{\"status\":\"success\",\"power_state\":{\"vm1\":\"PowerOn\",\"vm2\":\"PowerOff\"}}"))
+
.when(provisioner).getInstanceStatusesOnExternalSystem(anyString(),
anyString(), anyString(), anyMap(), anyInt());
+
+ Map<String, HostVmStateReportEntry> result =
provisioner.getVmPowerStates(host, accessDetails, "test-extension",
"test-path");
+
+ assertNotNull(result);
+ assertEquals(2, result.size());
+ assertEquals(VirtualMachine.PowerState.PowerOn,
result.get("vm1").getState());
+ assertEquals(VirtualMachine.PowerState.PowerOff,
result.get("vm2").getState());
+ }
+
+ @Test
+ public void getVmPowerStatesReturnsNullWhenResponseIsFailure() {
+ Host host = mock(Host.class);
+ when(host.getName()).thenReturn("test-host");
+
+ Map<String, Map<String, String>> accessDetails = new HashMap<>();
+ doReturn(new Pair<>(false, "Error")).when(provisioner)
+ .getInstanceStatusesOnExternalSystem(anyString(), anyString(),
anyString(), anyMap(), anyInt());
+
+ Map<String, HostVmStateReportEntry> result =
provisioner.getVmPowerStates(host, accessDetails, "test-extension",
"test-path");
+ assertNull(result);
+ }
+
@Test
public void getVirtualMachineTOReturnsValidTOWhenVmIsNotNull() {
VirtualMachine vm = mock(VirtualMachine.class);
@@ -986,4 +1018,120 @@ public class ExternalPathPayloadProvisionerTest {
String result =
provisioner.getExtensionConfigureError("test-extension", null);
assertEquals("Extension: test-extension not configured", result);
}
+
+ @Test
+ public void getVmPowerStatesReturnsNullWhenResponseIsEmpty() {
+ Host host = mock(Host.class);
+ when(host.getName()).thenReturn("test-host");
+
+ Map<String, Map<String, String>> accessDetails = new HashMap<>();
+ doReturn(new Pair<>(true, "")).when(provisioner)
+ .getInstanceStatusesOnExternalSystem(anyString(), anyString(),
anyString(), anyMap(), anyInt());
+
+ Map<String, HostVmStateReportEntry> result =
provisioner.getVmPowerStates(host, accessDetails, "test-extension",
"test-path");
+
+ assertNull(result);
+ }
+
+ @Test
+ public void getVmPowerStatesReturnsNullWhenResponseHasInvalidStatus() {
+ Host host = mock(Host.class);
+ when(host.getName()).thenReturn("test-host");
+
+ Map<String, Map<String, String>> accessDetails = new HashMap<>();
+ doReturn(new Pair<>(true,
"{\"status\":\"failure\"}")).when(provisioner)
+ .getInstanceStatusesOnExternalSystem(anyString(), anyString(),
anyString(), anyMap(), anyInt());
+
+ Map<String, HostVmStateReportEntry> result =
provisioner.getVmPowerStates(host, accessDetails, "test-extension",
"test-path");
+
+ assertNull(result);
+ }
+
+ @Test
+ public void getVmPowerStatesReturnsNullWhenPowerStateIsMissing() {
+ Host host = mock(Host.class);
+ when(host.getName()).thenReturn("test-host");
+
+ Map<String, Map<String, String>> accessDetails = new HashMap<>();
+ doReturn(new Pair<>(true,
"{\"status\":\"success\"}")).when(provisioner)
+ .getInstanceStatusesOnExternalSystem(anyString(), anyString(),
anyString(), anyMap(), anyInt());
+
+ Map<String, HostVmStateReportEntry> result =
provisioner.getVmPowerStates(host, accessDetails, "test-extension",
"test-path");
+
+ assertNull(result);
+ }
+
+ @Test
+ public void getVmPowerStatesReturnsNullWhenResponseIsMalformed() {
+ Host host = mock(Host.class);
+ when(host.getName()).thenReturn("test-host");
+
+ Map<String, Map<String, String>> accessDetails = new HashMap<>();
+ doReturn(new Pair<>(true, "{status:success")).when(provisioner)
+ .getInstanceStatusesOnExternalSystem(anyString(), anyString(),
anyString(), anyMap(), anyInt());
+
+ Map<String, HostVmStateReportEntry> result =
provisioner.getVmPowerStates(host, accessDetails, "test-extension",
"test-path");
+
+ assertNull(result);
+ }
+
+ @Test
+ public void
getInstanceStatusesOnExternalSystemReturnsSuccessWhenCommandExecutesSuccessfully()
{
+ doReturn(new Pair<>(true, "success")).when(provisioner)
+ .executeExternalCommand(eq("test-extension"), eq("statuses"),
anyMap(), eq(30), anyString(), eq("test-file"));
+
+ Pair<Boolean, String> result =
provisioner.getInstanceStatusesOnExternalSystem(
+ "test-extension", "test-file", "test-host", new HashMap<>(), 30);
+
+ assertTrue(result.first());
+ assertEquals("success", result.second());
+ }
+
+ @Test
+ public void
getInstanceStatusesOnExternalSystemReturnsFailureWhenCommandFails() {
+ doReturn(new Pair<>(false, "error")).when(provisioner)
+ .executeExternalCommand(eq("test-extension"), eq("statuses"),
anyMap(), eq(30), anyString(), eq("test-file"));
+
+ Pair<Boolean, String> result =
provisioner.getInstanceStatusesOnExternalSystem(
+ "test-extension", "test-file", "test-host", new HashMap<>(), 30);
+
+ assertFalse(result.first());
+ assertEquals("error", result.second());
+ }
+
+ @Test
+ public void getInstanceStatusesOnExternalSystemHandlesEmptyResponse() {
+ doReturn(new Pair<>(true, "")).when(provisioner)
+ .executeExternalCommand(eq("test-extension"), eq("statuses"),
anyMap(), eq(30), anyString(), eq("test-file"));
+
+ Pair<Boolean, String> result =
provisioner.getInstanceStatusesOnExternalSystem(
+ "test-extension", "test-file", "test-host", new HashMap<>(), 30);
+
+ assertTrue(result.first());
+ assertEquals("", result.second());
+ }
+
+ @Test
+ public void getInstanceStatusesOnExternalSystemHandlesNullResponse() {
+ doReturn(new Pair<>(true, null)).when(provisioner)
+ .executeExternalCommand(eq("test-extension"), eq("statuses"),
anyMap(), eq(30), anyString(), eq("test-file"));
+
+ Pair<Boolean, String> result =
provisioner.getInstanceStatusesOnExternalSystem(
+ "test-extension", "test-file", "test-host", new HashMap<>(), 30);
+
+ assertTrue(result.first());
+ assertNull(result.second());
+ }
+
+ @Test
+ public void getInstanceStatusesOnExternalSystemHandlesInvalidFilePath() {
+ doReturn(new Pair<>(false, "File not found")).when(provisioner)
+ .executeExternalCommand(eq("test-extension"), eq("statuses"),
anyMap(), eq(30), anyString(), eq("invalid-file"));
+
+ Pair<Boolean, String> result =
provisioner.getInstanceStatusesOnExternalSystem(
+ "test-extension", "invalid-file", "test-host", new HashMap<>(),
30);
+
+ assertFalse(result.first());
+ assertEquals("File not found", result.second());
+ }
}
diff --git a/scripts/vm/hypervisor/external/provisioner/provisioner.sh
b/scripts/vm/hypervisor/external/provisioner/provisioner.sh
index f067d892f1f..c92ac36f466 100755
--- a/scripts/vm/hypervisor/external/provisioner/provisioner.sh
+++ b/scripts/vm/hypervisor/external/provisioner/provisioner.sh
@@ -99,6 +99,14 @@ status() {
echo '{"status": "success", "power_state": "poweron"}'
}
+statuses() {
+ parse_json "$1" || exit 1
+ # This external system can not return an output like the following:
+ #
{"status":"success","power_state":{"i-3-23-VM":"poweroff","i-2-25-VM":"poweron"}}
+ # CloudStack can fallback to retrieving the power state of the single VM
using the "status" action
+ echo '{"status": "error", "message": "Not supported"}'
+}
+
get_console() {
parse_json "$1" || exit 1
local response
@@ -145,6 +153,9 @@ case $action in
status)
status "$parameters"
;;
+ statuses)
+ statuses "$parameters"
+ ;;
getconsole)
get_console "$parameters"
;;