You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by av...@apache.org on 2020/07/29 13:06:38 UTC

[ignite] branch ignite-ducktape updated: Make ducktape work with ISE / ISP (#8071)

This is an automated email from the ASF dual-hosted git repository.

av pushed a commit to branch ignite-ducktape
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/ignite-ducktape by this push:
     new 5f2ca37  Make ducktape work with ISE / ISP (#8071)
5f2ca37 is described below

commit 5f2ca372fa8baa07b4df44a8929cc676e6cce54e
Author: Maksim Timonin <ti...@gmail.com>
AuthorDate: Wed Jul 29 16:06:15 2020 +0300

    Make ducktape work with ISE / ISP (#8071)
---
 modules/ducktests/tests/README.md                  |  13 ++
 modules/ducktests/tests/docker/clean_up.sh         |   4 +-
 modules/ducktests/tests/docker/ducker-ignite       | 140 +++++++++++++++------
 modules/ducktests/tests/docker/run_tests.sh        | 123 +++++++++++++++++-
 modules/ducktests/tests/ignitetest/__init__.py     |   2 +-
 .../ducktests/tests/ignitetest/services/ignite.py  |   8 +-
 .../tests/ignitetest/services/ignite_app.py        |   7 +-
 .../tests/ignitetest/services/ignite_spark_app.py  |   7 +-
 .../ducktests/tests/ignitetest/services/spark.py   |   6 +-
 .../ignitetest/services/utils/config/ignite.xml.j2 |  36 ++++++
 .../ignitetest/services/utils/config/log4j.xml.j2  |  57 +++++++++
 .../ignitetest/services/utils/ignite_aware.py      |  23 ++--
 .../ignitetest/services/utils/ignite_aware_app.py  |   7 +-
 .../ignitetest/services/utils/ignite_config.py     | 112 +++++++----------
 .../tests/ignitetest/services/utils/ignite_path.py |   4 +-
 .../ignitetest/tests/add_node_rebalance_test.py    |   7 --
 .../tests/ignitetest/tests/discovery_test.py       |   5 +-
 .../tests/ignitetest/tests/pme_free_switch_test.py |   9 +-
 .../ignitetest/tests/spark_integration_test.py     |  28 +++--
 .../tests/ignitetest/tests/utils/version.py        |  14 ++-
 20 files changed, 444 insertions(+), 168 deletions(-)

diff --git a/modules/ducktests/tests/README.md b/modules/ducktests/tests/README.md
new file mode 100644
index 0000000..cbf7482
--- /dev/null
+++ b/modules/ducktests/tests/README.md
@@ -0,0 +1,13 @@
+## Overview
+The `ignitetest` framework provides basic functionality and services
+to write integration tests for Apache Ignite. This framework bases on 
+the `ducktape` test framework, for information about it check the links:
+- https://github.com/confluentinc/ducktape - source code of the `ducktape`;
+- http://ducktape-docs.readthedocs.io - documentation to the `ducktape`.
+
+Structure of the `ignitetest` directory is:
+- `./ignitetest/services` contains basic services functionality;
+- `./ignitetest/tests/utils` contains utils for testing.  
+
+## Review Checklist
+1. All tests must be parameterized with `version`.
diff --git a/modules/ducktests/tests/docker/clean_up.sh b/modules/ducktests/tests/docker/clean_up.sh
old mode 100644
new mode 100755
index 34ce05d..a5efe2d
--- a/modules/ducktests/tests/docker/clean_up.sh
+++ b/modules/ducktests/tests/docker/clean_up.sh
@@ -15,5 +15,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-docker stop $(docker ps -a -q)
-docker rm $(docker ps -a -q)
\ No newline at end of file
+bash ./ducker-ignite down
+
diff --git a/modules/ducktests/tests/docker/ducker-ignite b/modules/ducktests/tests/docker/ducker-ignite
index e7287ca..9a92247 100755
--- a/modules/ducktests/tests/docker/ducker-ignite
+++ b/modules/ducktests/tests/docker/ducker-ignite
@@ -60,10 +60,18 @@ Usage: ${script_path} [command] [options]
 help|-h|--help
     Display this help message
 
+build [-j|--jdk JDK] [-c|--context] [image-name]
+    Build a docker image that represents ducker node. Image is tagged with specified ${image_name}.
+
+    If --jdk is specified then we will use this argument as base image for ducker docker images.
+    Otherwise ${default_jdk} is used.
+
+    If --context is specified then build docker image from this path. Context directory must contain Dockerfile.
+
 up [-n|--num-nodes NUM_NODES] [-f|--force] [docker-image]
-        [-C|--custom-ducktape DIR] [-e|--expose-ports ports]
+        [-C|--custom-ducktape DIR] [-e|--expose-ports ports] [-j|--jdk JDK_VERSION]
     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
+    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
@@ -75,6 +83,9 @@ up [-n|--num-nodes NUM_NODES] [-f|--force] [docker-image]
     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.
 
+    If --jdk is specified then we will use this argument as base image for ducktest's docker images.
+    Otherwise ${default_jdk} is used.
+
 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:
@@ -96,6 +107,11 @@ down [-q|--quiet] [-f|--force]
 purge [--f|--force]
     Purge Docker images created by ducker-ignite.  This will free disk space.
     If --force is set, we run 'docker rmi -f'.
+
+compare [docker-image]
+    Compare image id of last run and last build for specified docker-image. If they are different then cluster runs
+    non-actual version of image. Rerun is required.
+
 EOF
     exit "${exit_status}"
 }
@@ -189,9 +205,9 @@ must_do() {
             *) break;;
         esac
     done
-    local cmd="${@}"
-    [[ "${verbose}" -eq 1 ]] && echo "${cmd}"
-    ${cmd} >${output} || die "${1} failed"
+
+    [[ "${verbose}" -eq 1 ]] && echo "${@}"
+    eval "${@}" >${output} || die "${1} failed"
 }
 
 # Ask the user a yes/no question.
@@ -214,9 +230,11 @@ ask_yes_no() {
 
 # Build a docker image.
 #
-# $1: The name of the image to build.
-ducker_build() {
-    local image_name="${1}"
+# $1: The docker build context
+# $2: The name of the image to build.
+ducker_build_image() {
+    local docker_context="${1}"
+    local image_name="${2}"
 
     # Use SECONDS, a builtin bash variable that gets incremented each second, to measure the docker
     # build duration.
@@ -227,16 +245,38 @@ ducker_build() {
     # (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} -- .
+        -f "${docker_context}/Dockerfile" -- "${docker_context}"
     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."
+        die "** ERROR: Failed to build ${what} image after $((duration / 60))m $((duration % 60))s."
     fi
-    echo "** Successfully built ${what} image in $((${duration} / 60))m \
-$((${duration} % 60))s.  See ${build_log} for details."
+
+    # Save docker image id to the file. Then could use this file to find version of docker image built last time.
+    # It could be useful if we don't confident about necessity of stoping the cluster.
+    get_image_id "${image_name}" > "${ducker_dir}/build/image_${image_name}.build"
+
+    echo "** Successfully built ${what} image in $((duration / 60))m $((duration % 60))s."
+}
+
+ducker_build() {
+    require_commands docker
+
+    local docker_context=
+    while [[ $# -ge 1 ]]; do
+        case "${1}" in
+            -j|--jdk) set_once jdk_version "${2}" "the OpenJDK base image"; shift 2;;
+            -c|--context) docker_context="${2}"; shift 2;;
+            *) set_once image_name "${1}" "docker image name"; shift;;
+        esac
+    done
+
+    [[ -n "${jdk_version}" ]] || jdk_version="${default_jdk}"
+    [[ -n "${image_name}" ]] || image_name="${default_image_name}-${jdk_version/:/-}"
+    [[ -n "${docker_context}" ]] || docker_context="${ducker_dir}"
+
+    ducker_build_image "${docker_context}" "${image_name}"
 }
 
 docker_run() {
@@ -256,9 +296,9 @@ docker_run() {
     # 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}" \
+        -d -t -h "${node}" --network ducknet ${expose_ports} \
         --memory=${docker_run_memory_limit} --memory-swappiness=1 \
-        -v "${ignite_dir}:/opt/ignite-dev:delegated" --name "${node}" -- "${image_name}"
+        --mount type=bind,source="${ignite_dir}",target=/opt/ignite-dev,consistency=delegated --name "${node}" -- "${image_name}"
 }
 
 setup_custom_ducktape() {
@@ -268,7 +308,8 @@ setup_custom_ducktape() {
     [[ -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)"
+    local running_container
+    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/modules/ducktests/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."
@@ -284,14 +325,12 @@ ducker_up() {
             -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/:/-}"
+    [[ -n "${image_name}" ]] || image_name="${default_image_name}-${default_jdk/:/-}"
     [[ "${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."
@@ -306,21 +345,6 @@ use only ${num_nodes}."
 
     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
@@ -330,6 +354,9 @@ attempting to start new ones."
     fi
 
     echo "ducker_up: Bringing up ${image_name} with ${num_nodes} nodes..."
+    docker image inspect "${image_name}" &>/dev/null || \
+      must_do -v -o docker pull "${image_name}"
+
     if docker network inspect ducknet &>/dev/null; then
         must_do -v docker network rm ducknet
     fi
@@ -359,6 +386,11 @@ attempting to start new ones."
     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"
+
+    # Save docker image id to the file. Then could use this file to find version of docker image that is running.
+    # It could be useful if we don't confident about necessity of rebuilding image.
+    get_image_id "${image_name}" > "${ducker_dir}/build/image_id.up"
+
     echo "** ducker_up: successfully brought up ${num_nodes} nodes."
 }
 
@@ -508,7 +540,8 @@ ducker_down() {
     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)"
+    local all_containers
+    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."
@@ -519,13 +552,14 @@ ducker_down() {
         verbose_flag="-v"
     fi
     if [[ -n "${running_containers}" ]]; then
-        must_do ${verbose_flag} docker kill "${running_containers}"
+        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
+    rm "${ducker_dir}/build/image_id.up"
     maybe_echo "${verbose}" "ducker_down: removed $(count ${all_containers}) containers."
 }
 
@@ -559,6 +593,38 @@ ducker_purge() {
     must_do -v -o docker rmi ${force_str} ${images}
 }
 
+get_image_id() {
+    require_commands docker
+    local image_name="${1}"
+
+    must_do -o docker image inspect --format "{{.Id}}" "${image_name}"
+}
+
+ducker_compare() {
+    local cmd=""
+
+    local verbose=1
+    local force_str=""
+
+    while [[ $# -ge 1 ]]; do
+        case "${1}" in
+            -q|--quiet) verbose=0; cmd="${cmd} ${1}"; shift;;
+            -f|--force) force_str="-f"; cmd="${cmd} ${1}"; shift;;
+            *) set_once image_name "${1}" "docker image name"; shift;;
+        esac
+    done
+
+    [ -n "${image_name}" ] || image_name="${default_image_name}-${default_jdk/:/-}"
+
+    cmp -s "${ducker_dir}/build/image_${image_name}.build" "${ducker_dir}/build/image_id.up"
+    local ret="$?"
+
+    if [[ $ret != "0" ]]; then
+        echo "Docker image ${image_name} is outdated. Stop the cluster"
+        ducker_down ${cmd}
+    fi
+}
+
 # Parse command-line arguments
 [[ $# -lt 1 ]] && usage 0
 # Display the help text if -h or --help appears in the command line
@@ -574,7 +640,7 @@ shift
 case "${action}" in
     help) usage 0;;
 
-    up|test|ssh|down|purge)
+    build|up|test|ssh|down|purge|compare)
         ducker_${action} "${@}"; exit 0;;
 
     *)  echo "Unknown command '${action}'.  Type '${script_path} --help' for usage information."
diff --git a/modules/ducktests/tests/docker/run_tests.sh b/modules/ducktests/tests/docker/run_tests.sh
index e392e3d..e5dc561 100755
--- a/modules/ducktests/tests/docker/run_tests.sh
+++ b/modules/ducktests/tests/docker/run_tests.sh
@@ -16,15 +16,128 @@
 # limitations under the License.
 
 SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+###
+# DuckerUp parameters are specified with env variables
+
+# Num of cotainers that ducktape will prepare for tests
 IGNITE_NUM_CONTAINERS=${IGNITE_NUM_CONTAINERS:-11}
-TC_PATHS=${TC_PATHS:-./ignitetest/}
+
+# Image name to run nodes
+default_image_name="ducker-ignite-openjdk-8"
+IMAGE_NAME="${IMAGE_NAME:-$default_image_name}"
+
+###
+# DuckerTest parameters are specified with options to the script
+
+# Path to ducktests
+TC_PATHS="./ignitetest/"
+# Global parameters to pass to ducktape util with --global param
+GLOBALS="{}"
+# Ducktests parameters to pass to ducktape util with --parameters param
+PARAMETERS="{}"
+
+###
+# RunTests parameters
+# Force flag:
+# - skips ducker-ignite compare step;
+# - sends to duck-ignite scripts.
+FORCE=
+
+usage() {
+    cat <<EOF
+run_tests.sh: useful entrypoint to ducker-ignite util
+
+Usage: ${0} [options]
+
+The options are as follows:
+-h|--help
+    Display this help message
+
+-p|--param
+    Use specified param to inject in tests. Could be used multiple times.
+
+    ./run_tests.sh --param version=2.8.1
+
+-g|--global
+    Use specified global param to pass to test context. Could be used multiple times.
+
+    List of supported global parameters:
+    - project: is used to build path to Ignite binaries within container (/opt/PROJECT-VERSION)
+    - ignite_client_config_path: abs path within container to Ignite client config template
+    - ignite_server_config_path: abs path within container to Ignite server config template
+    - jvm_opts: array of JVM options to use when Ignite node started
+
+-t|--tc-paths
+    Path to ducktests. Must be relative path to 'IGNITE/modules/ducktests/tests' directory
+
+EOF
+    exit 0
+}
+
 
 die() {
-    echo $@
+    echo "$@"
     exit 1
 }
 
-if ${SCRIPT_DIR}/ducker-ignite ssh | grep -q '(none)'; then
-    ${SCRIPT_DIR}/ducker-ignite up -n "${IGNITE_NUM_CONTAINERS}" || die "ducker-ignite up failed"
+_extend_json() {
+    python - "$1" "$2" <<EOF
+import sys
+import json
+
+[j, key_val] = sys.argv[1:]
+[key, val] = key_val.split('=', 1)
+j = json.loads(j)
+j[key] = val
+
+print(json.dumps(j))
+
+EOF
+}
+
+duck_add_global() {
+  GLOBALS="$(_extend_json "${GLOBALS}" "${1}")"
+}
+
+duck_add_param() {
+  PARAMETERS="$(_extend_json "${PARAMETERS}" "${1}")"
+}
+
+while [[ $# -ge 1 ]]; do
+    case "$1" in
+        -h|--help) usage;;
+        -p|--param) duck_add_param "$2"; shift 2;;
+        -g|--global) duck_add_global "$2"; shift 2;;
+        -t|--tc-paths) TC_PATHS="$2"; shift 2;;
+        -f|--force) FORCE=$1; shift;;
+        *) break;;
+    esac
+done
+
+if [[ "$IMAGE_NAME" == "$default_image_name" ]]; then
+    "$SCRIPT_DIR"/ducker-ignite build "$IMAGE_NAME" || die "ducker-ignite build failed"
+else
+    echo "[WARN] Used non-default image $IMAGE_NAME. Be sure you use actual version of the image. " \
+         "Otherwise build it with 'ducker-ignite build' command"
+fi
+
+if [ -z "$FORCE" ]; then
+    # If docker image changed then restart cluster (down here and up within next step)
+    "$SCRIPT_DIR"/ducker-ignite compare "$IMAGE_NAME" || die "ducker-ignite compare failed"
+fi
+
+# Up cluster if nothing is running
+if "$SCRIPT_DIR"/ducker-ignite ssh | grep -q '(none)'; then
+    # do not quote FORCE as bash recognize "" as input param instead of image name
+    "$SCRIPT_DIR"/ducker-ignite up $FORCE -n "$IGNITE_NUM_CONTAINERS" "$IMAGE_NAME" || die "ducker-ignite up failed"
+fi
+
+DUCKTAPE_OPTIONS="--globals '$GLOBALS'"
+# If parameters are passed in options than it must contain all possible parameters, otherwise None will be injected
+if [[ "$PARAMETERS" != "{}" ]]; then
+    DUCKTAPE_OPTIONS="$DUCKTAPE_OPTIONS --parameters '$PARAMETERS'"
 fi
-${SCRIPT_DIR}/ducker-ignite test ${TC_PATHS} ${_DUCKTAPE_OPTIONS} || die "ducker-ignite test failed"
+
+"$SCRIPT_DIR"/ducker-ignite test "$TC_PATHS" "$DUCKTAPE_OPTIONS" \
+  || die "ducker-ignite test failed"
diff --git a/modules/ducktests/tests/ignitetest/__init__.py b/modules/ducktests/tests/ignitetest/__init__.py
index a4b3d8c..1c2357f 100644
--- a/modules/ducktests/tests/ignitetest/__init__.py
+++ b/modules/ducktests/tests/ignitetest/__init__.py
@@ -22,4 +22,4 @@
 # 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'
+__version__ = '2.10.0.dev0'
diff --git a/modules/ducktests/tests/ignitetest/services/ignite.py b/modules/ducktests/tests/ignitetest/services/ignite.py
index 375c2ad..05f108e 100644
--- a/modules/ducktests/tests/ignitetest/services/ignite.py
+++ b/modules/ducktests/tests/ignitetest/services/ignite.py
@@ -20,6 +20,7 @@ from ducktape.cluster.remoteaccount import RemoteCommandError
 from ducktape.utils.util import wait_until
 
 from ignitetest.services.utils.ignite_aware import IgniteAwareService
+from ignitetest.services.utils.ignite_config import IgniteServerConfig, IgniteClientConfig
 from ignitetest.tests.utils.version import DEV_BRANCH
 
 
@@ -37,8 +38,8 @@ class IgniteService(IgniteAwareService):
             "collect_default": False}
     }
 
-    def __init__(self, context, num_nodes, version=DEV_BRANCH, properties=""):
-        super(IgniteService, self).__init__(context, num_nodes, version, properties)
+    def __init__(self, context, num_nodes, client_mode=False, version=DEV_BRANCH, properties=""):
+        super(IgniteService, self).__init__(context, num_nodes, client_mode, version, properties)
 
     def start(self, timeout_sec=180):
         super(IgniteService, self).start()
@@ -49,7 +50,8 @@ class IgniteService(IgniteAwareService):
             self.await_node_started(node, timeout_sec)
 
     def start_cmd(self, node):
-        jvm_opts = "-J-DIGNITE_SUCCESS_FILE=" + IgniteService.PERSISTENT_ROOT + "/success_file "
+        jvm_opts = self.jvm_options + " "
+        jvm_opts += "-J-DIGNITE_SUCCESS_FILE=" + IgniteService.PERSISTENT_ROOT + "/success_file "
         jvm_opts += "-J-Dlog4j.configDebug=true "
 
         cmd = "export EXCLUDE_TEST_CLASSES=true; "
diff --git a/modules/ducktests/tests/ignitetest/services/ignite_app.py b/modules/ducktests/tests/ignitetest/services/ignite_app.py
index a073fb3..64de176 100644
--- a/modules/ducktests/tests/ignitetest/services/ignite_app.py
+++ b/modules/ducktests/tests/ignitetest/services/ignite_app.py
@@ -24,6 +24,7 @@ The Ignite application service allows to perform custom logic writen on java.
 class IgniteApplicationService(IgniteAwareApplicationService):
     service_java_class_name = "org.apache.ignite.internal.ducktest.utils.IgniteApplicationService"
 
-    def __init__(self, context, java_class_name, version=DEV_BRANCH, properties="", params="", timeout_sec=60):
-        super(IgniteApplicationService, self).__init__(context, java_class_name, version, properties, params,
-                                                       timeout_sec, self.service_java_class_name)
+    def __init__(self, context, java_class_name, client_mode=True, version=DEV_BRANCH, properties="", params="",
+                 timeout_sec=60):
+        super(IgniteApplicationService, self).__init__(context, java_class_name, client_mode, version, properties,
+                                                       params, timeout_sec, self.service_java_class_name)
diff --git a/modules/ducktests/tests/ignitetest/services/ignite_spark_app.py b/modules/ducktests/tests/ignitetest/services/ignite_spark_app.py
index 5cc66c3..775f19c1 100644
--- a/modules/ducktests/tests/ignitetest/services/ignite_spark_app.py
+++ b/modules/ducktests/tests/ignitetest/services/ignite_spark_app.py
@@ -21,9 +21,10 @@ from ignitetest.tests.utils.version import DEV_BRANCH
 
 
 class SparkIgniteApplicationService(IgniteAwareApplicationService):
-    def __init__(self, context, java_class_name, version=DEV_BRANCH, properties="", params="", timeout_sec=60):
-        super(SparkIgniteApplicationService, self).__init__(context, java_class_name, version, properties, params,
-                                                            timeout_sec)
+    def __init__(self, context, java_class_name, client_mode=True, version=DEV_BRANCH, properties="", params="",
+                 timeout_sec=60):
+        super(SparkIgniteApplicationService, self).__init__(context, java_class_name, client_mode, version, properties,
+                                                            params, timeout_sec)
 
     def env(self):
         return IgniteAwareApplicationService.env(self) + \
diff --git a/modules/ducktests/tests/ignitetest/services/spark.py b/modules/ducktests/tests/ignitetest/services/spark.py
index 1ac937d..fdd5c73 100644
--- a/modules/ducktests/tests/ignitetest/services/spark.py
+++ b/modules/ducktests/tests/ignitetest/services/spark.py
@@ -19,7 +19,8 @@ from ducktape.cluster.remoteaccount import RemoteCommandError
 from ducktape.services.service import Service
 
 from ignitetest.services.utils.ignite_aware import IgniteAwareService
-from ignitetest.services.utils.ignite_config import IgniteConfig
+from ignitetest.services.utils.ignite_config import IgniteServerConfig
+
 from ignitetest.tests.utils.version import DEV_BRANCH
 
 
@@ -34,10 +35,9 @@ class SparkService(IgniteAwareService):
         :param context: test context
         :param num_nodes: number of Ignite nodes.
         """
-        IgniteAwareService.__init__(self, context, num_nodes, version, properties)
+        IgniteAwareService.__init__(self, context, num_nodes, False, version, properties)
 
         self.log_level = "DEBUG"
-        self.ignite_config = IgniteConfig()
 
         for node in self.nodes:
             self.logs["master_logs" + node.account.hostname] = {
diff --git a/modules/ducktests/tests/ignitetest/services/utils/config/ignite.xml.j2 b/modules/ducktests/tests/ignitetest/services/utils/config/ignite.xml.j2
new file mode 100644
index 0000000..5ceb5b6
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/config/ignite.xml.j2
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+                            http://www.springframework.org/schema/beans/spring-beans.xsd">
+    <bean class="org.apache.ignite.configuration.IgniteConfiguration">
+        <property name="workDirectory" value="{{ work_dir }}" />
+        <property name="gridLogger">
+            <bean class="org.apache.ignite.logger.log4j.Log4JLogger">
+                <constructor-arg type="java.lang.String" value="{{ config_dir }}/ignite-log4j.xml"/>
+            </bean>
+        </property>
+
+        <property name="clientMode" value="{{ client_mode or False | lower }}"/>
+
+        {{ properties }}
+    </bean>
+</beans>
diff --git a/modules/ducktests/tests/ignitetest/services/utils/config/log4j.xml.j2 b/modules/ducktests/tests/ignitetest/services/utils/config/log4j.xml.j2
new file mode 100644
index 0000000..e8b3be8
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/config/log4j.xml.j2
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN"
+    "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
+
+<!--
+ 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.
+-->
+
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
+    <appender name="CONSOLE_ERR" class="org.apache.log4j.ConsoleAppender">
+        <param name="Target" value="System.err"/>
+
+        <param name="Threshold" value="INFO"/>
+
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{{ISO8601}}][%-5p][%t][%c{{1}}] %m%n"/>
+        </layout>
+    </appender>
+
+    <category name="org.springframework">
+        <level value="WARN"/>
+    </category>
+
+    <category name="org.eclipse.jetty">
+        <level value="WARN"/>
+    </category>
+
+    <category name="org.eclipse.jetty.util.log">
+        <level value="ERROR"/>
+    </category>
+
+    <category name="org.eclipse.jetty.util.component">
+        <level value="ERROR"/>
+    </category>
+
+    <category name="com.amazonaws">
+        <level value="WARN"/>
+    </category>
+
+    <root>
+        <level value="INFO"/>
+        <appender-ref ref="CONSOLE_ERR"/>
+    </root>
+</log4j:configuration>
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
index 106493c..578d728 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
@@ -18,7 +18,7 @@ from abc import abstractmethod
 from ducktape.services.background_thread import BackgroundThreadService
 from ducktape.utils.util import wait_until
 
-from ignitetest.services.utils.ignite_config import IgniteConfig
+from ignitetest.services.utils.ignite_config import IgniteLoggerConfig, IgniteServerConfig, IgniteClientConfig
 from ignitetest.services.utils.ignite_path import IgnitePath
 from ignitetest.services.utils.jmx_utils import ignite_jmx_mixin
 
@@ -41,14 +41,17 @@ class IgniteAwareService(BackgroundThreadService):
             "collect_default": True}
     }
 
-    def __init__(self, context, num_nodes, version, properties):
+    def __init__(self, context, num_nodes, client_mode, version, properties):
         super(IgniteAwareService, self).__init__(context, num_nodes)
 
+        self.path = IgnitePath(context)
+        self.jvm_options = context.globals.get("jvm_opts", "")
+
         self.log_level = "DEBUG"
-        self.config = IgniteConfig()
-        self.path = IgnitePath()
         self.properties = properties
         self.version = version
+        self.logger_config = IgniteLoggerConfig()
+        self.client_mode = client_mode
 
         for node in self.nodes:
             node.version = version
@@ -64,9 +67,9 @@ class IgniteAwareService(BackgroundThreadService):
 
     def init_persistent(self, node):
         node.account.mkdirs(self.PERSISTENT_ROOT)
-        node.account.create_file(self.CONFIG_FILE, self.config.render(
-            self.PERSISTENT_ROOT, self.WORK_DIR, properties=self.properties))
-        node.account.create_file(self.LOG4J_CONFIG_FILE, self.config.render_log4j(self.WORK_DIR))
+        node.account.create_file(self.CONFIG_FILE, self.config().render(
+            config_dir=self.PERSISTENT_ROOT, work_dir=self.WORK_DIR, properties=self.properties))
+        node.account.create_file(self.LOG4J_CONFIG_FILE, self.logger_config.render(work_dir=self.WORK_DIR))
 
     @abstractmethod
     def start_cmd(self, node):
@@ -76,6 +79,12 @@ class IgniteAwareService(BackgroundThreadService):
     def pids(self, node):
         raise NotImplementedError
 
+    def config(self):
+        if self.client_mode:
+            return IgniteClientConfig(self.context)
+        else:
+            return IgniteServerConfig(self.context)
+
     def _worker(self, idx, node):
         cmd = self.start_cmd(node)
 
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware_app.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware_app.py
index 7b37633..fd2c860 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware_app.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware_app.py
@@ -17,6 +17,7 @@ import re
 from ducktape.services.service import Service
 
 from ignitetest.services.utils.ignite_aware import IgniteAwareService
+from ignitetest.services.utils.ignite_config import IgniteClientConfig
 
 """
 The base class to build Ignite aware application written on java.
@@ -24,9 +25,9 @@ The base class to build Ignite aware application written on java.
 
 
 class IgniteAwareApplicationService(IgniteAwareService):
-    def __init__(self, context, java_class_name, version, properties, params, timeout_sec,
+    def __init__(self, context, java_class_name, client_mode, version, properties, params, timeout_sec,
                  service_java_class_name="org.apache.ignite.internal.ducktest.utils.IgniteAwareApplicationService"):
-        super(IgniteAwareApplicationService, self).__init__(context, 1, version, properties)
+        super(IgniteAwareApplicationService, self).__init__(context, 1, client_mode, version, properties)
 
         self.servicejava_class_name = service_java_class_name
         self.java_class_name = java_class_name
@@ -87,7 +88,7 @@ class IgniteAwareApplicationService(IgniteAwareService):
                "-J-Dlog4j.configDebug=true " \
                "-J-Xmx1G " \
                "-J-ea " \
-               "-J-DIGNITE_ALLOW_ATOMIC_OPS_IN_TX=false "
+               "-J-DIGNITE_ALLOW_ATOMIC_OPS_IN_TX=false " + self.jvm_options
 
     def env(self):
         return "export MAIN_CLASS={main_class}; ".format(main_class=self.servicejava_class_name) + \
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_config.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_config.py
index 5ffaa81..077c39b 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_config.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_config.py
@@ -17,71 +17,49 @@
 This module renders Ignite config and all related artifacts
 """
 
+from jinja2 import FileSystemLoader, Environment
+
+import os
+
+DEFAULT_CONFIG_PATH = os.path.dirname(os.path.abspath(__file__)) + "/config"
+DEFAULT_IGNITE_CONF = DEFAULT_CONFIG_PATH + "/ignite.xml.j2"
+
+
+class Config(object):
+    def __init__(self, path):
+        tmpl_dir = os.path.dirname(path)
+        tmpl_file = os.path.basename(path)
+
+        tmpl_loader = FileSystemLoader(searchpath=tmpl_dir)
+        env = Environment(loader=tmpl_loader)
+
+        self.template = env.get_template(tmpl_file)
+        self.default_params = {}
+
+    def render(self, **kwargs):
+        kwargs.update(self.default_params)
+        res = self.template.render(**kwargs)
+        return res
+
+
+class IgniteServerConfig(Config):
+    def __init__(self, context):
+        path = DEFAULT_IGNITE_CONF
+        if 'ignite_server_config_path' in context.globals:
+            path = context.globals['ignite_server_config_path']
+        super(IgniteServerConfig, self).__init__(path)
+
+
+class IgniteClientConfig(Config):
+    def __init__(self, context):
+        path = DEFAULT_IGNITE_CONF
+        if 'ignite_client_config_path' in context.globals:
+            path = context.globals['ignite_client_config_path']
+        super(IgniteClientConfig, self).__init__(path)
+        self.default_params.update(client_mode=True)
+
+
+class IgniteLoggerConfig(Config):
+    def __init__(self):
+        super(IgniteLoggerConfig, self).__init__(DEFAULT_CONFIG_PATH + "/log4j.xml.j2")
 
-class IgniteConfig:
-    def __init__(self, project="ignite"):
-        self.project = project
-
-    def render(self, config_dir, work_dir, properties=""):
-        return """<?xml version="1.0" encoding="UTF-8"?>
-
-<beans xmlns="http://www.springframework.org/schema/beans"
-       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-       xsi:schemaLocation="http://www.springframework.org/schema/beans
-                            http://www.springframework.org/schema/beans/spring-beans.xsd">
-    <bean class="org.apache.ignite.configuration.IgniteConfiguration">
-        <property name="workDirectory" value="{work_dir}" />
-        <property name="gridLogger">
-            <bean class="org.apache.ignite.logger.log4j.Log4JLogger">
-                <constructor-arg type="java.lang.String" value="{config_dir}/ignite-log4j.xml"/>
-            </bean>
-        </property>
-        {properties}
-    </bean>
-</beans>
-        """.format(config_dir=config_dir,
-                   work_dir=work_dir,
-                   properties=properties)
-
-    def render_log4j(self, work_dir):
-        return """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN"
-    "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
-
-<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
-    <appender name="CONSOLE_ERR" class="org.apache.log4j.ConsoleAppender">
-        <param name="Target" value="System.err"/>
-
-        <param name="Threshold" value="INFO"/>
-
-        <layout class="org.apache.log4j.PatternLayout">
-            <param name="ConversionPattern" value="[%d{{ISO8601}}][%-5p][%t][%c{{1}}] %m%n"/>
-        </layout>
-    </appender>
-
-    <category name="org.springframework">
-        <level value="WARN"/>
-    </category>
-
-    <category name="org.eclipse.jetty">
-        <level value="WARN"/>
-    </category>
-
-    <category name="org.eclipse.jetty.util.log">
-        <level value="ERROR"/>
-    </category>
-
-    <category name="org.eclipse.jetty.util.component">
-        <level value="ERROR"/>
-    </category>
-
-    <category name="com.amazonaws">
-        <level value="WARN"/>
-    </category>
-
-    <root>
-        <level value="INFO"/>
-        <appender-ref ref="CONSOLE_ERR"/>
-    </root>
-</log4j:configuration>
-                """.format(work_dir=work_dir)
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py
index 8e4603a..4eab4a4 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py
@@ -34,8 +34,8 @@ class IgnitePath:
         ...
     """
 
-    def __init__(self, project="ignite"):
-        self.project = project
+    def __init__(self, context):
+        self.project = context.globals.get("project", "ignite")
 
     def home(self, node_or_version, project=None):
         version = self._version(node_or_version)
diff --git a/modules/ducktests/tests/ignitetest/tests/add_node_rebalance_test.py b/modules/ducktests/tests/ignitetest/tests/add_node_rebalance_test.py
index a558638..a3c9546 100644
--- a/modules/ducktests/tests/ignitetest/tests/add_node_rebalance_test.py
+++ b/modules/ducktests/tests/ignitetest/tests/add_node_rebalance_test.py
@@ -32,12 +32,6 @@ class AddNodeRebalanceTest(IgniteTest):
     Test performs rebalance tests.
     """
 
-    @staticmethod
-    def properties(client_mode="false"):
-        return """
-            <property name="clientMode" value="{client_mode}"/>
-        """.format(client_mode=client_mode)
-
     def __init__(self, test_context):
         super(AddNodeRebalanceTest, self).__init__(test_context=test_context)
 
@@ -70,7 +64,6 @@ class AddNodeRebalanceTest(IgniteTest):
         # This client just put some data to the cache.
         IgniteApplicationService(self.test_context,
                                  java_class_name="org.apache.ignite.internal.ducktest.tests.DataGenerationApplication",
-                                 properties=self.properties(client_mode="true"),
                                  version=ignite_version,
                                  params="test-cache,%d" % self.DATA_AMOUNT,
                                  timeout_sec=self.PRELOAD_TIMEOUT).run()
diff --git a/modules/ducktests/tests/ignitetest/tests/discovery_test.py b/modules/ducktests/tests/ignitetest/tests/discovery_test.py
index 246416a..01aebe4 100644
--- a/modules/ducktests/tests/ignitetest/tests/discovery_test.py
+++ b/modules/ducktests/tests/ignitetest/tests/discovery_test.py
@@ -28,7 +28,6 @@ class DiscoveryTest(IgniteTest):
     NUM_NODES = 7
 
     CONFIG_TEMPLATE = """
-        <property name="clientMode" value="{{ client_mode | lower }}"/>
         {% if zookeeper_settings %}
         {% with zk = zookeeper_settings %}
         <property name="discoverySpi">
@@ -49,9 +48,9 @@ class DiscoveryTest(IgniteTest):
         self.servers = None
 
     @staticmethod
-    def properties(client_mode="false", zookeeper_settings=None):
+    def properties(zookeeper_settings=None):
         return Template(DiscoveryTest.CONFIG_TEMPLATE) \
-            .render(client_mode=client_mode, zookeeper_settings=zookeeper_settings)
+            .render(zookeeper_settings=zookeeper_settings)
 
     def setUp(self):
         pass
diff --git a/modules/ducktests/tests/ignitetest/tests/pme_free_switch_test.py b/modules/ducktests/tests/ignitetest/tests/pme_free_switch_test.py
index 89653e9..0df2f84d 100644
--- a/modules/ducktests/tests/ignitetest/tests/pme_free_switch_test.py
+++ b/modules/ducktests/tests/ignitetest/tests/pme_free_switch_test.py
@@ -27,9 +27,8 @@ class PmeFreeSwitchTest(IgniteTest):
     NUM_NODES = 3
 
     @staticmethod
-    def properties(client_mode="false"):
+    def properties():
         return """
-            <property name="clientMode" value="{client_mode}"/>
             <property name="cacheConfiguration">
                 <list>
                     <bean class="org.apache.ignite.configuration.CacheConfiguration">
@@ -39,7 +38,7 @@ class PmeFreeSwitchTest(IgniteTest):
                     </bean>
                 </list>
             </property>
-        """.format(client_mode=client_mode)
+        """
 
     def __init__(self, test_context):
         super(PmeFreeSwitchTest, self).__init__(test_context=test_context)
@@ -73,7 +72,7 @@ class PmeFreeSwitchTest(IgniteTest):
         long_tx_streamer = IgniteApplicationService(
             self.test_context,
             java_class_name="org.apache.ignite.internal.ducktest.tests.pme_free_switch_test.LongTxStreamerApplication",
-            properties=self.properties(client_mode="true"),
+            properties=self.properties(),
             params="test-cache",
             version=ignite_version)
 
@@ -84,7 +83,7 @@ class PmeFreeSwitchTest(IgniteTest):
         single_key_tx_streamer = IgniteApplicationService(
             self.test_context,
             java_class_name="org.apache.ignite.internal.ducktest.tests.pme_free_switch_test.SingleKeyTxStreamerApplication",
-            properties=self.properties(client_mode="true"),
+            properties=self.properties(),
             params="test-cache,1000",
             version=ignite_version)
 
diff --git a/modules/ducktests/tests/ignitetest/tests/spark_integration_test.py b/modules/ducktests/tests/ignitetest/tests/spark_integration_test.py
index 45c0a38..7e2b4fc 100644
--- a/modules/ducktests/tests/ignitetest/tests/spark_integration_test.py
+++ b/modules/ducktests/tests/ignitetest/tests/spark_integration_test.py
@@ -13,11 +13,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from ducktape.mark import parametrize
+
 from ignitetest.services.ignite import IgniteService
 from ignitetest.services.ignite_app import IgniteApplicationService
 from ignitetest.services.ignite_spark_app import SparkIgniteApplicationService
 from ignitetest.services.spark import SparkService
 from ignitetest.tests.utils.ignite_test import IgniteTest
+from ignitetest.tests.utils.version import DEV_BRANCH
 
 
 class SparkIntegrationTest(IgniteTest):
@@ -28,33 +31,33 @@ class SparkIntegrationTest(IgniteTest):
     3. Checks results of client application.
     """
 
-    @staticmethod
-    def properties(client_mode="false"):
-        return """
-            <property name="clientMode" value="{client_mode}"/>
-        """.format(client_mode=client_mode)
-
     def __init__(self, test_context):
         super(SparkIntegrationTest, self).__init__(test_context=test_context)
-        self.spark = SparkService(test_context, num_nodes=2)
-        self.ignite = IgniteService(test_context, num_nodes=1)
+        self.spark = None
+        self.ignite = None
 
     def setUp(self):
-        self.spark.start()
-        self.ignite.start()
+        pass
 
     def teardown(self):
         self.spark.stop()
         self.ignite.stop()
 
-    def test_spark_client(self):
+    @parametrize(version=str(DEV_BRANCH))
+    def test_spark_client(self, version):
+        self.spark = SparkService(self.test_context, version=version, num_nodes=2)
+        self.spark.start()
+
+        self.ignite = IgniteService(self.test_context, version=version, num_nodes=1)
+        self.ignite.start()
+
         self.stage("Starting sample data generator")
 
         IgniteApplicationService(
             self.test_context,
             java_class_name="org.apache.ignite.internal.ducktest.tests.spark_integration_test.SampleDataStreamerApplication",
             params="cache,1000",
-            properties=self.properties(client_mode="true")).run()
+            version=version).run()
 
         self.stage("Starting Spark application")
 
@@ -62,4 +65,5 @@ class SparkIntegrationTest(IgniteTest):
             self.test_context,
             "org.apache.ignite.internal.ducktest.tests.spark_integration_test.SparkApplication",
             params="spark://" + self.spark.nodes[0].account.hostname + ":7077",
+            version=version,
             timeout_sec=120).run()
diff --git a/modules/ducktests/tests/ignitetest/tests/utils/version.py b/modules/ducktests/tests/ignitetest/tests/utils/version.py
index 9ba5c8e..2f1f003 100644
--- a/modules/ducktests/tests/ignitetest/tests/utils/version.py
+++ b/modules/ducktests/tests/ignitetest/tests/utils/version.py
@@ -15,6 +15,8 @@
 
 
 from distutils.version import LooseVersion
+from ducktape.cluster.cluster import ClusterNode
+
 from ignitetest import __version__
 
 
@@ -56,14 +58,16 @@ 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
+    if isinstance(node, ClusterNode) and hasattr(node, 'version'):
+        return getattr(node, 'version')
+
+    if isinstance(node, str) or isinstance(node, unicode):
+        return node
+
+    return DEV_BRANCH
 
 
 DEV_BRANCH = IgniteVersion("dev")
-DEV_VERSION = IgniteVersion("2.9.0-SNAPSHOT")
 
 # 2.7.x versions
 V_2_7_6 = IgniteVersion("2.7.6")