#!/usr/bin/env bash # Copyright 2021 The Rook Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -xeEo pipefail ############# # VARIABLES # ############# REPO_DIR="$(readlink -f -- "${BASH_SOURCE%/*}/../..")" NETWORK_ERROR="connection reset by peer" SERVICE_UNAVAILABLE_ERROR="Service Unavailable" INTERNAL_ERROR="INTERNAL_ERROR" INTERNAL_SERVER_ERROR="500 Internal Server Error" # Architecture detection ARCH=$(uname -m) if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then ARCH_SUFFIX="arm64" else ARCH_SUFFIX="amd64" fi ############# # FUNCTIONS # ############# # Create one or more real block devices via a local iSCSI (LIO) target so that # tests have access to genuine SCSI disks (/dev/sd*). The argument is the number # of disks to create (default 1). All disks share a single target/TPG and are # exposed as separate LUNs, so a single initiator login attaches them all. function create_extra_disk() { local count="${1:-1}" sudo apt install -y targetcli-fb open-iscsi local init_iqn=iqn.2026-02.initiator.local local target_iqn=iqn.2026-02.target.local:disk1 echo "InitiatorName=${init_iqn}" | sudo tee /etc/iscsi/initiatorname.iscsi >/dev/null # The target and its initiator ACL are shared by all disks; create them once. # "|| true" keeps re-invocation (to append more disks) idempotent. sudo targetcli /iscsi create ${target_iqn} 2>/dev/null || true sudo targetcli /iscsi/${target_iqn}/tpg1/acls create ${init_iqn} 2>/dev/null || true # Count disks that already exist so additional disks append rather than # clobber lun0 (LIO auto-assigns LUN numbers sequentially in creation order). local existing=0 f for f in ~/iscsi-disk*.img; do if [ -e "$f" ]; then existing=$((existing + 1)) fi done local j i lun for j in $(seq 1 "$count"); do i=$((existing + j)) lun=$((i - 1)) truncate -s 75G ~/iscsi-disk${i}.img sudo targetcli /backstores/fileio create disk${i} ~/iscsi-disk${i}.img 75G # "luns create" auto-maps the new LUN into the existing ACL as # lun${lun} -> mapped_lun${lun}, so the explicit mapping below is a fallback # for targetcli versions that do not auto-map; "|| true" tolerates the # "already exists" error once it has been auto-mapped. sudo targetcli /iscsi/${target_iqn}/tpg1/luns create /backstores/fileio/disk${i} sudo targetcli /iscsi/${target_iqn}/tpg1/acls/${init_iqn} create tpg_lun_or_backstore=lun${lun} mapped_lun=${lun} 2>/dev/null || true done sudo iscsiadm -m discovery -t sendtargets -p 127.0.0.1 sudo iscsiadm -m node --login 2>/dev/null || true sudo iscsiadm -m node -R 2>/dev/null || true # rescan to pick up newly-added LUNs sudo udevadm settle 2>/dev/null || true # wait for the new /dev/sd* to appear } # Print the basename of the first non-boot/loop/nbd whole disk, provisioning an # iSCSI disk if none exist. Thin wrapper over find_extra_block_devs(); kept as a # convenience for its many callers that expect a single basename. function find_extra_block_dev() { local devs mapfile -t devs < <(find_extra_block_devs 1) echo "${devs[0]}" # the first disk } # find_extra_block_devs [min_count] # Print ALL non-boot/loop/nbd whole-disk basenames, one per line, in stable # lsblk order, provisioning additional iSCSI disks if fewer than min_count exist. # This is the single source of truth for disk discovery; find_extra_block_dev() # returns the first and find_second_block_dev() the second. Debug goes to stderr # so stdout stays clean. function find_extra_block_devs() { local min_count="${1:-1}" # shellcheck disable=SC2005 # redirect doesn't work with sudo, so use echo echo "$(sudo lsblk)" >/dev/stderr # print lsblk output to stderr for debugging # relevant lsblk --pairs example: (MOUNTPOINT identifies boot partition)(PKNAME is Parent dev ID) # NAME="sda15" SIZE="106M" TYPE="part" MOUNTPOINT="/boot/efi" PKNAME="sda" # NAME="sdb" SIZE="75G" TYPE="disk" MOUNTPOINT="" PKNAME="" # NAME="sdb1" SIZE="75G" TYPE="part" MOUNTPOINT="/mnt" PKNAME="sdb" boot_dev="$(sudo lsblk --noheading --list --output MOUNTPOINT,PKNAME | grep boot | awk '{print $2}')" local devs count # --nodeps ignores partitions devs="$(sudo lsblk --noheading --list --nodeps --output KNAME | egrep -v "($boot_dev|loop|nbd)" || true)" count="$(echo "$devs" | grep -c . || true)" if [ "$count" -lt "$min_count" ]; then create_extra_disk "$((min_count - count))" >/dev/stderr devs="$(sudo lsblk --noheading --list --nodeps --output KNAME | egrep -v "($boot_dev|loop|nbd)")" fi echo " == find_extra_block_devs(): wanted>=${min_count} devs='$(echo "$devs" | tr '\n' ' ')'" >/dev/stderr echo "$devs" # output of function } # Print the basename of a second whole disk, distinct from the primary disk # returned by find_extra_block_dev(). Ensures at least two disks exist first # (provisioning an iSCSI disk if needed). Debug goes to stderr; stdout is clean. function find_second_block_dev() { local devs mapfile -t devs < <(find_extra_block_devs 2) echo "${devs[1]}" # the second disk; find_extra_block_dev() returns the first } function block_dev() { declare -g DEFAULT_BLOCK_DEV : "${DEFAULT_BLOCK_DEV:=/dev/$(block_dev_basename)}" echo "$DEFAULT_BLOCK_DEV" } function block_dev_basename() { declare -g DEFAULT_BLOCK_DEV_BASENAME : "${DEFAULT_BLOCK_DEV_BASENAME:=$(find_extra_block_dev)}" echo "$DEFAULT_BLOCK_DEV_BASENAME" } function install_deps() { sudo wget https://github.com/mikefarah/yq/releases/download/3.4.1/yq_linux_${ARCH_SUFFIX} -O /usr/local/bin/yq sudo chmod +x /usr/local/bin/yq } function install_nvme_initiator_prerequisites() { sudo apt-get update sudo apt-get install -y nvme-cli sudo apt-get install -y "linux-modules-extra-$(uname -r)" || sudo apt-get install -y linux-modules-extra-azure || true sudo modprobe nvme_fabrics || true sudo modprobe nvme_tcp || true if [[ ! -d /sys/module/nvme_tcp ]]; then echo "nvme_tcp kernel module is unavailable on this runner kernel: $(uname -r)" exit 1 fi } function print_k8s_cluster_status() { kubectl cluster-info kubectl get pods -n kube-system } function use_local_disk() { BLOCK_DATA_PART="$(block_dev)1" sudo apt purge snapd -y sudo dmsetup version || true sudo swapoff --all --verbose if mountpoint -q /mnt; then sudo umount /mnt # search for the device since it keeps changing between sda and sdb sudo wipefs --all --force "$BLOCK_DATA_PART" else # it's the hosted runner! sudo sgdisk --zap-all -- "$(block_dev)" sudo dd if=/dev/zero of="$(block_dev)" bs=1M count=10 oflag=direct,dsync sudo parted -s "$(block_dev)" mklabel gpt fi sudo lsblk } function use_local_disk_for_integration_test() { sudo apt purge snapd -y sudo udevadm control --log-priority=debug sudo swapoff --all --verbose # Create an extra disk if doesn't exist. : "$(block_dev)" sudo lsblk # Stop udev from watching the OSD disk, on every kind of runner. Without this, every # close-after-write on the device retriggers a udev probe of the device, and the resulting # re-probe storm during OSD restarts has been seen to wedge ceph-volume activate in # uninterruptible sleep on the block device lock on runners using the iSCSI fallback disk. # See https://github.com/rook/rook/issues/8975 and https://access.redhat.com/solutions/1465913 echo "ACTION==\"add|change\", KERNEL==\"$(block_dev_basename)\", OPTIONS:=\"nowatch\"" | sudo tee -a /etc/udev/rules.d/99-z-rook-nowatch.rules sudo udevadm control --reload-rules || true sudo udevadm trigger || true time sudo udevadm settle || true unset pipefail mountpoint -q /mnt || return 0 set pipefail sudo umount /mnt sudo sed -i.bak '/\/mnt/d' /etc/fstab # search for the device since it keeps changing between sda and sdb PARTITION="$(block_dev)1" sudo wipefs --all --force "$PARTITION" sudo dd if=/dev/zero of="${PARTITION}" bs=1M count=1 sudo lsblk --bytes # add a udev rule to force the disk partitions to ceph # we have observed that some runners keep detaching/re-attaching the additional disk overriding the permissions to the default root:disk # for more details see: https://github.com/rook/rook/issues/7405 echo "SUBSYSTEM==\"block\", ATTR{size}==\"29356032\", ACTION==\"add\", RUN+=\"/bin/chown 167:167 $PARTITION\"" | sudo tee -a /etc/udev/rules.d/01-rook.rules # The partition is still getting reloaded occasionally during operation. See https://github.com/rook/rook/issues/8975 # Try issuing some disk-inspection commands to jog the system so it won't reload the partitions # during OSD provisioning. sudo udevadm control --reload-rules || true sudo udevadm trigger || true time sudo udevadm settle || true sudo partprobe || true sudo lsblk --noheadings --pairs "$(block_dev)" || true sudo sgdisk --print "$(block_dev)" || true sudo udevadm info --query=property "$(block_dev)" || true sudo lsblk --noheadings --pairs "${PARTITION}" || true journalctl -o short-precise --dmesg | tail -40 || true cat /etc/fstab || true } function create_partitions_for_osds() { tests/scripts/create-bluestore-partitions.sh --disk "$(block_dev)" --osd-count 2 sudo lsblk } function create_bluestore_partitions_and_pvcs() { BLOCK_PART="$(block_dev)2" DB_PART="$(block_dev)1" tests/scripts/create-bluestore-partitions.sh --disk "$(block_dev)" --bluestore-type block.db --osd-count 1 tests/scripts/localPathPV.sh "$BLOCK_PART" "$DB_PART" } function create_bluestore_partitions_and_pvcs_for_wal() { BLOCK_PART="$(block_dev)3" DB_PART="$(block_dev)1" WAL_PART="$(block_dev)2" tests/scripts/create-bluestore-partitions.sh --disk "$(block_dev)" --bluestore-type block.wal --osd-count 1 tests/scripts/localPathPV.sh "$BLOCK_PART" "$DB_PART" "$WAL_PART" } function collect_udev_logs_in_background() { local log_dir="${1:-"/home/runner/work/rook/rook/tests/integration/_output/tests"}" mkdir -p "${log_dir}" udevadm monitor --property &>"${log_dir}"/udev-monitor-property.txt & udevadm monitor --kernel &>"${log_dir}"/udev-monitor-kernel.txt & udevadm monitor --udev &>"${log_dir}"/udev-monitor-udev.txt & } function check_empty_file() { output_file=$1 if [ -s "$output_file" ]; then echo "script failed with stderr error" cat "$output_file" rm -f "$output_file" exit 1 fi } function build_rook() { build_type=build if [ -n "$1" ]; then build_type=$1 fi GOPATH=$(go env GOPATH) make clean for _ in $(seq 1 3); do # capture stderr too: go/make write network errors (the strings # matched below) to stderr, so without 2>&1 $o never contains them # and a transient failure skips the retry and is treated as fatal. if ! o=$(make -j"$(nproc)" "$build_type" 2>&1); then case "$o" in *"$NETWORK_ERROR"*) echo "network failure occurred, retrying..." continue ;; *"$SERVICE_UNAVAILABLE_ERROR"*) echo "network failure occurred, retrying..." continue ;; *"$INTERNAL_ERROR"*) echo "network failure occurred, retrying..." continue ;; *"$INTERNAL_SERVER_ERROR"*) echo "network failure occurred, retrying..." continue ;; *) # valid failure echo "failed with the following log:" echo "$o" exit 1 ;; esac fi # no errors so we break the loop after the first iteration break done # validate build tests/scripts/validate_modified_files.sh build docker images if [[ "$build_type" == "build" ]]; then docker tag "$(docker images | awk '/build-/ {print $1}')" docker.io/rook/ceph:local-build load_image_into_cluster docker.io/rook/ceph:local-build fi } function build_rook_all() { build_rook build.all } function ensure_kind_is_available() { # The kind-based CI helpers drive the cluster through the kind CLI; fail clearly if it is absent. command -v kind >/dev/null 2>&1 || { echo "the 'kind' command is required but was not found" >&2 exit 1 } } function load_image_into_cluster() { # Under kind, a locally built image lives only in the host docker daemon and must be # explicitly imported into each cluster node's containerd before any pod can run it. # # Import through the node's own ctr rather than `kind load docker-image`: kind's loader parses # the node's containerd config, and the kind version bundled with helm/kind-action cannot read # the config version shipped by current kindest/node images ("unknown containerd config # version: 4"). Piping `docker save` into the node's ctr sidesteps that version mismatch. ensure_kind_is_available local image="${1?image is required}" local cluster="${KIND_CLUSTER_NAME:-kind}" if ! kind get clusters 2>/dev/null | grep -qx "$cluster"; then echo "load_image_into_cluster: no kind cluster '$cluster'" >&2 exit 1 fi local node for node in $(kind get nodes --name "$cluster"); do docker save "$image" | docker exec -i "$node" ctr --namespace=k8s.io images import - done } function add_host_routes_to_cluster() { # kind runs the cluster inside a docker network, so route the Service and pod CIDRs to the kind # node so host-side tests (the `go test` process runs on the runner; e.g. an S3 PutObject to the # RGW ClusterIP) can reach in-cluster services. ensure_kind_is_available local cluster="${KIND_CLUSTER_NAME:-kind}" local node ip node=$(kind get nodes --name "$cluster" | grep control-plane | head -1) ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$node") [ -n "$ip" ] || { echo "ERROR: no kind node IP for routing" >&2; return 1; } # kind defaults: service subnet 10.96.0.0/16, pod subnet 10.244.0.0/16 sudo ip route replace 10.96.0.0/16 via "$ip" sudo ip route replace 10.244.0.0/16 via "$ip" ip route show | grep -E '10\.(96|244)\.' || true # diagnostic print only } function prepare_kind_node() { # Prepare each kind node for the host-level operations rook performs against the node's host # (remount /sys rw for krbd; install lvm2/cryptsetup for LVM/encryption OSDs). Best-effort: a # failure must not break the raw-device OSD jobs that need none of it. ensure_kind_is_available local cluster="${KIND_CLUSTER_NAME:-kind}" local node for node in $(kind get nodes --name "$cluster"); do # kind mounts the node's /sys read-only, so CSI's `rbd map --device-type krbd` fails with # "rbd: sysfs write failed ... Read-only file system" writing /sys/bus/rbd/add. The node is # privileged, so remount /sys read-write to let kernel RBD mapping work. docker exec "$node" mount -o remount,rw /sys || echo "WARNING: could not remount /sys rw in kind node $node" >&2 # Rook provisions LVM- and encryption-backed OSDs by running lvm/cryptsetup in the node's # mount namespace; kindest/node images don't ship those tools, so install them. for _ in 1 2 3; do if docker exec "$node" sh -c "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y lvm2 cryptsetup"; then break fi echo "WARNING: could not install lvm2/cryptsetup into kind node $node; retrying" >&2 sleep 5 done done } function validate_yaml() { cd "${REPO_DIR}/deploy/examples" kubectl create -f crds.yaml -f common.yaml -f csi-operator.yaml # create the volume replication CRDs replication_version=v0.3.0 replication_url="https://raw.githubusercontent.com/csi-addons/volume-replication-operator/${replication_version}/config/crd/bases" kubectl create -f "${replication_url}/replication.storage.openshift.io_volumereplications.yaml" kubectl create -f "${replication_url}/replication.storage.openshift.io_volumereplicationclasses.yaml" #create the KEDA CRDS keda_version=2.4.0 keda_url="https://github.com/kedacore/keda/releases/download/v${keda_version}/keda-${keda_version}.yaml" kubectl apply -f "${keda_url}" #create the COSI CRDS cosi_crd_url="github.com/kubernetes-sigs/container-object-storage-interface-api" kubectl create -k "${cosi_crd_url}" # skipping folders and some yamls that are only for openshift. manifests="$(find . -maxdepth 1 -type f -name '*.yaml' -and -not -name '*openshift*' -and -not -name 'scc*' -and -not -name 'kustomization*')" with_f_arg="$(echo "$manifests" | awk '{printf " -f %s",$1}')" # don't add newline # shellcheck disable=SC2086 # '-f manifest1.yaml -f manifest2.yaml etc.' should not be quoted kubectl create ${with_f_arg} --dry-run=client } function create_cluster_prerequisites() { # this might be called from another function that has already done a cd (cd "${REPO_DIR}/deploy/examples" && kubectl create -f crds.yaml -f common.yaml -f csi-operator.yaml) } function remove_cluster_prerequisites() { (cd "${REPO_DIR}/deploy/examples" && kubectl delete -f crds.yaml -f common.yaml) } function deploy_manifest_with_local_build() { if [[ "$1" == "deploy/examples/operator.yaml" || "$1" == "operator.yaml" ]]; then sed -i "s|deployCsiAddons: false|deployCsiAddons: true|g" $1 sed -i "s|replicas: 2|replicas: 1|g" $1 fi if [[ "$USE_LOCAL_BUILD" != "false" ]]; then sed -i "s|image: docker.io/rook/ceph:.*|image: docker.io/rook/ceph:local-build|g" $1 fi sed -i "s|ROOK_LOG_LEVEL:.*|ROOK_LOG_LEVEL: DEBUG|g" "$1" kubectl create -f $1 } # Deploy toolbox with same ceph version as the cluster-test for ci function deploy_toolbox() { cd "${REPO_DIR}/deploy/examples" sed -i 's/image: quay\.io\/ceph\/ceph:.*/image: quay.io\/ceph\/ceph:v18/' toolbox.yaml local ns ns=$(yq r toolbox.yaml metadata.namespace 2>/dev/null) timeout 300 bash -c 'until kubectl get secret rook-ceph-mon -n "$1" &>/dev/null && kubectl get cm rook-ceph-mon-endpoints -n "$1" &>/dev/null; do sleep 2; done' _ "${ns}" kubectl create -f toolbox.yaml kubectl -n "${ns}" wait --for=condition=available deployment/rook-ceph-tools --timeout=300s } function replace_ceph_image() { local file="$1" # parameter 1: the file in which to replace the ceph image local ceph_image="${2?ceph_image is required}" # parameter 2: the new ceph image to use # check for ceph_image being an empty string if [ -z "$ceph_image" ]; then echo "ceph_image may not be an empty string" exit 1 fi sed -i "s|image: .*ceph/ceph:.*|image: ${ceph_image}|g" "${file}" } # Deploy the operator, a CephCluster, and the toolbox. This is intended to be a # minimal deployment generic enough to be used by most canary tests. Each # canary test should be installing its own set of resources as a job step or # using dedicated helper functions. function deploy_cluster() { cd "${REPO_DIR}/deploy/examples" kubectl create -f networkpolicy.yaml deploy_manifest_with_local_build operator.yaml if [ $# == 0 ]; then sed -i "s|#deviceFilter:|deviceFilter: $(block_dev_basename)|g" cluster-test.yaml elif [ "$1" = "two_osds_in_device" ]; then sed -i "s|#deviceFilter:|deviceFilter: $(block_dev_basename)\n config:\n osdsPerDevice: \"2\"|g" cluster-test.yaml elif [ "$1" = "osd_with_metadata_device" ]; then sed -i "s|#deviceFilter:|deviceFilter: $(block_dev_basename)\n config:\n metadataDevice: /dev/test-rook-vg/test-rook-lv|g" cluster-test.yaml elif [ "$1" = "osd_with_metadata_partition_device" ]; then yq w -i -d0 cluster-test.yaml spec.storage.devices[0].name "$(block_dev_basename)2" yq w -i -d0 cluster-test.yaml spec.storage.devices[0].config.metadataDevice "$(block_dev_basename)1" elif [ "$1" = "encryption" ]; then sed -i "s|#deviceFilter:|deviceFilter: $(block_dev_basename)\n config:\n encryptedDevice: \"true\"|g" cluster-test.yaml elif [ "$1" = "lvm" ]; then sed -i "s|#deviceFilter:|devices:\n - name: \"/dev/test-rook-vg/test-rook-lv\"|g" cluster-test.yaml elif [ "$1" = "two_raw_disks" ]; then # use two real disks as raw OSDs; ROOK_DATA_DEV and ROOK_EXTRA_DEV are # basenames (e.g. sdb, sdc) exported by the job. An explicit devices list # plus useAllDevices=false pins the OSD count regardless of any stray disk. sed -i "s|#deviceFilter:|devices:\n - name: \"${ROOK_DATA_DEV}\"\n - name: \"${ROOK_EXTRA_DEV}\"|g" cluster-test.yaml yq w -i -d0 cluster-test.yaml spec.storage.useAllDevices false else echo "invalid argument: $*" >&2 exit 1 fi # enable monitoring yq w -i -d0 cluster-test.yaml spec.monitoring.enabled true kubectl create -f https://raw.githubusercontent.com/coreos/prometheus-operator/v0.82.0/bundle.yaml kubectl create -f monitoring/rbac.yaml kubectl create -f cluster-test.yaml deploy_toolbox } # These resources were extracted from the original deploy_cluster(), which was # deploying a smorgasbord of resources used by multiple canary tests. # # Use of this function is discouraged. Existing users should migrate away and # deploy only the necessary resources for the test scenario in their own job # steps. # # The addition of new resources this function is forbidden! function deploy_all_additional_resources_on_cluster() { cd "${REPO_DIR}/deploy/examples" kubectl create -f object-shared-pools-test.yaml kubectl create -f object-a.yaml kubectl create -f object-b.yaml kubectl create -f pool-test.yaml kubectl create -f filesystem-test.yaml sed -i "/resources:/,/ # priorityClassName:/d" rbdmirror.yaml kubectl create -f rbdmirror.yaml sed -i "/resources:/,/ # priorityClassName:/d" filesystem-mirror.yaml kubectl create -f filesystem-mirror.yaml kubectl create -f nfs-test.yaml kubectl create -f subvolumegroup.yaml } function wait_for_prepare_pod() { # wait for a mon to be created # most of this time is likely waiting for the detect version job to pull the ceph image get_pod_cmd=(kubectl --namespace rook-ceph get pod --no-headers) timeout=600 start_time="${SECONDS}" while [[ $((SECONDS - start_time)) -lt $timeout ]]; do pod="$("${get_pod_cmd[@]}" --selector=app=rook-ceph-mon --output custom-columns=NAME:.metadata.name,PHASE:status.phase)" if echo "$pod" | grep 'rook-ceph-mon-a'; then break; fi echo 'waiting for mon.a to be created' sleep 5 done # wait for at an osd prepare pod to complete OSD_COUNT=$1 get_pod_cmd=(kubectl --namespace rook-ceph get pod --no-headers) timeout=450 start_time="${SECONDS}" while [[ $((SECONDS - start_time)) -lt $timeout ]]; do pod="$("${get_pod_cmd[@]}" --selector=app=rook-ceph-osd-prepare --output custom-columns=NAME:.metadata.name,PHASE:status.phase | awk 'FNR <= 1')" if echo "$pod" | grep 'Running\|Succeeded\|Failed'; then break; fi echo 'waiting for at least one osd prepare pod to be running or finished' sleep 5 done pod="$("${get_pod_cmd[@]}" --selector app=rook-ceph-osd-prepare --output name | awk 'FNR <= 1')" kubectl --namespace rook-ceph logs --follow "$pod" # wait for an osd daemon pod to start timeout=60 start_time="${SECONDS}" while [[ $((SECONDS - start_time)) -lt $timeout ]]; do pod_count="$("${get_pod_cmd[@]}" --selector app=rook-ceph-osd --output custom-columns=NAME:.metadata.name,PHASE:status.phase | grep --count 'Running' || true)" if [ "$pod_count" -ge "$OSD_COUNT" ]; then break; fi echo 'waiting for $OSD_COUNT OSD pod(s) to be running' sleep 1 done # getting the below logs is a best-effort attempt, so use '|| true' to allow failures pod="$("${get_pod_cmd[@]}" --selector app=rook-ceph-osd,ceph_daemon_id=0 --output name)" || true kubectl --namespace rook-ceph logs "$pod" || true job="$(kubectl --namespace rook-ceph get job --selector app=rook-ceph-osd-prepare --output name | awk 'FNR <= 1')" || true kubectl -n rook-ceph describe "$job" || true kubectl -n rook-ceph describe deployment/rook-ceph-osd-0 || true } function wait_for_cleanup_pod() { # rook names the disk-cleanup job per node: cluster-cleanup-job-. Resolve the actual k8s # node name rather than assuming it equals the runner hostname ($(uname -n)): under kind the # node is "kind-control-plane", not the runner host. local node node="$(kubectl get nodes -o name | head -1 | cut -d/ -f2)" timeout 180 bash <&2 exit 1 } function verify_operator_log_message() { local message="$1" # param 1: the message to verify exists local namespace="${2:-rook-ceph}" # optional param 2: the namespace of the CephCluster (default: rook-ceph) kubectl --namespace "$namespace" logs deployment/rook-ceph-operator | grep "$message" } function wait_for_operator_log_message() { local message="$1" # param 1: the message to look for local timeout="$2" # param 2: the timeout for waiting for the message to exist local namespace="${3:-rook-ceph}" # optional param 3: the namespace of the CephCluster (default: rook-ceph) start_time="${SECONDS}" while [[ $((SECONDS - start_time)) -lt $timeout ]]; do if verify_operator_log_message "$message" "$namespace"; then return 0; fi sleep 5 done echo "timed out" >&2 && return 1 } function restart_operator() { local namespace="${1:-rook-ceph}" # optional param 1: the namespace of the CephCluster (default: rook-ceph) kubectl --namespace "$namespace" delete pod --selector app=rook-ceph-operator # wait for new pod to be running get_pod_cmd=(kubectl --namespace "$namespace" get pod --selector app=rook-ceph-operator --no-headers) timeout 20 bash -c \ "until [[ -n \"\$(${get_pod_cmd[*]} --field-selector=status.phase=Running 2>/dev/null)\" ]] ; do echo waiting && sleep 1; done" "${get_pod_cmd[@]}" } function get_clusterip() { local ns=${1?namespace is required} local cluster_name=${2?cluster name is required} kubectl -n "$ns" get svc "$cluster_name" -o jsonpath="{.spec.clusterIP}" } function get_secret_key() { local ns=${1?namespace is required} local secret_name=${2?secret name is required} local key=${3?skey is required} kubectl -n "$ns" get secrets "$secret_name" -o jsonpath="{.data.$key}" | base64 --decode } function s3cmd() { command timeout 200 s3cmd -v --config=s3cfg --access_key="${S3CMD_ACCESS_KEY}" --secret_key="${S3CMD_SECRET_KEY}" "$@" } function retry() { local retries=$1; shift local attempt for (( attempt=1; attempt<=retries; attempt++ )); do if "$@"; then return 0; fi echo "$* failed (attempt ${attempt}/${retries}), retrying in 5s..." sleep 5 done echo "$* failed after ${retries} attempts" exit 1 } function retry_for() { local timeout_seconds=${1?timeout in seconds is required} shift local deadline=$((SECONDS + timeout_seconds)) while true; do if "$@"; then return 0; fi if [[ $SECONDS -ge $deadline ]]; then echo "$* failed after retrying for ${timeout_seconds}s" return 1 fi echo "$* failed, retrying in 5s..." sleep 5 done } function wait_for_rgw_http() { local endpoint_ip=${1?rgw endpoint ip is required} local timeout_seconds=${2:-300} # Require a few consecutive successes: RGWs pause their HTTP frontends while reloading the # realm after RGW configuration period changes, so a single success can land between pauses. local want_consecutive=3 local consecutive=0 local deadline=$((SECONDS + timeout_seconds)) local code while [[ $SECONDS -lt $deadline ]]; do code=$(curl --silent --output /dev/null --max-time 5 --write-out '%{http_code}' "http://${endpoint_ip}:80/") || code="000" if [[ "$code" == "200" ]]; then consecutive=$((consecutive + 1)) if [[ $consecutive -ge $want_consecutive ]]; then echo "RGW at ${endpoint_ip} answered ${consecutive} consecutive requests" return 0 fi else echo "RGW at ${endpoint_ip} is not serving yet (HTTP code ${code})" consecutive=0 fi sleep 2 done echo "timed out after ${timeout_seconds}s waiting for RGW at ${endpoint_ip} to serve HTTP" return 1 } function wait_for_sync_status() { local ns=${1?namespace is required} local zone=${2?zone name is required} local want=${3?expected sync status line is required} local timeout_seconds=${4:-600} local deadline=$((SECONDS + timeout_seconds)) local status="" while [[ $SECONDS -lt $deadline ]]; do status=$(kubectl -n "$ns" exec deploy/rook-ceph-tools -- \ radosgw-admin sync status --rgw-realm=realm-a --rgw-zonegroup=zonegroup-a --rgw-zone="$zone") || status="" # require a non-empty status so a failed exec can't match a stale/empty buffer if [[ -n "$status" ]] && grep -q "$want" <<<"$status"; then echo "zone ${zone} reports \"${want}\"" return 0 fi echo "waiting for zone ${zone} to report \"${want}\"" sleep 10 done echo "timed out after ${timeout_seconds}s waiting for zone ${zone} to report \"${want}\"; last sync status:" echo "$status" return 1 } # nudge_multisite_secondary forces the secondary zone to converge on the latest period when it # has wedged. The master pushes each new period to the secondary's RGW, but that push is # rejected (HTTP 403) until the realm system user is resolvable locally on the secondary; when # it starts out unresolvable the master keeps re-pushing identically and the secondary never # recovers, so its metadata sync never leaves the "failed to read sync status" state. Pulling # the period directly (outbound auth, which works) and restarting the RGW so it re-runs metadata # sync init against the now-ready master breaks the wedge. This mirrors ceph's own multisite QA, # which restarts zone gateways after the period is committed. function nudge_multisite_secondary() { echo "nudging the secondary zone to converge: pulling the latest period and restarting its RGW" kubectl -n rook-ceph-secondary exec deploy/rook-ceph-tools -- \ radosgw-admin period pull --rgw-realm=realm-a || true kubectl -n rook-ceph-secondary rollout restart deploy/rook-ceph-rgw-zone-b-multisite-store-a kubectl -n rook-ceph-secondary rollout status deploy/rook-ceph-rgw-zone-b-multisite-store-a --timeout=120s } function wait_for_multisite_sync_established() { # Wait until both zones report multisite sync fully established before exercising # replication, mirroring the checkpoints ceph's own multisite QA performs before asserting # anything. This also rides out the RGW frontend pauses caused by the configuration period # changes that follow the second zone joining the zonegroup. # # The secondary's metadata sync can wedge on a fresh setup (see nudge_multisite_secondary); if # it has not initialized within a couple of minutes, nudge it to converge rather than waiting # out a doomed timeout, then retry. Healthy setups pass the first attempt in seconds, so the # nudge only ever runs when the sync is genuinely stuck. local attempt for attempt in 1 2 3; do if wait_for_sync_status rook-ceph-secondary zone-b "metadata is caught up with master" 120; then break fi if [[ $attempt -ge 3 ]]; then echo "zone-b metadata sync never established after ${attempt} attempts" return 1 fi nudge_multisite_secondary done # Data sync legitimately reports caught up at 0/0 shards before any objects are written, so the # meaningful precondition is that metadata sync (above) has actually initialized. wait_for_sync_status rook-ceph-secondary zone-b "data is caught up with source" 300 wait_for_sync_status rook-ceph zone-a "data is caught up with source" 300 } function dump_multisite_diagnostics() { set +e local ns zone for ns in rook-ceph rook-ceph-secondary; do # sync status without an explicit zone falls back to the unrelated "default" zone, so name # the realm/zonegroup/zone for each cluster. if [[ "$ns" == "rook-ceph" ]]; then zone="zone-a"; else zone="zone-b"; fi echo "===== ${ns}: pods" kubectl -n "$ns" get pods -o wide echo "===== ${ns}: rgw pod details" kubectl -n "$ns" describe pods -l app=rook-ceph-rgw echo "===== ${ns}: rgw pod logs" kubectl -n "$ns" logs -l app=rook-ceph-rgw --all-containers --tail=100 echo "===== ${ns}: sync status (${zone})" kubectl -n "$ns" exec deploy/rook-ceph-tools -- \ radosgw-admin sync status --rgw-realm=realm-a --rgw-zonegroup=zonegroup-a --rgw-zone="$zone" echo "===== ${ns}: period" kubectl -n "$ns" exec deploy/rook-ceph-tools -- radosgw-admin period get --rgw-realm=realm-a done set -e return 0 } function write_object_read_from_replica_cluster() { local write_cluster_ip=${1?ip address of cluster to write to is required} local read_cluster_ip=${2?ip address of cluster to read from is required} local test_bucket_name=${3?name of the test bucket is required} local write_zone=${4?name of the zone being written to is required} local read_zone=${5?name of the zone being read from is required} local read_cluster_ns=${6?namespace of the cluster being read from is required} local test_object_name="${test_bucket_name}-1mib-test.dat" fallocate -l 1M "$test_object_name" # ensure that test file has unique data echo "$test_object_name" >>"$test_object_name" # RGWs pause their HTTP frontends to reload the realm whenever the RGW configuration period # changes, and period changes keep happening for a while after the second zone joins the # zonegroup. While a frontend is paused, connections are refused, so give the writes a time # budget rather than a fixed number of quick retries. retry_for 120 s3cmd --host="${write_cluster_ip}" mb "s3://${test_bucket_name}" retry_for 120 s3cmd --host="${write_cluster_ip}" put "$test_object_name" "s3://${test_bucket_name}" # Wait until the reading zone's sync position for the bucket catches up with the source # zone's position, the same marker-based barrier ceph's own multisite QA uses. The bucket # metadata has to arrive on the reading zone via metadata sync before the checkpoint can # resolve the bucket at all, hence the outer retry. retry_for 120 kubectl -n "$read_cluster_ns" exec deploy/rook-ceph-tools -- \ radosgw-admin bucket sync checkpoint --rgw-realm=realm-a --rgw-zonegroup=zonegroup-a --rgw-zone="$read_zone" \ --bucket="$test_bucket_name" --source-zone="$write_zone" --retry-delay-ms=5000 --timeout-sec=300 # The checkpoint confirms the bucket's data sync markers are caught up at the RADOS level, but # the reading RGW can still briefly serve 404 for the freshly synced object until it refreshes, # so give the read a generous budget rather than asserting it is immediately available. retry_for 180 s3cmd --host="${read_cluster_ip}" get "s3://${test_bucket_name}/${test_object_name}" "${test_object_name}.get" --force diff "$test_object_name" "${test_object_name}.get" } function test_multisite_object_replication() { S3CMD_ACCESS_KEY=$(get_secret_key rook-ceph realm-a-keys access-key) readonly S3CMD_ACCESS_KEY S3CMD_SECRET_KEY=$(get_secret_key rook-ceph realm-a-keys secret-key) readonly S3CMD_SECRET_KEY local cluster_1_ip cluster_1_ip=$(get_clusterip rook-ceph rook-ceph-rgw-multisite-store) local cluster_2_ip cluster_2_ip=$(get_clusterip rook-ceph-secondary rook-ceph-rgw-zone-b-multisite-store) cd "${REPO_DIR}/deploy/examples" cat <<-EOF >s3cfg [default] host_bucket = no.way use_https = False EOF # both RGWs must be reliably serving before anything is written wait_for_rgw_http "$cluster_1_ip" wait_for_rgw_http "$cluster_2_ip" write_object_read_from_replica_cluster "$cluster_1_ip" "$cluster_2_ip" test1 zone-a zone-b rook-ceph-secondary write_object_read_from_replica_cluster "$cluster_2_ip" "$cluster_1_ip" test2 zone-b zone-a rook-ceph } function create_helm_tag() { helm_tag="$(cat _output/version)" build_image="$(docker images | awk '/build-/ {print $1}')" docker tag "${build_image}" "rook/ceph:${helm_tag}" load_image_into_cluster "rook/ceph:${helm_tag}" } function test_multus_connections() { EXEC='kubectl -n rook-ceph exec -t deploy/rook-ceph-tools -- ceph --connect-timeout 10' # daemons register their network addresses in the mon maps asynchronously # after the cluster reports ready (e.g., MDSes may not be in the fsmap yet), # so retry each check until it converges # each OSD should exist on both public and cluster network timeout 120 bash -c "until $EXEC osd dump | grep osd.0 | grep '192.168.20.' | grep '192.168.21.'; do sleep 5; done" # MDSes should exist on public network and NOT on cluster network timeout 120 bash -c "until $EXEC fs dump | grep myfs-a | grep '192.168.20.' | grep -v '192.168.21.'; do sleep 5; done" } function create_operator_toolbox() { cd "${REPO_DIR}/deploy/examples" sed -i "s|image: docker.io/rook/ceph:.*|image: docker.io/rook/ceph:local-build|g" toolbox-operator-image.yaml kubectl create -f toolbox-operator-image.yaml } function test_csi_rbd_workload { cd "${REPO_DIR}/deploy/examples/csi/rbd" sed -i 's|size: 3|size: 1|g' storageclass.yaml sed -i 's|requireSafeReplicaSize: true|requireSafeReplicaSize: false|g' storageclass.yaml kubectl create -f storageclass.yaml kubectl create -f pvc.yaml kubectl create -f pod.yaml timeout 90 sh -c 'until kubectl exec -t pod/csirbd-demo-pod -- dd if=/dev/random of=/var/lib/www/html/test bs=1M count=1; do echo "waiting for test pod to be ready" && sleep 1; done' kubectl exec -t pod/csirbd-demo-pod -- dd if=/dev/random of=/var/lib/www/html/test oflag=direct bs=1M count=1 kubectl -n rook-ceph logs ds/rook-ceph.rbd.csi.ceph.com-nodeplugin -c csi-rbdplugin kubectl -n rook-ceph delete "$(kubectl -n rook-ceph get pod --selector=app=rook-ceph.rbd.csi.ceph.com-nodeplugin --field-selector=status.phase=Running -o name)" kubectl exec -t pod/csirbd-demo-pod -- dd if=/dev/random of=/var/lib/www/html/test1 oflag=direct bs=1M count=1 kubectl exec -t pod/csirbd-demo-pod -- ls -alh /var/lib/www/html/ } function test_csi_cephfs_workload { cd "${REPO_DIR}/deploy/examples/csi/cephfs" kubectl create -f storageclass.yaml kubectl create -f pvc.yaml kubectl create -f pod.yaml timeout 90 sh -c 'until kubectl exec -t pod/csicephfs-demo-pod -- dd if=/dev/random of=/var/lib/www/html/test bs=1M count=1; do echo "waiting for test pod to be ready" && sleep 1; done' kubectl exec -t pod/csicephfs-demo-pod -- dd if=/dev/random of=/var/lib/www/html/test oflag=direct bs=1M count=1 kubectl -n rook-ceph logs ds/rook-ceph.cephfs.csi.ceph.com-nodeplugin -c csi-cephfsplugin kubectl -n rook-ceph delete "$(kubectl -n rook-ceph get pod --selector=app=rook-ceph.cephfs.csi.ceph.com-nodeplugin --field-selector=status.phase=Running -o name)" kubectl exec -t pod/csicephfs-demo-pod -- dd if=/dev/random of=/var/lib/www/html/test1 oflag=direct bs=1M count=1 kubectl exec -t pod/csicephfs-demo-pod -- ls -alh /var/lib/www/html/ } function test_csi_nfs_workload { cd "${REPO_DIR}/deploy/examples/csi/nfs" sed -i "s|#- debug|- nolock|" storageclass.yaml kubectl create -f storageclass.yaml kubectl create -f pvc.yaml kubectl create -f pod.yaml timeout 90 sh -c 'until kubectl exec -t pod/csinfs-demo-pod -- dd if=/dev/random of=/var/lib/www/html/test bs=1M count=1; do echo "waiting for test pod to be ready" && sleep 1; done' kubectl exec -t pod/csinfs-demo-pod -- dd if=/dev/random of=/var/lib/www/html/test oflag=direct bs=1M count=1 kubectl -n rook-ceph delete "$(kubectl -n rook-ceph get pod --selector=app=rook-ceph.nfs.csi.ceph.com-nodeplugin --field-selector=status.phase=Running -o name)" kubectl exec -t pod/csinfs-demo-pod -- dd if=/dev/random of=/var/lib/www/html/test1 oflag=direct bs=1M count=1 kubectl exec -t pod/csinfs-demo-pod -- ls -alh /var/lib/www/html/ } function test_csi_nvmeof_workload { cd "${REPO_DIR}/deploy/examples/csi/nvmeof" local old_gateway_pod sed -i 's/failureDomain: .*/failureDomain: osd/' nvmeof-pool.yaml sed -i 's/size: .*/size: 1/' nvmeof-pool.yaml kubectl create -f nvmeof-pool.yaml wait_for cephblockpool nvmeof rook-ceph 600 kubectl create -f "${REPO_DIR}/deploy/examples/nvmeof-test.yaml" wait_for cephblockpool builtin-nvmeof rook-ceph 600 wait_for cephnvmeofgateway nvmeof rook-ceph 600 timeout 300 bash </dev/null | awk '/rook-ceph-nvmeof-nvmeof-a-/ && \$3 == "Running" {f=1} END {exit !f}'; do echo "waiting for NVMe-oF gateway pod to be running" sleep 5 done EOF # ceph-csi-operator creates these deployments, make sure they are available timeout 300 bash </dev/null | grep -q "nvmeof"; do echo "waiting for CSI operator to create NVMe-oF controller plugin deployment" sleep 5 done EOF kubectl -n rook-ceph wait --for=condition=available \ deployment/rook-ceph.nvmeof.csi.ceph.com-ctrlplugin --timeout=300s timeout 300 bash </dev/null | grep -q "nvmeof"; do echo "waiting for CSI operator to create NVMe-oF node plugin daemonset" sleep 5 done EOF kubectl -n rook-ceph rollout status \ daemonset/rook-ceph.nvmeof.csi.ceph.com-nodeplugin --timeout=300s lsmod | grep -E 'nvme(_|-)(tcp|fabrics)' || true if [[ ! -d /sys/module/nvme_tcp ]]; then echo "nvme_tcp kernel module is required but unavailable on kernel $(uname -r)" return 1 fi kubectl create -f storageclass.yaml kubectl create -f pvc.yaml if ! kubectl wait pvc/nvmeof-external-volume --for=jsonpath='{.status.phase}'=Bound --timeout=300s; then echo "PVC nvmeof-external-volume did not reach Bound; collecting diagnostics..." kubectl describe pvc nvmeof-external-volume || true kubectl get events -n default --sort-by=.metadata.creationTimestamp | tail -n 50 || true kubectl -n rook-ceph logs -l "app.kubernetes.io/part-of=rook-ceph.nvmeof.csi.ceph.com" --tail=200 || true return 1 fi kubectl create -f pod.yaml kubectl wait pod/nvmeof-test-pod --for=condition=Ready --timeout=300s kubectl -n default exec pod/nvmeof-test-pod -- sh -c "echo 'abcd' > /mnt/nvmeof/test.txt && cat /mnt/nvmeof/test.txt" kubectl -n default exec pod/nvmeof-test-pod -- sh -c "grep -qx 'abcd' /mnt/nvmeof/test.txt" old_gateway_pod="$(kubectl -n rook-ceph get pod --no-headers | awk '/rook-ceph-nvmeof-nvmeof-a-/ {if (!f) f=$1} END {print f}')" kubectl -n rook-ceph delete pod "${old_gateway_pod}" timeout 300 bash < /mnt/nvmeof/test.txt && grep -qx 'abcd efg' /mnt/nvmeof/test.txt" kubectl delete pod nvmeof-test-pod --grace-period=0 --force kubectl wait --for=delete pod/nvmeof-test-pod --timeout=180s kubectl create -f pod.yaml kubectl wait pod/nvmeof-test-pod --for=condition=Ready --timeout=300s kubectl -n default exec pod/nvmeof-test-pod -- sh -c "grep -qx 'abcd efg' /mnt/nvmeof/test.txt" kubectl -n default exec pod/nvmeof-test-pod -- sh -c "echo 'abcd efg hij' > /mnt/nvmeof/test.txt && grep -qx 'abcd efg hij' /mnt/nvmeof/test.txt" } function toolbox() { kubectl -n rook-ceph exec -it "$(kubectl -n rook-ceph get pod -l "app=rook-ceph-tools" -o jsonpath='{.items[0].metadata.name}')" -- "$@" } function ceph() { toolbox ceph "$@" } function rbd() { toolbox rbd "$@" } function radosgw-admin() { toolbox radosgw-admin "$@" } function test_object_separate_pools() { expected_pools=( .mgr .rgw.root object-separate-pools.rgw.control object-separate-pools.rgw.meta object-separate-pools.rgw.log object-separate-pools.rgw.buckets.index object-separate-pools.rgw.buckets.non-ec object-separate-pools.rgw.otp object-separate-pools.rgw.buckets.data ) output=$(ceph osd pool ls) readarray -t live_pools < <(printf '%s' "$output") errors=0 for l in "${live_pools[@]}"; do found=false for e in "${expected_pools[@]}"; do if [[ "$l" == "$e" ]]; then found=true break fi done if [[ "$found" == false ]]; then echo "Live pool $l is not an expected pool" errors=$((errors + 1)) fi done if [[ $errors -gt 0 ]]; then echo "Found $errors errors" exit $errors fi } function delete_cluster() { kubectl --namespace rook-ceph patch cephcluster rook-ceph --type merge -p '{"spec":{"cleanupPolicy":{"confirmation":"yes-really-destroy-data"}}}' kubectl --namespace rook-ceph delete cephcluster rook-ceph kubectl --namespace rook-ceph logs deploy/rook-ceph-operator wait_for_cleanup_pod kubectl --namespace rook-ceph delete --ignore-not-found=true -f deploy/examples/operator.yaml kubectl --namespace rook-ceph delete clientprofile rook-ceph kubectl --namespace rook-ceph logs deploy/ceph-csi-controller-manager kubectl --namespace rook-ceph delete --ignore-not-found=true -f deploy/examples/csi-operator.yaml remove_cluster_prerequisites } function check_keys_exists() { toolbox=$(kubectl get pod -l app=rook-ceph-tools -n rook-ceph -o jsonpath='{.items[*].metadata.name}') for key in "${@}"; do if kubectl -n rook-ceph exec "$toolbox" -- ceph auth get "$key"; then echo "key '$key' exists" else echo "key '$key' not exists" exit 1 fi done } function check_keys_not_exists() { toolbox=$(kubectl get pod -l app=rook-ceph-tools -n rook-ceph -o jsonpath='{.items[*].metadata.name}') for key in "${@}"; do if kubectl -n rook-ceph exec "$toolbox" -- ceph auth get "$key"; then echo "key '$key' exists" exit 1 else echo "key '$key' not exists" fi done } FUNCTION="$1" shift # remove function arg now that we've recorded it # call the function with the remainder of the user-provided args # -e, -E, and -o=pipefail will ensure this script returns a failure if a part of the function fails $FUNCTION "$@"