Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/actions/setup-vagrant-libvirt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# setup-vagrant-libvirt

Install Vagrant, the vagrant-libvirt provider and libvirt on an Ubuntu
GitHub Actions runner, for Beaker acceptance tests with
`BEAKER_HYPERVISOR=vagrant_libvirt`.

Ubuntu stopped shipping `vagrant` and `vagrant-libvirt` after 23.10.
[`ppa:evgeni/vagrant`](https://launchpad.net/~evgeni/+archive/ubuntu/vagrant)
packages Vagrant 2.3.7 (the last MPL-licensed release) and a matching
vagrant-libvirt. Installing from the PPA alone fails the job whenever
Launchpad is down, so this action tries each source in turn:

1. **The Actions cache**: the PPA's packages (including any dependencies
the PPA supplies), saved by a `cache-only` run on the default branch.
This path makes no requests to Launchpad; everything else comes from
the Ubuntu archive.
2. **The PPA**: add it, refresh only its index (so an error from that
source fails the attempt rather than being a warning), and install.
Each step is tried three times, 30s and then 60s apart.
3. **HashiCorp**: `vagrant` (pinned to `vagrant-version`) from
<https://apt.releases.hashicorp.com>, plus `vagrant plugin install
vagrant-libvirt` (pinned to `vagrant-libvirt-version`), built against
`libvirt-dev`. HashiCorp's Vagrant is BSL-licensed, so the action emits
a warning when it falls back to it.

### Which runner

The cache and the PPA only apply on the Ubuntu series in `ppa-series`. The
PPA publishes for `noble` alone, so they need `runs-on: ubuntu-24.04`. On
any other series the action goes straight to HashiCorp, with a notice
rather than a warning. `ubuntu-latest` now resolves to `ubuntu-26.04`
(`resolute`), so a job on `ubuntu-latest` always installs HashiCorp's
Vagrant and never contacts Launchpad, and the cache below is only a
hedge for jobs on `ubuntu-24.04`.

## Usage

In the acceptance job:

```yaml
- name: Setup libvirt for Vagrant
uses: simp/gha-workflows/.github/actions/setup-vagrant-libvirt@v1.1.0
- name: beaker
env:
BEAKER_HYPERVISOR: 'vagrant_libvirt'
VAGRANT_DEFAULT_PROVIDER: 'libvirt'
run: bundle exec rake beaker:suites[default,almalinux9]
```

### Warming the cache

**The cache only helps pull requests if the default branch fills it.**
A pull request can restore caches saved from its own ref, its base branch
or the default branch, but not caches saved by other pull requests. Add a
separate workflow that runs this action with `cache-only: 'true'` on the
default branch, on a runner whose Ubuntu series the PPA publishes for:

```yaml
name: Vagrant package cache

'on':
push:
branches: [main]
schedule:
- cron: '17 6 * * 1,4'
workflow_dispatch: {}

jobs:
cache:
runs-on: ubuntu-24.04
steps:
- uses: simp/gha-workflows/.github/actions/setup-vagrant-libvirt@v1.1.0
with:
cache-only: 'true'
```

A `cache-only` run asks apt which packages the PPA would currently supply
and derives the cache key from their file names. If an entry with that key
already exists, the run restores it, which resets GitHub's seven-day
eviction clock. Otherwise it downloads the packages and saves a new entry.
A new PPA build therefore gets a new key, and installs restore the newest
key for their Ubuntu series and architecture. The run fails if the PPA is
still unreachable after the retries, so an outage shows up in the
scheduled run.

GitHub disables scheduled workflows in public repos after 60 days without
activity, so on a quiet repo the cache can expire and the next pull
request starts cold.

## Inputs

| Input | Default | Description |
|---|---|---|
| `cache-only` | `'false'` | `'true'` saves the PPA's packages to the Actions cache instead of installing anything |
| `cache` | `'true'` | `'false'` ignores the Actions cache when installing |
| `ppa-series` | `'noble'` | Space-separated Ubuntu codenames the PPA publishes for |
| `vagrant-version` | `'2.4.9-1'` | HashiCorp's apt version of `vagrant`, for the fallback |
| `vagrant-libvirt-version` | `'0.12.2'` | The `vagrant-libvirt` plugin version for the fallback |

## Outputs

| Output | Description |
|---|---|
| `source` | Install: `cache`, `ppa` or `hashicorp`. Cache-only: `cached` (already cached), `saved`, or `none` (the PPA doesn't publish for this series) |
123 changes: 123 additions & 0 deletions .github/actions/setup-vagrant-libvirt/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
name: 'Set up Vagrant with libvirt'
description: >-
Install Vagrant, vagrant-libvirt and libvirt on an Ubuntu runner from
ppa:evgeni/vagrant, the Actions cache, or HashiCorp's apt repository,
so a Launchpad outage doesn't fail the job.

inputs:
cache-only:
description: >-
'true' to save the PPA's packages to the Actions cache instead of
installing anything. Run it from the default branch (push, schedule)
so pull requests can restore the cache.
required: false
default: 'false'
cache:
description: "'false' to ignore the Actions cache when installing"
required: false
default: 'true'
ppa-series:
description: >-
Space-separated Ubuntu codenames that ppa:evgeni/vagrant publishes
for. On any other series the action goes straight to HashiCorp.
required: false
default: 'noble'
Comment thread
michael-riddle marked this conversation as resolved.
Resolved
vagrant-version:
description: "HashiCorp's apt version of vagrant, for the fallback when the PPA can't be used"
required: false
default: '2.4.9-1'
vagrant-libvirt-version:
description: 'The vagrant-libvirt plugin version installed with HashiCorp vagrant'
required: false
default: '0.12.2'

outputs:
source:
description: >-
Where Vagrant came from: 'cache', 'ppa' or 'hashicorp' (install), or
'cached', 'saved' or 'none' (cache-only)
value: ${{ steps.install.outputs.source || steps.result.outputs.source }}

runs:
using: composite
steps:
- name: Check the Ubuntu series
id: series
shell: bash
env:
PPA_SERIES: ${{ inputs.ppa-series }}
run: '"$GITHUB_ACTION_PATH/setup-vagrant-libvirt.bash" series'

# Install: restore the newest cached package set, if any
- name: Restore the cached PPA packages
if: >-
inputs.cache-only != 'true' && inputs.cache != 'false' &&
steps.series.outputs.ppa == 'true'
uses: actions/cache/restore@v6
with:
path: ${{ steps.series.outputs.dir }}
key: ${{ steps.series.outputs.prefix }}
restore-keys: ${{ steps.series.outputs.prefix }}

- name: Install Vagrant and libvirt
id: install
if: inputs.cache-only != 'true'
shell: bash
env:
DEBS: ${{ steps.series.outputs.dir }}
PPA: ${{ steps.series.outputs.ppa }}
CODENAME: ${{ steps.series.outputs.codename }}
VAGRANT_VERSION: ${{ inputs.vagrant-version }}
VAGRANT_LIBVIRT_VERSION: ${{ inputs.vagrant-libvirt-version }}
run: '"$GITHUB_ACTION_PATH/setup-vagrant-libvirt.bash" install'

# Cache-only: name the PPA's current packages, then save them once
- name: Resolve the PPA packages
id: resolve
if: inputs.cache-only == 'true' && steps.series.outputs.ppa == 'true'
shell: bash
env:
PREFIX: ${{ steps.series.outputs.prefix }}
run: '"$GITHUB_ACTION_PATH/setup-vagrant-libvirt.bash" resolve'

# A restore of an existing key also keeps it from being evicted
- name: Check the cache
id: cached
if: steps.resolve.outcome == 'success'
uses: actions/cache/restore@v6
with:
path: ${{ steps.series.outputs.dir }}
key: ${{ steps.resolve.outputs.key }}

- name: Download the PPA packages
if: steps.resolve.outcome == 'success' && steps.cached.outputs.cache-hit != 'true'
shell: bash
env:
DEBS: ${{ steps.series.outputs.dir }}
run: '"$GITHUB_ACTION_PATH/setup-vagrant-libvirt.bash" download'

- name: Save the PPA packages
if: steps.resolve.outcome == 'success' && steps.cached.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
path: ${{ steps.series.outputs.dir }}
key: ${{ steps.resolve.outputs.key }}

- name: Report the cache result
id: result
if: inputs.cache-only == 'true'
shell: bash
env:
PPA: ${{ steps.series.outputs.ppa }}
CODENAME: ${{ steps.series.outputs.codename }}
HIT: ${{ steps.cached.outputs.cache-hit }}
run: |
if [ "$PPA" != 'true' ]; then
echo "::notice::ppa:evgeni/vagrant has no packages for Ubuntu ${CODENAME}; nothing to cache"
echo 'source=none' >> "$GITHUB_OUTPUT"
elif [ "$HIT" = 'true' ]; then
echo 'source=cached' >> "$GITHUB_OUTPUT"
else
echo 'source=saved' >> "$GITHUB_OUTPUT"
fi
169 changes: 169 additions & 0 deletions .github/actions/setup-vagrant-libvirt/setup-vagrant-libvirt.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/bin/bash
#
# The steps of action.yml. Usage: setup-vagrant-libvirt.bash COMMAND
#
# series report the Ubuntu codename, whether the PPA publishes for it,
# and the cache directory and key prefix
# install install Vagrant, vagrant-libvirt and libvirt from the cached
# PPA packages, else the PPA, else HashiCorp and rubygems.org
# resolve add the PPA and name the packages it would install, with a
# cache key derived from their file names
# download download the resolved packages into the cache directory
# (run in the same job as resolve, which adds the PPA)
#
# ppa:evgeni/vagrant has vagrant 2.3.7, the last MPL-licensed release, and
# a matching vagrant-libvirt. Ubuntu dropped both after 23.10.

set -euo pipefail

PPA_NAME='ppa:evgeni/vagrant'
PPA_URL='https://ppa.launchpadcontent.net/evgeni/vagrant/'
PPA_LIST="${RUNNER_TEMP:-/tmp}/vagrant-ppa-debs.list"
LIBVIRT_PACKAGES=(libvirt-daemon-system libvirt-daemon qemu-system-x86 qemu-utils dnsmasq)

# Up to three attempts, 30s and then 60s apart
retry() {
local delay
for delay in 30 60 0; do
"$@" && return 0
if [ "$delay" -eq 0 ]; then
return 1
fi
echo "::notice::Retrying in ${delay}s: $*"
sleep "$delay"
done
}

ppa_sources() {
compgen -G '/etc/apt/sources.list.d/evgeni-ubuntu-vagrant-*' || true
}

# Refresh the PPA's index alone, failing on any error. A plain
# `apt-get update` only warns when a source is unreachable.
update_ppa() {
local sources
sources=$(ppa_sources)
[ -n "$sources" ] || return 1
sudo apt-get update --error-on=any \
-o Dir::Etc::sourcelist="$sources" \
-o Dir::Etc::sourceparts=- \
-o APT::Get::List-Cleanup=0
}

add_ppa() {
retry sudo add-apt-repository -y -n "$PPA_NAME" && retry update_ppa
}

# Removing the PPA with add-apt-repository can itself need Launchpad
remove_ppa() {
sudo rm -f /etc/apt/sources.list.d/evgeni-ubuntu-vagrant-*
}

series() {
local ppa=false arch s
# shellcheck source=/dev/null
. /etc/os-release
arch=$(dpkg --print-architecture)
for s in ${PPA_SERIES:-}; do
if [ "$s" = "$VERSION_CODENAME" ]; then
ppa=true
fi
done
{
echo "codename=${VERSION_CODENAME}"
echo "ppa=${ppa}"
echo "dir=${RUNNER_TEMP}/vagrant-ppa-debs"
echo "prefix=vagrant-ppa-debs-${VERSION_CODENAME}-${arch}-"
} >> "$GITHUB_OUTPUT"
}

from_cache() {
if ! compgen -G "${DEBS}/*.deb" > /dev/null; then
return 1
fi
echo "Installing the cached PPA packages: $(cd "$DEBS" && echo *.deb)"
if retry sudo apt-get update &&
retry sudo apt-get install -y --no-install-recommends "$DEBS"/*.deb "${LIBVIRT_PACKAGES[@]}"; then
return 0
fi
echo '::warning::Installing the cached PPA packages failed; trying the PPA'
return 1
}

from_ppa() {
if [ "${PPA:-}" != 'true' ]; then
return 1
fi
retry sudo apt-get update || true
if add_ppa && retry sudo apt-get install -y --no-install-recommends vagrant vagrant-libvirt "${LIBVIRT_PACKAGES[@]}"; then
return 0
fi
remove_ppa
return 1
}

from_hashicorp() {
local fallback="BSL-licensed Vagrant ${VAGRANT_VERSION} from HashiCorp and vagrant-libvirt ${VAGRANT_LIBVIRT_VERSION} from rubygems.org"
if [ "${PPA:-}" = 'true' ]; then
echo "::warning::ppa:evgeni/vagrant is unavailable, so this job uses ${fallback}"
else
echo "::notice::ppa:evgeni/vagrant has no packages for Ubuntu ${CODENAME}, so this job uses ${fallback}"
fi
retry curl -fsSL -o "${RUNNER_TEMP}/hashicorp.asc" https://apt.releases.hashicorp.com/gpg
sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg "${RUNNER_TEMP}/hashicorp.asc"
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com ${CODENAME} main" |
sudo tee /etc/apt/sources.list.d/hashicorp.list
retry sudo apt-get update
retry sudo apt-get install -y --no-install-recommends "vagrant=${VAGRANT_VERSION}" \
libvirt-dev build-essential pkg-config "${LIBVIRT_PACKAGES[@]}"
retry vagrant plugin install vagrant-libvirt --plugin-version "$VAGRANT_LIBVIRT_VERSION"
}

install() {
local source
if from_cache; then
source=cache
elif from_ppa; then
source=ppa
else
from_hashicorp
source=hashicorp
fi
sudo chmod 666 /var/run/libvirt/libvirt-sock
echo "Installed $(vagrant --version) (source: ${source})"
echo "source=${source}" >> "$GITHUB_OUTPUT"
}

# The PPA's packages that `apt-get install vagrant vagrant-libvirt` would
# fetch, as apt's "'URL' FILE SIZE HASH" lines. Archive packages are left
# out: the install step gets those from the archive either way.
resolve() {
local key
retry sudo apt-get update || true
add_ppa
apt-get install --print-uris -qq vagrant vagrant-libvirt | grep -F "'${PPA_URL}" > "$PPA_LIST" || true
if [ ! -s "$PPA_LIST" ]; then
echo '::error::ppa:evgeni/vagrant provides none of the packages for vagrant and vagrant-libvirt'
return 1
fi
cat "$PPA_LIST"
key="${PREFIX}$(awk '{ print $2 }' "$PPA_LIST" | sort | sha256sum | cut -c1-16)"
echo "key=${key}" >> "$GITHUB_OUTPUT"
}

# apt-get download checks each file against the PPA's signed index
download() {
local packages
mapfile -t packages < <(awk '{ split($2, file, "_"); print file[1] }' "$PPA_LIST")
mkdir -p "$DEBS"
(cd "$DEBS" && retry apt-get download "${packages[@]}")
ls -l "$DEBS"
}

case "${1:-}" in
series | install | resolve | download) "$1" ;;
*)
echo "Usage: $0 series|install|resolve|download" >&2
exit 2
;;
esac
Loading
Loading