You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ni...@apache.org on 2020/05/25 08:08:16 UTC
[ignite] 01/24: ignite-ducktape: Initial commit.
This is an automated email from the ASF dual-hosted git repository.
nizhikov pushed a commit to branch ignite-ducktape
in repository https://gitbox.apache.org/repos/asf/ignite.git
commit d970146620834490f735fc4a1eee498e4b64f68d
Author: Nikolay Izhikov <ni...@apache.org>
AuthorDate: Tue Apr 7 18:01:34 2020 +0300
ignite-ducktape: Initial commit.
---
tests/MANIFEST.in | 16 +
tests/docker/Dockerfile | 63 ++++
tests/docker/build/cluster.json | 163 +++++++++
tests/docker/build/node_hosts | 14 +
tests/docker/ducker-ignite | 580 +++++++++++++++++++++++++++++++
tests/docker/run_tests.sh | 30 ++
tests/docker/ssh-config | 21 ++
tests/docker/ssh/authorized_keys | 15 +
tests/docker/ssh/config | 21 ++
tests/docker/ssh/id_rsa | 27 ++
tests/docker/ssh/id_rsa.pub | 1 +
tests/ignitetest/__init__.py | 25 ++
tests/ignitetest/utils/__init__.py | 16 +
tests/ignitetest/utils/remote_account.py | 41 +++
tests/ignitetest/utils/util.py | 138 ++++++++
tests/ignitetest/version.py | 140 ++++++++
tests/setup.cfg | 30 ++
tests/setup.py | 57 +++
18 files changed, 1398 insertions(+)
diff --git a/tests/MANIFEST.in b/tests/MANIFEST.in
new file mode 100644
index 0000000..edef4ab
--- /dev/null
+++ b/tests/MANIFEST.in
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+recursive-include ignitetest */templates/*
diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile
new file mode 100644
index 0000000..0ee09b4
--- /dev/null
+++ b/tests/docker/Dockerfile
@@ -0,0 +1,63 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+ARG jdk_version=openjdk:8
+FROM $jdk_version
+
+MAINTAINER Apache Ignite dev@ignite.apache.org
+VOLUME ["/opt/ignite-dev"]
+
+# Set the timezone.
+ENV TZ="/usr/share/zoneinfo/America/Los_Angeles"
+
+# Do not ask for confirmations when running apt-get, etc.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Set the ducker.creator label so that we know that this is a ducker image. This will make it
+# visible to 'ducker purge'. The ducker.creator label also lets us know what UNIX user built this
+# image.
+ARG ducker_creator=default
+LABEL ducker.creator=$ducker_creator
+
+# Update Linux and install necessary utilities.
+RUN apt update && apt install -y sudo netcat iptables rsync unzip wget curl jq coreutils openssh-server net-tools vim python-pip python-dev libffi-dev libssl-dev cmake pkg-config libfuse-dev iperf traceroute && apt-get -y clean
+RUN python -m pip install -U pip==9.0.3;
+RUN pip install --upgrade cffi virtualenv pyasn1 boto3 pycrypto pywinrm ipaddress enum34 && pip install --upgrade ducktape==0.7.6
+
+# Set up ssh
+COPY ./ssh-config /root/.ssh/config
+# NOTE: The paramiko library supports the PEM-format private key, but does not support the RFC4716 format.
+RUN ssh-keygen -m PEM -q -t rsa -N '' -f /root/.ssh/id_rsa && cp -f /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys
+RUN echo 'PermitUserEnvironment yes' >> /etc/ssh/sshd_config
+
+# Install binary test dependencies.
+# we use the same versions as in vagrant/base.sh
+ARG IGNITE_MIRROR="https://apache-mirror.rbc.ru/pub/apache/"
+ARG ARCHIVE_NAME="apache-ignite-2.8.0-bin.zip"
+RUN mkdir -p "/opt/ignite-2.8.0" && chmod a+rw /opt/ignite-2.8.0 && cd /opt/ignite-2.8.0 && curl -s "$IGNITE_MIRROR/ignite/2.8.0/$ARCHIVE_NAME" > /opt/ignite-2.8.0/$ARCHIVE_NAME && unzip /opt/ignite-2.8.0/$ARCHIVE_NAME -d /opt/ignite-2.8.0/
+
+# The version of Kibosh to use for testing.
+# If you update this, also update vagrant/base.sh
+ARG KIBOSH_VERSION="8841dd392e6fbf02986e2fb1f1ebf04df344b65a"
+
+# Install Kibosh
+RUN apt-get install fuse
+RUN cd /opt && git clone -q https://github.com/confluentinc/kibosh.git && cd "/opt/kibosh" && git reset --hard $KIBOSH_VERSION && mkdir "/opt/kibosh/build" && cd "/opt/kibosh/build" && ../configure && make -j 2
+
+# Set up the ducker user.
+RUN useradd -ms /bin/bash ducker && mkdir -p /home/ducker/ && rsync -aiq /root/.ssh/ /home/ducker/.ssh && chown -R ducker /home/ducker/ /mnt/ /var/log/ && echo "PATH=$(runuser -l ducker -c 'echo $PATH'):$JAVA_HOME/bin" >> /home/ducker/.ssh/environment && echo 'PATH=$PATH:'"$JAVA_HOME/bin" >> /home/ducker/.profile && echo 'ducker ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
+USER ducker
+
+CMD sudo service ssh start && tail -f /dev/null
diff --git a/tests/docker/build/cluster.json b/tests/docker/build/cluster.json
new file mode 100644
index 0000000..3597cac
--- /dev/null
+++ b/tests/docker/build/cluster.json
@@ -0,0 +1,163 @@
+{
+ "_comment": [
+ "Licensed to the Apache Software Foundation (ASF) under one or more",
+ "contributor license agreements. See the NOTICE file distributed with",
+ "this work for additional information regarding copyright ownership.",
+ "The ASF licenses this file to You 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."
+ ],
+ "nodes": [
+ {
+ "externally_routable_ip": "ducker02",
+ "ssh_config": {
+ "host": "ducker02",
+ "hostname": "ducker02",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker03",
+ "ssh_config": {
+ "host": "ducker03",
+ "hostname": "ducker03",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker04",
+ "ssh_config": {
+ "host": "ducker04",
+ "hostname": "ducker04",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker05",
+ "ssh_config": {
+ "host": "ducker05",
+ "hostname": "ducker05",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker06",
+ "ssh_config": {
+ "host": "ducker06",
+ "hostname": "ducker06",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker07",
+ "ssh_config": {
+ "host": "ducker07",
+ "hostname": "ducker07",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker08",
+ "ssh_config": {
+ "host": "ducker08",
+ "hostname": "ducker08",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker09",
+ "ssh_config": {
+ "host": "ducker09",
+ "hostname": "ducker09",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker10",
+ "ssh_config": {
+ "host": "ducker10",
+ "hostname": "ducker10",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker11",
+ "ssh_config": {
+ "host": "ducker11",
+ "hostname": "ducker11",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker12",
+ "ssh_config": {
+ "host": "ducker12",
+ "hostname": "ducker12",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker13",
+ "ssh_config": {
+ "host": "ducker13",
+ "hostname": "ducker13",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ },
+ {
+ "externally_routable_ip": "ducker14",
+ "ssh_config": {
+ "host": "ducker14",
+ "hostname": "ducker14",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ }
+ ]
+}
diff --git a/tests/docker/build/node_hosts b/tests/docker/build/node_hosts
new file mode 100644
index 0000000..e0aba24
--- /dev/null
+++ b/tests/docker/build/node_hosts
@@ -0,0 +1,14 @@
+172.19.0.2 ducker01
+172.19.0.3 ducker02
+172.19.0.4 ducker03
+172.19.0.5 ducker04
+172.19.0.6 ducker05
+172.19.0.7 ducker06
+172.19.0.8 ducker07
+172.19.0.9 ducker08
+172.19.0.10 ducker09
+172.19.0.11 ducker10
+172.19.0.12 ducker11
+172.19.0.13 ducker12
+172.19.0.14 ducker13
+172.19.0.15 ducker14
diff --git a/tests/docker/ducker-ignite b/tests/docker/ducker-ignite
new file mode 100755
index 0000000..6ea13a0
--- /dev/null
+++ b/tests/docker/ducker-ignite
@@ -0,0 +1,580 @@
+#!/usr/bin/env bash
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+#
+# Ducker-Ignite: a tool for running Apache Ignite system tests inside Docker images.
+#
+# Note: this should be compatible with the version of bash that ships on most
+# Macs, bash 3.2.57.
+#
+
+script_path="${0}"
+
+# The absolute path to the directory which this script is in. This will also be the directory
+# which we run docker build from.
+ducker_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# The absolute path to the root Ignite directory
+ignite_dir="$( cd "${ducker_dir}/../.." && pwd )"
+
+# The memory consumption to allow during the docker build.
+# This does not include swap.
+docker_build_memory_limit="3200m"
+
+# The maximum mmemory consumption to allow in containers.
+docker_run_memory_limit="2000m"
+
+# The default number of cluster nodes to bring up if a number is not specified.
+default_num_nodes=14
+
+# The default OpenJDK base image.
+default_jdk="openjdk:8"
+
+# The default ducker-ignite image name.
+default_image_name="ducker-ak"
+
+# Display a usage message on the terminal and exit.
+#
+# $1: The exit status to use
+usage() {
+ local exit_status="${1}"
+ cat <<EOF
+ducker-ak: a tool for running Apache Ignite tests inside Docker images.
+
+Usage: ${script_path} [command] [options]
+
+help|-h|--help
+ Display this help message
+
+up [-n|--num-nodes NUM_NODES] [-f|--force] [docker-image]
+ [-C|--custom-ducktape DIR] [-e|--expose-ports ports]
+ Bring up a cluster with the specified amount of nodes (defaults to ${default_num_nodes}).
+ The docker image name defaults to ${default_image_name}. If --force is specified, we will
+ attempt to bring up an image even some parameters are not valid.
+
+ If --custom-ducktape is specified, we will install the provided custom
+ ducktape source code directory before bringing up the nodes. The provided
+ directory should be the ducktape git repo, not the ducktape installed module directory.
+
+ if --expose-ports is specified then we will expose those ports to random ephemeral ports
+ on the host. The argument can be a single port (like 5005), a port range like (5005-5009)
+ or a combination of port/port-range separated by comma (like 2181,9092 or 2181,5005-5008).
+ By default no port is exposed. See README.md for more detail on this option.
+
+test [test-name(s)]
+ Run a test or set of tests inside the currently active Ducker nodes.
+ For example, to run the system test produce_bench_test, you would run:
+ ./tests/docker/ducker-ak test ./tests/ignitetest/test/core/rebalance_test.py
+
+ssh [node-name|user-name@node-name] [command]
+ Log in to a running ducker container. If node-name is not given, it prints
+ the names of all running nodes. If node-name is 'all', we will run the
+ command on every node. If user-name is given, we will try to log in as
+ that user. Otherwise, we will log in as the 'ducker' user. If a command
+ is specified, we will run that command. Otherwise, we will provide a login
+ shell.
+
+down [-q|--quiet] [-f|--force]
+ Tear down all the currently active ducker-ak nodes. If --quiet is specified,
+ only error messages are printed. If --force or -f is specified, "docker rm -f"
+ will be used to remove the nodes, which kills currently running ducker-ak test.
+
+purge [--f|--force]
+ Purge Docker images created by ducker-ak. This will free disk space.
+ If --force is set, we run 'docker rmi -f'.
+EOF
+ exit "${exit_status}"
+}
+
+# Exit with an error message.
+die() {
+ echo $@
+ exit 1
+}
+
+# Check for the presence of certain commands.
+#
+# $@: The commands to check for. This function will die if any of these commands are not found by
+# the 'which' command.
+require_commands() {
+ local cmds="${@}"
+ for cmd in ${cmds}; do
+ which -- "${cmd}" &> /dev/null || die "You must install ${cmd} to run this script."
+ done
+}
+
+# Set a global variable to a value.
+#
+# $1: The variable name to set. This function will die if the variable already has a value. The
+# variable will be made readonly to prevent any future modifications.
+# $2: The value to set the variable to. This function will die if the value is empty or starts
+# with a dash.
+# $3: A human-readable description of the variable.
+set_once() {
+ local key="${1}"
+ local value="${2}"
+ local what="${3}"
+ [[ -n "${!key}" ]] && die "Error: more than one value specified for ${what}."
+ verify_command_line_argument "${value}" "${what}"
+ # It would be better to use declare -g, but older bash versions don't support it.
+ export ${key}="${value}"
+}
+
+# Verify that a command-line argument is present and does not start with a slash.
+#
+# $1: The command-line argument to verify.
+# $2: A human-readable description of the variable.
+verify_command_line_argument() {
+ local value="${1}"
+ local what="${2}"
+ [[ -n "${value}" ]] || die "Error: no value specified for ${what}"
+ [[ ${value} == -* ]] && die "Error: invalid value ${value} specified for ${what}"
+}
+
+# Echo a message if a flag is set.
+#
+# $1: If this is 1, the message will be echoed.
+# $@: The message
+maybe_echo() {
+ local verbose="${1}"
+ shift
+ [[ "${verbose}" -eq 1 ]] && echo "${@}"
+}
+
+# Counts the number of elements passed to this subroutine.
+count() {
+ echo $#
+}
+
+# Push a new directory on to the bash directory stack, or exit with a failure message.
+#
+# $1: The directory push on to the directory stack.
+must_pushd() {
+ local target_dir="${1}"
+ pushd -- "${target_dir}" &> /dev/null || die "failed to change directory to ${target_dir}"
+}
+
+# Pop a directory from the bash directory stack, or exit with a failure message.
+must_popd() {
+ popd &> /dev/null || die "failed to popd"
+}
+
+# Run a command and die if it fails.
+#
+# Optional flags:
+# -v: print the command before running it.
+# -o: display the command output.
+# $@: The command to run.
+must_do() {
+ local verbose=0
+ local output="/dev/null"
+ while true; do
+ case ${1} in
+ -v) verbose=1; shift;;
+ -o) output="/dev/stdout"; shift;;
+ *) break;;
+ esac
+ done
+ local cmd="${@}"
+ [[ "${verbose}" -eq 1 ]] && echo "${cmd}"
+ ${cmd} >${output} || die "${1} failed"
+}
+
+# Ask the user a yes/no question.
+#
+# $1: The prompt to use
+# $_return: 0 if the user answered no; 1 if the user anМинус - создаст дополнительную нагрузку на core team.swered yes.
+ask_yes_no() {
+ local prompt="${1}"
+ while true; do
+ read -r -p "${prompt} " response
+ case "${response}" in
+ [yY]|[yY][eE][sS]) _return=1; return;;
+ [nN]|[nN][oO]) _return=0; return;;
+ *);;
+ esac
+ echo "Please respond 'yes' or 'no'."
+ echo
+ done
+}
+
+# Build a docker image.
+#
+# $1: The name of the image to build.
+ducker_build() {
+ local image_name="${1}"
+
+ # Use SECONDS, a builtin bash variable that gets incremented each second, to measure the docker
+ # build duration.
+ SECONDS=0
+
+ must_pushd "${ducker_dir}"
+ # Tip: if you are scratching your head for some dependency problems that are referring to an old code version
+ # (for example java.lang.NoClassDefFoundError), add --no-cache flag to the build shall give you a clean start.
+ must_do -v -o docker build --memory="${docker_build_memory_limit}" \
+ --build-arg "ducker_creator=${user_name}" --build-arg "jdk_version=${jdk_version}" -t "${image_name}" \
+ -f "${ducker_dir}/Dockerfile" ${docker_args} -- .
+ docker_status=$?
+ must_popd
+ duration="${SECONDS}"
+ if [[ ${docker_status} -ne 0 ]]; then
+ die "** ERROR: Failed to build ${what} image after $((${duration} / 60))m \
+$((${duration} % 60))s. See ${build_log} for details."
+ fi
+ echo "** Successfully built ${what} image in $((${duration} / 60))m \
+$((${duration} % 60))s. See ${build_log} for details."
+}
+
+docker_run() {
+ local node=${1}
+ local image_name=${2}
+ local ports_option=${3}
+
+ local expose_ports=""
+ if [[ -n ${ports_option} ]]; then
+ expose_ports="-P"
+ for expose_port in ${ports_option//,/ }; do
+ expose_ports="${expose_ports} --expose ${expose_port}"
+ done
+ fi
+
+ # Invoke docker-run. We need privileged mode to be able to run iptables
+ # and mount FUSE filesystems inside the container. We also need it to
+ # run iptables inside the container.
+ must_do -v docker run --privileged \
+ -d -t -h "${node}" --network ducknet "${expose_ports}" \
+ --memory=${docker_run_memory_limit} --memory-swappiness=1 \
+ -v "${ignite_dir}:/opt/ignite-dev" --name "${node}" -- "${image_name}"
+}
+
+setup_custom_ducktape() {
+ local custom_ducktape="${1}"
+ local image_name="${2}"
+
+ [[ -f "${custom_ducktape}/ducktape/__init__.py" ]] || \
+ die "You must supply a valid ducktape directory to --custom-ducktape"
+ docker_run ducker01 "${image_name}"
+ local running_container="$(docker ps -f=network=ducknet -q)"
+ must_do -v -o docker cp "${custom_ducktape}" "${running_container}:/opt/ducktape"
+ docker exec --user=root ducker01 bash -c 'set -x && cd /opt/ignite-dev/tests && sudo python ./setup.py develop install && cd /opt/ducktape && sudo python ./setup.py develop install'
+ [[ $? -ne 0 ]] && die "failed to install the new ducktape."
+ must_do -v -o docker commit ducker01 "${image_name}"
+ must_do -v docker kill "${running_container}"
+ must_do -v docker rm ducker01
+}
+
+ducker_up() {
+ require_commands docker
+ while [[ $# -ge 1 ]]; do
+ case "${1}" in
+ -C|--custom-ducktape) set_once custom_ducktape "${2}" "the custom ducktape directory"; shift 2;;
+ -f|--force) force=1; shift;;
+ -n|--num-nodes) set_once num_nodes "${2}" "number of nodes"; shift 2;;
+ -j|--jdk) set_once jdk_version "${2}" "the OpenJDK base image"; shift 2;;
+ -e|--expose-ports) set_once expose_ports "${2}" "the ports to expose"; shift 2;;
+ *) set_once image_name "${1}" "docker image name"; shift;;
+ esac
+ done
+ [[ -n "${num_nodes}" ]] || num_nodes="${default_num_nodes}"
+ [[ -n "${jdk_version}" ]] || jdk_version="${default_jdk}"
+ [[ -n "${image_name}" ]] || image_name="${default_image_name}-${jdk_version/:/-}"
+ [[ "${num_nodes}" =~ ^-?[0-9]+$ ]] || \
+ die "ducker_up: the number of nodes must be an integer."
+ [[ "${num_nodes}" -gt 0 ]] || die "ducker_up: the number of nodes must be greater than 0."
+ if [[ "${num_nodes}" -lt 2 ]]; then
+ if [[ "${force}" -ne 1 ]]; then
+ echo "ducker_up: It is recommended to run at least 2 nodes, since ducker01 is only \
+used to run ducktape itself. If you want to do it anyway, you can use --force to attempt to \
+use only ${num_nodes}."
+ exit 1
+ fi
+ fi
+
+ docker ps >/dev/null || die "ducker_up: failed to run docker. Please check that the daemon is started."
+
+ ducker_build "${image_name}"
+
+ docker inspect --format='{{.Config.Labels}}' --type=image "${image_name}" | grep -q 'ducker.type'
+ local docker_status=${PIPESTATUS[0]}
+ local grep_status=${PIPESTATUS[1]}
+ [[ "${docker_status}" -eq 0 ]] || die "ducker_up: failed to inspect image ${image_name}. \
+Please check that it exists."
+ if [[ "${grep_status}" -ne 0 ]]; then
+ if [[ "${force}" -ne 1 ]]; then
+ echo "ducker_up: ${image_name} does not appear to be a ducker image. It lacks the \
+ducker.type label. If you think this is a mistake, you can use --force to attempt to bring \
+it up anyway."
+ exit 1
+ fi
+ fi
+ local running_containers="$(docker ps -f=network=ducknet -q)"
+ local num_running_containers=$(count ${running_containers})
+ if [[ ${num_running_containers} -gt 0 ]]; then
+ die "ducker_up: there are ${num_running_containers} ducker containers \
+running already. Use ducker down to bring down these containers before \
+attempting to start new ones."
+ fi
+
+ echo "ducker_up: Bringing up ${image_name} with ${num_nodes} nodes..."
+ if docker network inspect ducknet &>/dev/null; then
+ must_do -v docker network rm ducknet
+ fi
+ must_do -v docker network create ducknet
+ if [[ -n "${custom_ducktape}" ]]; then
+ setup_custom_ducktape "${custom_ducktape}" "${image_name}"
+ fi
+ for n in $(seq -f %02g 1 ${num_nodes}); do
+ local node="ducker${n}"
+ docker_run "${node}" "${image_name}" "${expose_ports}"
+ done
+ mkdir -p "${ducker_dir}/build"
+ exec 3<> "${ducker_dir}/build/node_hosts"
+ for n in $(seq -f %02g 1 ${num_nodes}); do
+ local node="ducker${n}"
+ docker exec --user=root "${node}" grep "${node}" /etc/hosts >&3
+ [[ $? -ne 0 ]] && die "failed to find the /etc/hosts entry for ${node}"
+ done
+ exec 3>&-
+ for n in $(seq -f %02g 1 ${num_nodes}); do
+ local node="ducker${n}"
+ docker exec --user=root "${node}" \
+ bash -c "grep -v ${node} /opt/ignite-dev/tests/docker/build/node_hosts >> /etc/hosts"
+ [[ $? -ne 0 ]] && die "failed to append to the /etc/hosts file on ${node}"
+ done
+
+ echo "ducker_up: added the latest entries to /etc/hosts on each node."
+ generate_cluster_json_file "${num_nodes}" "${ducker_dir}/build/cluster.json"
+ echo "ducker_up: successfully wrote ${ducker_dir}/build/cluster.json"
+ echo "** ducker_up: successfully brought up ${num_nodes} nodes."
+}
+
+# Generate the cluster.json file used by ducktape to identify cluster nodes.
+#
+# $1: The number of cluster nodes.
+# $2: The path to write the cluster.json file to.
+generate_cluster_json_file() {
+ local num_nodes="${1}"
+ local path="${2}"
+ exec 3<> "${path}"
+cat<<EOF >&3
+{
+ "_comment": [
+ "Licensed to the Apache Software Foundation (ASF) under one or more",
+ "contributor license agreements. See the NOTICE file distributed with",
+ "this work for additional information regarding copyright ownership.",
+ "The ASF licenses this file to You 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."
+ ],
+ "nodes": [
+EOF
+ for n in $(seq 2 ${num_nodes}); do
+ if [[ ${n} -eq ${num_nodes} ]]; then
+ suffix=""
+ else
+ suffix=","
+ fi
+ local node=$(printf ducker%02d ${n})
+cat<<EOF >&3
+ {
+ "externally_routable_ip": "${node}",
+ "ssh_config": {
+ "host": "${node}",
+ "hostname": "${node}",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ }${suffix}
+EOF
+ done
+cat<<EOF >&3
+ ]
+}
+EOF
+ exec 3>&-
+}
+
+ducker_test() {
+ require_commands docker
+ docker inspect ducker01 &>/dev/null || \
+ die "ducker_test: the ducker01 instance appears to be down. Did you run 'ducker up'?"
+ [[ $# -lt 1 ]] && \
+ die "ducker_test: you must supply at least one system test to run. Type --help for help."
+ local args=""
+ local ignite_test=0
+ for arg in "${@}"; do
+ local regex=".*\/ignitetest\/(.*)"
+ if [[ $arg =~ $regex ]]; then
+ local kpath=${BASH_REMATCH[1]}
+ args="${args} ./tests/ignitetest/${kpath}"
+ else
+ args="${args} ${arg}"
+ fi
+ done
+ must_pushd "${ignite_dir}"
+ (test -f ./gradlew || gradle) && ./gradlew systemTestLibs
+ must_popd
+ cmd="cd /opt/ignite-dev && ducktape --cluster-file /opt/ignite-dev/tests/docker/build/cluster.json $args"
+ echo "docker exec ducker01 bash -c \"${cmd}\""
+ exec docker exec --user=ducker ducker01 bash -c "${cmd}"
+}
+
+ducker_ssh() {
+ require_commands docker
+ [[ $# -eq 0 ]] && die "ducker_ssh: Please specify a container name to log into. \
+Currently active containers: $(echo_running_container_names)"
+ local node_info="${1}"
+ shift
+ local guest_command="$*"
+ local user_name="ducker"
+ if [[ "${node_info}" =~ @ ]]; then
+ user_name="${node_info%%@*}"
+ local node_name="${node_info##*@}"
+ else
+ local node_name="${node_info}"
+ fi
+ local docker_flags=""
+ if [[ -z "${guest_command}" ]]; then
+ local docker_flags="${docker_flags} -t"
+ local guest_command_prefix=""
+ guest_command=bash
+ else
+ local guest_command_prefix="bash -c"
+ fi
+ if [[ "${node_name}" == "all" ]]; then
+ local nodes=$(echo_running_container_names)
+ [[ "${nodes}" == "(none)" ]] && die "ducker_ssh: can't locate any running ducker nodes."
+ for node in ${nodes}; do
+ docker exec --user=${user_name} -i ${docker_flags} "${node}" \
+ ${guest_command_prefix} "${guest_command}" || die "docker exec ${node} failed"
+ done
+ else
+ docker inspect --type=container -- "${node_name}" &>/dev/null || \
+ die "ducker_ssh: can't locate node ${node_name}. Currently running nodes: \
+$(echo_running_container_names)"
+ exec docker exec --user=${user_name} -i ${docker_flags} "${node_name}" \
+ ${guest_command_prefix} "${guest_command}"
+ fi
+}
+
+# Echo all the running Ducker container names, or (none) if there are no running Ducker containers.
+echo_running_container_names() {
+ node_names="$(docker ps -f=network=ducknet -q --format '{{.Names}}' | sort)"
+ if [[ -z "${node_names}" ]]; then
+ echo "(none)"
+ else
+ echo ${node_names//$'\n'/ }
+ fi
+}
+
+ducker_down() {
+ require_commands docker
+ local verbose=1
+ local force_str=""
+ while [[ $# -ge 1 ]]; do
+ case "${1}" in
+ -q|--quiet) verbose=0; shift;;
+ -f|--force) force_str="-f"; shift;;
+ *) die "ducker_down: unexpected command-line argument ${1}";;
+ esac
+ done
+ local running_containers
+ running_containers="$(docker ps -f=network=ducknet -q)"
+ [[ $? -eq 0 ]] || die "ducker_down: docker command failed. Is the docker daemon running?"
+ running_containers=${running_containers//$'\n'/ }
+ local all_containers="$(docker ps -a -f=network=ducknet -q)"
+ all_containers=${all_containers//$'\n'/ }
+ if [[ -z "${all_containers}" ]]; then
+ maybe_echo "${verbose}" "No ducker containers found."
+ return
+ fi
+ verbose_flag=""
+ if [[ ${verbose} == 1 ]]; then
+ verbose_flag="-v"
+ fi
+ if [[ -n "${running_containers}" ]]; then
+ must_do ${verbose_flag} docker kill "${running_containers}"
+ fi
+ must_do ${verbose_flag} docker rm ${force_str} "${all_containers}"
+ must_do ${verbose_flag} -o rm -f -- "${ducker_dir}/build/node_hosts" "${ducker_dir}/build/cluster.json"
+ if docker network inspect ducknet &>/dev/null; then
+ must_do -v docker network rm ducknet
+ fi
+ maybe_echo "${verbose}" "ducker_down: removed $(count ${all_containers}) containers."
+}
+
+ducker_purge() {
+ require_commands docker
+ local force_str=""
+ while [[ $# -ge 1 ]]; do
+ case "${1}" in
+ -f|--force) force_str="-f"; shift;;
+ *) die "ducker_purge: unknown argument ${1}";;
+ esac
+ done
+ echo "** ducker_purge: attempting to locate ducker images to purge"
+ local images
+ images=$(docker images -q -a -f label=ducker.creator)
+ [[ $? -ne 0 ]] && die "docker images command failed"
+ images=${images//$'\n'/ }
+ declare -a purge_images=()
+ if [[ -z "${images}" ]]; then
+ echo "** ducker_purge: no images found to purge."
+ exit 0
+ fi
+ echo "** ducker_purge: images to delete:"
+ for image in ${images}; do
+ echo -n "${image} "
+ docker inspect --format='{{.Config.Labels}} {{.Created}}' --type=image "${image}"
+ [[ $? -ne 0 ]] && die "docker inspect ${image} failed"
+ done
+ ask_yes_no "Delete these docker images? [y/n]"
+ [[ "${_return}" -eq 0 ]] && exit 0
+ must_do -v -o docker rmi ${force_str} ${images}
+}
+
+# Parse command-line arguments
+[[ $# -lt 1 ]] && usage 0
+# Display the help text if -h or --help appears in the command line
+for arg in ${@}; do
+ case "${arg}" in
+ -h|--help) usage 0;;
+ --) break;;
+ *);;
+ esac
+done
+action="${1}"
+shift
+case "${action}" in
+ help) usage 0;;
+
+ up|test|ssh|down|purge)
+ ducker_${action} "${@}"; exit 0;;
+
+ *) echo "Unknown command '${action}'. Type '${script_path} --help' for usage information."
+ exit 1;;
+esac
diff --git a/tests/docker/run_tests.sh b/tests/docker/run_tests.sh
new file mode 100755
index 0000000..09c6797
--- /dev/null
+++ b/tests/docker/run_tests.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+IGNITE_NUM_CONTAINERS=${IGNITE_NUM_CONTAINERS:-14}
+TC_PATHS=${TC_PATHS:-./ignitetest/}
+
+die() {
+ echo $@
+ exit 1
+}
+
+if ${SCRIPT_DIR}/ducker-ignite ssh | grep -q '(none)'; then
+ ${SCRIPT_DIR}/ducker-ignite up -n "${IGNITE_NUM_CONTAINERS}" || die "ducker-ak up failed"
+fi
+${SCRIPT_DIR}/ducker-ignite test ${TC_PATHS} ${_DUCKTAPE_OPTIONS} || die "ducker-ak test failed"
diff --git a/tests/docker/ssh-config b/tests/docker/ssh-config
new file mode 100644
index 0000000..1f87417
--- /dev/null
+++ b/tests/docker/ssh-config
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+Host *
+ ControlMaster auto
+ ControlPath ~/.ssh/master-%r@%h:%p
+ StrictHostKeyChecking no
+ ConnectTimeout=10
+ IdentityFile ~/.ssh/id_rsa
diff --git a/tests/docker/ssh/authorized_keys b/tests/docker/ssh/authorized_keys
new file mode 100644
index 0000000..9f9da1f
--- /dev/null
+++ b/tests/docker/ssh/authorized_keys
@@ -0,0 +1,15 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0qDT9kEPWc8JQ53b4KnT/ZJOLwb+3c//jpLW/2ofjDyIsPW4FohLpicfouch/zsRpN4G38lua+2BsGls9sMIZc6PXY2L+NIGCkqEMdCoU1Ym8SMtyJklfzp3m/0PeK9s2dLlR3PFRYvyFA4btQK5hkbYDNZPzf4airvzdRzLkrFf81+RemaMI2EtONwJRcbLViPaTXVKJdbFwJTJ1u7yu9wDYWHKBMA92mHTQeP6bhVYCqxJn3to/RfZYd+sHw6mfxVg5OrAlUOYpSV4pDNCAsIHdtZ56V8NQlJL6NJ2vzzSSYUwLMqe88fhrC8yYHoxC07QPy1EdkSTHdohAicyT root@knode01.knw
diff --git a/tests/docker/ssh/config b/tests/docker/ssh/config
new file mode 100644
index 0000000..1f87417
--- /dev/null
+++ b/tests/docker/ssh/config
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+Host *
+ ControlMaster auto
+ ControlPath ~/.ssh/master-%r@%h:%p
+ StrictHostKeyChecking no
+ ConnectTimeout=10
+ IdentityFile ~/.ssh/id_rsa
diff --git a/tests/docker/ssh/id_rsa b/tests/docker/ssh/id_rsa
new file mode 100644
index 0000000..276e07b
--- /dev/null
+++ b/tests/docker/ssh/id_rsa
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAtKg0/ZBD1nPCUOd2+Cp0/2STi8G/t3P/46S1v9qH4w8iLD1u
+BaIS6YnH6LnIf87EaTeBt/JbmvtgbBpbPbDCGXOj12Ni/jSBgpKhDHQqFNWJvEjL
+ciZJX86d5v9D3ivbNnS5UdzxUWL8hQOG7UCuYZG2AzWT83+Goq783Ucy5KxX/Nfk
+XpmjCNhLTjcCUXGy1Yj2k11SiXWxcCUydbu8rvcA2FhygTAPdph00Hj+m4VWAqsS
+Z97aP0X2WHfrB8Opn8VYOTqwJVDmKUleKQzQgLCB3bWeelfDUJSS+jSdr880kmFM
+CzKnvPH4awvMmB6MQtO0D8tRHZEkx3aIQInMkwIDAQABAoIBAQCz6EMFNNLp0NP1
+X9yRXS6wW4e4CRWUazesiw3YZpcmnp6IchCMGZA99FEZyVILPW1J3tYWyotBdw7Z
++RFeCRXy5L+IMtiVkNJcpwss7M4ve0w0LkY0gj5V49xJ+3Gp4gDnZSxcguvrAem5
+yP5obR572fDpl0SknB4HCr6U2l+rauzrLyevy5eeDT/vmXbuM1cdHpNIXmmElz4L
+t31n+exQRn6tP1h516iXbcYbopxDgdv2qKGAqzWKE6TyWpzF5x7kjOEYt0bZ5QO3
+Lwh7AAqE/3mwxlYwng1L4WAT7RtcP19W+9JDIc7ENInMGxq6q46p1S3IPZsf1cj/
+aAJ9q3LBAoGBAOVJr0+WkR786n3BuswpGQWBgVxfai4y9Lf90vuGKawdQUzXv0/c
+EB/CFqP/dIsquukA8PfzjNMyTNmEHXi4Sf16H8Rg4EGhIYMEqIQojx1t/yLLm0aU
+YPEvW/02Umtlg3pJw9fQAAzFVqCasw2E2lUdAUkydGRwDUJZmv2/b3NzAoGBAMm0
+Jo7Et7ochH8Vku6uA+hG+RdwlKFm5JA7/Ci3DOdQ1zmJNrvBBFQLo7AjA4iSCoBd
+s9+y0nrSPcF4pM3l6ghLheaqbnIi2HqIMH9mjDbrOZiWvbnjvjpOketgNX8vV3Ye
+GUkSjoNcmvRmdsICmUjeML8bGOmq4zF9W/GIfTphAoGBAKGRo8R8f/SLGh3VtvCI
+gUY89NAHuEWnyIQii1qMNq8+yjYAzaHTm1UVqmiT6SbrzFvGOwcuCu0Dw91+2Fmp
+2xGPzfTOoxf8GCY/0ROXlQmS6jc1rEw24Hzz92ldrwRYuyYf9q4Ltw1IvXtcp5F+
+LW/OiYpv0E66Gs3HYI0wKbP7AoGBAJMZWeFW37LQJ2TTJAQDToAwemq4xPxsoJX7
+2SsMTFHKKBwi0JLe8jwk/OxwrJwF/bieHZcvv8ao2zbkuDQcz6/a/D074C5G8V9z
+QQM4k1td8vQwQw91Yv782/gvgvRNX1iaHNCowtxURgGlVEirQoTc3eoRZfrLkMM/
+7DTa2JEhAoGACEu3zHJ1sgyeOEgLArUJXlQM30A/ulMrnCd4MEyIE+ReyWAUevUQ
+0lYdVNva0/W4C5e2lUOJL41jjIPLqI7tcFR2PZE6n0xTTkxNH5W2u1WpFeKjx+O3
+czv7Bt6wYyLHIMy1JEqAQ7pw1mtJ5s76UDvXUhciF+DU2pWYc6APKR0=
+-----END RSA PRIVATE KEY-----
diff --git a/tests/docker/ssh/id_rsa.pub b/tests/docker/ssh/id_rsa.pub
new file mode 100644
index 0000000..76e8f5f
--- /dev/null
+++ b/tests/docker/ssh/id_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0qDT9kEPWc8JQ53b4KnT/ZJOLwb+3c//jpLW/2ofjDyIsPW4FohLpicfouch/zsRpN4G38lua+2BsGls9sMIZc6PXY2L+NIGCkqEMdCoU1Ym8SMtyJklfzp3m/0PeK9s2dLlR3PFRYvyFA4btQK5hkbYDNZPzf4airvzdRzLkrFf81+RemaMI2EtONwJRcbLViPaTXVKJdbFwJTJ1u7yu9wDYWHKBMA92mHTQeP6bhVYCqxJn3to/RfZYd+sHw6mfxVg5OrAlUOYpSV4pDNCAsIHdtZ56V8NQlJL6NJ2vzzSSYUwLMqe88fhrC8yYHoxC07QPy1EdkSTHdohAicyT root@knode01.knw
diff --git a/tests/ignitetest/__init__.py b/tests/ignitetest/__init__.py
new file mode 100644
index 0000000..c5862fe
--- /dev/null
+++ b/tests/ignitetest/__init__.py
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# This determines the version of kafkatest that can be published to PyPi and installed with pip
+#
+# Note that in development, this version name can't follow Kafka's convention of having a trailing "-SNAPSHOT"
+# due to python version naming restrictions, which are enforced by python packaging tools
+# (see https://www.python.org/dev/peps/pep-0440/)
+#
+# Instead, in development branches, the version should have a suffix of the form ".devN"
+#
+# For example, when Ignite is at version 2.9.0-SNAPSHOT, this should be something like "2.9.0.dev0"
+__version__ = '2.9.0.dev0'
diff --git a/tests/ignitetest/utils/__init__.py b/tests/ignitetest/utils/__init__.py
new file mode 100644
index 0000000..d753d8e
--- /dev/null
+++ b/tests/ignitetest/utils/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+from util import ignitetest_version, is_int, is_int_with_prefix, node_is_reachable, validate_delivery
diff --git a/tests/ignitetest/utils/remote_account.py b/tests/ignitetest/utils/remote_account.py
new file mode 100644
index 0000000..e838a96
--- /dev/null
+++ b/tests/ignitetest/utils/remote_account.py
@@ -0,0 +1,41 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+
+def file_exists(node, file):
+ """Quick and dirty check for existence of remote file."""
+ try:
+ node.account.ssh("cat " + file, allow_fail=False)
+ return True
+ except:
+ return False
+
+
+def path_exists(node, path):
+ """Quick and dirty check for existence of remote path."""
+ try:
+ node.account.ssh("ls " + path, allow_fail=False)
+ return True
+ except:
+ return False
+
+
+def line_count(node, file):
+ """Return the line count of file on node"""
+ out = [line for line in node.account.ssh_capture("wc -l %s" % file)]
+ if len(out) != 1:
+ raise Exception("Expected single line of output from wc -l")
+
+ return int(out[0].strip().split(" ")[0])
diff --git a/tests/ignitetest/utils/util.py b/tests/ignitetest/utils/util.py
new file mode 100644
index 0000000..0bb22eb
--- /dev/null
+++ b/tests/ignitetest/utils/util.py
@@ -0,0 +1,138 @@
+# Copyright 2015 Confluent Inc.
+#
+# 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.
+
+from ignitetest import __version__ as __ignitetest_version__
+
+import math
+import re
+import time
+
+
+def ignitetest_version():
+ """Return string representation of current ducktape version."""
+ return __ignitetest_version__
+
+
+def is_int(msg):
+ """Method used to check whether the given message is an integer
+
+ return int or raises an exception if message is not an integer
+ """
+ try:
+ return int(msg)
+ except ValueError:
+ raise Exception("Unexpected message format (expected an integer). Message: %s" % (msg))
+
+
+def is_int_with_prefix(msg):
+ """
+ Method used check whether the given message is of format 'integer_prefix'.'integer_value'
+
+ :param msg: message to validate
+ :return: msg or raises an exception is a message is of wrong format
+ """
+ try:
+ parts = msg.split(".")
+ if len(parts) != 2:
+ raise Exception("Unexpected message format. Message should be of format: integer "
+ "prefix dot integer value. Message: %s" % (msg))
+ int(parts[0])
+ int(parts[1])
+ return msg
+ except ValueError:
+ raise Exception("Unexpected message format. Message should be of format: integer "
+ "prefix dot integer value, but one of the two parts (before or after dot) "
+ "are not integers. Message: %s" % (msg))
+
+
+def node_is_reachable(src_node, dst_node):
+ """
+ Returns true if a node is unreachable from another node.
+
+ :param src_node: The source node to check from reachability from.
+ :param dst_node: The destination node to check for reachability to.
+ :return: True only if dst is reachable from src.
+ """
+ return 0 == src_node.account.ssh("nc -w 3 -z %s 22" % dst_node.account.hostname, allow_fail=True)
+
+
+def annotate_missing_msgs(missing, acked, consumed, msg):
+ missing_list = list(missing)
+ msg += "%s acked message did not make it to the Consumer. They are: " %\
+ len(missing_list)
+ if len(missing_list) < 20:
+ msg += str(missing_list) + ". "
+ else:
+ msg += ", ".join(str(m) for m in missing_list[:20])
+ msg += "...plus %s more. Total Acked: %s, Total Consumed: %s. " \
+ % (len(missing_list) - 20, len(set(acked)), len(set(consumed)))
+ return msg
+
+
+def annotate_data_lost(data_lost, msg, number_validated):
+ print_limit = 10
+ if len(data_lost) > 0:
+ msg += "The first %s missing messages were validated to ensure they are in Kafka's data files. " \
+ "%s were missing. This suggests data loss. Here are some of the messages not found in the data files: %s\n" \
+ % (number_validated, len(data_lost), str(data_lost[0:print_limit]) if len(data_lost) > print_limit else str(data_lost))
+ else:
+ msg += "We validated that the first %s of these missing messages correctly made it into Kafka's data files. " \
+ "This suggests they were lost on their way to the consumer." % number_validated
+ return msg
+
+
+def validate_delivery(acked, consumed, idempotence_enabled=False, check_lost_data=None, may_truncate_acked_records=False):
+ """Check that each acked message was consumed."""
+ success = True
+ msg = ""
+
+ # Correctness of the set difference operation depends on using equivalent
+ # message_validators in producer and consumer
+ missing = set(acked) - set(consumed)
+
+ # Were all acked messages consumed?
+ if len(missing) > 0:
+ msg = annotate_missing_msgs(missing, acked, consumed, msg)
+
+ # Did we miss anything due to data loss?
+ if check_lost_data:
+ max_truncate_count = 100 if may_truncate_acked_records else 0
+ max_validate_count = max(1000, max_truncate_count)
+
+ to_validate = list(missing)[0:min(len(missing), max_validate_count)]
+ data_lost = check_lost_data(to_validate)
+
+ # With older versions of message format before KIP-101, data loss could occur due to truncation.
+ # These records won't be in the data logs. Tolerate limited data loss for this case.
+ if len(missing) < max_truncate_count and len(data_lost) == len(missing):
+ msg += "The %s missing messages were not present in Kafka's data files. This suggests data loss " \
+ "due to truncation, which is possible with older message formats and hence are ignored " \
+ "by this test. The messages lost: %s\n" % (len(data_lost), str(data_lost))
+ else:
+ msg = annotate_data_lost(data_lost, msg, len(to_validate))
+ success = False
+ else:
+ success = False
+
+ # Are there duplicates?
+ if len(set(consumed)) != len(consumed):
+ num_duplicates = abs(len(set(consumed)) - len(consumed))
+
+ if idempotence_enabled:
+ success = False
+ msg += "Detected %d duplicates even though idempotence was enabled.\n" % num_duplicates
+ else:
+ msg += "(There are also %d duplicate messages in the log - but that is an acceptable outcome)\n" % num_duplicates
+
+ return success, msg
diff --git a/tests/ignitetest/version.py b/tests/ignitetest/version.py
new file mode 100644
index 0000000..9509909
--- /dev/null
+++ b/tests/ignitetest/version.py
@@ -0,0 +1,140 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+
+from distutils.version import LooseVersion
+from ignitetest.utils import ignitetest_version
+
+
+class KafkaVersion(LooseVersion):
+ """Container for kafka versions which makes versions simple to compare.
+
+ distutils.version.LooseVersion (and StrictVersion) has robust comparison and ordering logic.
+
+ Example:
+
+ v10 = KafkaVersion("0.10.0")
+ v9 = KafkaVersion("0.9.0.1")
+ assert v10 > v9 # assertion passes!
+ """
+ def __init__(self, version_string):
+ self.is_dev = (version_string.lower() == "dev")
+ if self.is_dev:
+ version_string = kafkatest_version()
+
+ # Drop dev suffix if present
+ dev_suffix_index = version_string.find(".dev")
+ if dev_suffix_index >= 0:
+ version_string = version_string[:dev_suffix_index]
+
+ # Don't use the form super.(...).__init__(...) because
+ # LooseVersion is an "old style" python class
+ LooseVersion.__init__(self, version_string)
+
+ def __str__(self):
+ if self.is_dev:
+ return "dev"
+ else:
+ return LooseVersion.__str__(self)
+
+ def supports_named_listeners(self):
+ return self >= V_0_10_2_0
+
+ def topic_command_supports_bootstrap_server(self):
+ return self >= V_2_3_0
+
+def get_version(node=None):
+ """Return the version attached to the given node.
+ Default to DEV_BRANCH if node or node.version is undefined (aka None)
+ """
+ if node is not None and hasattr(node, "version") and node.version is not None:
+ return node.version
+ else:
+ return DEV_BRANCH
+
+DEV_BRANCH = KafkaVersion("dev")
+DEV_VERSION = KafkaVersion("2.6.0-SNAPSHOT")
+
+# 0.8.2.x versions
+V_0_8_2_1 = KafkaVersion("0.8.2.1")
+V_0_8_2_2 = KafkaVersion("0.8.2.2")
+LATEST_0_8_2 = V_0_8_2_2
+
+# 0.9.0.x versions
+V_0_9_0_0 = KafkaVersion("0.9.0.0")
+V_0_9_0_1 = KafkaVersion("0.9.0.1")
+LATEST_0_9 = V_0_9_0_1
+
+# 0.10.0.x versions
+V_0_10_0_0 = KafkaVersion("0.10.0.0")
+V_0_10_0_1 = KafkaVersion("0.10.0.1")
+LATEST_0_10_0 = V_0_10_0_1
+
+# 0.10.1.x versions
+V_0_10_1_0 = KafkaVersion("0.10.1.0")
+V_0_10_1_1 = KafkaVersion("0.10.1.1")
+LATEST_0_10_1 = V_0_10_1_1
+
+# 0.10.2.x versions
+V_0_10_2_0 = KafkaVersion("0.10.2.0")
+V_0_10_2_1 = KafkaVersion("0.10.2.1")
+V_0_10_2_2 = KafkaVersion("0.10.2.2")
+LATEST_0_10_2 = V_0_10_2_2
+
+LATEST_0_10 = LATEST_0_10_2
+
+# 0.11.0.x versions
+V_0_11_0_0 = KafkaVersion("0.11.0.0")
+V_0_11_0_1 = KafkaVersion("0.11.0.1")
+V_0_11_0_2 = KafkaVersion("0.11.0.2")
+V_0_11_0_3 = KafkaVersion("0.11.0.3")
+LATEST_0_11_0 = V_0_11_0_3
+LATEST_0_11 = LATEST_0_11_0
+
+# 1.0.x versions
+V_1_0_0 = KafkaVersion("1.0.0")
+V_1_0_1 = KafkaVersion("1.0.1")
+V_1_0_2 = KafkaVersion("1.0.2")
+LATEST_1_0 = V_1_0_2
+
+# 1.1.x versions
+V_1_1_0 = KafkaVersion("1.1.0")
+V_1_1_1 = KafkaVersion("1.1.1")
+LATEST_1_1 = V_1_1_1
+
+# 2.0.x versions
+V_2_0_0 = KafkaVersion("2.0.0")
+V_2_0_1 = KafkaVersion("2.0.1")
+LATEST_2_0 = V_2_0_1
+
+# 2.1.x versions
+V_2_1_0 = KafkaVersion("2.1.0")
+V_2_1_1 = KafkaVersion("2.1.1")
+LATEST_2_1 = V_2_1_1
+
+# 2.2.x versions
+V_2_2_0 = KafkaVersion("2.2.0")
+V_2_2_1 = KafkaVersion("2.2.1")
+V_2_2_2 = KafkaVersion("2.2.2")
+LATEST_2_2 = V_2_2_2
+
+# 2.3.x versions
+V_2_3_0 = KafkaVersion("2.3.0")
+V_2_3_1 = KafkaVersion("2.3.1")
+LATEST_2_3 = V_2_3_1
+
+# 2.4.x versions
+V_2_4_0 = KafkaVersion("2.4.0")
+LATEST_2_4 = V_2_4_0
diff --git a/tests/setup.cfg b/tests/setup.cfg
new file mode 100644
index 0000000..974d5bb
--- /dev/null
+++ b/tests/setup.cfg
@@ -0,0 +1,30 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# pytest configuration (can also be defined in tox.ini or pytest.ini file)
+#
+# This file defines naming convention and root search directory for autodiscovery of
+# pytest unit tests for the system test service classes.
+#
+# To ease possible confusion, 'check' instead of 'test' as a prefix for unit tests, since
+# many system test files, classes, and methods have 'test' somewhere in the name
+[pytest]
+testpaths=unit
+python_files=check_*.py
+python_classes=Check
+python_functions=check_*
+
+# don't search inside any resources directory for unit tests
+norecursedirs = resources
diff --git a/tests/setup.py b/tests/setup.py
new file mode 100644
index 0000000..3e47716
--- /dev/null
+++ b/tests/setup.py
@@ -0,0 +1,57 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+import re
+import sys
+from setuptools import find_packages, setup
+from setuptools.command.test import test as TestCommand
+
+version = ''
+with open('ignitetest/__init__.py', 'r') as fd:
+ version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1)
+
+
+class PyTest(TestCommand):
+ user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
+
+ def initialize_options(self):
+ TestCommand.initialize_options(self)
+ self.pytest_args = []
+
+ def finalize_options(self):
+ TestCommand.finalize_options(self)
+ self.test_args = []
+ self.test_suite = True
+
+ def run_tests(self):
+ # import here, cause outside the eggs aren't loaded
+ import pytest
+ print(self.pytest_args)
+ errno = pytest.main(self.pytest_args)
+ sys.exit(errno)
+
+# Note: when changing the version of ducktape, also revise tests/docker/Dockerfile
+setup(name="ignitetest",
+ version=version,
+ description="Apache Ignite System Tests",
+ author="Apache Ignite",
+ platforms=["any"],
+ license="apache2.0",
+ packages=find_packages(),
+ include_package_data=True,
+ install_requires=["ducktape==0.7.6", "requests==2.20.0"],
+ tests_require=["pytest", "mock"],
+ cmdclass={'test': PyTest}
+)