From 3d2ee6cc8564945e3e2bada548c09e8b26526f0d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 19:57:13 +0000 Subject: [PATCH 1/7] feat: add CoreOS-based immutable image for remote test labs Add containerized infrastructure for running labgrid coordinator and exporter on Fedora CoreOS with automatic updates. Components: - Container images for labgrid, pdudaemon, dnsmasq, ser2net - Quadlet systemd units for container management - Butane/Ignition configuration generator - Simple YAML config template for lab setup - GitHub Actions workflow for container builds Features: - Immutable OS with automatic updates via Zincati - Container auto-updates via Podman - VLAN-based device isolation - USB serial device passthrough - TFTP/DHCP for device provisioning --- .github/workflows/containers.yml | 82 +++ coreos/README.md | 257 +++++++++ coreos/containers/Containerfile.dnsmasq | 15 + coreos/containers/Containerfile.labgrid | 43 ++ coreos/containers/Containerfile.pdudaemon | 29 + coreos/containers/Containerfile.ser2net | 23 + coreos/containers/entrypoint-labgrid.sh | 27 + coreos/ignition/labnode-standalone.bu | 415 ++++++++++++++ coreos/ignition/labnode.bu | 251 ++++++++ coreos/lab-config.yaml.example | 75 +++ coreos/quadlet/dnsmasq.container | 31 + coreos/quadlet/labgrid-coordinator.container | 22 + coreos/quadlet/labgrid-exporter.container | 47 ++ coreos/quadlet/pdudaemon.container | 24 + coreos/quadlet/podman-auto-update.timer | 10 + coreos/scripts/build-containers.sh | 113 ++++ coreos/scripts/build-ignition.sh | 91 +++ coreos/scripts/generate-butane.py | 569 +++++++++++++++++++ coreos/scripts/generate-ignition.sh | 106 ++++ 19 files changed, 2230 insertions(+) create mode 100644 .github/workflows/containers.yml create mode 100644 coreos/README.md create mode 100644 coreos/containers/Containerfile.dnsmasq create mode 100644 coreos/containers/Containerfile.labgrid create mode 100644 coreos/containers/Containerfile.pdudaemon create mode 100644 coreos/containers/Containerfile.ser2net create mode 100644 coreos/containers/entrypoint-labgrid.sh create mode 100644 coreos/ignition/labnode-standalone.bu create mode 100644 coreos/ignition/labnode.bu create mode 100644 coreos/lab-config.yaml.example create mode 100644 coreos/quadlet/dnsmasq.container create mode 100644 coreos/quadlet/labgrid-coordinator.container create mode 100644 coreos/quadlet/labgrid-exporter.container create mode 100644 coreos/quadlet/pdudaemon.container create mode 100644 coreos/quadlet/podman-auto-update.timer create mode 100755 coreos/scripts/build-containers.sh create mode 100755 coreos/scripts/build-ignition.sh create mode 100755 coreos/scripts/generate-butane.py create mode 100755 coreos/scripts/generate-ignition.sh diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml new file mode 100644 index 000000000..eb9b3dc7b --- /dev/null +++ b/.github/workflows/containers.yml @@ -0,0 +1,82 @@ +name: Build Lab Containers + +on: + push: + branches: + - main + paths: + - 'coreos/containers/**' + - '.github/workflows/containers.yml' + pull_request: + paths: + - 'coreos/containers/**' + - '.github/workflows/containers.yml' + workflow_dispatch: + schedule: + # Rebuild weekly to pick up base image updates + - cron: '0 2 * * 0' + +env: + REGISTRY: ghcr.io + IMAGE_PREFIX: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + strategy: + fail-fast: false + matrix: + image: + - name: labgrid + file: Containerfile.labgrid + - name: pdudaemon + file: Containerfile.pdudaemon + - name: dnsmasq + file: Containerfile.dnsmasq + - name: ser2net + file: Containerfile.ser2net + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.image.name }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: coreos/containers + file: coreos/containers/${{ matrix.image.file }} + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/coreos/README.md b/coreos/README.md new file mode 100644 index 000000000..977cc4a20 --- /dev/null +++ b/coreos/README.md @@ -0,0 +1,257 @@ +# OpenWrt Test Lab - Immutable OS Image + +Self-contained, auto-updating immutable OS image for remote OpenWrt test labs based on Fedora CoreOS with containerized services. + +## Overview + +This setup provides: +- **Immutable base OS**: Fedora CoreOS with automatic updates via Zincati +- **Containerized services**: labgrid coordinator/exporter, pdudaemon, dnsmasq +- **Auto-updating containers**: Podman auto-update pulls new images automatically +- **Simple configuration**: Single YAML file generates complete system config + +## Quick Start + +```bash +# 1. Copy and edit the example config +cp lab-config.yaml.example lab-config.yaml +vim lab-config.yaml # Add your SSH keys, devices, etc. + +# 2. Generate Ignition file +./scripts/build-ignition.sh lab-config.yaml + +# 3. Install Fedora CoreOS +# Download from: https://fedoraproject.org/coreos/download +sudo coreos-installer install /dev/sdX --ignition-file labnode.ign +``` + +That's it! The system will boot with all services configured and auto-updating. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Fedora CoreOS Host │ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ labgrid-coord │ │ labgrid-exporter │ │ +│ │ (container) │◄─┤ (container) │ │ +│ │ :20408 │ │ host network │ │ +│ └──────────────────┘ └────────┬─────────┘ │ +│ │ │ +│ ┌──────────────────┐ ┌───────┴──────────┐ │ +│ │ pdudaemon │ │ dnsmasq │ │ +│ │ (container) │ │ (container) │ │ +│ │ :16421 │ │ DHCP/TFTP │ │ +│ └────────┬─────────┘ └───────┬──────────┘ │ +│ │ │ │ +│ ┌────────┴────────────────────┴──────────┐ │ +│ │ Host Network / VLANs │ │ +│ └────────────────────┬───────────────────┘ │ +└───────────────────────┼─────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ + │ Device 1│ │ Device 2│ │ Device N│ + │ (VLAN) │ │ (VLAN) │ │ (VLAN) │ + └─────────┘ └─────────┘ └─────────┘ +``` + +## Configuration + +### Lab Configuration File + +Create `lab-config.yaml` from the example: + +```yaml +# Lab identification +lab: + name: my-lab + hostname: labgrid-mylab + +# SSH access - add your public keys +ssh_keys: + - ssh-ed25519 AAAAC3Nz... admin@example.com + +# Network VLANs for device isolation +network: + interface: eth0 + vlans: + - id: 101 + address: 192.168.101.1/24 + dhcp_start: 192.168.101.100 + dhcp_end: 192.168.101.200 + +# Power distribution units +pdus: + - address: 192.168.128.2 + driver: ubus + +# Test devices +devices: + - name: openwrt-one + serial: + id_path: pci-0000:00:14.0-usb-0:1:1.0 # from: udevadm info /dev/ttyUSB0 + speed: 115200 + network: + vlan: 101 + power: + pdu: 192.168.128.2 + outlet: 1 +``` + +### Finding Serial Port ID_PATH + +```bash +# List USB serial devices +ls /dev/ttyUSB* /dev/ttyACM* + +# Get ID_PATH for a device +udevadm info /dev/ttyUSB0 | grep ID_PATH +``` + +## Directory Structure + +``` +coreos/ +├── lab-config.yaml.example # Template - copy and customize +├── containers/ # Container definitions +│ ├── Containerfile.labgrid +│ ├── Containerfile.pdudaemon +│ ├── Containerfile.dnsmasq +│ └── entrypoint-labgrid.sh +├── scripts/ +│ ├── build-ignition.sh # Main build script +│ ├── generate-butane.py # Config → Butane converter +│ └── build-containers.sh # Container image builder +├── ignition/ # Advanced: raw Butane configs +└── quadlet/ # Advanced: container unit files +``` + +## Container Images + +Pre-built images from GitHub Container Registry: + +| Image | Description | +|-------|-------------| +| `ghcr.io/openwrt/openwrt-tests/labgrid` | Coordinator and exporter | +| `ghcr.io/openwrt/openwrt-tests/pdudaemon` | Power control daemon | +| `ghcr.io/openwrt/openwrt-tests/dnsmasq` | DHCP/TFTP server | + +Images are automatically rebuilt weekly and on changes. + +### Building Locally + +```bash +./scripts/build-containers.sh # Build all +./scripts/build-containers.sh --push # Build and push +``` + +## Auto-Updates + +### OS Updates + +Fedora CoreOS updates automatically via Zincati: +- **Default schedule**: Sundays 03:00 UTC +- **Automatic rollback** on boot failure + +```bash +rpm-ostree status # Check current/pending updates +systemctl status zincati # Update service status +``` + +### Container Updates + +Containers update daily via Podman auto-update: +- **Default schedule**: Daily 04:00 UTC +- Pulls new `:latest` images automatically + +```bash +sudo podman auto-update --dry-run # Check for updates +sudo podman auto-update # Update now +``` + +## Post-Installation + +### Verify Services + +```bash +# Check containers +sudo podman ps + +# Check services +systemctl status labgrid-coordinator labgrid-exporter pdudaemon dnsmasq + +# View logs +journalctl -u labgrid-exporter -f +``` + +### Configure VLANs (if not using ignition network config) + +```bash +# Create VLAN interface +sudo nmcli con add type vlan con-name vlan101 dev eth0 id 101 \ + ipv4.addresses 192.168.101.1/24 ipv4.method manual +sudo nmcli con up vlan101 +``` + +## Troubleshooting + +### Container Logs + +```bash +sudo podman logs labgrid-coordinator +sudo podman logs labgrid-exporter +sudo podman logs pdudaemon +``` + +### Restart Services + +```bash +sudo systemctl restart labgrid-exporter +sudo systemctl daemon-reload # After config changes +``` + +### Serial Devices Not Found + +```bash +# Check devices exist +ls -la /dev/ttyUSB* /dev/ttyACM* + +# Check container has access +sudo podman exec labgrid-exporter ls /dev/ttyUSB0 +``` + +## Hardware Requirements + +- **CPU**: x86_64 or aarch64 +- **RAM**: 2GB minimum +- **Storage**: 16GB minimum +- **Network**: Gigabit Ethernet +- **USB**: Ports for serial consoles + +### Tested Platforms + +- Raspberry Pi 5 +- Intel NUC +- Generic x86_64 servers + +## Advanced: Manual Butane Configuration + +For complex setups, you can edit the Butane files directly: + +```bash +# Edit the standalone Butane file +vim ignition/labnode-standalone.bu + +# Generate Ignition +./scripts/generate-ignition.sh ignition/labnode-standalone.bu +``` + +## Contributing + +1. Edit config template: `lab-config.yaml.example` +2. Edit containers: `containers/Containerfile.*` +3. Edit generator: `scripts/generate-butane.py` +4. Container images auto-build on push to main diff --git a/coreos/containers/Containerfile.dnsmasq b/coreos/containers/Containerfile.dnsmasq new file mode 100644 index 000000000..76066a775 --- /dev/null +++ b/coreos/containers/Containerfile.dnsmasq @@ -0,0 +1,15 @@ +# dnsmasq container for DHCP and TFTP services +FROM alpine:3.19 + +LABEL org.opencontainers.image.source="https://github.com/openwrt/openwrt-tests" +LABEL org.opencontainers.image.description="dnsmasq DHCP/TFTP server for OpenWrt testing" +LABEL org.opencontainers.image.licenses="MIT" + +RUN apk add --no-cache dnsmasq + +# Create tftp directory +RUN mkdir -p /srv/tftp + +# DNS disabled by default (port=0), only DHCP and TFTP +ENTRYPOINT ["dnsmasq", "-k", "--log-facility=-"] +CMD ["-C", "/etc/dnsmasq.conf"] diff --git a/coreos/containers/Containerfile.labgrid b/coreos/containers/Containerfile.labgrid new file mode 100644 index 000000000..0d3dc7987 --- /dev/null +++ b/coreos/containers/Containerfile.labgrid @@ -0,0 +1,43 @@ +# Labgrid container for coordinator and exporter +# Supports both roles via entrypoint arguments +FROM python:3.13-slim-bookworm + +LABEL org.opencontainers.image.source="https://github.com/openwrt/openwrt-tests" +LABEL org.opencontainers.image.description="Labgrid coordinator and exporter for OpenWrt testing" +LABEL org.opencontainers.image.licenses="MIT" + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + openssh-client \ + socat \ + microcom \ + iproute2 \ + iputils-ping \ + usbutils \ + && rm -rf /var/lib/apt/lists/* + +# Install labgrid from custom fork +RUN pip install --no-cache-dir \ + git+https://github.com/aparcar/labgrid.git@aparcar/staging \ + usbsdmux + +# Create labgrid user +RUN useradd -m -s /bin/bash -G dialout,plugdev labgrid + +# Create required directories +RUN mkdir -p /etc/labgrid /var/cache/labgrid /srv/tftp \ + && chown -R labgrid:labgrid /etc/labgrid /var/cache/labgrid /srv/tftp + +# Copy entrypoint script +COPY entrypoint-labgrid.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +USER labgrid +WORKDIR /home/labgrid + +# Default to exporter mode +ENV LABGRID_MODE=exporter +ENV LABGRID_CONFIG=/etc/labgrid/exporter.yaml + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/coreos/containers/Containerfile.pdudaemon b/coreos/containers/Containerfile.pdudaemon new file mode 100644 index 000000000..076097069 --- /dev/null +++ b/coreos/containers/Containerfile.pdudaemon @@ -0,0 +1,29 @@ +# PDUDaemon container for power control +FROM python:3.13-slim-bookworm + +LABEL org.opencontainers.image.source="https://github.com/openwrt/openwrt-tests" +LABEL org.opencontainers.image.description="PDUDaemon for OpenWrt testing power control" +LABEL org.opencontainers.image.licenses="MIT" + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + telnet \ + && rm -rf /var/lib/apt/lists/* + +# Install pdudaemon from fork with additional drivers +RUN pip install --no-cache-dir \ + git+https://github.com/jonasjelonek/pdudaemon.git@main + +# Create pdudaemon user +RUN useradd -m -s /bin/bash pdudaemon + +# Create config directory +RUN mkdir -p /etc/pdudaemon /run/pdudaemon \ + && chown -R pdudaemon:pdudaemon /etc/pdudaemon /run/pdudaemon + +USER pdudaemon + +EXPOSE 16421 + +ENTRYPOINT ["pdudaemon", "--conf", "/etc/pdudaemon/pdudaemon.conf"] diff --git a/coreos/containers/Containerfile.ser2net b/coreos/containers/Containerfile.ser2net new file mode 100644 index 000000000..daba4fc43 --- /dev/null +++ b/coreos/containers/Containerfile.ser2net @@ -0,0 +1,23 @@ +# ser2net container for serial console access +FROM debian:bookworm-slim + +LABEL org.opencontainers.image.source="https://github.com/openwrt/openwrt-tests" +LABEL org.opencontainers.image.description="ser2net for serial console access" +LABEL org.opencontainers.image.licenses="MIT" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ser2net \ + && rm -rf /var/lib/apt/lists/* + +# Create ser2net user with dialout group +RUN useradd -m -s /bin/bash -G dialout ser2net + +RUN mkdir -p /etc/ser2net \ + && chown -R ser2net:ser2net /etc/ser2net + +# Default config location +ENV SER2NET_CONF=/etc/ser2net/ser2net.yaml + +# Run as root to access serial devices (group permissions handled by host) +ENTRYPOINT ["ser2net", "-n", "-c"] +CMD ["/etc/ser2net/ser2net.yaml"] diff --git a/coreos/containers/entrypoint-labgrid.sh b/coreos/containers/entrypoint-labgrid.sh new file mode 100644 index 000000000..1492666c8 --- /dev/null +++ b/coreos/containers/entrypoint-labgrid.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +case "${LABGRID_MODE}" in + coordinator) + echo "Starting labgrid-coordinator..." + exec labgrid-coordinator \ + --listen "${LABGRID_COORDINATOR_LISTEN:-::}" \ + ${LABGRID_COORDINATOR_ARGS:-} + ;; + exporter) + if [ ! -f "${LABGRID_CONFIG}" ]; then + echo "ERROR: Exporter config not found at ${LABGRID_CONFIG}" + exit 1 + fi + echo "Starting labgrid-exporter..." + exec labgrid-exporter \ + --config "${LABGRID_CONFIG}" \ + --name "${LABGRID_EXPORTER_NAME:-$(hostname)}" \ + ${LABGRID_EXPORTER_ARGS:-} + ;; + *) + echo "Unknown mode: ${LABGRID_MODE}" + echo "Use LABGRID_MODE=coordinator or LABGRID_MODE=exporter" + exit 1 + ;; +esac diff --git a/coreos/ignition/labnode-standalone.bu b/coreos/ignition/labnode-standalone.bu new file mode 100644 index 000000000..ddc2eaab4 --- /dev/null +++ b/coreos/ignition/labnode-standalone.bu @@ -0,0 +1,415 @@ +# Butane configuration for OpenWrt Test Lab Node (standalone version) +# Compile with: butane --strict labnode-standalone.bu > labnode.ign +# +# This is a self-contained configuration with all quadlet files embedded. +# For customization, modify the exporter.yaml and pdudaemon.conf sections. + +variant: fcos +version: "1.5.0" + +passwd: + users: + - name: labgrid + groups: + - wheel + - sudo + - dialout + - plugdev + ssh_authorized_keys: + # Coordinator public key + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP0ZVlD9TmfAXL53Vq7V9WKE3KPomOa1jINyflrPWAlJ coordinator + # ADD YOUR SSH KEYS BELOW: + # - ssh-ed25519 AAAA... your-key-name + shell: /bin/bash + +storage: + directories: + - path: /etc/labgrid + mode: 0755 + - path: /etc/pdudaemon + mode: 0755 + - path: /etc/dnsmasq.d + mode: 0755 + - path: /srv/tftp + mode: 0755 + user: + name: labgrid + group: + name: labgrid + - path: /var/cache/labgrid + mode: 0755 + user: + name: labgrid + group: + name: labgrid + - path: /etc/containers/systemd + mode: 0755 + + files: + # ============================================ + # QUADLET CONTAINER DEFINITIONS + # ============================================ + + # Labgrid coordinator container + - path: /etc/containers/systemd/labgrid-coordinator.container + mode: 0644 + contents: + inline: | + [Unit] + Description=Labgrid Coordinator + After=network-online.target + Wants=network-online.target + + [Container] + Image=ghcr.io/openwrt/openwrt-tests/labgrid:latest + ContainerName=labgrid-coordinator + Environment=LABGRID_MODE=coordinator + Environment=LABGRID_COORDINATOR_LISTEN=:: + PublishPort=20408:20408 + Volume=/etc/labgrid:/etc/labgrid:ro + AutoUpdate=registry + Label=io.containers.autoupdate=registry + + [Service] + Restart=always + RestartSec=30 + TimeoutStartSec=300 + + [Install] + WantedBy=multi-user.target + + # Labgrid exporter container + - path: /etc/containers/systemd/labgrid-exporter.container + mode: 0644 + contents: + inline: | + [Unit] + Description=Labgrid Exporter + After=network-online.target labgrid-coordinator.service + Wants=network-online.target + Requires=labgrid-coordinator.service + + [Container] + Image=ghcr.io/openwrt/openwrt-tests/labgrid:latest + ContainerName=labgrid-exporter + Environment=LABGRID_MODE=exporter + Environment=LABGRID_CONFIG=/etc/labgrid/exporter.yaml + Environment=LG_CROSSBAR=ws://localhost:20408/ws + Network=host + Volume=/etc/labgrid:/etc/labgrid:ro + Volume=/srv/tftp:/srv/tftp:rw + Volume=/var/cache/labgrid:/var/cache/labgrid:rw + AddDevice=/dev/ttyUSB0 + AddDevice=/dev/ttyUSB1 + AddDevice=/dev/ttyUSB2 + AddDevice=/dev/ttyUSB3 + AddDevice=/dev/ttyACM0 + AddDevice=/dev/ttyACM1 + GroupAdd=dialout + GroupAdd=plugdev + SecurityLabelDisable=true + AutoUpdate=registry + Label=io.containers.autoupdate=registry + + [Service] + Restart=always + RestartSec=30 + TimeoutStartSec=300 + + [Install] + WantedBy=multi-user.target + + # PDUDaemon container + - path: /etc/containers/systemd/pdudaemon.container + mode: 0644 + contents: + inline: | + [Unit] + Description=PDU Daemon for power control + After=network-online.target + Wants=network-online.target + + [Container] + Image=ghcr.io/openwrt/openwrt-tests/pdudaemon:latest + ContainerName=pdudaemon + Network=host + Volume=/etc/pdudaemon:/etc/pdudaemon:ro + AutoUpdate=registry + Label=io.containers.autoupdate=registry + + [Service] + Restart=always + RestartSec=30 + TimeoutStartSec=120 + + [Install] + WantedBy=multi-user.target + + # Dnsmasq DHCP/TFTP container + - path: /etc/containers/systemd/dnsmasq.container + mode: 0644 + contents: + inline: | + [Unit] + Description=Dnsmasq DHCP/TFTP server + After=network-online.target + Wants=network-online.target + + [Container] + Image=ghcr.io/openwrt/openwrt-tests/dnsmasq:latest + ContainerName=dnsmasq + Network=host + AddCapability=NET_ADMIN + AddCapability=NET_RAW + AddCapability=NET_BIND_SERVICE + Volume=/etc/dnsmasq.d:/etc/dnsmasq.d:ro + Volume=/etc/dnsmasq.conf:/etc/dnsmasq.conf:ro + Volume=/srv/tftp:/srv/tftp:ro + AutoUpdate=registry + Label=io.containers.autoupdate=registry + + [Service] + Restart=always + RestartSec=10 + TimeoutStartSec=60 + + [Install] + WantedBy=multi-user.target + + # ============================================ + # SYSTEM CONFIGURATION + # ============================================ + + # Enable IP forwarding + - path: /etc/sysctl.d/99-ip-forward.conf + mode: 0644 + contents: + inline: | + net.ipv4.ip_forward = 1 + net.ipv6.conf.all.forwarding = 1 + + # ============================================ + # SERVICE CONFIGURATIONS - CUSTOMIZE THESE! + # ============================================ + + # Labgrid exporter configuration + # CUSTOMIZE THIS FOR YOUR DEVICES + - path: /etc/labgrid/exporter.yaml + mode: 0644 + contents: + inline: | + # Labgrid exporter configuration + # Documentation: https://labgrid.readthedocs.io/en/latest/configuration.html + # + # Example device configuration: + # + # openwrt-router-1: + # USBSerialPort: + # match: + # ID_PATH: pci-0000:00:14.0-usb-0:1:1.0 + # speed: 115200 + # NetworkService: + # address: 192.168.101.1%vlan101 + # username: root + # PDUDaemonPort: + # host: localhost:16421 + # pdu: 192.168.128.2 + # index: 1 + # TFTPProvider: + # internal: /srv/tftp/openwrt-router-1/ + # external: openwrt-router-1/ + + # PDUDaemon configuration + # CUSTOMIZE THIS FOR YOUR PDUs + - path: /etc/pdudaemon/pdudaemon.conf + mode: 0644 + contents: + inline: | + { + "daemon": { + "hostname": "0.0.0.0", + "port": 16421, + "logging_level": "INFO" + }, + "pdus": { + "example-pdu": { + "driver": "ubus", + "comment": "Example OpenWrt PDU - replace with your PDU config" + } + } + } + + # Dnsmasq main configuration + - path: /etc/dnsmasq.conf + mode: 0644 + contents: + inline: | + # Disable DNS (only DHCP and TFTP) + port=0 + + # Enable TFTP + enable-tftp + tftp-root=/srv/tftp + + # Log DHCP transactions + log-dhcp + + # Include per-VLAN configurations + conf-dir=/etc/dnsmasq.d/,*.conf + + # Example VLAN dnsmasq configuration + - path: /etc/dnsmasq.d/vlan101.conf.example + mode: 0644 + contents: + inline: | + # Example VLAN 101 DHCP configuration + # Rename to vlan101.conf to enable + # + # interface=vlan101 + # dhcp-range=set:vlan101,192.168.101.100,192.168.101.200,24h + # dhcp-option=tag:vlan101,option:router,192.168.101.1 + + # ============================================ + # UDEV RULES + # ============================================ + + # USB-SD-Mux rules + - path: /etc/udev/rules.d/99-usbsdmux.rules + mode: 0644 + contents: + inline: | + SUBSYSTEM=="scsi_generic", KERNEL=="sg[0-9]*", ATTRS{manufacturer}=="Linux Automation GmbH", ATTRS{product}=="usb-sd-mux*", SYMLINK+="usb-sd-mux/id-$attr{serial}", GROUP="plugdev", TAG+="uaccess" + SUBSYSTEM=="scsi_generic", KERNEL=="sg[0-9]*", ATTRS{manufacturer}=="Pengutronix", ATTRS{product}=="usb-sd-mux*", SYMLINK+="usb-sd-mux/id-$attr{serial}", GROUP="plugdev", TAG+="uaccess" + SUBSYSTEM=="block", KERNEL=="sd[a-z]", ATTRS{manufacturer}=="Linux Automation GmbH", GROUP="plugdev" + SUBSYSTEM=="block", KERNEL=="sd[a-z]", ATTRS{manufacturer}=="Pengutronix", GROUP="plugdev" + + # ============================================ + # HELPER SCRIPTS + # ============================================ + + # labgrid-bound-connect for SSH tunneling through VLANs + - path: /usr/local/sbin/labgrid-bound-connect + mode: 0755 + contents: + inline: | + #!/bin/bash + set -e + + if [ $# -lt 3 ]; then + echo "Usage: $0 " + exit 1 + fi + + INTERFACE="$1" + HOST="$2" + PORT="$3" + + if ! ip link show "$INTERFACE" &>/dev/null; then + echo "Error: Interface $INTERFACE does not exist" + exit 1 + fi + + if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then + echo "Error: Invalid port $PORT" + exit 1 + fi + + exec socat - "TCP:${HOST}:${PORT},bind-dev=${INTERFACE},connect-timeout=10,keepalive,keepidle=10,keepintvl=10,keepcnt=3" + + # Sudoers entry + - path: /etc/sudoers.d/labgrid-bound-connect + mode: 0440 + contents: + inline: | + ALL ALL = NOPASSWD: /usr/local/sbin/labgrid-bound-connect + + # ============================================ + # AUTO-UPDATE CONFIGURATION + # ============================================ + + # Zincati (OS) auto-update - weekly Sunday 3am UTC + - path: /etc/zincati/config.d/55-updates-strategy.toml + mode: 0644 + contents: + inline: | + [updates] + strategy = "periodic" + + [updates.periodic] + time_zone = "UTC" + + [[updates.periodic.window]] + days = [ "Sun" ] + start_time = "03:00" + length_minutes = 120 + + # Podman auto-update timer + - path: /etc/systemd/system/podman-auto-update.timer + mode: 0644 + contents: + inline: | + [Unit] + Description=Podman auto-update timer + + [Timer] + OnCalendar=*-*-* 04:00:00 + RandomizedDelaySec=900 + Persistent=true + + [Install] + WantedBy=timers.target + +systemd: + units: + # Enable container auto-update timer + - name: podman-auto-update.timer + enabled: true + + # Enable OS auto-updates via zincati + - name: zincati.service + enabled: true + + # Cache cleanup timer + - name: labgrid-cache-cleanup.timer + enabled: true + contents: | + [Unit] + Description=Labgrid cache cleanup timer + + [Timer] + OnCalendar=daily + RandomizedDelaySec=3600 + Persistent=true + + [Install] + WantedBy=timers.target + + - name: labgrid-cache-cleanup.service + contents: | + [Unit] + Description=Clean old labgrid cache files + + [Service] + Type=oneshot + ExecStart=/usr/bin/find /var/cache/labgrid -type f -mtime +7 -delete + + # Pull container images on first boot + - name: pull-labgrid-images.service + enabled: true + contents: | + [Unit] + Description=Pull labgrid container images + After=network-online.target + Wants=network-online.target + ConditionPathExists=!/var/lib/labgrid-images-pulled + + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStart=/usr/bin/podman pull ghcr.io/openwrt/openwrt-tests/labgrid:latest + ExecStart=/usr/bin/podman pull ghcr.io/openwrt/openwrt-tests/pdudaemon:latest + ExecStart=/usr/bin/podman pull ghcr.io/openwrt/openwrt-tests/dnsmasq:latest + ExecStartPost=/usr/bin/touch /var/lib/labgrid-images-pulled + + [Install] + WantedBy=multi-user.target diff --git a/coreos/ignition/labnode.bu b/coreos/ignition/labnode.bu new file mode 100644 index 000000000..1d700fec5 --- /dev/null +++ b/coreos/ignition/labnode.bu @@ -0,0 +1,251 @@ +# Butane configuration for OpenWrt Test Lab Node +# Compile with: butane --strict labnode.bu > labnode.ign +variant: fcos +version: "1.5.0" + +passwd: + users: + - name: labgrid + groups: + - wheel + - sudo + - dialout + - plugdev + ssh_authorized_keys: + # Coordinator public key - allows coordinator to access this node + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP0ZVlD9TmfAXL53Vq7V9WKE3KPomOa1jINyflrPWAlJ coordinator + # Add developer keys here or via afterburn + shell: /bin/bash + +storage: + directories: + - path: /etc/labgrid + mode: 0755 + - path: /etc/pdudaemon + mode: 0755 + - path: /etc/dnsmasq.d + mode: 0755 + - path: /srv/tftp + mode: 0755 + user: + name: labgrid + group: + name: labgrid + - path: /var/cache/labgrid + mode: 0755 + user: + name: labgrid + group: + name: labgrid + - path: /etc/containers/systemd + mode: 0755 + + files: + # Labgrid coordinator quadlet + - path: /etc/containers/systemd/labgrid-coordinator.container + mode: 0644 + contents: + local: quadlet/labgrid-coordinator.container + + # Labgrid exporter quadlet + - path: /etc/containers/systemd/labgrid-exporter.container + mode: 0644 + contents: + local: quadlet/labgrid-exporter.container + + # PDUDaemon quadlet + - path: /etc/containers/systemd/pdudaemon.container + mode: 0644 + contents: + local: quadlet/pdudaemon.container + + # Dnsmasq quadlet + - path: /etc/containers/systemd/dnsmasq.container + mode: 0644 + contents: + local: quadlet/dnsmasq.container + + # Auto-update timer + - path: /etc/systemd/system/podman-auto-update.timer + mode: 0644 + contents: + local: quadlet/podman-auto-update.timer + + # Enable IP forwarding + - path: /etc/sysctl.d/99-ip-forward.conf + mode: 0644 + contents: + inline: | + net.ipv4.ip_forward = 1 + net.ipv6.conf.all.forwarding = 1 + + # Default exporter configuration (template) + - path: /etc/labgrid/exporter.yaml + mode: 0644 + contents: + inline: | + # Labgrid exporter configuration + # This file should be customized for each lab node + # See: https://labgrid.readthedocs.io/en/latest/configuration.html + + # Example device export: + # my-device: + # USBSerialPort: + # match: + # ID_PATH: pci-0000:00:14.0-usb-0:1:1.0 + # NetworkService: + # address: 192.168.101.1%vlan101 + # username: root + # PDUDaemonPort: + # host: localhost:16421 + # pdu: 192.168.128.2 + # index: 1 + + # Default PDUDaemon configuration + - path: /etc/pdudaemon/pdudaemon.conf + mode: 0644 + contents: + inline: | + { + "daemon": { + "hostname": "0.0.0.0", + "port": 16421, + "logging_level": "INFO" + }, + "pdus": { + } + } + + # Default dnsmasq configuration + - path: /etc/dnsmasq.conf + mode: 0644 + contents: + inline: | + # Disable DNS (only DHCP and TFTP) + port=0 + + # Enable TFTP + enable-tftp + tftp-root=/srv/tftp + + # Log DHCP transactions + log-dhcp + + # Include per-VLAN configurations + conf-dir=/etc/dnsmasq.d/,*.conf + + # USB-SD-Mux udev rules + - path: /etc/udev/rules.d/99-usbsdmux.rules + mode: 0644 + contents: + inline: | + SUBSYSTEM=="scsi_generic", KERNEL=="sg[0-9]*", ATTRS{manufacturer}=="Linux Automation GmbH", ATTRS{product}=="usb-sd-mux*", SYMLINK+="usb-sd-mux/id-$attr{serial}", GROUP="plugdev", TAG+="uaccess" + SUBSYSTEM=="scsi_generic", KERNEL=="sg[0-9]*", ATTRS{manufacturer}=="Pengutronix", ATTRS{product}=="usb-sd-mux*", SYMLINK+="usb-sd-mux/id-$attr{serial}", GROUP="plugdev", TAG+="uaccess" + SUBSYSTEM=="block", KERNEL=="sd[a-z]", ATTRS{manufacturer}=="Linux Automation GmbH", GROUP="plugdev" + SUBSYSTEM=="block", KERNEL=="sd[a-z]", ATTRS{manufacturer}=="Pengutronix", GROUP="plugdev" + + # labgrid-bound-connect script for SSH tunneling + - path: /usr/local/sbin/labgrid-bound-connect + mode: 0755 + contents: + inline: | + #!/bin/bash + # labgrid-bound-connect - Establish SSH connection bound to specific interface + set -e + + if [ $# -lt 3 ]; then + echo "Usage: $0 " + exit 1 + fi + + INTERFACE="$1" + HOST="$2" + PORT="$3" + + # Validate interface name + if ! ip link show "$INTERFACE" &>/dev/null; then + echo "Error: Interface $INTERFACE does not exist" + exit 1 + fi + + # Validate port + if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then + echo "Error: Invalid port $PORT" + exit 1 + fi + + exec socat - "TCP:${HOST}:${PORT},bind-dev=${INTERFACE},connect-timeout=10,keepalive,keepidle=10,keepintvl=10,keepcnt=3" + + # Sudoers entry for labgrid-bound-connect + - path: /etc/sudoers.d/labgrid-bound-connect + mode: 0440 + contents: + inline: | + ALL ALL = NOPASSWD: /usr/local/sbin/labgrid-bound-connect + + # Zincati auto-update configuration (for OS updates) + - path: /etc/zincati/config.d/55-updates-strategy.toml + mode: 0644 + contents: + inline: | + [updates] + strategy = "periodic" + + [updates.periodic] + time_zone = "UTC" + + [[updates.periodic.window]] + days = [ "Sun" ] + start_time = "03:00" + length_minutes = 120 + + # Container auto-update configuration + - path: /etc/systemd/system/podman-auto-update.service.d/override.conf + mode: 0644 + contents: + inline: | + [Service] + # Restart containers after update + ExecStartPost=/usr/bin/systemctl daemon-reload + +systemd: + units: + # Enable auto-update timer for containers + - name: podman-auto-update.timer + enabled: true + + # Enable zincati for OS updates + - name: zincati.service + enabled: true + + # Regenerate systemd units from quadlet files on boot + - name: systemd-tmpfiles-setup.service + dropins: + - name: quadlet.conf + contents: | + [Service] + ExecStartPost=/usr/libexec/podman/quadlet --dryrun + + # Cache cleanup timer + - name: labgrid-cache-cleanup.timer + enabled: true + contents: | + [Unit] + Description=Labgrid cache cleanup timer + + [Timer] + OnCalendar=daily + RandomizedDelaySec=3600 + Persistent=true + + [Install] + WantedBy=timers.target + + - name: labgrid-cache-cleanup.service + contents: | + [Unit] + Description=Clean old labgrid cache files + + [Service] + Type=oneshot + ExecStart=/usr/bin/find /var/cache/labgrid -type f -mtime +7 -delete diff --git a/coreos/lab-config.yaml.example b/coreos/lab-config.yaml.example new file mode 100644 index 000000000..a60197d9c --- /dev/null +++ b/coreos/lab-config.yaml.example @@ -0,0 +1,75 @@ +# OpenWrt Test Lab Configuration +# Copy this file to lab-config.yaml and customize for your lab + +# Lab identification +lab: + name: my-lab + hostname: labgrid-mylab + +# SSH access - add your public keys here +ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample... admin@example.com + # - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAnother... dev@example.com + +# Network configuration +network: + # Main network interface + interface: eth0 + + # VLANs for device isolation + vlans: + - id: 101 + address: 192.168.101.1/24 + dhcp_start: 192.168.101.100 + dhcp_end: 192.168.101.200 + - id: 102 + address: 192.168.102.1/24 + dhcp_start: 192.168.102.100 + dhcp_end: 192.168.102.200 + +# Power distribution units +pdus: + # OpenWrt device with ubus + - address: 192.168.128.2 + driver: ubus + + # NETIO4 smart PDU (uncomment if used) + # - address: 192.168.128.3 + # driver: netio4 + # username: netio + # password: netio + +# Test devices +devices: + - name: openwrt-one + serial: + # Find with: udevadm info /dev/ttyUSB0 | grep ID_PATH + id_path: pci-0000:00:14.0-usb-0:1:1.0 + speed: 115200 + network: + vlan: 101 + power: + pdu: 192.168.128.2 + outlet: 1 + + - name: banana-pi-r4 + serial: + id_path: pci-0000:00:14.0-usb-0:2:1.0 + network: + vlan: 102 + power: + pdu: 192.168.128.2 + outlet: 2 + +# Container registry (defaults to ghcr.io/openwrt/openwrt-tests) +# registry: ghcr.io/openwrt/openwrt-tests + +# Update schedule +updates: + # OS updates (Zincati) - day of week and time (UTC) + os: + day: Sun + time: "03:00" + # Container updates - time (UTC) + containers: + time: "04:00" diff --git a/coreos/quadlet/dnsmasq.container b/coreos/quadlet/dnsmasq.container new file mode 100644 index 000000000..93a6a2860 --- /dev/null +++ b/coreos/quadlet/dnsmasq.container @@ -0,0 +1,31 @@ +[Unit] +Description=Dnsmasq DHCP/TFTP server +After=network-online.target +Wants=network-online.target + +[Container] +Image=ghcr.io/openwrt/openwrt-tests/dnsmasq:latest +ContainerName=dnsmasq + +# Host network for DHCP broadcast and VLAN access +Network=host + +# Privileged for DHCP (requires raw sockets) +AddCapability=NET_ADMIN +AddCapability=NET_RAW +AddCapability=NET_BIND_SERVICE + +Volume=/etc/dnsmasq.d:/etc/dnsmasq.d:ro +Volume=/etc/dnsmasq.conf:/etc/dnsmasq.conf:ro +Volume=/srv/tftp:/srv/tftp:ro + +AutoUpdate=registry +Label=io.containers.autoupdate=registry + +[Service] +Restart=always +RestartSec=10 +TimeoutStartSec=60 + +[Install] +WantedBy=multi-user.target diff --git a/coreos/quadlet/labgrid-coordinator.container b/coreos/quadlet/labgrid-coordinator.container new file mode 100644 index 000000000..db6fa8c41 --- /dev/null +++ b/coreos/quadlet/labgrid-coordinator.container @@ -0,0 +1,22 @@ +[Unit] +Description=Labgrid Coordinator +After=network-online.target +Wants=network-online.target + +[Container] +Image=ghcr.io/openwrt/openwrt-tests/labgrid:latest +ContainerName=labgrid-coordinator +Environment=LABGRID_MODE=coordinator +Environment=LABGRID_COORDINATOR_LISTEN=:: +PublishPort=20408:20408 +Volume=/etc/labgrid:/etc/labgrid:ro +AutoUpdate=registry +Label=io.containers.autoupdate=registry + +[Service] +Restart=always +RestartSec=30 +TimeoutStartSec=300 + +[Install] +WantedBy=multi-user.target diff --git a/coreos/quadlet/labgrid-exporter.container b/coreos/quadlet/labgrid-exporter.container new file mode 100644 index 000000000..34d67935c --- /dev/null +++ b/coreos/quadlet/labgrid-exporter.container @@ -0,0 +1,47 @@ +[Unit] +Description=Labgrid Exporter +After=network-online.target labgrid-coordinator.service +Wants=network-online.target +Requires=labgrid-coordinator.service + +[Container] +Image=ghcr.io/openwrt/openwrt-tests/labgrid:latest +ContainerName=labgrid-exporter +Environment=LABGRID_MODE=exporter +Environment=LABGRID_CONFIG=/etc/labgrid/exporter.yaml +Environment=LABGRID_EXPORTER_NAME=%H +Environment=LG_CROSSBAR=ws://labgrid-coordinator:20408/ws + +# Host network required for VLAN access +Network=host + +# Mount configurations +Volume=/etc/labgrid:/etc/labgrid:ro +Volume=/srv/tftp:/srv/tftp:rw +Volume=/var/cache/labgrid:/var/cache/labgrid:rw + +# Device access for serial consoles and USB +AddDevice=/dev/ttyUSB0 +AddDevice=/dev/ttyUSB1 +AddDevice=/dev/ttyUSB2 +AddDevice=/dev/ttyUSB3 +AddDevice=/dev/ttyACM0 +AddDevice=/dev/ttyACM1 + +# Group permissions +GroupAdd=dialout +GroupAdd=plugdev + +# Security options for device access +SecurityLabelDisable=true + +AutoUpdate=registry +Label=io.containers.autoupdate=registry + +[Service] +Restart=always +RestartSec=30 +TimeoutStartSec=300 + +[Install] +WantedBy=multi-user.target diff --git a/coreos/quadlet/pdudaemon.container b/coreos/quadlet/pdudaemon.container new file mode 100644 index 000000000..8289aacb4 --- /dev/null +++ b/coreos/quadlet/pdudaemon.container @@ -0,0 +1,24 @@ +[Unit] +Description=PDU Daemon for power control +After=network-online.target +Wants=network-online.target + +[Container] +Image=ghcr.io/openwrt/openwrt-tests/pdudaemon:latest +ContainerName=pdudaemon + +# Host network for PDU access +Network=host + +Volume=/etc/pdudaemon:/etc/pdudaemon:ro + +AutoUpdate=registry +Label=io.containers.autoupdate=registry + +[Service] +Restart=always +RestartSec=30 +TimeoutStartSec=120 + +[Install] +WantedBy=multi-user.target diff --git a/coreos/quadlet/podman-auto-update.timer b/coreos/quadlet/podman-auto-update.timer new file mode 100644 index 000000000..50be391d3 --- /dev/null +++ b/coreos/quadlet/podman-auto-update.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Podman auto-update timer + +[Timer] +OnCalendar=daily +RandomizedDelaySec=900 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/coreos/scripts/build-containers.sh b/coreos/scripts/build-containers.sh new file mode 100755 index 000000000..7430174de --- /dev/null +++ b/coreos/scripts/build-containers.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# Build and optionally push container images for OpenWrt test lab +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COREOS_DIR="$(dirname "$SCRIPT_DIR")" +CONTAINERS_DIR="$COREOS_DIR/containers" + +# Default registry +REGISTRY="${REGISTRY:-ghcr.io/openwrt/openwrt-tests}" +TAG="${TAG:-latest}" + +# Container images to build +IMAGES=( + "labgrid:Containerfile.labgrid" + "pdudaemon:Containerfile.pdudaemon" + "dnsmasq:Containerfile.dnsmasq" + "ser2net:Containerfile.ser2net" +) + +usage() { + echo "Usage: $0 [OPTIONS] [IMAGE...]" + echo "" + echo "Build container images for OpenWrt test lab" + echo "" + echo "Options:" + echo " -p, --push Push images to registry after building" + echo " -r, --registry Set container registry (default: $REGISTRY)" + echo " -t, --tag Set image tag (default: $TAG)" + echo " -h, --help Show this help message" + echo "" + echo "Images: labgrid, pdudaemon, dnsmasq, ser2net (default: all)" +} + +PUSH=false +BUILD_IMAGES=() + +while [[ $# -gt 0 ]]; do + case $1 in + -p|--push) + PUSH=true + shift + ;; + -r|--registry) + REGISTRY="$2" + shift 2 + ;; + -t|--tag) + TAG="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + BUILD_IMAGES+=("$1") + shift + ;; + esac +done + +# If no specific images requested, build all +if [ ${#BUILD_IMAGES[@]} -eq 0 ]; then + for img in "${IMAGES[@]}"; do + BUILD_IMAGES+=("${img%%:*}") + done +fi + +# Find containerfile for image +get_containerfile() { + local name="$1" + for img in "${IMAGES[@]}"; do + if [[ "${img%%:*}" == "$name" ]]; then + echo "${img#*:}" + return 0 + fi + done + return 1 +} + +echo "=== OpenWrt Test Lab Container Builder ===" +echo "Registry: $REGISTRY" +echo "Tag: $TAG" +echo "Push: $PUSH" +echo "" + +cd "$CONTAINERS_DIR" + +for name in "${BUILD_IMAGES[@]}"; do + containerfile=$(get_containerfile "$name") + if [ -z "$containerfile" ]; then + echo "ERROR: Unknown image: $name" + continue + fi + + image_name="$REGISTRY/$name:$TAG" + echo ">>> Building $image_name from $containerfile" + + podman build \ + -t "$image_name" \ + -f "$containerfile" \ + . + + if [ "$PUSH" = true ]; then + echo ">>> Pushing $image_name" + podman push "$image_name" + fi + + echo "" +done + +echo "=== Build complete ===" diff --git a/coreos/scripts/build-ignition.sh b/coreos/scripts/build-ignition.sh new file mode 100755 index 000000000..8087e8959 --- /dev/null +++ b/coreos/scripts/build-ignition.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Build Ignition configuration from simple lab config +# Usage: ./build-ignition.sh lab-config.yaml [output.ign] +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COREOS_DIR="$(dirname "$SCRIPT_DIR")" + +usage() { + echo "Usage: $0 [output.ign]" + echo "" + echo "Generate Ignition configuration from lab config file." + echo "" + echo "Arguments:" + echo " lab-config.yaml Lab configuration (see lab-config.yaml.example)" + echo " output.ign Output file (default: labnode.ign)" + echo "" + echo "Example:" + echo " $0 lab-config.yaml" + echo " $0 my-lab.yaml my-lab.ign" +} + +if [ $# -lt 1 ]; then + usage + exit 1 +fi + +CONFIG="$1" +OUTPUT="${2:-labnode.ign}" + +if [ ! -f "$CONFIG" ]; then + echo "Error: Config file not found: $CONFIG" + exit 1 +fi + +# Check for required tools +check_tool() { + if ! command -v "$1" &> /dev/null; then + echo "Error: $1 not found. Please install it first." + echo " Fedora/RHEL: sudo dnf install $2" + echo " Debian/Ubuntu: sudo apt install $2" + exit 1 + fi +} + +check_tool python3 python3 +check_tool pip3 python3-pip + +# Install PyYAML if needed +if ! python3 -c "import yaml" 2>/dev/null; then + echo "Installing PyYAML..." + pip3 install --user pyyaml +fi + +# Check for butane +if ! command -v butane &> /dev/null; then + echo "Butane not found. Downloading..." + ARCH=$(uname -m) + case $ARCH in + x86_64) ARCH="x86_64" ;; + aarch64) ARCH="aarch64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + BUTANE_VERSION="v0.20.0" + BUTANE_URL="https://github.com/coreos/butane/releases/download/${BUTANE_VERSION}/butane-${ARCH}-unknown-linux-gnu" + curl -sLo /tmp/butane "$BUTANE_URL" + chmod +x /tmp/butane + BUTANE="/tmp/butane" +else + BUTANE="butane" +fi + +echo "=== Building Ignition Configuration ===" +echo "Config: $CONFIG" +echo "Output: $OUTPUT" +echo "" + +# Generate Butane from config, then compile to Ignition +python3 "$SCRIPT_DIR/generate-butane.py" "$CONFIG" | $BUTANE --strict --pretty > "$OUTPUT" + +echo "Success! Generated: $OUTPUT" +echo "" +echo "Next steps:" +echo "1. Download Fedora CoreOS from: https://fedoraproject.org/coreos/download" +echo "2. Install to disk:" +echo " sudo coreos-installer install /dev/sdX --ignition-file $OUTPUT" +echo "" +echo "3. Or boot ISO/PXE and provide ignition via:" +echo " - Kernel arg: ignition.config.url=http://server/$OUTPUT" +echo " - USB drive: copy to /ignition/config.ign on FAT partition" diff --git a/coreos/scripts/generate-butane.py b/coreos/scripts/generate-butane.py new file mode 100755 index 000000000..84924513c --- /dev/null +++ b/coreos/scripts/generate-butane.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python3 +""" +Generate Butane configuration from simple lab config YAML. + +Usage: + ./generate-butane.py lab-config.yaml > labnode.bu + ./generate-butane.py lab-config.yaml | butane --strict > labnode.ign +""" + +import argparse +import json +import sys +from pathlib import Path + +import yaml + +# Coordinator public key (always included) +COORDINATOR_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP0ZVlD9TmfAXL53Vq7V9WKE3KPomOa1jINyflrPWAlJ coordinator" + + +def generate_exporter_yaml(config: dict) -> str: + """Generate labgrid exporter.yaml content.""" + devices = config.get("devices", []) + if not devices: + return "# No devices configured\n" + + lines = ["# Auto-generated labgrid exporter configuration", ""] + + for device in devices: + name = device["name"] + lines.append(f"{name}:") + + # Serial port + if "serial" in device: + serial = device["serial"] + lines.append(" USBSerialPort:") + lines.append(" match:") + lines.append(f" ID_PATH: {serial['id_path']}") + if "speed" in serial: + lines.append(f" speed: {serial['speed']}") + + # Network service + if "network" in device: + net = device["network"] + vlan = net.get("vlan", 101) + vlan_ip = f"192.168.{vlan}.1" + lines.append(" NetworkService:") + lines.append(f" address: {vlan_ip}%vlan{vlan}") + lines.append(" username: root") + + # Power control + if "power" in device: + power = device["power"] + lines.append(" PDUDaemonPort:") + lines.append(" host: localhost:16421") + lines.append(f" pdu: {power['pdu']}") + lines.append(f" index: {power['outlet']}") + + # TFTP provider + lines.append(" TFTPProvider:") + lines.append(f" internal: /srv/tftp/{name}/") + lines.append(f" external: {name}/") + lines.append("") + + return "\n".join(lines) + + +def generate_pdudaemon_conf(config: dict) -> str: + """Generate pdudaemon.conf content.""" + pdus = config.get("pdus", []) + + pdu_config = { + "daemon": {"hostname": "0.0.0.0", "port": 16421, "logging_level": "INFO"}, + "pdus": {}, + } + + for pdu in pdus: + addr = pdu["address"] + driver = pdu.get("driver", "ubus") + pdu_entry = {"driver": driver} + + if driver == "netio4": + pdu_entry["username"] = pdu.get("username", "netio") + pdu_entry["password"] = pdu.get("password", "netio") + pdu_entry["telnetport"] = pdu.get("telnetport", 23) + + pdu_config["pdus"][addr] = pdu_entry + + return json.dumps(pdu_config, indent=2) + + +def generate_dnsmasq_vlan_conf(vlan: dict) -> str: + """Generate dnsmasq VLAN config.""" + vlan_id = vlan["id"] + lines = [ + f"# VLAN {vlan_id} DHCP configuration", + f"interface=vlan{vlan_id}", + f"dhcp-range=set:vlan{vlan_id},{vlan['dhcp_start']},{vlan['dhcp_end']},24h", + ] + + # Extract gateway from address + addr = vlan.get("address", f"192.168.{vlan_id}.1/24") + gateway = addr.split("/")[0] + lines.append(f"dhcp-option=tag:vlan{vlan_id},option:router,{gateway}") + + return "\n".join(lines) + + +def generate_butane(config: dict) -> dict: + """Generate complete Butane configuration.""" + lab = config.get("lab", {}) + hostname = lab.get("hostname", "labgrid-node") + registry = config.get("registry", "ghcr.io/openwrt/openwrt-tests") + updates = config.get("updates", {}) + + # SSH keys + ssh_keys = [COORDINATOR_KEY] + config.get("ssh_keys", []) + + # Build Butane structure + butane = { + "variant": "fcos", + "version": "1.5.0", + "passwd": { + "users": [ + { + "name": "labgrid", + "groups": ["wheel", "sudo", "dialout", "plugdev"], + "ssh_authorized_keys": ssh_keys, + "shell": "/bin/bash", + } + ] + }, + "storage": {"directories": [], "files": []}, + "systemd": {"units": []}, + } + + # Directories + dirs = [ + "/etc/labgrid", + "/etc/pdudaemon", + "/etc/dnsmasq.d", + "/etc/containers/systemd", + ] + for d in dirs: + butane["storage"]["directories"].append({"path": d, "mode": 0o755}) + + # Special directories with ownership + butane["storage"]["directories"].extend( + [ + { + "path": "/srv/tftp", + "mode": 0o755, + "user": {"name": "labgrid"}, + "group": {"name": "labgrid"}, + }, + { + "path": "/var/cache/labgrid", + "mode": 0o755, + "user": {"name": "labgrid"}, + "group": {"name": "labgrid"}, + }, + ] + ) + + # Device TFTP directories + for device in config.get("devices", []): + butane["storage"]["directories"].append( + { + "path": f"/srv/tftp/{device['name']}", + "mode": 0o755, + "user": {"name": "labgrid"}, + "group": {"name": "labgrid"}, + } + ) + + files = butane["storage"]["files"] + + # Hostname + files.append( + {"path": "/etc/hostname", "mode": 0o644, "contents": {"inline": hostname}} + ) + + # Quadlet: labgrid-coordinator + files.append( + { + "path": "/etc/containers/systemd/labgrid-coordinator.container", + "mode": 0o644, + "contents": { + "inline": f"""[Unit] +Description=Labgrid Coordinator +After=network-online.target +Wants=network-online.target + +[Container] +Image={registry}/labgrid:latest +ContainerName=labgrid-coordinator +Environment=LABGRID_MODE=coordinator +Environment=LABGRID_COORDINATOR_LISTEN=:: +PublishPort=20408:20408 +Volume=/etc/labgrid:/etc/labgrid:ro +AutoUpdate=registry +Label=io.containers.autoupdate=registry + +[Service] +Restart=always +RestartSec=30 + +[Install] +WantedBy=multi-user.target +""" + }, + } + ) + + # Quadlet: labgrid-exporter + files.append( + { + "path": "/etc/containers/systemd/labgrid-exporter.container", + "mode": 0o644, + "contents": { + "inline": f"""[Unit] +Description=Labgrid Exporter +After=network-online.target labgrid-coordinator.service +Wants=network-online.target + +[Container] +Image={registry}/labgrid:latest +ContainerName=labgrid-exporter +Environment=LABGRID_MODE=exporter +Environment=LABGRID_CONFIG=/etc/labgrid/exporter.yaml +Environment=LG_CROSSBAR=ws://localhost:20408/ws +Network=host +Volume=/etc/labgrid:/etc/labgrid:ro +Volume=/srv/tftp:/srv/tftp:rw +Volume=/var/cache/labgrid:/var/cache/labgrid:rw +AddDevice=/dev/ttyUSB0 +AddDevice=/dev/ttyUSB1 +AddDevice=/dev/ttyUSB2 +AddDevice=/dev/ttyUSB3 +AddDevice=/dev/ttyACM0 +AddDevice=/dev/ttyACM1 +GroupAdd=dialout +GroupAdd=plugdev +SecurityLabelDisable=true +AutoUpdate=registry +Label=io.containers.autoupdate=registry + +[Service] +Restart=always +RestartSec=30 + +[Install] +WantedBy=multi-user.target +""" + }, + } + ) + + # Quadlet: pdudaemon + files.append( + { + "path": "/etc/containers/systemd/pdudaemon.container", + "mode": 0o644, + "contents": { + "inline": f"""[Unit] +Description=PDU Daemon +After=network-online.target +Wants=network-online.target + +[Container] +Image={registry}/pdudaemon:latest +ContainerName=pdudaemon +Network=host +Volume=/etc/pdudaemon:/etc/pdudaemon:ro +AutoUpdate=registry +Label=io.containers.autoupdate=registry + +[Service] +Restart=always +RestartSec=30 + +[Install] +WantedBy=multi-user.target +""" + }, + } + ) + + # Quadlet: dnsmasq + files.append( + { + "path": "/etc/containers/systemd/dnsmasq.container", + "mode": 0o644, + "contents": { + "inline": f"""[Unit] +Description=Dnsmasq DHCP/TFTP +After=network-online.target +Wants=network-online.target + +[Container] +Image={registry}/dnsmasq:latest +ContainerName=dnsmasq +Network=host +AddCapability=NET_ADMIN +AddCapability=NET_RAW +AddCapability=NET_BIND_SERVICE +Volume=/etc/dnsmasq.d:/etc/dnsmasq.d:ro +Volume=/etc/dnsmasq.conf:/etc/dnsmasq.conf:ro +Volume=/srv/tftp:/srv/tftp:ro +AutoUpdate=registry +Label=io.containers.autoupdate=registry + +[Service] +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +""" + }, + } + ) + + # Exporter configuration + files.append( + { + "path": "/etc/labgrid/exporter.yaml", + "mode": 0o644, + "contents": {"inline": generate_exporter_yaml(config)}, + } + ) + + # PDUDaemon configuration + files.append( + { + "path": "/etc/pdudaemon/pdudaemon.conf", + "mode": 0o644, + "contents": {"inline": generate_pdudaemon_conf(config)}, + } + ) + + # Dnsmasq main config + files.append( + { + "path": "/etc/dnsmasq.conf", + "mode": 0o644, + "contents": { + "inline": """port=0 +enable-tftp +tftp-root=/srv/tftp +log-dhcp +conf-dir=/etc/dnsmasq.d/,*.conf +""" + }, + } + ) + + # VLAN dnsmasq configs + for vlan in config.get("network", {}).get("vlans", []): + files.append( + { + "path": f"/etc/dnsmasq.d/vlan{vlan['id']}.conf", + "mode": 0o644, + "contents": {"inline": generate_dnsmasq_vlan_conf(vlan)}, + } + ) + + # IP forwarding + files.append( + { + "path": "/etc/sysctl.d/99-ip-forward.conf", + "mode": 0o644, + "contents": { + "inline": "net.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1\n" + }, + } + ) + + # USB-SD-Mux udev rules + files.append( + { + "path": "/etc/udev/rules.d/99-usbsdmux.rules", + "mode": 0o644, + "contents": { + "inline": """SUBSYSTEM=="scsi_generic", KERNEL=="sg[0-9]*", ATTRS{manufacturer}=="Linux Automation GmbH", ATTRS{product}=="usb-sd-mux*", SYMLINK+="usb-sd-mux/id-$attr{serial}", GROUP="plugdev", TAG+="uaccess" +SUBSYSTEM=="scsi_generic", KERNEL=="sg[0-9]*", ATTRS{manufacturer}=="Pengutronix", ATTRS{product}=="usb-sd-mux*", SYMLINK+="usb-sd-mux/id-$attr{serial}", GROUP="plugdev", TAG+="uaccess" +SUBSYSTEM=="block", KERNEL=="sd[a-z]", ATTRS{manufacturer}=="Linux Automation GmbH", GROUP="plugdev" +SUBSYSTEM=="block", KERNEL=="sd[a-z]", ATTRS{manufacturer}=="Pengutronix", GROUP="plugdev" +""" + }, + } + ) + + # labgrid-bound-connect script + files.append( + { + "path": "/usr/local/sbin/labgrid-bound-connect", + "mode": 0o755, + "contents": { + "inline": """#!/bin/bash +set -e +[ $# -lt 3 ] && { echo "Usage: $0 "; exit 1; } +INTERFACE="$1"; HOST="$2"; PORT="$3" +ip link show "$INTERFACE" &>/dev/null || { echo "Error: Interface $INTERFACE not found"; exit 1; } +[[ "$PORT" =~ ^[0-9]+$ ]] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] || { echo "Error: Invalid port"; exit 1; } +exec socat - "TCP:${HOST}:${PORT},bind-dev=${INTERFACE},connect-timeout=10,keepalive" +""" + }, + } + ) + + # Sudoers + files.append( + { + "path": "/etc/sudoers.d/labgrid-bound-connect", + "mode": 0o440, + "contents": { + "inline": "ALL ALL = NOPASSWD: /usr/local/sbin/labgrid-bound-connect\n" + }, + } + ) + + # Zincati update config + os_update = updates.get("os", {}) + update_day = os_update.get("day", "Sun") + update_time = os_update.get("time", "03:00") + files.append( + { + "path": "/etc/zincati/config.d/55-updates-strategy.toml", + "mode": 0o644, + "contents": { + "inline": f"""[updates] +strategy = "periodic" + +[updates.periodic] +time_zone = "UTC" + +[[updates.periodic.window]] +days = [ "{update_day}" ] +start_time = "{update_time}" +length_minutes = 120 +""" + }, + } + ) + + # Container auto-update timer + container_time = updates.get("containers", {}).get("time", "04:00") + files.append( + { + "path": "/etc/systemd/system/podman-auto-update.timer", + "mode": 0o644, + "contents": { + "inline": f"""[Unit] +Description=Podman auto-update timer + +[Timer] +OnCalendar=*-*-* {container_time}:00 +RandomizedDelaySec=900 +Persistent=true + +[Install] +WantedBy=timers.target +""" + }, + } + ) + + # Systemd units + units = butane["systemd"]["units"] + + units.append({"name": "podman-auto-update.timer", "enabled": True}) + units.append({"name": "zincati.service", "enabled": True}) + + # Cache cleanup + units.append( + { + "name": "labgrid-cache-cleanup.timer", + "enabled": True, + "contents": """[Unit] +Description=Labgrid cache cleanup timer + +[Timer] +OnCalendar=daily +RandomizedDelaySec=3600 +Persistent=true + +[Install] +WantedBy=timers.target +""", + } + ) + + units.append( + { + "name": "labgrid-cache-cleanup.service", + "contents": """[Unit] +Description=Clean old labgrid cache files + +[Service] +Type=oneshot +ExecStart=/usr/bin/find /var/cache/labgrid -type f -mtime +7 -delete +""", + } + ) + + # Pull images on first boot + units.append( + { + "name": "pull-labgrid-images.service", + "enabled": True, + "contents": f"""[Unit] +Description=Pull labgrid container images +After=network-online.target +Wants=network-online.target +ConditionPathExists=!/var/lib/labgrid-images-pulled + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/bin/podman pull {registry}/labgrid:latest +ExecStart=/usr/bin/podman pull {registry}/pdudaemon:latest +ExecStart=/usr/bin/podman pull {registry}/dnsmasq:latest +ExecStartPost=/usr/bin/touch /var/lib/labgrid-images-pulled + +[Install] +WantedBy=multi-user.target +""", + } + ) + + return butane + + +def main(): + parser = argparse.ArgumentParser( + description="Generate Butane config from lab configuration" + ) + parser.add_argument("config", help="Lab configuration YAML file") + parser.add_argument( + "-o", "--output", help="Output file (default: stdout)", default="-" + ) + args = parser.parse_args() + + # Load config + config_path = Path(args.config) + if not config_path.exists(): + print(f"Error: Config file not found: {config_path}", file=sys.stderr) + sys.exit(1) + + with open(config_path) as f: + config = yaml.safe_load(f) + + # Generate Butane + butane = generate_butane(config) + + # Output + output = yaml.dump(butane, default_flow_style=False, sort_keys=False) + + if args.output == "-": + print(output) + else: + with open(args.output, "w") as f: + f.write(output) + print(f"Generated: {args.output}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/coreos/scripts/generate-ignition.sh b/coreos/scripts/generate-ignition.sh new file mode 100755 index 000000000..d8167d21a --- /dev/null +++ b/coreos/scripts/generate-ignition.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# Generate Ignition configuration from Butane files +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COREOS_DIR="$(dirname "$SCRIPT_DIR")" +IGNITION_DIR="$COREOS_DIR/ignition" + +usage() { + echo "Usage: $0 [OPTIONS] " + echo "" + echo "Generate Ignition JSON from Butane YAML configuration" + echo "" + echo "Options:" + echo " -o, --output FILE Output file (default: .ign)" + echo " -s, --strict Enable strict mode (fail on warnings)" + echo " -h, --help Show this help message" + echo "" + echo "Example:" + echo " $0 ignition/labnode-standalone.bu" + echo " $0 -o my-lab.ign ignition/labnode-standalone.bu" +} + +OUTPUT="" +STRICT="" + +while [[ $# -gt 0 ]]; do + case $1 in + -o|--output) + OUTPUT="$2" + shift 2 + ;; + -s|--strict) + STRICT="--strict" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + INPUT="$1" + shift + ;; + esac +done + +if [ -z "$INPUT" ]; then + echo "ERROR: No input file specified" + usage + exit 1 +fi + +# Resolve input path +if [[ "$INPUT" != /* ]]; then + INPUT="$COREOS_DIR/$INPUT" +fi + +if [ ! -f "$INPUT" ]; then + echo "ERROR: Input file not found: $INPUT" + exit 1 +fi + +# Default output filename +if [ -z "$OUTPUT" ]; then + OUTPUT="${INPUT%.bu}.ign" +fi + +# Check if butane is installed +if ! command -v butane &> /dev/null; then + echo "Butane not found. Installing..." + + # Detect architecture + ARCH=$(uname -m) + case $ARCH in + x86_64) ARCH="x86_64" ;; + aarch64) ARCH="aarch64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + # Download butane + BUTANE_VERSION="v0.20.0" + BUTANE_URL="https://github.com/coreos/butane/releases/download/${BUTANE_VERSION}/butane-${ARCH}-unknown-linux-gnu" + + echo "Downloading butane ${BUTANE_VERSION}..." + curl -sLo /tmp/butane "$BUTANE_URL" + chmod +x /tmp/butane + BUTANE="/tmp/butane" +else + BUTANE="butane" +fi + +echo "=== Generating Ignition Configuration ===" +echo "Input: $INPUT" +echo "Output: $OUTPUT" +echo "" + +# Generate ignition +$BUTANE $STRICT --pretty "$INPUT" > "$OUTPUT" + +echo "Successfully generated: $OUTPUT" +echo "" +echo "To use this configuration:" +echo "1. Download Fedora CoreOS: https://fedoraproject.org/coreos/download" +echo "2. Boot with: coreos-installer install /dev/sdX --ignition-file $OUTPUT" +echo " Or for cloud: provide as user-data" From bad28c262626115b4dc226658e5c160f7bd0d8b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 20:01:54 +0000 Subject: [PATCH 2/7] feat: add Raspberry Pi setup with Docker and cloud-init Add simplified lab node setup for Raspberry Pi using Docker Compose: - docker-compose.yml with all services (labgrid, pdudaemon, dnsmasq) - cloud-init user-data for automatic first-boot configuration - setup.sh for manual installation - Watchtower for automatic container updates This provides an easier alternative to Fedora CoreOS for Raspberry Pi based lab nodes, with the same containerized architecture. --- coreos/README.md | 36 ++-- coreos/raspberry-pi/README.md | 211 +++++++++++++++++++++++ coreos/raspberry-pi/cloud-init/meta-data | 2 + coreos/raspberry-pi/cloud-init/user-data | 195 +++++++++++++++++++++ coreos/raspberry-pi/docker-compose.yml | 84 +++++++++ coreos/raspberry-pi/setup.sh | 174 +++++++++++++++++++ 6 files changed, 691 insertions(+), 11 deletions(-) create mode 100644 coreos/raspberry-pi/README.md create mode 100644 coreos/raspberry-pi/cloud-init/meta-data create mode 100644 coreos/raspberry-pi/cloud-init/user-data create mode 100644 coreos/raspberry-pi/docker-compose.yml create mode 100755 coreos/raspberry-pi/setup.sh diff --git a/coreos/README.md b/coreos/README.md index 977cc4a20..1500260e4 100644 --- a/coreos/README.md +++ b/coreos/README.md @@ -1,16 +1,32 @@ -# OpenWrt Test Lab - Immutable OS Image +# OpenWrt Test Lab - Containerized Infrastructure -Self-contained, auto-updating immutable OS image for remote OpenWrt test labs based on Fedora CoreOS with containerized services. +Self-contained, auto-updating lab node setup for remote OpenWrt test labs with containerized services. -## Overview +## Platform Options -This setup provides: -- **Immutable base OS**: Fedora CoreOS with automatic updates via Zincati -- **Containerized services**: labgrid coordinator/exporter, pdudaemon, dnsmasq -- **Auto-updating containers**: Podman auto-update pulls new images automatically -- **Simple configuration**: Single YAML file generates complete system config +| Platform | Best For | Auto-Updates | +|----------|----------|--------------| +| **[Raspberry Pi](raspberry-pi/)** | Most labs, easy setup | Watchtower (containers) | +| [Fedora CoreOS](#fedora-coreos) | x86 servers, immutable OS | Zincati (OS) + Podman | -## Quick Start +## Quick Start - Raspberry Pi (Recommended) + +```bash +# 1. Flash Raspberry Pi OS Lite (64-bit) to SD card + +# 2. Copy cloud-init files to boot partition +cp raspberry-pi/cloud-init/user-data /media/$USER/bootfs/ +cp raspberry-pi/cloud-init/meta-data /media/$USER/bootfs/ + +# 3. Edit user-data - add your SSH keys +nano /media/$USER/bootfs/user-data + +# 4. Boot the Pi - auto-configures in ~5-10 minutes +``` + +See [raspberry-pi/README.md](raspberry-pi/README.md) for detailed instructions. + +## Quick Start - Fedora CoreOS ```bash # 1. Copy and edit the example config @@ -25,8 +41,6 @@ vim lab-config.yaml # Add your SSH keys, devices, etc. sudo coreos-installer install /dev/sdX --ignition-file labnode.ign ``` -That's it! The system will boot with all services configured and auto-updating. - ## Architecture ``` diff --git a/coreos/raspberry-pi/README.md b/coreos/raspberry-pi/README.md new file mode 100644 index 000000000..57c7f2709 --- /dev/null +++ b/coreos/raspberry-pi/README.md @@ -0,0 +1,211 @@ +# OpenWrt Test Lab - Raspberry Pi Setup + +Docker-based labgrid setup for Raspberry Pi with automatic container updates. + +## Quick Start (Cloud-Init) + +The easiest way - automatic setup on first boot: + +```bash +# 1. Flash Raspberry Pi OS Lite (64-bit) to SD card +# Use Raspberry Pi Imager: https://www.raspberrypi.com/software/ + +# 2. Mount the boot partition and copy cloud-init files +cp cloud-init/user-data /media/$USER/bootfs/ +cp cloud-init/meta-data /media/$USER/bootfs/ + +# 3. Edit user-data to add your SSH keys (search for "ADD YOUR SSH KEYS") +nano /media/$USER/bootfs/user-data + +# 4. Unmount and boot the Pi +# First boot takes ~5-10 minutes to install Docker and pull images +``` + +## Quick Start (Manual) + +```bash +# 1. Flash Raspberry Pi OS Lite and boot +# 2. SSH into the Pi and run: +curl -fsSL https://raw.githubusercontent.com/openwrt/openwrt-tests/main/coreos/raspberry-pi/setup.sh | sudo bash + +# 3. Configure your devices +sudo nano /opt/labgrid/config/labgrid/exporter.yaml + +# 4. Start services +sudo systemctl start labgrid +``` + +## Finding Serial Device Paths + +```bash +# List all USB serial devices with their ID_PATH +labgrid-find-serial + +# Example output: +# Device: /dev/ttyUSB0 +# ID_PATH=platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0 +# ID_SERIAL=FTDI_FT232R_USB_UART_A50285BI +``` + +Use the `ID_PATH` value in your `exporter.yaml`. + +## Configuration Files + +All config files are in `/opt/labgrid/config/`: + +``` +/opt/labgrid/ +├── docker-compose.yml +├── config/ +│ ├── labgrid/ +│ │ └── exporter.yaml # Device definitions +│ ├── pdudaemon/ +│ │ └── pdudaemon.conf # PDU configuration +│ └── dnsmasq/ +│ └── *.conf # VLAN DHCP configs +└── tftp/ # TFTP root for firmware +``` + +### Example Device Configuration + +Edit `/opt/labgrid/config/labgrid/exporter.yaml`: + +```yaml +openwrt-one: + USBSerialPort: + match: + # Get this from: labgrid-find-serial + ID_PATH: platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0 + speed: 115200 + NetworkService: + address: 192.168.101.1%vlan101 + username: root + PDUDaemonPort: + host: localhost:16421 + pdu: 192.168.128.2 + index: 1 + TFTPProvider: + internal: /srv/tftp/openwrt-one/ + external: openwrt-one/ +``` + +### PDU Configuration + +Edit `/opt/labgrid/config/pdudaemon/pdudaemon.conf`: + +```json +{ + "daemon": { + "hostname": "0.0.0.0", + "port": 16421, + "logging_level": "INFO" + }, + "pdus": { + "192.168.128.2": { + "driver": "ubus" + } + } +} +``` + +## VLAN Setup + +```bash +# Create VLAN interface +sudo ip link add link eth0 name vlan101 type vlan id 101 +sudo ip addr add 192.168.101.1/24 dev vlan101 +sudo ip link set vlan101 up + +# Make persistent (add to /etc/network/interfaces.d/vlans) +echo "auto vlan101 +iface vlan101 inet static + address 192.168.101.1/24 + vlan-raw-device eth0" | sudo tee /etc/network/interfaces.d/vlans +``` + +## Managing Services + +```bash +# Start/stop all services +sudo systemctl start labgrid +sudo systemctl stop labgrid + +# Or use docker compose directly +cd /opt/labgrid +docker compose up -d +docker compose down + +# View logs +docker logs -f labgrid-exporter +docker logs -f labgrid-coordinator + +# Restart a single service +docker compose restart labgrid-exporter +``` + +## Auto-Updates + +Watchtower automatically updates containers daily at 4:00 AM: + +```bash +# Check watchtower logs +docker logs watchtower + +# Force update now +docker exec watchtower /watchtower --run-once +``` + +## Hardware Recommendations + +### Raspberry Pi Models +- **Pi 5 (4GB+)**: Recommended - best USB and network performance +- **Pi 4 (4GB+)**: Works well +- **Pi 3**: Not recommended (limited USB bandwidth) + +### USB Hub +Use a powered USB hub for multiple serial adapters: +- Plugable 7-Port USB 3.0 Hub +- Anker 7-Port USB 3.0 Hub + +### Serial Adapters +- FTDI FT232R-based adapters (most reliable) +- CH340/CH341 adapters (budget option) + +## Troubleshooting + +### Serial devices not accessible + +```bash +# Check device permissions +ls -la /dev/ttyUSB* + +# Add user to dialout group +sudo usermod -aG dialout $USER + +# Restart containers +docker compose restart labgrid-exporter +``` + +### Container won't start + +```bash +# Check logs +docker logs labgrid-exporter + +# Common issues: +# - Serial device doesn't exist: update devices in docker-compose.yml +# - Config error: check exporter.yaml syntax +``` + +### Network issues with VLANs + +```bash +# Verify VLAN interface exists +ip -d link show vlan101 + +# Check routing +ip route + +# Test connectivity +ping -I vlan101 192.168.101.100 +``` diff --git a/coreos/raspberry-pi/cloud-init/meta-data b/coreos/raspberry-pi/cloud-init/meta-data new file mode 100644 index 000000000..2f798c2e3 --- /dev/null +++ b/coreos/raspberry-pi/cloud-init/meta-data @@ -0,0 +1,2 @@ +instance-id: labgrid-pi +local-hostname: labgrid-pi diff --git a/coreos/raspberry-pi/cloud-init/user-data b/coreos/raspberry-pi/cloud-init/user-data new file mode 100644 index 000000000..1ab8d0760 --- /dev/null +++ b/coreos/raspberry-pi/cloud-init/user-data @@ -0,0 +1,195 @@ +#cloud-config +# OpenWrt Test Lab - Raspberry Pi Cloud-Init +# +# Usage: +# 1. Flash Raspberry Pi OS Lite (64-bit) to SD card +# 2. Copy this file to boot partition as 'user-data' +# 3. Create empty 'meta-data' file on boot partition +# 4. Boot the Pi - it will auto-configure + +hostname: labgrid-pi + +# Create labgrid user +users: + - name: labgrid + groups: [adm, dialout, plugdev, docker, sudo] + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + ssh_authorized_keys: + # Coordinator key + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP0ZVlD9TmfAXL53Vq7V9WKE3KPomOa1jINyflrPWAlJ coordinator + # ADD YOUR SSH KEYS HERE: + # - ssh-ed25519 AAAA... your-key + +# System packages +packages: + - git + - socat + - vlan + - iptables-persistent + +# Enable IP forwarding +write_files: + - path: /etc/sysctl.d/99-labgrid.conf + content: | + net.ipv4.ip_forward = 1 + net.ipv6.conf.all.forwarding = 1 + + - path: /etc/modules-load.d/vlan.conf + content: | + 8021q + + - path: /opt/labgrid/docker-compose.yml + content: | + services: + labgrid-coordinator: + image: ghcr.io/openwrt/openwrt-tests/labgrid:latest + container_name: labgrid-coordinator + restart: always + environment: + - LABGRID_MODE=coordinator + ports: + - "20408:20408" + volumes: + - ./config/labgrid:/etc/labgrid:ro + labels: + - "com.centurylinklabs.watchtower.enable=true" + + labgrid-exporter: + image: ghcr.io/openwrt/openwrt-tests/labgrid:latest + container_name: labgrid-exporter + restart: always + depends_on: + - labgrid-coordinator + environment: + - LABGRID_MODE=exporter + - LABGRID_CONFIG=/etc/labgrid/exporter.yaml + - LG_CROSSBAR=ws://labgrid-coordinator:20408/ws + network_mode: host + volumes: + - ./config/labgrid:/etc/labgrid:ro + - ./tftp:/srv/tftp:rw + devices: + - /dev/ttyUSB0:/dev/ttyUSB0 + - /dev/ttyUSB1:/dev/ttyUSB1 + - /dev/ttyUSB2:/dev/ttyUSB2 + - /dev/ttyUSB3:/dev/ttyUSB3 + privileged: true + labels: + - "com.centurylinklabs.watchtower.enable=true" + + pdudaemon: + image: ghcr.io/openwrt/openwrt-tests/pdudaemon:latest + container_name: pdudaemon + restart: always + network_mode: host + volumes: + - ./config/pdudaemon:/etc/pdudaemon:ro + labels: + - "com.centurylinklabs.watchtower.enable=true" + + dnsmasq: + image: ghcr.io/openwrt/openwrt-tests/dnsmasq:latest + container_name: dnsmasq + restart: always + network_mode: host + cap_add: + - NET_ADMIN + - NET_RAW + volumes: + - ./config/dnsmasq:/etc/dnsmasq.d:ro + - ./config/dnsmasq.conf:/etc/dnsmasq.conf:ro + - ./tftp:/srv/tftp:ro + labels: + - "com.centurylinklabs.watchtower.enable=true" + + watchtower: + image: containrrr/watchtower:latest + container_name: watchtower + restart: always + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_SCHEDULE=0 0 4 * * * + - WATCHTOWER_LABEL_ENABLE=true + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + - path: /opt/labgrid/config/labgrid/exporter.yaml + content: | + # Add your devices here + # See: labgrid-find-serial + + - path: /opt/labgrid/config/pdudaemon/pdudaemon.conf + content: | + { + "daemon": { + "hostname": "0.0.0.0", + "port": 16421, + "logging_level": "INFO" + }, + "pdus": {} + } + + - path: /opt/labgrid/config/dnsmasq.conf + content: | + port=0 + enable-tftp + tftp-root=/srv/tftp + log-dhcp + conf-dir=/etc/dnsmasq.d/,*.conf + + - path: /etc/systemd/system/labgrid.service + content: | + [Unit] + Description=OpenWrt Test Lab + After=docker.service + Requires=docker.service + + [Service] + Type=oneshot + RemainAfterExit=yes + WorkingDirectory=/opt/labgrid + ExecStart=/usr/bin/docker compose up -d + ExecStop=/usr/bin/docker compose down + + [Install] + WantedBy=multi-user.target + + - path: /usr/local/bin/labgrid-find-serial + permissions: '0755' + content: | + #!/bin/bash + echo "=== USB Serial Devices ===" + for dev in /dev/ttyUSB* /dev/ttyACM* 2>/dev/null; do + [ -e "$dev" ] || continue + echo -e "\nDevice: $dev" + udevadm info "$dev" | grep -E "(ID_PATH|ID_SERIAL)=" + done + +runcmd: + # Install Docker + - curl -fsSL https://get.docker.com | sh + - usermod -aG docker labgrid + + # Create directories + - mkdir -p /opt/labgrid/{config/labgrid,config/pdudaemon,config/dnsmasq,tftp} + - chown -R labgrid:labgrid /opt/labgrid + + # Enable services + - systemctl daemon-reload + - systemctl enable labgrid.service + + # Apply sysctl + - sysctl -p /etc/sysctl.d/99-labgrid.conf + + # Load VLAN module + - modprobe 8021q + + # Start services (will pull images) + - systemctl start labgrid.service + +final_message: | + OpenWrt Test Lab ready! + SSH: ssh labgrid@$HOSTNAME + Config: /opt/labgrid/config/ + Find serial devices: labgrid-find-serial diff --git a/coreos/raspberry-pi/docker-compose.yml b/coreos/raspberry-pi/docker-compose.yml new file mode 100644 index 000000000..275094d0d --- /dev/null +++ b/coreos/raspberry-pi/docker-compose.yml @@ -0,0 +1,84 @@ +# OpenWrt Test Lab - Docker Compose for Raspberry Pi +# Usage: docker compose up -d + +services: + labgrid-coordinator: + image: ghcr.io/openwrt/openwrt-tests/labgrid:latest + container_name: labgrid-coordinator + restart: always + environment: + - LABGRID_MODE=coordinator + - LABGRID_COORDINATOR_LISTEN=:: + ports: + - "20408:20408" + volumes: + - ./config/labgrid:/etc/labgrid:ro + labels: + - "com.centurylinklabs.watchtower.enable=true" + + labgrid-exporter: + image: ghcr.io/openwrt/openwrt-tests/labgrid:latest + container_name: labgrid-exporter + restart: always + depends_on: + - labgrid-coordinator + environment: + - LABGRID_MODE=exporter + - LABGRID_CONFIG=/etc/labgrid/exporter.yaml + - LG_CROSSBAR=ws://labgrid-coordinator:20408/ws + network_mode: host + volumes: + - ./config/labgrid:/etc/labgrid:ro + - ./tftp:/srv/tftp:rw + - labgrid-cache:/var/cache/labgrid + devices: + - /dev/ttyUSB0:/dev/ttyUSB0 + - /dev/ttyUSB1:/dev/ttyUSB1 + - /dev/ttyUSB2:/dev/ttyUSB2 + - /dev/ttyUSB3:/dev/ttyUSB3 + - /dev/ttyAMA0:/dev/ttyAMA0 + group_add: + - dialout + privileged: true # Needed for USB serial access + labels: + - "com.centurylinklabs.watchtower.enable=true" + + pdudaemon: + image: ghcr.io/openwrt/openwrt-tests/pdudaemon:latest + container_name: pdudaemon + restart: always + network_mode: host + volumes: + - ./config/pdudaemon:/etc/pdudaemon:ro + labels: + - "com.centurylinklabs.watchtower.enable=true" + + dnsmasq: + image: ghcr.io/openwrt/openwrt-tests/dnsmasq:latest + container_name: dnsmasq + restart: always + network_mode: host + cap_add: + - NET_ADMIN + - NET_RAW + volumes: + - ./config/dnsmasq:/etc/dnsmasq.d:ro + - ./config/dnsmasq.conf:/etc/dnsmasq.conf:ro + - ./tftp:/srv/tftp:ro + labels: + - "com.centurylinklabs.watchtower.enable=true" + + # Auto-update containers daily + watchtower: + image: containrrr/watchtower:latest + container_name: watchtower + restart: always + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_SCHEDULE=0 0 4 * * * + - WATCHTOWER_LABEL_ENABLE=true + volumes: + - /var/run/docker.sock:/var/run/docker.sock + +volumes: + labgrid-cache: diff --git a/coreos/raspberry-pi/setup.sh b/coreos/raspberry-pi/setup.sh new file mode 100755 index 000000000..fe2dc09ed --- /dev/null +++ b/coreos/raspberry-pi/setup.sh @@ -0,0 +1,174 @@ +#!/bin/bash +# OpenWrt Test Lab - Raspberry Pi Setup Script +# Run on a fresh Raspberry Pi OS Lite installation +set -e + +echo "=== OpenWrt Test Lab Setup for Raspberry Pi ===" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "Please run as root: sudo $0" + exit 1 +fi + +# Detect architecture +ARCH=$(uname -m) +echo "Architecture: $ARCH" + +# Update system +echo ">>> Updating system..." +apt-get update +apt-get upgrade -y + +# Install Docker +echo ">>> Installing Docker..." +if ! command -v docker &> /dev/null; then + curl -fsSL https://get.docker.com | sh + usermod -aG docker pi 2>/dev/null || usermod -aG docker $SUDO_USER +fi + +# Install docker-compose plugin +echo ">>> Installing Docker Compose..." +apt-get install -y docker-compose-plugin + +# Install additional tools +echo ">>> Installing tools..." +apt-get install -y \ + git \ + socat \ + ser2net \ + vlan \ + iptables-persistent + +# Enable IP forwarding +echo ">>> Enabling IP forwarding..." +cat > /etc/sysctl.d/99-labgrid.conf << 'EOF' +net.ipv4.ip_forward = 1 +net.ipv6.conf.all.forwarding = 1 +EOF +sysctl -p /etc/sysctl.d/99-labgrid.conf + +# Load 8021q module for VLANs +echo ">>> Enabling VLAN support..." +modprobe 8021q +echo "8021q" >> /etc/modules + +# Create labgrid user +echo ">>> Creating labgrid user..." +if ! id labgrid &>/dev/null; then + useradd -m -s /bin/bash -G dialout,plugdev,docker labgrid +fi + +# Setup directory structure +echo ">>> Creating directory structure..." +LABDIR="/opt/labgrid" +mkdir -p $LABDIR/{config/labgrid,config/pdudaemon,config/dnsmasq,tftp} +chown -R labgrid:labgrid $LABDIR + +# Copy docker-compose if in same directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/docker-compose.yml" ]; then + cp "$SCRIPT_DIR/docker-compose.yml" $LABDIR/ +fi + +# Create default configs if they don't exist +if [ ! -f "$LABDIR/config/labgrid/exporter.yaml" ]; then + cat > $LABDIR/config/labgrid/exporter.yaml << 'EOF' +# Labgrid exporter configuration +# Add your devices here. Example: +# +# my-device: +# USBSerialPort: +# match: +# ID_PATH: platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1:1.0 +# speed: 115200 +# NetworkService: +# address: 192.168.101.1%vlan101 +# username: root +# PDUDaemonPort: +# host: localhost:16421 +# pdu: 192.168.128.2 +# index: 1 +EOF +fi + +if [ ! -f "$LABDIR/config/pdudaemon/pdudaemon.conf" ]; then + cat > $LABDIR/config/pdudaemon/pdudaemon.conf << 'EOF' +{ + "daemon": { + "hostname": "0.0.0.0", + "port": 16421, + "logging_level": "INFO" + }, + "pdus": { + } +} +EOF +fi + +if [ ! -f "$LABDIR/config/dnsmasq.conf" ]; then + cat > $LABDIR/config/dnsmasq.conf << 'EOF' +port=0 +enable-tftp +tftp-root=/srv/tftp +log-dhcp +conf-dir=/etc/dnsmasq.d/,*.conf +EOF +fi + +# Create systemd service +echo ">>> Creating systemd service..." +cat > /etc/systemd/system/labgrid.service << EOF +[Unit] +Description=OpenWrt Test Lab Services +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=$LABDIR +ExecStart=/usr/bin/docker compose up -d +ExecStop=/usr/bin/docker compose down +User=labgrid +Group=docker + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable labgrid.service + +# Create helper script for serial device discovery +cat > /usr/local/bin/labgrid-find-serial << 'EOF' +#!/bin/bash +# Find USB serial devices and their ID_PATH +echo "=== USB Serial Devices ===" +for dev in /dev/ttyUSB* /dev/ttyACM* 2>/dev/null; do + [ -e "$dev" ] || continue + echo "" + echo "Device: $dev" + udevadm info "$dev" | grep -E "(ID_PATH|ID_SERIAL|ID_VENDOR|ID_MODEL)=" +done +EOF +chmod +x /usr/local/bin/labgrid-find-serial + +echo "" +echo "=== Setup Complete ===" +echo "" +echo "Next steps:" +echo "1. Edit device configuration:" +echo " sudo nano $LABDIR/config/labgrid/exporter.yaml" +echo "" +echo "2. Find your serial devices:" +echo " labgrid-find-serial" +echo "" +echo "3. Start the services:" +echo " sudo systemctl start labgrid" +echo " # or: cd $LABDIR && docker compose up -d" +echo "" +echo "4. Check status:" +echo " docker ps" +echo " docker logs labgrid-exporter" +echo "" From 60d21a9af5455049535a435580a872059c24505f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 20:06:08 +0000 Subject: [PATCH 3/7] feat: add Fedora CoreOS flash script for Raspberry Pi Add flash-coreos.sh that: - Downloads Fedora CoreOS aarch64 image - Installs RPi4 UEFI firmware automatically - Generates Ignition from lab-config.yaml - Handles all setup in one command Update README to make CoreOS the primary approach for Raspberry Pi, with Docker/RPi OS as fallback option. --- coreos/raspberry-pi/README.md | 265 ++++++++++++---------------- coreos/raspberry-pi/flash-coreos.sh | 192 ++++++++++++++++++++ 2 files changed, 304 insertions(+), 153 deletions(-) create mode 100755 coreos/raspberry-pi/flash-coreos.sh diff --git a/coreos/raspberry-pi/README.md b/coreos/raspberry-pi/README.md index 57c7f2709..c2060f2e7 100644 --- a/coreos/raspberry-pi/README.md +++ b/coreos/raspberry-pi/README.md @@ -1,211 +1,170 @@ -# OpenWrt Test Lab - Raspberry Pi Setup +# OpenWrt Test Lab - Raspberry Pi -Docker-based labgrid setup for Raspberry Pi with automatic container updates. +Fedora CoreOS on Raspberry Pi 4/5 with auto-updating containers. -## Quick Start (Cloud-Init) - -The easiest way - automatic setup on first boot: +## Quick Start ```bash -# 1. Flash Raspberry Pi OS Lite (64-bit) to SD card -# Use Raspberry Pi Imager: https://www.raspberrypi.com/software/ - -# 2. Mount the boot partition and copy cloud-init files -cp cloud-init/user-data /media/$USER/bootfs/ -cp cloud-init/meta-data /media/$USER/bootfs/ +# 1. Create your lab config +cp ../lab-config.yaml.example ../lab-config.yaml +nano ../lab-config.yaml # Add SSH keys, devices, PDUs -# 3. Edit user-data to add your SSH keys (search for "ADD YOUR SSH KEYS") -nano /media/$USER/bootfs/user-data +# 2. Flash SD card +sudo ./flash-coreos.sh /dev/sdX -# 4. Unmount and boot the Pi -# First boot takes ~5-10 minutes to install Docker and pull images +# 3. Boot Pi, do one-time UEFI setup, done! ``` -## Quick Start (Manual) +## Requirements -```bash -# 1. Flash Raspberry Pi OS Lite and boot -# 2. SSH into the Pi and run: -curl -fsSL https://raw.githubusercontent.com/openwrt/openwrt-tests/main/coreos/raspberry-pi/setup.sh | sudo bash - -# 3. Configure your devices -sudo nano /opt/labgrid/config/labgrid/exporter.yaml +- Raspberry Pi 4 (4GB+) or Pi 5 +- SD card 32GB+ (or USB/NVMe storage) +- Monitor + keyboard (first boot only, for UEFI setup) -# 4. Start services -sudo systemctl start labgrid -``` +## Installation -## Finding Serial Device Paths +### 1. Configure Your Lab ```bash -# List all USB serial devices with their ID_PATH -labgrid-find-serial - -# Example output: -# Device: /dev/ttyUSB0 -# ID_PATH=platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0 -# ID_SERIAL=FTDI_FT232R_USB_UART_A50285BI +cd coreos/ +cp lab-config.yaml.example lab-config.yaml +nano lab-config.yaml ``` -Use the `ID_PATH` value in your `exporter.yaml`. - -## Configuration Files - -All config files are in `/opt/labgrid/config/`: - -``` -/opt/labgrid/ -├── docker-compose.yml -├── config/ -│ ├── labgrid/ -│ │ └── exporter.yaml # Device definitions -│ ├── pdudaemon/ -│ │ └── pdudaemon.conf # PDU configuration -│ └── dnsmasq/ -│ └── *.conf # VLAN DHCP configs -└── tftp/ # TFTP root for firmware +Minimum config: +```yaml +lab: + name: my-lab + hostname: labgrid-pi + +ssh_keys: + - ssh-ed25519 AAAA... your-key@hostname + +devices: + - name: my-router + serial: + id_path: platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1:1.0 + network: + vlan: 101 + power: + pdu: 192.168.128.2 + outlet: 1 ``` -### Example Device Configuration +### 2. Flash SD Card -Edit `/opt/labgrid/config/labgrid/exporter.yaml`: +```bash +# Find your SD card +lsblk -```yaml -openwrt-one: - USBSerialPort: - match: - # Get this from: labgrid-find-serial - ID_PATH: platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0 - speed: 115200 - NetworkService: - address: 192.168.101.1%vlan101 - username: root - PDUDaemonPort: - host: localhost:16421 - pdu: 192.168.128.2 - index: 1 - TFTPProvider: - internal: /srv/tftp/openwrt-one/ - external: openwrt-one/ +# Flash (downloads CoreOS + UEFI firmware) +sudo ./raspberry-pi/flash-coreos.sh /dev/sdX ``` -### PDU Configuration - -Edit `/opt/labgrid/config/pdudaemon/pdudaemon.conf`: - -```json -{ - "daemon": { - "hostname": "0.0.0.0", - "port": 16421, - "logging_level": "INFO" - }, - "pdus": { - "192.168.128.2": { - "driver": "ubus" - } - } -} -``` +### 3. First Boot - UEFI Setup (One Time) -## VLAN Setup +1. Insert SD card, connect monitor + keyboard +2. Power on, press **Esc** to enter UEFI +3. Go to: **Device Manager → Raspberry Pi Configuration → Advanced** +4. Set **Limit RAM to 3GB → Disabled** +5. Press **F10** to save, **Esc** to exit +6. Pi boots Fedora CoreOS + +### 4. Verify ```bash -# Create VLAN interface -sudo ip link add link eth0 name vlan101 type vlan id 101 -sudo ip addr add 192.168.101.1/24 dev vlan101 -sudo ip link set vlan101 up - -# Make persistent (add to /etc/network/interfaces.d/vlans) -echo "auto vlan101 -iface vlan101 inet static - address 192.168.101.1/24 - vlan-raw-device eth0" | sudo tee /etc/network/interfaces.d/vlans +ssh labgrid@ +sudo podman ps ``` -## Managing Services +## Finding Serial Paths -```bash -# Start/stop all services -sudo systemctl start labgrid -sudo systemctl stop labgrid +After boot, find your USB serial devices: -# Or use docker compose directly -cd /opt/labgrid -docker compose up -d -docker compose down +```bash +# List devices +ls /dev/ttyUSB* -# View logs -docker logs -f labgrid-exporter -docker logs -f labgrid-coordinator +# Get ID_PATH +udevadm info /dev/ttyUSB0 | grep ID_PATH= -# Restart a single service -docker compose restart labgrid-exporter +# Typical Pi 4 path: +# platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0 ``` ## Auto-Updates -Watchtower automatically updates containers daily at 4:00 AM: +| Component | Schedule | Method | +|-----------|----------|--------| +| Fedora CoreOS | Sundays 3am | Zincati (rpm-ostree) | +| Containers | Daily 4am | Podman auto-update | ```bash -# Check watchtower logs -docker logs watchtower +# Check OS updates +rpm-ostree status -# Force update now -docker exec watchtower /watchtower --run-once +# Check container updates +sudo podman auto-update --dry-run ``` -## Hardware Recommendations +## Configuration -### Raspberry Pi Models -- **Pi 5 (4GB+)**: Recommended - best USB and network performance -- **Pi 4 (4GB+)**: Works well -- **Pi 3**: Not recommended (limited USB bandwidth) +Configs are in `/etc/`: -### USB Hub -Use a powered USB hub for multiple serial adapters: -- Plugable 7-Port USB 3.0 Hub -- Anker 7-Port USB 3.0 Hub +``` +/etc/labgrid/exporter.yaml # Devices +/etc/pdudaemon/pdudaemon.conf # PDUs +/etc/dnsmasq.d/*.conf # DHCP +/srv/tftp/ # Firmware +``` -### Serial Adapters -- FTDI FT232R-based adapters (most reliable) -- CH340/CH341 adapters (budget option) +Edit and restart: +```bash +sudo nano /etc/labgrid/exporter.yaml +sudo systemctl restart labgrid-exporter +``` ## Troubleshooting -### Serial devices not accessible +### Won't boot past UEFI +- Disable 3GB RAM limit in UEFI settings +- `nomodeset` is added automatically by flash script +### No serial devices ```bash -# Check device permissions -ls -la /dev/ttyUSB* - -# Add user to dialout group -sudo usermod -aG dialout $USER +lsusb # Check USB +ls -la /dev/ttyUSB* # Check devices +sudo podman exec labgrid-exporter ls /dev/ +``` -# Restart containers -docker compose restart labgrid-exporter +### Container errors +```bash +sudo journalctl -u labgrid-exporter -f ``` -### Container won't start +## Alternative: Raspberry Pi OS + Docker + +If you prefer Raspberry Pi OS: ```bash -# Check logs -docker logs labgrid-exporter +# Use cloud-init (automatic) +cp cloud-init/user-data /media/$USER/bootfs/ +cp cloud-init/meta-data /media/$USER/bootfs/ +# Edit user-data, add SSH keys, boot -# Common issues: -# - Serial device doesn't exist: update devices in docker-compose.yml -# - Config error: check exporter.yaml syntax +# Or manual setup +sudo ./setup.sh ``` -### Network issues with VLANs +## Hardware -```bash -# Verify VLAN interface exists -ip -d link show vlan101 +**Recommended:** +- Raspberry Pi 5 (8GB) or Pi 4 (4GB+) +- Powered USB 3.0 hub +- FTDI-based serial adapters -# Check routing -ip route +## References -# Test connectivity -ping -I vlan101 192.168.101.100 -``` +- [Fedora CoreOS on RPi4 - Docs](https://docs.fedoraproject.org/es_419/fedora-coreos/provisioning-raspberry-pi4/) +- [RPi4 UEFI Firmware](https://github.com/pftf/RPi4) +- [RPi Forum Guide](https://forums.raspberrypi.com/viewtopic.php?t=381870) diff --git a/coreos/raspberry-pi/flash-coreos.sh b/coreos/raspberry-pi/flash-coreos.sh new file mode 100755 index 000000000..d3336ce27 --- /dev/null +++ b/coreos/raspberry-pi/flash-coreos.sh @@ -0,0 +1,192 @@ +#!/bin/bash +# Flash Fedora CoreOS to Raspberry Pi SD card +# Usage: ./flash-coreos.sh /dev/sdX [ignition-file] +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COREOS_DIR="$(dirname "$SCRIPT_DIR")" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +usage() { + echo "Usage: $0 [ignition-file]" + echo "" + echo "Flash Fedora CoreOS to SD card for Raspberry Pi 4/5" + echo "" + echo "Arguments:" + echo " device Target device (e.g., /dev/sdb, /dev/mmcblk0)" + echo " ignition-file Ignition config (default: generate from lab-config.yaml)" + echo "" + echo "Example:" + echo " $0 /dev/sdb" + echo " $0 /dev/sdb labnode.ign" +} + +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Please run as root: sudo $0 $@${NC}" + exit 1 +fi + +if [ $# -lt 1 ]; then + usage + exit 1 +fi + +DEVICE="$1" +IGNITION="${2:-}" + +# Verify device exists and is a block device +if [ ! -b "$DEVICE" ]; then + echo -e "${RED}Error: $DEVICE is not a block device${NC}" + exit 1 +fi + +# Safety check - don't flash system disk +if mount | grep -q "^$DEVICE"; then + echo -e "${RED}Error: $DEVICE appears to be mounted. Unmount first.${NC}" + exit 1 +fi + +# Confirm +echo -e "${YELLOW}WARNING: This will ERASE ALL DATA on $DEVICE${NC}" +lsblk "$DEVICE" +echo "" +read -p "Are you sure? Type 'yes' to continue: " confirm +if [ "$confirm" != "yes" ]; then + echo "Aborted." + exit 1 +fi + +# Generate ignition if not provided +if [ -z "$IGNITION" ]; then + if [ -f "$COREOS_DIR/lab-config.yaml" ]; then + echo -e "${GREEN}>>> Generating Ignition from lab-config.yaml${NC}" + IGNITION="/tmp/labnode-$$.ign" + + # Check for dependencies + if ! command -v python3 &> /dev/null; then + echo -e "${RED}Error: python3 required${NC}" + exit 1 + fi + + python3 -c "import yaml" 2>/dev/null || pip3 install --user pyyaml + + # Download butane if needed + if ! command -v butane &> /dev/null; then + echo "Downloading butane..." + curl -sLo /tmp/butane https://github.com/coreos/butane/releases/download/v0.21.0/butane-aarch64-unknown-linux-gnu + chmod +x /tmp/butane + BUTANE=/tmp/butane + else + BUTANE=butane + fi + + python3 "$COREOS_DIR/scripts/generate-butane.py" "$COREOS_DIR/lab-config.yaml" | \ + $BUTANE --strict > "$IGNITION" + + echo "Generated: $IGNITION" + else + echo -e "${RED}Error: No ignition file provided and no lab-config.yaml found${NC}" + echo "Create lab-config.yaml first: cp lab-config.yaml.example lab-config.yaml" + exit 1 + fi +fi + +if [ ! -f "$IGNITION" ]; then + echo -e "${RED}Error: Ignition file not found: $IGNITION${NC}" + exit 1 +fi + +# Check for coreos-installer +if ! command -v coreos-installer &> /dev/null; then + echo -e "${GREEN}>>> Installing coreos-installer${NC}" + + # Try package manager first + if command -v dnf &> /dev/null; then + dnf install -y coreos-installer + elif command -v apt-get &> /dev/null; then + # Use container for Debian/Ubuntu + echo "Using podman/docker to run coreos-installer..." + if command -v podman &> /dev/null; then + CONTAINER_CMD="podman" + elif command -v docker &> /dev/null; then + CONTAINER_CMD="docker" + else + echo -e "${RED}Error: Install podman or docker, or run on Fedora${NC}" + exit 1 + fi + + # Run coreos-installer via container + echo -e "${GREEN}>>> Flashing Fedora CoreOS (aarch64) to $DEVICE${NC}" + $CONTAINER_CMD run --rm --privileged \ + -v /dev:/dev \ + -v /run/udev:/run/udev \ + -v "$(dirname "$IGNITION"):/data:ro" \ + quay.io/coreos/coreos-installer:release \ + install "$DEVICE" \ + --architecture aarch64 \ + --ignition-file "/data/$(basename "$IGNITION")" \ + --append-karg nomodeset \ + --append-karg console=tty1 + + # Skip to firmware setup + INSTALLED_VIA_CONTAINER=1 + fi +fi + +if [ -z "$INSTALLED_VIA_CONTAINER" ]; then + echo -e "${GREEN}>>> Flashing Fedora CoreOS (aarch64) to $DEVICE${NC}" + coreos-installer install "$DEVICE" \ + --architecture aarch64 \ + --ignition-file "$IGNITION" \ + --append-karg nomodeset \ + --append-karg console=tty1 +fi + +# Setup UEFI firmware for Raspberry Pi +echo -e "${GREEN}>>> Installing Raspberry Pi UEFI firmware${NC}" + +# Find EFI partition +sleep 2 # Wait for kernel to re-read partition table +partprobe "$DEVICE" 2>/dev/null || true + +if [[ "$DEVICE" == *"mmcblk"* ]] || [[ "$DEVICE" == *"nvme"* ]]; then + EFI_PART="${DEVICE}p1" +else + EFI_PART="${DEVICE}1" +fi + +# Mount EFI partition +EFI_MOUNT="/tmp/efi-$$" +mkdir -p "$EFI_MOUNT" +mount "$EFI_PART" "$EFI_MOUNT" + +# Download latest RPi4 UEFI firmware +UEFI_VERSION="v1.39" +UEFI_URL="https://github.com/pftf/RPi4/releases/download/${UEFI_VERSION}/RPi4_UEFI_Firmware_${UEFI_VERSION}.zip" + +echo "Downloading UEFI firmware ${UEFI_VERSION}..." +curl -sL "$UEFI_URL" -o /tmp/rpi-uefi.zip +unzip -o /tmp/rpi-uefi.zip -d "$EFI_MOUNT/" + +# Cleanup +umount "$EFI_MOUNT" +rmdir "$EFI_MOUNT" +rm /tmp/rpi-uefi.zip + +echo "" +echo -e "${GREEN}=== Flash Complete ===${NC}" +echo "" +echo "Next steps:" +echo "1. Insert SD card into Raspberry Pi 4/5" +echo "2. Connect monitor and keyboard for first boot (UEFI setup)" +echo "3. In UEFI menu: Device Manager → Raspberry Pi Configuration" +echo " - Advanced Configuration → Limit RAM to 3GB → Disabled" +echo " - (For Pi 5: may need additional settings)" +echo "4. Save and exit UEFI, system will boot Fedora CoreOS" +echo "" +echo "SSH access: ssh labgrid@" From f9505b3311ff3ae7ca27dd43aa42054d389e3a2b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 20:13:20 +0000 Subject: [PATCH 4/7] refactor: simplify Raspberry Pi CoreOS flashing - Add flash-sd.sh: simpler script that just flashes + adds ignition - Update README with multiple methods (coreos-installer, script, manual) - Add -o flag to build-ignition.sh for output file - Remove overcomplicated flash-coreos.sh The key insight: just flash the official image and drop ignition file into /mnt/ignition/config.ign - no complex tooling needed. --- coreos/raspberry-pi/README.md | 209 +++++++++++++--------------- coreos/raspberry-pi/flash-coreos.sh | 192 ------------------------- coreos/raspberry-pi/flash-sd.sh | 156 +++++++++++++++++++++ coreos/scripts/build-ignition.sh | 36 +++-- 4 files changed, 278 insertions(+), 315 deletions(-) delete mode 100755 coreos/raspberry-pi/flash-coreos.sh create mode 100755 coreos/raspberry-pi/flash-sd.sh diff --git a/coreos/raspberry-pi/README.md b/coreos/raspberry-pi/README.md index c2060f2e7..99c10e7dd 100644 --- a/coreos/raspberry-pi/README.md +++ b/coreos/raspberry-pi/README.md @@ -1,170 +1,153 @@ # OpenWrt Test Lab - Raspberry Pi -Fedora CoreOS on Raspberry Pi 4/5 with auto-updating containers. +Fedora CoreOS on Raspberry Pi with safe, atomic auto-updates. -## Quick Start +## Quickest Method (from any Linux) ```bash -# 1. Create your lab config +# Install coreos-installer +sudo dnf install coreos-installer # Fedora +# or: cargo install coreos-installer + +# Create your config cp ../lab-config.yaml.example ../lab-config.yaml -nano ../lab-config.yaml # Add SSH keys, devices, PDUs +nano ../lab-config.yaml # Add SSH keys, devices + +# Generate ignition +../scripts/build-ignition.sh ../lab-config.yaml -o config.ign -# 2. Flash SD card -sudo ./flash-coreos.sh /dev/sdX +# Flash SD card (downloads CoreOS automatically) +sudo coreos-installer install /dev/sdX \ + --architecture aarch64 \ + --ignition-file config.ign \ + --append-karg nomodeset -# 3. Boot Pi, do one-time UEFI setup, done! +# Then add UEFI firmware (see below) ``` -## Requirements +## Simple Method (script does everything) + +```bash +# Configure your lab +cp ../lab-config.yaml.example ../lab-config.yaml +nano ../lab-config.yaml -- Raspberry Pi 4 (4GB+) or Pi 5 -- SD card 32GB+ (or USB/NVMe storage) -- Monitor + keyboard (first boot only, for UEFI setup) +# Flash (downloads CoreOS + UEFI firmware) +sudo ./flash-sd.sh /dev/sdX +``` -## Installation +## Manual Method (most control) -### 1. Configure Your Lab +### 1. Download and flash CoreOS image ```bash -cd coreos/ -cp lab-config.yaml.example lab-config.yaml -nano lab-config.yaml -``` - -Minimum config: -```yaml -lab: - name: my-lab - hostname: labgrid-pi +# Download latest stable aarch64 image +curl -LO https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/.../fedora-coreos-...-metal.aarch64.raw.xz -ssh_keys: - - ssh-ed25519 AAAA... your-key@hostname - -devices: - - name: my-router - serial: - id_path: platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1:1.0 - network: - vlan: 101 - power: - pdu: 192.168.128.2 - outlet: 1 +# Flash to SD card +xzcat fedora-coreos-*.raw.xz | sudo dd of=/dev/sdX bs=4M status=progress ``` -### 2. Flash SD Card +### 2. Add ignition config ```bash -# Find your SD card -lsblk +# Mount boot partition (partition 2 on CoreOS) +sudo mount /dev/sdX2 /mnt -# Flash (downloads CoreOS + UEFI firmware) -sudo ./raspberry-pi/flash-coreos.sh /dev/sdX +# Copy your ignition file +sudo mkdir -p /mnt/ignition +sudo cp config.ign /mnt/ignition/config.ign + +sudo umount /mnt ``` -### 3. First Boot - UEFI Setup (One Time) +### 3. Add Raspberry Pi UEFI firmware -1. Insert SD card, connect monitor + keyboard -2. Power on, press **Esc** to enter UEFI -3. Go to: **Device Manager → Raspberry Pi Configuration → Advanced** -4. Set **Limit RAM to 3GB → Disabled** -5. Press **F10** to save, **Esc** to exit -6. Pi boots Fedora CoreOS +```bash +# Mount EFI partition (partition 1) +sudo mount /dev/sdX1 /mnt -### 4. Verify +# Download and extract UEFI firmware +curl -LO https://github.com/pftf/RPi4/releases/download/v1.39/RPi4_UEFI_Firmware_v1.39.zip +sudo unzip RPi4_UEFI_Firmware_v1.39.zip -d /mnt/ -```bash -ssh labgrid@ -sudo podman ps +sudo umount /mnt ``` -## Finding Serial Paths +### 4. First boot UEFI setup + +1. Connect monitor + keyboard +2. Power on, press **Esc** for UEFI +3. **Device Manager → Raspberry Pi Configuration → Advanced** +4. **Limit RAM to 3GB → Disabled** +5. **F10** save, **Esc** exit -After boot, find your USB serial devices: +## Applying Config Changes Later + +Ignition only runs on **first boot**. To change config later: ```bash -# List devices -ls /dev/ttyUSB* +# SSH into the Pi +ssh labgrid@ -# Get ID_PATH -udevadm info /dev/ttyUSB0 | grep ID_PATH= +# Edit configs directly +sudo nano /etc/labgrid/exporter.yaml +sudo systemctl restart labgrid-exporter -# Typical Pi 4 path: -# platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0 +# Or use Ansible from your workstation +ansible-playbook -i inventory playbook.yml --limit my-pi ``` ## Auto-Updates -| Component | Schedule | Method | -|-----------|----------|--------| -| Fedora CoreOS | Sundays 3am | Zincati (rpm-ostree) | -| Containers | Daily 4am | Podman auto-update | +| Component | Method | Schedule | Rollback | +|-----------|--------|----------|----------| +| **Fedora CoreOS** | Zincati + rpm-ostree | Sundays 3am | Automatic | +| **Containers** | Podman auto-update | Daily 4am | Manual | ```bash -# Check OS updates +# Check OS update status rpm-ostree status +# Force OS update now +sudo rpm-ostree upgrade + # Check container updates sudo podman auto-update --dry-run ``` -## Configuration +### Why this is safe -Configs are in `/etc/`: +- **A/B partitions**: OS updates install to inactive partition +- **Auto-rollback**: If new OS fails to boot 3 times → reverts +- **Staged updates**: Zincati coordinates timing across fleet +- **No apt/dnf**: Can't accidentally break the system -``` -/etc/labgrid/exporter.yaml # Devices -/etc/pdudaemon/pdudaemon.conf # PDUs -/etc/dnsmasq.d/*.conf # DHCP -/srv/tftp/ # Firmware -``` +## Minimal lab-config.yaml -Edit and restart: -```bash -sudo nano /etc/labgrid/exporter.yaml -sudo systemctl restart labgrid-exporter -``` - -## Troubleshooting +```yaml +lab: + hostname: labgrid-pi -### Won't boot past UEFI -- Disable 3GB RAM limit in UEFI settings -- `nomodeset` is added automatically by flash script +ssh_keys: + - ssh-ed25519 AAAA... you@host -### No serial devices -```bash -lsusb # Check USB -ls -la /dev/ttyUSB* # Check devices -sudo podman exec labgrid-exporter ls /dev/ +# Add devices after first boot (easier to find serial paths) +devices: [] ``` -### Container errors -```bash -sudo journalctl -u labgrid-exporter -f -``` +## Troubleshooting -## Alternative: Raspberry Pi OS + Docker +### Won't boot +- Did you disable 3GB RAM limit in UEFI? +- Check ignition syntax: `butane --strict config.bu` -If you prefer Raspberry Pi OS: +### Can't SSH +- Default user is `core` if no lab-config.yaml +- Check ignition was placed in `/mnt/ignition/config.ign` +### Find serial device paths ```bash -# Use cloud-init (automatic) -cp cloud-init/user-data /media/$USER/bootfs/ -cp cloud-init/meta-data /media/$USER/bootfs/ -# Edit user-data, add SSH keys, boot - -# Or manual setup -sudo ./setup.sh +ls /dev/ttyUSB* +udevadm info /dev/ttyUSB0 | grep ID_PATH ``` - -## Hardware - -**Recommended:** -- Raspberry Pi 5 (8GB) or Pi 4 (4GB+) -- Powered USB 3.0 hub -- FTDI-based serial adapters - -## References - -- [Fedora CoreOS on RPi4 - Docs](https://docs.fedoraproject.org/es_419/fedora-coreos/provisioning-raspberry-pi4/) -- [RPi4 UEFI Firmware](https://github.com/pftf/RPi4) -- [RPi Forum Guide](https://forums.raspberrypi.com/viewtopic.php?t=381870) diff --git a/coreos/raspberry-pi/flash-coreos.sh b/coreos/raspberry-pi/flash-coreos.sh deleted file mode 100755 index d3336ce27..000000000 --- a/coreos/raspberry-pi/flash-coreos.sh +++ /dev/null @@ -1,192 +0,0 @@ -#!/bin/bash -# Flash Fedora CoreOS to Raspberry Pi SD card -# Usage: ./flash-coreos.sh /dev/sdX [ignition-file] -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -COREOS_DIR="$(dirname "$SCRIPT_DIR")" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -usage() { - echo "Usage: $0 [ignition-file]" - echo "" - echo "Flash Fedora CoreOS to SD card for Raspberry Pi 4/5" - echo "" - echo "Arguments:" - echo " device Target device (e.g., /dev/sdb, /dev/mmcblk0)" - echo " ignition-file Ignition config (default: generate from lab-config.yaml)" - echo "" - echo "Example:" - echo " $0 /dev/sdb" - echo " $0 /dev/sdb labnode.ign" -} - -if [ "$EUID" -ne 0 ]; then - echo -e "${RED}Please run as root: sudo $0 $@${NC}" - exit 1 -fi - -if [ $# -lt 1 ]; then - usage - exit 1 -fi - -DEVICE="$1" -IGNITION="${2:-}" - -# Verify device exists and is a block device -if [ ! -b "$DEVICE" ]; then - echo -e "${RED}Error: $DEVICE is not a block device${NC}" - exit 1 -fi - -# Safety check - don't flash system disk -if mount | grep -q "^$DEVICE"; then - echo -e "${RED}Error: $DEVICE appears to be mounted. Unmount first.${NC}" - exit 1 -fi - -# Confirm -echo -e "${YELLOW}WARNING: This will ERASE ALL DATA on $DEVICE${NC}" -lsblk "$DEVICE" -echo "" -read -p "Are you sure? Type 'yes' to continue: " confirm -if [ "$confirm" != "yes" ]; then - echo "Aborted." - exit 1 -fi - -# Generate ignition if not provided -if [ -z "$IGNITION" ]; then - if [ -f "$COREOS_DIR/lab-config.yaml" ]; then - echo -e "${GREEN}>>> Generating Ignition from lab-config.yaml${NC}" - IGNITION="/tmp/labnode-$$.ign" - - # Check for dependencies - if ! command -v python3 &> /dev/null; then - echo -e "${RED}Error: python3 required${NC}" - exit 1 - fi - - python3 -c "import yaml" 2>/dev/null || pip3 install --user pyyaml - - # Download butane if needed - if ! command -v butane &> /dev/null; then - echo "Downloading butane..." - curl -sLo /tmp/butane https://github.com/coreos/butane/releases/download/v0.21.0/butane-aarch64-unknown-linux-gnu - chmod +x /tmp/butane - BUTANE=/tmp/butane - else - BUTANE=butane - fi - - python3 "$COREOS_DIR/scripts/generate-butane.py" "$COREOS_DIR/lab-config.yaml" | \ - $BUTANE --strict > "$IGNITION" - - echo "Generated: $IGNITION" - else - echo -e "${RED}Error: No ignition file provided and no lab-config.yaml found${NC}" - echo "Create lab-config.yaml first: cp lab-config.yaml.example lab-config.yaml" - exit 1 - fi -fi - -if [ ! -f "$IGNITION" ]; then - echo -e "${RED}Error: Ignition file not found: $IGNITION${NC}" - exit 1 -fi - -# Check for coreos-installer -if ! command -v coreos-installer &> /dev/null; then - echo -e "${GREEN}>>> Installing coreos-installer${NC}" - - # Try package manager first - if command -v dnf &> /dev/null; then - dnf install -y coreos-installer - elif command -v apt-get &> /dev/null; then - # Use container for Debian/Ubuntu - echo "Using podman/docker to run coreos-installer..." - if command -v podman &> /dev/null; then - CONTAINER_CMD="podman" - elif command -v docker &> /dev/null; then - CONTAINER_CMD="docker" - else - echo -e "${RED}Error: Install podman or docker, or run on Fedora${NC}" - exit 1 - fi - - # Run coreos-installer via container - echo -e "${GREEN}>>> Flashing Fedora CoreOS (aarch64) to $DEVICE${NC}" - $CONTAINER_CMD run --rm --privileged \ - -v /dev:/dev \ - -v /run/udev:/run/udev \ - -v "$(dirname "$IGNITION"):/data:ro" \ - quay.io/coreos/coreos-installer:release \ - install "$DEVICE" \ - --architecture aarch64 \ - --ignition-file "/data/$(basename "$IGNITION")" \ - --append-karg nomodeset \ - --append-karg console=tty1 - - # Skip to firmware setup - INSTALLED_VIA_CONTAINER=1 - fi -fi - -if [ -z "$INSTALLED_VIA_CONTAINER" ]; then - echo -e "${GREEN}>>> Flashing Fedora CoreOS (aarch64) to $DEVICE${NC}" - coreos-installer install "$DEVICE" \ - --architecture aarch64 \ - --ignition-file "$IGNITION" \ - --append-karg nomodeset \ - --append-karg console=tty1 -fi - -# Setup UEFI firmware for Raspberry Pi -echo -e "${GREEN}>>> Installing Raspberry Pi UEFI firmware${NC}" - -# Find EFI partition -sleep 2 # Wait for kernel to re-read partition table -partprobe "$DEVICE" 2>/dev/null || true - -if [[ "$DEVICE" == *"mmcblk"* ]] || [[ "$DEVICE" == *"nvme"* ]]; then - EFI_PART="${DEVICE}p1" -else - EFI_PART="${DEVICE}1" -fi - -# Mount EFI partition -EFI_MOUNT="/tmp/efi-$$" -mkdir -p "$EFI_MOUNT" -mount "$EFI_PART" "$EFI_MOUNT" - -# Download latest RPi4 UEFI firmware -UEFI_VERSION="v1.39" -UEFI_URL="https://github.com/pftf/RPi4/releases/download/${UEFI_VERSION}/RPi4_UEFI_Firmware_${UEFI_VERSION}.zip" - -echo "Downloading UEFI firmware ${UEFI_VERSION}..." -curl -sL "$UEFI_URL" -o /tmp/rpi-uefi.zip -unzip -o /tmp/rpi-uefi.zip -d "$EFI_MOUNT/" - -# Cleanup -umount "$EFI_MOUNT" -rmdir "$EFI_MOUNT" -rm /tmp/rpi-uefi.zip - -echo "" -echo -e "${GREEN}=== Flash Complete ===${NC}" -echo "" -echo "Next steps:" -echo "1. Insert SD card into Raspberry Pi 4/5" -echo "2. Connect monitor and keyboard for first boot (UEFI setup)" -echo "3. In UEFI menu: Device Manager → Raspberry Pi Configuration" -echo " - Advanced Configuration → Limit RAM to 3GB → Disabled" -echo " - (For Pi 5: may need additional settings)" -echo "4. Save and exit UEFI, system will boot Fedora CoreOS" -echo "" -echo "SSH access: ssh labgrid@" diff --git a/coreos/raspberry-pi/flash-sd.sh b/coreos/raspberry-pi/flash-sd.sh new file mode 100755 index 000000000..1e84f9cdb --- /dev/null +++ b/coreos/raspberry-pi/flash-sd.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# Simple Fedora CoreOS flash for Raspberry Pi +# Usage: ./flash-sd.sh /dev/sdX [lab-config.yaml] +set -e + +DEVICE="${1:-}" +CONFIG="${2:-../lab-config.yaml}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +if [ -z "$DEVICE" ]; then + echo "Usage: $0 /dev/sdX [lab-config.yaml]" + echo "" + echo "Steps this script performs:" + echo " 1. Downloads Fedora CoreOS aarch64 image" + echo " 2. Flashes to SD card" + echo " 3. Adds your ignition config" + echo " 4. Adds Raspberry Pi UEFI firmware" + exit 1 +fi + +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Run as root: sudo $0 $@${NC}" + exit 1 +fi + +if [ ! -b "$DEVICE" ]; then + echo -e "${RED}$DEVICE is not a block device${NC}" + exit 1 +fi + +# Safety check +echo -e "${RED}WARNING: This will ERASE $DEVICE${NC}" +lsblk "$DEVICE" +read -p "Type 'yes' to continue: " confirm +[ "$confirm" = "yes" ] || exit 1 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="/tmp/coreos-flash-$$" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +# 1. Download Fedora CoreOS +STREAM="stable" +ARCH="aarch64" +echo -e "${GREEN}>>> Downloading Fedora CoreOS ($STREAM, $ARCH)...${NC}" + +# Get latest image URL from stream metadata +META_URL="https://builds.coreos.fedoraproject.org/streams/${STREAM}.json" +IMAGE_URL=$(curl -sL "$META_URL" | python3 -c " +import sys, json +d = json.load(sys.stdin) +print(d['architectures']['$ARCH']['artifacts']['metal']['formats']['raw.xz']['disk']['location']) +") + +IMAGE_FILE="fcos.raw.xz" +if [ ! -f "$IMAGE_FILE" ]; then + curl -L "$IMAGE_URL" -o "$IMAGE_FILE" +fi + +# 2. Flash to SD card +echo -e "${GREEN}>>> Flashing to $DEVICE...${NC}" +xzcat "$IMAGE_FILE" | dd of="$DEVICE" bs=4M status=progress conv=fsync + +# Wait for partitions +sleep 2 +partprobe "$DEVICE" 2>/dev/null || true +sleep 2 + +# Find partitions +if [[ "$DEVICE" == *"mmcblk"* ]] || [[ "$DEVICE" == *"nvme"* ]]; then + EFI_PART="${DEVICE}p1" + BOOT_PART="${DEVICE}p2" +else + EFI_PART="${DEVICE}1" + BOOT_PART="${DEVICE}2" +fi + +# 3. Add Ignition config +echo -e "${GREEN}>>> Adding Ignition configuration...${NC}" + +BOOT_MOUNT="$WORK_DIR/boot" +mkdir -p "$BOOT_MOUNT" +mount "$BOOT_PART" "$BOOT_MOUNT" + +mkdir -p "$BOOT_MOUNT/ignition" + +# Generate ignition from lab-config.yaml if it exists +if [ -f "$SCRIPT_DIR/$CONFIG" ] || [ -f "$CONFIG" ]; then + CONFIG_PATH="$CONFIG" + [ -f "$SCRIPT_DIR/$CONFIG" ] && CONFIG_PATH="$SCRIPT_DIR/$CONFIG" + + echo "Generating ignition from: $CONFIG_PATH" + + # Ensure dependencies + python3 -c "import yaml" 2>/dev/null || pip3 install --quiet pyyaml + + # Get butane + if ! command -v butane &>/dev/null; then + curl -sLo /tmp/butane https://github.com/coreos/butane/releases/download/v0.21.0/butane-x86_64-unknown-linux-gnu + chmod +x /tmp/butane + BUTANE=/tmp/butane + else + BUTANE=butane + fi + + python3 "$SCRIPT_DIR/../scripts/generate-butane.py" "$CONFIG_PATH" | \ + $BUTANE --strict > "$BOOT_MOUNT/ignition/config.ign" +else + echo -e "${RED}No config found. Creating minimal ignition...${NC}" + echo "You'll need to add SSH keys manually!" + + cat > "$BOOT_MOUNT/ignition/config.ign" << 'EOF' +{ + "ignition": { "version": "3.4.0" }, + "passwd": { + "users": [{ + "name": "core", + "groups": ["wheel", "sudo", "dialout"] + }] + } +} +EOF +fi + +umount "$BOOT_MOUNT" + +# 4. Add RPi UEFI firmware +echo -e "${GREEN}>>> Adding Raspberry Pi UEFI firmware...${NC}" + +EFI_MOUNT="$WORK_DIR/efi" +mkdir -p "$EFI_MOUNT" +mount "$EFI_PART" "$EFI_MOUNT" + +UEFI_VERSION="v1.39" +curl -sL "https://github.com/pftf/RPi4/releases/download/${UEFI_VERSION}/RPi4_UEFI_Firmware_${UEFI_VERSION}.zip" -o uefi.zip +unzip -o uefi.zip -d "$EFI_MOUNT/" + +umount "$EFI_MOUNT" + +# Cleanup +cd / +rm -rf "$WORK_DIR" + +echo "" +echo -e "${GREEN}=== Done! ===${NC}" +echo "" +echo "Next steps:" +echo "1. Insert SD card into Raspberry Pi 4/5" +echo "2. First boot: Press Esc for UEFI setup" +echo " → Device Manager → Raspberry Pi Configuration → Advanced" +echo " → Limit RAM to 3GB → Disabled" +echo " → F10 to save, Esc to exit" +echo "3. SSH: ssh core@ (or labgrid@ if you used lab-config.yaml)" diff --git a/coreos/scripts/build-ignition.sh b/coreos/scripts/build-ignition.sh index 8087e8959..c82509814 100755 --- a/coreos/scripts/build-ignition.sh +++ b/coreos/scripts/build-ignition.sh @@ -1,33 +1,49 @@ #!/bin/bash # Build Ignition configuration from simple lab config -# Usage: ./build-ignition.sh lab-config.yaml [output.ign] +# Usage: ./build-ignition.sh lab-config.yaml [-o output.ign] set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" COREOS_DIR="$(dirname "$SCRIPT_DIR")" usage() { - echo "Usage: $0 [output.ign]" + echo "Usage: $0 [-o output.ign]" echo "" echo "Generate Ignition configuration from lab config file." echo "" - echo "Arguments:" - echo " lab-config.yaml Lab configuration (see lab-config.yaml.example)" - echo " output.ign Output file (default: labnode.ign)" + echo "Options:" + echo " -o FILE Output file (default: labnode.ign)" echo "" echo "Example:" echo " $0 lab-config.yaml" - echo " $0 my-lab.yaml my-lab.ign" + echo " $0 lab-config.yaml -o config.ign" } -if [ $# -lt 1 ]; then +CONFIG="" +OUTPUT="labnode.ign" + +while [[ $# -gt 0 ]]; do + case $1 in + -o|--output) + OUTPUT="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + CONFIG="$1" + shift + ;; + esac +done + +if [ -z "$CONFIG" ]; then usage exit 1 fi -CONFIG="$1" -OUTPUT="${2:-labnode.ign}" - if [ ! -f "$CONFIG" ]; then echo "Error: Config file not found: $CONFIG" exit 1 From 2bf65c7389cdffc51af623c5c47f583f4cfec99e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 20:17:50 +0000 Subject: [PATCH 5/7] refactor: use podman container for coreos-installer No need to install coreos-installer - run it via container: podman run quay.io/coreos/coreos-installer:release install ... flash-sd.sh now: - Uses podman (or docker) to run coreos-installer - Downloads CoreOS image automatically - Adds UEFI firmware for Raspberry Pi - Only requires: podman + unzip + curl --- coreos/README.md | 44 +++----- coreos/raspberry-pi/README.md | 31 ++--- coreos/raspberry-pi/flash-sd.sh | 194 +++++++++++++++----------------- 3 files changed, 115 insertions(+), 154 deletions(-) diff --git a/coreos/README.md b/coreos/README.md index 1500260e4..7620482a2 100644 --- a/coreos/README.md +++ b/coreos/README.md @@ -2,43 +2,33 @@ Self-contained, auto-updating lab node setup for remote OpenWrt test labs with containerized services. -## Platform Options - -| Platform | Best For | Auto-Updates | -|----------|----------|--------------| -| **[Raspberry Pi](raspberry-pi/)** | Most labs, easy setup | Watchtower (containers) | -| [Fedora CoreOS](#fedora-coreos) | x86 servers, immutable OS | Zincati (OS) + Podman | - -## Quick Start - Raspberry Pi (Recommended) +## Quick Start ```bash -# 1. Flash Raspberry Pi OS Lite (64-bit) to SD card +# 1. Create your lab config +cp lab-config.yaml.example lab-config.yaml +nano lab-config.yaml # Add SSH keys, devices -# 2. Copy cloud-init files to boot partition -cp raspberry-pi/cloud-init/user-data /media/$USER/bootfs/ -cp raspberry-pi/cloud-init/meta-data /media/$USER/bootfs/ +# 2. Generate ignition file +./scripts/build-ignition.sh lab-config.yaml -o config.ign -# 3. Edit user-data - add your SSH keys -nano /media/$USER/bootfs/user-data +# 3. Flash SD card (Raspberry Pi) - uses podman container +sudo ./raspberry-pi/flash-sd.sh /dev/sdX config.ign -# 4. Boot the Pi - auto-configures in ~5-10 minutes +# 4. Boot Pi, configure UEFI once, done! ``` -See [raspberry-pi/README.md](raspberry-pi/README.md) for detailed instructions. +Only requires **podman** (or docker) - no other tools to install. -## Quick Start - Fedora CoreOS +See [raspberry-pi/README.md](raspberry-pi/README.md) for details. -```bash -# 1. Copy and edit the example config -cp lab-config.yaml.example lab-config.yaml -vim lab-config.yaml # Add your SSH keys, devices, etc. - -# 2. Generate Ignition file -./scripts/build-ignition.sh lab-config.yaml +## x86 Servers -# 3. Install Fedora CoreOS -# Download from: https://fedoraproject.org/coreos/download -sudo coreos-installer install /dev/sdX --ignition-file labnode.ign +```bash +# Use coreos-installer directly (or via container) +podman run --rm --privileged -v /dev:/dev -v .:/data:ro \ + quay.io/coreos/coreos-installer:release \ + install /dev/sdX --ignition-file /data/config.ign ``` ## Architecture diff --git a/coreos/raspberry-pi/README.md b/coreos/raspberry-pi/README.md index 99c10e7dd..ba7f04c50 100644 --- a/coreos/raspberry-pi/README.md +++ b/coreos/raspberry-pi/README.md @@ -2,37 +2,26 @@ Fedora CoreOS on Raspberry Pi with safe, atomic auto-updates. -## Quickest Method (from any Linux) +## Quick Start ```bash -# Install coreos-installer -sudo dnf install coreos-installer # Fedora -# or: cargo install coreos-installer - -# Create your config +# 1. Create config (optional but recommended) cp ../lab-config.yaml.example ../lab-config.yaml -nano ../lab-config.yaml # Add SSH keys, devices +nano ../lab-config.yaml # Add SSH keys -# Generate ignition +# 2. Generate ignition ../scripts/build-ignition.sh ../lab-config.yaml -o config.ign -# Flash SD card (downloads CoreOS automatically) -sudo coreos-installer install /dev/sdX \ - --architecture aarch64 \ - --ignition-file config.ign \ - --append-karg nomodeset - -# Then add UEFI firmware (see below) +# 3. Flash SD card (uses podman, no install needed) +sudo ./flash-sd.sh /dev/sdX config.ign ``` -## Simple Method (script does everything) +Only requires **podman** (or docker) - coreos-installer runs in a container. -```bash -# Configure your lab -cp ../lab-config.yaml.example ../lab-config.yaml -nano ../lab-config.yaml +## Without Config (Minimal Install) -# Flash (downloads CoreOS + UEFI firmware) +```bash +# Just flash, add SSH keys later sudo ./flash-sd.sh /dev/sdX ``` diff --git a/coreos/raspberry-pi/flash-sd.sh b/coreos/raspberry-pi/flash-sd.sh index 1e84f9cdb..5c49f9555 100755 --- a/coreos/raspberry-pi/flash-sd.sh +++ b/coreos/raspberry-pi/flash-sd.sh @@ -1,23 +1,33 @@ #!/bin/bash -# Simple Fedora CoreOS flash for Raspberry Pi -# Usage: ./flash-sd.sh /dev/sdX [lab-config.yaml] +# Flash Fedora CoreOS to SD card for Raspberry Pi using podman +# Usage: ./flash-sd.sh /dev/sdX [config.ign] set -e DEVICE="${1:-}" -CONFIG="${2:-../lab-config.yaml}" +IGNITION="${2:-}" RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' -if [ -z "$DEVICE" ]; then - echo "Usage: $0 /dev/sdX [lab-config.yaml]" +usage() { + echo "Usage: $0 /dev/sdX [config.ign]" + echo "" + echo "Flash Fedora CoreOS to SD card for Raspberry Pi 4/5" + echo "" + echo "Arguments:" + echo " /dev/sdX Target device (SD card)" + echo " config.ign Ignition file (optional, generate with build-ignition.sh)" echo "" - echo "Steps this script performs:" - echo " 1. Downloads Fedora CoreOS aarch64 image" - echo " 2. Flashes to SD card" - echo " 3. Adds your ignition config" - echo " 4. Adds Raspberry Pi UEFI firmware" + echo "Examples:" + echo " $0 /dev/sdb # Minimal install" + echo " $0 /dev/sdb config.ign # With ignition config" + echo "" + echo "Requires: podman or docker" +} + +if [ -z "$DEVICE" ]; then + usage exit 1 fi @@ -31,126 +41,98 @@ if [ ! -b "$DEVICE" ]; then exit 1 fi +# Find container runtime +if command -v podman &>/dev/null; then + CONTAINER_CMD="podman" +elif command -v docker &>/dev/null; then + CONTAINER_CMD="docker" +else + echo -e "${RED}Error: podman or docker required${NC}" + echo "Install with: sudo dnf install podman" + exit 1 +fi + # Safety check -echo -e "${RED}WARNING: This will ERASE $DEVICE${NC}" +echo -e "${RED}WARNING: This will ERASE ALL DATA on $DEVICE${NC}" lsblk "$DEVICE" +echo "" read -p "Type 'yes' to continue: " confirm [ "$confirm" = "yes" ] || exit 1 -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORK_DIR="/tmp/coreos-flash-$$" -mkdir -p "$WORK_DIR" -cd "$WORK_DIR" - -# 1. Download Fedora CoreOS -STREAM="stable" -ARCH="aarch64" -echo -e "${GREEN}>>> Downloading Fedora CoreOS ($STREAM, $ARCH)...${NC}" - -# Get latest image URL from stream metadata -META_URL="https://builds.coreos.fedoraproject.org/streams/${STREAM}.json" -IMAGE_URL=$(curl -sL "$META_URL" | python3 -c " -import sys, json -d = json.load(sys.stdin) -print(d['architectures']['$ARCH']['artifacts']['metal']['formats']['raw.xz']['disk']['location']) -") - -IMAGE_FILE="fcos.raw.xz" -if [ ! -f "$IMAGE_FILE" ]; then - curl -L "$IMAGE_URL" -o "$IMAGE_FILE" +# Build coreos-installer arguments +INSTALLER_ARGS=( + install "$DEVICE" + --architecture aarch64 + --append-karg nomodeset + --append-karg console=tty1 +) + +# Add ignition if provided +if [ -n "$IGNITION" ]; then + if [ ! -f "$IGNITION" ]; then + echo -e "${RED}Ignition file not found: $IGNITION${NC}" + exit 1 + fi + IGNITION_ABS=$(realpath "$IGNITION") + IGNITION_DIR=$(dirname "$IGNITION_ABS") + IGNITION_FILE=$(basename "$IGNITION_ABS") + INSTALLER_ARGS+=(--ignition-file "/data/$IGNITION_FILE") + VOLUME_ARGS=(-v "$IGNITION_DIR:/data:ro") +else + VOLUME_ARGS=() + echo -e "${GREEN}No ignition file provided - will create minimal install${NC}" + echo "You can add ignition later to /mnt/ignition/config.ign" fi -# 2. Flash to SD card -echo -e "${GREEN}>>> Flashing to $DEVICE...${NC}" -xzcat "$IMAGE_FILE" | dd of="$DEVICE" bs=4M status=progress conv=fsync +# Flash using coreos-installer container +echo -e "${GREEN}>>> Flashing Fedora CoreOS (aarch64) to $DEVICE...${NC}" +echo "Using: $CONTAINER_CMD run quay.io/coreos/coreos-installer:release" +echo "" + +$CONTAINER_CMD run --rm --privileged \ + -v /dev:/dev \ + -v /run/udev:/run/udev \ + "${VOLUME_ARGS[@]}" \ + quay.io/coreos/coreos-installer:release \ + "${INSTALLER_ARGS[@]}" + +# Add Raspberry Pi UEFI firmware +echo "" +echo -e "${GREEN}>>> Adding Raspberry Pi UEFI firmware...${NC}" -# Wait for partitions sleep 2 partprobe "$DEVICE" 2>/dev/null || true -sleep 2 +sleep 1 -# Find partitions +# Find EFI partition if [[ "$DEVICE" == *"mmcblk"* ]] || [[ "$DEVICE" == *"nvme"* ]]; then EFI_PART="${DEVICE}p1" - BOOT_PART="${DEVICE}p2" else EFI_PART="${DEVICE}1" - BOOT_PART="${DEVICE}2" fi -# 3. Add Ignition config -echo -e "${GREEN}>>> Adding Ignition configuration...${NC}" - -BOOT_MOUNT="$WORK_DIR/boot" -mkdir -p "$BOOT_MOUNT" -mount "$BOOT_PART" "$BOOT_MOUNT" - -mkdir -p "$BOOT_MOUNT/ignition" - -# Generate ignition from lab-config.yaml if it exists -if [ -f "$SCRIPT_DIR/$CONFIG" ] || [ -f "$CONFIG" ]; then - CONFIG_PATH="$CONFIG" - [ -f "$SCRIPT_DIR/$CONFIG" ] && CONFIG_PATH="$SCRIPT_DIR/$CONFIG" - - echo "Generating ignition from: $CONFIG_PATH" - - # Ensure dependencies - python3 -c "import yaml" 2>/dev/null || pip3 install --quiet pyyaml - - # Get butane - if ! command -v butane &>/dev/null; then - curl -sLo /tmp/butane https://github.com/coreos/butane/releases/download/v0.21.0/butane-x86_64-unknown-linux-gnu - chmod +x /tmp/butane - BUTANE=/tmp/butane - else - BUTANE=butane - fi - - python3 "$SCRIPT_DIR/../scripts/generate-butane.py" "$CONFIG_PATH" | \ - $BUTANE --strict > "$BOOT_MOUNT/ignition/config.ign" -else - echo -e "${RED}No config found. Creating minimal ignition...${NC}" - echo "You'll need to add SSH keys manually!" - - cat > "$BOOT_MOUNT/ignition/config.ign" << 'EOF' -{ - "ignition": { "version": "3.4.0" }, - "passwd": { - "users": [{ - "name": "core", - "groups": ["wheel", "sudo", "dialout"] - }] - } -} -EOF -fi - -umount "$BOOT_MOUNT" - -# 4. Add RPi UEFI firmware -echo -e "${GREEN}>>> Adding Raspberry Pi UEFI firmware...${NC}" - -EFI_MOUNT="$WORK_DIR/efi" -mkdir -p "$EFI_MOUNT" -mount "$EFI_PART" "$EFI_MOUNT" +WORK_DIR=$(mktemp -d) +mount "$EFI_PART" "$WORK_DIR" UEFI_VERSION="v1.39" -curl -sL "https://github.com/pftf/RPi4/releases/download/${UEFI_VERSION}/RPi4_UEFI_Firmware_${UEFI_VERSION}.zip" -o uefi.zip -unzip -o uefi.zip -d "$EFI_MOUNT/" - -umount "$EFI_MOUNT" +curl -sL "https://github.com/pftf/RPi4/releases/download/${UEFI_VERSION}/RPi4_UEFI_Firmware_${UEFI_VERSION}.zip" -o /tmp/uefi.zip +unzip -o /tmp/uefi.zip -d "$WORK_DIR/" +rm /tmp/uefi.zip -# Cleanup -cd / -rm -rf "$WORK_DIR" +umount "$WORK_DIR" +rmdir "$WORK_DIR" echo "" echo -e "${GREEN}=== Done! ===${NC}" echo "" echo "Next steps:" echo "1. Insert SD card into Raspberry Pi 4/5" -echo "2. First boot: Press Esc for UEFI setup" -echo " → Device Manager → Raspberry Pi Configuration → Advanced" +echo "2. First boot: Press Esc → UEFI setup" +echo " Device Manager → Raspberry Pi Configuration → Advanced" echo " → Limit RAM to 3GB → Disabled" -echo " → F10 to save, Esc to exit" -echo "3. SSH: ssh core@ (or labgrid@ if you used lab-config.yaml)" +echo " → F10 save, Esc exit" +if [ -n "$IGNITION" ]; then + echo "3. SSH: ssh labgrid@" +else + echo "3. SSH: ssh core@ (add your key to UEFI shell or use console)" +fi From 3f59117e4baea8b6fae1b75392071c53ca61ac8c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 20:34:22 +0000 Subject: [PATCH 6/7] refactor: separate image build from flashing Split into two steps: 1. build-image.sh - generates .img file (no root needed) 2. dd - user flashes manually (only root operation) Image manipulation runs inside fedora container via podman, so no root/sudo needed on the host for building. User workflow: ./build-image.sh config.ign -o labnode.img sudo dd if=labnode.img of=/dev/sdX bs=4M status=progress --- coreos/README.md | 13 +-- coreos/raspberry-pi/README.md | 106 ++++++++----------- coreos/raspberry-pi/build-image.sh | 157 +++++++++++++++++++++++++++++ coreos/raspberry-pi/flash-sd.sh | 138 ------------------------- 4 files changed, 208 insertions(+), 206 deletions(-) create mode 100755 coreos/raspberry-pi/build-image.sh delete mode 100755 coreos/raspberry-pi/flash-sd.sh diff --git a/coreos/README.md b/coreos/README.md index 7620482a2..82d22088b 100644 --- a/coreos/README.md +++ b/coreos/README.md @@ -9,23 +9,24 @@ Self-contained, auto-updating lab node setup for remote OpenWrt test labs with c cp lab-config.yaml.example lab-config.yaml nano lab-config.yaml # Add SSH keys, devices -# 2. Generate ignition file +# 2. Generate ignition ./scripts/build-ignition.sh lab-config.yaml -o config.ign -# 3. Flash SD card (Raspberry Pi) - uses podman container -sudo ./raspberry-pi/flash-sd.sh /dev/sdX config.ign +# 3. Build image (no root needed) +./raspberry-pi/build-image.sh config.ign -o labnode.img -# 4. Boot Pi, configure UEFI once, done! +# 4. Flash (only root step) +sudo dd if=labnode.img of=/dev/sdX bs=4M status=progress conv=fsync ``` -Only requires **podman** (or docker) - no other tools to install. +Requires: **podman** (or docker), curl, unzip, xz See [raspberry-pi/README.md](raspberry-pi/README.md) for details. ## x86 Servers ```bash -# Use coreos-installer directly (or via container) +# Use coreos-installer via container podman run --rm --privileged -v /dev:/dev -v .:/data:ro \ quay.io/coreos/coreos-installer:release \ install /dev/sdX --ignition-file /data/config.ign diff --git a/coreos/raspberry-pi/README.md b/coreos/raspberry-pi/README.md index ba7f04c50..88fdb5da8 100644 --- a/coreos/raspberry-pi/README.md +++ b/coreos/raspberry-pi/README.md @@ -12,58 +12,34 @@ nano ../lab-config.yaml # Add SSH keys # 2. Generate ignition ../scripts/build-ignition.sh ../lab-config.yaml -o config.ign -# 3. Flash SD card (uses podman, no install needed) -sudo ./flash-sd.sh /dev/sdX config.ign -``` - -Only requires **podman** (or docker) - coreos-installer runs in a container. - -## Without Config (Minimal Install) +# 3. Build image (no root needed, uses podman) +./build-image.sh config.ign -o labnode.img -```bash -# Just flash, add SSH keys later -sudo ./flash-sd.sh /dev/sdX +# 4. Flash (only step requiring root) +sudo dd if=labnode.img of=/dev/sdX bs=4M status=progress conv=fsync ``` -## Manual Method (most control) +Requires: **podman** (or docker), curl, unzip, xz -### 1. Download and flash CoreOS image +## What build-image.sh Does -```bash -# Download latest stable aarch64 image -curl -LO https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/.../fedora-coreos-...-metal.aarch64.raw.xz +1. Downloads Fedora CoreOS (aarch64, stable) +2. Downloads Raspberry Pi UEFI firmware +3. Embeds ignition config into image +4. Outputs ready-to-flash `.img` file -# Flash to SD card -xzcat fedora-coreos-*.raw.xz | sudo dd of=/dev/sdX bs=4M status=progress -``` +All image manipulation runs inside a container - no root on host. -### 2. Add ignition config +## Without Config (Minimal Install) ```bash -# Mount boot partition (partition 2 on CoreOS) -sudo mount /dev/sdX2 /mnt - -# Copy your ignition file -sudo mkdir -p /mnt/ignition -sudo cp config.ign /mnt/ignition/config.ign - -sudo umount /mnt +./build-image.sh -o minimal.img +sudo dd if=minimal.img of=/dev/sdX bs=4M status=progress ``` -### 3. Add Raspberry Pi UEFI firmware - -```bash -# Mount EFI partition (partition 1) -sudo mount /dev/sdX1 /mnt - -# Download and extract UEFI firmware -curl -LO https://github.com/pftf/RPi4/releases/download/v1.39/RPi4_UEFI_Firmware_v1.39.zip -sudo unzip RPi4_UEFI_Firmware_v1.39.zip -d /mnt/ - -sudo umount /mnt -``` +Default user is `core` - you'll need console access to add SSH keys. -### 4. First boot UEFI setup +## First Boot - UEFI Setup 1. Connect monitor + keyboard 2. Power on, press **Esc** for UEFI @@ -71,9 +47,11 @@ sudo umount /mnt 4. **Limit RAM to 3GB → Disabled** 5. **F10** save, **Esc** exit -## Applying Config Changes Later +This is a one-time setup. + +## After First Boot -Ignition only runs on **first boot**. To change config later: +Ignition only runs once. To change config later: ```bash # SSH into the Pi @@ -82,9 +60,6 @@ ssh labgrid@ # Edit configs directly sudo nano /etc/labgrid/exporter.yaml sudo systemctl restart labgrid-exporter - -# Or use Ansible from your workstation -ansible-playbook -i inventory playbook.yml --limit my-pi ``` ## Auto-Updates @@ -98,19 +73,27 @@ ansible-playbook -i inventory playbook.yml --limit my-pi # Check OS update status rpm-ostree status -# Force OS update now -sudo rpm-ostree upgrade - # Check container updates sudo podman auto-update --dry-run ``` -### Why this is safe +### Why Updates Are Safe + +- **A/B partitions**: Updates install to inactive partition +- **Auto-rollback**: Failed boot (3x) reverts automatically +- **Immutable OS**: Can't accidentally break with apt/dnf + +## Finding Serial Devices + +After boot: -- **A/B partitions**: OS updates install to inactive partition -- **Auto-rollback**: If new OS fails to boot 3 times → reverts -- **Staged updates**: Zincati coordinates timing across fleet -- **No apt/dnf**: Can't accidentally break the system +```bash +ls /dev/ttyUSB* +udevadm info /dev/ttyUSB0 | grep ID_PATH + +# Typical Pi 4/5 path: +# platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0 +``` ## Minimal lab-config.yaml @@ -121,22 +104,21 @@ lab: ssh_keys: - ssh-ed25519 AAAA... you@host -# Add devices after first boot (easier to find serial paths) +# Add devices after first boot devices: [] ``` ## Troubleshooting -### Won't boot -- Did you disable 3GB RAM limit in UEFI? -- Check ignition syntax: `butane --strict config.bu` +### Won't boot past UEFI +- Disable 3GB RAM limit in UEFI settings ### Can't SSH -- Default user is `core` if no lab-config.yaml -- Check ignition was placed in `/mnt/ignition/config.ign` +- Check ignition was embedded: `./build-image.sh config.ign` +- Default user is `core` without config -### Find serial device paths +### Container issues ```bash -ls /dev/ttyUSB* -udevadm info /dev/ttyUSB0 | grep ID_PATH +sudo journalctl -u labgrid-exporter -f +sudo podman ps ``` diff --git a/coreos/raspberry-pi/build-image.sh b/coreos/raspberry-pi/build-image.sh new file mode 100755 index 000000000..f052043e5 --- /dev/null +++ b/coreos/raspberry-pi/build-image.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# Generate Fedora CoreOS image for Raspberry Pi +# No root required - uses podman for image manipulation +# Usage: ./build-image.sh [config.ign] [-o output.img] +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +IGNITION="" +OUTPUT="labnode-rpi.img" + +usage() { + echo "Usage: $0 [config.ign] [-o output.img]" + echo "" + echo "Generate a ready-to-flash Fedora CoreOS image for Raspberry Pi" + echo "No root required - uses podman for image manipulation" + echo "" + echo "Options:" + echo " config.ign Ignition file (optional)" + echo " -o FILE Output image (default: labnode-rpi.img)" + echo "" + echo "Example:" + echo " $0 # Minimal image" + echo " $0 config.ign # With ignition" + echo " $0 config.ign -o mylab.img # Custom output name" + echo "" + echo "Then flash with:" + echo " sudo dd if=labnode-rpi.img of=/dev/sdX bs=4M status=progress" + echo "" + echo "Requires: podman (or docker), curl" +} + +while [[ $# -gt 0 ]]; do + case $1 in + -o|--output) + OUTPUT="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + IGNITION="$1" + shift + ;; + esac +done + +# Find container runtime +if command -v podman &>/dev/null; then + PODMAN="podman" +elif command -v docker &>/dev/null; then + PODMAN="docker" +else + echo "Error: podman or docker required" + exit 1 +fi + +if ! command -v curl &>/dev/null; then + echo "Error: curl required" + exit 1 +fi + +# Resolve ignition path +if [ -n "$IGNITION" ]; then + if [ ! -f "$IGNITION" ]; then + echo "Error: Ignition file not found: $IGNITION" + exit 1 + fi + IGNITION=$(realpath "$IGNITION") +fi + +OUTPUT=$(realpath "$OUTPUT") + +WORK_DIR=$(mktemp -d) +trap "rm -rf $WORK_DIR" EXIT +cd "$WORK_DIR" + +echo "=== Building Fedora CoreOS Image for Raspberry Pi ===" +echo "Output: $OUTPUT" +[ -n "$IGNITION" ] && echo "Ignition: $IGNITION" +echo "" + +# 1. Download Fedora CoreOS +STREAM="stable" +ARCH="aarch64" +echo ">>> Downloading Fedora CoreOS ($STREAM, $ARCH)..." + +META_URL="https://builds.coreos.fedoraproject.org/streams/${STREAM}.json" +IMAGE_URL=$(curl -sL "$META_URL" | python3 -c " +import sys, json +d = json.load(sys.stdin) +print(d['architectures']['$ARCH']['artifacts']['metal']['formats']['raw.xz']['disk']['location']) +") + +curl -L --progress-bar "$IMAGE_URL" -o fcos.raw.xz + +echo ">>> Extracting image..." +xz -d fcos.raw.xz + +# 2. Download UEFI firmware +echo ">>> Downloading Raspberry Pi UEFI firmware..." +UEFI_VERSION="v1.39" +curl -sL "https://github.com/pftf/RPi4/releases/download/${UEFI_VERSION}/RPi4_UEFI_Firmware_${UEFI_VERSION}.zip" -o uefi.zip +unzip -q uefi.zip -d uefi/ + +# 3. Create modification script for container +cat > modify.sh << 'SCRIPT' +#!/bin/bash +set -e + +# Setup loop device +LOOP=$(losetup --find --show --partscan /work/fcos.raw) +trap "losetup -d $LOOP" EXIT + +# Mount and modify EFI partition +mkdir -p /mnt/efi +mount "${LOOP}p1" /mnt/efi +cp -r /work/uefi/* /mnt/efi/ +umount /mnt/efi + +# Add ignition if provided +if [ -f /work/config.ign ]; then + mkdir -p /mnt/boot + mount "${LOOP}p2" /mnt/boot + mkdir -p /mnt/boot/ignition + cp /work/config.ign /mnt/boot/ignition/config.ign + umount /mnt/boot +fi + +echo "Image modified successfully" +SCRIPT +chmod +x modify.sh + +# Copy ignition if provided +[ -n "$IGNITION" ] && cp "$IGNITION" config.ign + +# 4. Run modification in container (needs privileges for losetup) +echo ">>> Modifying image (in container)..." + +$PODMAN run --rm --privileged \ + -v "$WORK_DIR:/work:Z" \ + fedora:latest \ + /work/modify.sh + +# 5. Move to output +mv fcos.raw "$OUTPUT" + +echo "" +echo "=== Done! ===" +echo "" +echo "Image: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" +echo "" +echo "Flash with:" +echo " sudo dd if=$OUTPUT of=/dev/sdX bs=4M status=progress conv=fsync" +echo "" +echo "First boot: Press Esc for UEFI, disable 3GB RAM limit" diff --git a/coreos/raspberry-pi/flash-sd.sh b/coreos/raspberry-pi/flash-sd.sh deleted file mode 100755 index 5c49f9555..000000000 --- a/coreos/raspberry-pi/flash-sd.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/bin/bash -# Flash Fedora CoreOS to SD card for Raspberry Pi using podman -# Usage: ./flash-sd.sh /dev/sdX [config.ign] -set -e - -DEVICE="${1:-}" -IGNITION="${2:-}" - -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' - -usage() { - echo "Usage: $0 /dev/sdX [config.ign]" - echo "" - echo "Flash Fedora CoreOS to SD card for Raspberry Pi 4/5" - echo "" - echo "Arguments:" - echo " /dev/sdX Target device (SD card)" - echo " config.ign Ignition file (optional, generate with build-ignition.sh)" - echo "" - echo "Examples:" - echo " $0 /dev/sdb # Minimal install" - echo " $0 /dev/sdb config.ign # With ignition config" - echo "" - echo "Requires: podman or docker" -} - -if [ -z "$DEVICE" ]; then - usage - exit 1 -fi - -if [ "$EUID" -ne 0 ]; then - echo -e "${RED}Run as root: sudo $0 $@${NC}" - exit 1 -fi - -if [ ! -b "$DEVICE" ]; then - echo -e "${RED}$DEVICE is not a block device${NC}" - exit 1 -fi - -# Find container runtime -if command -v podman &>/dev/null; then - CONTAINER_CMD="podman" -elif command -v docker &>/dev/null; then - CONTAINER_CMD="docker" -else - echo -e "${RED}Error: podman or docker required${NC}" - echo "Install with: sudo dnf install podman" - exit 1 -fi - -# Safety check -echo -e "${RED}WARNING: This will ERASE ALL DATA on $DEVICE${NC}" -lsblk "$DEVICE" -echo "" -read -p "Type 'yes' to continue: " confirm -[ "$confirm" = "yes" ] || exit 1 - -# Build coreos-installer arguments -INSTALLER_ARGS=( - install "$DEVICE" - --architecture aarch64 - --append-karg nomodeset - --append-karg console=tty1 -) - -# Add ignition if provided -if [ -n "$IGNITION" ]; then - if [ ! -f "$IGNITION" ]; then - echo -e "${RED}Ignition file not found: $IGNITION${NC}" - exit 1 - fi - IGNITION_ABS=$(realpath "$IGNITION") - IGNITION_DIR=$(dirname "$IGNITION_ABS") - IGNITION_FILE=$(basename "$IGNITION_ABS") - INSTALLER_ARGS+=(--ignition-file "/data/$IGNITION_FILE") - VOLUME_ARGS=(-v "$IGNITION_DIR:/data:ro") -else - VOLUME_ARGS=() - echo -e "${GREEN}No ignition file provided - will create minimal install${NC}" - echo "You can add ignition later to /mnt/ignition/config.ign" -fi - -# Flash using coreos-installer container -echo -e "${GREEN}>>> Flashing Fedora CoreOS (aarch64) to $DEVICE...${NC}" -echo "Using: $CONTAINER_CMD run quay.io/coreos/coreos-installer:release" -echo "" - -$CONTAINER_CMD run --rm --privileged \ - -v /dev:/dev \ - -v /run/udev:/run/udev \ - "${VOLUME_ARGS[@]}" \ - quay.io/coreos/coreos-installer:release \ - "${INSTALLER_ARGS[@]}" - -# Add Raspberry Pi UEFI firmware -echo "" -echo -e "${GREEN}>>> Adding Raspberry Pi UEFI firmware...${NC}" - -sleep 2 -partprobe "$DEVICE" 2>/dev/null || true -sleep 1 - -# Find EFI partition -if [[ "$DEVICE" == *"mmcblk"* ]] || [[ "$DEVICE" == *"nvme"* ]]; then - EFI_PART="${DEVICE}p1" -else - EFI_PART="${DEVICE}1" -fi - -WORK_DIR=$(mktemp -d) -mount "$EFI_PART" "$WORK_DIR" - -UEFI_VERSION="v1.39" -curl -sL "https://github.com/pftf/RPi4/releases/download/${UEFI_VERSION}/RPi4_UEFI_Firmware_${UEFI_VERSION}.zip" -o /tmp/uefi.zip -unzip -o /tmp/uefi.zip -d "$WORK_DIR/" -rm /tmp/uefi.zip - -umount "$WORK_DIR" -rmdir "$WORK_DIR" - -echo "" -echo -e "${GREEN}=== Done! ===${NC}" -echo "" -echo "Next steps:" -echo "1. Insert SD card into Raspberry Pi 4/5" -echo "2. First boot: Press Esc → UEFI setup" -echo " Device Manager → Raspberry Pi Configuration → Advanced" -echo " → Limit RAM to 3GB → Disabled" -echo " → F10 save, Esc exit" -if [ -n "$IGNITION" ]; then - echo "3. SSH: ssh labgrid@" -else - echo "3. SSH: ssh core@ (add your key to UEFI shell or use console)" -fi From 847f0f425ba0a091fa4c96bff1a3db6d2be534c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Jan 2026 17:53:30 +0000 Subject: [PATCH 7/7] refactor: remove privileged container, user controls all sudo build-image.sh now just downloads files to a directory: - fcos.raw.xz (CoreOS image) - uefi/ (RPi firmware) - config.ign (if provided) - flash.sh (simple script user can inspect) No containers, no --privileged. User runs sudo only on flash.sh which they can review first. Workflow: ./build-image.sh config.ign # no root cd coreos-rpi cat flash.sh # inspect if desired sudo ./flash.sh /dev/sdX # user controls this --- coreos/README.md | 10 +- coreos/raspberry-pi/README.md | 22 ++-- coreos/raspberry-pi/build-image.sh | 192 +++++++++++++++-------------- 3 files changed, 118 insertions(+), 106 deletions(-) diff --git a/coreos/README.md b/coreos/README.md index 82d22088b..ca80aaccf 100644 --- a/coreos/README.md +++ b/coreos/README.md @@ -12,14 +12,14 @@ nano lab-config.yaml # Add SSH keys, devices # 2. Generate ignition ./scripts/build-ignition.sh lab-config.yaml -o config.ign -# 3. Build image (no root needed) -./raspberry-pi/build-image.sh config.ign -o labnode.img +# 3. Download CoreOS + UEFI (no root, no containers) +./raspberry-pi/build-image.sh config.ign -# 4. Flash (only root step) -sudo dd if=labnode.img of=/dev/sdX bs=4M status=progress conv=fsync +# 4. Flash (inspect flash.sh first if you want) +cd coreos-rpi && sudo ./flash.sh /dev/sdX ``` -Requires: **podman** (or docker), curl, unzip, xz +No privileged containers. You control all root operations. See [raspberry-pi/README.md](raspberry-pi/README.md) for details. diff --git a/coreos/raspberry-pi/README.md b/coreos/raspberry-pi/README.md index 88fdb5da8..bfd1ccdab 100644 --- a/coreos/raspberry-pi/README.md +++ b/coreos/raspberry-pi/README.md @@ -12,23 +12,25 @@ nano ../lab-config.yaml # Add SSH keys # 2. Generate ignition ../scripts/build-ignition.sh ../lab-config.yaml -o config.ign -# 3. Build image (no root needed, uses podman) -./build-image.sh config.ign -o labnode.img +# 3. Download CoreOS + UEFI (no root needed) +./build-image.sh config.ign -# 4. Flash (only step requiring root) -sudo dd if=labnode.img of=/dev/sdX bs=4M status=progress conv=fsync +# 4. Flash (review flash.sh first if you want) +cd coreos-rpi +sudo ./flash.sh /dev/sdX ``` -Requires: **podman** (or docker), curl, unzip, xz +Requires: curl, unzip, xz (python3 for ignition generation) ## What build-image.sh Does -1. Downloads Fedora CoreOS (aarch64, stable) -2. Downloads Raspberry Pi UEFI firmware -3. Embeds ignition config into image -4. Outputs ready-to-flash `.img` file +Downloads files to `coreos-rpi/` directory: +- `fcos.raw.xz` - Fedora CoreOS image +- `uefi/` - Raspberry Pi UEFI firmware +- `config.ign` - Your ignition config (if provided) +- `flash.sh` - Simple script to flash everything -All image manipulation runs inside a container - no root on host. +**No root or privileged containers** - just downloads. You run `sudo` only on `flash.sh` which you can inspect first. ## Without Config (Minimal Install) diff --git a/coreos/raspberry-pi/build-image.sh b/coreos/raspberry-pi/build-image.sh index f052043e5..6e9ad5624 100755 --- a/coreos/raspberry-pi/build-image.sh +++ b/coreos/raspberry-pi/build-image.sh @@ -1,38 +1,31 @@ #!/bin/bash -# Generate Fedora CoreOS image for Raspberry Pi -# No root required - uses podman for image manipulation -# Usage: ./build-image.sh [config.ign] [-o output.img] +# Prepare Fedora CoreOS files for Raspberry Pi +# Downloads everything, user does the privileged operations +# Usage: ./build-image.sh [config.ign] [-o output-dir] set -e -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" IGNITION="" -OUTPUT="labnode-rpi.img" +OUTDIR="coreos-rpi" usage() { - echo "Usage: $0 [config.ign] [-o output.img]" + echo "Usage: $0 [config.ign] [-o output-dir]" echo "" - echo "Generate a ready-to-flash Fedora CoreOS image for Raspberry Pi" - echo "No root required - uses podman for image manipulation" + echo "Download and prepare Fedora CoreOS for Raspberry Pi" + echo "No root/privileged operations - just downloads files" echo "" echo "Options:" echo " config.ign Ignition file (optional)" - echo " -o FILE Output image (default: labnode-rpi.img)" + echo " -o DIR Output directory (default: coreos-rpi)" echo "" echo "Example:" - echo " $0 # Minimal image" - echo " $0 config.ign # With ignition" - echo " $0 config.ign -o mylab.img # Custom output name" - echo "" - echo "Then flash with:" - echo " sudo dd if=labnode-rpi.img of=/dev/sdX bs=4M status=progress" - echo "" - echo "Requires: podman (or docker), curl" + echo " $0 config.ign" + echo " $0 config.ign -o my-lab" } while [[ $# -gt 0 ]]; do case $1 in -o|--output) - OUTPUT="$2" + OUTDIR="$2" shift 2 ;; -h|--help) @@ -46,112 +39,129 @@ while [[ $# -gt 0 ]]; do esac done -# Find container runtime -if command -v podman &>/dev/null; then - PODMAN="podman" -elif command -v docker &>/dev/null; then - PODMAN="docker" -else - echo "Error: podman or docker required" +# Validate ignition if provided +if [ -n "$IGNITION" ] && [ ! -f "$IGNITION" ]; then + echo "Error: Ignition file not found: $IGNITION" exit 1 fi +# Check for curl if ! command -v curl &>/dev/null; then echo "Error: curl required" exit 1 fi -# Resolve ignition path -if [ -n "$IGNITION" ]; then - if [ ! -f "$IGNITION" ]; then - echo "Error: Ignition file not found: $IGNITION" - exit 1 - fi - IGNITION=$(realpath "$IGNITION") -fi - -OUTPUT=$(realpath "$OUTPUT") +mkdir -p "$OUTDIR" +cd "$OUTDIR" -WORK_DIR=$(mktemp -d) -trap "rm -rf $WORK_DIR" EXIT -cd "$WORK_DIR" - -echo "=== Building Fedora CoreOS Image for Raspberry Pi ===" -echo "Output: $OUTPUT" -[ -n "$IGNITION" ] && echo "Ignition: $IGNITION" +echo "=== Preparing Fedora CoreOS for Raspberry Pi ===" +echo "Output: $OUTDIR/" echo "" -# 1. Download Fedora CoreOS +# 1. Download CoreOS STREAM="stable" ARCH="aarch64" -echo ">>> Downloading Fedora CoreOS ($STREAM, $ARCH)..." - -META_URL="https://builds.coreos.fedoraproject.org/streams/${STREAM}.json" -IMAGE_URL=$(curl -sL "$META_URL" | python3 -c " +if [ ! -f fcos.raw.xz ]; then + echo ">>> Downloading Fedora CoreOS ($STREAM, $ARCH)..." + META_URL="https://builds.coreos.fedoraproject.org/streams/${STREAM}.json" + IMAGE_URL=$(curl -sL "$META_URL" | python3 -c " import sys, json d = json.load(sys.stdin) print(d['architectures']['$ARCH']['artifacts']['metal']['formats']['raw.xz']['disk']['location']) ") - -curl -L --progress-bar "$IMAGE_URL" -o fcos.raw.xz - -echo ">>> Extracting image..." -xz -d fcos.raw.xz + curl -L --progress-bar "$IMAGE_URL" -o fcos.raw.xz +else + echo ">>> Using cached fcos.raw.xz" +fi # 2. Download UEFI firmware -echo ">>> Downloading Raspberry Pi UEFI firmware..." UEFI_VERSION="v1.39" -curl -sL "https://github.com/pftf/RPi4/releases/download/${UEFI_VERSION}/RPi4_UEFI_Firmware_${UEFI_VERSION}.zip" -o uefi.zip -unzip -q uefi.zip -d uefi/ +if [ ! -d uefi ]; then + echo ">>> Downloading Raspberry Pi UEFI firmware..." + curl -sL "https://github.com/pftf/RPi4/releases/download/${UEFI_VERSION}/RPi4_UEFI_Firmware_${UEFI_VERSION}.zip" -o uefi.zip + unzip -q uefi.zip -d uefi + rm uefi.zip +else + echo ">>> Using cached uefi/" +fi + +# 3. Copy ignition if provided +if [ -n "$IGNITION" ]; then + cp "$IGNITION" config.ign + echo ">>> Copied ignition config" +fi -# 3. Create modification script for container -cat > modify.sh << 'SCRIPT' +# 4. Generate flash script +cat > flash.sh << 'SCRIPT' #!/bin/bash set -e +DEVICE="${1:-}" -# Setup loop device -LOOP=$(losetup --find --show --partscan /work/fcos.raw) -trap "losetup -d $LOOP" EXIT - -# Mount and modify EFI partition -mkdir -p /mnt/efi -mount "${LOOP}p1" /mnt/efi -cp -r /work/uefi/* /mnt/efi/ -umount /mnt/efi - -# Add ignition if provided -if [ -f /work/config.ign ]; then - mkdir -p /mnt/boot - mount "${LOOP}p2" /mnt/boot - mkdir -p /mnt/boot/ignition - cp /work/config.ign /mnt/boot/ignition/config.ign - umount /mnt/boot +if [ -z "$DEVICE" ]; then + echo "Usage: sudo ./flash.sh /dev/sdX" + exit 1 fi -echo "Image modified successfully" -SCRIPT -chmod +x modify.sh +if [ "$EUID" -ne 0 ]; then + echo "Run as root: sudo $0 $DEVICE" + exit 1 +fi -# Copy ignition if provided -[ -n "$IGNITION" ] && cp "$IGNITION" config.ign +echo "=== Flashing to $DEVICE ===" +echo "WARNING: This will ERASE ALL DATA" +lsblk "$DEVICE" +read -p "Type 'yes' to continue: " confirm +[ "$confirm" = "yes" ] || exit 1 -# 4. Run modification in container (needs privileges for losetup) -echo ">>> Modifying image (in container)..." +# Flash +echo ">>> Writing image..." +xzcat fcos.raw.xz | dd of="$DEVICE" bs=4M status=progress conv=fsync -$PODMAN run --rm --privileged \ - -v "$WORK_DIR:/work:Z" \ - fedora:latest \ - /work/modify.sh +sleep 2 +partprobe "$DEVICE" 2>/dev/null || true +sleep 1 -# 5. Move to output -mv fcos.raw "$OUTPUT" +# Detect partition naming +if [[ "$DEVICE" == *"mmcblk"* ]] || [[ "$DEVICE" == *"nvme"* ]]; then + P1="${DEVICE}p1" + P2="${DEVICE}p2" +else + P1="${DEVICE}1" + P2="${DEVICE}2" +fi + +# Add UEFI firmware +echo ">>> Adding UEFI firmware..." +mount "$P1" /mnt +cp -r uefi/* /mnt/ +umount /mnt + +# Add ignition if present +if [ -f config.ign ]; then + echo ">>> Adding ignition config..." + mount "$P2" /mnt + mkdir -p /mnt/ignition + cp config.ign /mnt/ignition/config.ign + umount /mnt +fi echo "" echo "=== Done! ===" +echo "First boot: Press Esc for UEFI, disable 3GB RAM limit" +SCRIPT +chmod +x flash.sh + echo "" -echo "Image: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" +echo "=== Ready ===" echo "" -echo "Flash with:" -echo " sudo dd if=$OUTPUT of=/dev/sdX bs=4M status=progress conv=fsync" +echo "Contents of $OUTDIR/:" +ls -lh echo "" -echo "First boot: Press Esc for UEFI, disable 3GB RAM limit" +echo "To flash, run:" +echo " cd $OUTDIR" +echo " sudo ./flash.sh /dev/sdX" +echo "" +echo "Or manually:" +echo " xzcat fcos.raw.xz | sudo dd of=/dev/sdX bs=4M status=progress" +echo " sudo mount /dev/sdX1 /mnt && sudo cp -r uefi/* /mnt/ && sudo umount /mnt" +[ -f config.ign ] && echo " sudo mount /dev/sdX2 /mnt && sudo mkdir -p /mnt/ignition && sudo cp config.ign /mnt/ignition/ && sudo umount /mnt"