Skip to content

Commit c666e85

Browse files
Merge branch 'main' into 4.22-extension-home-dir
2 parents c2a7201 + 3159fa7 commit c666e85

File tree

42 files changed

+1755
-249
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1755
-249
lines changed

api/src/main/java/org/apache/cloudstack/api/ApiConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class ApiConstants {
8080
public static final String BYTES_WRITE_RATE_MAX = "byteswriteratemax";
8181
public static final String BYTES_WRITE_RATE_MAX_LENGTH = "byteswriteratemaxlength";
8282
public static final String BYPASS_VLAN_OVERLAP_CHECK = "bypassvlanoverlapcheck";
83+
public static final String CALLER = "caller";
8384
public static final String CAPACITY = "capacity";
8485
public static final String CATEGORY = "category";
8586
public static final String CAN_REVERT = "canrevert";

api/src/main/java/org/apache/cloudstack/api/command/user/consoleproxy/CreateConsoleEndpointCmd.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.cloudstack.context.CallContext;
3636
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
3737
import org.apache.commons.collections.MapUtils;
38+
import org.apache.commons.lang3.ObjectUtils;
3839

3940
import javax.inject.Inject;
4041
import java.util.Map;
@@ -86,6 +87,10 @@ private CreateConsoleEndpointResponse createResponse(ConsoleEndpoint endpoint) {
8687
}
8788

8889
private ConsoleEndpointWebsocketResponse createWebsocketResponse(ConsoleEndpoint endpoint) {
90+
if (ObjectUtils.allNull(endpoint.getWebsocketHost(), endpoint.getWebsocketPort(), endpoint.getWebsocketPath(),
91+
endpoint.getWebsocketToken(), endpoint.getWebsocketExtra())) {
92+
return null;
93+
}
8994
ConsoleEndpointWebsocketResponse wsResponse = new ConsoleEndpointWebsocketResponse();
9095
wsResponse.setHost(endpoint.getWebsocketHost());
9196
wsResponse.setPort(endpoint.getWebsocketPort());

api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ public void setNetworkAvailable(String networkAvailable) {
569569

570570
@Override
571571
public void setVpcLimit(String vpcLimit) {
572-
this.vpcLimit = networkLimit;
572+
this.vpcLimit = vpcLimit;
573573
}
574574

575575
@Override
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package com.cloud.agent.api;
19+
20+
public class GetExternalConsoleAnswer extends Answer {
21+
22+
private String url;
23+
private String host;
24+
private Integer port;
25+
@LogLevel(LogLevel.Log4jLevel.Off)
26+
private String password;
27+
private String protocol;
28+
private boolean passwordOneTimeUseOnly;
29+
30+
public GetExternalConsoleAnswer(Command command, String details) {
31+
super(command, false, details);
32+
}
33+
34+
public GetExternalConsoleAnswer(Command command, String url, String host, Integer port, String password,
35+
boolean passwordOneTimeUseOnly, String protocol) {
36+
super(command, true, "");
37+
this.url = url;
38+
this.host = host;
39+
this.port = port;
40+
this.password = password;
41+
this.passwordOneTimeUseOnly = passwordOneTimeUseOnly;
42+
this.protocol = protocol;
43+
}
44+
45+
public String getUrl() {
46+
return url;
47+
}
48+
49+
public String getHost() {
50+
return host;
51+
}
52+
53+
public Integer getPort() {
54+
return port;
55+
}
56+
57+
public String getPassword() {
58+
return password;
59+
}
60+
61+
public String getProtocol() {
62+
return protocol;
63+
}
64+
65+
public boolean isPasswordOneTimeUseOnly() {
66+
return passwordOneTimeUseOnly;
67+
}
68+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package com.cloud.agent.api;
19+
20+
import com.cloud.agent.api.to.VirtualMachineTO;
21+
22+
public class GetExternalConsoleCommand extends Command {
23+
String vmName;
24+
VirtualMachineTO vm;
25+
protected boolean executeInSequence;
26+
27+
public GetExternalConsoleCommand(String vmName, VirtualMachineTO vm) {
28+
this.vmName = vmName;
29+
this.vm = vm;
30+
this.executeInSequence = false;
31+
}
32+
33+
public String getVmName() {
34+
return this.vmName;
35+
}
36+
37+
public void setVirtualMachine(VirtualMachineTO vm) {
38+
this.vm = vm;
39+
}
40+
41+
public VirtualMachineTO getVirtualMachine() {
42+
return vm;
43+
}
44+
45+
@Override
46+
public boolean executeInSequence() {
47+
return executeInSequence;
48+
}
49+
50+
public void setExecuteInSequence(boolean executeInSequence) {
51+
this.executeInSequence = executeInSequence;
52+
}
53+
}

core/src/main/java/com/cloud/agent/api/RunCustomActionCommand.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121

2222
import java.util.Map;
2323

24+
import com.cloud.agent.api.to.VirtualMachineTO;
25+
2426
public class RunCustomActionCommand extends Command {
2527

2628
String actionName;
27-
Long vmId;
29+
VirtualMachineTO vmTO;
2830
Map<String, Object> parameters;
2931

3032
public RunCustomActionCommand(String actionName) {
@@ -36,12 +38,12 @@ public String getActionName() {
3638
return actionName;
3739
}
3840

39-
public Long getVmId() {
40-
return vmId;
41+
public VirtualMachineTO getVmTO() {
42+
return vmTO;
4143
}
4244

43-
public void setVmId(Long vmId) {
44-
this.vmId = vmId;
45+
public void setVmTO(VirtualMachineTO vmTO) {
46+
this.vmTO = vmTO;
4547
}
4648

4749
public Map<String, Object> getParameters() {

engine/components-api/src/main/java/com/cloud/hypervisor/ExternalProvisioner.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.util.Map;
2020

21+
import com.cloud.agent.api.GetExternalConsoleAnswer;
22+
import com.cloud.agent.api.GetExternalConsoleCommand;
2123
import com.cloud.agent.api.HostVmStateReportEntry;
2224
import com.cloud.agent.api.PrepareExternalProvisioningAnswer;
2325
import com.cloud.agent.api.PrepareExternalProvisioningCommand;
@@ -57,5 +59,7 @@ public interface ExternalProvisioner extends Manager {
5759

5860
Map<String, HostVmStateReportEntry> getHostVmStateReport(long hostId, String extensionName, String extensionRelativePath);
5961

62+
GetExternalConsoleAnswer getInstanceConsole(String hostGuid, String extensionName, String extensionRelativePath, GetExternalConsoleCommand cmd);
63+
6064
RunCustomActionAnswer runCustomAction(String hostGuid, String extensionName, String extensionRelativePath, RunCustomActionCommand cmd);
6165
}

extensions/HyperV/hyperv.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626

2727
def fail(message):
28-
print(json.dumps({"error": message}))
28+
print(json.dumps({"status": "error", "error": message}))
2929
sys.exit(1)
3030

3131

@@ -220,6 +220,9 @@ def delete(self):
220220
fail(str(e))
221221
succeed({"status": "success", "message": "Instance deleted"})
222222

223+
def get_console(self):
224+
fail("Operation not supported")
225+
223226
def suspend(self):
224227
self.run_ps(f'Suspend-VM -Name "{self.data["vmname"]}"')
225228
succeed({"status": "success", "message": "Instance suspended"})
@@ -283,6 +286,7 @@ def main():
283286
"reboot": manager.reboot,
284287
"delete": manager.delete,
285288
"status": manager.status,
289+
"getconsole": manager.get_console,
286290
"suspend": manager.suspend,
287291
"resume": manager.resume,
288292
"listsnapshots": manager.list_snapshots,

extensions/Proxmox/proxmox.sh

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
parse_json() {
2020
local json_string="$1"
21-
echo "$json_string" | jq '.' > /dev/null || { echo '{"error":"Invalid JSON input"}'; exit 1; }
21+
echo "$json_string" | jq '.' > /dev/null || { echo '{"status": "error", "error": "Invalid JSON input"}'; exit 1; }
2222

2323
local -A details
2424
while IFS="=" read -r key value; do
@@ -112,9 +112,14 @@ call_proxmox_api() {
112112
curl_opts+=(-d "$data")
113113
fi
114114

115-
#echo curl "${curl_opts[@]}" "https://${url}:8006/api2/json${path}" >&2
116115
response=$(curl "${curl_opts[@]}" "https://${url}:8006/api2/json${path}")
116+
local status=$?
117+
if [[ $status -ne 0 ]]; then
118+
echo "{\"errors\":{\"curl\":\"API call failed with status $status: $(echo "$response" | jq -Rsa . | jq -r .)\"}}"
119+
return $status
120+
fi
117121
echo "$response"
122+
return 0
118123
}
119124

120125
wait_for_proxmox_task() {
@@ -129,7 +134,7 @@ wait_for_proxmox_task() {
129134
local now
130135
now=$(date +%s)
131136
if (( now - start_time > timeout )); then
132-
echo '{"error":"Timeout while waiting for async task"}'
137+
echo '{"status": "error", "error":"Timeout while waiting for async task"}'
133138
exit 1
134139
fi
135140

@@ -139,7 +144,7 @@ wait_for_proxmox_task() {
139144
if [[ -z "$status_response" || "$status_response" == *'"errors":'* ]]; then
140145
local msg
141146
msg=$(echo "$status_response" | jq -r '.message // "Unknown error"')
142-
echo "{\"error\":\"$msg\"}"
147+
echo "{\"status\": \"error\", \"error\": \"$msg\"}"
143148
exit 1
144149
fi
145150

@@ -285,6 +290,86 @@ status() {
285290
echo "{\"status\": \"success\", \"power_state\": \"$powerstate\"}"
286291
}
287292

293+
get_node_host() {
294+
check_required_fields node
295+
local net_json host
296+
297+
if ! net_json="$(call_proxmox_api GET "/nodes/${node}/network")"; then
298+
echo ""
299+
return 1
300+
fi
301+
302+
# Prefer a static non-bridge IP
303+
host="$(echo "$net_json" | jq -r '
304+
.data
305+
| map(select(
306+
(.type // "") != "bridge" and
307+
(.type // "") != "bond" and
308+
(.method // "") == "static" and
309+
((.address // .cidr // "") != "")
310+
))
311+
| map(.address // (.cidr | split("/")[0]))
312+
| .[0] // empty
313+
' 2>/dev/null)"
314+
315+
# Fallback: first interface with a CIDR
316+
if [[ -z "$host" ]]; then
317+
host="$(echo "$net_json" | jq -r '
318+
.data
319+
| map(select((.cidr // "") != ""))
320+
| map(.cidr | split("/")[0])
321+
| .[0] // empty
322+
' 2>/dev/null)"
323+
fi
324+
325+
echo "$host"
326+
}
327+
328+
get_console() {
329+
check_required_fields node vmid
330+
331+
local api_resp port ticket
332+
if ! api_resp="$(call_proxmox_api POST "/nodes/${node}/qemu/${vmid}/vncproxy")"; then
333+
echo "$api_resp" | jq -c '{status:"error", error:(.errors.curl // (.errors|tostring))}'
334+
exit 1
335+
fi
336+
337+
port="$(echo "$api_resp" | jq -re '.data.port // empty' 2>/dev/null || true)"
338+
ticket="$(echo "$api_resp" | jq -re '.data.ticket // empty' 2>/dev/null || true)"
339+
340+
if [[ -z "$port" || -z "$ticket" ]]; then
341+
jq -n --arg raw "$api_resp" \
342+
'{status:"error", error:"Proxmox response missing port/ticket", upstream:$raw}'
343+
exit 1
344+
fi
345+
346+
# Derive host from node’s network info
347+
local host
348+
host="$(get_node_host)"
349+
if [[ -z "$host" ]]; then
350+
jq -n --arg msg "Could not determine host IP for node $node" \
351+
'{status:"error", error:$msg}'
352+
exit 1
353+
fi
354+
355+
jq -n \
356+
--arg host "$host" \
357+
--arg port "$port" \
358+
--arg password "$ticket" \
359+
--argjson passwordonetimeuseonly true \
360+
'{
361+
status: "success",
362+
message: "Console retrieved",
363+
console: {
364+
host: $host,
365+
port: $port,
366+
password: $password,
367+
passwordonetimeuseonly: $passwordonetimeuseonly,
368+
protocol: "vnc"
369+
}
370+
}'
371+
}
372+
288373
list_snapshots() {
289374
snapshot_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/snapshot")
290375
echo "$snapshot_response" | jq '
@@ -356,7 +441,12 @@ parameters_file="$2"
356441
wait_time=$3
357442

358443
if [[ -z "$action" || -z "$parameters_file" ]]; then
359-
echo '{"error":"Missing required arguments"}'
444+
echo '{"status": "error", "error": "Missing required arguments"}'
445+
exit 1
446+
fi
447+
448+
if [[ ! -r "$parameters_file" ]]; then
449+
echo '{"status": "error", "error": "File not found or unreadable"}'
360450
exit 1
361451
fi
362452

@@ -396,6 +486,9 @@ case $action in
396486
status)
397487
status
398488
;;
489+
getconsole)
490+
get_console
491+
;;
399492
ListSnapshots)
400493
list_snapshots
401494
;;
@@ -409,7 +502,7 @@ case $action in
409502
delete_snapshot
410503
;;
411504
*)
412-
echo '{"error":"Invalid action"}'
505+
echo '{"status": "error", "error": "Invalid action"}'
413506
exit 1
414507
;;
415508
esac

0 commit comments

Comments
 (0)