diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 215346a..4450a2a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,23 +1,17 @@ name: Integration Tests # Real integration tests against a live Proxmox VE instance. -# Runs on a self-hosted runner with network access to the PVE API. +# By default, provisions a throwaway nested PVE VM via Terraform, runs tests, +# then destroys it. Can also run against a pre-existing PVE with skip_provision. # -# Triggers: -# - push to main (if self-hosted runner is available) -# - manual dispatch with optional host override +# Required repository secrets (for provisioning): +# PVE_ENDPOINT - Parent PVE API URL (e.g. https://pve.example.com:8006) +# PVE_API_TOKEN - Parent PVE API token (for Terraform + uploads) +# PVE_TARGET_NODE - Parent PVE node name # -# Runner setup: -# See tests/infrastructure/runner/README.md for self-hosted runner -# installation instructions. -# -# Required repository secrets: -# PVETEST_HOST - Hostname or IP of the PVE node -# PVETEST_PORT - API port (default 8006) -# PVETEST_APITOKEN - API token in USER@REALM!TOKENID=UUID format -# PVETEST_NODE - Node name to run tests against -# PVETEST_STORAGE - Storage name for upload tests -# PVETEST_ISO_PATH - Path to a small test ISO on the runner +# Optional secrets (for skip_provision mode): +# PVETEST_HOST - Hostname or IP of a pre-existing nested PVE +# PVETEST_APITOKEN - API token for the pre-existing nested PVE # # WARNING: Integration tests create and destroy real VMs, upload files, # and modify network configuration. Never run against production. @@ -29,85 +23,191 @@ on: branches: [ main ] workflow_dispatch: inputs: - pve_host: - description: 'Proxmox VE host (overrides secret)' + pve_version: + description: 'PVE version to test (8 or 9)' required: false - type: string - skip_terraform: - description: 'Skip Terraform provisioning (use existing PVE)' + type: choice + options: + - '9' + - '8' + default: '9' + skip_provision: + description: 'Skip provisioning (use existing PVE from PVETEST_HOST secret)' required: false type: boolean - default: true + default: false + +env: + INFRA_DIR: tests/infrastructure + SCRIPTS_DIR: tests/infrastructure/scripts + PVE_PASSWORD: "Testpass123!" jobs: - # Gate job: only proceed if a self-hosted runner with the right labels - # picks up the job AND secrets are configured integration: runs-on: [self-hosted, proxmox, integration] - timeout-minutes: 30 + timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - name: Check required secrets - id: check-secrets + # ── Provision nested PVE ─────────────────────────────────────────── + + - name: Select PVE ISO + if: inputs.skip_provision != true + id: iso shell: bash run: | - if [ -z "${{ secrets.PVETEST_HOST }}" ] && [ -z "${{ inputs.pve_host }}" ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "::warning::Integration tests skipped - PVETEST_HOST not configured" - elif [ -z "${{ secrets.PVETEST_APITOKEN }}" ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "::warning::Integration tests skipped - PVETEST_APITOKEN not configured" + VERSION="${{ inputs.pve_version || '9' }}" + if [ "$VERSION" = "9" ]; then + echo "base=/opt/pve-isos/proxmox-ve_9.1-1.iso" >> "$GITHUB_OUTPUT" + echo "filename=proxmox-ve_9.1-1-auto.iso" >> "$GITHUB_OUTPUT" else - echo "skip=false" >> $GITHUB_OUTPUT + echo "base=/opt/pve-isos/proxmox-ve_8.4-1.iso" >> "$GITHUB_OUTPUT" + echo "filename=proxmox-ve_8.4-1-auto.iso" >> "$GITHUB_OUTPUT" + fi + + - name: Generate answer file + if: inputs.skip_provision != true + shell: bash + run: | + sed "s/\${root_password}/${PVE_PASSWORD}/" \ + ${INFRA_DIR}/answer.toml.tftpl > /tmp/answer.toml + echo "Generated answer file:" + cat /tmp/answer.toml + + - name: Prepare auto-install ISO + if: inputs.skip_provision != true + shell: bash + run: | + bash ${SCRIPTS_DIR}/prepare-auto-iso.sh \ + "${{ steps.iso.outputs.base }}" \ + /tmp/answer.toml \ + ${SCRIPTS_DIR}/first-boot.sh \ + "/tmp/${{ steps.iso.outputs.filename }}" + + - name: Upload ISO to PVE storage + if: inputs.skip_provision != true + id: upload + shell: bash + env: + PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} + run: | + bash ${SCRIPTS_DIR}/upload-to-pve.sh \ + "$(echo '${{ secrets.PVE_ENDPOINT }}' | sed -E 's|https?://||;s|:.*||')" \ + "${PVE_API_TOKEN}" \ + "${{ secrets.PVE_TARGET_NODE }}" \ + "local" \ + "/tmp/${{ steps.iso.outputs.filename }}" \ + "iso" | tee /tmp/upload-output.txt + ISO_VOLID=$(tail -1 /tmp/upload-output.txt) + echo "iso_volid=${ISO_VOLID}" >> "$GITHUB_OUTPUT" + + - name: Terraform init + if: inputs.skip_provision != true + shell: bash + working-directory: ${{ env.INFRA_DIR }} + run: terraform init -input=false + + - name: Terraform apply + if: inputs.skip_provision != true + id: terraform + shell: bash + working-directory: ${{ env.INFRA_DIR }} + env: + TF_VAR_proxmox_endpoint: ${{ secrets.PVE_ENDPOINT }} + TF_VAR_proxmox_api_token: ${{ secrets.PVE_API_TOKEN }} + TF_VAR_target_node: ${{ secrets.PVE_TARGET_NODE }} + TF_VAR_iso_file_id: ${{ steps.upload.outputs.iso_volid }} + TF_VAR_test_vm_password: ${{ env.PVE_PASSWORD }} + run: | + terraform apply -auto-approve -input=false + echo "vm_id=$(terraform output -raw pve_test_vm_id)" >> "$GITHUB_OUTPUT" + + - name: Wait for install and create API token + if: inputs.skip_provision != true + id: provision + shell: bash + env: + PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} + run: | + PARENT_HOST=$(echo '${{ secrets.PVE_ENDPOINT }}' | sed -E 's|https?://||;s|:.*||') + OUTPUT=$(bash ${SCRIPTS_DIR}/create-api-token.sh \ + "${PARENT_HOST}" \ + "${PVE_API_TOKEN}" \ + "${{ steps.terraform.outputs.vm_id }}" \ + "${PVE_PASSWORD}" \ + 900) + echo "$OUTPUT" + VM_IP=$(echo "$OUTPUT" | grep "^IP=" | cut -d= -f2) + VM_TOKEN=$(echo "$OUTPUT" | grep "^TOKEN=" | cut -d= -f2-) + echo "host=${VM_IP}" >> "$GITHUB_OUTPUT" + echo "token=${VM_TOKEN}" >> "$GITHUB_OUTPUT" + + # ── Resolve test target ──────────────────────────────────────────── + + - name: Set test target + id: target + shell: bash + run: | + if [ "${{ inputs.skip_provision }}" = "true" ]; then + echo "host=${{ secrets.PVETEST_HOST }}" >> "$GITHUB_OUTPUT" + echo "port=${{ secrets.PVETEST_PORT || '8006' }}" >> "$GITHUB_OUTPUT" + echo "token=${{ secrets.PVETEST_APITOKEN }}" >> "$GITHUB_OUTPUT" + echo "node=${{ secrets.PVETEST_NODE || 'pve' }}" >> "$GITHUB_OUTPUT" + echo "storage=${{ secrets.PVETEST_STORAGE || 'local' }}" >> "$GITHUB_OUTPUT" + else + echo "host=${{ steps.provision.outputs.host }}" >> "$GITHUB_OUTPUT" + echo "port=8006" >> "$GITHUB_OUTPUT" + echo "token=${{ steps.provision.outputs.token }}" >> "$GITHUB_OUTPUT" + echo "node=pve" >> "$GITHUB_OUTPUT" + echo "storage=local" >> "$GITHUB_OUTPUT" fi - name: Verify PVE API reachable - if: steps.check-secrets.outputs.skip != 'true' shell: bash - env: - PVETEST_HOST: ${{ inputs.pve_host || secrets.PVETEST_HOST }} - PVETEST_PORT: ${{ secrets.PVETEST_PORT || '8006' }} - PVETEST_APITOKEN: ${{ secrets.PVETEST_APITOKEN }} run: | - echo "Testing connectivity to PVE API at ${PVETEST_HOST}:${PVETEST_PORT}..." + HOST="${{ steps.target.outputs.host }}" + PORT="${{ steps.target.outputs.port }}" + TOKEN="${{ steps.target.outputs.token }}" + echo "Testing connectivity to PVE API at ${HOST}:${PORT}..." if curl -sk --connect-timeout 10 \ - -H "Authorization: PVEAPIToken=${PVETEST_APITOKEN}" \ - "https://${PVETEST_HOST}:${PVETEST_PORT}/api2/json/nodes" | grep -q '"node"'; then + -H "Authorization: PVEAPIToken=${TOKEN}" \ + "https://${HOST}:${PORT}/api2/json/nodes" | grep -q '"node"'; then echo "PVE API is responsive" else - echo "::error::Cannot reach PVE API at ${PVETEST_HOST}:${PVETEST_PORT}" + echo "::error::Cannot reach PVE API at ${HOST}:${PORT}" exit 1 fi + # ── Build and test ───────────────────────────────────────────────── + - name: Build module - if: steps.check-secrets.outputs.skip != 'true' run: dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --framework net9.0 --output ./publish/net9.0 - name: Install Pester 5 - if: steps.check-secrets.outputs.skip != 'true' shell: pwsh run: Install-Module -Name Pester -MinimumVersion 5.0 -Force -Scope CurrentUser - name: Copy module to module path - if: steps.check-secrets.outputs.skip != 'true' shell: pwsh run: | $modulePath = "$HOME/.local/share/powershell/Modules/PSProxmoxVE" New-Item -ItemType Directory -Path $modulePath -Force | Out-Null Copy-Item -Path ./publish/net9.0/* -Destination $modulePath -Recurse -Force + - name: Create test ISO + shell: bash + run: dd if=/dev/urandom of=/tmp/pvetest.iso bs=1M count=1 2>/dev/null + - name: Run integration tests - if: steps.check-secrets.outputs.skip != 'true' shell: pwsh env: - PVETEST_HOST: ${{ inputs.pve_host || secrets.PVETEST_HOST }} - PVETEST_PORT: ${{ secrets.PVETEST_PORT || '8006' }} - PVETEST_APITOKEN: ${{ secrets.PVETEST_APITOKEN }} - PVETEST_NODE: ${{ secrets.PVETEST_NODE }} - PVETEST_STORAGE: ${{ secrets.PVETEST_STORAGE }} - PVETEST_ISO_PATH: ${{ secrets.PVETEST_ISO_PATH }} + PVETEST_HOST: ${{ steps.target.outputs.host }} + PVETEST_PORT: ${{ steps.target.outputs.port }} + PVETEST_APITOKEN: ${{ steps.target.outputs.token }} + PVETEST_NODE: ${{ steps.target.outputs.node }} + PVETEST_STORAGE: ${{ steps.target.outputs.storage }} + PVETEST_ISO_PATH: /tmp/pvetest.iso run: | Import-Module Pester -MinimumVersion 5.0 $config = New-PesterConfiguration @@ -120,8 +220,34 @@ jobs: Invoke-Pester -Configuration $config - name: Upload test results - if: always() && steps.check-secrets.outputs.skip != 'true' + if: always() uses: actions/upload-artifact@v4 with: name: integration-test-results path: TestResults/ + + # ── Teardown ─────────────────────────────────────────────────────── + + - name: Terraform destroy + if: always() && inputs.skip_provision != true + shell: bash + working-directory: ${{ env.INFRA_DIR }} + env: + TF_VAR_proxmox_endpoint: ${{ secrets.PVE_ENDPOINT }} + TF_VAR_proxmox_api_token: ${{ secrets.PVE_API_TOKEN }} + TF_VAR_target_node: ${{ secrets.PVE_TARGET_NODE }} + TF_VAR_iso_file_id: ${{ steps.upload.outputs.iso_volid }} + TF_VAR_test_vm_password: ${{ env.PVE_PASSWORD }} + run: terraform destroy -auto-approve -input=false + + - name: Clean up uploaded ISO + if: always() && inputs.skip_provision != true + shell: bash + env: + PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} + run: | + PARENT_HOST=$(echo '${{ secrets.PVE_ENDPOINT }}' | sed -E 's|https?://||;s|:.*||') + bash ${SCRIPTS_DIR}/cleanup-pve-storage.sh \ + "${PARENT_HOST}" \ + "${PVE_API_TOKEN}" \ + "${{ steps.upload.outputs.iso_volid }}" || true diff --git a/tests/infrastructure/answer.toml.tftpl b/tests/infrastructure/answer.toml.tftpl index 7415025..4ccce1e 100644 --- a/tests/infrastructure/answer.toml.tftpl +++ b/tests/infrastructure/answer.toml.tftpl @@ -5,13 +5,12 @@ fqdn = "pve-test.local" mailto = "test@test.local" timezone = "UTC" root_password = "${root_password}" +reboot_on_error = true +root_ssh_keys = [] +on_first_boot = "first-boot.sh" [network] -source = "from-answer" -cidr = "${cidr}" -dns = "${dns}" -gateway = "${gateway}" -filter.ID_NET_NAME = "*" +source = "from-dhcp" [disk-setup] filesystem = "ext4" diff --git a/tests/infrastructure/main.tf b/tests/infrastructure/main.tf index 2070784..1dce7f8 100644 --- a/tests/infrastructure/main.tf +++ b/tests/infrastructure/main.tf @@ -12,11 +12,6 @@ provider "proxmox" { endpoint = var.proxmox_endpoint api_token = var.proxmox_api_token insecure = var.proxmox_insecure - - ssh { - agent = true - username = "root" - } } resource "proxmox_virtual_environment_vm" "nested_pve" { @@ -63,6 +58,10 @@ resource "proxmox_virtual_environment_vm" "nested_pve" { type = "l26" } + agent { + enabled = true + } + started = true lifecycle { diff --git a/tests/infrastructure/outputs.tf b/tests/infrastructure/outputs.tf index a972799..4fcec7b 100644 --- a/tests/infrastructure/outputs.tf +++ b/tests/infrastructure/outputs.tf @@ -1,15 +1,3 @@ -output "pve_test_host" { - value = var.test_vm_ip -} - -output "pve_test_port" { - value = 8006 -} - -output "pve_test_url" { - value = "https://${var.test_vm_ip}:8006" -} - output "pve_test_vm_id" { value = var.vm_id } @@ -18,8 +6,3 @@ output "pve_test_node_name" { value = "pve" description = "Default node name inside a fresh PVE install" } - -output "pve_test_api_token" { - value = fileexists("${path.module}/.api-token") ? trimspace(file("${path.module}/.api-token")) : "not-yet-created" - sensitive = true -} diff --git a/tests/infrastructure/scripts/cleanup-pve-storage.sh b/tests/infrastructure/scripts/cleanup-pve-storage.sh new file mode 100755 index 0000000..6766bff --- /dev/null +++ b/tests/infrastructure/scripts/cleanup-pve-storage.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Remove an uploaded ISO from PVE storage. +# +# Usage: cleanup-pve-storage.sh +# e.g.: cleanup-pve-storage.sh 172.16.100.150 "root@pam!tok=secret" "local:iso/proxmox-ve_9.1-1-auto.iso" +set -euo pipefail + +PVE_HOST="$1" +API_TOKEN="$2" +VOLUME_ID="$3" + +# Extract node name from API +NODE=$(curl -sk -H "Authorization: PVEAPIToken=${API_TOKEN}" \ + "https://${PVE_HOST}/api2/json/nodes" \ + | python3 -c "import json,sys; print(json.load(sys.stdin)['data'][0]['node'])") + +echo "Deleting ${VOLUME_ID} from node ${NODE}..." +ENCODED_VOLID=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${VOLUME_ID}', safe=''))") + +RESPONSE=$(curl -sk -X DELETE \ + -H "Authorization: PVEAPIToken=${API_TOKEN}" \ + "https://${PVE_HOST}/api2/json/nodes/${NODE}/storage/local/content/${ENCODED_VOLID}") + +echo "Response: $RESPONSE" +echo "Cleanup complete." diff --git a/tests/infrastructure/scripts/create-api-token.sh b/tests/infrastructure/scripts/create-api-token.sh new file mode 100755 index 0000000..0d409a6 --- /dev/null +++ b/tests/infrastructure/scripts/create-api-token.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Wait for a fresh nested PVE instance to boot, discover its IP via the QEMU guest agent, +# then wait for the PVE API and create an API token. +# +# Usage: create-api-token.sh [max-wait-seconds] +# Outputs two lines: +# IP= +# TOKEN=root@pam!integration= +set -euo pipefail + +PARENT_HOST="$1" +PARENT_TOKEN="$2" +VM_ID="$3" +ROOT_PASSWORD="$4" +MAX_WAIT="${5:-600}" +INTERVAL=10 +PARENT_API="https://${PARENT_HOST}/api2/json" +PARENT_NODE=$(curl -sk -H "Authorization: PVEAPIToken=${PARENT_TOKEN}" \ + "${PARENT_API}/nodes" | python3 -c "import json,sys; print(json.load(sys.stdin)['data'][0]['node'])") + +# --- Phase 1: Discover IP via QEMU guest agent --- +echo "Waiting for guest agent on VM ${VM_ID} (node: ${PARENT_NODE})..." +VM_IP="" +elapsed=0 +while [ $elapsed -lt $MAX_WAIT ]; do + AGENT_RESPONSE=$(curl -sk \ + -H "Authorization: PVEAPIToken=${PARENT_TOKEN}" \ + "${PARENT_API}/nodes/${PARENT_NODE}/qemu/${VM_ID}/agent/network-get-interfaces" 2>/dev/null || true) + + VM_IP=$(echo "$AGENT_RESPONSE" | python3 -c " +import json, sys +try: + data = json.load(sys.stdin).get('data', {}).get('result', []) + for iface in data: + if iface.get('name') == 'lo': + continue + for addr in iface.get('ip-addresses', []): + if addr.get('ip-address-type') == 'ipv4' and not addr['ip-address'].startswith('127.'): + print(addr['ip-address']) + sys.exit(0) +except: + pass +" 2>/dev/null || true) + + if [ -n "$VM_IP" ]; then + echo "Discovered VM IP: $VM_IP (after ${elapsed}s)" + break + fi + echo " Guest agent not ready yet (${elapsed}s elapsed)..." + sleep $INTERVAL + elapsed=$((elapsed + INTERVAL)) +done + +if [ -z "$VM_IP" ]; then + echo "ERROR: Could not discover VM IP via guest agent after ${MAX_WAIT}s" >&2 + exit 1 +fi + +# --- Phase 2: Wait for PVE API on the nested instance --- +NESTED_API="https://${VM_IP}:8006/api2/json" +echo "Waiting for nested PVE API at ${NESTED_API}..." +while [ $elapsed -lt $MAX_WAIT ]; do + if curl -sk --connect-timeout 5 "${NESTED_API}/version" 2>/dev/null | grep -q '"version"'; then + echo "Nested PVE API is responsive after ${elapsed}s" + break + fi + echo " API not ready yet (${elapsed}s elapsed)..." + sleep $INTERVAL + elapsed=$((elapsed + INTERVAL)) +done + +if [ $elapsed -ge $MAX_WAIT ]; then + echo "ERROR: Nested PVE API not responsive after ${MAX_WAIT}s" >&2 + exit 1 +fi + +# --- Phase 3: Authenticate and create API token --- +echo "Authenticating as root@pam on nested PVE..." +AUTH_RESPONSE=$(curl -sk -d "username=root@pam&password=${ROOT_PASSWORD}" \ + "${NESTED_API}/access/ticket") +TICKET=$(echo "$AUTH_RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['ticket'])" 2>/dev/null || true) +CSRF=$(echo "$AUTH_RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['CSRFPreventionToken'])" 2>/dev/null || true) + +if [ -z "$TICKET" ] || [ -z "$CSRF" ]; then + echo "ERROR: Authentication failed. Response: $AUTH_RESPONSE" >&2 + exit 1 +fi + +echo "Creating API token root@pam!integration..." +TOKEN_RESPONSE=$(curl -sk \ + -b "PVEAuthCookie=${TICKET}" \ + -H "CSRFPreventionToken: ${CSRF}" \ + -d "privsep=0" \ + "${NESTED_API}/access/users/root@pam/token/integration") + +TOKEN_VALUE=$(echo "$TOKEN_RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['value'])" 2>/dev/null || true) + +if [ -z "$TOKEN_VALUE" ]; then + echo "ERROR: Token creation failed. Response: $TOKEN_RESPONSE" >&2 + exit 1 +fi + +echo "IP=${VM_IP}" +echo "TOKEN=root@pam!integration=${TOKEN_VALUE}" diff --git a/tests/infrastructure/scripts/first-boot.sh b/tests/infrastructure/scripts/first-boot.sh new file mode 100755 index 0000000..0a3934e --- /dev/null +++ b/tests/infrastructure/scripts/first-boot.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# First-boot script for nested PVE test instances. +# Runs once after auto-install completes and the system reboots. +# Installs qemu-guest-agent so the parent PVE can discover the VM's IP via the guest agent API. + +set -e + +apt-get update -qq +apt-get install -y -qq qemu-guest-agent +systemctl enable --now qemu-guest-agent diff --git a/tests/infrastructure/scripts/prepare-auto-iso.sh b/tests/infrastructure/scripts/prepare-auto-iso.sh new file mode 100755 index 0000000..3ab2b14 --- /dev/null +++ b/tests/infrastructure/scripts/prepare-auto-iso.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Prepare a PVE auto-install ISO with the answer file and first-boot script baked in. +# Uses --fetch-from iso mode so only a single CD-ROM is needed, no HTTP server required. +# +# Usage: prepare-auto-iso.sh [first-boot-script] [output-iso] +set -euo pipefail + +BASE_ISO="$1" +ANSWER_FILE="$2" +FIRST_BOOT="${3:-}" +OUTPUT_ISO="${4:-${BASE_ISO%.iso}-auto.iso}" + +for f in "$BASE_ISO" "$ANSWER_FILE"; do + if [ ! -f "$f" ]; then + echo "ERROR: File not found: $f" >&2 + exit 1 + fi +done + +FIRST_BOOT_ARGS=() +if [ -n "$FIRST_BOOT" ] && [ -f "$FIRST_BOOT" ]; then + FIRST_BOOT_ARGS=(--on-first-boot "$FIRST_BOOT") +fi + +echo "Preparing auto-install ISO..." +echo " Base ISO: $BASE_ISO" +echo " Answer file: $ANSWER_FILE" +echo " First boot: ${FIRST_BOOT:-none}" +echo " Output: $OUTPUT_ISO" + +proxmox-auto-install-assistant prepare-iso \ + --fetch-from iso \ + --answer-file "$ANSWER_FILE" \ + "${FIRST_BOOT_ARGS[@]}" \ + --output "$OUTPUT_ISO" \ + "$BASE_ISO" + +echo "Created: $OUTPUT_ISO" diff --git a/tests/infrastructure/scripts/upload-to-pve.sh b/tests/infrastructure/scripts/upload-to-pve.sh new file mode 100755 index 0000000..82aa4b6 --- /dev/null +++ b/tests/infrastructure/scripts/upload-to-pve.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Upload a file to PVE storage via the REST API. +# +# Usage: upload-to-pve.sh +# content-type: "iso" or "vztmpl" +set -euo pipefail + +PVE_HOST="$1" +API_TOKEN="$2" +NODE="$3" +STORAGE="$4" +FILE_PATH="$5" +CONTENT_TYPE="${6:-iso}" + +FILENAME=$(basename "$FILE_PATH") +API_URL="https://${PVE_HOST}/api2/json/nodes/${NODE}/storage/${STORAGE}/upload" + +# Check if file already exists on storage +echo "Checking if ${FILENAME} already exists on ${STORAGE}..." +EXISTING=$(curl -sk \ + -H "Authorization: PVEAPIToken=${API_TOKEN}" \ + "https://${PVE_HOST}/api2/json/nodes/${NODE}/storage/${STORAGE}/content" \ + | python3 -c " +import json, sys +data = json.load(sys.stdin).get('data', []) +for item in data: + if item.get('volid', '').endswith('/${FILENAME}'): + print(item['volid']) + break +" 2>/dev/null || true) + +if [ -n "$EXISTING" ]; then + echo "Already exists: $EXISTING (skipping upload)" + echo "$EXISTING" + exit 0 +fi + +echo "Uploading ${FILENAME} ($(du -h "$FILE_PATH" | cut -f1)) to ${STORAGE}:${CONTENT_TYPE}/..." +RESPONSE=$(curl -sk --progress-bar \ + -H "Authorization: PVEAPIToken=${API_TOKEN}" \ + -F "content=${CONTENT_TYPE}" \ + -F "filename=@${FILE_PATH}" \ + "$API_URL") + +UPID=$(echo "$RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin)['data'])" 2>/dev/null || true) +if [ -z "$UPID" ]; then + echo "ERROR: Upload failed. Response: $RESPONSE" >&2 + exit 1 +fi + +echo "Upload started: $UPID" + +# Wait for upload task to complete +echo "Waiting for upload task..." +for i in $(seq 1 60); do + STATUS=$(curl -sk \ + -H "Authorization: PVEAPIToken=${API_TOKEN}" \ + "https://${PVE_HOST}/api2/json/nodes/${NODE}/tasks/${UPID}/status" \ + | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(d.get('status','unknown'))" 2>/dev/null || echo "unknown") + if [ "$STATUS" = "stopped" ]; then + echo "Upload complete: ${STORAGE}:${CONTENT_TYPE}/${FILENAME}" + echo "${STORAGE}:${CONTENT_TYPE}/${FILENAME}" + exit 0 + fi + sleep 2 +done + +echo "ERROR: Upload task did not complete within timeout" >&2 +exit 1 diff --git a/tests/infrastructure/terraform.tfvars.example b/tests/infrastructure/terraform.tfvars.example index f5d11f2..7c0e6ae 100644 --- a/tests/infrastructure/terraform.tfvars.example +++ b/tests/infrastructure/terraform.tfvars.example @@ -3,16 +3,12 @@ proxmox_endpoint = "https://pve.example.com:8006" proxmox_api_token = "root@pam!terraform=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" target_node = "pve" -# Path to PVE ISO (download from proxmox.com/en/downloads) -iso_file = "/path/to/proxmox-ve_9.1-1.iso" - -# Network config for nested PVE (must be routable from CI runner) -test_vm_ip = "192.168.1.200" -test_vm_gateway = "192.168.1.1" +# Auto-install ISO (prepared by scripts/prepare-auto-iso.sh, then uploaded) +iso_file_id = "local:iso/proxmox-ve_9.1-1-auto.iso" +disk_storage = "local-lvm" # Optional overrides # cores = 4 # memory = 8192 # disk_size = 64 -# disk_storage = "local-lvm" # network_bridge = "vmbr0" diff --git a/tests/infrastructure/variables.tf b/tests/infrastructure/variables.tf index 1e8fde7..6a95b44 100644 --- a/tests/infrastructure/variables.tf +++ b/tests/infrastructure/variables.tf @@ -57,9 +57,8 @@ variable "disk_storage" { } variable "iso_file_id" { - description = "Proxmox file ID of the PVE installation ISO (e.g. nas-nfs:iso/proxmox-ve_9.1-1.iso)" + description = "Proxmox file ID of the auto-install PVE ISO (e.g. local:iso/proxmox-ve_9.1-1-auto.iso)" type = string - default = "nas-nfs:iso/proxmox-ve_9.1-1.iso" } variable "network_bridge" { @@ -68,32 +67,9 @@ variable "network_bridge" { default = "Core" } -variable "test_vm_ip" { - description = "Static IP address to assign to the nested PVE instance" - type = string -} - -variable "test_vm_gateway" { - description = "Default gateway for the nested PVE instance" - type = string -} - -variable "test_vm_netmask_bits" { - description = "CIDR prefix length for the nested PVE network (e.g. 24 for /24)" - type = string - default = "24" -} - -variable "test_vm_dns" { - description = "DNS server for the nested PVE instance" - type = string - default = "1.1.1.1" -} - variable "test_vm_password" { description = "Root password for the nested PVE instance" type = string sensitive = true default = "Testpass123!" } -