commit 3cbd4525a076d3f8aff8ecf79ade61b84b12afdf Author: Stephen Simpson Date: Tue Nov 25 15:35:04 2025 -0600 Initial commit: Sparrowdo Testing Orchestrator framework - Add VM provisioning, cleanup, and setup scripts - Add Jenkins pipeline with parameterized builds - Add comprehensive documentation - Support for parallel test execution with QCOW2 linked clones diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..393d33c --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +# QCOW2 Images +*.qcow2 +*.qcow2.bak + +# Log files +*.log +*.txt +logs/ + +# Temporary prep scripts +prep-*.sh + +# Workspace directories +ws-*/ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Editor files +*.swp +*.swo +*~ +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Raku/Perl6 +.precomp/ + +# Temporary files +tmp/ +temp/ +*.tmp +*.bak +*.cache + +# Jenkins workspace artifacts +jenkins-workspace/ + +# SSH keys (safety) +id_rsa +id_rsa.pub +*.pem +*.key diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..fae6acc --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,374 @@ +pipeline { + agent { + label 'fedora-testing' + customWorkspace "/home/jenkins/jenkins-workspace/${BUILD_NUMBER}" + } + + parameters { + // QCOW2 Image Source + string( + name: 'QCOW2_URL', + defaultValue: 'https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2', + description: 'URL to the base QCOW2 image to use for testing' + ) + + // Test Matrix - JSON array format + text( + name: 'TEST_MATRIX', + defaultValue: '''[ + { + "name": "example-test", + "url": "https://github.com/your-org/sparrowdo-tests.git", + "branch": "main", + "description": "Example Sparrowdo test suite" + } +]''', + description: 'JSON array of test definitions (name, url, branch, description)' + ) + + // Custom Golden Image Preparation + text( + name: 'GOLDEN_PREP_SCRIPT', + defaultValue: '''#!/bin/bash +set -e + +echo "Preparing standard golden image..." + +# Update system +dnf update -y + +# Install common testing dependencies +dnf install -y \\ + perl \\ + git \\ + wget \\ + tar \\ + openssh-server \\ + firewalld \\ + chrony \\ + vim + +# Configure services +systemctl enable sshd +systemctl enable firewalld +systemctl enable chronyd + +# Configure firewall +firewall-cmd --permanent --add-service=ssh +firewall-cmd --reload + +# Set consistent hostname +hostnamectl set-hostname test-node + +# Create testing user +useradd -m testuser 2>/dev/null || true +echo "testuser:testpass" | chpasswd +echo "testuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + +echo "Golden image preparation complete!" +''', + description: 'Shell script to run inside the golden image' + ) + + // Test Selection + string( + name: 'TEST_FILTER', + defaultValue: '.*', + description: 'Regex pattern to filter tests (e.g., "login.*" or ".*")' + ) + + // Concurrency Control + choice( + name: 'MAX_CONCURRENT', + choices: ['1', '2', '3', '5', '10'], + description: 'Maximum concurrent VMs' + ) + + // Cleanup Control + booleanParam( + name: 'KEEP_GOLDEN_IMAGE', + defaultValue: false, + description: 'Keep golden image after testing' + ) + + // SSH Configuration + string( + name: 'SSH_PRIVATE_KEY_PATH', + defaultValue: '${HOME}/.ssh/id_rsa', + description: 'Path to SSH private key for VM access' + ) + + string( + name: 'SSH_PUBLIC_KEY_PATH', + defaultValue: '${HOME}/.ssh/id_rsa.pub', + description: 'Path to SSH public key to inject into VMs' + ) + } + + environment { + BUILD_ID = "${BUILD_NUMBER}" + IMAGES_DIR = "/var/lib/libvirt/images" + MAX_PARALLEL = "${params.MAX_CONCURRENT}" + } + + options { + disableConcurrentBuilds() + timeout(time: 2, unit: 'HOURS') + buildDiscarder(logRotator(numToKeepStr: '10')) + } + + stages { + stage('Initialize') { + steps { + script { + echo "==========================================" + echo "Build ${BUILD_ID}" + echo "Node: ${env.NODE_NAME}" + echo "Max Concurrent: ${params.MAX_CONCURRENT}" + echo "Test Filter: ${params.TEST_FILTER}" + echo "==========================================" + + sh ''' + sudo mkdir -p ${IMAGES_DIR} + sudo chown ${USER}:${USER} ${IMAGES_DIR} + chmod +x scripts/*.sh + echo "Environment initialized" + ''' + } + } + } + + stage('Download Base Image') { + steps { + script { + dir(IMAGES_DIR) { + sh ''' + if [ ! -f "base-${BUILD_ID}.qcow2" ]; then + echo "Downloading QCOW2 image from: ${QCOW2_URL}" + curl -L --progress-bar -o "base-${BUILD_ID}.qcow2" "${QCOW2_URL}" + echo "" + echo "Image downloaded successfully:" + qemu-img info "base-${BUILD_ID}.qcow2" | head -5 + else + echo "Using cached base image" + fi + ''' + env.BASE_IMAGE_PATH = "${IMAGES_DIR}/base-${BUILD_ID}.qcow2" + } + } + } + } + + stage('Prepare Golden Image') { + steps { + script { + def goldenImage = "${IMAGES_DIR}/golden-${BUILD_ID}.qcow2" + def prepScript = "prep-${BUILD_ID}.sh" + + // Expand SSH key paths + def sshPubKey = params.SSH_PUBLIC_KEY_PATH.replace('${HOME}', env.HOME) + + // Write prep script + writeFile file: prepScript, text: params.GOLDEN_PREP_SCRIPT + sh "chmod +x ${prepScript}" + + env.PREP_SCRIPT_PATH = pwd() + "/" + prepScript + env.GOLDEN_IMAGE_PATH = goldenImage + env.SSH_PUB_KEY_PATH = sshPubKey + + echo "Creating golden image: ${goldenImage}" + echo "Using SSH public key: ${sshPubKey}" + + sh ''' + ./scripts/setup_base.sh \\ + ${BASE_IMAGE_PATH} \\ + ${PREP_SCRIPT_PATH} \\ + ${GOLDEN_IMAGE_PATH} \\ + ${SSH_PUB_KEY_PATH} + ''' + + sh "rm -f ${prepScript}" + } + } + } + + stage('Parse Test Matrix') { + steps { + script { + def testMatrix = readJSON text: params.TEST_MATRIX.trim() + def filtered = testMatrix.findAll { it.name =~ params.TEST_FILTER } + + if (filtered.isEmpty()) { + error "No tests match filter: ${params.TEST_FILTER}" + } + + echo "==========================================" + echo "Tests to run (${filtered.size()}):" + filtered.each { echo " - ${it.name}: ${it.description}" } + echo "==========================================" + + env.FILTERED_TESTS = groovy.json.JsonOutput.toJson(filtered) + } + } + } + + stage('Run Tests') { + steps { + script { + def tests = readJSON text: env.FILTERED_TESTS + def tasks = [:] + + // Expand SSH key path + def sshPrivateKey = params.SSH_PRIVATE_KEY_PATH.replace('${HOME}', env.HOME) + + tests.each { test -> + def testName = test.name + def testUrl = test.url + def testBranch = test.branch ?: 'main' + + tasks["${testName}"] = { + lock(resource: 'kvm-slots', quantity: 1) { + stage("Test: ${testName}") { + def vmId = "${testName}-${BUILD_ID}" + def targetIp = "" + + try { + dir("ws-${testName}") { + echo "[${testName}] Provisioning VM..." + def ipOutput = sh( + script: "${WORKSPACE}/scripts/provision_vm.sh ${vmId} ${GOLDEN_IMAGE_PATH} 60", + returnStdout: true + ).trim() + + if (ipOutput.contains("ERROR")) { + error "Failed to provision VM" + } + + // Extract IP from output (last line) + targetIp = ipOutput.split('\n').last() + echo "[${testName}] VM ready at: ${targetIp}" + + // Wait for SSH to be ready + echo "[${testName}] Waiting for SSH..." + sh """ + for i in {1..30}; do + if ssh -i ${sshPrivateKey} -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@${targetIp} 'echo SSH ready' 2>/dev/null; then + echo "SSH connection established" + break + fi + echo "Attempt \$i/30..." + sleep 2 + done + """ + + // Clone test repository + echo "[${testName}] Cloning test repository..." + sh "git clone -b ${testBranch} ${testUrl} test-repo || true" + + // Verify sparrowfile exists + def sparrowfilePath = sh( + script: 'find test-repo -name sparrowfile -type f | head -1', + returnStdout: true + ).trim() + + if (!sparrowfilePath) { + error "No sparrowfile found in test repository" + } + + echo "[${testName}] Found sparrowfile: ${sparrowfilePath}" + + // Run test + echo "[${testName}] Running Sparrowdo..." + sh """ + mkdir -p logs + timeout 900 sparrowdo \\ + --host=${targetIp} \\ + --ssh_user=root \\ + --ssh_private_key=${sshPrivateKey} \\ + --ssh_args='-o StrictHostKeyChecking=no -o ConnectTimeout=10' \\ + --no_sudo \\ + --sparrowfile=${sparrowfilePath} 2>&1 | tee logs/test.log + """ + + echo "[${testName}] Test completed successfully" + archiveArtifacts artifacts: "logs/**", allowEmptyArchive: true + } + + } catch (e) { + echo "[${testName}] FAILED: ${e.message}" + + // Archive any logs that were generated + dir("ws-${testName}") { + archiveArtifacts artifacts: "logs/**", allowEmptyArchive: true + } + + currentBuild.result = 'UNSTABLE' + + } finally { + echo "[${testName}] Cleaning up VM..." + sh "${WORKSPACE}/scripts/cleanup_vm.sh ${vmId} || true" + } + } + } + } + } + + // Run all tests in parallel (respecting lock quantity) + parallel tasks + } + } + } + } + + post { + always { + script { + echo "==========================================" + echo "Post-build cleanup" + echo "==========================================" + + if (!params.KEEP_GOLDEN_IMAGE) { + sh ''' + echo "Removing temporary images..." + sudo rm -f ${IMAGES_DIR}/base-${BUILD_ID}.qcow2 || true + sudo rm -f ${IMAGES_DIR}/golden-${BUILD_ID}.qcow2 || true + echo "Temporary images cleaned up" + ''' + } else { + echo "Golden image preserved: ${GOLDEN_IMAGE_PATH}" + echo "Base image preserved: ${BASE_IMAGE_PATH}" + } + + // Clean up any orphaned VMs + sh ''' + echo "Checking for orphaned VMs from build ${BUILD_ID}..." + for vm in $(virsh list --all --name 2>/dev/null | grep -E "-${BUILD_ID}"); do + echo "Cleaning orphaned VM: $vm" + sudo virsh destroy "$vm" 2>/dev/null || true + sudo virsh undefine "$vm" 2>/dev/null || true + sudo rm -f "/var/lib/libvirt/images/${vm}.qcow2" 2>/dev/null || true + done + echo "Orphan cleanup complete" + ''' + } + } + + success { + echo "==========================================" + echo "Build ${BUILD_ID} completed successfully!" + echo "==========================================" + } + + failure { + echo "==========================================" + echo "Build ${BUILD_ID} failed!" + echo "==========================================" + } + + unstable { + echo "==========================================" + echo "Build ${BUILD_ID} completed with test failures" + echo "==========================================" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..b19e64f --- /dev/null +++ b/README.md @@ -0,0 +1,389 @@ +# Sparrowdo Testing Orchestrator + +Automated testing framework for Rocky Linux using Sparrowdo, QCOW2 images, and Jenkins. + +## Overview + +This framework provides a production-ready continuous integration/testing system that: + +- **Automates VM Provisioning**: Spins up isolated Rocky Linux VMs on-demand using QCOW2 images +- **Runs Sparrowdo Tests in Parallel**: Executes your test suite across multiple isolated environments simultaneously +- **Supports Dynamic Configuration**: Runtime customization of base images, preparation scripts, test selection, and concurrency +- **Provides Web Interface**: Jenkins UI for triggering builds, viewing results, and downloading logs +- **Scales Across Machines**: Multiple team members can connect their desktops as Jenkins agents + +## How It Works + +``` +User clicks "Build" in Jenkins + ↓ +Download QCOW2 base image + ↓ +Run custom prep script (install packages, configure services) + ↓ +Create golden image + ↓ +Create linked clones for each test (fast - copy-on-write) + ↓ +Run Sparrowdo tests in parallel (isolated VMs) + ↓ +Collect logs and archive results + ↓ +Auto-cleanup VMs and temporary images +``` + +## Directory Structure + +``` +. +├── Jenkinsfile # Jenkins Pipeline definition +├── README.md # This file +├── scripts/ +│ ├── setup_base.sh # Prepares golden image from QCOW2 +│ ├── provision_vm.sh # Creates and starts test VM +│ └── cleanup_vm.sh # Destroys VM and removes disk +├── tests/ +│ └── (sample tests here) +└── docs/ + └── (additional documentation) +``` + +## Prerequisites + +### On Jenkins Agent (Fedora/Rocky Linux Desktop) + +1. **KVM/QEMU/Libvirt** + ```bash + sudo dnf install -y qemu-kvm libvirt virt-install libguestfs-tools-c + sudo systemctl enable --now libvirtd + sudo usermod -a -G libvirt $(whoami) + ``` + +2. **Sparrowdo** + ```bash + # Install Raku (Perl 6) + sudo dnf install -y rakudo + + # Install zef (Raku module manager) + git clone https://github.com/ugexe/zef.git + cd zef && raku -I. bin/zef install . + + # Install Sparrowdo + zef install Sparrowdo + ``` + +3. **SSH Keys** + ```bash + # Generate SSH key pair if needed + ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N "" + ``` + +4. **Jenkins Agent** + - Connect your Fedora desktop as a Jenkins agent + - Label it as `fedora-testing` + - Ensure the agent user has sudo access for virsh/libvirt commands + +## Quick Start + +### 1. Clone Repository + +```bash +cd ~/ +git clone testing-orchestrator +cd testing-orchestrator +``` + +### 2. Test Scripts Locally (Optional) + +```bash +# Make scripts executable +chmod +x scripts/*.sh + +# Download a test QCOW2 image +cd /var/lib/libvirt/images +curl -LO https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 + +# Create golden image +cd ~/testing-orchestrator +./scripts/setup_base.sh \ + /var/lib/libvirt/images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 \ + "" \ + /var/lib/libvirt/images/golden-test.qcow2 \ + ~/.ssh/id_rsa.pub + +# Provision a test VM +./scripts/provision_vm.sh test-vm-1 /var/lib/libvirt/images/golden-test.qcow2 + +# Clean up +./scripts/cleanup_vm.sh test-vm-1 +``` + +### 3. Configure Jenkins Job + +1. In Jenkins, create a new **Pipeline** job +2. Name it "rocky-sparrowdo-tests" +3. Under "Pipeline", select "Pipeline script from SCM" +4. Set SCM to "Git" and provide your repository URL +5. Set Script Path to "Jenkinsfile" +6. Save + +### 4. Run Your First Build + +1. Click "Build with Parameters" +2. Configure parameters: + - **QCOW2_URL**: Rocky Linux base image URL + - **TEST_MATRIX**: JSON array of your Sparrowdo test repositories + - **GOLDEN_PREP_SCRIPT**: Customize image preparation + - **TEST_FILTER**: Regex to filter which tests run + - **MAX_CONCURRENT**: Number of parallel VMs +3. Click "Build" + +## Configuration + +### TEST_MATRIX Format + +The `TEST_MATRIX` parameter accepts a JSON array of test definitions: + +```json +[ + { + "name": "login-tests", + "url": "https://github.com/your-org/login-tests.git", + "branch": "main", + "description": "SSH login and connectivity tests" + }, + { + "name": "database-tests", + "url": "https://github.com/your-org/database-tests.git", + "branch": "develop", + "description": "PostgreSQL configuration tests" + } +] +``` + +Each test repository should contain a `sparrowfile` at the root or in a subdirectory. + +### Custom Golden Image Preparation + +The `GOLDEN_PREP_SCRIPT` parameter accepts a bash script that runs inside the QCOW2 image during preparation: + +```bash +#!/bin/bash +set -e + +# Update system +dnf update -y + +# Install dependencies +dnf install -y perl git wget postgresql-server + +# Configure services +systemctl enable postgresql +firewall-cmd --permanent --add-service=postgresql + +echo "Custom preparation complete!" +``` + +### Test Filtering + +Use the `TEST_FILTER` parameter to run specific tests: + +- `.*` - Run all tests +- `login.*` - Run all tests starting with "login" +- `database-postgres` - Run only the specific test +- `(api|integration).*` - Run API or integration tests + +## Features + +### Linked Clones for Speed + +The framework uses QCOW2 linked clones (copy-on-write), which means: +- Creating a new VM takes seconds, not minutes +- Each test VM only stores differences from the golden image +- Disk space usage is minimal + +### Parallel Execution with Isolation + +- Each test runs in its own isolated VM +- Tests cannot interfere with each other +- Concurrency is controlled via `MAX_CONCURRENT` parameter +- Jenkins lock mechanism prevents desktop overload + +### Automatic Cleanup + +- VMs are automatically destroyed after each test +- Temporary images are removed after build +- Orphaned VMs from failed builds are cleaned up +- Optional: Keep golden image for debugging + +### Test Result Archival + +- Each test's output is captured in `logs/test.log` +- Logs are archived as Jenkins artifacts +- Download logs directly from Jenkins UI + +## Troubleshooting + +### VM Won't Start + +```bash +# Check libvirt status +sudo systemctl status libvirtd + +# Verify image integrity +qemu-img info /var/lib/libvirt/images/golden-*.qcow2 + +# Check disk space +df -h /var/lib/libvirt/images/ +``` + +### VM Doesn't Get IP Address + +```bash +# Check default network +sudo virsh net-list --all +sudo virsh net-start default # If it's not running + +# Verify DHCP +sudo virsh net-dhcp-leases default +``` + +### SSH Connection Fails + +```bash +# Test SSH manually (get IP from Jenkins logs) +ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no root@ + +# Check SSH key injection +sudo virt-cat -a /var/lib/libvirt/images/golden-*.qcow2 /root/.ssh/authorized_keys +``` + +### Sparrowdo Test Fails + +```bash +# View test logs in Jenkins artifacts +# Or run Sparrowdo manually against VM: +sparrowdo \ + --host= \ + --ssh_user=root \ + --ssh_private_key=~/.ssh/id_rsa \ + --sparrowfile=/path/to/sparrowfile +``` + +### Permission Denied Errors + +```bash +# Ensure user is in libvirt group +sudo usermod -a -G libvirt $(whoami) +newgrp libvirt + +# Fix image directory permissions +sudo chown -R $(whoami):$(whoami) /var/lib/libvirt/images +``` + +### Jenkins Agent Offline + +```bash +# Check agent service (if running as systemd service) +sudo systemctl status jenkins-agent + +# View logs +sudo journalctl -u jenkins-agent -f + +# Test connection from Jenkins controller +ssh jenkins@ 'echo "Connection successful"' +``` + +## Advanced Usage + +### Testing Beta Images + +Change the `QCOW2_URL` parameter to point to beta images: + +``` +https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base-9.4-beta.x86_64.qcow2 +``` + +### Custom VM Resources + +Edit `scripts/provision_vm.sh` to adjust memory/CPU: + +```bash +sudo virt-install \ + --name "$VM_NAME" \ + --memory 4096 \ # Increase memory + --vcpus 4 \ # Increase CPUs + ... +``` + +### Preserving Golden Images for Debugging + +1. Set `KEEP_GOLDEN_IMAGE` to `true` +2. After build, the golden image is preserved +3. Boot it manually for inspection: + +```bash +VM_NAME="debug-vm" +sudo virt-install \ + --name "$VM_NAME" \ + --memory 2048 \ + --vcpus 2 \ + --disk /var/lib/libvirt/images/golden-.qcow2 \ + --import \ + --os-variant rocky9-unknown \ + --network network=default + +# Connect via console +sudo virsh console $VM_NAME +``` + +### Running Tests Without Jenkins + +```bash +# Manual test execution +./scripts/setup_base.sh \ + /path/to/base.qcow2 \ + /path/to/prep-script.sh \ + /path/to/golden.qcow2 \ + ~/.ssh/id_rsa.pub + +IP=$(./scripts/provision_vm.sh my-test-vm /path/to/golden.qcow2) + +sparrowdo \ + --host=$IP \ + --ssh_user=root \ + --ssh_private_key=~/.ssh/id_rsa \ + --sparrowfile=/path/to/test/sparrowfile + +./scripts/cleanup_vm.sh my-test-vm +``` + +## Contributing + +### Adding New Tests + +1. Create a new Sparrowdo test repository +2. Add a `sparrowfile` at the root or in a subdirectory +3. Add the test to the `TEST_MATRIX` parameter in Jenkins + +### Modifying Scripts + +1. Test changes locally first +2. Update documentation if behavior changes +3. Commit and push to repository +4. Jenkins will use updated scripts on next build + +## Support + +For issues or questions: +- Check logs in Jenkins artifacts +- Review troubleshooting section above +- Verify prerequisites are installed correctly + +## License + +[Specify your license here] + +## Authors + +Rocky Linux Testing Team diff --git a/scripts/cleanup_vm.sh b/scripts/cleanup_vm.sh new file mode 100755 index 0000000..0bc828b --- /dev/null +++ b/scripts/cleanup_vm.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +VM_NAME="$1" + +if [ -z "$VM_NAME" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "[Cleanup] Starting cleanup for VM: $VM_NAME" + +# Force stop VM +echo "[Cleanup] Destroying VM..." +sudo virsh destroy "$VM_NAME" 2>/dev/null || echo "[Cleanup] VM was not running" + +# Remove VM definition +echo "[Cleanup] Undefining VM..." +sudo virsh undefine "$VM_NAME" 2>/dev/null || echo "[Cleanup] VM definition already removed" + +# Remove disk image +echo "[Cleanup] Removing disk image..." +sudo rm -f "/var/lib/libvirt/images/${VM_NAME}.qcow2" || echo "[Cleanup] Disk already removed" + +echo "[Cleanup] Complete for $VM_NAME" diff --git a/scripts/provision_vm.sh b/scripts/provision_vm.sh new file mode 100755 index 0000000..ead4c3a --- /dev/null +++ b/scripts/provision_vm.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -e + +VM_NAME="$1" +GOLDEN_IMAGE="$2" +MAX_WAIT="${3:-30}" + +if [ -z "$VM_NAME" ] || [ -z "$GOLDEN_IMAGE" ]; then + echo "Usage: $0 [max_wait_seconds]" + exit 1 +fi + +echo "[Provision] Creating VM: $VM_NAME" + +# Create linked clone (very fast - just a pointer to base) +VM_DISK="/var/lib/libvirt/images/${VM_NAME}.qcow2" + +echo "[Provision] Creating overlay disk: $VM_DISK" +sudo qemu-img create -f qcow2 -b "$GOLDEN_IMAGE" -F qcow2 "$VM_DISK" 2>/dev/null + +# Define and start VM +echo "[Provision] Starting VM with virt-install..." +sudo virt-install \ + --name "$VM_NAME" \ + --memory 2048 \ + --vcpus 2 \ + --disk path="$VM_DISK",format=qcow2 \ + --import \ + --os-variant rocky9-unknown \ + --network network=default \ + --noautoconsole \ + --wait 0 \ + --transient \ + 2>&1 | grep -v "WARNING" || true + +# Wait for IP address +echo "[Provision] Waiting for VM to obtain IP address (max ${MAX_WAIT}s)..." +COUNTER=0 +while [ $COUNTER -lt $MAX_WAIT ]; do + # Try to get IP from DHCP lease + IP=$(sudo virsh domifaddr "$VM_NAME" --source lease 2>/dev/null | awk '/ipv4/ {print $4}' | cut -d/ -f1 | head -1) + + if [ -n "$IP" ] && [ "$IP" != "0.0.0.0" ]; then + echo "[Provision] IP obtained: $IP" + echo "$IP" + exit 0 + fi + + sleep 2 + ((COUNTER++)) + + # Show progress every 5 iterations + if [ $((COUNTER % 5)) -eq 0 ]; then + echo "[Provision] Still waiting... (${COUNTER}/${MAX_WAIT}s)" + fi +done + +echo "[Provision] ERROR: Could not obtain IP for $VM_NAME after $MAX_WAIT seconds" +echo "[Provision] Destroying failed VM..." +sudo virsh destroy "$VM_NAME" 2>/dev/null || true +sudo virsh undefine "$VM_NAME" 2>/dev/null || true +sudo rm -f "$VM_DISK" +echo "ERROR" +exit 1 diff --git a/scripts/setup_base.sh b/scripts/setup_base.sh new file mode 100755 index 0000000..c9027e4 --- /dev/null +++ b/scripts/setup_base.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e + +# Signature: ./setup_base.sh +QCOW2_PATH="$1" +PREP_SCRIPT_PATH="$2" +GOLDEN_IMAGE="$3" +SSH_PUB_KEY="$4" + +if [ -z "$QCOW2_PATH" ] || [ -z "$GOLDEN_IMAGE" ] || [ -z "$SSH_PUB_KEY" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "==========================================" +echo "Setting up golden image" +echo "==========================================" +echo "Source QCOW2: $QCOW2_PATH" +echo "Output Golden: $GOLDEN_IMAGE" +echo "Prep Script: $PREP_SCRIPT_PATH" +echo "SSH Key: $SSH_PUB_KEY" + +# Ensure libvirt is running +sudo systemctl is-active --quiet libvirtd || sudo systemctl start libvirtd +sleep 2 + +# Copy original to golden +echo "[Step 1/3] Copying base image to golden image..." +cp "$QCOW2_PATH" "$GOLDEN_IMAGE" + +# Apply custom preparation script if provided +if [ -f "$PREP_SCRIPT_PATH" ]; then + echo "[Step 2/3] Applying custom preparation script..." + export LIBGUESTFS_BACKEND=direct + + # Run the prep script inside the image + sudo virt-customize -a "$GOLDEN_IMAGE" \ + --run "$PREP_SCRIPT_PATH" \ + --ssh-inject root:file:"$SSH_PUB_KEY" \ + --root-password password:rockytesting \ + --selinux-relabel 2>&1 || { + echo "ERROR: virt-customize failed" + exit 1 + } +else + echo "[Step 2/3] No custom prep script provided, applying defaults..." + export LIBGUESTFS_BACKEND=direct + + sudo virt-customize -a "$GOLDEN_IMAGE" \ + --ssh-inject root:file:"$SSH_PUB_KEY" \ + --root-password password:rockytesting \ + --install perl,git,wget,tar,openssh-server \ + --run-command 'systemctl enable sshd' \ + --selinux-relabel 2>&1 || { + echo "ERROR: virt-customize failed" + exit 1 + } +fi + +echo "[Step 3/3] Verifying golden image..." +qemu-img info "$GOLDEN_IMAGE" | head -5 + +echo "==========================================" +echo "Golden image ready: $GOLDEN_IMAGE" +echo "=========================================="