Merge pull request #16 from torlando-tech/refactor/abstraction-layer
Refactor/abstraction layer
This commit is contained in:
commit
a8b47e465d
33 changed files with 12158 additions and 1159 deletions
106
.github/workflows/README.md
vendored
106
.github/workflows/README.md
vendored
|
|
@ -66,13 +66,18 @@ pytest tests/test_fragmentation.py tests/test_prioritization.py -v \
|
|||
--cov=src/RNS/Interfaces/BLEFragmentation.py \
|
||||
--cov-report=term-missing
|
||||
|
||||
# Integration tests
|
||||
# Integration tests (excludes v2.2 protocol tests that need full RNS)
|
||||
pytest tests/ -v -m "not hardware" \
|
||||
--ignore=tests/test_v2_2_identity_handshake.py \
|
||||
--ignore=tests/test_v2_2_mac_sorting.py \
|
||||
--ignore=tests/test_v2_2_race_conditions.py \
|
||||
--cov=src/RNS/Interfaces \
|
||||
--cov-report=term-missing \
|
||||
--tb=short
|
||||
```
|
||||
|
||||
**Note:** The v2.2 protocol test suites (`test_v2_2_*.py`) are excluded from CI because they require the full RNS module environment. These tests document expected behavior and will run when the interface is integrated into the main Reticulum repository.
|
||||
|
||||
## Why Two Jobs?
|
||||
|
||||
Separating unit and integration tests provides several benefits:
|
||||
|
|
@ -83,12 +88,109 @@ Separating unit and integration tests provides several benefits:
|
|||
4. **Separate Coverage**: Track unit test coverage separately from integration coverage
|
||||
5. **Granular Status**: See exactly which test category failed in PR checks
|
||||
|
||||
### deploy.yml - Continuous Deployment
|
||||
|
||||
This workflow automatically deploys code to Raspberry Pi devices on your local network after tests pass.
|
||||
|
||||
#### Deployment Flow
|
||||
1. **Trigger**: Push to any branch (when `src/**` changes)
|
||||
2. **Dependencies**: Waits for `unit-tests` and `integration-tests` to pass
|
||||
3. **Runner**: Executes on self-hosted runner (must be on same network as Pis)
|
||||
4. **Deployment Steps** (per Pi):
|
||||
- Navigate to repository directory
|
||||
- Fetch and checkout the pushed branch
|
||||
- Pull latest changes
|
||||
- Copy `src/RNS/Interfaces/*.py` to `~/.reticulum/interfaces/`
|
||||
- Restart `rnsd` service
|
||||
|
||||
#### Required Secrets
|
||||
|
||||
Configure these in GitHub Settings → Secrets and variables → Actions:
|
||||
|
||||
| Secret | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `PI_HOSTS` | Comma-separated list of Pi hostnames/IPs | `pi1.local,pi2.local,192.168.1.100` |
|
||||
| `PI_REPO_PATH` | Absolute path to repository on Pis | `/home/pi/ble-reticulum` |
|
||||
| `PI_USER` | SSH username for Pi access | `pi` |
|
||||
| `PI_SSH_KEY` | SSH private key for passwordless authentication | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
|
||||
|
||||
#### SSH Configuration
|
||||
|
||||
**For containerized runners (k3s, Docker, etc.):**
|
||||
|
||||
Since the runner is ephemeral, the SSH key is stored in GitHub Secrets and configured at runtime:
|
||||
|
||||
```bash
|
||||
# 1. Generate SSH key pair (on any machine)
|
||||
ssh-keygen -t ed25519 -C "github-runner-deployment" -f ~/.ssh/github_runner_deploy
|
||||
# Press Enter for no passphrase (required for automation)
|
||||
|
||||
# 2. Copy public key to each Raspberry Pi
|
||||
ssh-copy-id -i ~/.ssh/github_runner_deploy.pub pi@pi1.local
|
||||
ssh-copy-id -i ~/.ssh/github_runner_deploy.pub pi@pi2.local
|
||||
|
||||
# 3. Add private key to GitHub Secrets
|
||||
# Copy the private key content:
|
||||
cat ~/.ssh/github_runner_deploy
|
||||
# Then add to GitHub Settings → Secrets → PI_SSH_KEY
|
||||
# (Paste the entire key including -----BEGIN and -----END lines)
|
||||
|
||||
# 4. Test from any machine with the private key
|
||||
ssh -i ~/.ssh/github_runner_deploy pi@pi1.local 'echo "Connection successful"'
|
||||
```
|
||||
|
||||
**For persistent runners:**
|
||||
|
||||
If your runner has persistent storage, you can use traditional SSH key setup:
|
||||
|
||||
```bash
|
||||
# On the self-hosted runner
|
||||
ssh-keygen -t ed25519 -C "github-runner"
|
||||
ssh-copy-id pi@pi1.local
|
||||
ssh-copy-id pi@pi2.local
|
||||
|
||||
# Then set PI_SSH_KEY to the private key content
|
||||
cat ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
#### Deployment Status
|
||||
|
||||
The workflow fails if ANY Pi fails to deploy. Check job logs for:
|
||||
- Individual Pi deployment status (✓ success / ✗ failed)
|
||||
- Deployment summary with success/failure counts
|
||||
- GitHub Actions summary with commit info
|
||||
|
||||
#### Troubleshooting Deployment
|
||||
|
||||
**Deployment skipped:**
|
||||
- Check that tests passed (deployment depends on test jobs)
|
||||
- Verify changes were in `src/**` directory
|
||||
|
||||
**SSH connection failed:**
|
||||
- Verify Pi is reachable: `ping pi1.local`
|
||||
- Check SSH keys are configured correctly
|
||||
- Ensure `PI_HOSTS` secret matches actual hostnames
|
||||
|
||||
**Git operations failed:**
|
||||
- Verify `PI_REPO_PATH` is correct
|
||||
- Ensure repository exists on Pis
|
||||
- Check branch exists on remote
|
||||
|
||||
**rnsd restart failed:**
|
||||
- Check if systemd service exists: `systemctl status rnsd`
|
||||
- Verify user has sudo permissions (for systemd)
|
||||
- Check if rnsd binary is in PATH
|
||||
|
||||
## Workflow Triggers
|
||||
|
||||
Both workflows trigger on:
|
||||
### test.yml
|
||||
- **Push** to any branch
|
||||
- **Pull request** to any branch
|
||||
|
||||
### deploy.yml
|
||||
- **Push** to any branch (only if `src/**` or workflow file changes)
|
||||
- Automatically runs after tests pass
|
||||
|
||||
## Dependencies
|
||||
|
||||
The workflows install:
|
||||
|
|
|
|||
333
.github/workflows/deploy.yml
vendored
Normal file
333
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
name: Deploy to Raspberry Pi
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# ============================================================================
|
||||
# JOB 1: Parse PI_HOSTS into matrix for parallel deployment
|
||||
# ============================================================================
|
||||
setup:
|
||||
name: Setup Deployment Matrix
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if tests passed (for workflow_run) or if manually triggered
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
branch: ${{ steps.get-branch.outputs.branch }}
|
||||
|
||||
steps:
|
||||
- name: Validate required secrets
|
||||
run: |
|
||||
if [ -z "${{ secrets.PI_HOSTS }}" ]; then
|
||||
echo "Error: PI_HOSTS secret is not set"
|
||||
echo "Please set PI_HOSTS secret with comma-separated hostnames (e.g., 'pi1.local,pi2.local')"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${{ secrets.PI_REPO_PATH }}" ]; then
|
||||
echo "Error: PI_REPO_PATH secret is not set"
|
||||
echo "Please set PI_REPO_PATH secret with repository path (e.g., '/home/pi/ble-reticulum')"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${{ secrets.PI_USER }}" ]; then
|
||||
echo "Error: PI_USER secret is not set"
|
||||
echo "Please set PI_USER secret with SSH username (e.g., 'pi')"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${{ secrets.PI_SSH_KEY }}" ]; then
|
||||
echo "Error: PI_SSH_KEY secret is not set"
|
||||
echo "Please set PI_SSH_KEY secret with SSH private key for Pi access"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ All required secrets are configured"
|
||||
|
||||
- name: Get branch name
|
||||
id: get-branch
|
||||
run: |
|
||||
BRANCH="${{ github.event.workflow_run.head_branch || github.ref_name }}"
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
echo "Deployment branch: $BRANCH"
|
||||
|
||||
- name: Parse PI_HOSTS into deployment matrix
|
||||
id: set-matrix
|
||||
env:
|
||||
PI_HOSTS: ${{ secrets.PI_HOSTS }}
|
||||
run: |
|
||||
# Split comma-separated PI_HOSTS into array
|
||||
IFS=',' read -ra HOSTS <<< "$PI_HOSTS"
|
||||
|
||||
# Build JSON array for matrix
|
||||
JSON='['
|
||||
for i in "${!HOSTS[@]}"; do
|
||||
HOST=$(echo "${HOSTS[$i]}" | xargs)
|
||||
if [ $i -gt 0 ]; then JSON+=','; fi
|
||||
JSON+="{\"host\":\"$HOST\",\"index\":$i}"
|
||||
done
|
||||
JSON+=']'
|
||||
|
||||
echo "matrix=$JSON" >> $GITHUB_OUTPUT
|
||||
echo "Deployment matrix created for ${#HOSTS[@]} Pi(s)"
|
||||
echo "$JSON" | jq '.'
|
||||
|
||||
# ============================================================================
|
||||
# JOB 2: Deploy to each Pi (parallel matrix execution)
|
||||
# ============================================================================
|
||||
deploy:
|
||||
name: Deploy to Pi ${{ matrix.pi.index }} (${{ matrix.pi.host }})
|
||||
runs-on: self-hosted
|
||||
needs: setup
|
||||
strategy:
|
||||
matrix:
|
||||
pi: ${{ fromJson(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false # Continue deploying to other Pis if one fails
|
||||
|
||||
steps:
|
||||
- name: Setup SSH key
|
||||
env:
|
||||
PI_SSH_KEY: ${{ secrets.PI_SSH_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "$PI_SSH_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
|
||||
cat >> ~/.ssh/config <<EOF
|
||||
Host *.local 10.0.0.* 192.168.*
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
LogLevel ERROR
|
||||
EOF
|
||||
chmod 600 ~/.ssh/config
|
||||
|
||||
- name: Deploy to ${{ matrix.pi.host }}
|
||||
env:
|
||||
PI_HOST: ${{ matrix.pi.host }}
|
||||
PI_REPO_PATH: ${{ secrets.PI_REPO_PATH }}
|
||||
PI_USER: ${{ secrets.PI_USER }}
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
echo "==================================="
|
||||
echo "Deploying to Pi ${{ matrix.pi.index }}"
|
||||
echo "==================================="
|
||||
echo "Host: $PI_HOST"
|
||||
echo "Branch: $BRANCH_NAME"
|
||||
echo "Repository: $PI_REPO_PATH"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
|
||||
# Deployment script
|
||||
DEPLOY_SCRIPT="set -e
|
||||
echo ' [1/8] Navigating to repository...'
|
||||
cd '$PI_REPO_PATH' || exit 1
|
||||
|
||||
echo ' [2/8] Fetching latest changes...'
|
||||
git fetch --all || exit 1
|
||||
|
||||
echo ' [3/8] Checking out branch: $BRANCH_NAME...'
|
||||
git checkout '$BRANCH_NAME' || exit 1
|
||||
|
||||
echo ' [4/8] Pulling latest code...'
|
||||
git pull || exit 1
|
||||
|
||||
echo ' [5/8] Creating ~/.reticulum/interfaces directory...'
|
||||
mkdir -p ~/.reticulum/interfaces || exit 1
|
||||
|
||||
echo ' [6/8] Copying interface files...'
|
||||
cp -v src/RNS/Interfaces/*.py ~/.reticulum/interfaces/ || exit 1
|
||||
|
||||
echo ' [7/8] Stopping rnsd and clearing logs...'
|
||||
RNSD_BIN=\"\$HOME/.local/bin/rnsd\"
|
||||
if systemctl is-active --quiet rnsd 2>/dev/null; then
|
||||
sudo systemctl stop rnsd || exit 1
|
||||
echo ' ✓ rnsd stopped via systemd'
|
||||
else
|
||||
pkill -9 rnsd 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
# Clear the log file for clean validation
|
||||
echo '' > ~/.reticulum/logfile
|
||||
echo ' ✓ Log file cleared'
|
||||
|
||||
echo ' [8/8] Starting rnsd...'
|
||||
if systemctl is-active --quiet rnsd.service 2>/dev/null || systemctl is-enabled --quiet rnsd.service 2>/dev/null; then
|
||||
sudo systemctl start rnsd || exit 1
|
||||
echo ' ✓ rnsd started via systemd'
|
||||
else
|
||||
nohup \"\$RNSD_BIN\" -s > /dev/null 2>&1 &
|
||||
sleep 2
|
||||
if pgrep -x rnsd > /dev/null; then
|
||||
echo ' ✓ rnsd started successfully'
|
||||
else
|
||||
echo ' ✗ Failed to start rnsd'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ' ✓ Deployment successful!'"
|
||||
|
||||
# Execute deployment via SSH
|
||||
if echo "$DEPLOY_SCRIPT" | ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$PI_USER@$PI_HOST" bash; then
|
||||
echo ""
|
||||
echo "✓ Successfully deployed to $PI_HOST"
|
||||
else
|
||||
echo ""
|
||||
echo "✗ Failed to deploy to $PI_HOST"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Cleanup SSH key
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/id_ed25519
|
||||
|
||||
# ============================================================================
|
||||
# JOB 3: Validate BLE interface on each Pi (parallel matrix execution)
|
||||
# ============================================================================
|
||||
validate:
|
||||
name: Validate Pi ${{ matrix.pi.index }} (${{ matrix.pi.host }})
|
||||
runs-on: self-hosted
|
||||
needs: [setup, deploy]
|
||||
strategy:
|
||||
matrix:
|
||||
pi: ${{ fromJson(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Setup SSH key
|
||||
env:
|
||||
PI_SSH_KEY: ${{ secrets.PI_SSH_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "$PI_SSH_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
|
||||
- name: Validate BLE interface on ${{ matrix.pi.host }}
|
||||
env:
|
||||
PI_HOST: ${{ matrix.pi.host }}
|
||||
PI_USER: ${{ secrets.PI_USER }}
|
||||
run: |
|
||||
echo "==================================="
|
||||
echo "Validating Pi ${{ matrix.pi.index }}"
|
||||
echo "==================================="
|
||||
echo "Host: $PI_HOST"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
|
||||
# Validation script
|
||||
VALIDATION_SCRIPT='set -e
|
||||
|
||||
echo " [1/4] Waiting for startup (5s)..."
|
||||
sleep 5
|
||||
|
||||
echo " [2/4] Checking rnsd process..."
|
||||
if ! pgrep -x rnsd > /dev/null; then
|
||||
echo " ✗ rnsd process not running"
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ rnsd is running (PID: $(pgrep -x rnsd))"
|
||||
|
||||
echo " [3/4] Checking BLE interface logs..."
|
||||
LOG_FILE="$HOME/.reticulum/logfile"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo " ✗ Log file not found at $LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Retry 3 times with 3s delay
|
||||
SUCCESS=false
|
||||
for attempt in 1 2 3; do
|
||||
STARTUP_LOGS=$(head -200 "$LOG_FILE" 2>/dev/null || echo "")
|
||||
|
||||
# Check for critical errors
|
||||
if echo "$STARTUP_LOGS" | grep -qE "(failed to start driver|Timeout waiting for Transport)"; then
|
||||
echo " ✗ BLE driver/identity error detected"
|
||||
echo ""
|
||||
echo " Startup error logs:"
|
||||
head -100 "$LOG_FILE" | grep -E "(BLE|ERROR)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for success
|
||||
if echo "$STARTUP_LOGS" | grep -q "interface online"; then
|
||||
echo " ✓ BLE interface online"
|
||||
SUCCESS=true
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $attempt -lt 3 ]; then
|
||||
echo " Retry $attempt/3 (waiting 3s)..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$SUCCESS" = false ]; then
|
||||
echo " ✗ Interface did not come online after 3 attempts"
|
||||
echo ""
|
||||
echo " Startup logs:"
|
||||
head -100 "$LOG_FILE" | grep -E "(BLE|ERROR|WARNING)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " [4/4] Checking Bluetooth adapter..."
|
||||
if bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then
|
||||
ADAPTER_MAC=$(bluetoothctl show 2>/dev/null | grep "Address:" | awk "{print \$2}")
|
||||
echo " ✓ Bluetooth adapter powered ($ADAPTER_MAC)"
|
||||
else
|
||||
echo " ⚠ Bluetooth adapter status unknown"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ✓ Validation successful!"
|
||||
'
|
||||
|
||||
# Execute validation via SSH
|
||||
if echo "$VALIDATION_SCRIPT" | ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$PI_USER@$PI_HOST" bash; then
|
||||
echo ""
|
||||
echo "✓ $PI_HOST validation passed"
|
||||
else
|
||||
echo ""
|
||||
echo "✗ $PI_HOST validation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Cleanup SSH key
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/id_ed25519
|
||||
|
||||
# ============================================================================
|
||||
# JOB 4: Summary (runs after all deploy + validate jobs complete)
|
||||
# ============================================================================
|
||||
summary:
|
||||
name: Deployment Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [setup, deploy, validate]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Generate summary
|
||||
run: |
|
||||
echo "## 🎉 Deployment Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Branch:** ${{ needs.setup.outputs.branch }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ needs.deploy.result }}" == "success" ] && [ "${{ needs.validate.result }}" == "success" ]; then
|
||||
echo "### ✅ All Pis Deployed and Validated Successfully" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "### ⚠️ Some Pis Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ needs.deploy.result }}" != "success" ]; then
|
||||
echo "- **Deploy:** ${{ needs.deploy.result }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
if [ "${{ needs.validate.result }}" != "success" ]; then
|
||||
echo "- **Validate:** ${{ needs.validate.result }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Check individual job logs for details." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
|
@ -140,7 +140,11 @@ jobs:
|
|||
- name: Run integration tests
|
||||
run: |
|
||||
# Run integration tests (no hardware required)
|
||||
# Exclude v2.2 protocol tests that require full RNS environment
|
||||
python -m pytest tests/ -v -m "not hardware" \
|
||||
--ignore=tests/test_v2_2_identity_handshake.py \
|
||||
--ignore=tests/test_v2_2_mac_sorting.py \
|
||||
--ignore=tests/test_v2_2_race_conditions.py \
|
||||
--cov=src/RNS/Interfaces \
|
||||
--cov-report=term-missing \
|
||||
--cov-report=xml:coverage-integration.xml \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue