mirror of
https://github.com/apache/cloudstack.git
synced 2025-12-16 10:32:34 +01:00
Make storage, disk-size and os-type configurable in the Proxmox extension Doc PR: apache/cloudstack-documentation#601 --------- Co-authored-by: dahn <daan.hoogland@gmail.com>
517 lines
16 KiB
Bash
Executable File
517 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Licensed to the Apache Software Foundation (ASF) under one
|
||
# or more contributor license agreements. See the NOTICE file
|
||
# distributed with this work for additional information
|
||
# regarding copyright ownership. The ASF licenses this file
|
||
# to you under the Apache License, Version 2.0 (the
|
||
# "License"); you may not use this file except in compliance
|
||
# with the License. You may obtain a copy of the License at
|
||
#
|
||
# http://www.apache.org/licenses/LICENSE-2.0
|
||
#
|
||
# Unless required by applicable law or agreed to in writing,
|
||
# software distributed under the License is distributed on an
|
||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||
# KIND, either express or implied. See the License for the
|
||
# specific language governing permissions and limitations
|
||
# under the License.
|
||
|
||
parse_json() {
|
||
local json_string="$1"
|
||
echo "$json_string" | jq '.' > /dev/null || { echo '{"status": "error", "error": "Invalid JSON input"}'; exit 1; }
|
||
|
||
local -A details
|
||
while IFS="=" read -r key value; do
|
||
details[$key]="$value"
|
||
done < <(echo "$json_string" | jq -r '{
|
||
"extension_url": (.externaldetails.extension.url // ""),
|
||
"extension_user": (.externaldetails.extension.user // ""),
|
||
"extension_token": (.externaldetails.extension.token // ""),
|
||
"extension_secret": (.externaldetails.extension.secret // ""),
|
||
"host_url": (.externaldetails.host.url // ""),
|
||
"host_user": (.externaldetails.host.user // ""),
|
||
"host_token": (.externaldetails.host.token // ""),
|
||
"host_secret": (.externaldetails.host.secret // ""),
|
||
"node": (.externaldetails.host.node // ""),
|
||
"network_bridge": (.externaldetails.host.network_bridge // ""),
|
||
"verify_tls_certificate": (.externaldetails.host.verify_tls_certificate // "true"),
|
||
"vm_name": (.externaldetails.virtualmachine.vm_name // ""),
|
||
"template_id": (.externaldetails.virtualmachine.template_id // ""),
|
||
"template_type": (.externaldetails.virtualmachine.template_type // ""),
|
||
"iso_path": (.externaldetails.virtualmachine.iso_path // ""),
|
||
"iso_os_type": (.externaldetails.virtualmachine.iso_os_type // "l26"),
|
||
"disk_size_gb": (.externaldetails.virtualmachine.disk_size_gb // "64"),
|
||
"storage": (.externaldetails.virtualmachine.storage // "local-lvm"),
|
||
"is_full_clone": (.externaldetails.virtualmachine.is_full_clone // "false"),
|
||
"snap_name": (.parameters.snap_name // ""),
|
||
"snap_description": (.parameters.snap_description // ""),
|
||
"snap_save_memory": (.parameters.snap_save_memory // ""),
|
||
"vmid": (."cloudstack.vm.details".details.proxmox_vmid // ""),
|
||
"vm_internal_name": (."cloudstack.vm.details".name // ""),
|
||
"vmmemory": (."cloudstack.vm.details".minRam // ""),
|
||
"vmcpus": (."cloudstack.vm.details".cpus // ""),
|
||
"vlans": ([."cloudstack.vm.details".nics[]?.broadcastUri // "" | sub("vlan://"; "")] | join(",")),
|
||
"mac_addresses": ([."cloudstack.vm.details".nics[]?.mac // ""] | join(","))
|
||
} | to_entries | .[] | "\(.key)=\(.value)"')
|
||
|
||
for key in "${!details[@]}"; do
|
||
declare -g "$key=${details[$key]}"
|
||
done
|
||
|
||
# set url, user, token, secret to host values if present, otherwise use extension values
|
||
url="${host_url:-$extension_url}"
|
||
user="${host_user:-$extension_user}"
|
||
token="${host_token:-$extension_token}"
|
||
secret="${host_secret:-$extension_secret}"
|
||
|
||
check_required_fields vm_internal_name url user token secret node
|
||
}
|
||
|
||
urlencode() {
|
||
encoded_data=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$1'''))")
|
||
echo "$encoded_data"
|
||
}
|
||
|
||
check_required_fields() {
|
||
local missing=()
|
||
for varname in "$@"; do
|
||
local value="${!varname}"
|
||
if [[ -z "$value" ]]; then
|
||
missing+=("$varname")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
echo "{\"error\":\"Missing required fields: ${missing[*]}\"}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
validate_name() {
|
||
local entity="$1"
|
||
local name="$2"
|
||
if [[ ! "$name" =~ ^[a-zA-Z0-9-]+$ ]]; then
|
||
echo "{\"error\":\"Invalid $entity name '$name'. Only alphanumeric characters and dashes (-) are allowed.\"}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
call_proxmox_api() {
|
||
local method=$1
|
||
local path=$2
|
||
local data=$3
|
||
|
||
curl_opts=(
|
||
-s
|
||
--fail
|
||
-X "$method"
|
||
-H "Authorization: PVEAPIToken=${user}!${token}=${secret}"
|
||
)
|
||
|
||
if [[ "$verify_tls_certificate" == "false" ]]; then
|
||
curl_opts+=(-k)
|
||
fi
|
||
|
||
if [[ -n "$data" ]]; then
|
||
curl_opts+=(-d "$data")
|
||
fi
|
||
|
||
response=$(curl "${curl_opts[@]}" "https://${url}:8006/api2/json${path}")
|
||
local status=$?
|
||
if [[ $status -ne 0 ]]; then
|
||
echo "{\"errors\":{\"curl\":\"API call failed with status $status: $(echo "$response" | jq -Rsa . | jq -r .)\"}}"
|
||
return $status
|
||
fi
|
||
echo "$response"
|
||
return 0
|
||
}
|
||
|
||
wait_for_proxmox_task() {
|
||
local upid="$1"
|
||
local timeout="${2:-$wait_time}"
|
||
local interval="${3:-1}"
|
||
|
||
local start_time
|
||
start_time=$(date +%s)
|
||
|
||
while true; do
|
||
local now
|
||
now=$(date +%s)
|
||
if (( now - start_time > timeout )); then
|
||
echo '{"status": "error", "error":"Timeout while waiting for async task"}'
|
||
exit 1
|
||
fi
|
||
|
||
local status_response
|
||
status_response=$(call_proxmox_api GET "/nodes/${node}/tasks/$(urlencode "$upid")/status")
|
||
|
||
if [[ -z "$status_response" || "$status_response" == *'"errors":'* ]]; then
|
||
local msg
|
||
msg=$(echo "$status_response" | jq -r '.message // "Unknown error"')
|
||
echo "{\"status\": \"error\", \"error\": \"$msg\"}"
|
||
exit 1
|
||
fi
|
||
|
||
local task_status
|
||
task_status=$(echo "$status_response" | jq -r '.data.status')
|
||
|
||
if [[ "$task_status" == "stopped" ]]; then
|
||
local exit_status
|
||
exit_status=$(echo "$status_response" | jq -r '.data.exitstatus')
|
||
if [[ "$exit_status" != "OK" ]]; then
|
||
echo "{\"error\":\"Task failed with exit status: $exit_status\"}"
|
||
exit 1
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
sleep "$interval"
|
||
done
|
||
}
|
||
|
||
execute_and_wait() {
|
||
local method="$1"
|
||
local path="$2"
|
||
local data="$3"
|
||
local response upid msg
|
||
|
||
response=$(call_proxmox_api "$method" "$path" "$data")
|
||
upid=$(echo "$response" | jq -r '.data // ""')
|
||
|
||
if [[ -z "$upid" ]]; then
|
||
msg=$(echo "$response" | jq -r '.message // "Unknown error"')
|
||
echo "{\"error\":\"Failed to execute API or retrieve UPID. Message: $msg\"}"
|
||
exit 1
|
||
fi
|
||
|
||
wait_for_proxmox_task "$upid"
|
||
}
|
||
|
||
vm_not_present() {
|
||
response=$(call_proxmox_api GET "/cluster/nextid?vmid=$vmid")
|
||
vmid_result=$(echo "$response" | jq -r '.data // empty')
|
||
if [[ "$vmid_result" == "$vmid" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
prepare() {
|
||
response=$(call_proxmox_api GET "/cluster/nextid")
|
||
vmid=$(echo "$response" | jq -r '.data // ""')
|
||
|
||
echo "{\"details\":{\"proxmox_vmid\": \"$vmid\"}}"
|
||
}
|
||
|
||
create() {
|
||
if [[ -z "$vm_name" ]]; then
|
||
vm_name="$vm_internal_name"
|
||
fi
|
||
validate_name "VM" "$vm_name"
|
||
check_required_fields vmid network_bridge vmcpus vmmemory
|
||
|
||
if [[ "${template_type^^}" == "ISO" ]]; then
|
||
check_required_fields iso_path
|
||
local data="vmid=$vmid"
|
||
data+="&name=$vm_name"
|
||
data+="&ide2=$(urlencode "$iso_path,media=cdrom")"
|
||
data+="&ostype=$iso_os_type"
|
||
data+="&scsihw=virtio-scsi-single"
|
||
data+="&scsi0=$(urlencode "$storage:$disk_size_gb,iothread=on")"
|
||
data+="&sockets=1"
|
||
data+="&cores=$vmcpus"
|
||
data+="&numa=0"
|
||
data+="&cpu=x86-64-v2-AES"
|
||
data+="&memory=$((vmmemory / 1024 / 1024))"
|
||
|
||
execute_and_wait POST "/nodes/${node}/qemu/" "$data"
|
||
cleanup_vm=1
|
||
|
||
else
|
||
check_required_fields template_id
|
||
local data="newid=$vmid"
|
||
data+="&name=$vm_name"
|
||
clone_flag=$(( is_full_clone == "true" ))
|
||
data+="&storage=$storage&full=$clone_flag"
|
||
execute_and_wait POST "/nodes/${node}/qemu/${template_id}/clone" "$data"
|
||
cleanup_vm=1
|
||
|
||
data="cores=$vmcpus"
|
||
data+="&memory=$((vmmemory / 1024 / 1024))"
|
||
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/config" "$data"
|
||
fi
|
||
|
||
IFS=',' read -ra vlan_array <<< "$vlans"
|
||
IFS=',' read -ra mac_array <<< "$mac_addresses"
|
||
for i in "${!vlan_array[@]}"; do
|
||
network="net${i}=$(urlencode "virtio=${mac_array[i]},bridge=${network_bridge},tag=${vlan_array[i]},firewall=0")"
|
||
call_proxmox_api PUT "/nodes/${node}/qemu/${vmid}/config/" "$network" > /dev/null
|
||
done
|
||
|
||
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/start"
|
||
|
||
cleanup_vm=0
|
||
echo '{"status": "success", "message": "Instance created"}'
|
||
}
|
||
|
||
start() {
|
||
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/start"
|
||
echo '{"status": "success", "message": "Instance started"}'
|
||
}
|
||
|
||
delete() {
|
||
if vm_not_present; then
|
||
echo '{"status": "success", "message": "Instance deleted"}'
|
||
return 0
|
||
fi
|
||
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}"
|
||
echo '{"status": "success", "message": "Instance deleted"}'
|
||
}
|
||
|
||
stop() {
|
||
if vm_not_present; then
|
||
echo '{"status": "success", "message": "Instance stopped"}'
|
||
return 0
|
||
fi
|
||
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/stop"
|
||
echo '{"status": "success", "message": "Instance stopped"}'
|
||
}
|
||
|
||
reboot() {
|
||
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/reboot"
|
||
echo '{"status": "success", "message": "Instance rebooted"}'
|
||
}
|
||
|
||
status() {
|
||
local status_response vm_status powerstate
|
||
status_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/status/current")
|
||
vm_status=$(echo "$status_response" | jq -r '.data.status')
|
||
case "$vm_status" in
|
||
running) powerstate="poweron" ;;
|
||
stopped) powerstate="poweroff" ;;
|
||
*) powerstate="unknown" ;;
|
||
esac
|
||
|
||
echo "{\"status\": \"success\", \"power_state\": \"$powerstate\"}"
|
||
}
|
||
|
||
get_node_host() {
|
||
check_required_fields node
|
||
local net_json host
|
||
|
||
if ! net_json="$(call_proxmox_api GET "/nodes/${node}/network")"; then
|
||
echo ""
|
||
return 1
|
||
fi
|
||
|
||
# Prefer a static non-bridge IP
|
||
host="$(echo "$net_json" | jq -r '
|
||
.data
|
||
| map(select(
|
||
(.type // "") != "bridge" and
|
||
(.type // "") != "bond" and
|
||
(.method // "") == "static" and
|
||
((.address // .cidr // "") != "")
|
||
))
|
||
| map(.address // (.cidr | split("/")[0]))
|
||
| .[0] // empty
|
||
' 2>/dev/null)"
|
||
|
||
# Fallback: first interface with a CIDR
|
||
if [[ -z "$host" ]]; then
|
||
host="$(echo "$net_json" | jq -r '
|
||
.data
|
||
| map(select((.cidr // "") != ""))
|
||
| map(.cidr | split("/")[0])
|
||
| .[0] // empty
|
||
' 2>/dev/null)"
|
||
fi
|
||
|
||
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"
|
||
}
|
||
}'
|
||
}
|
||
|
||
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 // "-")
|
||
}]
|
||
}
|
||
'
|
||
}
|
||
|
||
create_snapshot() {
|
||
check_required_fields snap_name
|
||
validate_name "Snapshot" "$snap_name"
|
||
|
||
local data vmstate
|
||
data="snapname=$snap_name"
|
||
if [[ -n "$snap_description" ]]; then
|
||
data+="&description=$snap_description"
|
||
fi
|
||
if [[ -n "$snap_save_memory" && "$snap_save_memory" == "true" ]]; then
|
||
vmstate="1"
|
||
else
|
||
vmstate="0"
|
||
fi
|
||
data+="&vmstate=$vmstate"
|
||
|
||
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/snapshot" "$data"
|
||
echo '{"status": "success", "message": "Instance Snapshot created"}'
|
||
}
|
||
|
||
restore_snapshot() {
|
||
check_required_fields snap_name
|
||
validate_name "Snapshot" "$snap_name"
|
||
|
||
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/snapshot/${snap_name}/rollback"
|
||
|
||
status_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/status/current")
|
||
vm_status=$(echo "$status_response" | jq -r '.data.status')
|
||
if [ "$vm_status" = "stopped" ];then
|
||
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/start"
|
||
fi
|
||
|
||
echo '{"status": "success", "message": "Instance Snapshot restored"}'
|
||
}
|
||
|
||
delete_snapshot() {
|
||
check_required_fields snap_name
|
||
validate_name "Snapshot" "$snap_name"
|
||
|
||
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}/snapshot/${snap_name}"
|
||
echo '{"status": "success", "message": "Instance Snapshot deleted"}'
|
||
}
|
||
|
||
action=$1
|
||
parameters_file="$2"
|
||
wait_time=$3
|
||
|
||
if [[ -z "$action" || -z "$parameters_file" ]]; then
|
||
echo '{"status": "error", "error": "Missing required arguments"}'
|
||
exit 1
|
||
fi
|
||
|
||
if [[ ! -r "$parameters_file" ]]; then
|
||
echo '{"status": "error", "error": "File not found or unreadable"}'
|
||
exit 1
|
||
fi
|
||
|
||
# Read file content as parameters (assumes space-separated arguments)
|
||
parameters=$(<"$parameters_file")
|
||
|
||
parse_json "$parameters" || exit 1
|
||
|
||
cleanup_vm=0
|
||
cleanup() {
|
||
if (( cleanup_vm == 1 )); then
|
||
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}"
|
||
fi
|
||
}
|
||
|
||
trap cleanup EXIT
|
||
|
||
case $action in
|
||
prepare)
|
||
prepare
|
||
;;
|
||
create)
|
||
create
|
||
;;
|
||
delete)
|
||
delete
|
||
;;
|
||
start)
|
||
start
|
||
;;
|
||
stop)
|
||
stop
|
||
;;
|
||
reboot)
|
||
reboot
|
||
;;
|
||
status)
|
||
status
|
||
;;
|
||
getconsole)
|
||
get_console
|
||
;;
|
||
ListSnapshots)
|
||
list_snapshots
|
||
;;
|
||
CreateSnapshot)
|
||
create_snapshot
|
||
;;
|
||
RestoreSnapshot)
|
||
restore_snapshot
|
||
;;
|
||
DeleteSnapshot)
|
||
delete_snapshot
|
||
;;
|
||
*)
|
||
echo '{"status": "error", "error": "Invalid action"}'
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
exit 0
|