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
