1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
|
#!/bin/bash
# This file is part of cloud-init. See LICENSE file for license information.
#
# shellcheck disable=2015,2016,2039,2162,2166
set -u
VERBOSITY=0
KEEP=false
CONTAINER=""
WAIT_MAX=30
LXC=${LXC:-lxc}
error() { echo "$@" 1>&2; }
fail() { [ $# -eq 0 ] || error "$@"; exit 1; }
errorrc() { local r=$?; error "$@" "ret=$r"; return $r; }
Usage() {
cat <<EOF
Usage: ${0##*/} [ options ] [images:]image-ref
This utility can makes it easier to run tests, build rpm and source rpm
generation inside a LXC of the specified version of CentOS.
To see images available, run 'lxc image list images:'
Example input:
rockylinux/9
opensuse/15.4
debian/10
options:
-a | --artifacts DIR copy build artifacts out to DIR.
by default artifacts are not copied out.
--dirty apply local changes before running tests.
If not provided, a clean checkout of branch is
tested. Inside container, changes are in
local-changes.diff.
-k | --keep keep container after tests
-p | --package build a binary package (.deb or .rpm)
-s | --source-package build source package (debuild -S or srpm)
-u | --unittest run unit tests
--vm use a VM instead of a container
--wait-max max time to wait or a container or VM to be ready
--commitish commit to package and run, default: HEAD
Example:
* ${0##*/} --package --source-package --unittest centos/6
EOF
}
bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; }
cleanup() {
if [ -n "$CONTAINER" ]; then
if [ "$KEEP" = "true" ]; then
error "not deleting container '$CONTAINER' due to --keep"
else
delete_instance "$CONTAINER"
fi
fi
}
debug() {
local level=${1}; shift;
[ "${level}" -gt "${VERBOSITY}" ] && return
error "${@}"
}
inside_as() {
# inside_as(container_name, user, cmd[, args])
# executes cmd with args inside container as user in users home dir.
local name="$1" user="$2"
shift 2
if [ "$user" = "root" ]; then
inside "$name" "$@"
return
fi
local stuffed="" b64=""
stuffed=$(getopt --shell sh --options "" -- -- "$@")
stuffed=${stuffed# -- }
b64=$(printf "%s\n" "$stuffed" | base64 --wrap=0)
inside "$name" su "$user" -c \
'cd; eval set -- "$(echo '"$b64"' | base64 --decode)" && exec "$@"';
}
inside_as_cd() {
local name="$1" user="$2" dir="$3"
shift 3
inside_as "$name" "$user" sh -c 'cd "$0" && exec "$@"' "$dir" "$@"
}
inside() {
local name="$1"
shift
$LXC exec "$name" -- "$@"
}
inject_cloud_init(){
# take current cloud-init git dir and put it inside $name at
# ~$user/cloud-init.
local name="$1" user="$2" dirty="$3"
local dname="cloud-init" gitdir="" commitish=""
gitdir=$(git rev-parse --git-dir) || {
errorrc "Failed to get git dir in $PWD";
return
}
local t=${gitdir%/*}
case "$t" in
*/worktrees)
if [ -f "${t%worktrees}/config" ]; then
gitdir="${t%worktrees}"
fi
esac
# attempt to get branch name.
commitish=$(git rev-parse --abbrev-ref "$COMMITISH") || {
errorrc "Failed git rev-parse --abbrev-ref $COMMITISH"
return
}
if [ "$commitish" = "HEAD" ]; then
# detached head
commitish=$(git rev-parse HEAD) || {
errorrc "failed git rev-parse HEAD"
return
}
fi
local local_changes=false
if ! git diff --quiet "$commitish"; then
# there are local changes not committed.
local_changes=true
if [ "$dirty" = "false" ]; then
error "WARNING: You had uncommitted changes. Those changes will "
error "be put into 'local-changes.diff' inside the container. "
error "To test these changes you must pass --dirty."
fi
fi
debug 1 "collecting ${gitdir} ($dname) into user $user in $name."
tar -C "${gitdir}" -cpf - . |
inside_as "$name" "$user" sh -ec '
dname=$1
commitish=$2
rm -Rf "$dname"
mkdir -p $dname/.git
cd $dname/.git
tar -xpf -
cd ..
git config core.bare false
out=$(git checkout $commitish 2>&1) ||
{ echo "failed git checkout $commitish: $out" 1>&2; exit 1; }
out=$(git checkout . 2>&1) ||
{ echo "failed git checkout .: $out" 1>&2; exit 1; }
' extract "$dname" "$commitish"
[ "${PIPESTATUS[*]}" = "0 0" ] || {
error "Failed to push tarball of '$gitdir' into $name" \
" for user $user (dname=$dname)"
return 1
}
echo "local_changes=$local_changes dirty=$dirty"
if [ "$local_changes" = "true" ]; then
git diff "$commitish" |
inside_as "$name" "$user" sh -exc '
cd "$1"
if [ "$2" = "true" ]; then
git apply
else
cat > local-changes.diff
fi
' insert_changes "$dname" "$dirty"
[ "${PIPESTATUS[*]}" = "0 0" ] || {
error "Failed to apply local changes."
return 1
}
fi
return 0
}
get_os_info_in() {
# prep the container (install very basic dependencies)
[ -n "${OS_VERSION:-}" -a -n "${OS_NAME:-}" ] && return 0
data=$(run_self_inside "$name" os_info) ||
{ errorrc "Failed to get os-info in container $name"; return; }
eval "$data" && [ -n "${OS_VERSION:-}" -a -n "${OS_NAME:-}" ] || return
debug 1 "determined $name is $OS_NAME/$OS_VERSION"
}
os_info() {
get_os_info || return
echo "OS_NAME=$OS_NAME"
echo "OS_VERSION=$OS_VERSION"
}
get_os_info() {
# run inside container, set OS_NAME, OS_VERSION
# example OS_NAME are centos, debian, opensuse, rockylinux
[ -n "${OS_NAME:-}" -a -n "${OS_VERSION:-}" ] && return 0
if [ -f /etc/os-release ]; then
OS_NAME=$(sh -c '. /etc/os-release; echo $ID')
OS_VERSION=$(sh -c '. /etc/os-release; echo $VERSION_ID')
if [ -z "$OS_VERSION" ]; then
local pname=""
pname=$(sh -c '. /etc/os-release; echo $PRETTY_NAME')
case "$pname" in
*buster*) OS_VERSION=10;;
*sid*) OS_VERSION="sid";;
esac
fi
elif [ -f /etc/centos-release ]; then
local line=""
read line < /etc/centos-release
case "$line" in
CentOS\ *\ 6.*) OS_VERSION="6"; OS_NAME="centos";;
esac
fi
[ -n "${OS_NAME:-}" -a -n "${OS_VERSION:-}" ] ||
{ error "Unable to determine OS_NAME/OS_VERSION"; return 1; }
}
yum_install() {
local n=0 max=10 ret
bcmd="yum install --downloadonly --assumeyes --setopt=keepcache=1"
while n=$((n+1)); do
error ":: running $bcmd $* [$n/$max]"
$bcmd "$@"
ret=$?
[ $ret -eq 0 ] && break
[ $n -ge $max ] && { error "gave up on $bcmd"; exit $ret; }
nap=$((n*5))
error ":: failed [$ret] ($n/$max). sleeping $nap."
sleep $nap
done
error ":: running yum install --cacheonly --assumeyes $*"
yum install --cacheonly --assumeyes "$@"
}
zypper_install() {
local pkgs="$*"
set -- zypper --non-interactive --gpg-auto-import-keys install \
--auto-agree-with-licenses "$@"
debug 1 ":: installing $pkgs with zypper: $*"
"$@"
}
apt_install() {
apt-get update -q && apt-get install --no-install-recommends "$@"
}
install_packages() {
get_os_info || return
case "$OS_NAME" in
centos|rocky*|fedora) yum_install "$@";;
opensuse*) zypper_install "$@";;
debian|ubuntu) apt_install "$@" -y;;
*) error "Do not know how to install packages on ${OS_NAME}";
return 1;;
esac
}
prep() {
# we need some very basic things not present in the container.
# - git
# - tar (CentOS 6 lxc container does not have it)
# - python3
local needed="" pair="" pkg="" cmd="" needed=""
local pairs="tar:tar git:git"
get_os_info
local py3pkg="python3"
case "$OS_NAME" in
opensuse)
py3pkg="python3-base";;
esac
pairs="$pairs python3:$py3pkg"
for pair in $pairs; do
pkg=${pair#*:}
cmd=${pair%%:*}
command -v "$cmd" >/dev/null 2>&1 || needed="${needed} $pkg"
done
needed=${needed# }
if [ -z "$needed" ]; then
error "No prep packages needed"
return 0
fi
error "Installing prep packages: ${needed}"
# shellcheck disable=SC2086
set -- $needed
install_packages "$@"
}
pytest() {
python3 -m pytest "$@"
}
is_done_cloudinit() {
[ -e "/run/cloud-init/result.json" ]
_RET=""
}
is_done_systemd() {
local s="" num="$1"
s=$(systemctl is-system-running 2>&1);
_RET="$? $s"
case "$s" in
initializing|starting) return 1;;
*[Ff]ailed*connect*bus*)
# warn if not the first run.
[ "$num" -lt 5 ] ||
error "Failed to connect to systemd bus [${_RET%% *}]";
return 1;;
esac
return 0
}
is_done_other() {
local out=""
out=$(getent hosts ubuntu.com 2>&1)
return
}
wait_inside() {
local name="$1" max="${2:-${WAIT_MAX}}" debug=${3:-0}
local i=0 check="is_done_other";
if [ -e /run/systemd ]; then
check=is_done_systemd
elif [ -x /usr/bin/cloud-init ]; then
check=is_done_cloudinit
fi
[ "$debug" != "0" ] && debug 1 "check=$check"
while ! $check $i && i=$((i+1)); do
[ "$i" -ge "$max" ] && exit 1
[ "$debug" = "0" ] || echo -n .
sleep 1
done
if [ "$debug" != "0" ]; then
read up _ </proc/uptime
debug 1 "[$name ${i:+done after $i }up=$up${_RET:+ ${_RET}}]"
fi
}
wait_for_boot() {
local name="$1"
local out="" ret="" wtime=$WAIT_MAX
local system_up=false
for i in {0..30}; do
[ "$i" -gt 1 ] && sleep 5
inside "$name" true 2>/dev/null && system_up=true && break
done
[ $system_up == true ] || { errorrc "exec command inside $name failed."; return; }
get_os_info_in "$name"
[ "$OS_NAME" = "debian" ] && wtime=300 &&
debug 1 "on debian we wait for ${wtime}s"
debug 1 "waiting for boot of $name"
run_self_inside "$name" wait_inside "$name" "$wtime" "$VERBOSITY" ||
{ errorrc "wait inside $name failed."; return; }
if [ -n "${http_proxy-}" ]; then
if [ "$OS_NAME" = "centos" -o "$OS_NAME" = "fedora" ]; then
debug 1 "configuring proxy ${http_proxy}"
inside "$name" sh -c "echo proxy=$http_proxy >> /etc/yum.conf"
inside "$name" sh -c "sed -i --regexp-extended '/^#baseurl=/s/#// ; /^(mirrorlist|metalink)=/s/^/#/' /etc/yum.repos.d/*.repo"
inside "$name" sh -c "sed -i 's/download\.fedoraproject\.org/dl.fedoraproject.org/g' /etc/yum.repos.d/*.repo"
inside "$name" sh -c "sed -i 's/download\.example/dl.fedoraproject.org/g' /etc/yum.repos.d/*.repo"
if [ "$OS_NAME" = "centos" ]; then
CENTOS_REPO="/etc/yum.repos.d/centos.repo"
inside "$name" sh -c "grep -q baseurl $CENTOS_REPO"
if [ $? -eq 1 ]; then
# CentOS 9 does not provide baseurl definitions
inside "$name" sh -c "sed -i '/\[baseos\]/a baseurl=https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os' ${CENTOS_REPO}"
inside "$name" sh -c "sed -i '/\[appstream\]/a baseurl=https://mirror.stream.centos.org/9-stream/AppStream/x86_64/os' ${CENTOS_REPO}"
inside "$name" sh -c "sed -i '/\[crb\]/a baseurl=https://mirror.stream.centos.org/9-stream/CRB/x86_64/os' ${CENTOS_REPO}"
CENTOS_EXTRAS_REPO="/etc/yum.repos.d/centos-addons.repo"
inside "$name" sh -c "sed -i '/\[extras-common\]/a baseurl=https://mirror.stream.centos.org/SIGs/9-stream/extras/x86_64/extras-common' ${CENTOS_EXTRAS_REPO}"
inside "$name" sh -c "dnf install -y 'dnf-command(config-manager)'"
inside "$name" sh -c "dnf config-manager --set-enabled crb"
inside "$name" sh -c "dnf config-manager --set-disabled epel-cisco-openh264" || true
fi
fi
else
debug 1 "do not know how to configure proxy on $OS_NAME"
fi
fi
}
start_instance() {
local src="$1" name="$2" use_vm="$3"
debug 1 "starting container $name from '$src'"
launch_flags=()
[ "$use_vm" == true ] && launch_flags+=(--vm)
$LXC launch "$src" "$name" "${launch_flags[@]}" || {
errorrc "Failed to start container '$name' from '$src'";
return
}
CONTAINER=$name
wait_for_boot "$name"
}
delete_instance() {
debug 1 "removing container $1 [--keep to keep]"
$LXC delete --force "$1"
}
run_self_inside() {
# run_self_inside(container, args)
local name="$1"
shift
inside "$name" bash -s "$@" <"$0"
}
run_self_inside_as_cd() {
local name="$1" user="$2" dir="$3"
shift 3
inside_as_cd "$name" "$user" "$dir" bash -s "$@" <"$0"
}
main() {
local short_opts="a:hknpsuv"
local long_opts="artifacts:,dirty,help,keep,name:,package,source-package,unittest,verbose,vm,wait-max:,commitish:"
local getopt_out=""
getopt_out=$(getopt --name "${0##*/}" \
--options "${short_opts}" --long "${long_opts}" -- "$@") &&
eval set -- "${getopt_out}" ||
{ bad_Usage; return; }
local cur="" next=""
local package=false srcpackage=false unittest="" name=""
local dirty=false artifact_d="."
local use_vm=false
while [ $# -ne 0 ]; do
cur="${1:-}"; next="${2:-}";
case "$cur" in
-a|--artifacts) artifact_d="$next";;
--dirty) dirty=true;;
-h|--help) Usage ; exit 0;;
-k|--keep) KEEP=true;;
-n|--name) name="$next"; shift;;
-p|--package) package=true;;
-s|--source-package) srcpackage=true;;
-u|--unittest) unittest=1;;
-v|--verbose) VERBOSITY=$((VERBOSITY+1));;
--vm) use_vm=true;;
--wait-max) WAIT_MAX="$next"; shift;;
--commitish) COMMITISH="$next"; shift;;
--) shift; break;;
esac
shift;
done
COMMITISH=${COMMITISH:-HEAD}
[ $# -eq 1 ] || { bad_Usage "Expected 1 arg, got $# ($*)"; return; }
local img_ref_in="$1"
case "${img_ref_in}" in
*:*) img_ref="${img_ref_in}";;
*) img_ref="images:${img_ref_in}";;
esac
# program starts here
local out="" user="ci-test" cdir="" home=""
home="/home/$user"
cdir="$home/cloud-init"
if [ -z "$name" ]; then
if out=$(petname 2>&1); then
name="ci-${out}"
elif out=$(uuidgen -t 2>&1); then
name="ci-${out%%-*}"
else
error "Must provide name or have petname or uuidgen"
return 1
fi
fi
trap cleanup EXIT
start_instance "$img_ref" "$name" "$use_vm" ||
{ errorrc "Failed to start container for $img_ref"; return; }
get_os_info_in "$name" ||
{ errorrc "failed to get os_info in $name"; return; }
# prep the container (install very basic dependencies)
run_self_inside "$name" prep ||
{ errorrc "Failed to prep container $name"; return; }
# add the user
inside "$name" useradd "$user" --create-home "--home-dir=$home" ||
{ errorrc "Failed to add user '$user' in '$name'"; return 1; }
debug 1 "inserting cloud-init"
inject_cloud_init "$name" "$user" "$dirty" || {
errorrc "FAIL: injecting cloud-init into $name failed."
return
}
local rdcmd=(python3 tools/read-dependencies "--distro=${OS_NAME}" --install --test-distro)
inside_as_cd "$name" root "$cdir" "${rdcmd[@]}" || {
errorrc "FAIL: failed to install dependencies with read-dependencies"
return
}
local errors=( )
inside_as_cd "$name" "$user" "$cdir" git status || {
errorrc "git checkout failed."
errors[${#errors[@]}]="git checkout";
}
if [ -n "$unittest" ]; then
debug 1 "running unit tests."
run_self_inside_as_cd "$name" "$user" "$cdir" pytest \
tests/unittests cloudinit/ || {
errorrc "pytest failed.";
errors[${#errors[@]}]="pytest"
}
fi
local build_pkg="" build_srcpkg="" pkg_ext="" distflag=""
case "$OS_NAME" in
centos|rocky*|fedora) distflag="--distro=redhat";;
opensuse*) distflag="--distro=suse";;
esac
case "$OS_NAME" in
debian|ubuntu)
build_pkg="./packages/bddeb -d"
build_srcpkg="./packages/bddeb -S -d"
pkg_ext=".deb";;
centos|opensuse*|rocky*|fedora)
build_pkg="./packages/brpm $distflag"
build_srcpkg="./packages/brpm $distflag --srpm"
pkg_ext=".rpm";;
esac
if [ "$srcpackage" = "true" ]; then
[ -n "$build_srcpkg" ] || {
error "Unknown package command for $OS_NAME"
return 1
}
debug 1 "building source package with $build_srcpkg."
# shellcheck disable=SC2086
inside_as_cd "$name" "$user" "$cdir" python3 $build_srcpkg || {
errorrc "failed: $build_srcpkg";
errors[${#errors[@]}]="source package"
}
fi
if [ "$package" = "true" ]; then
[ -n "$build_pkg" ] || {
error "Unknown build source command for $OS_NAME"
return 1
}
debug 1 "building binary package with $build_pkg."
# shellcheck disable=SC2086
inside_as_cd "$name" "$user" "$cdir" python3 $build_pkg || {
errorrc "failed: $build_pkg";
errors[${#errors[@]}]="binary package"
}
fi
if [ -n "$artifact_d" ] &&
[ "$package" = "true" -o "$srcpackage" = "true" ]; then
local art=""
artifact_d="${artifact_d%/}/"
[ -d "${artifact_d}" ] || mkdir -p "$artifact_d" || {
errorrc "failed to create artifact dir '$artifact_d'"
return
}
for art in $(inside "$name" sh -c "echo $cdir/*${pkg_ext}"); do
$LXC file pull "$name/$art" "$artifact_d" || {
errorrc "Failed to pull '$name/$art' to ${artifact_d}"
errors[${#errors[@]}]="artifact copy: $art"
}
debug 1 "wrote ${artifact_d}${art##*/}"
done
fi
if [ "${#errors[@]}" != "0" ]; then
local e=""
error "there were ${#errors[@]} errors."
for e in "${errors[@]}"; do
error " $e"
done
return 1
fi
return 0
}
case "${1:-}" in
prep|os_info|wait_inside|pytest) _n=$1; shift; "$_n" "$@";;
*) main "$@";;
esac
|