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..ca80aaccf --- /dev/null +++ b/coreos/README.md @@ -0,0 +1,262 @@ +# OpenWrt Test Lab - Containerized Infrastructure + +Self-contained, auto-updating lab node setup for remote OpenWrt test labs with containerized services. + +## Quick Start + +```bash +# 1. Create your lab config +cp lab-config.yaml.example lab-config.yaml +nano lab-config.yaml # Add SSH keys, devices + +# 2. Generate ignition +./scripts/build-ignition.sh lab-config.yaml -o config.ign + +# 3. Download CoreOS + UEFI (no root, no containers) +./raspberry-pi/build-image.sh config.ign + +# 4. Flash (inspect flash.sh first if you want) +cd coreos-rpi && sudo ./flash.sh /dev/sdX +``` + +No privileged containers. You control all root operations. + +See [raspberry-pi/README.md](raspberry-pi/README.md) for details. + +## x86 Servers + +```bash +# 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 +``` + +## 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/raspberry-pi/README.md b/coreos/raspberry-pi/README.md new file mode 100644 index 000000000..bfd1ccdab --- /dev/null +++ b/coreos/raspberry-pi/README.md @@ -0,0 +1,126 @@ +# OpenWrt Test Lab - Raspberry Pi + +Fedora CoreOS on Raspberry Pi with safe, atomic auto-updates. + +## Quick Start + +```bash +# 1. Create config (optional but recommended) +cp ../lab-config.yaml.example ../lab-config.yaml +nano ../lab-config.yaml # Add SSH keys + +# 2. Generate ignition +../scripts/build-ignition.sh ../lab-config.yaml -o config.ign + +# 3. Download CoreOS + UEFI (no root needed) +./build-image.sh config.ign + +# 4. Flash (review flash.sh first if you want) +cd coreos-rpi +sudo ./flash.sh /dev/sdX +``` + +Requires: curl, unzip, xz (python3 for ignition generation) + +## What build-image.sh Does + +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 + +**No root or privileged containers** - just downloads. You run `sudo` only on `flash.sh` which you can inspect first. + +## Without Config (Minimal Install) + +```bash +./build-image.sh -o minimal.img +sudo dd if=minimal.img of=/dev/sdX bs=4M status=progress +``` + +Default user is `core` - you'll need console access to add SSH keys. + +## 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 + +This is a one-time setup. + +## After First Boot + +Ignition only runs once. To change config later: + +```bash +# SSH into the Pi +ssh labgrid@ + +# Edit configs directly +sudo nano /etc/labgrid/exporter.yaml +sudo systemctl restart labgrid-exporter +``` + +## Auto-Updates + +| Component | Method | Schedule | Rollback | +|-----------|--------|----------|----------| +| **Fedora CoreOS** | Zincati + rpm-ostree | Sundays 3am | Automatic | +| **Containers** | Podman auto-update | Daily 4am | Manual | + +```bash +# Check OS update status +rpm-ostree status + +# Check container updates +sudo podman auto-update --dry-run +``` + +### 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: + +```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 + +```yaml +lab: + hostname: labgrid-pi + +ssh_keys: + - ssh-ed25519 AAAA... you@host + +# Add devices after first boot +devices: [] +``` + +## Troubleshooting + +### Won't boot past UEFI +- Disable 3GB RAM limit in UEFI settings + +### Can't SSH +- Check ignition was embedded: `./build-image.sh config.ign` +- Default user is `core` without config + +### Container issues +```bash +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..6e9ad5624 --- /dev/null +++ b/coreos/raspberry-pi/build-image.sh @@ -0,0 +1,167 @@ +#!/bin/bash +# 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 + +IGNITION="" +OUTDIR="coreos-rpi" + +usage() { + echo "Usage: $0 [config.ign] [-o output-dir]" + echo "" + 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 DIR Output directory (default: coreos-rpi)" + echo "" + echo "Example:" + echo " $0 config.ign" + echo " $0 config.ign -o my-lab" +} + +while [[ $# -gt 0 ]]; do + case $1 in + -o|--output) + OUTDIR="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + IGNITION="$1" + shift + ;; + esac +done + +# 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 + +mkdir -p "$OUTDIR" +cd "$OUTDIR" + +echo "=== Preparing Fedora CoreOS for Raspberry Pi ===" +echo "Output: $OUTDIR/" +echo "" + +# 1. Download CoreOS +STREAM="stable" +ARCH="aarch64" +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 +else + echo ">>> Using cached fcos.raw.xz" +fi + +# 2. Download UEFI firmware +UEFI_VERSION="v1.39" +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 + +# 4. Generate flash script +cat > flash.sh << 'SCRIPT' +#!/bin/bash +set -e +DEVICE="${1:-}" + +if [ -z "$DEVICE" ]; then + echo "Usage: sudo ./flash.sh /dev/sdX" + exit 1 +fi + +if [ "$EUID" -ne 0 ]; then + echo "Run as root: sudo $0 $DEVICE" + exit 1 +fi + +echo "=== Flashing to $DEVICE ===" +echo "WARNING: This will ERASE ALL DATA" +lsblk "$DEVICE" +read -p "Type 'yes' to continue: " confirm +[ "$confirm" = "yes" ] || exit 1 + +# Flash +echo ">>> Writing image..." +xzcat fcos.raw.xz | dd of="$DEVICE" bs=4M status=progress conv=fsync + +sleep 2 +partprobe "$DEVICE" 2>/dev/null || true +sleep 1 + +# 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 "=== Ready ===" +echo "" +echo "Contents of $OUTDIR/:" +ls -lh +echo "" +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" 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 "" 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..c82509814 --- /dev/null +++ b/coreos/scripts/build-ignition.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# Build Ignition configuration from simple lab config +# 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 [-o output.ign]" + echo "" + echo "Generate Ignition configuration from lab config file." + echo "" + echo "Options:" + echo " -o FILE Output file (default: labnode.ign)" + echo "" + echo "Example:" + echo " $0 lab-config.yaml" + echo " $0 lab-config.yaml -o config.ign" +} + +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 + +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"