mirror of
				https://github.com/apache/cloudstack.git
				synced 2025-11-04 00:02:37 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			418 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			418 lines
		
	
	
		
			12 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 '{"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 // ""),
 | 
						|
        "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
 | 
						|
 | 
						|
    #echo curl "${curl_opts[@]}" "https://${url}:8006/api2/json${path}" >&2
 | 
						|
    response=$(curl "${curl_opts[@]}" "https://${url}:8006/api2/json${path}")
 | 
						|
    echo "$response"
 | 
						|
}
 | 
						|
 | 
						|
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 '{"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 "{\"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=l26"
 | 
						|
        data+="&scsihw=virtio-scsi-single"
 | 
						|
        data+="&scsi0=$(urlencode "local-lvm:64,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"
 | 
						|
        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\"}"
 | 
						|
}
 | 
						|
 | 
						|
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 '{"error":"Missing required arguments"}'
 | 
						|
    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
 | 
						|
        ;;
 | 
						|
    ListSnapshots)
 | 
						|
        list_snapshots
 | 
						|
        ;;
 | 
						|
    CreateSnapshot)
 | 
						|
        create_snapshot
 | 
						|
        ;;
 | 
						|
    RestoreSnapshot)
 | 
						|
        restore_snapshot
 | 
						|
        ;;
 | 
						|
    DeleteSnapshot)
 | 
						|
        delete_snapshot
 | 
						|
        ;;
 | 
						|
    *)
 | 
						|
        echo '{"error":"Invalid action"}'
 | 
						|
        exit 1
 | 
						|
        ;;
 | 
						|
esac
 | 
						|
 | 
						|
exit 0
 |