Files
fips/testing/nat/scripts/setup-topology.sh
Johnathan Corgan f97d3648b9 Scope install-suite images and suite container labels to the CI run
Scope the deb-install and dns-resolver e2e image tags to the run.

Both suites bake the run's artifact into an image they build themselves:
deb-install copies the package in, and the dns-resolver e2e scenarios copy
the fips and fips-gateway binaries in. The tags, fips-deb-test:<distro> and
fips-dns-test:e2e-<distro>, were the same for every run while the container
names already carried the run's suffix, so two local CI runs on one Docker
daemon rebuilt each other's tags, and a run that built just before another
booted the other run's package or binaries. The RPM suite on master failed
this way on the internal builder.

The tags now end in ${FIPS_CI_NAME_SUFFIX}, the images are built with the
generic and run labels (a CI_LABELS array in testing/lib/image-build.sh,
whose build_inline now passes extra arguments to docker build), and each
scenario removes its image when it ends. A run killed before that leaves
its images to ci-local.sh's teardown, which already reaps every image
carrying the run label; the Debian upgrade image, already suffixed, gets
the labels too.

After boot each scenario checks that the container holds this run's
artifact by sha256, the package in deb-install and both binaries in
dns-resolver, and stops the scenario if not. Two runs of one commit build
packages of the same name, so only the hash tells them apart.

check-image-scoping.sh now also fails a fips-deb-test or fips-dns-test:e2e-
tag that does not carry the run suffix. dns-resolver's other fips-dns-test
tags hold nothing of the run and stay shared.

Give every suite container the CI run label, and bound the systemd waits.

A dns-resolver run cancelled mid-scenario left its container running on
the internal builder for three days. The suite's containers carried only
the generic com.corganlabs.fips-ci=1 label; ci-local.sh's teardown reaps
by the run label, so it passed the container over, and the builder's
orphan sweep, which is skipped while any generic-labelled container is
up, stopped for every run after it. Cancelling a run this way on a local
host reproduces it: the teardown runs and the container survives.

The install suites, native-api and mdns now take their labels from the
CI_LABELS array in testing/lib/image-build.sh, which all of them source,
so the run label cannot be left off one site; the NAT topology helper,
which does not source the library, adds the run label beside the generic
one as its sibling scripts do. dns-resolver also gets a TERM/INT/EXIT trap
that removes the scenario container and e2e image in use, since a SIGKILL
30 seconds after the TERM leaves ci-local.sh's teardown no time.

testing/check-ci-labels.py, run in local CI's guards and the CI parity
job, fails any docker run, create, build, or network or volume create
whose labels, with variables and arrays expanded, include the generic
label without the run label, and any argument array that does.

The scenario had stalled for about 36 minutes before it was cancelled,
and `systemctl is-system-running --wait` could have been where: it blocks
until boot finishes, so the wait loops in dns-resolver, deb-install and
tarball-install, which bounded only their attempt count, could block for
ever on one call. Each call now runs under `timeout` inside the container
(systemd_state in systemd-container.sh), as do dns-resolver's boot
diagnostics and its fips-dns-setup and fips-dns-teardown runs, so a stall
ends and names its step.
2026-10-22 07:20:26 +00:00

229 lines
7.5 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NAT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ROOT_DIR="$(cd "$NAT_DIR/../.." && pwd)"
SCENARIO="${1:?usage: setup-topology.sh <cone|symmetric>}"
case "$SCENARIO" in
cone)
node_a="fips-nat-cone-a${FIPS_CI_NAME_SUFFIX:-}"
node_b="fips-nat-cone-b${FIPS_CI_NAME_SUFFIX:-}"
;;
symmetric)
node_a="fips-nat-symmetric-a${FIPS_CI_NAME_SUFFIX:-}"
node_b="fips-nat-symmetric-b${FIPS_CI_NAME_SUFFIX:-}"
;;
*)
echo "Unsupported topology scenario: $SCENARIO" >&2
exit 1
;;
esac
router_a="fips-nat-router-a${FIPS_CI_NAME_SUFFIX:-}"
router_b="fips-nat-router-b${FIPS_CI_NAME_SUFFIX:-}"
# Host interface names live in one global namespace and are capped at
# IFNAMSIZ-1 = 15 characters — far short of the run suffix, which is 20-plus at
# the default run-id length. Two concurrent runs both creating `vna0` damage
# each other: setup_pair deletes, adds and moves the interface as separate
# commands, so B's re-add between A's add and A's move sends B's interface
# into A's namespace and fails B's own move. Running them in one helper
# container narrows that window but does not close it; the names still have to
# differ.
#
# Scope the names with the same four-hex token the chaos simulation uses, and
# derive it by calling sim.naming rather than re-implementing sha1 here, so
# ci-cleanup.sh's reaper (which calls the same module) cannot end up matching a
# different width. Empty suffix yields an empty token and today's exact names.
#
# No fallback on a derivation failure: silently reverting to `vna0` would
# reinstate the collision this exists to remove, so let it fail loudly.
veth_token() {
local suffix="${FIPS_CI_NAME_SUFFIX:-}"
if [ -z "$suffix" ]; then
echo ""
return 0
fi
PYTHONPATH="$ROOT_DIR/testing/chaos" python3 -m sim.naming "$suffix"
}
helper_image() {
if [ -n "${IP_HELPER_IMAGE:-}" ]; then
echo "$IP_HELPER_IMAGE"
return 0
fi
docker inspect -f '{{.Config.Image}}' "$router_a"
}
wait_for_pid() {
local container="$1"
local timeout_secs="${2:-30}"
local deadline=$((SECONDS + timeout_secs))
local pid="" status=""
while [ "$SECONDS" -lt "$deadline" ]; do
read -r pid status < <(docker inspect -f '{{.State.Pid}} {{.State.Status}}' "$container" 2>/dev/null) || true
if [[ "$pid" =~ ^[0-9]+$ ]] && [ "$pid" -gt 0 ]; then
echo "$pid"
return 0
fi
# An exited container never gets a PID back; say so now rather than
# after the whole wait.
if [ "$status" = exited ] || [ "$status" = dead ]; then
break
fi
sleep 0.5
done
echo "Timed out waiting for container PID: $container" >&2
return 1
}
# Fail, naming the container, if it is no longer running.
#
# Both entrypoints give up and exit if their interface has not arrived within
# their wait, and a namespace move into an exited container then fails with
# only `Invalid "netns" value` and a stale PID. This turns that into a line
# that says which container exited, with its exit code and the end of its log.
require_running() {
local container="$1"
local state="" running="" code=""
state="$(docker inspect -f '{{.State.Running}} {{.State.ExitCode}}' "$container" 2>/dev/null)" || state=""
if [ -z "$state" ]; then
echo "$container does not exist; it was removed before its interface was moved" >&2
return 1
fi
read -r running code <<<"$state"
if [ "$running" != true ]; then
echo "$container exited (code $code) before its interface was moved; last 20 log lines:" >&2
docker logs --tail 20 "$container" 2>&1 | sed 's/^/ /' >&2 || true
return 1
fi
return 0
}
# Create one veth pair on the host and move each end into its container's
# network namespace, in a single helper container.
#
# One container per pair, not one per command: starting a container is the
# slow part, and it has been measured at 3 to 15 s at a time while other labs
# start on the same daemon. Ten serial starts then outran the entrypoints'
# interface waits. The script echoes each step so a failure shows which one.
move_pair() {
local image="$1" host_node="$2" host_router="$3" node_pid="$4" router_pid="$5"
docker run --rm \
--label com.corganlabs.fips-ci=1 \
--label "com.corganlabs.fips-ci.run=${FIPS_CI_RUN_ID:-manual}" \
--privileged \
--net=host \
--pid=host \
--entrypoint sh \
"$image" \
-c '
set -e
echo "topology: delete stale $1 and $2"
ip link delete "$1" 2>/dev/null || true
ip link delete "$2" 2>/dev/null || true
echo "topology: add veth $1 <-> $2"
ip link add "$1" type veth peer name "$2"
echo "topology: move $1 into netns of pid $3"
ip link set "$1" netns "$3"
echo "topology: move $2 into netns of pid $4"
ip link set "$2" netns "$4"
' sh "$host_node" "$host_router" "$node_pid" "$router_pid" || return 1
return 0
}
configure_node_iface() {
local container="$1"
local current_name="$2"
local final_name="$3"
local cidr="$4"
docker exec "$container" sh -lc "
ip link set lo up &&
ip link set '$current_name' name '$final_name' &&
ip addr flush dev '$final_name' &&
ip addr add '$cidr' dev '$final_name' &&
ip link set '$final_name' up
"
}
configure_router_iface() {
local container="$1"
local current_name="$2"
local final_name="$3"
local cidr="$4"
docker exec "$container" sh -lc "
ip link set lo up &&
ip link set '$current_name' name '$final_name' &&
ip addr flush dev '$final_name' &&
ip addr add '$cidr' dev '$final_name' &&
ip link set '$final_name' up
"
}
setup_pair() {
local image="$1"
local node_container="$2"
local router_container="$3"
local host_node="$4"
local host_router="$5"
local node_cidr="$6"
local router_cidr="$7"
local node_pid router_pid
node_pid="$(wait_for_pid "$node_container")" || {
require_running "$node_container" || true
return 1
}
router_pid="$(wait_for_pid "$router_container")" || {
require_running "$router_container" || true
return 1
}
require_running "$node_container" || return 1
require_running "$router_container" || return 1
# Run both checks after a failure, so each container that exited mid-run
# is named, not only the first.
if ! move_pair "$image" "$host_node" "$host_router" "$node_pid" "$router_pid"; then
require_running "$node_container" || true
require_running "$router_container" || true
return 1
fi
if ! configure_node_iface "$node_container" "$host_node" eth0 "$node_cidr"; then
require_running "$node_container" || true
return 1
fi
if ! configure_router_iface "$router_container" "$host_router" eth1 "$router_cidr"; then
require_running "$router_container" || true
return 1
fi
return 0
}
main() {
cd "$NAT_DIR"
local image token
image="$(helper_image)"
token="$(veth_token)"
setup_pair "$image" "$node_a" "$router_a" \
"vna${token}0" "vna${token}1" 172.31.1.10/24 172.31.1.254/24 || exit 1
setup_pair "$image" "$node_b" "$router_b" \
"vnb${token}0" "vnb${token}1" 172.31.2.10/24 172.31.2.254/24 || exit 1
}
main "$@"