fix(ci): use Terraform provider for ISO upload, fix port/lockfile issues

- Use proxmox_virtual_environment_file resource to upload the auto-install
  ISO via the API. terraform destroy now cleans up both the VM and ISO.
- Remove manual upload-to-pve.sh and cleanup-pve-storage.sh scripts.
- Commit .terraform.lock.hcl for reproducible CI builds.
- Replace iso_file_id variable with iso_local_path and iso_storage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-18 14:19:24 -05:00
parent 910c686abc
commit 0dff14f4ba
8 changed files with 89 additions and 132 deletions
+2 -31
View File
@@ -85,23 +85,6 @@ jobs:
${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
@@ -117,7 +100,7 @@ jobs:
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_iso_local_path: /tmp/${{ steps.iso.outputs.filename }}
TF_VAR_test_vm_password: ${{ env.PVE_PASSWORD }}
run: |
terraform apply -auto-approve -input=false
@@ -236,18 +219,6 @@ jobs:
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_iso_local_path: /tmp/${{ steps.iso.outputs.filename }}
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
+1 -1
View File
@@ -2,6 +2,6 @@
.terraform/
*.tfvars
!terraform.tfvars.example
.terraform.lock.hcl
# Keep .terraform.lock.hcl tracked for reproducible CI builds
.api-token
.build/
+63
View File
@@ -0,0 +1,63 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/bpg/proxmox" {
version = "0.98.1"
constraints = ">= 0.70.0"
hashes = [
"h1:oegYetLP9VqFSnRfHeUQnm6v58dH/VT4ivJSXg4RLnI=",
"zh:165705d0a0db92ccc5b1d23b0033cc87011f1151f4f178ad9df61473c3ca30b5",
"zh:3ecab62be80d89cf8f019ab23809254f854eb5a443682a1a22a52e0d80b1273f",
"zh:48b520a3f5b8cccca66e0dc7b1e264e01d2f35ef8a9bbfc6b02964cd7235914b",
"zh:7fa7e166b924b5192ff963f47a56772c7b34f0004d825d830c3122eab970534f",
"zh:804bdf5353d3a3ad9d06e6faada1b8600af8ecfd1e5b23280a81e62b0cb7037d",
"zh:87ebe8b56a895870ebc0d353fba5ebbb4615401dc3f53a2e157f158cb9025d2d",
"zh:9b0c8e02c036152bcb2f54c354da4647d10d016396e5ca2b70084a8bd5546970",
"zh:a0039465282dc75ac9c53750b61ba4038d56bed545cb39df669b6216bdff82da",
"zh:bbc2c187c8283b6a5f44cbd65f4b6e5d534a3be1eeb1a7c8caaaa6f1a7e48e76",
"zh:d04aed773582e95c8d7610833e85b204fe8c01c09c94e7128914c8e22b9218c7",
"zh:d7a1433a7d6f1b158f499a93d3fb1825eec73eac4c61b00a39f2f5e95ae0965a",
"zh:df46cb59fa1ddda335934720ad22d8205bb8902ce780ca32205f1622f1fa1649",
"zh:e100ad439313db667a5c4ca53ae4c54d8b1c72299a2240a1493823cb89cb0781",
"zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597",
]
}
provider "registry.terraform.io/hashicorp/local" {
version = "2.7.0"
constraints = ">= 2.0.0"
hashes = [
"h1:sSwlfp2etjCaE9hIF7bJBDjRIhDCVFglEOVyiCI7vgs=",
"zh:261fec71bca13e0a7812dc0d8ae9af2b4326b24d9b2e9beab3d2400fab5c5f9a",
"zh:308da3b5376a9ede815042deec5af1050ec96a5a5410a2206ae847d82070a23e",
"zh:3d056924c420464dc8aba10e1915956b2e5c4d55b11ffff79aa8be563fbfe298",
"zh:643256547b155459c45e0a3e8aab0570db59923c68daf2086be63c444c8c445b",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:7aa4d0b853f84205e8cf79f30c9b2c562afbfa63592f7231b6637e5d7a6b5b27",
"zh:7dc251bbc487d58a6ab7f5b07ec9edc630edb45d89b761dba28e0e2ba6b1c11f",
"zh:7ee0ca546cd065030039168d780a15cbbf1765a4c70cd56d394734ab112c93da",
"zh:b1d5d80abb1906e6c6b3685a52a0192b4ca6525fe090881c64ec6f67794b1300",
"zh:d81ea9856d61db3148a4fc6c375bf387a721d78fc1fea7a8823a027272a47a78",
"zh:df0a1f0afc947b8bfc88617c1ad07a689ce3bd1a29fd97318392e6bdd32b230b",
"zh:dfbcad800240e0c68c43e0866f2a751cff09777375ec701918881acf67a268da",
]
}
provider "registry.terraform.io/hashicorp/null" {
version = "3.2.4"
hashes = [
"h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=",
"zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43",
"zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a",
"zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991",
"zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f",
"zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e",
"zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615",
"zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442",
"zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5",
"zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f",
"zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f",
]
}
+11 -1
View File
@@ -14,6 +14,16 @@ provider "proxmox" {
insecure = var.proxmox_insecure
}
resource "proxmox_virtual_environment_file" "auto_iso" {
content_type = "iso"
datastore_id = var.iso_storage
node_name = var.target_node
source_file {
path = var.iso_local_path
}
}
resource "proxmox_virtual_environment_vm" "nested_pve" {
name = var.vm_name
node_name = var.target_node
@@ -45,7 +55,7 @@ resource "proxmox_virtual_environment_vm" "nested_pve" {
}
cdrom {
file_id = var.iso_file_id
file_id = proxmox_virtual_environment_file.auto_iso.id
interface = "ide2"
}
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
# Remove an uploaded ISO from PVE storage.
#
# Usage: cleanup-pve-storage.sh <pve-host> <api-token> <volume-id>
# 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."
@@ -1,69 +0,0 @@
#!/usr/bin/env bash
# Upload a file to PVE storage via the REST API.
#
# Usage: upload-to-pve.sh <pve-host> <api-token> <node> <storage> <file-path> <content-type>
# 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
@@ -3,12 +3,13 @@ proxmox_endpoint = "https://pve.example.com:8006"
proxmox_api_token = "root@pam!terraform=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
target_node = "pve"
# 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"
# Local path to the prepared auto-install ISO (output of prepare-auto-iso.sh)
iso_local_path = "/tmp/proxmox-ve_9.1-1-auto.iso"
disk_storage = "local-lvm"
# Optional overrides
# cores = 4
# memory = 8192
# disk_size = 64
# network_bridge = "vmbr0"
# iso_storage = "local"
+8 -2
View File
@@ -56,11 +56,17 @@ variable "disk_storage" {
default = "nas-iscsi-lvm"
}
variable "iso_file_id" {
description = "Proxmox file ID of the auto-install PVE ISO (e.g. local:iso/proxmox-ve_9.1-1-auto.iso)"
variable "iso_local_path" {
description = "Local path to the prepared auto-install PVE ISO on the runner"
type = string
}
variable "iso_storage" {
description = "Proxmox storage pool for uploading the ISO (must accept ISO content type)"
type = string
default = "local"
}
variable "network_bridge" {
description = "Network bridge on the host to attach the nested PVE VM to"
type = string