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
This commit is contained in:
79
.gitignore
vendored
Normal file
79
.gitignore
vendored
Normal file
@@ -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
|
||||
374
Jenkinsfile
vendored
Normal file
374
Jenkinsfile
vendored
Normal file
@@ -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 "=========================================="
|
||||
}
|
||||
}
|
||||
}
|
||||
389
README.md
Normal file
389
README.md
Normal file
@@ -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 <your-repo-url> 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@<VM_IP>
|
||||
|
||||
# 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=<VM_IP> \
|
||||
--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@<agent-host> '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-<BUILD_ID>.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
|
||||
25
scripts/cleanup_vm.sh
Executable file
25
scripts/cleanup_vm.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
VM_NAME="$1"
|
||||
|
||||
if [ -z "$VM_NAME" ]; then
|
||||
echo "Usage: $0 <vm_name>"
|
||||
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"
|
||||
64
scripts/provision_vm.sh
Executable file
64
scripts/provision_vm.sh
Executable file
@@ -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 <vm_name> <golden_image_path> [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
|
||||
65
scripts/setup_base.sh
Executable file
65
scripts/setup_base.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Signature: ./setup_base.sh <qcow2_path> <prep_script_path> <output_golden_image> <ssh_pub_key>
|
||||
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 <qcow2_path> <prep_script_path> <output_golden_image> <ssh_pub_key>"
|
||||
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 "=========================================="
|
||||
Reference in New Issue
Block a user