Replace Cirrus CI with GitHub Actions running FreeBSD inside a QEMU/KVM
VM on ubuntu-24.04 runners.  The VM image is pre-built from the official
FreeBSD BASIC-CLOUDINIT release, provisioned via nuageinit, and cached
between runs to keep build times short.

FreeBSD version is updated from 13.5/14.3 to 15.0.  Only a single
FreeBSD version and clang are tested, as clang is the native FreeBSD
compiler, keeping GitHub Actions resource usage to a minimum.

Assisted-by: Claude Sonnet 4.6, OpenCode
Co-authored-by: Ilya Maximets <[email protected]>
Signed-off-by: Ilya Maximets <[email protected]>
Signed-off-by: Eelco Chaudron <[email protected]>
---
 .ci/freebsd-build.sh          |  61 ++++++++++++
 .ci/freebsd-prepare-image.sh  |  87 ++++++++++++++++
 .ci/freebsd-vm.sh             | 183 ++++++++++++++++++++++++++++++++++
 .github/workflows/freebsd.yml |  80 +++++++++++++++
 Makefile.am                   |   4 +
 README.rst                    |   4 +-
 utilities/checkpatch_dict.txt |   1 +
 7 files changed, 418 insertions(+), 2 deletions(-)
 create mode 100755 .ci/freebsd-build.sh
 create mode 100755 .ci/freebsd-prepare-image.sh
 create mode 100755 .ci/freebsd-vm.sh
 create mode 100755 .github/workflows/freebsd.yml

diff --git a/.ci/freebsd-build.sh b/.ci/freebsd-build.sh
new file mode 100755
index 000000000..25ec93c9b
--- /dev/null
+++ b/.ci/freebsd-build.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# Builds and tests OVS inside a FreeBSD QEMU VM.
+#
+# Requires FREEBSD_VER and CC to be set (e.g. via the workflow env).
+# The cached image freebsd-${FREEBSD_VER}.qcow2 must exist in the
+# current directory (restored from actions/cache by the workflow).
+
+set -o errexit
+set -x
+
+FREEBSD_VER="${FREEBSD_VER:?Must set FREEBSD_VER}"
+CC="${CC:?Must set CC}"
+
+BASE_IMG="freebsd-${FREEBSD_VER}.qcow2"
+RUN_IMG="freebsd-run.qcow2"
+
+if [ ! -f "${BASE_IMG}" ]; then
+    echo "ERROR: ${BASE_IMG} not found." >&2
+    exit 1
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+. "${SCRIPT_DIR}/freebsd-vm.sh"
+
+KEY_DIR="$(mktemp -d)"
+SSH_KEY="${KEY_DIR}/id_ed25519"
+ssh-keygen -t ed25519 -f "${SSH_KEY}" -N "" -q
+export FREEBSD_SSH_KEY="${SSH_KEY}"
+
+# COW overlay keeps the cached base image unmodified.
+qemu-img create -f qcow2 -F qcow2 -b "$(realpath "${BASE_IMG}")" "${RUN_IMG}"
+
+freebsd_create_seed "${SSH_KEY}.pub" /tmp/freebsd-seed /tmp/freebsd-seed.iso 
false
+
+OVMF_VARS="/tmp/freebsd-ovmf-vars.fd"
+cp "${FREEBSD_OVMF_VARS}" "${OVMF_VARS}"
+freebsd_start_vm "${RUN_IMG}" /tmp/freebsd-seed.iso "${OVMF_VARS}"
+
+cleanup() {
+    mkdir -p tests
+    freebsd_rsync_from /root/ovs/config.log          ./     2>/dev/null || true
+    freebsd_rsync_from /root/ovs/tests/testsuite.log tests/ 2>/dev/null || true
+    freebsd_rsync_from /root/ovs/tests/testsuite.dir tests/ 2>/dev/null || true
+    freebsd_rsync_from /var/log/nuageinit.log        ./     2>/dev/null || true
+    freebsd_stop_vm
+    cp /tmp/freebsd-vm.log ./freebsd-console.log 2>/dev/null || true
+    rm -rf "${KEY_DIR}" "${RUN_IMG}" "${OVMF_VARS}" \
+           /tmp/freebsd-seed /tmp/freebsd-seed.iso
+}
+trap cleanup EXIT
+
+freebsd_wait_ssh 20 10
+freebsd_wait_firstboot 30 5
+
+freebsd_ssh "mkdir -p /root/ovs"
+freebsd_rsync_to "$(pwd)/" /root/ovs/
+
+freebsd_ssh "cd /root/ovs && ./boot.sh && \
+    ./configure CC=${CC} CFLAGS='-g -O2 -Wall' MAKE=gmake --enable-Werror"
+
+freebsd_ssh "cd /root/ovs && gmake -j8 check TESTSUITEFLAGS=-j8 RECHECK=yes"
diff --git a/.ci/freebsd-prepare-image.sh b/.ci/freebsd-prepare-image.sh
new file mode 100755
index 000000000..7bf3649e9
--- /dev/null
+++ b/.ci/freebsd-prepare-image.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+# Prepares a FreeBSD QEMU image with CI dependencies pre-installed.
+#
+# Requires FREEBSD_VER and FREEBSD_PACKAGES to be set
+# (e.g. via the workflow env).
+# Downloads the FreeBSD BASIC-CLOUDINIT qcow2 image, boots it with
+# nuageinit to install packages and configure SSH, then compresses
+# the result for caching.
+#
+# Output: freebsd-${FREEBSD_VER}.qcow2
+
+set -o errexit
+set -x
+
+FREEBSD_VER="${FREEBSD_VER:?Must set FREEBSD_VER}"
+FREEBSD_PACKAGES="${FREEBSD_PACKAGES:?Must set FREEBSD_PACKAGES}"
+
+RELEASE="${FREEBSD_VER}-RELEASE"
+BASE_URL="https://download.freebsd.org/releases/VM-IMAGES/${RELEASE}/amd64/Latest";
+
+IMG_NAME="FreeBSD-${RELEASE}-amd64-BASIC-CLOUDINIT-ufs.qcow2"
+IMG_XZ="${IMG_NAME}.xz"
+OUT_IMG="freebsd-${FREEBSD_VER}.qcow2"
+
+wget -q "${BASE_URL}/CHECKSUM.SHA256" -O freebsd-checksum.txt
+wget -q "${BASE_URL}/${IMG_XZ}" -O "${IMG_XZ}"
+
+expected_sha=$(grep "(${IMG_XZ})" freebsd-checksum.txt | awk '{print $NF}')
+actual_sha=$(sha256sum "${IMG_XZ}" | awk '{print $1}')
+if [ "${expected_sha}" != "${actual_sha}" ]; then
+    echo "ERROR: SHA256 mismatch for ${IMG_XZ}" >&2
+    echo "  expected: ${expected_sha}" >&2
+    echo "  actual:   ${actual_sha}" >&2
+    exit 1
+fi
+
+xz --decompress --keep "${IMG_XZ}"
+mv "${IMG_NAME}" "${OUT_IMG}"
+rm -f "${IMG_XZ}"
+
+qemu-img resize "${OUT_IMG}" +8G
+
+KEY_DIR="$(mktemp -d)"
+SSH_KEY="${KEY_DIR}/id_ed25519"
+ssh-keygen -t ed25519 -f "${SSH_KEY}" -N "" -q
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+. "${SCRIPT_DIR}/freebsd-vm.sh"
+export FREEBSD_SSH_KEY="${SSH_KEY}"
+
+freebsd_create_seed "${SSH_KEY}.pub" /tmp/freebsd-seed /tmp/freebsd-seed.iso
+
+OVMF_VARS="/tmp/freebsd-ovmf-vars.fd"
+cp "${FREEBSD_OVMF_VARS}" "${OVMF_VARS}"
+freebsd_start_vm "${OUT_IMG}" /tmp/freebsd-seed.iso "${OVMF_VARS}"
+
+VM_STOPPED=false
+cleanup() {
+    if ! ${VM_STOPPED}; then
+        freebsd_rsync_from /var/log/nuageinit.log ./ 2>/dev/null || true
+        freebsd_stop_vm
+    fi
+    cp /tmp/freebsd-vm.log ./freebsd-console.log 2>/dev/null || true
+    rm -rf "${KEY_DIR}" "${OVMF_VARS}" \
+           /tmp/freebsd-seed /tmp/freebsd-seed.iso
+}
+trap cleanup EXIT
+
+# Image preparation covers two boots: boot 1 (freebsd-update + reboot)
+# then boot 2 (package install + runcmds).
+freebsd_wait_ssh 90 10
+freebsd_wait_firstboot 12 10
+
+# Verify all CI packages were installed successfully.
+freebsd_ssh "pkg info ${FREEBSD_PACKAGES}"
+
+# Restore /firstboot so nuageinit re-runs on build job boots to inject
+# per-job SSH keys.
+freebsd_ssh "touch /firstboot"
+
+freebsd_stop_vm
+VM_STOPPED=true
+
+qemu-img convert -c -O qcow2 "${OUT_IMG}" "${OUT_IMG}.tmp"
+mv "${OUT_IMG}.tmp" "${OUT_IMG}"
+
+echo "Image ready: ${OUT_IMG} ($(du -sh "${OUT_IMG}" | cut -f1))"
diff --git a/.ci/freebsd-vm.sh b/.ci/freebsd-vm.sh
new file mode 100755
index 000000000..96d97d7c6
--- /dev/null
+++ b/.ci/freebsd-vm.sh
@@ -0,0 +1,183 @@
+#!/bin/bash
+# FreeBSD QEMU VM helpers.  Source this file; do not execute directly.
+#
+# Requires FREEBSD_SSH_KEY (path to private key) to be set before use.
+
+FREEBSD_SSH_PORT=2222
+FREEBSD_VM_PIDFILE=/tmp/freebsd-vm.pid
+
+FREEBSD_OVMF_CODE="/usr/share/OVMF/OVMF_CODE_4M.fd"
+FREEBSD_OVMF_VARS="/usr/share/OVMF/OVMF_VARS_4M.fd"
+
+_FREEBSD_SSH_OPTS=(
+    -p "$FREEBSD_SSH_PORT"
+    -o StrictHostKeyChecking=no
+    -o UserKnownHostsFile=/dev/null
+    -o ConnectTimeout=5
+    -o BatchMode=yes
+    -o ServerAliveInterval=15
+    -o ServerAliveCountMax=4
+    -o LogLevel=ERROR
+)
+
+freebsd_ssh() {
+    ssh "${_FREEBSD_SSH_OPTS[@]}" -i "${FREEBSD_SSH_KEY}" \
+        root@localhost "$@"
+}
+
+freebsd_rsync_to() {
+    local src="${1:?source required}"
+    local dst="${2:?destination required}"
+
+    rsync -az --delete \
+        -e "ssh ${_FREEBSD_SSH_OPTS[*]} -i ${FREEBSD_SSH_KEY}" \
+        "${src}" "root@localhost:${dst}"
+}
+
+freebsd_rsync_from() {
+    local src="${1:?source required}"
+    local dst="${2:?destination required}"
+
+    rsync -az \
+        -e "ssh ${_FREEBSD_SSH_OPTS[*]} -i ${FREEBSD_SSH_KEY}" \
+        "root@localhost:${src}" "${dst}"
+}
+
+# freebsd_start_vm <image> <seed_iso> <ovmf_vars>
+freebsd_start_vm() {
+    local img="${1:?image file required}"
+    local seed_iso="${2:?seed ISO required}"
+    local ovmf_vars="${3:?OVMF vars file required}"
+
+    qemu-system-x86_64 \
+        -enable-kvm -cpu host \
+        -m 4096 -smp 4 \
+        -nographic \
+        -netdev "user,id=net0,hostfwd=tcp::${FREEBSD_SSH_PORT}-:22" \
+        -device virtio-net-pci,netdev=net0 \
+        -drive "file=${img},if=virtio,format=qcow2,cache=unsafe" \
+        -device virtio-rng-pci \
+        -pidfile "${FREEBSD_VM_PIDFILE}" \
+        -device ahci,id=ahci0 \
+        -drive 
"if=none,id=seed,file=${seed_iso},format=raw,media=cdrom,readonly=on" \
+        -device ide-cd,bus=ahci0.0,drive=seed \
+        -drive "if=pflash,format=raw,readonly=on,file=${FREEBSD_OVMF_CODE}" \
+        -drive "if=pflash,format=raw,file=${ovmf_vars}" \
+        > /tmp/freebsd-vm.log 2>&1 &
+
+    echo "FreeBSD VM launched (PID $!); log: /tmp/freebsd-vm.log"
+}
+
+freebsd_stop_vm() {
+    local pid
+
+    [ -f "${FREEBSD_VM_PIDFILE}" ] || return 0
+    pid=$(cat "${FREEBSD_VM_PIDFILE}" 2>/dev/null) || return 0
+
+    freebsd_ssh "shutdown -p now" 2>/dev/null || true
+
+    local i
+    for i in $(seq 1 30); do
+        kill -0 "${pid}" 2>/dev/null || {
+            rm -f "${FREEBSD_VM_PIDFILE}"
+            return 0
+        }
+        sleep 2
+    done
+
+    kill "${pid}" 2>/dev/null || true
+    rm -f "${FREEBSD_VM_PIDFILE}"
+}
+
+# freebsd_wait_ssh <max_attempts> <delay>
+freebsd_wait_ssh() {
+    local max="${1}" delay="${2}" i
+
+    echo "Waiting for SSH on port ${FREEBSD_SSH_PORT} ..."
+    for i in $(seq 1 "${max}"); do
+        if freebsd_ssh true 2>/dev/null; then
+            echo "SSH ready (attempt ${i})."
+            return 0
+        fi
+        echo "  attempt ${i}/${max} ..."
+        [ "${i}" != "${max}" ] && sleep "${delay}"
+    done
+
+    echo "ERROR: SSH not available after $((max * delay))s." >&2
+    return 1
+}
+
+# freebsd_wait_firstboot <max_attempts> <delay>
+# Waits until /firstboot is removed, meaning nuageinit (and its sshd
+# restart runcmd) has finished.  Call after freebsd_wait_ssh.
+freebsd_wait_firstboot() {
+    local max="${1}" delay="${2}" i
+
+    echo "Waiting for firstboot to complete ..."
+    for i in $(seq 1 "${max}"); do
+        if freebsd_ssh "test ! -f /firstboot" 2>/dev/null; then
+            echo "Firstboot complete (attempt ${i})."
+            return 0
+        fi
+        echo "  attempt ${i}/${max} ..."
+        sleep "${delay}"
+    done
+
+    echo "ERROR: /firstboot still present after $((max * delay))s." >&2
+    return 1
+}
+
+# freebsd_create_seed <pubkey_file> <work_dir> <output_iso> [install_packages]
+# Creates a NoCloud seed ISO for nuageinit with SSH key injection.
+# When install_packages is "true" (default), the seed also includes
+# package_update and the CI package list from FREEBSD_PACKAGES
+# (used during image preparation).  Pass "false" for build jobs where
+# the cached image already has all packages installed.
+freebsd_create_seed() {
+    local pub_key_file="${1:?public key file required}"
+    local work_dir="${2:?work dir required}"
+    local out_iso="${3:?output ISO required}"
+    local install_packages="${4:-true}"
+    local pub_key
+
+    pub_key=$(cat "${pub_key_file}")
+    mkdir -p "${work_dir}"
+
+    cat > "${work_dir}/meta-data" <<EOF
+instance-id: freebsd-ci
+local-hostname: freebsd-ci
+EOF
+
+    cat > "${work_dir}/user-data" <<EOF
+#cloud-config
+users:
+  - name: root
+    ssh_authorized_keys:
+      - ${pub_key}
+EOF
+
+    if [ "${install_packages}" = "true" ]; then
+        local packages="${FREEBSD_PACKAGES:?Must set FREEBSD_PACKAGES}"
+        {
+            echo "package_update: true"
+            echo "packages:"
+            for pkg in ${packages}; do
+                echo "  - ${pkg}"
+            done
+        } >> "${work_dir}/user-data"
+    fi
+
+    cat >> "${work_dir}/user-data" <<EOF
+runcmd:
+  - printf '\nPermitRootLogin yes\n' >> /etc/ssh/sshd_config
+  - grep -q kern.coredump /etc/sysctl.conf || echo 'kern.coredump=0' >> 
/etc/sysctl.conf
+  - sysctl -w kern.coredump=0 || true
+  - service sshd onerestart || true
+EOF
+
+    genisoimage -output "${out_iso}" \
+        -volid cidata -rational-rock -joliet \
+        "${work_dir}/user-data" "${work_dir}/meta-data" 2>/dev/null
+
+    echo "Seed ISO created: ${out_iso}"
+}
diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml
new file mode 100755
index 000000000..9f219ffb0
--- /dev/null
+++ b/.github/workflows/freebsd.yml
@@ -0,0 +1,80 @@
+name: FreeBSD Build and Test
+
+on: [push, pull_request]
+
+env:
+  FREEBSD_VER: "15.0"
+  CC: clang
+  FREEBSD_PACKAGES: >-
+    automake libtool gmake openssl python3 rsync
+    py311-sphinx py311-netaddr py311-pyparsing
+  dependencies: >-
+    qemu-system-x86 qemu-utils genisoimage rsync openssh-client ovmf
+    wget xz-utils
+
+jobs:
+  build-freebsd:
+    name: freebsd
+    runs-on: ubuntu-24.04
+    timeout-minutes: 60
+
+    steps:
+    - name: checkout
+      uses: actions/checkout@v6
+
+    - name: update APT cache
+      run:  sudo apt update || true
+    - name: install common dependencies
+      run:  sudo apt install -y ${{ env.dependencies }}
+
+    - name: enable KVM access
+      run: |
+        echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", 
OPTIONS+="static_node=kvm"' \
+          | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+        sudo udevadm control --reload-rules
+        sudo udevadm trigger --name-match=kvm
+
+    - name: generate image cache key
+      id: gen_key
+      run: |
+        key="freebsd-${{ env.FREEBSD_VER }}-"
+        key+="${{ hashFiles('.github/workflows/freebsd.yml',
+                            '.ci/freebsd-build.sh',
+                            '.ci/freebsd-prepare-image.sh',
+                            '.ci/freebsd-vm.sh') }}"
+        echo "key=${key}" >> "$GITHUB_OUTPUT"
+
+    - name: restore image cache
+      id: image_cache
+      uses: actions/cache@v5
+      with:
+        path: freebsd-${{ env.FREEBSD_VER }}.qcow2
+        key:  ${{ steps.gen_key.outputs.key }}
+
+    - name: prepare image
+      if: steps.image_cache.outputs.cache-hit != 'true'
+      run: ./.ci/freebsd-prepare-image.sh
+
+    - name: build and test
+      run: ./.ci/freebsd-build.sh
+
+    - name: copy logs on failure
+      if: failure() || cancelled()
+      run: |
+        # upload-artifact throws exceptions if it tries to upload socket
+        # files and we could have some socket files in testsuite.dir.
+        # Also, upload-artifact doesn't work well enough with wildcards.
+        # So, we're just archiving everything here to avoid any issues.
+        mkdir logs
+        cp config.log ./logs/ || true
+        cp -r ./tests/testsuite.* ./logs/ || true
+        cp nuageinit.log ./logs/ || true
+        cp freebsd-console.log ./logs/ || true
+        sudo tar -czvf logs.tgz logs/
+
+    - name: upload logs on failure
+      if: failure() || cancelled()
+      uses: actions/upload-artifact@v7
+      with:
+        name: logs-freebsd-${{ env.FREEBSD_VER }}-${{ env.CC }}
+        path: logs.tgz
diff --git a/Makefile.am b/Makefile.am
index 65597f9dc..83a39a7ea 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -64,6 +64,9 @@ EXTRA_DIST = \
        NOTICE \
        .ci/dpdk-build.sh \
        .ci/dpdk-prepare.sh \
+       .ci/freebsd-build.sh \
+       .ci/freebsd-prepare-image.sh \
+       .ci/freebsd-vm.sh \
        .ci/linux-build.sh \
        .ci/linux-prepare.sh \
        .ci/osx-build.sh \
@@ -71,6 +74,7 @@ EXTRA_DIST = \
        .cirrus.yml \
        .editorconfig \
        .github/workflows/build-and-test.yml \
+       .github/workflows/freebsd.yml \
        .readthedocs.yaml \
        boot.sh \
        $(MAN_FRAGMENTS) \
diff --git a/README.rst b/README.rst
index f55ecc674..de6acfc50 100644
--- a/README.rst
+++ b/README.rst
@@ -8,8 +8,8 @@ Open vSwitch
 
 .. image:: 
https://github.com/openvswitch/ovs/workflows/Build%20and%20Test/badge.svg
     :target: https://github.com/openvswitch/ovs/actions
-.. image:: https://api.cirrus-ci.com/github/openvswitch/ovs.svg
-    :target: https://cirrus-ci.com/github/openvswitch/ovs
+.. image:: 
https://github.com/openvswitch/ovs/workflows/FreeBSD%20Build%20and%20Test/badge.svg
+    :target: https://github.com/openvswitch/ovs/actions
 .. image:: https://readthedocs.org/projects/openvswitch/badge/?version=latest
     :target: https://docs.openvswitch.org/en/latest/
 
diff --git a/utilities/checkpatch_dict.txt b/utilities/checkpatch_dict.txt
index dfd3bb594..16c408732 100644
--- a/utilities/checkpatch_dict.txt
+++ b/utilities/checkpatch_dict.txt
@@ -185,6 +185,7 @@ nicira
 nics
 ns
 nsec
+nuageinit
 num
 numa
 odp
-- 
2.53.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to