#!/usr/bin/env bash # Copyright (C) 2013 Inktank Storage, Inc. # # This is free software; see the source for copying conditions. # There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. # # This is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as # published by the Free Software Foundation version 2. # Alex Elder # January 29, 2013 ################################################################ # The purpose of this test is to exercise paths through the rbd # code, making sure no bad pointer references or invalid reference # count operations occur in the face of concurrent activity. # # Each pass of the test creates an rbd image, maps it, and writes # some data into the image. It also reads some data from all of the # other images that exist at the time the pass executes. Finally, # the image is unmapped and removed. The image removal completes in # the background. # # An iteration of the test consists of performing some number of # passes, initating each pass as a background job, and finally # sleeping for a variable delay. The delay is initially a specified # value, but each iteration shortens that proportionally, such that # the last iteration will not delay at all. # # The result exercises concurrent creates and deletes of rbd images, # writes to new images, reads from both written and unwritten image # data (including reads concurrent with writes), and attempts to # unmap images being read. # Usage: concurrent [-i ] [-c ] [-d ] # # Exit status: # 0: success # 1: usage error # 2: other runtime error # 99: argument count error (programming error) # 100: getopt error (internal error) ################################################################ set -ex # Default flag values; RBD_CONCURRENT_ITER names are intended # to be used in yaml scripts to pass in alternate values, e.g.: # env: # RBD_CONCURRENT_ITER: 20 # RBD_CONCURRENT_COUNT: 5 # RBD_CONCURRENT_DELAY: 3 ITER_DEFAULT=${RBD_CONCURRENT_ITER:-100} COUNT_DEFAULT=${RBD_CONCURRENT_COUNT:-5} DELAY_DEFAULT=${RBD_CONCURRENT_DELAY:-5} # seconds CEPH_SECRET_FILE=${CEPH_SECRET_FILE:-} CEPH_ID=${CEPH_ID:-admin} SECRET_ARGS="" if [ "${CEPH_SECRET_FILE}" ]; then SECRET_ARGS="--secret $CEPH_SECRET_FILE" fi ################################################################ function setup() { ID_MAX_DIR=$(mktemp -d /tmp/image_max_id.XXXXX) ID_COUNT_DIR=$(mktemp -d /tmp/image_ids.XXXXXX) NAMES_DIR=$(mktemp -d /tmp/image_names.XXXXXX) SOURCE_DATA=$(mktemp /tmp/source_data.XXXXXX) # Use urandom to generate SOURCE_DATA dd if=/dev/urandom of=${SOURCE_DATA} bs=2048 count=66 \ >/dev/null 2>&1 # List of rbd id's *not* created by this script export INITIAL_RBD_IDS=$(ls /sys/bus/rbd/devices) # Set up some environment for normal teuthology test setup. # This really should not be necessary but I found it was. export CEPH_ARGS=" --name client.0" } function cleanup() { [ ! "${ID_MAX_DIR}" ] && return local id local image # Unmap mapped devices for id in $(rbd_ids); do image=$(cat "/sys/bus/rbd/devices/${id}/name") rbd_unmap_image "${id}" rbd_destroy_image "${image}" done # Get any leftover images for image in $(rbd ls 2>/dev/null); do rbd_destroy_image "${image}" done wait sync rm -f "${SOURCE_DATA}" [ -d "${NAMES_DIR}" ] && rmdir "${NAMES_DIR}" echo "Max concurrent rbd image count was $(get_max "${ID_COUNT_DIR}")" rm -rf "${ID_COUNT_DIR}" echo "Max rbd image id was $(get_max "${ID_MAX_DIR}")" rm -rf "${ID_MAX_DIR}" } function get_max() { [ $# -eq 1 ] || exit 99 local dir="$1" ls -U "${dir}" | sort -n | tail -1 } trap cleanup HUP INT QUIT # print a usage message and quit # # if a message is supplied, print that first, and then exit # with non-zero status function usage() { if [ $# -gt 0 ]; then echo "" >&2 echo "$@" >&2 fi echo "" >&2 echo "Usage: ${PROGNAME} " >&2 echo "" >&2 echo " options:" >&2 echo " -h or --help" >&2 echo " show this message" >&2 echo " -i or --iterations" >&2 echo " iteration count (1 or more)" >&2 echo " -c or --count" >&2 echo " images created per iteration (1 or more)" >&2 echo " -d or --delay" >&2 echo " maximum delay between iterations" >&2 echo "" >&2 echo " defaults:" >&2 echo " iterations: ${ITER_DEFAULT}" echo " count: ${COUNT_DEFAULT}" echo " delay: ${DELAY_DEFAULT} (seconds)" echo "" >&2 [ $# -gt 0 ] && exit 1 exit 0 # This is used for a --help } # parse command line arguments function parseargs() { ITER="${ITER_DEFAULT}" COUNT="${COUNT_DEFAULT}" DELAY="${DELAY_DEFAULT}" # Short option flags SHORT_OPTS="" SHORT_OPTS="${SHORT_OPTS},h" SHORT_OPTS="${SHORT_OPTS},i:" SHORT_OPTS="${SHORT_OPTS},c:" SHORT_OPTS="${SHORT_OPTS},d:" # Short option flags LONG_OPTS="" LONG_OPTS="${LONG_OPTS},help" LONG_OPTS="${LONG_OPTS},iterations:" LONG_OPTS="${LONG_OPTS},count:" LONG_OPTS="${LONG_OPTS},delay:" TEMP=$(getopt --name "${PROGNAME}" \ --options "${SHORT_OPTS}" \ --longoptions "${LONG_OPTS}" \ -- "$@") eval set -- "$TEMP" while [ "$1" != "--" ]; do case "$1" in -h|--help) usage ;; -i|--iterations) ITER="$2" [ "${ITER}" -lt 1 ] && usage "bad iterations value" shift ;; -c|--count) COUNT="$2" [ "${COUNT}" -lt 1 ] && usage "bad count value" shift ;; -d|--delay) DELAY="$2" shift ;; *) exit 100 # Internal error ;; esac shift done shift } function rbd_ids() { [ $# -eq 0 ] || exit 99 local ids local i [ -d /sys/bus/rbd ] || return ids=" $(echo $(ls /sys/bus/rbd/devices)) " for i in ${INITIAL_RBD_IDS}; do ids=${ids/ ${i} / } done echo ${ids} } function update_maxes() { local ids="$@" local last_id # These aren't 100% safe against concurrent updates but it # should be pretty close count=$(echo ${ids} | wc -w) touch "${ID_COUNT_DIR}/${count}" last_id=${ids% } last_id=${last_id##* } touch "${ID_MAX_DIR}/${last_id}" } function rbd_create_image() { [ $# -eq 0 ] || exit 99 local image=$(basename $(mktemp "${NAMES_DIR}/image.XXXXXX")) rbd create "${image}" --size=1024 echo "${image}" } function rbd_image_id() { [ $# -eq 1 ] || exit 99 local image="$1" grep -l "${image}" /sys/bus/rbd/devices/*/name 2>/dev/null | cut -d / -f 6 } function rbd_map_image() { [ $# -eq 1 ] || exit 99 local image="$1" local id sudo rbd map "${image}" --user "${CEPH_ID}" ${SECRET_ARGS} \ > /dev/null 2>&1 id=$(rbd_image_id "${image}") echo "${id}" } function rbd_write_image() { [ $# -eq 1 ] || exit 99 local id="$1" # Offset and size here are meant to ensure beginning and end # cross both (4K or 64K) page and (4MB) rbd object boundaries. # It assumes the SOURCE_DATA file has size 66 * 2048 bytes dd if="${SOURCE_DATA}" of="/dev/rbd${id}" bs=2048 seek=2015 \ > /dev/null 2>&1 } # All starting and ending offsets here are selected so they are not # aligned on a (4 KB or 64 KB) page boundary function rbd_read_image() { [ $# -eq 1 ] || exit 99 local id="$1" # First read starting and ending at an offset before any # written data. The osd zero-fills data read from an # existing rbd object, but before any previously-written # data. dd if="/dev/rbd${id}" of=/dev/null bs=2048 count=34 skip=3 \ > /dev/null 2>&1 # Next read starting at an offset before any written data, # but ending at an offset that includes data that's been # written. The osd zero-fills unwritten data at the # beginning of a read. dd if="/dev/rbd${id}" of=/dev/null bs=2048 count=34 skip=1983 \ > /dev/null 2>&1 # Read the data at offset 2015 * 2048 bytes (where it was # written) and make sure it matches the original data. cmp --quiet "${SOURCE_DATA}" "/dev/rbd${id}" 0 4126720 || echo "MISMATCH!!!" # Now read starting within the pre-written data, but ending # beyond it. The rbd client zero-fills the unwritten # portion at the end of a read. dd if="/dev/rbd${id}" of=/dev/null bs=2048 count=34 skip=2079 \ > /dev/null 2>&1 # Now read starting from an unwritten range within a written # rbd object. The rbd client zero-fills this. dd if="/dev/rbd${id}" of=/dev/null bs=2048 count=34 skip=2115 \ > /dev/null 2>&1 # Finally read from an unwritten region which would reside # in a different (non-existent) osd object. The osd client # zero-fills unwritten data when the target object doesn't # exist. dd if="/dev/rbd${id}" of=/dev/null bs=2048 count=34 skip=4098 \ > /dev/null 2>&1 } function rbd_unmap_image() { [ $# -eq 1 ] || exit 99 local id="$1" sudo rbd unmap "/dev/rbd${id}" } function rbd_destroy_image() { [ $# -eq 1 ] || exit 99 local image="$1" # Don't wait for it to complete, to increase concurrency rbd rm "${image}" >/dev/null 2>&1 & rm -f "${NAMES_DIR}/${image}" } function one_pass() { [ $# -eq 0 ] || exit 99 local image local id local ids local i image=$(rbd_create_image) id=$(rbd_map_image "${image}") ids=$(rbd_ids) update_maxes "${ids}" for i in ${rbd_ids}; do if [ "${i}" -eq "${id}" ]; then rbd_write_image "${i}" else rbd_read_image "${i}" fi done rbd_unmap_image "${id}" rbd_destroy_image "${image}" } ################################################################ parseargs "$@" setup for iter in $(seq 1 "${ITER}"); do for count in $(seq 1 "${COUNT}"); do one_pass & done # Sleep longer at first, overlap iterations more later. # Use awk to get sub-second granularity (see sleep(1)). sleep $(echo "${DELAY}" "${iter}" "${ITER}" | awk '{ printf("%.2f\n", $1 - $1 * $2 / $3);}') done wait cleanup exit 0