Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
17 changes: 17 additions & 0 deletions extensions/HyperV/hyperv.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,22 @@ 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'
vms = json.loads(self.run_ps(command))
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 @@ -283,6 +299,7 @@ def main():
"reboot": manager.reboot,
"delete": manager.delete,
"status": manager.status,
"statuses": manager.statuses,
"suspend": manager.suspend,
"resume": manager.resume,
"listsnapshots": manager.list_snapshots,
Expand Down
35 changes: 35 additions & 0 deletions extensions/Proxmox/proxmox.sh
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,38 @@ status() {
echo "{\"status\": \"success\", \"power_state\": \"$powerstate\"}"
}

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",
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 '
Expand Down Expand Up @@ -396,6 +428,9 @@ case $action in
status)
status
;;
statuses)
statuses
;;
ListSnapshots)
list_snapshots
;;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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 @@ -129,7 +130,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 @@ -430,7 +431,7 @@ public StopAnswer expungeInstance(String hostGuid, 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 @@ -440,14 +441,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 @@ -647,7 +654,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
Preview

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 @@ -657,7 +664,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) {
final HypervisorGuru hvGuru = hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External);
VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVmVO);
Expand All @@ -675,6 +682,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 @@ -718,6 +765,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> executeExternalCommand(String extensionName, String action,
Map<String, Object> accessDetails, int wait, String errorLogPrefix, String file) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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.Hypervisor;
Expand Down Expand Up @@ -747,4 +748,152 @@ public void parsePowerStateFromResponseReturnsPowerStateForPlainTextResponse() {
VirtualMachine.PowerState result = provisioner.parsePowerStateFromResponse(vm, response);
assertEquals(VirtualMachine.PowerState.PowerOn, 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 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());
}
}
11 changes: 11 additions & 0 deletions scripts/vm/hypervisor/external/provisioner/provisioner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"}'
}

action=$1
parameters_file="$2"
wait_time="$3"
Expand Down Expand Up @@ -138,6 +146,9 @@ case $action in
status)
status "$parameters"
;;
statuses)
statuses "$parameters"
;;
*)
echo '{"error":"Invalid action"}'
exit 1
Expand Down
Loading