Merge pull request #87 from GoodOlClint/ci/pester-exit-sandbox-storage

ci: storage and answer services move to a VM inside the CI sandbox
This commit is contained in:
GoodOlClint
2026-08-31 15:03:54 -05:00
committed by GitHub
11 changed files with 327 additions and 299 deletions
+7 -10
View File
@@ -24,6 +24,9 @@ name: Integration Tests
# node with the most free memory and refuses to run when # node with the most free memory and refuses to run when
# no node has enough headroom — the token's user needs # no node has enough headroom — the token's user needs
# PVEAuditor on /nodes for the memory stats. # PVEAuditor on /nodes for the memory stats.
# STORAGE_VM_IP - Static CIDR address for the shared storage/answer-server
# VM on the CI VLAN (default: 172.16.60.60/24)
# STORAGE_VM_GATEWAY - Gateway + DNS for that VM (default: 172.16.60.1)
# #
# Optional secrets (for skip_provision mode): # Optional secrets (for skip_provision mode):
# PVETEST_HOST - Hostname or IP of a pre-existing nested PVE # PVETEST_HOST - Hostname or IP of a pre-existing nested PVE
@@ -122,11 +125,6 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
volumes: volumes:
- /opt/pve-integration:/opt/pve-integration - /opt/pve-integration:/opt/pve-integration
# WARNING: The Docker socket is mounted to allow storage containers to be
# managed from within the job container. This grants root-equivalent access
# to the runner's Docker daemon. Only use on dedicated, isolated self-hosted
# runners — never on shared runners.
- /var/run/docker.sock:/var/run/docker.sock
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
@@ -138,6 +136,8 @@ jobs:
PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }}
PVE_TARGET_NODE: ${{ vars.PVE_TARGET_NODE }} PVE_TARGET_NODE: ${{ vars.PVE_TARGET_NODE }}
PVE_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }} PVE_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }}
STORAGE_VM_IP: ${{ vars.STORAGE_VM_IP }}
STORAGE_VM_GATEWAY: ${{ vars.STORAGE_VM_GATEWAY }}
TF_VAR_disk_storage: ${{ vars.DISK_STORAGE }} TF_VAR_disk_storage: ${{ vars.DISK_STORAGE }}
TF_VAR_iso_storage: ${{ vars.ISO_STORAGE }} TF_VAR_iso_storage: ${{ vars.ISO_STORAGE }}
TF_VAR_network_bridge: ${{ vars.NETWORK_BRIDGE }} TF_VAR_network_bridge: ${{ vars.NETWORK_BRIDGE }}
@@ -201,11 +201,6 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
volumes: volumes:
- /opt/pve-integration:/opt/pve-integration - /opt/pve-integration:/opt/pve-integration
# WARNING: The Docker socket is mounted to allow storage containers to be
# stopped from within the job container. This grants root-equivalent access
# to the runner's Docker daemon. Only use on dedicated, isolated self-hosted
# runners — never on shared runners.
- /var/run/docker.sock:/var/run/docker.sock
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
@@ -217,6 +212,8 @@ jobs:
PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }} PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }}
PVE_TARGET_NODE: ${{ vars.PVE_TARGET_NODE }} PVE_TARGET_NODE: ${{ vars.PVE_TARGET_NODE }}
PVE_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }} PVE_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }}
STORAGE_VM_IP: ${{ vars.STORAGE_VM_IP }}
STORAGE_VM_GATEWAY: ${{ vars.STORAGE_VM_GATEWAY }}
TF_VAR_disk_storage: ${{ vars.DISK_STORAGE }} TF_VAR_disk_storage: ${{ vars.DISK_STORAGE }}
TF_VAR_iso_storage: ${{ vars.ISO_STORAGE }} TF_VAR_iso_storage: ${{ vars.ISO_STORAGE }}
TF_VAR_network_bridge: ${{ vars.NETWORK_BRIDGE }} TF_VAR_network_bridge: ${{ vars.NETWORK_BRIDGE }}
@@ -4,7 +4,7 @@
Pester 5 integration tests for shared storage backends (NFS, iSCSI). Pester 5 integration tests for shared storage backends (NFS, iSCSI).
These tests require the multi-node integration test infrastructure with These tests require the multi-node integration test infrastructure with
Docker-based storage services (iSCSI target + NFS server). They are the shared storage VM (iSCSI target + NFS server on the CI VLAN). They are
SKIPPED when the storage env vars are not set. SKIPPED when the storage env vars are not set.
Required environment variables (in addition to base integration vars): Required environment variables (in addition to base integration vars):
+2
View File
@@ -163,7 +163,9 @@ Note that AMD nested virtualization can be less stable than Intel in some config
| `main.tf` | Provider config, VM resource, provisioners | | `main.tf` | Provider config, VM resource, provisioners |
| `variables.tf` | Input variable definitions with defaults | | `variables.tf` | Input variable definitions with defaults |
| `outputs.tf` | Test connection details for integration tests | | `outputs.tf` | Test connection details for integration tests |
| `storage-vm.tf` | Shared NFS/iSCSI/answer-server VM on the CI VLAN |
| `answer.toml.tftpl` | Unattended PVE installer answer file template | | `answer.toml.tftpl` | Unattended PVE installer answer file template |
| `scripts/setup-storage-server.sh` | Configures services on the storage VM over SSH |
| `scripts/wait-for-api.sh` | Polls PVE API until responsive | | `scripts/wait-for-api.sh` | Polls PVE API until responsive |
| `terraform.tfvars.example` | Example variable values | | `terraform.tfvars.example` | Example variable values |
| `.gitignore` | Excludes state, provider cache, secrets | | `.gitignore` | Excludes state, provider cache, secrets |
@@ -1,66 +0,0 @@
## Shared storage services for integration testing.
## Provides iSCSI target and NFS server accessible from nested PVE nodes.
##
## These run on the GitHub runner (or any Docker host on the same network
## as the PVE nodes) using host networking so the PVE nodes can reach them.
##
## Usage:
## docker compose -f tests/infrastructure/docker-compose.storage.yml up -d
## docker compose -f tests/infrastructure/docker-compose.storage.yml down -v
services:
iscsi-target:
image: ubuntu:24.04
container_name: pvetest-iscsi
network_mode: host
privileged: true
volumes:
- iscsi-data:/srv/iscsi
environment:
ISCSI_IQN: ${ISCSI_IQN:-iqn.2024-01.local.test:storage}
ISCSI_LUN_SIZE: ${ISCSI_LUN_SIZE:-10G}
entrypoint: ["/bin/bash", "-c"]
command:
- |
set -e
apt-get update -qq && apt-get install -y -qq tgt >/dev/null 2>&1
mkdir -p /srv/iscsi
if [ ! -f /srv/iscsi/lun0.img ]; then
truncate -s $$ISCSI_LUN_SIZE /srv/iscsi/lun0.img
fi
tgtd --foreground &
sleep 2
# Create target if it does not already exist
if ! tgtadm --lld iscsi --op show --mode target | grep -q "Target 1: $$ISCSI_IQN"; then
tgtadm --lld iscsi --op new --mode target --tid 1 -T $$ISCSI_IQN
fi
# Create logical unit (LUN 1) if it does not already exist
if ! tgtadm --lld iscsi --op show --mode logicalunit --tid 1 2>/dev/null | grep -qE "LUN:[[:space:]]*1($|[^0-9])"; then
tgtadm --lld iscsi --op new --mode logicalunit --tid 1 --lun 1 --backing-store /srv/iscsi/lun0.img
fi
# Bind target to all initiators if not already bound
if ! tgtadm --lld iscsi --op show --mode target --tid 1 2>/dev/null | grep -q "Initiator-address: ALL"; then
tgtadm --lld iscsi --op bind --mode target --tid 1 -I ALL
fi
echo "iSCSI target ready: $$ISCSI_IQN (port 3260)"
wait
restart: unless-stopped
nfs-server:
image: erichough/nfs-server:2.2.1
container_name: pvetest-nfs
network_mode: host
privileged: true
volumes:
- nfs-data:/srv/nfs/shared
# NOTE: /lib/modules is required by the NFS kernel module loader.
# This mount is Linux-host-specific and will fail on macOS/Windows Docker Desktop.
# Integration tests using this compose file must run on a Linux host (e.g. GitHub-hosted runners or the dev container).
- /lib/modules:/lib/modules:ro
environment:
NFS_EXPORT_0: /srv/nfs/shared *(rw,sync,no_subtree_check,no_root_squash)
restart: unless-stopped
volumes:
iscsi-data:
nfs-data:
+6 -13
View File
@@ -3,11 +3,8 @@ terraform {
required_providers { required_providers {
proxmox = { proxmox = {
source = "bpg/proxmox" source = "bpg/proxmox"
version = ">= 0.70.0" # disk.import_from and content_type "import" need >= 0.79.0
} version = ">= 0.79.0"
docker = {
source = "kreuzwerker/docker"
version = ">= 3.0.0"
} }
} }
} }
@@ -18,11 +15,6 @@ provider "proxmox" {
insecure = var.proxmox_insecure insecure = var.proxmox_insecure
} }
provider "docker" {
# Uses the Docker socket from the dev-infra container
# (mounted at /var/run/docker.sock)
}
resource "proxmox_virtual_environment_file" "auto_iso" { resource "proxmox_virtual_environment_file" "auto_iso" {
for_each = var.pve_isos for_each = var.pve_isos
content_type = "iso" content_type = "iso"
@@ -91,9 +83,10 @@ resource "proxmox_virtual_environment_vm" "nested_pve" {
started = true started = true
# VMs must not boot until the HTTP answer server is running, # VMs must not boot until the storage VM exists — it serves the HTTP answer
# otherwise the PVE auto-installer can't fetch its answer file. # files the PVE auto-installer fetches. run-integration.sh additionally
depends_on = [docker_container.answer_server] # configures the storage VM (phase one) before applying these resources.
depends_on = [proxmox_virtual_environment_vm.storage]
lifecycle { lifecycle {
ignore_changes = [started, cdrom] ignore_changes = [started, cdrom]
+3 -13
View File
@@ -8,17 +8,7 @@ output "pve_test_node_name" {
description = "Default node name inside a fresh PVE install" description = "Default node name inside a fresh PVE install"
} }
output "storage_ip" { output "storage_vm_ip" {
description = "IP address where storage services are reachable" description = "Static address of the shared NFS/iSCSI storage VM"
value = var.docker_host_ip value = var.storage_vm_ip
}
output "storage_iscsi_iqn" {
description = "iSCSI target IQN"
value = var.storage_iscsi_iqn
}
output "storage_nfs_export" {
description = "NFS export path"
value = "${var.docker_host_ip}:/srv/nfs/shared"
} }
+97 -72
View File
@@ -4,11 +4,12 @@
# Single source of truth for the provision → test → cleanup lifecycle. # Single source of truth for the provision → test → cleanup lifecycle.
# Called by both the GitHub Actions workflow and the local dev container. # Called by both the GitHub Actions workflow and the local dev container.
# #
# Provisions two PVE nodes per version (a/b) for cluster testing, plus # Provisions two PVE nodes per version (a/b) for cluster testing, plus a
# Docker containers on the runner host for iSCSI/NFS shared storage. # small storage VM on the same isolated VLAN serving NFS, iSCSI, and the
# auto-install answer files.
# #
# Usage: # Usage:
# run-integration.sh provision [8|9|all] Provision nested PVE VMs + start storage containers # run-integration.sh provision [8|9|all] Provision storage VM + nested PVE VMs
# run-integration.sh test [8|9|all] [filter] Run integration tests (default: all, no filter) # run-integration.sh test [8|9|all] [filter] Run integration tests (default: all, no filter)
# run-integration.sh cleanup [8|9|all] Destroy provisioned VMs # run-integration.sh cleanup [8|9|all] Destroy provisioned VMs
# run-integration.sh all [8|9|all] Full lifecycle: provision → test → cleanup # run-integration.sh all [8|9|all] Full lifecycle: provision → test → cleanup
@@ -42,6 +43,10 @@
# MODULE_ARTIFACT Path to built module DLLs (default: ./publish/netstandard2.0) # MODULE_ARTIFACT Path to built module DLLs (default: ./publish/netstandard2.0)
# PVE_VERSIONS Space-separated versions to provision (default: "9 8") # PVE_VERSIONS Space-separated versions to provision (default: "9 8")
# STORAGE_ISCSI_IQN iSCSI IQN for storage target (default: iqn.2024-01.local.test:storage) # STORAGE_ISCSI_IQN iSCSI IQN for storage target (default: iqn.2024-01.local.test:storage)
# STORAGE_VM_IP Static CIDR address for the storage VM on the CI VLAN,
# outside the DHCP range (default: 172.16.60.60/24)
# STORAGE_VM_GATEWAY Gateway + DNS for the storage VM (default: 172.16.60.1)
# STORAGE_VMID VMID for the storage VM (default: 5080)
set -euo pipefail set -euo pipefail
@@ -50,7 +55,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INFRA_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" INFRA_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$INFRA_DIR/../.." && pwd)" REPO_ROOT="$(cd "$INFRA_DIR/../.." && pwd)"
# ── Defaults ──────────────────────────────────────────────────────── # ── Defaults ──────────────────────────────────────────────────────
CACHE_DIR="${CACHE_DIR:-/opt/pve-integration}" CACHE_DIR="${CACHE_DIR:-/opt/pve-integration}"
# Always use a path under CACHE_DIR (shared mount) so files are visible # Always use a path under CACHE_DIR (shared mount) so files are visible
# to sibling Docker containers. Do NOT use RUNNER_TEMP — it's container-local # to sibling Docker containers. Do NOT use RUNNER_TEMP — it's container-local
@@ -61,11 +66,18 @@ MODULE_ARTIFACT="${MODULE_ARTIFACT:-$REPO_ROOT/publish/netstandard2.0}"
PVE_VERSIONS="${PVE_VERSIONS:-9 8}" PVE_VERSIONS="${PVE_VERSIONS:-9 8}"
SKIP_PROVISION="${SKIP_PROVISION:-false}" SKIP_PROVISION="${SKIP_PROVISION:-false}"
STORAGE_ISCSI_IQN="${STORAGE_ISCSI_IQN:-iqn.2024-01.local.test:storage}" STORAGE_ISCSI_IQN="${STORAGE_ISCSI_IQN:-iqn.2024-01.local.test:storage}"
STORAGE_COMPOSE="$INFRA_DIR/docker-compose.storage.yml" STORAGE_VM_IP="${STORAGE_VM_IP:-172.16.60.60/24}"
STORAGE_VM_GATEWAY="${STORAGE_VM_GATEWAY:-172.16.60.1}"
STORAGE_VMID="${STORAGE_VMID:-5080}"
STORAGE_VM_HOST="${STORAGE_VM_IP%%/*}"
# Keypair for SSH to the storage VM (cloud images refuse password SSH)
STORAGE_VM_SSH_KEY="${STORAGE_VM_SSH_KEY:-$WORK_DIR/storage-vm-key}"
# Must match CLOUD_IMAGE_FILENAME in ensure-cloud-images.sh
CLOUD_IMAGE_NAME="noble-server-cloudimg-amd64.qcow2"
# Store Terraform state on the shared mount so it persists across CI jobs # Store Terraform state on the shared mount so it persists across CI jobs
TF_STATE_FILE="$WORK_DIR/terraform.tfstate" TF_STATE_FILE="$WORK_DIR/terraform.tfstate"
# ── Node config ─────────────────────────────────────────────────── # ── Node config ─────────────────────────────────────────────────
# Each version gets two nodes: a (primary) and b (secondary). # Each version gets two nodes: a (primary) and b (secondary).
# ISOs are per-version; nodes within a version share the same base ISO. # ISOs are per-version; nodes within a version share the same base ISO.
@@ -78,6 +90,14 @@ pve_iso() {
esac esac
} }
# Generic auto-install ISO name; embeds the answer-server host so a cached
# ISO baked with an older answer URL is never reused.
pve_auto_iso() {
local base
base="$(pve_iso "$1")"
echo "${base%.iso}-auto-${STORAGE_VM_HOST//./-}.iso"
}
pve_vmid() { pve_vmid() {
case "$1" in case "$1" in
9a) echo "${PVE9A_VMID:-5091}" ;; 9b) echo "${PVE9B_VMID:-5092}" ;; 9a) echo "${PVE9A_VMID:-5091}" ;; 9b) echo "${PVE9B_VMID:-5092}" ;;
@@ -122,7 +142,7 @@ expand_nodes() {
ALL_NODES="$(expand_nodes)" ALL_NODES="$(expand_nodes)"
# ── CI helpers ────────────────────────────────────────────────────── # ── CI helpers ────────────────────────────────────────────────────
ci_mask() { [[ "${GITHUB_ACTIONS:-}" == "true" ]] && echo "::add-mask::$1" || true; } ci_mask() { [[ "${GITHUB_ACTIONS:-}" == "true" ]] && echo "::add-mask::$1" || true; }
ci_error() { [[ "${GITHUB_ACTIONS:-}" == "true" ]] && echo "::error::$1" || echo "ERROR: $1" >&2; } ci_error() { [[ "${GITHUB_ACTIONS:-}" == "true" ]] && echo "::error::$1" || echo "ERROR: $1" >&2; }
@@ -157,7 +177,8 @@ pve_free_nodes() {
# the parent hypervisor. # the parent hypervisor.
resolve_target_node() { resolve_target_node() {
local count="$1" local count="$1"
local need=$(( count * ${PVE_VM_MEM_GB:-8} + ${PVE_MEM_HEADROOM_GB:-8} )) # +2 GiB for the storage VM provisioned alongside the nested nodes
local need=$(( count * ${PVE_VM_MEM_GB:-8} + 2 + ${PVE_MEM_HEADROOM_GB:-8} ))
local nodes local nodes
nodes="$(pve_free_nodes || true)" nodes="$(pve_free_nodes || true)"
@@ -186,7 +207,7 @@ resolve_target_node() {
export PVE_TARGET_NODE export PVE_TARGET_NODE
} }
# ── Subcommands ───────────────────────────────────────────────────── # ── Subcommands ───────────────────────────────────────────────────
cmd_provision() { cmd_provision() {
local requested="${1:-all}" local requested="${1:-all}"
@@ -203,7 +224,7 @@ cmd_provision() {
log "Starting provisioning..." log "Starting provisioning..."
log " Versions: $provision_versions" log " Versions: $provision_versions"
log " Nodes:$provision_nodes" log " Nodes:$provision_nodes"
log " Storage: Docker containers (iSCSI + NFS)" log " Storage: dedicated VM at $STORAGE_VM_HOST (NFS + iSCSI + answer server)"
require_env PVE_ENDPOINT require_env PVE_ENDPOINT
require_env PVE_API_TOKEN require_env PVE_API_TOKEN
require_env PVE_PASSWORD require_env PVE_PASSWORD
@@ -225,19 +246,6 @@ cmd_provision() {
CLOUD_IMAGE_PATH=$(echo "$cloud_output" | grep "^CLOUD_IMAGE_PATH=" | cut -d= -f2) CLOUD_IMAGE_PATH=$(echo "$cloud_output" | grep "^CLOUD_IMAGE_PATH=" | cut -d= -f2)
OVA_PATH=$(echo "$cloud_output" | grep "^OVA_PATH=" | cut -d= -f2) OVA_PATH=$(echo "$cloud_output" | grep "^OVA_PATH=" | cut -d= -f2)
# Discover Docker host IP early — needed for the HTTP auto-install ISO URL
# and for the docker_host_ip Terraform variable.
local storage_ip
storage_ip=$(docker run --rm --net=host alpine ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}')
if [ -z "$storage_ip" ]; then
storage_ip=$(docker info --format '{{.Swarm.NodeAddr}}' 2>/dev/null | cut -d: -f1)
fi
if [ -z "$storage_ip" ]; then
ci_error "Could not determine Docker host IP for storage services"
exit 1
fi
log "Docker host IP: $storage_ip"
# Generate per-MAC answer files for the HTTP answer server. # Generate per-MAC answer files for the HTTP answer server.
# Each node gets a file named by its MAC address so the server can # Each node gets a file named by its MAC address so the server can
# route the correct answer to each VM during auto-install. # route the correct answer to each VM during auto-install.
@@ -267,12 +275,12 @@ cmd_provision() {
for v in $provision_versions; do for v in $provision_versions; do
local base_iso_name generic_iso local base_iso_name generic_iso
base_iso_name="$(pve_iso "$v")" base_iso_name="$(pve_iso "$v")"
generic_iso="$WORK_DIR/${base_iso_name%.iso}-http-auto.iso" generic_iso="$WORK_DIR/$(pve_auto_iso "$v")"
if [ ! -f "$generic_iso" ]; then if [ ! -f "$generic_iso" ]; then
log "Preparing HTTP auto-install ISO for PVE $v..." log "Preparing HTTP auto-install ISO for PVE $v..."
proxmox-auto-install-assistant prepare-iso \ proxmox-auto-install-assistant prepare-iso \
--fetch-from http \ --fetch-from http \
--url "http://${storage_ip}:8000/answer" \ --url "http://${STORAGE_VM_HOST}:8000/answer" \
--on-first-boot "$SCRIPT_DIR/first-boot.sh" \ --on-first-boot "$SCRIPT_DIR/first-boot.sh" \
--tmp "$WORK_DIR" \ --tmp "$WORK_DIR" \
--output "$generic_iso" \ --output "$generic_iso" \
@@ -297,9 +305,7 @@ cmd_provision() {
# Build pve_isos map: version -> ISO path (one per version) # Build pve_isos map: version -> ISO path (one per version)
local isos='{}' local isos='{}'
for v in $PVE_VERSIONS; do for v in $PVE_VERSIONS; do
local iso_name local iso_path="$WORK_DIR/$(pve_auto_iso "$v")"
iso_name="$(pve_iso "$v")"
local iso_path="$WORK_DIR/${iso_name%.iso}-http-auto.iso"
isos="$(jq --arg key "$v" --arg path "$iso_path" \ isos="$(jq --arg key "$v" --arg path "$iso_path" \
'. + {($key): $path}' <<<"$isos")" '. + {($key): $path}' <<<"$isos")"
done done
@@ -325,6 +331,39 @@ cmd_provision() {
jq -n --argjson pve_instances "$instances" --argjson pve_isos "$isos" \ jq -n --argjson pve_instances "$instances" --argjson pve_isos "$isos" \
'{pve_instances: $pve_instances, pve_isos: $pve_isos}' > "$tfvars" '{pve_instances: $pve_instances, pve_isos: $pve_isos}' > "$tfvars"
# The nested PVE installers fetch their answer files from the storage VM,
# so it must be provisioned and configured before they boot: phase one
# creates and configures the storage VM, phase two everything else.
if [[ ! -f "$STORAGE_VM_SSH_KEY" ]]; then
ssh-keygen -t ed25519 -f "$STORAGE_VM_SSH_KEY" -N '' -C 'pvetest-storage' >/dev/null
fi
tf_apply() {
# TMPDIR: use work dir to avoid filling the container's /tmp with
# multi-GB ISO uploads.
(cd "$INFRA_DIR" && \
TMPDIR="$WORK_DIR" \
TF_VAR_proxmox_endpoint="$PVE_ENDPOINT" \
TF_VAR_proxmox_api_token="$PVE_API_TOKEN" \
TF_VAR_target_node="$PVE_TARGET_NODE" \
TF_VAR_test_vm_password="$PVE_PASSWORD" \
TF_VAR_cloud_image_path="$CLOUD_IMAGE_PATH" \
TF_VAR_storage_vm_ip="$STORAGE_VM_IP" \
TF_VAR_storage_vm_gateway="$STORAGE_VM_GATEWAY" \
TF_VAR_storage_vmid="$STORAGE_VMID" \
TF_VAR_storage_vm_ssh_public_key="$(cat "${STORAGE_VM_SSH_KEY}.pub")" \
terraform apply -auto-approve -input=false -state="$TF_STATE_FILE" -var-file="$tfvars" "$@")
}
log "Running Terraform apply (storage VM)..."
tf_apply \
-target=proxmox_virtual_environment_file.storage_cloud_image \
-target=proxmox_virtual_environment_vm.storage
log "Configuring storage VM at $STORAGE_VM_HOST..."
bash "$SCRIPT_DIR/setup-storage-server.sh" \
"$STORAGE_VM_HOST" "$STORAGE_VM_SSH_KEY" "$STORAGE_ISCSI_IQN" "$WORK_DIR/answer-server"
log "Running Terraform apply (PVE nodes)..." log "Running Terraform apply (PVE nodes)..."
local tf_targets="" local tf_targets=""
if [[ "$requested" != "all" ]]; then if [[ "$requested" != "all" ]]; then
@@ -334,26 +373,10 @@ cmd_provision() {
for node in $provision_nodes; do for node in $provision_nodes; do
tf_targets="$tf_targets -target=proxmox_virtual_environment_vm.nested_pve[\"$node\"]" tf_targets="$tf_targets -target=proxmox_virtual_environment_vm.nested_pve[\"$node\"]"
done done
# Always include shared Docker storage and answer server resources
tf_targets="$tf_targets -target=docker_image.ubuntu"
tf_targets="$tf_targets -target=docker_container.iscsi_target"
tf_targets="$tf_targets -target=docker_container.nfs_server"
tf_targets="$tf_targets -target=docker_container.answer_server"
tf_targets="$tf_targets -target=docker_volume.iscsi_data"
tf_targets="$tf_targets -target=docker_volume.nfs_data"
log "Terraform targets: $tf_targets" log "Terraform targets: $tf_targets"
fi fi
# TMPDIR: use work dir to avoid filling the container's /tmp with multi-GB ISO uploads. tf_apply $tf_targets
(cd "$INFRA_DIR" && \
TMPDIR="$WORK_DIR" \
TF_VAR_proxmox_endpoint="$PVE_ENDPOINT" \
TF_VAR_proxmox_api_token="$PVE_API_TOKEN" \
TF_VAR_target_node="$PVE_TARGET_NODE" \
TF_VAR_test_vm_password="$PVE_PASSWORD" \
TF_VAR_docker_host_ip="$storage_ip" \
TF_VAR_answer_server_dir="$WORK_DIR/answer-server" \
terraform apply -auto-approve -input=false -state="$TF_STATE_FILE" -var-file="$tfvars" $tf_targets)
# Wait for PVE instances to boot and discover IPs # Wait for PVE instances to boot and discover IPs
for node in $provision_nodes; do for node in $provision_nodes; do
@@ -401,7 +424,7 @@ cmd_provision() {
config=$(jq \ config=$(jq \
--arg cloud_image "${CLOUD_IMAGE_PATH:-}" \ --arg cloud_image "${CLOUD_IMAGE_PATH:-}" \
--arg ova "${OVA_PATH:-}" \ --arg ova "${OVA_PATH:-}" \
--arg storage_ip "$storage_ip" \ --arg storage_ip "$STORAGE_VM_HOST" \
--arg storage_iqn "$STORAGE_ISCSI_IQN" \ --arg storage_iqn "$STORAGE_ISCSI_IQN" \
'. + { '. + {
storage: {ip: $storage_ip, iscsi_iqn: $storage_iqn, nfs_export: ($storage_ip + ":/srv/nfs/shared")}, storage: {ip: $storage_ip, iscsi_iqn: $storage_iqn, nfs_export: ($storage_ip + ":/srv/nfs/shared")},
@@ -480,7 +503,7 @@ cmd_test() {
export PVETEST_OVA_PATH=$(jq -r '.ova_path' "$CONFIG_FILE") export PVETEST_OVA_PATH=$(jq -r '.ova_path' "$CONFIG_FILE")
# Secondary node (b) # Secondary node (b)
export PVETEST_HOST_B=$(jq -r ".pve${v}.nodes.b.host" "$CONFIG_FILE") export PVETEST_HOST_B=$(jq -r ".pve${v}.nodes.b.host" "$CONFIG_FILE")
# Storage services (Docker on runner) # Storage services (dedicated VM on the CI VLAN)
export PVETEST_STORAGE_VM_IP=$(jq -r '.storage.ip' "$CONFIG_FILE") export PVETEST_STORAGE_VM_IP=$(jq -r '.storage.ip' "$CONFIG_FILE")
export PVETEST_ISCSI_IQN=$(jq -r '.storage.iscsi_iqn' "$CONFIG_FILE") export PVETEST_ISCSI_IQN=$(jq -r '.storage.iscsi_iqn' "$CONFIG_FILE")
export PVETEST_NFS_EXPORT=$(jq -r '.storage.nfs_export' "$CONFIG_FILE") export PVETEST_NFS_EXPORT=$(jq -r '.storage.nfs_export' "$CONFIG_FILE")
@@ -552,11 +575,19 @@ cmd_test() {
} }
\$config.Filter.Tag = 'Integration' \$config.Filter.Tag = 'Integration'
\$config.Run.PassThru = \$true
\$config.Output.Verbosity = 'Detailed' \$config.Output.Verbosity = 'Detailed'
\$config.TestResult.Enabled = \$true \$config.TestResult.Enabled = \$true
\$config.TestResult.OutputFormat = 'NUnitXml' \$config.TestResult.OutputFormat = 'NUnitXml'
\$config.TestResult.OutputPath = \"TestResults/integration-results-pve\${PveVersion}.xml\" \$config.TestResult.OutputPath = \"TestResults/integration-results-pve\${PveVersion}.xml\"
Invoke-Pester -Configuration \$config \$result = Invoke-Pester -Configuration \$config
if (-not \$result -or \$result.TotalCount -eq 0) {
Write-Error 'No integration tests were discovered or executed'
exit 1
}
if (\$result.FailedCount -gt 0) {
exit 1
}
" || test_exit=$? " || test_exit=$?
if [[ $test_exit -ne 0 ]]; then if [[ $test_exit -ne 0 ]]; then
@@ -581,13 +612,6 @@ cmd_cleanup() {
fi fi
require_env PVE_TARGET_NODE require_env PVE_TARGET_NODE
# Discover Docker host IP for the docker_host_ip variable
local storage_ip
storage_ip=$(docker run --rm --net=host alpine ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}')
if [ -z "$storage_ip" ]; then
storage_ip=$(docker info --format '{{.Swarm.NodeAddr}}' 2>/dev/null | cut -d: -f1)
fi
# Build tfvars for all versions (Terraform needs the full variable map) # Build tfvars for all versions (Terraform needs the full variable map)
local tfvars="$WORK_DIR/instances.tfvars.json" local tfvars="$WORK_DIR/instances.tfvars.json"
if [[ ! -f "$tfvars" ]]; then if [[ ! -f "$tfvars" ]]; then
@@ -616,10 +640,6 @@ cmd_cleanup() {
(cd "$INFRA_DIR" && terraform init -input=false 2>/dev/null) (cd "$INFRA_DIR" && terraform init -input=false 2>/dev/null)
# Ensure answer server dir exists (terraform destroy validates host_path mounts)
mkdir -p "$WORK_DIR/answer-server/answers"
touch "$WORK_DIR/answer-server/default.toml"
# Build -target flags when destroying a subset # Build -target flags when destroying a subset
local tf_targets="" local tf_targets=""
if [[ "$requested" != "all" ]]; then if [[ "$requested" != "all" ]]; then
@@ -641,8 +661,9 @@ cmd_cleanup() {
TF_VAR_proxmox_api_token="$PVE_API_TOKEN" \ TF_VAR_proxmox_api_token="$PVE_API_TOKEN" \
TF_VAR_target_node="$PVE_TARGET_NODE" \ TF_VAR_target_node="$PVE_TARGET_NODE" \
TF_VAR_test_vm_password="${PVE_PASSWORD:-placeholder}" \ TF_VAR_test_vm_password="${PVE_PASSWORD:-placeholder}" \
TF_VAR_docker_host_ip="${storage_ip:-127.0.0.1}" \ TF_VAR_storage_vm_ip="$STORAGE_VM_IP" \
TF_VAR_answer_server_dir="${WORK_DIR}/answer-server" \ TF_VAR_storage_vm_gateway="$STORAGE_VM_GATEWAY" \
TF_VAR_storage_vmid="$STORAGE_VMID" \
terraform destroy -auto-approve -input=false -state="$TF_STATE_FILE" -var-file="$tfvars" $tf_targets) terraform destroy -auto-approve -input=false -state="$TF_STATE_FILE" -var-file="$tfvars" $tf_targets)
# Clean up work directory when destroying all # Clean up work directory when destroying all
@@ -665,6 +686,8 @@ cmd_force_cleanup() {
log "Force cleanup — bypassing Terraform, using direct API calls..." log "Force cleanup — bypassing Terraform, using direct API calls..."
# preflight-cleanup.sh reads PVE_TARGET_NODE from the environment; in auto
# mode resolve it from the node provision picked.
if [[ -z "${PVE_TARGET_NODE:-}" || "$PVE_TARGET_NODE" == "auto" ]]; then if [[ -z "${PVE_TARGET_NODE:-}" || "$PVE_TARGET_NODE" == "auto" ]]; then
PVE_TARGET_NODE="$(cat "$TARGET_NODE_FILE" 2>/dev/null || true)" PVE_TARGET_NODE="$(cat "$TARGET_NODE_FILE" 2>/dev/null || true)"
[[ -n "$PVE_TARGET_NODE" ]] && export PVE_TARGET_NODE [[ -n "$PVE_TARGET_NODE" ]] && export PVE_TARGET_NODE
@@ -674,14 +697,13 @@ cmd_force_cleanup() {
# Track which versions we've already cleaned up ISOs for (generic ISOs are shared) # Track which versions we've already cleaned up ISOs for (generic ISOs are shared)
local cleaned_iso_versions="" local cleaned_iso_versions=""
for node in $cleanup_nodes; do for node in $cleanup_nodes; do
local vm_id v iso_name iso_file local vm_id v iso_file
vm_id="$(pve_vmid "$node")" vm_id="$(pve_vmid "$node")"
v="${node%[ab]}" v="${node%[ab]}"
iso_name="$(pve_iso "$v")"
# Only clean up the generic ISO once per version # Only clean up the generic ISO once per version
iso_file="" iso_file=""
if [[ ! " $cleaned_iso_versions " =~ " $v " ]]; then if [[ ! " $cleaned_iso_versions " =~ " $v " ]]; then
iso_file="${iso_name%.iso}-http-auto.iso" iso_file="$(pve_auto_iso "$v")"
cleaned_iso_versions="$cleaned_iso_versions $v" cleaned_iso_versions="$cleaned_iso_versions $v"
fi fi
log "Force cleaning $node (VMID $vm_id)..." log "Force cleaning $node (VMID $vm_id)..."
@@ -691,11 +713,13 @@ cmd_force_cleanup() {
|| true || true
done done
# Always stop Docker containers in force mode — leaving them causes # Unconditional: the state wipe below is unconditional too, and a storage
# Terraform to fail on next provision (container already exists). # VM surviving its state entry cannot be reclaimed by the next provision.
log "Stopping storage and answer server containers..." log "Force cleaning storage VM (VMID $STORAGE_VMID)..."
docker rm -f pvetest-iscsi pvetest-nfs pvetest-answer-server 2>/dev/null || true bash "$SCRIPT_DIR/preflight-cleanup.sh" \
docker volume rm pvetest-iscsi-data pvetest-nfs-data 2>/dev/null || true "${PVE_ENDPOINT:-}" "${PVE_API_TOKEN:-}" \
"$STORAGE_VMID" "$CLOUD_IMAGE_NAME" "$INFRA_DIR" \
|| true
# Remove Terraform state (both local and shared mount) so next provision starts clean. # Remove Terraform state (both local and shared mount) so next provision starts clean.
# Keep .terraform.lock.hcl (provider version lock) for reproducibility. # Keep .terraform.lock.hcl (provider version lock) for reproducibility.
@@ -704,8 +728,9 @@ cmd_force_cleanup() {
rm -f "$TF_STATE_FILE" "${TF_STATE_FILE}.backup" rm -f "$TF_STATE_FILE" "${TF_STATE_FILE}.backup"
rm -rf "$INFRA_DIR/.terraform" rm -rf "$INFRA_DIR/.terraform"
# Remove work artifacts # Remove work artifacts, including locally cached auto-install ISOs
rm -f "$CONFIG_FILE" "$WORK_DIR"/instances.tfvars.json "$TARGET_NODE_FILE" rm -f "$CONFIG_FILE" "$WORK_DIR"/instances.tfvars.json "$TARGET_NODE_FILE"
rm -f "$WORK_DIR"/*-auto-*.iso "$WORK_DIR"/*-http-auto.iso
log "Force cleanup complete. Next provision will start from scratch." log "Force cleanup complete. Next provision will start from scratch."
} }
@@ -759,7 +784,7 @@ cmd_all() {
return $test_exit return $test_exit
} }
# ── Main ─────────────────────────────────────────────────────────── # ── Main ───────────────────────────────────────────────────────────
main() { main() {
local cmd="${1:-}" local cmd="${1:-}"
shift || true shift || true
@@ -775,7 +800,7 @@ main() {
echo "Usage: $(basename "$0") {provision|test|cleanup|force-cleanup|taint|all} [8|9|all] [test-filter]" echo "Usage: $(basename "$0") {provision|test|cleanup|force-cleanup|taint|all} [8|9|all] [test-filter]"
echo "" echo ""
echo "Subcommands:" echo "Subcommands:"
echo " provision [8|9|all] Provision nested PVE VMs + storage containers" echo " provision [8|9|all] Provision storage VM + nested PVE VMs"
echo " test [8|9|all] [filter] Run integration tests (default: all versions, no filter)" echo " test [8|9|all] [filter] Run integration tests (default: all versions, no filter)"
echo " cleanup [8|9|all] Destroy resources via terraform destroy (default: all)" echo " cleanup [8|9|all] Destroy resources via terraform destroy (default: all)"
echo " force-cleanup [8|9|all] Bypass Terraform — destroy via API + wipe state (recovery)" echo " force-cleanup [8|9|all] Bypass Terraform — destroy via API + wipe state (recovery)"
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Configures the shared services VM inside the CI sandbox VLAN: NFS export,
# iSCSI target, and the HTTP answer server the PVE auto-installers fetch
# from. Idempotent — safe to re-run against an already-configured VM.
#
# Usage: setup-storage-server.sh <storage-vm-ip> <ssh-private-key-path> <iscsi-iqn> <answer-dir>
# <answer-dir> is the local directory holding default.toml and answers/.
set -euo pipefail
STORAGE_IP="${1:?Usage: setup-storage-server.sh <ip> <ssh-key> <iqn> <answer-dir>}"
SSH_KEY="${2:?missing SSH private key path}"
ISCSI_IQN="${3:?missing iSCSI IQN}"
ANSWER_DIR="${4:?missing answer directory}"
# Same pinned image the Terraform-managed runner container used previously.
ANSWER_SERVER_IMAGE="slothcroissant/proxmox-auto-installer-server@sha256:0f45d7bfe6e3cc76aa00fc578e40b80b9054e377db18a79122866fe5522bc7ed"
SSH_OPTS="-i ${SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
echo "=== Configuring storage services on ${STORAGE_IP} ==="
# Cloud-init may still be running on first boot; wait for SSH to accept the
# key login it installs. The last attempt's stderr is kept so an auth failure
# is distinguishable from an unreachable host.
for i in $(seq 1 60); do
if ssh ${SSH_OPTS} -o ConnectTimeout=5 "ubuntu@${STORAGE_IP}" true 2>/tmp/storage-ssh-err; then
break
fi
if [[ $i -eq 60 ]]; then
echo "ERROR: storage VM at ${STORAGE_IP} not reachable over SSH" >&2
cat /tmp/storage-ssh-err >&2
exit 1
fi
sleep 5
done
# sshd accepts logins before cloud-init's first-boot work (and its apt/dpkg
# locks) is finished; a degraded cloud-init result is fine for our purposes,
# and a hung cloud-init must fail fast, not eat the job timeout.
timeout 300 ssh ${SSH_OPTS} "ubuntu@${STORAGE_IP}" \
"sudo cloud-init status --wait" || true
echo "Copying answer files..."
ssh ${SSH_OPTS} "ubuntu@${STORAGE_IP}" \
"sudo mkdir -p /opt/answer-server && sudo rm -rf /opt/answer-server/answers && sudo chown ubuntu /opt/answer-server"
scp ${SSH_OPTS} -r \
"${ANSWER_DIR}/default.toml" "${ANSWER_DIR}/answers" \
"ubuntu@${STORAGE_IP}:/opt/answer-server/"
ssh ${SSH_OPTS} "ubuntu@${STORAGE_IP}" \
"sudo ISCSI_IQN='${ISCSI_IQN}' ANSWER_SERVER_IMAGE='${ANSWER_SERVER_IMAGE}' bash -s" <<'REMOTE'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
# cloud-init's first-boot apt activity can hold the dpkg lock briefly
for i in $(seq 1 30); do
apt-get update -qq >/dev/null 2>&1 && break
sleep 5
done
apt-get install -y -qq nfs-kernel-server tgt docker.io >/dev/null
# Ubuntu's nfs-kernel-server package does not create /etc/exports.d
mkdir -p /etc/exports.d /srv/nfs/shared /srv/iscsi
echo '/srv/nfs/shared *(rw,sync,no_subtree_check,no_root_squash)' > /etc/exports.d/pvetest.exports
exportfs -ra
if [ ! -f /srv/iscsi/lun0.img ]; then
truncate -s 10G /srv/iscsi/lun0.img
fi
cat > /etc/tgt/conf.d/pvetest.conf <<CONF
<target ${ISCSI_IQN}>
backing-store /srv/iscsi/lun0.img
</target>
CONF
systemctl enable --now nfs-kernel-server tgt >/dev/null || {
journalctl -u nfs-kernel-server -u tgt --no-pager -n 30 >&2
exit 1
}
systemctl restart tgt
docker rm -f pvetest-answer-server >/dev/null 2>&1 || true
docker run -d --name pvetest-answer-server --restart unless-stopped \
--network host \
-v /opt/answer-server/answers:/app/answers \
-v /opt/answer-server/default.toml:/app/default.toml \
"${ANSWER_SERVER_IMAGE}" >/dev/null
# Ordering requirement: the PVE installers fetch answers as soon as this
# script returns, so :8000 must accept connections before exit (any HTTP
# status counts as listening).
for i in $(seq 1 20); do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 http://127.0.0.1:8000/ || true)
[ "$code" != "000" ] && break
if [ "$i" -eq 20 ]; then
echo "ERROR: answer server did not start listening on :8000" >&2
docker logs pvetest-answer-server 2>&1 | tail -20 >&2
exit 1
fi
sleep 3
done
echo "Services ready: NFS /srv/nfs/shared, iSCSI ${ISCSI_IQN}, answer server :8000"
REMOTE
echo "Storage server configuration complete."
+80
View File
@@ -0,0 +1,80 @@
# ── Shared storage VM ───────────────────────────────────────────────
# One small Ubuntu guest in the CI pool serves NFS + iSCSI to every nested
# PVE node. It lives on the same isolated VLAN as the nodes under test, so
# no firewall path out of the CI sandbox is needed. Package install and
# service configuration happen over SSH (setup-storage-server.sh) after boot.
resource "proxmox_virtual_environment_file" "storage_cloud_image" {
content_type = "import"
datastore_id = var.iso_storage
node_name = var.target_node
overwrite = true
source_file {
path = var.cloud_image_path
}
}
resource "proxmox_virtual_environment_vm" "storage" {
name = "pvetest-storage"
node_name = var.target_node
vm_id = var.storage_vmid
pool_id = var.pool_id
cpu {
type = "host"
cores = 2
sockets = 1
}
memory {
dedicated = 2048
}
disk {
datastore_id = var.disk_storage
interface = "scsi0"
size = 32
import_from = proxmox_virtual_environment_file.storage_cloud_image.id
}
initialization {
datastore_id = var.disk_storage
ip_config {
ipv4 {
address = var.storage_vm_ip
gateway = var.storage_vm_gateway
}
}
dns {
servers = [var.storage_vm_gateway]
}
user_account {
username = "ubuntu"
# Password is console-only: Ubuntu cloud images refuse SSH password auth
# and PVE's cloud-init never sets ssh_pwauth. SSH uses the key.
password = var.test_vm_password
keys = [trimspace(var.storage_vm_ssh_public_key)]
}
}
network_device {
bridge = var.network_bridge
model = "virtio"
}
operating_system {
type = "l26"
}
# Resized Ubuntu cloud images kernel-panic on boot without a serial console
# (bpg/terraform-provider-proxmox documented issue).
serial_device {
device = "socket"
}
started = true
}
-112
View File
@@ -1,112 +0,0 @@
# ── Answer server container ──────────────────────────────────────────
resource "docker_container" "answer_server" {
name = "pvetest-answer-server"
# Pin to digest for reproducibility. Update by pulling latest and running:
# docker inspect slothcroissant/proxmox-auto-installer-server:latest --format '{{index .RepoDigests 0}}'
image = "slothcroissant/proxmox-auto-installer-server@sha256:0f45d7bfe6e3cc76aa00fc578e40b80b9054e377db18a79122866fe5522bc7ed"
restart = "unless-stopped"
must_run = true
start = true
network_mode = "host"
volumes {
host_path = "${var.answer_server_dir}/answers"
container_path = "/app/answers"
}
volumes {
host_path = "${var.answer_server_dir}/default.toml"
container_path = "/app/default.toml"
}
}
# ── Docker images & volumes ──────────────────────────────────────────
resource "docker_image" "ubuntu" {
# Pin to digest for reproducibility. Update:
# docker pull ubuntu:24.04 && docker inspect ubuntu:24.04 --format '{{index .RepoDigests 0}}'
name = "ubuntu@sha256:186072bba1b2f436cbb91ef2567abca677337cfc786c86e107d25b7072feef0c"
}
resource "docker_volume" "iscsi_data" {
name = "pvetest-iscsi-data"
}
resource "docker_volume" "nfs_data" {
name = "pvetest-nfs-data"
}
# ── iSCSI target container ──────────────────────────────────────────
resource "docker_container" "iscsi_target" {
name = "pvetest-iscsi"
image = docker_image.ubuntu.image_id
privileged = true
restart = "unless-stopped"
network_mode = "host"
volumes {
volume_name = docker_volume.iscsi_data.name
container_path = "/srv/iscsi"
}
env = [
"ISCSI_IQN=${var.storage_iscsi_iqn}",
"ISCSI_LUN_SIZE=${var.storage_iscsi_lun_size}",
]
entrypoint = ["/bin/bash", "-c"]
command = [<<-EOT
set -e
apt-get update -qq && apt-get install -y -qq tgt >/dev/null 2>&1
mkdir -p /srv/iscsi
if [ ! -f /srv/iscsi/lun0.img ]; then
truncate -s $${ISCSI_LUN_SIZE} /srv/iscsi/lun0.img
fi
tgtd --foreground &
sleep 2
if ! tgtadm --lld iscsi --op show --mode target | grep -q "Target 1: $${ISCSI_IQN}"; then
tgtadm --lld iscsi --op new --mode target --tid 1 -T $${ISCSI_IQN}
fi
if ! tgtadm --lld iscsi --op show --mode logicalunit --tid 1 2>/dev/null | grep -qE "LUN:[[:space:]]*1($$|[^0-9])"; then
tgtadm --lld iscsi --op new --mode logicalunit --tid 1 --lun 1 --backing-store /srv/iscsi/lun0.img
fi
if ! tgtadm --lld iscsi --op show --mode target --tid 1 2>/dev/null | grep -q "Initiator-address: ALL"; then
tgtadm --lld iscsi --op bind --mode target --tid 1 -I ALL
fi
echo "iSCSI target ready: $${ISCSI_IQN} (port 3260)"
wait
EOT
]
}
# ── NFS server container ────────────────────────────────────────────
resource "docker_container" "nfs_server" {
name = "pvetest-nfs"
# Pin to digest for reproducibility. Update:
# docker pull erichough/nfs-server:2.2.1 && docker inspect erichough/nfs-server:2.2.1 --format '{{index .RepoDigests 0}}'
image = "erichough/nfs-server@sha256:1efd4ece380c5ba27479417585224ef857006daa46ab84560a28c1224bc71e9e"
privileged = true
restart = "unless-stopped"
network_mode = "host"
volumes {
volume_name = docker_volume.nfs_data.name
container_path = "/srv/nfs/shared"
}
volumes {
host_path = "/lib/modules"
container_path = "/lib/modules"
read_only = true
}
env = [
"NFS_EXPORT_0=/srv/nfs/shared *(rw,sync,no_subtree_check,no_root_squash)",
]
}
+24 -12
View File
@@ -61,7 +61,7 @@ variable "disk_storage" {
} }
variable "iso_storage" { variable "iso_storage" {
description = "Proxmox storage pool for uploading the ISO (must accept ISO content type)" description = "Proxmox storage pool for uploads (must accept the iso AND import content types — import is not enabled by default on most storages)"
type = string type = string
default = "local" default = "local"
} }
@@ -78,28 +78,40 @@ variable "test_vm_password" {
sensitive = true sensitive = true
} }
variable "storage_iscsi_iqn" { variable "storage_vmid" {
description = "iSCSI target IQN for the test storage" description = "VMID for the shared storage VM (must be inside the CI pool's reserved range)"
type = string type = number
default = "iqn.2024-01.local.test:storage" default = 5080
} }
variable "storage_iscsi_lun_size" { variable "storage_vm_ip" {
description = "Size of the iSCSI LUN backing file" description = "Static IPv4 address in CIDR form for the storage VM, outside the VLAN's DHCP range"
type = string type = string
default = "10G" default = "172.16.60.60/24"
validation {
condition = can(cidrnetmask(var.storage_vm_ip))
error_message = "storage_vm_ip must be CIDR notation, e.g. 172.16.60.60/24."
}
} }
variable "docker_host_ip" { variable "storage_vm_ssh_public_key" {
description = "IP of the Docker host, used by PVE nodes to reach storage containers" description = "SSH public key granted to the storage VM's ubuntu user (cloud images refuse password SSH; required for provision, unused on destroy)"
type = string type = string
default = ""
} }
variable "answer_server_dir" { variable "storage_vm_gateway" {
description = "Host path to the answer server root directory (contains default.toml and answers/ subdirectory)" description = "Gateway (and DNS server) for the storage VM"
type = string type = string
default = "172.16.60.1"
} }
variable "cloud_image_path" {
description = "Local path to the Ubuntu cloud image imported as the storage VM's disk (required for provision; unused on destroy)"
type = string
default = ""
}
variable "pool_id" { variable "pool_id" {
description = "Resource pool the nested VMs are created in (a pool-scoped API token can only allocate here)" description = "Resource pool the nested VMs are created in (a pool-scoped API token can only allocate here)"